# 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`](../../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. A token the lane refreshed is copied back to the host
only if the host still holds the token the lane started from; a host that
signed in again meanwhile is left alone. Never `codex logout` while a lane
runs: the revocation reaches the lane's copy and ends it. SSH keys, forge
tokens, `test/secrets/`, the host opencode config and the rest of the host
`~/.codex` (sessions, history, config) are never mounted; the Codex Security
plugin files are the one other thing copied in, when present.
- **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=
./run-tools.sh # offline scanner pass -> $OUT/tools/
OUT= ./run-audit.sh # every model x every scope task -> $OUT//
./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.
A task is a slice or an open brief; the prompt is built from these files:
| File | What it is |
|---|---|
| `AUDITOR.md` | How a slice is worked: read it whole, answer its questions, then hunt |
| `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 |
| `open/.md` | An open brief: the whole repository, no map, no questions; `bare` says only what to do, `stakes` adds what the system is and who attacks it |
| `codex-security/scan.md` | Hands method and report to OpenAI's Codex Security plugin (codex harness only); `REPORT.md` stays out |
| `REPORT.md` | The evidence every finding must carry, the rating axes, the output shape; ends every slice and open prompt |
A slice prompt is `AUDITOR.md`, `CONTEXT.md`, the slice, `REPORT.md`. An open
prompt is the brief and `REPORT.md`. A Codex Security prompt is the task file
and the scan directory's path. With no task named, every slice runs.
The Codex Security task runs the plugin's `security-scan` skill (one standard
pass) with its MCP workbench, and the plugin writes its own artifacts,
`scan-manifest.json`, `findings.json`, `coverage.json` and a generated
`report.md`, into `/codex-security__scan/` under the run directory; the
task's `.md` holds the model's closing summary. The plugin must be installed
on the host (`codex plugin add codex-security@openai-curated-remote`); a codex
lane copies it from `~/.codex/plugins/cache/` into its throwaway `CODEX_HOME`
and registers its MCP server there, and only this task shape reads that
config. The image carries the Node runtime the server needs.
Subsets and knobs:
```
OUT= MODELS="deepseek/deepseek-v4-pro" ./run-audit.sh 02-operator-boundary
OUT= TIMEOUT=3600 VARIANT=high ./run-audit.sh
OUT= MODELS="codex:gpt-6-astra" VARIANT=ultra TIMEOUT=7200 ./run-audit.sh
OUT= MODELS="codex:gpt-6-astra" VARIANT=ultra TIMEOUT=14400 ./run-audit.sh open/bare
OUT= MODELS="codex:gpt-5.6-sol" VARIANT=max TIMEOUT=14400 ./run-audit.sh codex-security/scan
```
`TIMEOUT` bounds one attempt. When a codex attempt stops on the plan's usage
limit, the lane keeps the session, waits `QUOTA_POLL` seconds (900), resumes it
in a fresh container, and repeats until the task finishes or `QUOTA_WAIT`
seconds of waiting (21600) are spent; a retry the plan still rejects costs
nothing. The lane log records each resume, and a note when codex switched
model mid-run. opencode lanes have no such detection yet.
`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 # one lane per model -> /out//
```
The brief is yours; the runner carries no design content. `` 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=` appends a file to the prompt;
`INCLUDE=" ..."` copies files into the disposable tree and names them for
the model to read. `OUT=` 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.