ReviewOS

also looking at this

stacks/buddy

fix(agent): stop handing an agent text a stranger wrote as its instructions

#1414
Merged glennmichael123 wants to merge feat/plan-mode into main
9 files +425 -5
9 days ago

Started as the planMode audit item. Closing it turned up something worse.

The audit item

planMode was written, exported and listed in AGENT_MODES — and nothing ever constructed it. Meanwhile @buddy plan was a command the parser recognised and docs/features/pr-conversations.md advertised, with no handler behind it. So it answered:

I don't know how to plan yet. Try @buddy help.

…and the help table didn't list it either.

What that turned up

buddy handle-issue built its agent task like this:

task: `${touch.buildTask({ summary: issue.title })}\n\nIssue:\n${issue.body}`

Both halves are written by whoever opened the issue — on a public repository, anyone at all.

The runner's own contract says this must not happen. AgentRunOptions.task is documented as "written by buddy — never by a third party", and wrapUntrusted exists because untrusted content "never reaches the system prompt or the task; it arrives only as tool output".

And a ticked "build" box routes to the autofix touch, whose mode is implementMode:

tiers: ['read', 'write', 'shell', 'git', 'comment']

So a stranger's issue body was reaching the trusted half of the context of an agent holding a shell.

The fix

Both paths now read the issue through read_issue — a read-tier tool returning title and body with untrusted: true, the flag the runner already wraps its marker around.

The tool is built by a factory bound to one issue number rather than registered globally. A plan needs the thing it's planning, and a tool that could fetch any issue would widen what a comment from a stranger can send the agent off to read.

runPlan (src/agent/plan.ts) sits alongside runReviewForPR and runFixCi, and builds its task from the issue number alone — nothing the issue's author wrote can appear in it. It runs in planMode, whose tier list is the guarantee rather than the instruction:

read_file ✓   read_issue ✓   write_file ✗   run_command ✗

handle-issue calls it too, which also makes a documented claim true for the first time — the quick-link path ran planTouch, whose mode is reviewMode, not plan mode.

@buddy plan is now in the help table it was missing from.

Left deliberately, for your call

  • planTouch still uses reviewMode for the buddy touch path. Different surface — maintainer-invoked on a PR, with a summary Buddy itself wrote — so lower exposure, but it is inconsistent.
  • restrictedMode is still unreachable. It isn't a missing wire: the review path builds prompts with ai.complete rather than the agent runtime, so there's no tier list to cap, and checkPermission already refuses non-collaborators everything except help and summary. It reads like a control that's in force and isn't — I'd lean toward deleting it, but that's a security-shaped abstraction Chris authored and I didn't want to remove it on a guess.

Tests

2010 → 2025, including the boundary itself: the planning task provably carries no issue text, and the tool's output is flagged untrusted.

Lint ✓ · typecheck ✓ · check:docs ✓ · 2025 pass / 0 fail

🤖 Generated with Claude Code

https://claude.ai/code/session_01UWouahBJT3msK7V2VDYY6T

9 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.