Skip to content

acpmux: a narrower policy holds on presets, resume, asides, handoffs and a deny-all default - #23639

Merged
teamleaderleo merged 8 commits into
feat-cmux-nextfrom
acpmux-policy-everywhere
Oct 21, 2026
Merged

teamleaderleo merged 8 commits into
feat-cmux-nextfrom
acpmux-policy-everywhere

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

#23572 put a chat with a narrower permission policy (ask, approve-reads, approve-edits, deny-all) in a mode that asks, but only on a plain session/new and on _acpmux/set_policy. Several other paths could still leave the chat in a mode that never asks, so the policy held nothing there. This PR closes those paths, at four points:

  • Every agent start (hold_policy_mode, run after cap_full_access on a cold or pooled start, and on a Claude Code start, which spawns in the policy's mode when its saved mode doesn't ask). This covers:
    • a preset's or folder profile's chat (a Codex preset with approve-reads ran in agent);
    • a profile that pins its mode;
    • a handoff target that keeps its harness's mode;
    • a resumed chat whose saved mode is full access;
    • a forked chat.
  • Every policy change, including acpmux's own.
    • Hub::set_policy now follows the policy, the same way _acpmux/set_policy already did.
    • An aside is forked from its chat, which copies the chat's mode, and is then set to deny-all. So an aside forked from a full-access chat answered its question in full access under deny-all. It now runs read-only on Codex and default on Claude.
    • A handoff target narrowed to its source policy follows the same way.
  • deny-all as the default policy (_acpmux/set_default_policy deny-all, or permissionPolicy: deny-all in config.json at startup).
    • It is a lockdown: every chat without a policy of its own follows it, live or stopped, and new chats start in its mode. The chats follow concurrently.
    • The default ask does not narrow anything. Chats still start in full access by design (Lawrence 2026-10-14).
  • Mode switches. A switch into a mode that does not ask, including a preset's start mode, is refused with data.reason: "policy_mode" while the chat's harness has a mode that keeps the policy. This covers session/set_mode and the mode config option, from any client. A harness with no such mode (OpenCode, pi) is left as before; it logs policy_unenforced.

Also:

  • The _acpmux/start_mode preview answers the policy's mode with source: "policy", for a preset's policy or a deny-all default.
  • The set_policy race from the acpmux: a narrower permission policy starts Codex and Claude in a mode that asks #23572 review is closed:
    • kill marks the chat closing until its agent starts again (or is readopted).
    • A chat being closed no longer counts as running just because its host still holds its record while it exits, so a set_policy in that window saves the mode instead of starting the chat again.
    • A smaller window is left: a kill that lands between follow_policy's running check and its set_mode still starts the chat again. Any client's set_mode has the same window today.
  • A profile that pins its mode in argv is always moved to the policy's mode, because the chat does not record the pinned mode until its first turn. A pinned mode that already asks (Claude plan, a Codex read-only sandbox) becomes the policy's mode, which also asks.
  • A chat that is moving when deny-all becomes the default is skipped. It follows the policy when its move ends without happening (an abort, or a begin that fails), before its queued prompts run again.

Changelog

Fixed: a chat with a narrower permission policy now stays in a mode that asks when it is started from a preset, resumed, forked as an aside, handed off, or locked down by a deny-all default policy.

No capture: daemon-only change with no UI.

Testing

Focused command: cmux-tui.yml dispatch, mode=focused.

  • Red at a788477 (tests only), run 39651918935, filter policy:
    • a_preset_chat_with_a_narrower_policy_starts_in_the_mode_that_keeps_it fails at tests/start_mode.rs:643: the Codex preset chat with approve-reads runs in agent;
    • a_policy_acpmux_sets_itself_moves_the_chat_out_of_full_access fails at :659: after Hub::set_policy(deny-all) the chat stays in agent-full-access;
    • deny_all_as_the_default_policy_holds_on_every_chat fails at :678: after set_default_policy deny-all the live chat stays in agent-full-access.
    • Every other policy test passes.
  • Green at d9d0e2c:
    • run 39652497071, filter policy: 108 passed, 0 failed, the three new tests included;
    • regressions: run 39652507007 aside 9/0, run 39652512044 handoff 45/0, run 39652517111 btw 13/0;
    • run 39652502106 _mode: 34 passed, 1 failed. The failure is transfer_guard::a_chat_with_no_asking_mode_moves_under_peer_control_but_not_under_web, a base red.
  • Red at a75f987 (test only), run 39659063257, filter pinned_claude: a_pinned_claude_chat_with_a_narrower_policy_leaves_full_access fails at tests/start_mode.rs:722. A Claude Code harness that pins --permission-mode bypassPermissions kept it under approve-reads.
  • Green at 52df039 (the Claude start path, plus untested changes for preset start modes and readopt; see Not run):
    • run 39659067571, filter policy: 109 passed, 0 failed;
    • run 39659083826, filter claude: 168 passed, 0 failed, the new test included;
    • run 39659072642, filter _mode: 34 passed, 1 failed, the same base red transfer_guard::a_chat_with_no_asking_mode_moves_under_peer_control_but_not_under_web.
    • (run 39659077913, filter start_gate, selected no tests.)
  • Red at ec1e964 (the move-abort test on a75f987's code: c068c4d plus 328b8ce's test fix), run 39663666308, filter deny_all: a_chat_whose_move_aborts_follows_a_deny_all_default_set_meanwhile fails at tests/start_mode.rs:762. After the move aborts, the chat is still in agent-full-access.
  • Green at 328b8ce (the abort follows the policy):
    • run 39663672035, filter deny_all: 12 passed, 0 failed. That includes the move-abort test and deny_all_in_config_holds_on_every_chat_from_startup (config.json deny-all, then finish_startup).
    • At e9c09c4, the same code as 328b8ce with the test's old transport: run 39662107360, filter policy, 109/0. Runs 39662112199 (transfer, 22/1) and 39662120630 (move, 43/1) fail only the base red transfer_guard::a_chat_with_no_asking_mode_moves_under_peer_control_but_not_under_web.
  • Green at bdc81bd (a failed begin goes through the same end_move as an abort): run 39666228156, filter deny_all, 12/0; run 39666231887, filter policy, 109/0. Run 39666222956, filter transfer, 22/1: only the base red transfer_guard::a_chat_with_no_asking_mode_moves_under_peer_control_but_not_under_web fails.
  • lint (linux) is red only on the base clippy redundant clones.

Observed / Not run

  • Observed: the runs above.
    • With the fake Claude Code, a harness pinned to bypassPermissions under approve-reads runs in default.
    • With the fake Codex bridge, a deny-all default moves a live chat and a stopped chat to read-only. The stopped chat stays read-only after its next prompt starts it again, and a new chat starts there.
    • The preview answers read-only with source policy.
    • session/set_mode to agent-full-access is refused with policy_mode.
  • Not run:
    • ask_aside and handoff_prepare end to end. Both now go through Hub::set_policy, which the test calls directly.
    • Preset start modes yielding to the policy in start_gate (the preset test covers the cold-start hold, not the start-mode refusal), readopt clearing closing, and an unpinned Claude chat resumed from bypassPermissions.
    • A move whose transfer_begin fails (a turn that doesn't stop within 20 s, a bundle error) while deny-all becomes the default. It goes through the same end_move as an abort, which the move-abort test covers.
    • The closing race on a hosted chat. I couldn't make the window between a kill and its host's exit deterministic in a test; the fix follows from the code.
    • A real Codex or Claude Code binary.
    • The _acpmux/start_mode preview for a web Codex chat (a remote chat answers no preview).

Agent session: cosmic-gray

🤖 Generated with Claude Code

teamleaderleo and others added 2 commits October 21, 2026 11:20
…olicy changes

A chat's narrower policy was put in a mode that asks only on a plain
session/new and _acpmux/set_policy. It now also holds:
- at every agent start (preset, folder profile, pinned profile, handoff
  target keeping its harness's mode, resume, fork);
- on every policy acpmux sets itself (an aside's deny-all, a handoff target
  narrowed to its source), since Hub::set_policy follows the policy;
- for deny-all as the default policy, which every chat without its own
  policy follows;
- against a switch into a mode that does not ask (refused, policy_mode);
- in the _acpmux/start_mode preview (source policy).
A chat being closed no longer counts as running because its host still
holds its record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d3d-4bad-7860-b096-c54282b4bec4&host=cosmic-gray
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 18:57 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review bdc81bd: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr23639 -f ref=acpmux-policy-everywhere -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base c8bfa7b; does not block

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by attachments-matrix on cosmic-gray · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-kxecr created for this PR by the cmux coordinator.

@blacksmith-sh

blacksmith-sh Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Found 9 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp1dqcmrev/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmp55nz9206/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
test_a_file_a_test_reads_selects_that_test (main.PullRequestTiers.test_a_file_a_tes
t_reads_selects_that_test)/
test_a_file_a_test_reads_selects_that_test (main.PullRequestTiers.test_a_file_a_tes
t_reads_selects_that_test)
View Logs
test_a_shared_module_selects_its_dependents (main.PullRequestTiers.test_a_shared_mo
dule_selects_its_dependents)/
test_a_shared_module_selects_its_dependents (main.PullRequestTiers.test_a_shared_mo
dule_selects_its_dependents)
View Logs

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

teamleaderleo and others added 2 commits October 21, 2026 12:07
…er a narrower policy

fake_claude now reports the mode it was switched to in its next init, as
Claude Code does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d3d-4bad-7860-b096-c54282b4bec4&host=cosmic-gray
…ig default and preset start modes

Review follow-ups:
- A Claude Code start returned before the policy's mode was held. It now
  spawns in the policy's mode, and a mode pinned in argv is moved after
  the start.
- deny-all written in config.json is followed at daemon start.
- A closed chat whose host is reached again is no longer marked closing.
- set_default_policy follows every chat at once, skipping chats being moved.
- A preset start mode that would not ask is skipped, not fatal.
- The refusal counts the configured asking modes (webAskingModes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d3d-4bad-7860-b096-c54282b4bec4&host=cosmic-gray
@cursor

cursor Bot commented Oct 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 19:09 — with GitHub Actions Inactive
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › Tier routing is red on 5 of 8 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 2 commits October 21, 2026 12:26
…r a deny-all default

Also pins the config.json deny-all lockdown at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d3d-4bad-7860-b096-c54282b4bec4&host=cosmic-gray
A deny-all default skips a moving chat, and nothing followed it after an
aborted move, so it kept running in full access. The abort now follows the
chat's policy before its queue runs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d3d-4bad-7860-b096-c54282b4bec4&host=cosmic-gray
@cursor

cursor Bot commented Oct 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 19:29 — with GitHub Actions Inactive
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 19:40 — with GitHub Actions Inactive
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 19:58 — with GitHub Actions Inactive
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent, correctness first): SHIP at bdc81bd

Earlier rounds found holes and each was fixed: the Claude start path, a deny-all default in config.json at startup, readopt, preset start modes, a move that aborts, and a move whose begin fails. This last review checked every write to moving. Every path that ends a move without the chat leaving now applies its policy: begin failures, aborts by a client or by the stalled-move watcher, and a restart's restore. Import and commit are correct as they are.

Not tested, and disclosed in the body:

  • a failed begin (it shares end_move with the abort test);
  • preset start modes yielding to the policy;
  • readopt clearing closing;
  • the small window where a kill lands between the running check and set_mode.

Base reds, not caused by this PR:

  • transfer_guard::a_chat_with_no_asking_mode_moves_under_peer_control_but_not_under_web;
  • lint (linux): clippy redundant clones.

@teamleaderleo
teamleaderleo merged commit 62ce64f into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the acpmux-policy-everywhere branch October 21, 2026 20:13
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…pane prompt

Red at this commit: set_restricted holds spawn_lock while set_policy's
follow_policy (from base #23639) takes it again, so the prompt hangs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d41-f5e8-7631-b4aa-a544e3ef96a8&host=cosmic-gray
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
set_policy now moves a mode that does not ask under spawn_lock
(follow_policy, #23639); set_restricted held that lock and the turn lock
across the call, so a chat that had run in full access hung at the prompt
that restricted it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d41-f5e8-7631-b4aa-a544e3ef96a8&host=cosmic-gray
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 25, 2026
…it drive a remote chat in Full access (#25038)

* acpmux: failing tests, a paired machine cannot be let run chats in Full access

The two remote_guard_codex_bridge tests red on base since #23572/#23639
become two states: with a paired machine's Full-access setting off (and on
the shared dashboard token or a peer token, which name no machine) a chat
the Web started stays in a mode that asks; with it on, that machine's next
request drives the Codex bridge in every mode. New: a machine without the
setting stays refused while one with it is allowed; turning it off or
revoking the machine counts for its next request; only the person turns it
on, for a client machine. Scaffolding: the setting on the device record,
_acpmux/pair_set, the CLI, and the device id on the connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: a paired machine the person lets run chats in Full access moves a remote chat out of ask

Leo: "#2 seems reasonable i think... it's a gate that you can enable for
your phone, which is an attack vector you opt in to".

A chat a remote connection starts has the policy ask, which since #23572
holds it in a mode that asks, so no Web client could put the Codex bridge
in agent-full-access. A client machine whose Full-access setting is on
(_acpmux/pair_set, on only from the person) may now do that for a chat a
remote connection started, for its own requests held to the Web's rules.
Deny-all still holds, and a chat started on this machine keeps its
policy's mode. The setting is read on every request; turning it off or
revoking the machine puts every such chat back in its policy's mode and
stops its turn before the Mac's request is answered, and a switch that
raced with that is put back too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: a chat in Full access by a machine's setting is driven only by a request it covers

A prompt, a steer or a queued prompt's dispatch on a chat a remote
connection started whose mode its policy no longer holds (only the
Full-access setting moves one there) needs the request's own machine to
have the setting now: the shared dashboard token and other machines are
refused (remote.full_access_device). An edit of a queued prompt keeps its
machine only when the same machine edits it; a restored prompt names none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: rustfmt, and the Full-access hold reads only the chat's record

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: review: only the granted machine changes a Full-access chat's turn and queue; a revoke stops a Web turn before its prompt

- the remote guard holds cancel, queue edits and removes, and permission
  answers on a chat in Full access by a machine's setting to that machine,
  and queue_edit checks again before it changes the queue;
- turning the setting off marks a running Web turn cancelled before the
  chat goes back to its policy's mode, and the turn checks the setting
  again just before its prompt goes out;
- a queued prompt sent now as a steer, and a dispatch's prompt, keep the
  machine that sent them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: rustfmt the review test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

* acpmux: review: a restart puts chats moved by a Full-access setting back; queue_remove checks again before it removes

- at daemon start, after the agent hosts are adopted, every chat a
  machine's setting moved out of its policy's mode goes back (which
  machine moved it is not kept, so it fails closed), and an adopted Web
  turn in such a mode is floor-cancelled;
- _acpmux/queue_remove checks the hold again after its log wait, right
  before it removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

3 inactive deployments
artifacts — bdc81bdd Deployed Oct 21, 2026 by teamleaderleo
sentry-dsyms — bdc81bdd Deployed Oct 21, 2026 by teamleaderleo
cmux-tui-merge-tree — bdc81bdd Deployed Oct 21, 2026 by teamleaderleo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant