Chief: your own turns never ask; approvals are opt-in, Stop ends the turn, and an approval is a card

Merged
TE
17d ago
+1454-146

Since 10-20, every Chief turn waits for approval before each local effect. That includes the owner's own cmux chief -p on the same Mac and Chief's own tell and send. Four problems stack on top of that:

  • remote.autoApprove doesn't lift the gate.
  • cmux chief stop doesn't end a turn whose tool call is waiting.
  • The model re-asks for a denied tool in a loop (turns 146 and 167).
  • The approval shows up as a raw JSON bubble.

The CLI was not the cause. cmux chief -p sends as user_local over the owner socket, which is local origin. The cause is e68aa1b4b53: it gates every turn that reads the Home view, and the host reads Home on every turn (host.rs always sets home_view). The bubble blamed "a paired device" only because that text is hard-coded. a4bbe5da6a1 had also made asking the default for paired devices.

What changes

  • Gates are opt-in. Two new per-Chief settings, both off by default:

    • remote.ask ("Ask before acting on requests from other devices")
    • home.ask ("Ask before acting on what Home shows")

    With both off, the owner's turns from this Mac, from the CLI and from their paired phone run like local turns. remote.autoApprove: true still wins over remote.ask. A turn where another participant wrote always asks, which covers the shared-Chief case. The host refuses to loosen a gate during remote-origin work.

  • Chief's own tools never wait. That covers send, reply, react, stop, sleep, profile and spawn. A child spawned in a gated turn still gets the ask floor. tell and control also run without asking when they carry no prompt. In a gated turn, a tell, or a control that carries a message or an answer, still asks unless the target subagent runs with ask itself. That way a gated turn can't pass a command through an approve-all subagent.

  • Stop means stop. chief.stop cancels the session even while a tool call is waiting for approval (Interrupt::request_hard). Every later request in that turn is denied at once. If the model repeats a request that was already denied, it is denied again without a new card; after three repeats the turn is stopped. A newer message no longer denies a pending approval.

  • A readable approval card. The approval is now a conversation question part. It shows "Allow Chief to run `rm -rf target`?", a reason line ("Started from Leo's iPhone: …"), the arguments pretty-printed as a monospace preview, and Allow once / Deny buttons. The owner answers with question.answer (only the owner's answer counts) or by typing allow or deny. If the owner refuses the part, the same two lines go out as plain text. No JSON appears in the chat.

Who added the gate

Lawrence added the paired-device gate on 2026-10-05 (0654733226a, default ask). He made auto-approve the default on 10-06 (8f1b5b9c65b: "i dont want stuff to require my approval"). On 10-20 his security hardening made asking the default again (a4bbe5da6a1) and gated every turn that reads Home (e68aa1b4b53), because terminal titles, screens and URLs are text an attacker can write. All three went straight to feat-cmux-next; none came through a PR.

Takes back a4bbe5da6a1 and e68aa1b4b53 (both pushed directly on 10-20): together they put an approval in front of every effect of every owner turn, Chief's own messages included. Leo decided on 10-20 that the owner's own devices run like the Mac by default. Both protections stay available as opt-in settings.

Security analysis (risk:high)

  • Prompt injection through Home text is now an accepted default risk. With home.ask off, text in the Home block reaches every turn and runs with the owner's policy, as it did before 10-20. home.ask on restores the 10-20 behavior.
  • A stolen paired phone can act without approval until it is revoked. This matches the 10-06 default. remote.ask on puts approvals back.
  • Other participants still always ask, and only the owner approves. That holds for both the text answer and the card. A card answer is checked against the respondent the owner stamps, never against the request.
  • The newly allowed tools stay inside the conversation or the ask floor:
    • send, reply and react post only to the conversation that asked. send can attach local files, but in a non-owner turn the participant already reads that conversation (residual risk).
    • Children from spawn get the ask floor.
    • tell and control with a prompt still ask for approve-all targets.
  • No relay command, allowlist entry or socket method is added.

Tests

Command: cmux-ci run --class isolated --script scripts/ci/cmux-tui-rust-check.sh --arg optchat-chief --ref <sha> (optchat-chief tests, then clippy -D warnings, then fmt).

Red commit d5e3840 (new tests only, on base): 8 of the 10 new approval_gates tests fail. The two guard tests (home.ask on, remote.autoApprove wins) already pass on base.

The base also fails four existing tests:

  • crash::a_pending_turn_and_its_items_survive_a_restart_in_the_database
  • daemon_workspaces::a_subagent_workspace_is_located_by_its_public_id_and_session
  • turn_seal_binding::a_subagent_cannot_reach_owner_controls_on_the_shared_socket
  • remote_policy::a_non_owner_turn_asks_and_only_the_owner_may_approve_it

Head 30b89d3:

  • All of approval_gates (10) and remote_policy (9) pass.
  • clippy and fmt pass.
  • The same three base reds in crash, daemon_workspaces and turn_seal_binding remain. This PR doesn't touch them.

What tests/approval_gates.rs checks:

  • (a) An owner CLI turn with Home runs approve-all. home.ask turns the Home gate on. By default a paired-device turn runs like a local one.
  • (b) remote.ask gates a device turn, and remote.autoApprove wins over it. The gate can't be loosened during remote work.
  • (c) Chief's tools are allowed at once. A tell to an approve-all subagent still asks.
  • (d) Stop cancels a turn whose call is waiting, and later requests are denied at once. A denied request isn't asked twice, and repeats stop the turn.
  • (e) A newer message leaves a pending card alone. The test checks the card's line, reason, arguments and buttons, and that a question.answer allows the call.

tests/remote_policy.rs now runs with remote.ask on, and its default-case test is flipped.

Observed / Not run

  • Observed: the fleet runs above.
  • Not run: the Mac app and webview card. Mac Home still draws a question part as its prompt plus "1. Allow once / 2. Deny" text. MessagesLabHome/Cmux/HomeMapping.swift falls back to text, and AgentQuestionCardView isn't wired to the custom-row seam yet. The Claude-desktop-style card with buttons, the āŒ˜ā†µ hint and the answered receipt is a follow-up UI PR. This PR only emits the data that card needs.
  • Not run: a live repro. I didn't reproduce with an isolated --chief-home on a nightly build. Leo's Chief stays stopped.
  • Not run: other brains. The cloud brains (backend/) keep their own policy, and the shared corpus remote_auto_approve cases are unchanged.

Changelog

  • Fixed: Chief no longer asks for approval on your own turns (the cmux chief CLI, Home, your paired phone). Asking on requests from other devices or on Home text is now opt-in, Stop ends a turn that is waiting on an approval, and an approval shows as a short card instead of raw JSON.

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Chief turns no longer wait for approval unless you opt in: your own turns from the CLI, from Home, and from your paired phone run like local turns by default, with remote.ask and home.ask off. chief.stop now ends a turn even while a tool call waits, a denied request isn't asked twice, and approvals appear as a card with Allow once / Deny instead of raw JSON.

Key changes

  • Two new per-Chief settings, both off by default: remote.ask gates paired-device turns, home.ask gates turns that read Home text; remote.autoApprove still wins, and another participant's turn always asks.
  • The Chief's own tools (send without attachments, reply, react, stop, sleep, profile reads, spawn, and tell/control without a prompt) never wait; in a gated turn, send with attachments, profile writes, and tell/control with a prompt still ask unless the target subagent runs with ask itself.
  • chief.stop cancels the session even while a tool call waits, and a request denied once is denied again without a new card; three repeats stop the turn with one line saying so.
  • Approvals are a question card with a reason line, pretty-printed arguments, and Allow once / Deny; only the owner's answer counts, a newer message never answers a pending card, and a card answered in a side conversation settles the approval posted there.
  • No gate can be loosened while a turn asks.
  • The Mac app and webview render the card as text for now; the button UI is a follow-up.
  • Security tradeoff: with gates off, Home text and a stolen paired phone act without approval; the 10-20 hardening is now opt-in.