Remediate six confirmed security issues: deployment-only config keys, bounded provider responses, short-lived registration sessions, private init file mode, FedWiki workflow authorization, and switch preview gates. - Add DeploymentOnly config key declaration; refuse runtime overrides for keys that decide where secrets are sent - Create httplimit package; bound all provider response reads at 8 MiB - Set fifteen-minute deadline on /register sessions - Write mc-config.yaml with 0600 permissions - Derive FedWiki workflow IDs from site IDs; re-authorize sites before mutating activities - Apply switch authorization gates to the proration preview
278 lines
13 KiB
Bash
278 lines
13 KiB
Bash
# Sourced by run-audit.sh and run-ux.sh: everything that differs between the
|
|
# coding agents a lane can run. A MODELS entry is "<harness>:<model>"; an
|
|
# entry with no prefix is an opencode id, so the original spelling still works.
|
|
#
|
|
# opencode:<provider>/<model> ids from `opencode models`
|
|
# codex:<slug> slugs the Codex CLI lists in its model picker
|
|
# (ChatGPT plan or API key, `codex login`)
|
|
#
|
|
# A credential enters a lane container through harness_mount and nowhere
|
|
# else, so each container carries exactly the one file its harness needs:
|
|
# opencode the host's auth.json, read-only.
|
|
# codex a copy of the host's ~/.codex/auth.json inside a throwaway
|
|
# CODEX_HOME, writable because codex rewrites that file when it
|
|
# refreshes the ChatGPT token. The copy is deleted with the lane;
|
|
# a refreshed token is copied back first, since the refresh may
|
|
# have rotated the host's token out of validity.
|
|
#
|
|
# VARIANT is the reasoning effort in each harness's own vocabulary. opencode
|
|
# passes it as --variant. codex passes it as model_reasoning_effort; the set
|
|
# is per model (low, medium, high, xhigh, max, ultra), and ultra delegates to
|
|
# subagents inside the same container.
|
|
#
|
|
# A plan's usage window can run out mid-task. When a harness reports that,
|
|
# the lane keeps its state, waits QUOTA_POLL seconds, and resumes the same
|
|
# session in a fresh container, repeating until the task finishes or
|
|
# QUOTA_WAIT seconds of waiting are spent. A rejected retry costs nothing.
|
|
# Detection is per harness; opencode has none yet, so its lanes fail as before.
|
|
|
|
HARNESSES="opencode codex"
|
|
QUOTA_POLL="${QUOTA_POLL:-900}"
|
|
QUOTA_WAIT="${QUOTA_WAIT:-21600}"
|
|
OPENCODE_AUTH="${AUDIT_AUTH_JSON:-$HOME/.local/share/opencode/auth.json}"
|
|
CODEX_AUTH="${AUDIT_CODEX_AUTH_JSON:-${CODEX_HOME:-$HOME/.codex}/auth.json}"
|
|
# The Codex Security plugin, as `codex plugin add codex-security@openai-curated-remote`
|
|
# leaves it on the host. A codex lane ships it in its throwaway CODEX_HOME;
|
|
# a task that names it (the codex-security/ shape) runs with it on.
|
|
CODEX_SECURITY_HOST="${AUDIT_CODEX_SECURITY_DIR:-$(dirname "$CODEX_AUTH")/plugins/cache/openai-curated-remote/codex-security}"
|
|
CODEX_SECURITY_LANE=/root/.codex/plugins/cache/openai-curated-remote/codex-security
|
|
HARNESS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# docker compose against this runner from any cwd. An array, not only a
|
|
# function, because `timeout` runs a program and cannot run a function.
|
|
HARNESS_COMPOSE=(docker compose -f "$HARNESS_DIR/compose.yaml" --project-directory "$HARNESS_DIR")
|
|
harness_compose() { "${HARNESS_COMPOSE[@]}" "$@"; }
|
|
|
|
# harness_of MODEL: the harness a MODELS entry names; opencode when it names none.
|
|
harness_of() {
|
|
local h="${1%%:*}"
|
|
if [[ "$1" == *:* && " $HARNESSES " == *" $h "* ]]; then echo "$h"; else echo opencode; fi
|
|
}
|
|
|
|
# model_of MODEL: the entry with a recognised harness prefix removed.
|
|
model_of() {
|
|
local h; h="$(harness_of "$1")"
|
|
if [[ "$1" == "$h:"* ]]; then echo "${1#"$h:"}"; else echo "$1"; fi
|
|
}
|
|
|
|
# safe_name MODEL: a directory name for the lane.
|
|
safe_name() { local s="${1//\//__}"; echo "${s//:/__}"; }
|
|
|
|
# harness_auth_path HARNESS: the host file that harness signs in with.
|
|
harness_auth_path() {
|
|
case "$1" in opencode) echo "$OPENCODE_AUTH";; codex) echo "$CODEX_AUTH";; esac
|
|
}
|
|
|
|
# harness_check_auth MODEL...: fail before any container starts if a named
|
|
# model's harness has no credential on the host.
|
|
harness_check_auth() {
|
|
local m h f rc=0
|
|
for m in "$@"; do
|
|
h="$(harness_of "$m")"; f="$(harness_auth_path "$h")"
|
|
if [[ ! -f "$f" ]]; then
|
|
echo "ERROR: no $h credential at $f (needed for $m)." >&2
|
|
case "$h" in
|
|
opencode) echo " Sign in on the host with 'opencode auth login' or set AUDIT_AUTH_JSON." >&2;;
|
|
codex) echo " Sign in on the host with 'codex login' or set AUDIT_CODEX_AUTH_JSON." >&2;;
|
|
esac
|
|
rc=1
|
|
fi
|
|
done
|
|
return $rc
|
|
}
|
|
|
|
# harness_mount HARNESS STATE_DIR: prints, one per line, the docker arguments
|
|
# that hand a lane container its credential. STATE_DIR is an absolute path the
|
|
# lane owns; codex state is created under it.
|
|
harness_mount() {
|
|
local h="$1" state="$2"
|
|
case "$h" in
|
|
opencode)
|
|
printf -- '-v\n%s:/root/.local/share/opencode/auth.json:ro\n' "$OPENCODE_AUTH";;
|
|
codex)
|
|
mkdir -p "$state/codex-home"
|
|
install -m 600 "$CODEX_AUTH" "$state/codex-home/auth.json"
|
|
# Remember which host token the lane started from, so release can tell
|
|
# a lane-side refresh from a host-side re-login.
|
|
jq -r '.last_refresh // ""' "$CODEX_AUTH" >"$state/host-last-refresh" 2>/dev/null || : >"$state/host-last-refresh"
|
|
codex_security_ship "$state/codex-home"
|
|
printf -- '-v\n%s:/root/.codex\n' "$state/codex-home";;
|
|
esac
|
|
}
|
|
|
|
# codex_security_ship CODEX_HOME_DIR: copy the plugin from the host into the
|
|
# lane's home and write the config that turns it on. codex only reads that
|
|
# config when a task drops --ignore-user-config (harness_command's PLUGINS
|
|
# argument), so slices and open briefs never see the plugin. The plugin's
|
|
# MCP server is registered directly from its .mcp.json, because codex does
|
|
# not start a plugin's server from the cache alone; the paths are the
|
|
# container's, since the config is read there.
|
|
codex_security_ship() {
|
|
local home="$1" ver
|
|
[[ -d "$CODEX_SECURITY_HOST" ]] || return 0
|
|
ver="$(ls "$CODEX_SECURITY_HOST" | grep -E '^[0-9]' | sort -V | tail -1)"
|
|
[[ -n "$ver" ]] || return 0
|
|
mkdir -p "$home/plugins/cache/openai-curated-remote"
|
|
cp -r "$CODEX_SECURITY_HOST" "$home/plugins/cache/openai-curated-remote/"
|
|
python3 - "$CODEX_SECURITY_HOST/$ver/.mcp.json" "$CODEX_SECURITY_LANE/$ver" "$home/config.toml" <<'EOF'
|
|
import json, sys
|
|
mcp_json, lane_dir, out = sys.argv[1:4]
|
|
m = json.load(open(mcp_json))["mcpServers"]["codex-security"]
|
|
lines = [
|
|
'[plugins."codex-security@openai-curated-remote"]', 'enabled = true', '',
|
|
'[mcp_servers.codex-security]',
|
|
f'command = "{lane_dir}/scripts/launch_codex_security_mcp"',
|
|
'args = ["--stdio"]',
|
|
f'cwd = "{lane_dir}"',
|
|
f'startup_timeout_sec = {m["startup_timeout_sec"]}',
|
|
f'tool_timeout_sec = {m["tool_timeout_sec"]}',
|
|
'env_vars = ' + json.dumps(m["env_vars"]),
|
|
]
|
|
open(out, "w").write("\n".join(lines) + "\n")
|
|
EOF
|
|
echo "$ver" >"$home/codex-security-version"
|
|
}
|
|
|
|
# harness_release HARNESS STATE_DIR: after the lane, hand back what the
|
|
# container changed and remove the state.
|
|
harness_release() {
|
|
local h="$1" state="$2"
|
|
if [[ "$h" == codex ]]; then
|
|
# The container runs as root and fills CODEX_HOME (system skills, caches,
|
|
# sqlite state, possibly a rewritten auth.json) with root-owned files, so
|
|
# the state comes back to the invoking user through a container first.
|
|
harness_compose run --rm --no-deps -T -v "$state:/state" auditor \
|
|
chown -R "$(id -u):$(id -g)" /state >/dev/null 2>&1 || true
|
|
if [[ -f "$state/codex-home/auth.json" ]]; then
|
|
# The lane's token goes back to the host only when the lane refreshed
|
|
# it (strictly newer than what it started from) AND the host still
|
|
# holds the token the lane started from. A host that re-logged in
|
|
# meanwhile owns the newer token; overwriting it would hand the host a
|
|
# revoked one (which is how the sol lane of 2026-09-21 broke the host
|
|
# login). Timestamps are RFC 3339 UTC, so string order is time order.
|
|
local started now lane
|
|
started="$(cat "$state/host-last-refresh" 2>/dev/null || true)"
|
|
now="$(jq -r '.last_refresh // ""' "$CODEX_AUTH" 2>/dev/null || true)"
|
|
lane="$(jq -r '.last_refresh // ""' "$state/codex-home/auth.json" 2>/dev/null || true)"
|
|
if [[ -n "$lane" && "$lane" > "$started" ]]; then
|
|
if [[ "$now" == "$started" ]]; then
|
|
install -m 600 "$state/codex-home/auth.json" "$CODEX_AUTH"
|
|
echo " codex refreshed its ChatGPT token in the lane; the host copy at $CODEX_AUTH now carries it"
|
|
else
|
|
echo " codex refreshed its token in the lane, but the host signed in again meanwhile; host left alone, lane copy kept at $state/codex-home/auth.json"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
rm -rf "$state"
|
|
rmdir --ignore-fail-on-non-empty "$(dirname "$state")" 2>/dev/null || true
|
|
}
|
|
|
|
# harness_command HARNESS MODEL VARIANT PROMPT_SH [PLUGINS]: prints the
|
|
# command a lane runs in the container with `bash -c`. PROMPT_SH is a shell
|
|
# snippet, run in the container, that prints the prompt. Both agents print
|
|
# their final answer on stdout and their progress on stderr, so a lane
|
|
# captures stdout as the artifact and stderr as the log. PLUGINS, when
|
|
# non-empty, runs the task with the harness's shipped plugins on (codex only).
|
|
harness_command() {
|
|
local h="$1" model="$2" variant="$3" prompt_sh="$4" plugins="${5:-}" vflag=""
|
|
case "$h" in
|
|
opencode)
|
|
[[ -n "$variant" ]] && vflag="--variant '$variant'"
|
|
# stdin closed: the headless quirk.
|
|
echo "opencode run --model '$model' $vflag \"\$($prompt_sh)\" </dev/null";;
|
|
codex)
|
|
# The prompt arrives on stdin (-). The copy has no .git, hence
|
|
# --skip-git-repo-check. project_doc_max_bytes=0 keeps the repo's
|
|
# AGENTS.md, which addresses the harnesses that develop it, out of the
|
|
# auditor's context. The session is recorded (in the lane's throwaway
|
|
# CODEX_HOME) so a usage-limit stop can be resumed. The container is the
|
|
# sandbox (internal network, disposable copy), which is the case the
|
|
# bypass flag is documented for; codex's own Landlock sandbox is not
|
|
# relied on inside it.
|
|
echo "{ $prompt_sh; } | codex exec $(codex_flags "$model" "$variant" "$plugins") -";;
|
|
esac
|
|
}
|
|
|
|
codex_flags() {
|
|
local model="$1" variant="$2" plugins="${3:-}" vflag="" cfg="--ignore-user-config"
|
|
[[ -n "$variant" ]] && vflag="-c model_reasoning_effort='\"$variant\"'"
|
|
# The lane's config.toml holds nothing but the shipped plugins, so reading
|
|
# it is what turns them on.
|
|
[[ -n "$plugins" ]] && cfg=""
|
|
echo "--model '$model' $vflag -c project_doc_max_bytes=0 --skip-git-repo-check" \
|
|
"$cfg --dangerously-bypass-approvals-and-sandbox"
|
|
}
|
|
|
|
# harness_quota_hit HARNESS ERR: true when the attempt stopped because the
|
|
# plan's usage window is spent (or the provider rate-limited it), which a
|
|
# later retry can get past. Anything else is a failure to report. Only the
|
|
# harness's own ERROR lines are read: the model's transcript and report also
|
|
# talk about rate limits, as findings.
|
|
harness_quota_hit() {
|
|
local h="$1" err="$2"
|
|
case "$h" in
|
|
codex) grep -E '^[^a-z]*ERROR' "$err" 2>/dev/null | grep -qiE 'usage.?limit|rate.?limit|spend cap|try again';;
|
|
*) return 1;;
|
|
esac
|
|
}
|
|
|
|
# harness_resume_command HARNESS MODEL VARIANT [PLUGINS]: the command that
|
|
# continues the lane's most recent session with the same flags.
|
|
harness_resume_command() {
|
|
local h="$1" model="$2" variant="$3" plugins="${4:-}"
|
|
case "$h" in
|
|
codex)
|
|
echo "codex exec resume --last $(codex_flags "$model" "$variant" "$plugins")" \
|
|
"'A usage limit interrupted you. Continue from where you stopped and finish the report in the required shape.'";;
|
|
esac
|
|
}
|
|
|
|
# harness_run_task HARNESS MODEL VARIANT PROMPT_SH OUT ERR LANE_LOG [ENV_SH]
|
|
# [PLUGINS]: runs one task to completion. Every attempt is a fresh --rm
|
|
# container; stdout appends to OUT (the artifact), stderr to ERR (the log).
|
|
# The caller puts the lane's docker arguments (credential mount, extra
|
|
# volumes) in the LANE_DOCKER array and sets TIMEOUT, the budget of a single
|
|
# attempt. ENV_SH is a shell snippet run in the container before the agent
|
|
# (exports a task needs); PLUGINS turns the shipped plugins on. Returns the
|
|
# last exit code.
|
|
harness_run_task() {
|
|
local h="$1" model="$2" variant="$3" prompt_sh="$4" out="$5" err="$6" lane_log="$7" env_sh="${8:-}" plugins="${9:-}"
|
|
local cmd attempt=1 rc deadline=$(( $(date +%s) + QUOTA_WAIT ))
|
|
cmd="$(harness_command "$h" "$model" "$variant" "$prompt_sh" "$plugins")"
|
|
: >"$out"; : >"$err"
|
|
while :; do
|
|
echo "== attempt $attempt, $(date -Is)" >>"$err"
|
|
if timeout "$TIMEOUT" "${HARNESS_COMPOSE[@]}" run --rm --no-deps -T "${LANE_DOCKER[@]}" auditor \
|
|
bash -c "${env_sh:+$env_sh; }$cmd" >>"$out" 2>>"$err"; then rc=0; else rc=$?; fi
|
|
if grep -q 'Automatically switched' "$err"; then
|
|
echo " note: the harness switched model mid-run (usage limits); see $err" | tee -a "$lane_log"
|
|
fi
|
|
if [[ $rc -eq 0 ]]; then return 0; fi
|
|
if ! harness_quota_hit "$h" "$err"; then
|
|
# Not a quota stop: put the harness's own ERROR lines in the lane log so
|
|
# a SUSPECT verdict explains itself (a policy refusal, an auth failure).
|
|
grep -E '^ERROR:' "$err" | sort -u | sed 's/^/ /' >>"$lane_log" || true
|
|
return $rc
|
|
fi
|
|
if (( $(date +%s) + QUOTA_POLL > deadline )); then
|
|
echo " quota: wait budget (QUOTA_WAIT=${QUOTA_WAIT}s) spent after $attempt attempt(s)" | tee -a "$lane_log"
|
|
return $rc
|
|
fi
|
|
echo " quota: usage limit on attempt $attempt at $(date +%H:%M:%S); resuming in ${QUOTA_POLL}s" | tee -a "$lane_log"
|
|
sleep "$QUOTA_POLL"
|
|
cmd="$(harness_resume_command "$h" "$model" "$variant" "$plugins")"
|
|
attempt=$((attempt + 1))
|
|
done
|
|
}
|
|
|
|
# harness_own_output DIR: files an agent wrote under the findings mount belong
|
|
# to root; hand the lane's directory back to the invoking user. DIR is a host
|
|
# path under OUT.
|
|
harness_own_output() {
|
|
local dir="$1" rel="${1#"$OUT"/}"
|
|
[[ -d "$dir" && "$rel" != "$1" ]] || return 0
|
|
harness_compose run --rm --no-deps -T auditor chown -R "$(id -u):$(id -g)" "/out/$rel" >/dev/null 2>&1 || true
|
|
}
|