feat(review): enforce reviewer read-only via tool allowlist (#260)#278
feat(review): enforce reviewer read-only via tool allowlist (#260)#278neversettle17-101 wants to merge 7 commits into
Conversation
The reviewer's read-only guarantee was prompt-only. Add AllowedTools/ DisallowedTools to ports.LaunchConfig and plumb them through the claude-code adapter to --allowedTools/--disallowedTools. Launch the reviewer off bypassPermissions (which ignores allow/deny rules) with an allowlist scoped to Read/Grep/Glob and the few Bash commands a reviewer needs (gh, git diff/log/show/status, ao review submit), and an explicit deny list for Edit/Write/NotebookEdit/git push/git commit. Resolve the review.md write flow without granting Write: ao review submit gains --body-text for inline Markdown and the prompt records the verdict inline instead of writing a file, so no write tool is granted at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
…251) Drives the real useWorkspaceQuery + real SessionsBoard / PullRequestsPage for an ordinary project (from /api/v1/projects) whose session has an open PR, mocking only the HTTP client and router. Confirms PR facts fetched from /sessions/{id}/pr flow through the shared workspace cache into both the Board card ("PR #278 · open") and the PR page row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…279) * fix(frontend): populate session.pullRequest from /pr endpoint (#251) WorkspaceSession.pullRequest was declared but never populated, so every Boolean(session.pullRequest) check was dead: the Summary tab gated its PR fetch on it (never ran), and the Board card and /prs page read it directly (always empty). Hydrate the field centrally in fetchWorkspaces — the single place that builds session objects for the workspace, board, PR page, and sidebar — by fetching GET /sessions/{sessionId}/pr per non-terminated session in parallel and attaching {number, state}. A per-session fetch error degrades to "no PR" rather than failing the whole workspace query; terminated sessions are skipped. GET /sessions/{sessionId}/pr stays the single source of truth, so no new backend endpoint and no changes to the consuming components. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: format with prettier [skip ci] * test(frontend): verify PR hydration end-to-end for a normal project (#251) Drives the real useWorkspaceQuery + real SessionsBoard / PullRequestsPage for an ordinary project (from /api/v1/projects) whose session has an open PR, mocking only the HTTP client and router. Confirms PR facts fetched from /sessions/{id}/pr flow through the shared workspace cache into both the Board card ("PR #278 · open") and the PR page row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(frontend): move PR hydration integration test into __tests__/integration Cross-component integration test belongs in a dedicated folder, separate from the co-located unit tests. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
neversettle17-101
left a comment
There was a problem hiding this comment.
This closes the gap from #260 (allow/deny lists are wired through and the review.md write is avoided), but the central guarantee — "launch off bypassPermissions" — isn't actually enforced, so the fix can silently no-op back to the exact vulnerability #260 describes.
Core issue: ReviewCommand leaves Permissions as the zero value and doesn't set Config at all. In GetLaunchCommand, an empty Permissions (and empty Config.Permissions) normalizes to PermissionModeDefault, which emits no --permission-mode flag — Claude's TUI then resolves the starting mode from the user's own ~/.claude/settings.json defaultMode. If that ambient default is bypassPermissions (plausible for AO operators who run agents autonomously), the reviewer launches in bypass mode, and per the adapter's own doc comment, "bypassPermissions ignores both lists" — so the allowlist/denylist do nothing and the reviewer is back to prompt-only enforcement, with no visible signal that the sandbox didn't take effect.
The new test (TestReviewCommandLaunchesReadOnlyOffBypass) doesn't catch this: it only asserts agent.got.Permissions != PermissionModeBypassPermissions, which is trivially true for the empty string, but doesn't verify the resolved launch is non-bypass.
To actually guarantee non-bypass (matching what #260 asked for: "the reviewer must launch in a non-bypass mode"), ReviewCommand should pass an explicit non-bypass mode (e.g. ports.PermissionModeAcceptEdits or ports.PermissionModeAuto) rather than relying on whatever defaultMode happens to be configured on the host. That makes the read-only guarantee independent of ambient user config, rather than a hope.
Everything else — the tool-flag plumbing, comma-joining to avoid splitting Bash(git diff:*)-style rules, the --body-text CLI flow, and the frontend reviewer-pane wiring — looks correct and well-tested.
| // Launch off bypassPermissions so the allow/deny lists are enforced: | ||
| // the reviewer's read-only guarantee is the sandbox, not just the | ||
| // prompt. Permissions is left unset (default mode) on purpose. | ||
| AllowedTools: reviewerAllowedTools, |
There was a problem hiding this comment.
Permissions is left as the zero value here, which (per GetLaunchCommand) emits no --permission-mode flag and defers to the user's ~/.claude/settings.json defaultMode. If that default is bypassPermissions, this allow/deny list is silently ignored — the exact failure mode #260 wanted closed. Pass an explicit non-bypass mode (e.g. ports.PermissionModeAcceptEdits) instead of relying on ambient config.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
neversettle17-101
left a comment
There was a problem hiding this comment.
This review is an approval. (Posted as a COMMENT-event review, not APPROVE, because GitHub does not allow the PR author to approve their own PR.)
This addresses the finding from my previous review (PR #278 at commit 39906b3) directly and correctly.
Fix verified: ReviewCommand now sets Permissions: ports.PermissionModeAuto explicitly, instead of leaving it unset. Per appendPermissionFlags, this emits --permission-mode auto, which guarantees the launch is non-bypass regardless of what the host's ~/.claude/settings.json defaultMode happens to be — closing the gap where an ambient bypassPermissions default would have silently disabled the allow/deny enforcement.
The test was updated in lockstep: TestReviewCommandLaunchesReadOnlyOffBypass now asserts agent.got.Permissions == ports.PermissionModeAuto instead of the previous != bypassPermissions check, which is the right strengthening (the old assertion was trivially satisfied by the empty string that caused the bug).
go build ./... and the reviewer/agent adapter test suites pass. No further findings — approving.
# Conflicts: # backend/internal/adapters/reviewer/claudecode/claudecode.go # backend/internal/review/prompt.go
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
neversettle17-101
left a comment
There was a problem hiding this comment.
This review is an approval. (Posted as a COMMENT-event review, not APPROVE, because GitHub does not allow the PR author to approve their own PR.)
This head (6372208) is a merge of origin/main into the PR branch, with conflicts in backend/internal/adapters/reviewer/claudecode/claudecode.go and backend/internal/review/prompt.go (both touched by upstream #259, "reviewer posts to GitHub and records its verdict autonomously").
Conflict resolution verified correct:
claudecode.go: the merge kept this PR's fix —Permissions: ports.PermissionModeAutoplus the allow/deny tool lists — rather than reverting to #259'sbypassPermissionslaunch. The non-bypass sandbox guarantee from my earlier review is intact.prompt.go: the merge correctly combined #259's "post to GitHub first, then record with AO" step ordering with this PR's "no review.md write, use--body-text" flow. Step numbering (1/2/3) and the "only if step 2 fails, still run step 3" fallback line are consistent and don't reference removed steps.- No leftover conflict markers (
grepfor<<<<<<</=======/>>>>>>>acrossbackend/is clean).
Diffing this PR's own files between the previously-reviewed head (283f3e3) and this one shows no new changes from this PR's own work — the rest of the diff (frontend/src/api/schema.ts, pr-hydration.test.tsx, useWorkspaceQuery.*) comes from the merged-in upstream commits (#251/#279, #230), not from this PR.
go build ./..., go vet ./..., gofmt -l ., and the full go test ./... backend suite all pass clean.
No further findings — approving.
Fixes #260
Summary
The reviewer's read-only guarantee was enforced only by the prompt. This makes it an enforced sandbox by launching the reviewer off
bypassPermissions(which skips the permission system and ignores allow/deny rules) with a scoped tool allowlist.ports.LaunchConfiggainsAllowedTools []string/DisallowedTools []string. Empty = unrestricted, so worker sessions are unaffected.--allowedTools/--disallowedTools. Each list is comma-joined into one value so a rule containing spaces (e.g.Bash(git diff:*)) is not split into separate tool names.Read,Grep,Glob,Bash(gh:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git status:*),Bash(ao review submit:*)Edit,Write,NotebookEdit,Bash(git push:*),Bash(git commit:*)review.mdwrite flow resolved without grantingWrite:ao review submitgains--body-textfor inline Markdown (--body <path>stays for humans; passing both is a usage error), and the prompt records the verdict inline instead of writing a file. No write tool is granted at all — a true read-only guarantee.Test
go build ./..., fullgo test ./...,gofmt,go vet— all pass.claudebinary needed), CLI--body-textinline + mutual-exclusion error.Notes
cat,rg,ls) would stall. This is the tuning tradeoff the issue calls out; worth an end-to-end review run to confirm the allowlist is permissive enough.🤖 Generated with Claude Code