cmux-next: a switch for full-access agent modes

Merged
TE
15d ago
+1577-40

Summary

Settings > Agents > Agent Sessions gets Allow Full-Access Modes (sessions.allowFullAccess), on by default. On keeps today's behavior and Lawrence's 2026-10-07 call: no sheet for full access, the gesture is the consent.

Turned off, the agent pane's transport refuses a switch into a mode that skips approvals (bypass permissions, full access, yolo, auto-approve) before the frame reaches acpmux. That covers both session/set_mode and the mode config option. The page then shows "Full-access modes are turned off in Settings." Other modes are unaffected.

  • acpmux enforces it for every chat and client. It reads sessions.allowFullAccess from the settings file, the same way it reads agents.defaultPermissionMode. While the setting is off:
    • A new chat never starts in full access, whether the latest pick, config.json, the setting or the default chose it.
    • Hub::forward refuses session/set_mode to a full-access mode, and session/set_config_option whose value is one, under any option id. This applies to the app, the CLI and scripts alike (reason: full_access_off).
    • A resumed chat doesn't get full access back. A restarted ACP chat skips a saved full-access mode or config value, and if its harness restores full access on load anyway, acpmux sends it an explicit mode that asks (the family's asking mode, else its first mode that doesn't skip approvals). If it offers no lower mode or refuses the switch, the agent stops and the chat stays idle (full_access_off) rather than running in full access. A restarted Claude chat, which respawns with its saved --permission-mode, runs in default instead, and its recorded mode says so.
    • A chat taken from the pool gets the same check as a cold start, so a harness whose own settings start it in full access (not its argv) is moved to a mode that asks, or its agent stops.
    • When a restart is refused, acpmux also ends a host that outlived the terminate grace, the way abandon_cold_start does, so the next request can't re-adopt it in full access. A host that was already live and re-adopted is a continuation, like any chat that was live when the setting went off.
    • An agent's sub-chat under a parent still in full access starts at the person's start level instead of inheriting full access (inheriting was refused, which killed the new sub-chat).
  • One rule for which ids are full access. acpmux and the app both read the markers in cmux-tui/spec/full-access-modes.json: lowercase the id, drop -, _ and spaces, then it's full access if it contains bypass, fullaccess, yolo, dangerous, autoapprove or skippermissions. So danger-full-access and auto-approve are refused by the CLI too, while Codex's full-auto (which still asks), fullscreen or full-context are not. FullAccessModeRuleTests fails if the Swift copy drifts from the spec. If the spec fails to parse, acpmux treats every mode as full access, so it refuses switches while the setting is off.
  • Fail modes:
    • An unreadable settings file fails closed, so no full access.
    • With no settings controller wired in the pane, the Swift fast path allows, but acpmux still enforces the file.
    • No settings file at all means allowed. That's the default (on), the same as a file without the key.
  • What this setting does not cover:
    • A harness profile that pins a mode in its own argv (for example --dangerously-skip-permissions). acpmux sets no mode over such profiles.
    • Turning it off doesn't move chats that are already in full access. They stay there until their next mode switch or restart, and both of those are refused or downgraded. Each chat's mode picker shows its current mode.
    • It's a user setting, not a sandbox. An agent with shell access can edit or delete ~/.config/cmux/cmux-next.json, and deleting it turns the setting back on (a missing file means allowed, unless the classic cmux.json turns it off). acpmux reads only that file, not managed preferences, so this doesn't stop an agent that runs commands as you. It keeps an agent from switching itself into full access through the protocol.
  • The mode picker's warning color (unrestricted in ComposerPickers.tsx) uses the same markers and folding, so full-auto draws plain and skip-permissions draws as a warning.
  • The transport reads the setting on every frame, so a change applies at the next mode switch. AgentPanePageSettings wires it to every pane.
  • CmuxNextApp doesn't grow. AgentPanePageSettings now follows its three plain values through one observe helper instead of three copies of the same Task, and that saving covers the new wiring.
  • Agents can't set the key (refused as destructive).
  • New strings are translated into all 21 locales in both the settings and agent-pane catalogs.

Testing

  • acpmux, locally on Linux: cargo test -p acpmux --test full_access_setting --test full_access_resume passed.
    • full_access_setting covers a new chat, set_mode, a non-mode config option, ids from the shared rule (danger-full-access, auto-approve, Bypass Permissions, dangerously_skip_permissions), an unreadable file, and turning the setting back on and off.
    • full_access_resume turns the setting off after a Claude chat and a Codex chat ran in full access, then restarts both harnesses. Neither comes back in full access. Without the resume fix, the Claude chat came back in bypassPermissions (red at 7baac1b0). --test start_mode_settings --test start_mode (existing) also passed, and cargo clippy -p acpmux --tests is clean.
  • The fleet export step built the package and its tests, and the regenerated settings exports are committed (d92aba24).
  • package-test-lane.sh suite at d92aba24 passed AgentPaneFullAccessRelayTests (a full-access mode refused while off and never reaching acpmux, other modes kept, on lets it through, and the picker-rule table), plus FullAccessSettingTests, AgentPaneRelayedAllowTests, SettingsSchemaExportTests and ManagedPreferencesManifestTests.
  • Follow-up commits, on huge-blue (Linux) at the branch head:
    • cargo test -p acpmux --test full_access_subsessions --test full_access_resume --test full_access_setting --test subsessions --test start_mode --test start_mode_settings: all pass; cargo clippy -p acpmux --tests and cargo fmt --check clean.
    • Pool (greptile): cargo test -p acpmux --test session_pool a_pooled_chat_that_started_in_full_access red at 27727eb9 (taken in full-access), green at f116ec79. At f116ec79 these all pass on huge-blue: session_pool (14), full_access_resume, full_access_setting, full_access_subsessions, start_mode, start_mode_settings, plus cargo fmt --check and cargo clippy -p acpmux --tests -D warnings. subsessions failed once under load 40 (a closed grandchild read disconnected), then passed 3 of 3 with the fix and 3 of 3 without it, so it's flaky and unrelated to this change.
    • Red before each fix: full_access_subsessions (the child was refused full_access_off, at 6ba80df4), full_access_resume (the restored chat stayed in full-access, at 2fbc88de), full_access_setting (full-auto refused, at 35c2e754), full_access_resume's no-lower-mode harness (restart reported success, at de35cfdb).
    • webviews: bun test test/transport-errors-full-access-off.test.ts test/new-tab-folder-keeps-pick.test.ts test/model-picker-picks.test.tsx test/access-chip-pending-warning.test.tsx 36 pass; bunx tsc --noEmit -p . clean; vp fmt --check clean. The refusal tests failed on da6b4cf1 (acpmux's raw English message, and no notice for a queued pick). web react-apps-check fails on base at the eager-JS budget step before these run, so they were run here.
    • The cmux-tui tree key now covers schemas/settings/source/subsessions.json, which acpmux embeds; check_cmux_tui_tree_inputs.py was red on base for it.
  • Settings exports regenerated on a fleet Mac (cmux-ci step c65029c7f5428c2bde9a2df1, run-swift-testing-suites.sh export-settings) at dcec4ef6.
  • CI at 87bbfeeb (before the latest base merge): every red is also red on base feat-cmux-next (a827da19e2) or on #22149, a test-only PR on the same base:
    • cmux-next checks: the same six steps fail on #22149. Literal state root in remote_host/config.rs, NSWorkspace.open in HomeHostView.swift, missing translations for agentPane.error.daemonRefused/sidebar.doc.*, raw font sizes in HomeFirstRunSwitchCard.swift, syncMotion.gallery.tsx importing a missing streams.json, and build-cmux-tui-client.test.sh ("an nx-remote job did not build"), which fails the same way on base a827da19e2 on huge-blue. None of these files is in this PR. Its tree-key-embeds.test.sh is red on base and green here.
    • web / react-apps-check: markdown page eager JS 1,407,890 of 1,400,000, the same number as base.
    • nightly-guards / Guard status (release-notary): red on every recent base commit.
    • cmux-next daemon tests: one Swift test, BranchStateResourcesTests.ephemeralStatusScreenAndTabStateReachTheRecords, timed out waiting for a pinned tab. This PR doesn't touch branch state or cmux-tui tabs.
  • Not run: a failing test for the lingering-host cleanup on a refused restart (936c4f57). It needs a host that ignores Terminate past the grace, which the fake agent can't do yet. FullAccessModeRuleTests with the new full-auto/fullscreen rows with the new full-auto/fullscreen rows (Swift; CI runs it), and a live tagged build.
  • The defaults guard will ask for a call on sessions.allowFullAccess = true. The coordinator approved on-by-default under Leo's "you pick, we toggle" rule.

Risk tier

  • Tier: normal under feat-cmux-next's current scripts/ci/risk_tier.py (agent review plus the relevant checks; no person's look needed). The branch's older copy graded it high, so the high-tier lines are here too.
  • Review: SHIP at 6affa7996b453f535f877dc62085f0d013680ceb (subagent, correctness first). The first pass at d300a965 found the argv-pinned profile bug (greptile thread), fixed in 6affa799 and re-reviewed.
  • Runtime test: cargo test -p acpmux --test session_pool --test full_access_setting --test full_access_resume --test full_access_subsessions on huge-blue at 6affa7996b45 (session_pool 16/16, including the new a_pooled_chat_pinned_to_full_access_by_its_argv_keeps_it_when_off).
  • Not UI: the 6affa799 fix is acpmux-only and changes no visible surface; the Settings row and refusal notice are unchanged from earlier heads.

Changelog

Added: Settings can turn off full-access agent modes (bypass permissions, yolo), so cmux refuses to switch an agent into them.

🤖 Generated with Claude Code