feat(tui): full-auto permission mode, and classify local dev servers separately - #883
Vasanthdev2004 wants to merge 18 commits into
Conversation
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe change makes ChangesFull-auto permission mode
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant TUIModel
participant TUIView
User->>TUIModel: press Shift+Tab
TUIModel->>TUIView: show full-auto confirmation offer
User->>TUIModel: press Ctrl+G immediately
TUIModel->>TUIView: display committed full-auto mode
Suggested reviewers: Merge Risk: 🟡 Moderate · up to Framework builds may access the network without approval, and full-auto launches can ignore an explicitly selected theme. These regressions should be addressed before merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/acp/agent.go (1)
358-362: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse
full-autoin user-visible diagnostics and comments.The changed branches now use the canonical mode, but they still expose the deprecated
unsafename.
internal/acp/agent.go#L358-L362: describefull-autoas the canonical mode and--skip-permissions-unsafeas its deprecated alias.internal/agent/loop.go#L1488-L1491: returnfull-auto permission mode permits unsandboxed retry.internal/agent/loop.go#L1542-L1544: return the equivalent full-auto wording for network retry.internal/cli/app.go#L291-L292: update branch comments and generic errors to use canonical terminology.internal/cli/app.go#L334-L334: direct one-shot users tozero exec --full-auto, not the deprecated spelling.As per coding guidelines, help text and comments must match shipped behavior.
🤖 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/acp/agent.go` around lines 358 - 362, Update the permission-mode diagnostics and comments to use canonical “full-auto” terminology: in internal/acp/agent.go:358-362, describe full-auto as canonical and --skip-permissions-unsafe as its deprecated alias; update the unsandboxed and network retry messages in internal/agent/loop.go:1488-1491 and 1542-1544; revise branch comments and generic errors in internal/cli/app.go:291-292; and direct one-shot users to “zero exec --full-auto” in internal/cli/app.go:334.Source: Coding guidelines
🤖 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/agent/types.go`:
- Around line 25-36: Preserve the source-compatible PermissionModeUnsafe alias
in internal/agent/types.go, and add or reuse a normalization helper at the agent
boundary that maps raw PermissionMode("unsafe") to PermissionModeFullAuto. In
internal/agent/loop.go lines 1168-1171, normalize Options.PermissionMode before
permission checks and tool execution so executeToolCall grants full-auto
behavior. Add a regression test in internal/agent/loop_test.go lines 2723-2749
covering PermissionMode("unsafe") and verifying permission is granted.
In `@internal/cli/exec.go`:
- Around line 575-592: Make the full-auto warning in the permission-mode
handling path accurate for both --full-auto and --skip-permissions-unsafe.
Replace the options.skipPermissionsUnsafe-specific reason with neutral wording
such as “full-auto mode was requested,” or preserve the original parsed flag
spelling and use it; update TestRunExecUnsafeTextModeWarns if source-specific
wording remains.
In `@internal/sandbox/analyzer.go`:
- Around line 338-347: Update commandRunsLocalServer so multi-purpose programs
such as npm, pnpm, yarn, bun, and other server-capable runners return true only
when their arguments select a server subcommand; preserve unconditional
detection for direct server binaries such as http-server. Add regression tests
covering non-server commands including next build, nuxt generate, astro check,
and vite build, ensuring they do not set LocalServer.
In `@internal/specialist/exec.go`:
- Line 158: Update specialistAutonomy to treat both "full-auto" and the legacy
"unsafe" value as the high-autonomy mode, while preserving the existing
low-autonomy result for other values. Add regression coverage in exec_test.go
verifying both inputs produce the expected autonomy.
In `@internal/tui/model.go`:
- Line 4710: Update the shell-escape denial notice in internal/tui/model.go at
lines 4710-4710 to use canonical full-auto wording and advertise the full-auto
entry point instead of unsafe mode or its deprecated flag. Update the
corresponding assertion in internal/tui/tui_fixes_test.go at lines 37-37 to
expect the canonical full-auto terminology.
- Around line 1361-1369: The unsafeArmed offer is cleared only for keypresses,
allowing paste or mouse input to leave it active. Update the input handling in
internal/tui/model.go around the unsafeArmed reset so tea.PasteMsg and
tea.MouseMsg also disarm the offer before processing, and add regression cases
in internal/tui/permission_mode_arm_test.go:59-81 proving Ctrl+G remains inert
after each input type.
In `@internal/tui/permission_mode_arm_test.go`:
- Around line 21-24: Replace the direct model literals in armedModel and the
standalone confirmation and Shift+Tab tests with newModel-based initialization,
preserving the explicit permissionMode configuration. Ensure every model passed
to pressKey has newModel’s now callback initialized.
---
Outside diff comments:
In `@internal/acp/agent.go`:
- Around line 358-362: Update the permission-mode diagnostics and comments to
use canonical “full-auto” terminology: in internal/acp/agent.go:358-362,
describe full-auto as canonical and --skip-permissions-unsafe as its deprecated
alias; update the unsandboxed and network retry messages in
internal/agent/loop.go:1488-1491 and 1542-1544; revise branch comments and
generic errors in internal/cli/app.go:291-292; and direct one-shot users to
“zero exec --full-auto” in internal/cli/app.go:334.
🪄 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
Run ID: f78fbed2-f8c9-4cca-9fda-535498941eb1
📒 Files selected for processing (29)
internal/acp/agent.gointernal/acp/agent_test.gointernal/agent/compaction_test.gointernal/agent/loop.gointernal/agent/loop_test.gointernal/agent/types.gointernal/cli/app.gointernal/cli/app_test.gointernal/cli/exec.gointernal/cli/exec_parse.gointernal/cli/exec_test.gointernal/cli/exec_tools.gointernal/cli/trust_e2e_test.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_test.gointernal/sandbox/engine.gointernal/sandbox/engine_test.gointernal/sandbox/normalize.gointernal/sandbox/risk.gointernal/sandbox/types.gointernal/specialist/exec.gointernal/tools/bash_tool_test.gointernal/tools/registry_test.gointernal/tui/keybindings.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/permission_mode_arm_test.gointernal/tui/tui_fixes_test.gointernal/tui/view.go
| func armedModel(t *testing.T) model { | ||
| t.Helper() | ||
| m := model{permissionMode: agent.PermissionModeAsk} | ||
| armed := pressKey(t, m, tea.Key{Code: tea.KeyTab, Mod: tea.ModShift}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Initialize permission-mode test models with newModel.
pressKey calls model.updateModel, which calls m.now() at internal/tui/model.go Line 1351. The direct model literals leave now nil, so these tests panic on their first keypress.
Proposed fix
func armedModel(t *testing.T) model {
t.Helper()
- m := model{permissionMode: agent.PermissionModeAsk}
+ m := newModel(context.Background(), Options{PermissionMode: agent.PermissionModeAsk})Apply the same constructor pattern to the direct model literals in the standalone confirmation and Shift+Tab tests.
Also applies to: 34-41, 86-103
🤖 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/permission_mode_arm_test.go` around lines 21 - 24, Replace the
direct model literals in armedModel and the standalone confirmation and
Shift+Tab tests with newModel-based initialization, preserving the explicit
permissionMode configuration. Ensure every model passed to pressKey has
newModel’s now callback initialized.
|
@gnanam1990 @anandh8x @jatmn this is the split you asked for on #808. All checks green. Small enough to read in one sitting, which was the point: +487/-128 across 29 files, versus the +9077 it was hiding inside. Three independent changes, and they are genuinely independent, so feel free to take a position on one and ignore the others. The parts I would look at hardest, because they are where I would expect to be wrong: The full-auto offer state lives on the model across keypresses. A stale flag would turn a later innocent The network reclassification loosens what counts as egress, which is exactly where a sandbox quietly weakens. @gnanam1990 already checked this on #808 and confirmed
On the rename: the old Go constants and the old on-disk value are both kept as permanent aliases rather than migrated. That is not politeness, it is the fix for how I broke CI on #808: deleting a constant that in-flight work is using makes the merge someone else's problem. What this does not change: full-auto skips permission PROMPTS. The OS sandbox stays on and the write jail still holds. That is why it is not called "unrestricted", and why the warning text names prompts specifically rather than implying something broader. Cherry-picked onto |
gnanam1990
left a comment
There was a problem hiding this comment.
Approving at 0311ffb9. This is the split I asked for on #808 — the permission-mode rename and the dev-server network reclassification lifted out of the Windows-principals PR into their own change. Thanks for doing it.
The permission-mode change is carefully built, and I checked the parts that would bite
The wire value changed, and the backward-compat is in the right place. PermissionModeUnsafe = "unsafe" became PermissionModeFullAuto = "full-auto", with PermissionModeUnsafe now a compile-time alias. The alias only keeps Go code compiling — it does nothing for a persisted or transmitted "unsafe" string, which no longer equals the constant. What actually saves that is NormalizePermissionMode mapping both "full-auto" and legacy "unsafe" to PermissionFullAuto, and it's called at engine.go:315 — inside Decide, right before risk classification and the allow/deny. That is the enforcement chokepoint, so however the mode string arrived (CLI, ACP, session replay, config), it's normalized at the one point that governs enforcement. A restored "unsafe" session is enforced as full-auto, correctly.
And the failure direction is safe. Anything NormalizePermissionMode doesn't recognize falls to PermissionModeAuto, and the TUI's advancePermissionMode folds unknown modes to Ask — both stricter. So a missed normalization anywhere is a downgrade, never an escalation. The legacyFullAutoPermissionMode comment makes the "visible downgrade beats silent one" reasoning explicit, which is the right call for a value that lives in user configs.
The arm/confirm gate genuinely prevents accidental full-auto. Full-auto isn't reachable by cycling — shift+tab from Ask arms an offer, and confirmUnsafePermissionMode commits only from a live offer that any other keypress clears. So the path is shift+tab → shift+tab (arm) → ctrl+g, and pressing shift+tab again while armed goes to Auto and drops the offer rather than confirming. I mutation-tested this rather than trust the read: removing the if !offered guard so confirm ignores the offer fails TestConfirmDoesNothingWithoutALiveOffer immediately (confirm with no offer from auto = full-auto, want unchanged). The security property is pinned.
CLI compat holds: --skip-permissions-unsafe and --full-auto are both accepted (exec_parse.go:23), and --auto resolves to PermissionModeFullAuto. Remaining "unsafe" strings in the tree are either code comments or import "unsafe", not user-facing.
The network change is the same one I verified on #808
Network and LocalServer are still set by independent checks (analyzer.go:193, 196), so a command that genuinely fetches keeps Network = true even if it also binds a port — no egress escape. The commands moved out of egress (npm run dev, vite, next dev, http.server, start/serve/dev/preview) only bind, and LocalServer preserves the inbound signal rather than dropping it. Correct and fail-closed, as before.
Verified
go build ./...andGOOS=windows go build ./...clean.internal/tui,internal/sandbox,internal/agentsuites green; the arm gate is mutation-tested as above.- The two
internal/clidoctor failures (TestRunDoctorConnectivityProbesProvider,…FormatsRedactedProviderDiagnostics) are pre-existing — they fail on untouchedmainand this PR touches no doctor/observability/connectivity file. - Feature runs against the real binary: macOS 7/7, Linux 7/7 (file writes, nested paths, sandbox refusal,
--add-dirgrants, control characters, specialist children,exec_command). Windows is green on the PR's own CI (Smoke (windows-latest)pass, 9m).
One scope note, not blocking
The PR is still two unrelated concerns in one — a TUI permission-mode rename and a sandbox command-classifier change; the title carries the "and." It's a large improvement over #808's four-in-one, and both halves are correct, so I'm not going to hold it up. But they'd be independently revertable as two PRs, and the network reclassification in particular is the kind of security-adjacent change that's easier to reason about — and to bisect later — on its own. Your call; flagging it rather than requesting it.
Clean work, and the arm gate is a genuinely thoughtful bit of security-UX.
|
Pushed fixes for the review. Two of the findings turned out to be bigger than they looked, and one of them was a hole in my own tests. The legacy-value finding was right, and wider than reported. The Go alias only covers callers that name the constant. It does not cover a mode that travels as a string, and three places do:
All three fail safe, which is why nothing broke loudly. Added On the test-panic finding: correct, and worse than described. Renamed the file, fixed the model construction, and added a repository-wide test that fails on any With those tests actually running, the paste/mouse gap was real. Probed it before fixing: armed the offer, sent a
One I am skipping: the Wording fixes for the retry reasons, the exec warning and the shell-escape notice are in. The exec warning now names both flag spellings, since one bool backs both and pointing someone at a flag they did not type is its own small bug. Verification: Sorry about the dismissed approval @gnanam1990, the push was unavoidable. Re-requesting. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/tui/model.go`:
- Around line 4716-4719: Normalize the legacy "unsafe" permission value before
assigning m.permissionMode in newModel, using the same normalization behavior as
agent.Run so shell escapes are allowed in the resulting full-auto mode. Add a
regression test in internal/tui/tui_fixes_test.go covering !echo hi with the raw
legacy permission value.
🪄 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
Run ID: 5af63042-9553-419a-b49d-aedcc6c2823b
📒 Files selected for processing (14)
internal/agent/loop.gointernal/agent/types.gointernal/cli/app.gointernal/cli/exec.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_local_server_test.gointernal/specialist/exec.gointernal/specialist/exec_test.gointernal/swarm/permission_rank_test.gointernal/swarm/team.gointernal/tui/model.gointernal/tui/permission_mode_offer_filename_test.gointernal/tui/permission_mode_offer_test.gointernal/tui/tui_fixes_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
- internal/agent/loop.go
- internal/cli/app.go
- internal/sandbox/analyzer.go
- internal/cli/exec.go
893545d to
e954fde
Compare
|
Rebased onto main. Mergeable again. One conflict, and it needed both sides rather than a pick. #884 added The other thing the rebase surfaced is worth flagging on its own:
@gnanam1990 your approval was dismissed by the earlier push, sorry; re-requesting. CodeRabbit is the remaining blocker. |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Preserve a host-reachable path for local preview servers
internal/sandbox/analyzer.go:208
Reclassifyingpython -m http.server, Vite, and package-manager dev commands as non-network suppresses the only permission path that grantsNetworkAllow, butLocalServerhas no execution-policy consumer. The default Linux sandbox consequently adds--unshare-net, so the listener is private to the sandbox namespace and the user's browser cannot reach it; macOS's(deny network*)rejects the bind outright. A normal request to start a preview now runs without a prompt but cannot provide the advertised local server. Keep a scoped approval/runner path for host-visible listening (or consumeLocalServerwith a suitable policy) and cover the real accessibility behavior. -
[P1] Do not trust package-script names as proof of no network access
internal/sandbox/analyzer.go:268
npm run dev(and the pnpm/yarn/bun equivalents) executes arbitrary workspacepredev/script/postdev hooks, yet this change clears its network classification solely from the requested script name. For example, a repository can makepredevupload secrets withcurl; on unelevated Windows the documented network isolation is only the per-command approval gate, so this now runs with real network access and without the former prompt. Keep package-manager script dispatch network-gated unless the resolved script can be safely analyzed; only direct known listener binaries should be eligible for the relaxed classification. -
[P2] Normalize the legacy mode before the TUI consumes it
internal/tui/model.go:865
The compatibility conversion happens inagent.Run, butnewModelcopies a persisted/rawPermissionMode("unsafe")unchanged. Since the deprecated Go constant now has the value"full-auto", this makes the TUI reject!shell escapes, publish a prompting peer identity, and select lower self-correction autonomy even though the ensuing agent run normalizes to full-auto. Normalize at the TUI boundary as well and add the regression case requested by the existing CodeRabbit review. -
[P2] Include the canonical flag in generated shell completions
internal/cli/completions.go:23
Both root andexeccompletion inventories retain only the deprecated--skip-permissions-unsafespelling. The new documented and parsed--full-autoflag therefore cannot be suggested or completed in bash, zsh, fish, PowerShell, or elvish. Add the canonical spelling to both inventories while retaining the alias.
e954fde to
0dd1a5b
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/tui/plan_mode_test.go (1)
51-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCover
PermissionModeFullAutorestoration.This test says it restores any prior mode, but it only tests
PermissionModeAsk. Add a case that starts inagent.PermissionModeFullAuto, enters plan mode, and asserts that/plan offrestoresPermissionModeFullAuto.As per coding guidelines, "Every behavior or security-boundary change requires a regression test."
Proposed test
+func TestPlanCommandRestoresFullAutoOnExit(t *testing.T) { + m := newModel(context.Background(), Options{PermissionMode: agent.PermissionModeFullAuto}) + + updated, _ := m.dispatchCommand(parseCommand("/plan on")) + next := updated.(model) + updated, _ = next.dispatchCommand(parseCommand("/plan off")) + next = updated.(model) + + if next.permissionMode != agent.PermissionModeFullAuto { + t.Fatalf("permissionMode after /plan off = %s, want full-auto", next.permissionMode) + } +}🤖 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 51 - 65, Extend TestPlanCommandRestoresPriorModeOnExit to cover an initial agent.PermissionModeFullAuto state: enter plan mode with /plan on, then exit with /plan off, and assert that permissionMode is restored to agent.PermissionModeFullAuto. Preserve the existing PermissionModeAsk coverage.Source: Coding guidelines
🤖 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.
Outside diff comments:
In `@internal/tui/plan_mode_test.go`:
- Around line 51-65: Extend TestPlanCommandRestoresPriorModeOnExit to cover an
initial agent.PermissionModeFullAuto state: enter plan mode with /plan on, then
exit with /plan off, and assert that permissionMode is restored to
agent.PermissionModeFullAuto. Preserve the existing PermissionModeAsk coverage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 06eecd36-b0d6-4cf8-86db-adc590b94142
📒 Files selected for processing (17)
internal/acp/agent.gointernal/acp/agent_test.gointernal/agent/loop.gointernal/agent/loop_test.gointernal/agent/types.gointernal/cli/app.gointernal/cli/app_test.gointernal/cli/exec.gointernal/cli/exec_parse.gointernal/cli/exec_tools.gointernal/specialist/exec.gointernal/specialist/exec_test.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/peer_messages.gointernal/tui/plan_mode_test.gointernal/tui/view.go
🚧 Files skipped from review as they are similar to previous changes (15)
- internal/cli/app_test.go
- internal/agent/loop_test.go
- internal/acp/agent_test.go
- internal/cli/exec.go
- internal/cli/exec_tools.go
- internal/tui/model.go
- internal/acp/agent.go
- internal/cli/exec_parse.go
- internal/specialist/exec_test.go
- internal/cli/app.go
- internal/specialist/exec.go
- internal/agent/loop.go
- internal/agent/types.go
- internal/tui/view.go
- internal/tui/model_test.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Preserve a host-reachable sandbox path for local preview servers
internal/sandbox/analyzer.go:192
The new classification setsLocalServerbut removesNetwork, and no policy or runner code consumesLocalServer. Consequently the existing network approval path is skipped and the command is executed with the defaultNetworkDenyprofile: Linux isolates it in a network namespace and macOS applies(deny network*). A normalpython -m http.serverorviterequest now runs without a prompt but cannot expose a preview to the user's browser. Keep a scoped host-listener approval/profile path (and cover runner-level accessibility), or retain the network approval path. -
[P1] Do not infer no egress from a package-script name
internal/sandbox/analyzer.go:257
npm/pnpm/yarn/bunrun dev(and the other serving names) are now classified as non-network without resolving the package script or its lifecycle hooks. A repository can makepredevordevexecutecurlbefore it starts a listener; on Windows, where the approval gate supplies the effective network protection, this change lets that host egress run without approval. Keep package-manager script dispatch network-gated unless the resolved hook chain can be safely analyzed. -
[P2] Normalize the legacy mode before the TUI applies mode-specific behavior
internal/tui/model.go:870
newModelcopies a rawPermissionMode("unsafe")option unchanged, while the TUI now checks onlyPermissionModeFullAuto. Any TUI entry point supplied with the accepted legacy value therefore rejects!shell escapes, publishes a prompting peer identity, and selects low self-correction, even thoughagent.Runlater normalizes the same run to full-auto. Normalize at the TUI boundary and add a regression case for the raw legacy value. -
[P2] Accept the canonical mode through the explicit exec-mode interface
internal/cli/exec_tools.go:88
The PR establishesfull-autoas the canonical raw/on-disk value, butresolveExecPermissionModeaccepts only the deprecatedunsafespelling (plushigh). Thuszero exec --permission-mode full-auto ...exits with an invalid-mode usage error beforeNormalizePermissionModeis reached, while the warning code already contains an unreachablefull-autobranch. Acceptfull-autowhile retainingunsafeas the alias, and update the usage/test coverage. -
[P2] Add the canonical flag to generated shell completions
internal/cli/completions.go:23
Both completion inventories still list only--skip-permissions-unsafe; neither root norexeccompletion can suggest the new documented--full-autoflag in any generated shell script. Include the canonical flag in both lists while preserving the deprecated alias.
|
Thanks for the review. I've addressed the key feedback points:
Verified: |
|
All five fixed at Both P1s were the same mistake in two places, which is worth saying plainly: Your package-manager half is the sharper one and I had no answer to it. The fix keeps the classification, because a scoped host-listener path will want it, and keeps the approval until that path exists. One line, since both findings flow through the same point. I rewrote the PRs own tests rather than flipping their assertions. The three P2s are done as described: Build, vet, gofmt clean and cross-compiled for windows, linux and darwin. One thing still open from your #866 review that I have not done: the P3 about |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/cli/completions.go`:
- Line 23: Update the completion regression expectations in assertCandidates
within completions_test.go for both the root and exec candidate lists to include
--full-auto, preserving the existing candidate ordering and coverage for the
newly added completion entry.
🪄 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
Run ID: b170ffb0-0ab3-47ce-9f9e-9f9bbdbf1ec3
📒 Files selected for processing (6)
internal/cli/completions.gointernal/cli/exec_tools.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_local_server_test.gointernal/sandbox/analyzer_test.gointernal/tui/model.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/tui/model.go
- internal/cli/exec_tools.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P3] Cover the new canonical completion flags
internal/cli/completions_test.go:156
The completion-tree test still asserts only the deprecated--skip-permissions-unsafespelling for both the root andexeccontexts. Removing either newly added--full-autoentry fromcompletionRootwould therefore leave all completion tests green, even though the advertised flag would no longer be suggested. Assert--full-autoin both contexts alongside the compatibility alias. -
[P3] Report accepted
full_autopermission-mode input accurately
internal/cli/exec.go:631
resolveExecPermissionModeaccepts--permission-mode full_autoand resolves it to full-auto, but this warning-source switch does not recognize that accepted spelling. It falls through to--auto high, so the safety warning claims a flag was passed that the user did not supply. Includefull_autohere (and preserve the supplied spelling), or use a generic--permission-modereason for all accepted aliases.
|
@coderabbitai full review
|
|
🧠 Learnings used✅ Action performedFull review finished. |
`python -m http.server` was denied with "network access requires approval". Serving files out of the workspace on a local port is not reaching out, and treating it as egress made the single most common thing an agent is asked to do while building something stop for an approval that protected nobody. The analyzer already had a localServerPrograms map sitting beside networkPrograms, but commandUsesNetwork returned true for it anyway, so the distinction had been drawn and never acted on. It is acted on now: AnalysisResult carries LocalServer separately, so the information is preserved rather than dropped, and a caller that does care about inbound can still see it. Covers the same set consistently. `next dev` and `npm run dev` are the same intent, so classifying one as binding and the other as fetching would have left users hitting exactly the same wall through the other door. Both are LocalServer now, across npm, pnpm, yarn and bun, as a direct subcommand and behind `run`. What still counts as network is unchanged: install, add, ci, create, publish, login, exec, and every fetching program. `npx http-server` in particular stays network, because npx downloads the package when it is missing; the test caught that when a first pass flipped it, which is why the bind-versus-fetch pairs are now pinned side by side in the table. The unparseable fallback regex is updated to agree. Left alone it would have flagged an obfuscated dev server for network while the same command written plainly did not, which is the kind of inconsistency that is very hard to explain to whoever hits it. Being honest about the edge: some of these touch the network incidentally, and `npm run dev` may install first. The claim is narrow, that BINDING is not EGRESS, not that dev tooling is inert. Anything that actually fetches still matches through its own program or subcommand. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
"unsafe" was a judgement rather than a description, and it sat oddly beside auto and ask. full-auto says what the mode does and reads as the obvious third step: ask, auto, full-auto. Not "unrestricted", which was the other candidate and is factually wrong. This mode turns off permission PROMPTS. The OS sandbox stays on, the write jail still holds, and a label implying nothing restrains the agent would be wrong in the dangerous direction. The on-disk value changes to "full-auto" and "unsafe" is accepted permanently as an alias rather than migrated. That value lives in user configs and in scripts, and a session silently falling back to auto because its saved mode no longer parsed would be a confusing downgrade rather than a visible error. --full-auto is the new flag; --skip-permissions-unsafe keeps working and is documented as the deprecated spelling. The exec warning names "the full-auto flag" generically, because either spelling reaches it and the bool does not record which was typed: claiming --full-auto when the caller passed the old one would be a small lie in a warning, which is the wrong place for one. The warning also says "prompt-gated tools run without approval" rather than anything broader, for the same reason the mode is not called unrestricted. What deliberately does NOT change is the colour. The name can be calm; the indicator should not be, so full-auto keeps the alarm style in the status bar and the ctrl+g confirm gate is untouched. The Go "unsafe" package is imported in fifteen files here, so the rename was done by identifier token only and the imports were verified intact afterwards rather than assumed. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
The rename broke CI, and it broke it in the merge rather than on the branch: this branch is ten commits behind main, CI builds the merge, and newer main code still references PermissionModeUnsafe and PermissionUnsafe. Deleting a constant that other work is actively using turns an ordinary merge into a compile failure for whoever merges second. Both old names are restored as deprecated aliases of the full-auto constants. Same value, so behaviour is identical, and code on either side of the rename compiles. My local check missed this because `go test ./internal/...` returned "ok (cached)" for internal/agent, and a cached result cannot catch a compile error. Verified this time with -count=1 and against an actual trial merge with origin/main rather than the branch alone. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
The rename kept a Go alias for PermissionModeUnsafe, which covers callers that name the constant. It does not cover a mode that moves as a string, and several do, so those paths went on comparing against "unsafe" and stopped matching. Three places were affected: - swarm permissionRank did not rank "full-auto", so it scored 0, the strictest tier. A full-auto parent clamped its members harder than an ask parent. - specialist specialistAutonomy did not match it either, so a full-auto parent's specialists dropped to read-only "low" autonomy. - the agent loop compares Options.PermissionMode directly, so a caller passing the old spelling got prompts instead of full-auto. All three fail safe, which is why nothing broke loudly. Added agent.NormalizePermissionMode and applied it at the single point the loop reads the mode off Options, and taught the swarm and specialist constants both spellings. Their existing tables only listed "unsafe", which is exactly how the rename passed review. Separately, permission_mode_arm_test.go was never compiled. Go read the trailing "_arm" as a GOARCH constraint and excluded the file from every amd64 build, so the five tests covering the shift+tab full-auto offer had never run, locally or in CI. Two panicked once they did. The file is renamed, the model construction fixed, and a repository-wide test now fails on any _test.go whose name ends in a GOOS/GOARCH token without an explicit build tag. With those tests actually running, the offer gate had a real hole: unsafeArmed was cleared only in the key handler, so a paste or a mouse click left the offer live and a later ctrl+g committed full-auto with nobody having accepted it. Paste and deliberate mouse actions now cancel it. Passive motion does not, since terminals stream motion while tracking is on and cancelling on a twitch would make the confirm key unreachable. Also: commandRunsLocalServer matched on program name alone, so "next build" and "vite build" claimed to bind a port while compiling. Nothing reads the flag yet, which is why it could be wrong quietly. Fixed to require a serving subcommand, falling back to bare-invocation behaviour when firstSubcommand lands on an option value rather than a subcommand. Remaining wording fixes name full-auto rather than unsafe in the retry reasons, the exec warning, and the shell-escape notice. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
The rebase renamed nextPermissionMode to advancePermissionMode, and main's plan-mode test still called the old name, so internal/tui did not build. Also asserts the second return value. Plan must never carry a full-auto OFFER either, or two presses from Plan would reach the mode that turns permission prompts off entirely, starting from the one mode that promises no mutation. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
…onical mode All five of jatmn's findings. The two P1s were the same mistake in two places: the LocalServer classification was treated as a REPLACEMENT for Network when nothing consumes it. No policy or runner code reads LocalServer, so classifying a serving command as local-only granted it no host listener; it only removed the network approval it used to get, and the command then ran under the default deny profile. On Linux that is a network namespace and on macOS (deny network*), so `python -m http.server` and `vite` started with no prompt and could not serve a preview to the operator's browser. The package-manager half is the sharper one. `npm run dev` is matched by SCRIPT NAME, and the repository decides what `dev` and `predev` actually do; either can curl before anything binds a port. On Windows the approval gate IS the network protection, so inferring no-egress from a name lets that egress run unprompted. LocalServer is now additive: the classification stays, because a scoped host-listener path will want it, and the approval path stays until that path exists. One line covers both findings, since both flow through the same classification. The PR's own tests asserted the premise being corrected, so they were rewritten rather than flipped: building still counts as no egress, serving now asserts it keeps BOTH the LocalServer classification and the network approval, and the table rows moved with it. The three P2s: resolveExecPermissionMode accepted only the deprecated `unsafe`, so `--permission-mode full-auto` failed usage validation before NormalizePermissionMode was ever reached, while the warning code already had an unreachable full-auto branch. It now accepts the canonical spelling and keeps unsafe as the alias, and the usage text names full-auto. newModel copied the raw mode through while the TUI tests only for PermissionModeFullAuto, so an entry point handed the accepted legacy value rejected ! shell escapes, published a prompting peer identity and chose low self-correction, even though agent.Run normalized the same run to full-auto. One value, two behaviours, decided by which layer looked at it. It is normalized at the TUI boundary now. Both completion inventories listed only --skip-permissions-unsafe, so no generated shell script could suggest the documented --full-auto. Both now carry the canonical flag alongside the deprecated alias. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
…red-input arm Five findings from review. The unparseable fallback lost the serving forms on the reasoning that they bind rather than fetch. The AST path sets Network for every recognized LocalServer anyway, deliberately, because nothing consumes LocalServer yet and `npm run dev` is matched by script name where the repository decides what dev and predev do. So dropping them did not align the two paths, it split them: the POSIX parser rejects Windows shell syntax the invoked shell accepts, and if "%OS%"=="Windows_NT" (npm run dev) else (npm start) came back parsed=false network=false while every parseable spelling got network. That is the approval gate disappearing on the platform where it is the only egress control. Restored as a superset, with a regression that asserts both spellings of the same intent classify alike and fails loudly if the parser ever learns the syntax. tea.PasteMsg cancels the full-auto offer, but a right-click paste does not arrive that way: it starts a clipboard read, and shift+tab can reach the offer while that read is in flight. The delayed delivery inserted text and left the arm alive, so an ordinary ctrl+g afterwards turned permission prompts off with the user several actions past the confirmation. Cleared before the branches, on the image delivery too, and on transcribed dictation for the same reason. Specialist registration read the raw CLI fields while execution read the resolved mode, so `--permission-mode full-auto` composed a registry without Task and swarm tooling while `--auto high` and `--full-auto` composed one with it. The mode is resolved before the registry now and the predicate consumes it, so a new alias cannot create another unequal capability set. The full-auto warning matched the typed --permission-mode value against three spellings while the resolver accepts four, so `--permission-mode full_auto` was told --auto high was responsible. It echoes what was typed instead of matching a list that has already drifted once. Help and validation text still led with the deprecated spelling: --permission-mode listed "unsafe" as its value, --auto high was described as enabling "unsafe tools", and the root-flag errors named --skip-permissions-unsafe. All now present full-auto as the mode and the primary flag; the deprecated alias stays where its compatibility is the point. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
… the network fallback from the analyzer Two findings, both follow-ons to fixes I made last round. A streaming dictation partial did not cancel the full-auto offer. The final transcript did, and my note there said starting dictation is a keypress that already clears the arm, which does not cover this: the offer can be armed AFTER dictation is running, the next partial rewrites the composer, and active dictation is not a blocking modal, so an ordinary ctrl+g still confirmed with the user several actions past the confirmation. Cleared at the dispatch boundary rather than inside handleDictationPartial, so a future partial-delivery path inherits the rule. The regression drives a genuinely live session and fails if the partial never reaches the composer, so it cannot pass on a session-mismatch early return. The unparseable network fallback is meant to be a superset of everything the AST path flags, and it was maintained as a separate inventory, so it drifted. The analyzer accepts "py" as a Python launcher and the regex listed only python, python2 and python3: py -m http.server 8000 parsed=true network=true if "%OS%"=="Windows_NT" (py -m http.server 8000) else (..) parsed=false network=false if "%OS%"=="Windows_NT" (python -m http.server ...) else .. parsed=false network=true On Windows that approval IS the egress control. Rather than adding the alias and leaving two lists to drift again, the launchers are one named inventory the analyzer uses at both call sites, and the fallback test iterates it, so a launcher added there fails immediately if the regex does not cover it. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
… at any arity Raised by CodeRabbit. Skipping flags is not enough, because it does not skip the words they CONSUME: `npm --prefix ./web install` resolved to "./web", the option's VALUE, and stopped being recognised at all — so a command genuinely fetching from the network lost the approval its plain spelling gets. The first fix offered a fixed window of two positions. That covers exactly ONE value-taking option, and a second walks straight past it. Measured before this change: npm --prefix ./web install -> network (correct) npm --prefix ./web --loglevel warn install -> NOT network npm --registry https://r.test --prefix ./web install -> NOT network pnpm -C ./web --filter web add left-pad -> NOT network All four genuinely fetch. ADJACENCY DECIDES NOW, because it is the actual grammar: an option can only consume the word immediately after it. Every leading operand sitting right after a flag might be that flag's value, so each opens another position; the first operand NOT preceded by a flag cannot be anyone's value, and the window closes there. The arity is counted rather than assumed, and no option is ever named — which was the point of not enumerating them, since that is four tools each free to add one. The run has to be CONTIGUOUS, not a count of flags anywhere in the line. Counting every flag lets TRAILING options widen the window back over words they have nothing to do with, and three cases flip to false positives when it does: npm run --grep foo --tag dev -> network AND localServer npm run test --reporter x --filter dev -> network npm run build --a x --b start -> network The false positives the old cap protected against are unaffected, and for a stronger reason than the cap gave: `npm run build --workspace dev` and `npm run test -- --grep start` put an unflagged operand FIRST, so the window is one position wide however many flags follow. The cap held only because two happened to be short enough. Tested as a PROPERTY rather than a list: each flagged form is paired against its unflagged baseline, so a change that shifts both still fails. Eight pairs, the "="-joined forms and repeated flags included. Two mutations, each caught — restoring the fixed window mis-classifies five, dropping the contiguity requirement widens three. Also from a verification pass: The doc comment claimed the cap at two was justified purely by argument-position false positives and never said a second value-taking option defeated it. It now describes what ships. `npm --prefix dev install` reads a directory named "dev" as a script name. Pinned rather than fixed: the ambiguity is the point of the window, Network is already correct because the install really does fetch, and LocalServer only implies Network — so the union is conservative in the safe direction. An earlier note justified leaving internal/tui/session.go unnormalised by saying the value feeds transcript rendering and the render cache. It feeds only the cache fingerprint — grepping every reader of PermissionMode in that package returns render_cache.go and nothing else — so the tradeoff described did not exist. It is normalised now, and two spellings of one mode no longer evict the cache for nothing. The ACP tests asserted on ConfigOptions[1], a magic index that would move onto a different option if the advertised order changed. They select by ID. Rebased onto ad34dc8. Pre-existing here and on main: TestRunDoctorFormatsRedactedProviderDiagnostics and TestRunDoctorConnectivityProbesProvider exit 3 in this environment. Origin-Session: local-c962d7 | Claude Code | 5 prompts Origin-Snapshot: 3bb420a0a97b
4a77c20 to
677b3f0
Compare
|
Rebased onto main at 677b3f0. All the conflicts were with #1019, which landed overnight: the root-flag allowlist now names --full-auto next to --skip-permissions-unsafe, the TUI launch keeps the escalation argument under the renamed mode, and the help and completion lists carry both flags. Build, vet and the cli, agent, sandbox and acp packages are green here; linux and darwin cross-builds pass. The push will have dismissed your approval at 4a77c20 @jatmn, so one more look when you have a moment. |
gnanam1990
left a comment
There was a problem hiding this comment.
I found one remaining security blocker on the current head.
Verdict
Changes requested. Reviewed exact base 6937a309cf00 and head 677b3f0c4f4d. The branch is mergeable and CI is green, but parseable framework builds lose the network-specific approval gate that main currently applies.
Finding
-
[P1] Preserve network approval independently of the local-server label
internal/sandbox/analyzer.go:253
internal/sandbox/analyzer_local_server_test.go:46The branch correctly makes
LocalServerdescribe binding rather than building, but it also removeslocalServerProgramsfromcommandUsesNetwork.frameworkSubcommandRunsLocalServerreturns false forbuild,generate, andcheck, and the additiveresult.Network = trueruns only whenLocalServeris true. Consequentlynext build,vite build,nuxt generate, andastro checkall produceNetwork=false;TestBuildingDoesNotCountAsNetworkEgresscurrently pins that unsafe result.These CLIs execute repository configuration, plugins, and application code during builds and can perform egress. The analyzer cannot prove otherwise from the subcommand name. On platforms where the approval gate is the network boundary, a caller that already granted the ordinary shell prompt now runs the build without the separate
ReasonNetworkBlockeddecision.Keep
LocalServerprecise, but restore an independent conservativeNetworkclassification for the framework CLI inventory. Add analyzer and Engine regressions asserting build-like commands areNetwork=true, LocalServer=falseand still receiveReasonNetworkBlockedafter an ordinary shell grant; serving commands should remainNetwork=true, LocalServer=true. Cover option-bearing build forms too so parsing the action more precisely cannot remove the egress gate.
Validation
- Existing focused analyzer, unparseable fallback, ACP mode-boundary, and full-auto CLI tests — pass.
- Disposable production-path regression for the four build commands — fails on all four: each reports
Network=false, andEngine.Evaluatereturnsallowafter an ordinary shell grant instead of a network prompt. - Current GitHub checks — all green; they do not cover this invariant.
git diff --check— pass.- No dependency or external-integration change in the PR delta.
The current ACP test already exercises the separate set_config_option rejection path through the legacy unsafe alias, which normalizes to full-auto; I am not treating the additional canonical-spelling fixture as a merge blocker.
Splitting LocalServer out of the network classification also dropped localServerPrograms from commandUsesNetwork, so next build, vite build, nuxt generate and astro check came back Network=false. Those CLIs run the repository config, plugins and application code during a build as surely as during a serve, and on the platforms where the approval gate is the network boundary an ordinary shell grant would have run them without the separate network decision main applies. The test that pinned Network=false for builds is replaced. Network stays conservative for the whole framework inventory; LocalServer alone carries the serve/build distinction. The framework classifier now walks the positions a subcommand can occupy instead of reading the first operand, so a global option before the subcommand (vite --config vite.prod.ts build) no longer makes a build count as a server. Regressions: build-like and option-bearing forms are Network=true and LocalServer=false at the analyzer, serving forms are both, and Engine.Evaluate still returns the network prompt for a framework build after a bash allow grant. Reported by @gnanam1990.
|
Fixed at 32c3527. You were right, and it is the same mistake I have a note about from #685: narrowing a classification and losing the consumer that read the old breadth. commandUsesNetwork has its localServerPrograms rule back, so the whole framework inventory is conservative for Network whatever the subcommand; LocalServer alone carries the serve/build distinction. The test that pinned Network=false for builds is replaced by one asserting build-like and option-bearing forms are Network=true and LocalServer=false while serving forms are both, and an Engine regression pins the consumer: with a bash allow grant, next build, vite build --mode production, nuxt generate and astro check all still return the network prompt with the grant not matched, the same as curl. Removing the restored line fails both. Adding the option-bearing forms also caught a precision defect: vite --config vite.prod.ts build read the option value as the subcommand and fell through to the bare-vite rule, so the framework classifier now walks the positions a subcommand can occupy, the same helpers the package managers use. Sandbox package green on Windows here, linux and darwin cross-builds and vet pass. @gnanam1990 this is the only change since your review. @jatmn your approval at 4a77c20 was dismissed by the rebase this morning and again by this push; one more look when you have a moment. |
…-and-classifier # Conflicts: # internal/acp/agent_test.go
|
@gnanam1990 your P1 is fixed and you were right about it.
Head is |
|
@jatmn both P1s from The dictation one was a real hole and your diagnosis of the root cause was the useful part: the offer was cancelled on selected deferred-input message types rather than on any user-visible input transition. The rebase is done and then some: head Also worth knowing since it postdates your review: @gnanam1990 found that this branch had removed CI 12 of 12 at this head. |
jatmn
left a comment
There was a problem hiding this comment.
I found two P2 issues that need to be addressed before this is ready. Please address the underlying contracts and their affected paths together so the next revision closes these issues completely.
Findings
[P2] Keep the full-auto offer visible before accepting confirmation
internal/tui/model.go:1991 — arming; confirmation at internal/tui/model.go:2005; display consumers at internal/tui/view.go:215 and internal/tui/view.go:238–245.
Reproduction and impact
- Start in Ask mode while dictation is recording or transcribing.
- Press Shift+Tab. The handler arms the full-auto offer and keeps the current mode at Ask.
- Inspect the footer: it displays REC or “transcribing…” instead of “full-auto? ctrl+g to confirm.”
- Press Ctrl+G before another transcription delivery. The mode becomes full-auto, even though the offer and its confirmation instruction were never displayed.
The recording/transcribing sequence reproduces at 40, 96 and 160 columns. The resulting mode indicator also remains hidden while dictation owns that footer space. At normal widths, the model-download status is another branch that replaces the permission chip.
A separate, immediate Ctrl+G is still required. This is a defect in presenting the new confirmation offer, not a bypass of the confirmation key or the OS sandbox. The PR makes this interaction newly relevant: the base's Shift+Tab cycle cannot enter full-auto through this sequence.
Root cause
The handler and renderer disagree about whether an offer is available to the operator. noBlockingModal() permits arming and confirmation during dictation. modeLabel() correctly produces the offer text, but its caller, statusLine(), subsequently replaces that text with another status. The tiny-width branch returns the dictation chip directly; the wider branch overwrites the left chip. An assertion against modeLabel() alone therefore passes while the actual footer omits the offer.
Requested outcome
Keep offer eligibility, presentation and confirmation consistent: an offer that the UI cannot display must not remain actionable. Either preserve the offer in the final footer or decline arming/confirmation while its display is unavailable. Please account for both footer width branches and the existing statuses that replace this chip, rather than special-casing only the recording example.
The implementation choice is yours. Preserve the existing deliberate Ctrl+G confirmation, ordinary mode cycle, plan-mode restrictions and dictation behavior. This does not require a general UI rewrite or a new confirmation mechanism.
Focused validation
- Drive the real key handler and inspect the final footer for idle, recording, transcribing and model-download states, at narrow and normal widths.
- For each supported state, prove either that the offer is displayed before confirmation succeeds, or that an unavailable offer cannot change the mode. If a status transition can hide an already-armed offer, cover that transition too.
- Retain the positive idle confirmation test and the existing cycle, modal and cancellation regressions. A fix that makes confirmation unreachable everywhere would satisfy only the negative half of the contract.
[P2] Isolate the new CLI parity helper from personal state
internal/cli/exec_full_auto_parity_test.go:13–17 — execListToolsFor.
Failure path and impact
The new helper supplies a temporary working directory and a fake resolveConfig, then calls the real CLI entry point. The remaining dependencies and user-directory settings retain their production behavior. Before --list-tools returns:
runExecstartsRefreshModelsDevCachein a goroutine (internal/cli/exec.go:176). A missing or stale personal cache can cause a models.dev request and a cache update.- Startup reaches the sandbox grant store and user MCP configuration/registration (
internal/cli/exec.go:342–357). The fresh working directory does not isolate the user configuration layer, so personally configured MCP servers can participate in startup.
The parity test invokes this helper seven times: once for the baseline and once for each of six alternative spellings. A clean CI environment can pass while a developer's configured environment performs unrelated network/process activity or changes the test's behavior.
The background request is reproducible with emulated user directories and an in-process HTTP transport, without contacting an external service. Isolating the helper suppresses that request. The MCP startup consequence follows the production dependency path; it does not require assuming that every developer has servers configured.
Root cause
The fixture isolates the project directory and one configuration function, but CLI startup resolves additional user state independently. --list-tools is an early exit from agent execution, not from all startup work. The fake resolveConfig does not replace the models refresh, MCP resolver or permission stores.
This is attributable to the newly added test caller. Production CLI startup already had these behaviors; changing production startup is not the requested fix. The repository's hermetic-test rule applies to the new helper.
Requested outcome
Establish isolation inside execListToolsFor, before calling runWithDeps, so every invocation inherits it. The existing isolateCLIUserState(t) is the natural starting point: it redirects user roots, gives the models cache a fixture path, disables background model fetching and avoids the host keyring. Complete dependency substitution is also acceptable. Check the remaining defaults reached by this helper so that faking the main config is not mistaken for isolating the whole invocation.
Keep the current baseline and all six alias cases, including the swarm_spawn assertion. The fix belongs in test setup; production tool listing should retain its intended integration behavior.
Focused validation
- Run the parity test against an emulated populated user environment, with all fixture files and any fake services confined to temporary test directories.
- Verify that the helper neither starts an unrelated user MCP fixture nor makes a models refresh request, and leaves the emulated personal files unchanged. Exercise the missing/stale-cache condition so a warm cache cannot hide an unisolated refresh.
- Use the repository's cross-platform user-root helper rather than redirecting only the working directory or one XDG variable. Run the parity test alongside the existing CLI isolation tests to catch dependence on another test having configured the environment first.
Guidance for closing this revision
These two findings share a concrete testing gap: the assertions stop before the downstream behavior that determines whether the feature works. The offer text exists in modeLabel, but the final renderer removes it. The parity helper receives a fake config, but startup still resolves other production dependencies. Passing those local assertions does not establish the user-visible or side-effect contract.
That is the pattern worth addressing here. It explains why these two defects can survive otherwise useful tests; it is not enough evidence to assign one cause to every earlier review round.
For this revision, please close each contract through its actual consumers:
- For the offer, follow input → armed state → final display → confirmation or cancellation. Test the real display and resulting mode together.
- For the parity fixture, follow helper setup → CLI startup → configuration/cache/integration dependencies → return and cleanup. Establish isolation before startup can schedule background work.
- For each fix, show that its regression test fails on the current code for the named reason and passes with the fix. Keep the positive behavior checks so refusing everything or skipping the intended assertions cannot make the test pass.
- In the next update, identify the affected paths covered, the focused and neighboring tests run, and any concrete validation limitation. This will make it possible to assess the complete fix together rather than uncover another consumer after reviewing a single patched branch.
The requested work is bounded to these two findings and the directly affected paths above. It does not expand this PR into a sandbox redesign, a new host-listener capability, a general CLI startup refactor or a repository-wide test cleanup. Keep unrelated pre-existing behavior outside the patch. A complete fix here should align the existing contracts and prove them at their observable boundaries.
The offer renders through modeLabel into the left chip, and several statuses replaced that chip wholesale: an active recording, a transcription, and a model download. Arming while the mic was live therefore showed REC while ctrl+g still committed full-auto, so the user was asked to confirm something the footer never put in front of them, and permission prompts went away. offerOwnsStatusChip makes a live offer outrank the statuses that merely report what is happening, in both statusLine branches, since the tiny and non-tiny paths reach the chip differently. The two confirmations that ask their own question still win, because those are what the user just pressed a key for. The confirm key consults the same rule through offerConfirmable, which takes the armed flag as an argument because the handler captures and clears m.unsafeArmed before its switch runs. Sharing one expression is the point: an offer that cannot be displayed is not one that can be accepted, so a status added later cannot reopen the gap by taking the chip. The regression asserts the implication through the real renderer and the real handler, matrixed over the contending states and three widths: if the mode became full-auto, the frame shown before the key contained the offer. It also asserts the converse, so a fix that merely made confirmation unreachable would fail it. Reported by @jatmn.
jatmn
left a comment
There was a problem hiding this comment.
I found two P2 issues that need to be addressed before this is ready, plus a smaller P3 confirmation-display issue. These concern the new interactive full-auto transition and its regression coverage.
Findings
[P2] Avoid blocking the event loop when full-auto releases held peer messages
internal/tui/model.go:2006
Trigger and impact. Start in ask mode with an active run. A message from a bypass-mode peer is held because the permission classes differ. openNextPeerApproval defers opening the approval prompt while m.pending, so the message can be queued without a blocking modal. Shift+Tab followed by Ctrl+G then enters full-auto and synchronizes the peer identity. If that makes the held message eligible for release, the UI freezes during confirmation.
Root cause. This identity update has a synchronous delivery effect:
- The Ctrl+G handler calls
syncPeerIdentityfrom inside Bubble Tea'sUpdate. Service.UpdateIdentityupdates the peer record, reevaluates held messages under the parity policy, and synchronously invokes the held-release handler for newly compatible messages.Runwires that handler throughforwardtoprogram.Send.program.Sendwaits on Bubble Tea's unbuffered message channel. The event loop that must receive the message is already executing the mode change and cannot return until this send completes.
The callback design already exists on the base branch. This PR newly reaches it through the prompting-to-bypass keyboard transition: on the base, Ask cycles back to Auto and these keys cannot enter bypass mode. The finding is about the new transition exposing that existing callback hazard.
A reproduction using the peer service and a real Bubble Tea event loop blocks inside the release callback until the program context is cancelled. The TUI route into that callback is established by the production call chain. The existing peer-release unit test uses a buffered callback channel, which verifies release eligibility but does not exercise this event-loop dependency.
Requested outcome. Make peer release safe when identity synchronization originates inside Update. Choose the appropriate delivery mechanism at that boundary so the event loop can return and subsequently process the released message. Preserve identity publication, the parity decision, and delivery of the queued message. Please account for ordering and shutdown when arranging deferred delivery; simply making the callback return is insufficient if it loses messages or leaves delivery blocked after shutdown.
Regression coverage. Exercise a held bypass-peer message while the receiver is prompting and an active run keeps its approval queued. Drive the new confirmation path with the production-style event-loop callback. Verify that confirmation returns without cancelling the program, the UI processes a subsequent message, and the released peer message reaches the existing inbox/delivery path once. This tests the consumer that the current buffered callback test misses.
[P2] Isolate user state in the new CLI parity helper
internal/cli/exec_full_auto_parity_test.go:13
Trigger and impact. Running TestFullAutoEntryPointsListTheSameSpecialistTools on a developer machine invokes execListToolsFor seven times. The helper supplies a temporary cwd and a mocked provider configuration, but runWithDeps fills its other dependencies with production defaults. Before --list-tools returns, startup can refresh the user's models cache, load and start configured MCP servers, and migrate sandbox grants or consume a pending migration notice. The test can therefore perform unrelated process/network activity, modify personal state, or fail because of the machine's configuration.
Root cause. The fixture isolates workspace configuration, while this entry point also consumes user-scoped configuration and starts background work. --list-tools returns after those startup effects have begun. Passing a temporary cwd does not redirect those user roots. The earlier test-isolation request remains unaddressed in this helper.
An emulated-user test with intercepted HTTP observes an attempted request to models.dev. Adding the existing isolateCLIUserState(t) fixture prevents that request. The MCP and grant effects follow from the production startup dependencies reached before the listing return; the HTTP reproduction does not claim to have exercised every possible personal integration.
Requested outcome. Apply the established user-state isolation fixture inside this helper before invoking runWithDeps. Ensure its background services and integrations consume only fixture-owned configuration. The repository already provides redirection of platform user roots and model-cache storage, disables background model fetching, and selects fixture-backed credential storage. Keep the real mode resolution and tool-registration behavior that this regression is intended to test.
Regression coverage. Add this helper to the existing helper-isolation coverage. Seed an emulated personal configuration outside the helper's fixture and verify that the helper redirects away from it, leaves it unchanged, and does not attempt the background model fetch. Keep the baseline plus all six alias cases and their swarm_spawn assertions. Calling the helper in isolation should be safe; it should not depend on another test having already configured the environment.
[P3] Keep the confirmation usable after the pet reserves footer space
internal/tui/view.go:210
Trigger and impact. With a docked pet, footerStatusLine reserves 11 columns before calling statusLine. At a supported 24-column terminal, the offer renders as ● full-aut…; at 30 columns it renders as ● full-auto? ctr…. The complete View omits the confirmation key in both cases, but Ctrl+G still enables full-auto. This is a narrow-layout defect; I would not block merging solely on this P3 item.
Root cause. offerConfirmable shares the armed/exit/cancel state between rendering and input handling, but that state does not establish that the final layout can display the confirmation. The footer can win the status-priority decision and subsequently be truncated by its effective width. Tests that call statusLine with the terminal width miss the width reduction made by its caller.
The full-View reproduction covers both the display and the subsequent keypress. Wider layouts and layouts without a pet provide controls where the key remains visible. The base has the same space reservation, but has no actionable full-auto offer, so the same key sequence cannot enable bypass there.
Requested outcome. Keep a readable offer and confirmation key visible at the final available footer width, or make an offer that cannot be displayed non-confirmable. Base the decision on the effective layout, including pet reservation. Preserve the existing offer cancellation and status-priority behavior.
Regression coverage. Drive Shift+Tab, inspect the complete View, then drive Ctrl+G at 24 and 30 columns with a docked pet. Assert the contract: either the user can see the offer and confirmation key and explicitly confirm it, or the hidden offer cannot enable full-auto. Include a wider or pet-free control so a fix cannot pass by disabling confirmation everywhere.
Guidance for closing this round
The common pattern in these three findings is that a locally correct change is being checked before its final consumer has finished applying it. The permission mode changes correctly, but its peer callback cannot complete on the UI event loop. The tool list is correct, but startup has already reached personal state. The offer wins the footer's priority decision, but the final layout removes its confirmation instruction.
That explains how these particular gaps can coexist with passing tests. The peer test substitutes a buffered delivery channel, the CLI test checks output without isolating all startup inputs, and the rendering test checks a component before its caller reserves space. The useful next step is to extend each regression through the corresponding production boundary and assert the resulting behavior.
Please address these three as one cohesive follow-up, with each fix and its regression tied to the failure path above. For each changed boundary, check the immediately adjacent caller and consumer so the fix preserves message delivery, tool-registration parity, and usable confirmation. Keep the remediation within those contracts; the existing peer service, CLI isolation fixture, and footer layout provide the relevant places to resolve them.
In the follow-up, please identify the underlying cause addressed, the test that fails without the fix, and the behavior it protects. That gives us concrete closure criteria for this round and reduces the chance of another example of the same underlying issue appearing in the next review. These findings establish specific gaps; they do not establish that every earlier review round had the same cause or that unrelated subsystems need redesign.
Keeps both sides of the two model.go hunks: a mouse event and a paste still answer a pending full-auto offer with not now, and then the terminal-attach overlay from #1055 takes the event as main intends.
… forwarder Service.UpdateIdentity invokes the held-release handler synchronously for every message that becomes deliverable, and the shell calls UpdateIdentity from inside Update on the shift+tab and ctrl+g paths that change the permission class. The handler called program.Send, which waits on the unbuffered message channel that only the loop reads, and the loop was inside that Update: confirming full-auto with a bypass-mode peer's message held froze the shell. Peer callbacks now append to a FIFO drained by one goroutine, so the caller returns, order among peer events is kept, and a late callback after exit returns on the cancelled program context. The wiring is a function with a fake-able service so the regression drives a real Bubble Tea program through the production shape.
execListToolsFor supplied a temporary cwd and a mocked provider config, but runWithDeps filled the rest with production dependencies, so --list-tools refreshed the developer's models cache, started their MCP servers and could touch their config before returning. The helper applies isolateCLIUserState first, and the helper-isolation table now covers it.
…tion key The footer reserves columns for a docked pet before it renders the status line, so at 24 and 30 columns the offer came out truncated with no ctrl+g in sight while ctrl+g still confirmed. The armed flag now means what it says only when the footer, at its effective width after the reservation, shows the key: the decision is made on the same truncation the footer applies, an offer that cannot be shown is not raised, and a live offer is withdrawn when the terminal shrinks under it. Pinned through the complete View at 24 and 30 columns with a pet, with 30 pet-free and 96 with a pet as controls.
|
@jatmn all three at Event loop. The cause is exactly the chain you traced:
CLI parity helper. Footer width. Locally |
Four commits lifted out of #808, where they did not belong. @gnanam1990 blocked that PR on scope and he was right: #808 rewrites Windows write-jail semantics, and burying a permission-mode rename and a network reclassification inside it makes the security-relevant core harder to review on its own.
Cherry-picked onto current main and verified there, not just moved.
The permission mode gains a third position
unsafemode existed and worked, but was reachable only through--skip-permissions-unsafeat launch, so nothing in the TUI told you it was there. It is now the third position in the shift+tab cycle.What it is NOT is reachable by repeating a navigation key. shift+tab only ever OFFERS it; committing takes
ctrl+gwhile that offer is live. Press shift+tab again instead and the offer is declined and the cycle continues, so every mode stays reachable with shift+tab alone.The offer lives on the model across keypresses, which is the part worth care: a stale flag would turn a later innocent
ctrl+ginto a silent drop into full-auto. So it is cleared unconditionally at the top of the key handler and re-armed only by the shift+tab branch. Forgetting a path cancels the offer rather than leaving it live, which is the harmless direction to be wrong in. The tests drive the real handler with eight different cancelling keys and assert the consequence, not just the flag.An earlier draft had the second shift+tab commit full-auto, which silently removed
ask -> autofrom the cycle. There is now a test that walks the whole loop and asserts every mode stays reachable, so that cannot come back unnoticed.unsafebecomesfull-auto"unsafe" was a judgement rather than a description and sat oddly beside auto and ask.
full-autosays what the mode does and reads as the obvious third step.Deliberately NOT "unrestricted", which was the other candidate and is factually wrong: this mode turns off permission PROMPTS. The OS sandbox stays on and the write jail still holds, so a label implying nothing restrains the agent would be wrong in the dangerous direction.
The on-disk value changes and
"unsafe"is accepted permanently as an alias rather than migrated, since that value lives in user configs and scripts.--full-autois the new flag;--skip-permissions-unsafekeeps working as the deprecated spelling. The old Go constants are kept as deprecated aliases too: deleting a constant that in-flight work is using makes the merge someone else's problem, which is exactly how I broke CI on #808 before adding them back.The colour does not change. The name can be calm; the indicator should not be.
Local dev servers are classified separately, and still need network approval
Narrowed from what this section used to claim. An earlier draft of this branch did remove the network approval for serving commands, the follow-up put it back, and the description kept describing the version that no longer ships.
What ships is the classification foundation.
AnalysisResultcarriesLocalServerseparately, so the distinction between binding a port and reaching out is preserved rather than collapsed. The analyzer already had alocalServerProgramsmap besidenetworkProgramsandcommandUsesNetworkreturned true for it anyway, so the distinction had been drawn and never acted on.What does NOT ship is dropping the approval.
Networkis still set for every recognizedLocalServer, on purpose, andTestServingStillRequiresNetworkApprovalpins it. Two reasons:LocalServeryet. No policy or runner reads it, so classifying a serving command as local-only would not grant it a scoped host listener, it would only remove the approval it used to get. The command then runs under the default deny profile, a network namespace on Linux and deny-network on macOS, sopython -m http.serverandvitewould start without a prompt and be unable to serve anything to the operator.npm run devis matched by SCRIPT NAME, and the repository decides whatdevandpredevactually do; either can fetch before a port is bound. On Windows the approval gate IS the egress protection, so inferring "no egress" from a name there lets that egress run unprompted.So
python -m http.server, Vite, and the package-manager serving commands remain approval-gated exactly as before. Coverage is consistent across npm, pnpm, yarn and bun, direct and behindrun, becausenext devandnpm run devare the same intent and classifying one differently would be arbitrary.The unparseable fallback is kept a superset of this for the same reason: the POSIX parser rejects Windows shell syntax the invoked shell accepts, and a serving command written in a spelling it cannot read must not lose the gate its parseable form receives.
A host-listener capability is the follow-up this foundation is for: classification, an explicit policy decision, and platform runner support for reachable binding while outbound egress stays controlled. Until that exists end to end, the gate stays.
Verification
Cherry-picked onto
7f39a630and checked there:go build ./...,go vet,GOOS=linux go vetandgofmtall clean. Test failures are only the ones already failing on main and on this machine:TestEagerToolSchemaTokenBudget(inherited, #877 fixes it),TestAltScreenTranscriptScrollKeepsFooterFixed, and three doctor connectivity probes that need network.#808 keeps
zero sandbox exec, which @gnanam1990 called borderline, since it is the vehicle for exercising the principal path on a clean elevated machine.Summary by CodeRabbit
New Features
--full-autopermission mode, offered with Shift+Tab and confirmed with Ctrl+G.Bug Fixes