feat(agent): add PermissionModePlan for interactive read-only planning - #853
feat(agent): add PermissionModePlan for interactive read-only planning#853euxaristia wants to merge 20 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (8)
WalkthroughChangesPlan mode enforcement
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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.
…-only subagent mode
… 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.
2fb5cb5 to
b88b4e3
Compare
There was a problem hiding this comment.
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 plan — internal/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:385—handleSetConfigOptionchanges the mode with noturnMu, whilehandleSetModeat:352now takes it. Both paths are advertised (modeStateandconfigOptionsboth list plan), so the lock covers one of two doors. Also notesess.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 makingsession/set_modeblock 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"emitsUnsafe 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 takesmember,member-auto,member_auto,spec_draft,high.--permission-modeisn'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./initin plan mode (internal/tui/init_command.go:17) launches a turn whose entire job is writing AGENTS.md, which plan mode then denies. Either addcommandInittoplanModeCommandUnavailableor have it say why.lsp_navigateis 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
|
Addressed review:
Also: ACP set-config lock, unsafe warning attribution, help/completions for |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
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.
|
@euxaristia flagging a collision before this goes further, not a review of the code yet.
#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. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
internal/tui/plan_mode_test.go (1)
132-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd regression tests for the untested failure paths.
The block tests cover
/rewind,/export,/sandbox-setup, and/init. Two guarded behaviors have no test:
planModeCommandUnavailablealso blockscommandSpecandcommandMCP. Nothing pins that. A later edit to the switch would remove those blocks silently.handlePlanCommandrefuses a mode change whilem.pendingis true, for bothonandoff. 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 liftPrefer a Safety/capability marker over the
lsp_navigatename literal.
toolAdvertisedInPlanexcludeslsp_navigateby name. The rest of this PR deliberately stops trusting tool names, becauseRegistry.Registerkeys only onName(). The name check therefore protects only the exact stringlsp_navigate. Any other tool that is classifiedSideEffectReadbut starts a host process still passes the gate.A durable rule keys on tool metadata instead. Options: add a
SpawnsProcess(or similar) field totools.Safety, or reuseToolCapabilities.Effectso the plan gate rejects every process-spawning tool. This also removes the second copy of the same literal ininternal/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
📒 Files selected for processing (25)
internal/acp/agent.gointernal/acp/agent_test.gointernal/agent/deferred_loop_test.gointernal/agent/loop.gointernal/agent/loop_test.gointernal/agent/system_prompt.gointernal/agent/types.gointernal/cli/app.gointernal/cli/completions.gointernal/cli/exec.gointernal/cli/exec_parse.gointernal/cli/exec_plan_test.gointernal/cli/exec_tools.gointernal/specialist/exec.gointernal/specialist/exec_test.gointernal/tools/tool_search.gointernal/tools/tool_search_test.gointernal/tui/commands.gointernal/tui/commands_test.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/plan_command.gointernal/tui/plan_mode_test.gointernal/tui/theme.gointernal/tui/view.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
|
Addressed open CodeRabbit findings on 5f4eea9:
@coderabbitai full review |
Rate Limit Exceeded
|
Summary
PermissionModePlan, a read-only agent mode for interactive planning: mutating tools, local commands, executable hooks, andrequest_permissionsare all denied while it's active, including the deferredtool_searchpath so spoofed/gated tools can't leak through--plan, rejected in combination with--worktree), and ACP entry points, with permission mode propagated through exec options and serialized across ACP mode changesTest plan
go test ./internal/agent/... ./internal/cli/... ./internal/tools/...Summary by CodeRabbit
--plan,--permission-mode plan, and the/plancommand.