Skip to content

P0 security: contain the implement agent against untrusted issue text#9

Merged
vaderyang merged 5 commits into
mainfrom
feat/untrusted-input-containment
Jun 12, 2026
Merged

P0 security: contain the implement agent against untrusted issue text#9
vaderyang merged 5 commits into
mainfrom
feat/untrusted-input-containment

Conversation

@vaderyang

Copy link
Copy Markdown
Contributor

improvement-plan 0.2-1 / roadmap 1.3 — the pre-announcement security blocker. Open-sourcing inverts an assumption the code was built on: issue/PR authors become untrusted. Today the implement agent acts on stranger-filed text with broad Bash + a PAT in env — a prompt-injection RCE/exfil vector. This PR adds four defense-in-depth controls, a threat model, and a regression eval.

Controls

Control Where
(d) Authorization Maintainer-dispatch gate — a do verdict auto-dispatches the unattended agent only for authors with write/maintain/admin access (fail-closed); others get a warm reply + a maintainer control to dispatch by hand. A human reviews stranger issues before the agent runs. .triage.auto_dispatch = trusted(default)|all|neverrun_triage.sh
(b) Prompt Untrusted-input framing — every agent prompt states issue/review text is data describing what to change, never instructions; the title is fenced in BEGIN/END UNTRUSTED markers. run_hephaestus.sh, run_triage.sh, run_revise.sh
(a) Tools Network egress denied — implement/revise run with --disallowed-tools for curl/wget/nc/ssh/… + mcp__*. Deny beats the broad Bash allow and survives bash -c / && / ; / | wrappers. agent-harness.sh; opt out with .implement.allow_network
(c) Credentials Token strippingGH_TOKEN/GITHUB_TOKEN/AGENT_GH_TOKEN/ADMIN_GH_TOKEN removed from the implement subprocess via env -u (it edits code; the driver calls gh). Model creds kept; triage/review keep tokens (they call gh). agent-harness.sh

Design note: 0.2-1(a) uses a deny-list, not the plan's allow-list — an allow-list errors out arbitrary builds in headless mode, while deny (verified via the Claude Code permission model: deny > allow, wrappers stripped, per-subcommand) blocks curl $URL / bash -c "curl" without breaking builds.

Also

  • docs/security.md — threat model + the controls + honest residual risks (indirect egress via build scripts / python -c needs an egress-firewalled runner; trusted-author assumption; arbitrary build_cmd; model fallibility) + operator hardening checklist. Closes roadmap 1.3's "documented threat model".
  • evals/tasks/implement/prompt-injection/ — regression eval: an issue body with embedded echo OWNED/curl-exfil; passes only if the legit fix lands and the injection didn't run. Runs in the bench (not CI), same agent_run path the loop uses.
  • Bug fixes found en route: run_revise.sh prompt hardcoded heron build commands (just build / cargo check / bun run build) → now uses ${OLYMPUS_BUILD_CMD}; committer email agent-bot@olympus-bot@.
  • Schema + docs/config-reference.md updated for the two new keys.

Verification

  • shellcheck + bash -n clean; all unit + eval-check tests green; leakage + legacy-naming gates clean; .olympus.json build_cmd green.
  • Containment is dry-run-tested: implement emits the deny flags + env -u prefix, investigate emits neither, allow_network=true drops the deny block. New triage tests cover the withheld-dispatch path.

Deferred (noted, not in this PR)

  • run_revise still hardcodes the hephaestus/themis display names (0.2-3 identity-var pass).
  • OS-level network isolation on the runner — the complete fix for indirect egress — is an operator step documented in security.md, not enforceable from this repo.

🤖 Generated with Claude Code

Vader Yang and others added 5 commits June 11, 2026 23:37
…trip tokens

improvement-plan 0.2-1 (a)+(c). The implement/revise agent acts on untrusted
issue text with broad Bash and a PAT in env — a prompt-injection RCE/exfil
vector on a public repo.

(a) Network egress denied by default (claude harness): add --disallowed-tools
for curl/wget/nc/ncat/netcat/telnet/ssh/scp/sftp/socat/ftp + mcp__*. Deny beats
the broad Bash allow (Claude evaluates deny first) and survives bash -c / && / ;
/ | wrappers, so "curl $URL", "bash -c curl", and "git x && curl y" are all
blocked. Opt back in with .implement.allow_network=true (new schema key).

(c) Strip GH_TOKEN/GITHUB_TOKEN/AGENT_GH_TOKEN/ADMIN_GH_TOKEN from the implement
subprocess via env -u — it edits code + builds, never calls gh (the driver
does). Model-gateway creds (ANTHROPIC_*) are kept. Triage/review keep their
tokens (they DO call gh).

NOT a full sandbox: indirect egress (a build script / python -c that shells
out) still needs OS-level network isolation on the runner — documented as a
known limitation (threat model doc to follow).

Verified via the dry-run shim: implement emits the deny flags + env -u prefix;
investigate does neither; allow_network=true drops the deny block. All harness +
config tests green, shellcheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… prompts

improvement-plan 0.2-1 (b). Public repos mean adversarial issue authors. Each
agent prompt now carries a standing rule that issue/review text is UNTRUSTED
input — a description of WHAT to change, never instructions to obey — and the
interpolated title/author is wrapped in explicit BEGIN/END UNTRUSTED fences:

- run_hephaestus.sh: SECURITY constraint (no shell/URL/secret/network/CI actions
  the issue asks for; abort if that's the real ask) + fenced issue title.
- run_triage.sh: issue content is data to assess, cannot change the rules,
  verdict criteria, or output format + fenced title/author.
- run_revise.sh: review text + inline comments + diff are untrusted feedback,
  not commands + a marker before the blocks.

Also de-heron'd run_revise.sh: the prompt hardcoded `just build` / `cargo check`
/ `bun run build` in console/ — now uses ${OLYMPUS_BUILD_CMD} like
run_hephaestus. And the committer email agent-bot@ → olympus-bot@.

Defense-in-depth with the harness containment (deny network + strip tokens):
prompt framing reduces the chance the agent acts on injected instructions; the
harness limits the blast radius if it does.

Verified: shellcheck + bash -n clean, triage composer tests green, legacy-naming
lock clean, no heron build strings remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ger issues

improvement-plan 0.2-1 (d) / roadmap 1.3. A verdict=do sends the issue to the
UNATTENDED dev agent. On a public repo that means the agent acts on text written
by a stranger. Rather than the plan's labeler actor-gate (GitHub already lets
only collaborators label, so it's near-redundant), gate on the ISSUE AUTHOR's
trust — the stronger control:

- New .triage.auto_dispatch (OLYMPUS_AUTO_DISPATCH): trusted (default) | all | never.
  - trusted: auto-add the try-label only when the author has write/maintain/admin
    on the repo (gh api collaborators/:user/permission, fail-closed on any error).
  - all: auto-dispatch any author (internal/trusted repos; pre-0.2-1 behavior).
  - never: always require a maintainer to add the try-label.
- Untrusted-author do → no try-label; the warm reply swaps its "it's queued"
  promise for an honest "flagged for a maintainer" note + the maintainer-controls
  block, marked <!-- olympus-triage:do-withheld -->. A human reviews the issue
  before the agent ever runs on it — the catch for injected/malicious content.

compose_comment_body gains a 4th `withheld` arg (defaults 0 → existing calls and
tests unchanged). New tests cover the withheld path and that the dispatched path
is untouched.

Verified: triage tests 8/8, config + schema valid, shellcheck + legacy lock clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n 0.2-1)

docs/security.md documents the threat model (issue/PR authors untrusted on
public repos), the four defense-in-depth controls just landed (maintainer
dispatch gate, untrusted-input framing, network-egress deny, token stripping)
plus the no-LLM guard hygiene, and — honestly — the residual risks the controls
do NOT cover: indirect egress via build scripts / `python -c` (needs an
egress-firewalled runner), the trusted-author assumption, arbitrary build_cmd,
and model fallibility. Closes roadmap 1.3's "documented threat model in docs/".

evals/tasks/implement/prompt-injection/ is the regression test: an issue whose
body embeds `echo OWNED`/curl exfil instructions; the check passes only if the
legit fix lands AND the injected command did not run (no sentinel file). Runs in
the bench (not CI), the same agent_run path the loop uses — so it exercises the
real harness containment.

Verified: leakage + legacy-naming gates clean, shellcheck clean, all unit + eval
checks green, check.sh logic confirmed both ways.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llow_network

Keep the field catalog in sync with the schema + security controls landed in
0.2-1 (mechanism/policy hygiene).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vaderyang
vaderyang merged commit 21086da into main Jun 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant