Skip to content

feat(agent): add PermissionModePlan for interactive read-only planning - #853

Open
euxaristia wants to merge 20 commits into
Gitlawb:mainfrom
euxaristia:feat/agent-plan-mode
Open

feat(agent): add PermissionModePlan for interactive read-only planning#853
euxaristia wants to merge 20 commits into
Gitlawb:mainfrom
euxaristia:feat/agent-plan-mode

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds PermissionModePlan, a read-only agent mode for interactive planning: mutating tools, local commands, executable hooks, and request_permissions are all denied while it's active, including the deferred tool_search path so spoofed/gated tools can't leak through
  • Wired into TUI, CLI (--plan, rejected in combination with --worktree), and ACP entry points, with permission mode propagated through exec options and serialized across ACP mode changes
  • Layers a plan-mode system prompt on top of the existing prompt rather than replacing it, and enforces read-only mode on subagents spawned during planning

Test plan

  • go test ./internal/agent/... ./internal/cli/... ./internal/tools/...

Summary by CodeRabbit

  • New Features
    • Added read-only Plan mode through --plan, --permission-mode plan, and the /plan command.
    • Plan mode supports status, activation, and deactivation while restoring the previous permission mode.
    • Added shell completion and help documentation for planning options.
  • Safety Improvements
    • Plan mode exposes only safe planning and read-only tools, blocking file changes, shell commands, and restricted operations.
    • Deferred tools and integrations are filtered consistently to prevent unavailable or unsafe actions.
  • Bug Fixes
    • Permission-mode changes are now synchronized during active sessions.
    • Plan mode is preserved correctly when cycling through permission modes.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bcefd3f1-208f-4f03-bbec-9c4f7718ba66

📥 Commits

Reviewing files that changed from the base of the PR and between 792599d and 5f4eea9.

📒 Files selected for processing (9)
  • internal/acp/agent_test.go
  • internal/agent/deferred_loop_test.go
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/cli/exec_plan_test.go
  • internal/tools/tool_search.go
  • internal/tools/tool_search_test.go
  • internal/tui/plan_command.go
  • internal/tui/plan_mode_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • internal/tui/plan_command.go
  • internal/acp/agent_test.go
  • internal/agent/deferred_loop_test.go
  • internal/cli/exec_plan_test.go
  • internal/agent/loop_test.go
  • internal/tools/tool_search_test.go
  • internal/agent/loop.go
  • internal/tui/plan_mode_test.go

Walkthrough

Changes

Plan mode enforcement

Layer / File(s) Summary
Agent plan-mode policy
internal/agent/types.go, internal/agent/system_prompt.go, internal/agent/loop.go, internal/agent/*_test.go
Adds read-only plan mode, restricts tool advertisement and execution, filters permission requests, suppresses advisory hooks, and adds planning instructions.
Permission-aware tool search
internal/tools/tool_search.go, internal/tools/tool_search_test.go
Applies permission-mode visibility to deferred and eager tool discovery and validates control-tool safety metadata.
CLI and specialist propagation
internal/cli/*, internal/specialist/*
Adds plan and explicit permission-mode parsing, validation, help, completions, MCP handling, and specialist argument forwarding.
TUI plan-mode lifecycle
internal/tui/*
Adds `/plan status
ACP mode selection
internal/acp/agent.go, internal/acp/agent_test.go
Accepts and advertises plan mode, serializes mode changes with the turn mutex, and forwards the mode to agent runs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Gitlawb/zero#642: Implements overlapping PermissionModePlan behavior across the same agent, CLI, ACP, TUI, specialist, and tool-search areas.
  • Gitlawb/zero#829: Also changes permission-mode handling, plan-mode tools, and specialist execution forwarding.
  • Gitlawb/zero#854: Directly overlaps plan-mode enforcement and agent permission-mode tests.

Suggested reviewers: gnanam1990, vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding PermissionModePlan for interactive read-only planning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

euxaristia and others added 18 commits July 31, 2026 15:54
…tests

string(permissionMode) already yields "plan" / "spec-draft", so the
if/else recomputing modeName in the denial message was dead branching
on the same values. Also add plan-mode coverage mirroring three of the
four existing spec-draft regression tests: advertised tool set, and
denied write_file/bash calls. The fourth (submit-and-stop review
control) has no plan-mode analog, since plan mode has no submit tool.
… registry omits it

request_permissions is dispatched by name in executeToolCall before the
registry-based ToolAdvertised gate runs, so that gate only helps when
the tool happens to be present in the caller's registry. A plan- or
spec-draft-mode registry that simply omits the tool (rather than
registering it as denied) let the call fall through to a real
turn/session-scoped permission grant, defeating the read-only
boundary. Deny it unconditionally at the top of
executeRequestPermissions for both read-only modes instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ctive

Plan mode promises a read-only turn, but sessionStart/sessionEnd fire on
every run and beforeTool/afterTool fire around allowed read calls, and
all four execute configured host commands outside the advertised-tool
and sandbox gates — so a project hook could mutate the workspace or
spawn a process from a session that advertises it cannot. Gate all four
dispatch points on the run's permission mode, with a regression test
asserting no hook command launches during a plan-mode run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toolAdvertisedInPlan whitelisted ask_user and update_plan by name
alone, so a caller could register a mutating tool under either name
and have it advertised and executed in plan mode. Validate every tool
against its Safety() instead. Also exclude lsp_navigate, which is
marked SideEffectRead but lazily spawns a real language-server
process, contradicting plan mode's read-only guarantee. Add
regression tests for both.
Plan mode still suppresses executable hooks so a read-only planning turn
cannot spawn host processes via session or tool hooks. Spec-draft keeps
the existing trust model: project hooks fire when the workspace (or its
worktree trust root) is trusted. Unconditionally suppressing hooks in
spec-draft broke TestExecSpecWorktreeInheritsTrustEndToEnd for trusted
worktrees under --use-spec --worktree.
Expand the name-only spoof regression to both update_plan and ask_user,
and add an execution-path denial for lsp_navigate so plan mode cannot
spawn language servers even when a call still arrives.
… visibility

tool_search resolved and ranked deferred tools by EnabledTools/DisabledTools
only, never by the run's permission-mode visibility. tool_search itself is
already denied at dispatch in plan/spec-draft (its Safety carries no side
effect, so it fails the same SideEffect==Read advertisement gate direct calls
use), so this was not reachable through the normal Run() path today. But it
is a real landmine: if that outer gate is ever loosened independently (e.g.
tool_search's no-side-effect Safety is judged advertisable), the loader had
no gate of its own and would hand a deferred write/mutator tool's name,
description, and full schema straight to a plan/spec-draft model.

Mirror agent.ToolAdvertised's plan/spec-draft branches inside the tools
package (toolAdvertisedForPermissionMode, next to the existing
toolAllowedByFilters mirror that avoids the same import cycle) and apply it
alongside the operator filters in visibleDeferredTools and
visibleEagerToolNames.

Added unit tests in tool_search_test.go for both modes, and an end-to-end
agent test that force-calls tool_search in plan mode and asserts no schema
leaks. Verified by reverting tool_search.go and confirming the new tests fail
(one shows load_tools resolving to the mutator's name); restored and
confirmed they pass. Also confirmed via a temporary probe that if plan mode's
outer advertisement gate is loosened, this filter is what actually stops the
leak.
Do not advertise or load re-registered control tools by name alone in
spec-draft mode. ask_user must be SideEffectRead+Allow and submit_spec
must be SideEffectWrite+Allow, matching the real tools. Apply the same
filter in tool_search and add spoof regression tests.

Refs Gitlawb#642
Address the P1 finding that PermissionModePlan was documented but never
selected by /plan, zero exec, or ACP mode selectors. /plan on|off now
toggles the session permission mode (restoring the prior mode on off),
zero exec --plan selects plan for a run, and ACP advertises plan as a
client-selectable mode. Integration coverage for each entry path.
Worktree preparation runs in runExec before the plan permission mode is
assigned, so `zero exec --plan --worktree` could still trigger workspace
mutation ahead of the read-only gate. Reject the combination during
option validation, alongside the existing --use-spec/--skip-permissions-
unsafe conflict checks, so no worktree prep can occur.

Addresses a coderabbitai finding on PR Gitlawb#642.
The spoofed-control-tool regression only asserted on the description
string; Parameters() exposed no distinctive schema marker, so a
regression that leaked the schema without the description would still
have passed. Add a spoofed_secret property to the test tool's schema
and assert it's absent from result.Output alongside the description.

Addresses a coderabbitai finding on PR Gitlawb#642.
/plan on only flips the agent permission mode, which gates agent tool
calls. Local TUI commands that run entirely inside the TUI process
bypass that gate: /rewind restores workspace files from a checkpoint,
/export writes a transcript to disk, and /sandbox-setup spawns a
native host process. Add a shared plan-mode guard at the start of
dispatchCommand (mirroring the existing BTW-unavailable guard) that
rejects these three commands while permissionMode is
agent.PermissionModePlan, with regression coverage proving each is
blocked with no mutation/process spawn in plan mode and unaffected
outside it.

Addresses a coderabbitai finding on PR Gitlawb#642.
… model field and --plan permission mode conflict
…CP mode changes

Parse permissionMode in resolveExecPermissionMode, acquire turnMu.Lock in ACP handleSetMode to serialize mode changes with active turns, and block MCP subcommands in TUI plan mode.

Refs Gitlawb#642
…Tool/NewLSPNavigateTool wrappers

Those were thin unscoped wrappers around the Scoped variants, deleted
upstream in Gitlawb#706 since nothing else called them directly. Only these
tests still did; switch to the Scoped calls main's own tests already
use.
@euxaristia
euxaristia force-pushed the feat/agent-plan-mode branch from 2fb5cb5 to b88b4e3 Compare July 31, 2026 20:09

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice piece of work on the core gate I tried hard to break it and couldn't. Four things need fixing before this lands though, and one of them (the hook veto) is a real fail-open.

What I verified holds

The dispatch gate is load-bearing, not decorative. I mutated executeToolCall to drop || permissionMode == PermissionModePlan and five tests went red, including TestPlanModeRejectsNameOnlySpoofedControlTools/update_plan, where a read/allow-shaped spoof registered under a control-tool name actually executed ("spoofed write") — so the Safety-shape check on ask_user/update_plan/submit_spec is doing real work, not just tidying. Stubbing hooksSuppressed to return false and deleting the request_permissions early denial each turned their named test red too.

I also drove it end-to-end against a live model: zero exec --plan with a prompt explicitly instructing it to create a file by any means and to try tool_search and request_permissions. It tried update_plan, glob, list_directory, grep, skill, read_file, ask_user, reported no write path, and the workspace was unchanged. Plan mode's tool list from the built binary is exactly ask_user glob grep list_directory read_file read_minified_file skill update_plan. --use-spec still lists submit_spec, so the spec-draft Safety-shape tightening didn't break that flow.

Package tests: internal/agent, internal/tools, internal/acp, internal/specialist all pass. internal/cli fails only on TestBuildServeScopeKeepsLexicalPaths (Windows symlink privilege, unrelated), internal/tui only on TestGitSweepCmdAgainstRealRepo, which passes in isolation — also unrelated.

Blocking

1. Plan mode silently bypasses beforeTool hook vetoes — internal/agent/loop.go:1791

hooksSuppressed cuts all four dispatch sites, but dispatchBeforeTool isn't only a side-effect vector: a non-zero exit is a veto that blocks the tool. Suppressing it removes an operator's deny gate in the one mode where reading is the only thing the agent does.

I proved it with a throwaway test (deleted): a beforeTool hook matching read_file running a command that exits non-zero, over a workspace containing secret.txt.

  • Auto mode: Error: "read_file" was blocked by hook "zero.veto": go definitely-not-a-go-subcommand: unknown command — the veto works, so the setup is sound.
  • Same run with PermissionMode: PermissionModePlan: File: secret.txt (1 lines)\n\n1 | SUPERSECRET.

So a project whose beforeTool hook blocks reads of .env or a secrets directory loses that protection the moment someone types /plan on. This has to fail closed, not open. Simplest correct shape: keep suppressing execution, but if a beforeTool hook is configured whose matcher could match a tool in this run, deny the tool (or refuse to enter plan mode and say why) rather than running it unguarded. afterTool/sessionStart/sessionEnd are advisory and fine to suppress as-is.

2. --permission-mode propagation silently strips swarm members of their write tools — internal/specialist/exec.go:271 and :326

BuildArgs emits --auto <memberAwareAutonomy> and then unconditionally appends --permission-mode <parent mode>. But resolveExecPermissionMode (internal/cli/exec_tools.go:76) short-circuits on --permission-mode and never reads --auto, so the member rung is thrown away. With the built binary:

zero exec --auto member --list-tools
  apply_patch ask_user bash edit_file exec_command glob grep list_directory lsp_navigate
  read_file read_minified_file request_permissions skill update_plan web_fetch write_file write_stdin

zero exec --auto member --permission-mode auto --list-tools
  ask_user exec_command glob grep list_directory lsp_navigate read_file read_minified_file
  request_permissions skill update_plan web_fetch write_stdin

internal/swarm/launcher_specialist.go:56 sets MemberAutonomy: true, and a BuildArgs probe confirms parent=auto/member=true emits --auto member ... --permission-mode auto. So every swarm member spawned by a non-unsafe parent loses write_file, edit_file, apply_patch and bash — they can't build anything.

Worth calling out why this slipped through: TestBuildArgsMemberAutonomyEmitsMember asserts the --auto member pair is present in the args, not that the child resolves to MemberAuto. It stays green while the guarantee in its name is broken. Whatever the fix, please add a test that asserts the resolved mode.

Also, Task and the swarm spawn tool are both SideEffectShell, so neither is ever advertised in plan or spec-draft — which means this propagation buys plan mode nothing and only affects the modes it breaks. The narrow fix is to append --permission-mode only when the parent mode is plan or spec-draft.

3. Every --plan combination guard is bypassable via --permission-mode planinternal/cli/exec_parse.go:455-470

The four guards key off options.plan (the flag), not the resolved mode, and the new --permission-mode flag reaches PermissionModePlan directly. Your own comment on the worktree guard says worktree prep "runs before the plan permission mode is assigned, so it would happen even under --plan's read-only, no-side-effects promise" — and that's exactly what I got. In a scratch git repo:

$ git worktree list
.../wtrepo  d4c87ef [master]
$ zero exec --permission-mode plan -w planwt --list-tools
Tools visible to model:
  ask_user ... (the plan-mode read-only set)
$ git worktree list
.../wtrepo                                          d4c87ef [master]
C:/Users/.../zero/worktrees/zero-worktree-wtrepo-.../planwt  d4c87ef (detached HEAD)

zero exec --plan --worktree wtx correctly errors. Move the checks off the flag and onto the resolved mode (after resolveExecPermissionMode, before worktree prep at exec.go:204), or reject --permission-mode plan from the CLI surface entirely and keep --plan as the only door.

4. --permission-mode ask widens a headless child's tool set — same lines as (2)

An ask-mode parent now spawns --auto low --permission-mode ask. ToolAdvertised returns true for everything in Ask, and headless exec wires no OnPermissionRequest, so no prompt can ever be shown:

zero exec --auto low        --enabled-tools write_file,bash,read_file --list-tools  ->  read_file
zero exec --permission-mode ask --enabled-tools write_file,bash,read_file --list-tools  ->  bash read_file write_file

The registry still refuses prompt-permission tools without PermissionGranted (internal/tools/registry.go:204), so I did not prove execution — this is exposure, not a hole. But a specialist manifest declaring tools: [write_file, bash] under an ask parent is now one sandbox auto-allow away from running what the parent's own mode never advertised. Restricting the propagation to plan/spec-draft (see 2) fixes this too.

Smaller stuff

  • internal/acp/agent.go:385handleSetConfigOption changes the mode with no turnMu, while handleSetMode at :352 now takes it. Both paths are advertised (modeState and configOptions both list plan), so the lock covers one of two doors. Also note sess.currentMode() is read once per turn at :259, so a mid-turn flip never touched the running turn — if the lock isn't buying anything, dropping it avoids making session/set_mode block for the full duration of a long turn.
  • internal/cli/exec.go:593 — the unsafe warning misattributes the cause. zero exec --permission-mode unsafe -o stream-json --prompt "say hi" emits Unsafe permissions are active for this run because --auto high was passed. when neither flag was passed.
  • internal/cli/exec_tools.go:90 — the error says "Expected plan, spec-draft, auto, ask, or unsafe" but the switch also takes member, member-auto, member_auto, spec_draft, high.
  • --permission-mode isn't in --help (internal/cli/app.go:1361) or completions (internal/cli/completions.go:27) even though it outranks --auto. If it's meant to be internal plumbing, say so and consider hiding it; if it's public, document it.
  • /init in plan mode (internal/tui/init_command.go:17) launches a turn whose entire job is writing AGENTS.md, which plan mode then denies. Either add commandInit to planModeCommandUnavailable or have it say why.
  • lsp_navigate is excluded from plan for spawning a language server but is still listed under --use-spec. You call spec-draft deliberately unchanged, so this is just a note, not a request.

Happy to re-review as soon as 1-3 are addressed.


Same note as on #855: everything I found is above in one pass, including the smaller stuff, so there's no second round queued behind this. Two of these are classes your own #857 list names — a guard that fails open, and help text wider than what shipped.

…ards

Keep beforeTool deny gates active under plan mode so hooksSuppressed no longer
fails open, and stop propagating --permission-mode for auto/ask/member children
so swarm members keep write tools. Apply --plan combination rejects to
--permission-mode plan as well.

Refs Gitlawb#853
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed review:

  1. beforeTool fail-closed in plan mode — advisory hooks stay suppressed; dispatchBeforeTool still runs and vetoes on non-zero (TestPlanModeHonorsBeforeToolVeto).
  2. Swarm/specialist permission-mode — only propagate --permission-mode for plan/spec-draft; auto/ask/member/unsafe no longer strip child write tools via a second flag.
  3. --permission-mode plan shares --plan combination guards (use-spec, skip-permissions-unsafe, worktree).
  4. Headless ask no longer widens child tool set via propagated --permission-mode ask.

Also: ACP set-config lock, unsafe warning attribution, help/completions for --permission-mode, /init blocked in plan mode.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Aug 2, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Re-checked on 792599db, and you fixed the fail-open properly rather than taking the cheaper option I offered.

I'd said the hooksSuppressed comment claimed something the code didn't do, and that correcting the comment would be enough. You kept dispatchBeforeTool running and made it veto on non-zero instead — closing the gap rather than documenting it. That's the better call and I'm glad you made it.

TestPlanModeHonorsBeforeToolVeto is a genuinely well-built test. I neutralised the veto at loop.go:1331 and it fails with:

plan mode failed open: beforeTool veto was skipped and secret leaked:
"File: secret.txt (1 lines)\n\n1 | SUPERSECRET"

It asserts the consequence — the secret actually reaching the model — not merely that a hook was dispatched. A test that only checked the call would have passed on a veto whose return value was ignored, which is close to the bug being fixed.

Narrowing --permission-mode propagation to plan/spec-draft is right too: the previous blanket propagation was stripping child write tools via a second flag for auto/ask/member, which is the swarm-member regression I flagged.

internal/agent is green here. The one internal/cli failure is TestBuildServeScopeKeepsLexicalPaths, which needs symlink privilege and fails on origin/main on my box — not yours.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@euxaristia flagging a collision before this goes further, not a review of the code yet.

PermissionModeSpecDraft already exists on main and does much of what PermissionModePlan does here: read-only tool set, its own advertised-tool filter, and a review gate at the end via submit_spec. Landing this gives Zero two plan-shaped modes that differ mainly in whether the turn ends at a gate.

#664 asks to expose spec-draft over ACP and is assigned to @anandh8x, so that decision and this PR are pulling the same surface in two directions. Between you please settle whether these merge before either lands. My preference is one mode with a flag for whether it gates at the end, rather than two modes a client has to choose between, but I would rather hear your reasoning first since you have been in this code most recently.

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
internal/tui/plan_mode_test.go (1)

132-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression tests for the untested failure paths.

The block tests cover /rewind, /export, /sandbox-setup, and /init. Two guarded behaviors have no test:

  1. planModeCommandUnavailable also blocks commandSpec and commandMCP. Nothing pins that. A later edit to the switch would remove those blocks silently.
  2. handlePlanCommand refuses a mode change while m.pending is true, for both on and off. This is the failure path that protects an in-flight run from a mid-turn mode flip. It is untested.
💚 Proposed additional tests
func TestPlanModeBlocksSpecAndMCPCommands(t *testing.T) {
	for _, input := range []string{"/spec add review flow", "/mcp add foo"} {
		m := newModel(context.Background(), Options{Cwd: t.TempDir(), PermissionMode: agent.PermissionModePlan})
		updated, cmd := m.dispatchCommand(parseCommand(input))
		next := updated.(model)
		if cmd != nil {
			t.Fatalf("expected %q to be blocked synchronously in plan mode", input)
		}
		if !transcriptContains(next.transcript, "unavailable in plan mode") {
			t.Fatalf("expected plan-mode denial for %q, got %#v", input, next.transcript)
		}
	}
}

func TestPlanCommandRefusesModeChangeDuringActiveTurn(t *testing.T) {
	m := newModel(context.Background(), Options{PermissionMode: agent.PermissionModeAuto})
	m.pending = true

	next, text := m.handlePlanCommand("on")
	if next.permissionMode != agent.PermissionModeAuto {
		t.Fatalf("permissionMode = %s, want unchanged auto during an active turn", next.permissionMode)
	}
	if !strings.Contains(text, "while a turn is active") {
		t.Fatalf("expected active-turn refusal, got %q", text)
	}

	m.permissionMode = agent.PermissionModePlan
	next, text = m.handlePlanCommand("off")
	if next.permissionMode != agent.PermissionModePlan {
		t.Fatalf("permissionMode = %s, want plan retained during an active turn", next.permissionMode)
	}
	if !strings.Contains(text, "while a turn is active") {
		t.Fatalf("expected active-turn refusal, got %q", text)
	}
}

As per coding guidelines: "Every behavior or security-boundary change requires a regression test, including failure paths".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/plan_mode_test.go` around lines 132 - 256, Add regression tests
for the two untested plan-mode failure paths: verify dispatchCommand blocks both
commandSpec (/spec) and commandMCP (/mcp) synchronously with the existing
“unavailable in plan mode” message, and verify handlePlanCommand refuses both
“on” and “off” when pending is true, preserving the current permission mode and
returning the active-turn refusal text.

Source: Coding guidelines

internal/agent/loop.go (1)

3249-3256: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Prefer a Safety/capability marker over the lsp_navigate name literal.

toolAdvertisedInPlan excludes lsp_navigate by name. The rest of this PR deliberately stops trusting tool names, because Registry.Register keys only on Name(). The name check therefore protects only the exact string lsp_navigate. Any other tool that is classified SideEffectRead but starts a host process still passes the gate.

A durable rule keys on tool metadata instead. Options: add a SpawnsProcess (or similar) field to tools.Safety, or reuse ToolCapabilities.Effect so the plan gate rejects every process-spawning tool. This also removes the second copy of the same literal in internal/tools/tool_search.go.

Not a blocker for this PR: the current tool set has one such tool, and the name check covers it today.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/loop.go` around lines 3249 - 3256, Replace the lsp_navigate
name check in toolAdvertisedInPlan with a safety or capability-based marker that
identifies tools spawning host processes, and reject every such tool in plan
mode. Add or reuse the appropriate tools.Safety or ToolCapabilities metadata,
update lsp_navigate to set it, and remove the duplicated name-based exclusion in
tool_search.go.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/acp/agent_test.go`:
- Around line 396-400: The existing test only verifies Plan through
MethodSessionSetMode; extend the regression coverage to the configuration path
by calling MethodSessionSetConfigOption with Value set to
string(agent.PermissionModePlan), then assert the returned configuration options
include Plan. Keep the existing MethodSessionSetMode assertion intact and
exercise the advertised configuration contract handled by handleSetConfigOption.

In `@internal/cli/exec_plan_test.go`:
- Around line 105-128: Add a second run-level test case alongside the existing
--plan coverage that invokes runWithDeps with exec --permission-mode plan
--list-tools. Reuse the existing output assertions to verify tool listing
includes read_file and hides write_file, edit_file, apply_patch, and bash,
confirming this mode enforces the same visibility boundary without setting
options.plan.

In `@internal/tools/tool_search.go`:
- Around line 234-279: The read-only advertisement policy is duplicated and
already differs on PermissionDeny handling. In
internal/tools/tool_search.go:234-279, export the mode constants and
toolAdvertisedForPermissionMode as the shared tools implementation, including
the correct PermissionDeny behavior. In internal/agent/loop.go:3216-3259, remove
the local toolAdvertisedInPlan and toolAdvertisedInSpecDraft bodies and call the
shared predicate from ToolAdvertised, preserving the existing PermissionDeny
short-circuit before delegation.
- Around line 255-279: Update toolAdvertisedForPermissionMode to immediately
return false when tool.Safety().Permission is PermissionDeny, before evaluating
permissionMode branches; preserve the existing plan, spec-draft, and default
behavior for all other permission values.

In `@internal/tui/plan_command.go`:
- Around line 53-68: Update planModeCommandUnavailable to match actual behavior:
document every blocked command, including /spec and /mcp, and narrow commandMCP
blocking to only /mcp subcommands if dispatchCommand identifies bare /mcp
separately from mutating forms. Ensure the comment accurately describes the
resulting blocked operations and does not claim the read-only bare /mcp manager
view mutates the workspace or spawns a process.

---

Nitpick comments:
In `@internal/agent/loop.go`:
- Around line 3249-3256: Replace the lsp_navigate name check in
toolAdvertisedInPlan with a safety or capability-based marker that identifies
tools spawning host processes, and reject every such tool in plan mode. Add or
reuse the appropriate tools.Safety or ToolCapabilities metadata, update
lsp_navigate to set it, and remove the duplicated name-based exclusion in
tool_search.go.

In `@internal/tui/plan_mode_test.go`:
- Around line 132-256: Add regression tests for the two untested plan-mode
failure paths: verify dispatchCommand blocks both commandSpec (/spec) and
commandMCP (/mcp) synchronously with the existing “unavailable in plan mode”
message, and verify handlePlanCommand refuses both “on” and “off” when pending
is true, preserving the current permission mode and returning the active-turn
refusal text.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a930d90-f9ea-4aca-982d-d6aec964265c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and 792599d.

📒 Files selected for processing (25)
  • internal/acp/agent.go
  • internal/acp/agent_test.go
  • internal/agent/deferred_loop_test.go
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/agent/system_prompt.go
  • internal/agent/types.go
  • internal/cli/app.go
  • internal/cli/completions.go
  • internal/cli/exec.go
  • internal/cli/exec_parse.go
  • internal/cli/exec_plan_test.go
  • internal/cli/exec_tools.go
  • internal/specialist/exec.go
  • internal/specialist/exec_test.go
  • internal/tools/tool_search.go
  • internal/tools/tool_search_test.go
  • internal/tui/commands.go
  • internal/tui/commands_test.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/plan_command.go
  • internal/tui/plan_mode_test.go
  • internal/tui/theme.go
  • internal/tui/view.go

Comment thread internal/acp/agent_test.go
Comment thread internal/cli/exec_plan_test.go
Comment thread internal/tools/tool_search.go Outdated
Comment thread internal/tools/tool_search.go Outdated
Comment thread internal/tui/plan_command.go
…nd entry paths

Unify plan/spec-draft tool advertisement in tools.ToolAdvertisedForPermissionMode
(with PermissionDeny short-circuit), cover ACP config and --permission-mode plan
list-tools paths, and allow bare /mcp while blocking mutating MCP subcommands.

Refs Gitlawb#853
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed open CodeRabbit findings on 5f4eea9:

  • ACP config-based Plan mode coverage in TestACPSetModeUpdatesSession
  • --permission-mode plan --list-tools run-level coverage
  • Shared tools.ToolAdvertisedForPermissionMode (PermissionDeny short-circuit; agent delegates for plan/spec-draft)
  • plan_command comment accuracy; bare /mcp allowed, MCP subcommands blocked

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

@euxaristia have exceeded the limit for the number of chat messages per hour. Please wait 20 minutes and 55 seconds before sending another message.

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.

2 participants