feat: agentic read-only code review -- give the reviewer its own worktree + read/grep tools #241
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor#241
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The code review runs as a one-shot
claude_call(tick.sh~L2440): the modelgets the PR diff as a TEXT blob and nothing else -- no worktree, no Read/Grep. So
it can't verify any claim that spans beyond the diff (leftover refs after a
rename, a caller it can't see, a constant defined elsewhere) and honestly punts
those to the human as COMMENT-instead-of-APPROVE. A human reviewer doesn't defer
that legwork to the dev -- they grep the codebase themselves. The reviewer should
too. (Deferring verification to the AUTHOR was considered and REJECTED -- that's
self-review, the exact boundary the binding flow protects.)
Make the review agentic + read-only: its own checkout of the PR's head + read/
grep tools, so it verifies whole-tree facts itself. Independence is preserved
(non-author
AGENT_MODEL_REVIEWtier, fresh context, adversarial directive); itjust gains a human reviewer's verification power.
The change (in
do_review_tick)claude_call->claude_run_with_cost-- the agentic primitiveissue-work already uses (
tick.sh~L3466). Reuse that invocation shape:cdinto the worktree, thenclaude_run_with_cost "review" "$log" "$timeout" --model "$AGENT_MODEL_REVIEW" --settings <review-profile> --append-system-prompt "$directive" --max-turns N --print "$user_msg".into a worktree off
origin/<pr-head>(fetched fresh), so the reviewer canRead/Grep the tree AT THE PR'S STATE. (Keep the dedup cheap -- no worktree for
skipped reviews.) Remove the worktree on every exit path (trap, like the
maintenance worktree).
part. The reviewer must be STRUCTURALLY unable to mutate what it reviews:
(
git log/show/diff/status,grep,find,cat/ls/head/tail/wc).git add/commit/push,anything that writes.
agent-review-settings.json-- NOT a mutation ofagent-settings.json(operator-coordinated). It's strictly MORE restrictivethan the worker profile (read-only), so it tightens, never loosens.
.agent/REVIEW.mdwith the existingcontract (a
VERDICT:line +===BODY===+ the markdown review). The harnessreads + parses it with the existing
review_parse_response. (An agentic rundoesn't hand back a clean string like
claude_calldoes -- a file is the robusthand-off, same pattern as
PR_BODY.md.) Normalize dashes on it like the other.agent/outputs.bin/lib/review-directive.md: tell the reviewer it has the PR's treechecked out READ-ONLY and should Grep/Read to confirm anything it can't verify
from the diff alone (leftover refs, callers, definitions elsewhere) BEFORE
settling on a can't-confirm COMMENT. It still primarily reviews the DIFF; the
tree is for verification, not a re-review of the whole repo.
Keep UNCHANGED
rework loop + 3-round cap). This ticket changes how the verdict is PRODUCED,
not what happens to it.
AGENT_MODEL_REVIEW(independence stays).Watch out for (safety-critical -- I will scrutinize these hardest on review)
enforcement. Backstop: after the run, assert the worktree has NO new commits and
NO dirty tracked files; if it does, DISCARD the verdict + log loudly (the
reviewer broke read-only -- a bug, never trusted).
mutated by its reviewer.
completion, on every un-reviewed bot-PR head. Bound it: a review timeout and a
sane
--max-turns. (Operator has accepted the cost: verification > spend.)agent-review-settings.jsonis a permission artifact -- get the allow-listexactly right; the operator reviews it on the PR.
Definition of done
whole tree (e.g. on a rename PR it confirms "no leftover refs" itself and reaches
APPROVE instead of punting).
.agent/REVIEW.md, parsedunchanged; the binding routing is untouched.
worktree is provably unmutated).
bash -n bin/tick.shclean;bash bin/check-sync.shpasses.The harness security review flagged a material issue in this change, so it was NOT pushed:
I reviewed this diff, which converts the PR reviewer from a pure text/diff call into an agentic reviewer that executes Bash and reads the filesystem while processing attacker-controlled PR content, then emits output (
.agent/REVIEW.md) that is posted as the public PR review.Findings:
agent-review-settings.jsongrantsReadandBash(cat:*)(plusfind,grep,ls,head,tail) to an agent that now runs inside a worktree checked out at the untrusted PR head (do_review_tickinbin/tick.sh).~/.ssh,~/.local/state/agent,~/.config/agent,/etc/shadow,/etc/sudoers— but not common credential locations that the agent can freely read, e.g.:cat ~/.config/gh/hosts.yml(GitHub CLI OAuth token — only~/.config/agentis denied, not~/.config/gh)cat ~/.netrc,cat ~/.aws/credentials,cat ~/.gitconfigcat /proc/self/environ(leaks the parent process environment, which for this tool plausibly includesANTHROPIC_API_KEY/ GitHub token).agent/REVIEW.md, which is published./proc, making token/credential leakage realistically exploitable.The worktree-mutation guard and the read-only checkout are reasonable, and the trap/path interpolation (repo names limited to safe chars) do not appear injectable. The material risk is the incomplete read denylist enabling credential exfiltration through the published review.
Recommendation: switch file access to an allowlist scoped to the worktree (or explicitly deny
~/.config/gh,~/.netrc,~/.aws,~/.gitconfig,/proc), and ensure the reviewer subprocess runs with a scrubbed environment.Address it, then remove
Status/Blockedto re-queue. (If the note above says the gate could not complete, that's a transient error -- just re-queue.)Closing per the security gate's finding + the board call.
Giving the review pass its own tools creates the lethal trifecta: untrusted PR content (the checked-out head) + read access (Read/
cat/grep) + a published output channel (.agent/REVIEW.mdis posted as the PR review). Confirmed exploitable —claude_run_with_coststrips onlyANTHROPIC_API_KEY, soFORGEJO_TOKEN(+ SMTP2GO/GSC/marketstack) stay in the env, and a prompt-injected reviewer couldcat /proc/self/environstraight into the public review. A denylist can't close this (inherently incomplete).Decision: keep the pure, diff-only reviewer (no tools, no exfil surface). Its honest "can't verify beyond the diff" is closed cheaply by the human-side review, which already has full tools. If autonomous whole-tree verification is ever wanted, the safe path is harness-mediated deterministic checks (reviewer emits grep requests; the harness runs them) — not an agent with read + a publish channel. Not worth building now.
Nixed. The security gate did its job catching this.