Files
cgalo5758 bea0ffc024 Add Codex CLI harness to agent runner
Support Codex CLI as a second agent harness alongside opencode, with
per-harness credential mounting, model routing, and reasoning effort
handling.
2026-09-21 02:42:53 -05:00

6.7 KiB

Agent runner

Runs a model from another family (DeepSeek, Gemini, GLM/Z.AI, Kimi, OpenAI) over a disposable copy of this repository, inside a container whose only route off-box is an allowlist proxy. Two lanes share that container: a security lane (run-audit.sh), which audits a named slice of the code, and an ideation lane (run-ux.sh), which answers a design brief with a proposal and a static mockup. Each lane runs a fresh container per model, so no run inherits another run's context.

Two coding agents ("harnesses") drive the models: opencode, for providers it lists, and Codex CLI, for OpenAI models under a ChatGPT plan or API key. A MODELS entry names its harness as a prefix, codex:gpt-6-astra; an entry with no prefix, deepseek/deepseek-v4-pro, is an opencode id. harness.sh holds what differs between them.

The method these lanes serve, and the rules a finding has to clear before it counts, are in docs/agent-runner.md.

What it touches, and what it does not

  • Working tree: never mounted. The model sees git archive HEAD unpacked into .code/ beside these scripts, which teardown.sh throws away. Edits it makes go nowhere.
  • Network: the model container sits on a Docker network marked internal, which has no route off-box. Its only exit is a tinyproxy sidecar that denies every host except the patterns in allowlist.txt. This is fail-closed: a misconfigured proxy costs the run its network rather than leaking the code. In practice the proxy refuses semgrep's telemetry and opencode's registry fetches while allowing the model API. Read the denials with docker compose exec egress-proxy cat /tmp/tinyproxy.log.
  • Credentials: one secret enters a lane container, the sign-in file of the harness that lane runs; the scanner pass gets none. An opencode lane mounts ~/.local/share/opencode/auth.json read-only (AUDIT_AUTH_JSON to point elsewhere). A codex lane mounts a throwaway CODEX_HOME holding a copy of ~/.codex/auth.json (AUDIT_CODEX_AUTH_JSON), writable because codex rewrites that file when it refreshes the ChatGPT token; the copy is removed when the lane ends, after a refreshed token is copied back to the host file. SSH keys, forge tokens, test/secrets/, the host opencode config and the rest of the host ~/.codex (sessions, history, config) are never mounted.
  • Live stack: not involved. There is no database, no identity provider, no webhook surface. Both lanes read code.
  • Findings: land in the directory you name with OUT. Keep it outside this one: this directory is tracked and a run's output is not.

Security lane

./prepare.sh                          # once: export HEAD, build images, start the proxy
OUT=<dir> ./run-tools.sh              # offline scanner pass -> $OUT/tools/
OUT=<dir> ./run-audit.sh              # every model x every scope task -> $OUT/<timestamp>/
./teardown.sh                         # remove the containers, networks, images and the copy

The scanner pass runs first and offline: gosec, govulncheck, semgrep, staticcheck and gitleaks write machine-readable output that task 08 gives the model to triage. run-audit.sh runs it itself if $OUT/tools/ is missing.

Inputs are the three files the prompt is built from:

File What it is
AUDITOR.md How the model works, the evidence every finding must carry, the output shape
CONTEXT.md The auth model, request pipeline and trust boundaries, supplied as fact
scope/NN-*.md One slice each: the files, a one-sentence threat model, falsifiable questions

Subsets and knobs:

OUT=<dir> MODELS="deepseek/deepseek-v4-pro" ./run-audit.sh 02-operator-boundary
OUT=<dir> TIMEOUT=3600 VARIANT=high ./run-audit.sh
OUT=<dir> MODELS="codex:gpt-6-astra" VARIANT=ultra TIMEOUT=7200 ./run-audit.sh

VARIANT is reasoning effort in the harness's own vocabulary and defaults to max; set it empty to omit the flag. opencode passes it as --variant. codex passes it as model_reasoning_effort, whose levels are per model (low, medium, high, xhigh, max, ultra); ultra delegates to subagents inside the same container and needs a TIMEOUT to match. Model ids drift, so confirm them with opencode models or the codex model picker and pass MODELS= when the defaults in run-audit.sh are stale. To add an opencode provider, authenticate it on the host with opencode auth login, add its API host to allowlist.txt, and name it in MODELS. Codex signs in once on the host with codex login; its hosts are already listed.

Codex runs with its own sandbox and approvals off (--dangerously-bypass-approvals-and-sandbox), which is the mode its documentation reserves for an externally sandboxed environment; here that is the container. The repo's AGENTS.md does not reach it (project_doc_max_bytes=0): it addresses the harnesses that develop this code, not an auditor.

Ideation lane

./prepare.sh                          # once, same as above
./run-ux.sh <brief-dir>               # one lane per model -> <brief-dir>/out/<timestamp>/

The brief is yours; the runner carries no design content. <brief-dir> holds four files, each overridable by the variable of the same name: DESIGNER.md (how the model works and what it must produce), CONTEXT.md (the surface, its constraints, what survives any redesign), SCREENS.md (what exists today) and brief.md (the deliverable). EXTRA=<file> appends a file to the prompt; INCLUDE="<file> ..." copies files into the disposable tree and names them for the model to read. OUT=<dir> moves the output elsewhere.

Each lane writes proposal.md, proposal.err and mockup.html; _static/ holds the stylesheets the mockups link, so a mockup opens in a browser.

Teardown

./teardown.sh removes the containers, both networks, the two built images and the disposable copy, then prints a residue check. --deep also drops the base images and prunes the build cache; --verify-only removes nothing and just reports. Findings are untouched, because they never live here.

Every model container runs with --rm, so no agent session or snapshot data survives a run. The opencode auth.json is read-only, so nothing is written back to it; a codex lane's CODEX_HOME copy lives under .run/ and is removed when the lane ends.

First build

prepare.sh is slow the first time. Everything the run needs from the network is fetched at build time, where the Docker daemon still has normal egress: the Go toolchain, opencode, Codex CLI, the five scanners, the semgrep rule corpus, the Go vulnerability database, and this project's module cache. At run time the scanners are fully offline. Codex CLI is pinned in the Dockerfile (CODEX_VERSION) to the version signed in on the host; bump it when the host upgrades.