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
162 lines
8.9 KiB
Markdown
162 lines
8.9 KiB
Markdown
# 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=<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.
|
|
|
|
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/<name>.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 `<lane>/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=<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
|
|
OUT=<dir> MODELS="codex:gpt-6-astra" VARIANT=ultra TIMEOUT=14400 ./run-audit.sh open/bare
|
|
OUT=<dir> 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 <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.
|