Skip to content

acpmux: a session an agent starts inherits its starter's policy and mode, never wider - #25155

Merged
teamleaderleo merged 12 commits into
feat-cmux-nextfrom
hb-acpmux/children-inherit-mode
Oct 25, 2026
Merged

teamleaderleo merged 12 commits into
feat-cmux-nextfrom
hb-acpmux/children-inherit-mode

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, word for word (2026-10-25): "Also, like, why would you, like, it shouldn't sub-agents just inherit the bypass permission situation? I don't know. I feel like it should. Maybe is that Claude, or is that us doing it wrong, or doing it kind of strangely?"

Is it Claude or us?

Partly each. For the prompt Leo saw, mostly Claude Code's own guard plus our default policy. This PR does not stop that exact prompt.

  • The mode was already inherited. cosmic-gray/claude-sr2/whats-new-video (a sub-session of cosmic-gray/claude-sr2) ran in bypassPermissions, the same as its parent. settle_subsession copies a same-family parent's mode (hub/subsession_guard.rs).
  • Claude Code asked anyway. Event seq 243 of that chat is Claude Code 2.1.307's own can_use_tool, sent while in bypassPermissions:
    "decision_reason": "Dangerous rm operation on statically-unresolvable target: /private/tmp/claude-501/.../scratchpad/f/*", "classifier_approvable": false, for cd ~/scratch/whats-new-video/reel && rm f/*; ffmpeg ... sheet.jpg. The //Users/leoli/scratch/... rule in the card is Claude Code's own absolute-path syntax, from its permission_suggestions.
  • acpmux sent it to a person because nobody set a policy. The child's acpmux policy was null, which means the daemon default ask. handle_permission_for maps that to no auto-answer (hub/permissions.rs:503, PermissionPolicy::Ask => None). The parent was also null/ask, so it would have been asked the same thing. The app's "Full access" sets only the harness mode, never the acpmux policy.
  • What we did wrong, which this PR fixes: a chat an agent starts with acpmux new|run from its shell, or with a local dispatch, did not inherit the agent's level. keeps_harness_mode (hub/start_gate.rs) gave it the person's start level, or the harness's own mode under an asking caller. It never got the caller's policy: NewRequest.policy was only the request's value or the family default (hub/lifecycle.rs, policy.or(defaults.policy)). A sub-session of another family got no mode at all under a parent that was not in full access (settle_subsession, else { None }).
  • Not changed here: making acpmux auto-approve requests Claude Code raises in bypass, and making app "Full access" set approve-all. Overriding the harness's own destructive-operation guard is a security decision for Leo; the coordinator is raising it.

Claude Code's own Task subagents already run in the parent's permission mode. Our launcher's children now do too.

Behaviour now, per starter

A session an agent starts with acpmux new|run from its shell, a local _acpmux/dispatch, or start_session / acpmux new --parent. The starter is the session proven by its caller token (Conn::caller). Each field the start leaves out is inherited on its own.

Starter Child policy (none asked) Child mode (none asked), same family Child mode, another family
Full access + approve-all approve-all the starter's full-access mode that family's full-access mode
Full access, own policy null (daemon ask) null (daemon ask, as the starter runs) the starter's full-access mode that family's full-access mode
approve-edits approve-edits the policy's mode (policy_mode.rs) the policy's mode
ask (own), or an asking mode ask (own), or null the starter's exact asking mode that family's asking mode; refused if it has none
no proven caller (plain CLI, forged token, peer, Web, LocalApp, the person) unchanged unchanged unchanged
a session a remote connection started (remote_origin) unchanged unchanged (an asking caller keeps the harness's own mode) unchanged
  • Never wider. Once the child has started, it is held to the starter as the starter is then (Hub::hold_to_starter). An inherited policy laxer than the starter's live policy is narrowed to it. The starter is read again after each round: one that changed meanwhile is held to again, and after 4 rounds of change the start is refused (start.starter_changing). The harness is held by Hub::hold_mode, the harness half of hold_subsession, split out unchanged: it is moved to an asking mode, or the start is refused and the child closed.
  • Family defaults. A family or preset default policy wider than the starter's effective policy is not inherited.
  • Toggles and the setting. A toggle such as plan is not copied. Full access is not copied while sessions.allowFullAccess is off, and forward_as refuses that switch anyway.
  • An explicit --mode that is wider is refused. This was already enforced: start_gate.rs start_mode_check returns permission.person_required unless the starter runs that exact mode. Tested.
  • An explicit --policy that is wider is NOT clamped (open gap). An agent caller can still pass --policy approve-all from an ask starter, because session/new with a policy other than ask or deny-all has been open to the unix socket since before the mode gate (hub/person.rs:218). Measured on 2026-10-25 with acpmux ls --json: about 1,300 live depth-0 sessions run approve-all (472 on huge-blue, 362 on cmux16, 158 local, 153 on cosmic-gray, plus the GCP lanes). Many were started that way by coordinators whose own policy is null. Clamping would move them to ask and send their asks to Leo, so it is left to a separate decision.
  • Remote starts. remote_guard's rules are unchanged (acpmux: a paired machine's Full-access setting (off by default) lets it drive a remote chat in Full access #25038, acpmux: a refused remote new says why and names the command that works #25049 read): a Web or peer connection has no caller, and a remote-origin chat is never a starter.

Changes

  • hub/start_mode.rs: the "Inherit" rule (module docs), start_mode_for, inherited_mode, inherited_policy, own_policy, and inherited_full_access_is_cmux_only_if_theirs. An inherited full access counts as cmux's own (trust_wait.rs) only when the starter's does.
  • hub/lifecycle.rs: NewRequest.starter picks the policy and the start mode, then hold_to_starter checks against the live starter. Both steps are Box::pinned so new_session fits a debug build's 2 MiB stack.
  • hub/start_gate.rs: starter_of; keeps_harness_mode defers to inheritance for a starter.
  • server/requests.rs: session/new passes the proven caller as the starter. A local dispatch goes through the same path.
  • hub/subsession_guard.rs: another family's sub-session uses the parent's level (start_mode_for) instead of the person's start level or none; hold_mode split out of hold_subsession.
  • README and skills/acpmux/SKILL.md: the rule, and why some requests still ask in full access.
  • tests/person_auto_start.rs: an asking caller's child now starts in the caller's own asking mode (strict) instead of the harness's (normal).

Tests (red, then green)

tests/inherit_start.rs, on the fleet (cmux-ci test).

  • Red on base (92cf6f4): 6 of 8 failed (full-access child, cross-family full-access child, approve-edits/ask starters, explicit wider mode, dispatch, cross-family sub-session). The no-proven-caller test and the cross-family asking sub-session test passed, as intended (unchanged behaviour).
  • Green at head: 13/13, three runs out of three. This includes the review additions: a family with no asking mode under an asking starter is refused; a wider family default is not inherited; one named field inherits the other; a starter that narrows while its child starts holds the child to it (red at 5f8a5bc: left: "approve-all", right: "ask"); a starter that narrows during the hold holds the child to it (red at 9a1883f, same values; FAKE_SET_MODE_DELAY_MS is new in fake_agent.py).
  • The shell-command case: a Full-access approve-all agent's child runs ask: rm f/*; ffmpeg ... and gets chose yes from the policy, with no person.
  • person_auto_start, subsessions, subsessions_safety, subsessions_delivery, subsessions_live, full_access_subsessions, start_mode, subsession_trust_inherit and hub_integration (401) pass. cargo fmt --check and clippy -D warnings are clean.
  • Full suite: daemon_chat_open::a_long_chat_opens_on_its_newest_page_and_scrolls_to_its_first_message fails the same way on base. session_pool::pooled_sessions_end_with_the_daemon_and_after_a_crash is also red on base in the full suite, and flaky on its own (1 of 2 runs failed on this branch).

Review

Codex gpt-6-astra at xhigh (hb-inherit-mode-review, its own read-only checkout). The verdicts were BLOCK, BLOCK, BLOCK, SHIP at 33999af, then SHIP at the final head after a line-neutral server/requests.rs change (kept at its base size for the god-file ceiling), with "Reviewed head: 04cc4de". Each finding was fixed with a test: a cross-family child under an asking starter, a remote-origin starter, a wider family default, and two starter-narrows-mid-start races. One finding was not taken: "inherit only when both fields are absent". The spec is per field, and an explicit policy is the open gap above.

CI reds that are not this PR's: Script tests (release-compile-derived-data.test.sh) is red on unrelated PRs too (runs on rail-opus-rail-minimap-setting and cmux/stop-says-stopping); release-notary and Guard status are base reds; app compile failed at "Set up runner".

No new timers or observers. hold_to_starter loops at most 4 rounds, then refuses the start.
No capture: daemon behaviour only, nothing visible changed.

Agent session: big-red
🤖 Generated with Claude Code

teamleaderleo and others added 9 commits October 25, 2026 14:08
…ode, never wider

A start with no policy and no mode from a proven agent session (its caller
token: acpmux new|run from its shell, a local dispatch) now gets that
session's own policy (or a narrower family/preset default) and its mode; on
another family the same level (full access, asking, or the harness's own).
Sub-sessions of another family now map the parent's level too, instead of
the person's start level (under a full-access parent) or no mode at all.
A start with no proven caller is unchanged.

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
… is now, clamp a wider family default, leave remote-origin starters out

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
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 25, 2026 22:30 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 04cc4de: 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=pr25155 -f ref=hb-acpmux/children-inherit-mode -f platform=macos -f backend_mode=local

web and size checks: green

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by hb-acpmux on big-red · session

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 25, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

@blacksmith-sh

blacksmith-sh Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Found 14 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpegjf9tfj/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpuvg5adr2/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
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'/
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'
View Logs
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'/
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'
View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
nightly.yml prepare-nightly-variant: 'Package prepared app for signing' is not in list/
nightly.yml prepare-nightly-variant: 'Package prepared app for signing' is not in list
View Logs

...and 4 more test failures. View all on Blacksmith

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / nightly-guards / workflow-guard-tests / release-notary is red on 17 of 21 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

Copy link
Copy Markdown
Collaborator Author

cmux-next fast gates / post cmux-next/fast-gates is red on 17 of 19 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
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 25, 2026 23:22 — with GitHub Actions Inactive
@teamleaderleo
teamleaderleo merged commit 304506c into feat-cmux-next Oct 25, 2026
141 of 147 checks passed
@teamleaderleo
teamleaderleo deleted the hb-acpmux/children-inherit-mode branch October 25, 2026 23:36
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
lane_reports test: the stand-in harness variants are of the starter's family, so a lane a fake parent starts inherits its mode (base #25155 refuses a start that would ask less).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a175c0-3004-7a40-8069-4149d94915ab&host=huge-blue
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
The merge dropped its only declaration with the saved-pick code; #25155's
inherit path still skips a toggle mode such as plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a175da-93d2-7310-8727-b20e1d89afe5&host=huge-blue
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
… a running chat (#24021)

* Test: a mode picked in a chat does not become the new-chat default

Leo 2026-10-21: "i think starting in full access is ok cuz the default for new chats != whatever u just temp set in an existing running chat"; "u can set default in other ways".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d18-a2fb-7ac0-84d5-432ffea53b8b&host=Air-Blue

* New chats start at the default, not the last mode picked in a running chat

A pick applies to its own chat. Settings, MDM/team values and config.json
defaultPermissionMode still set the default. A leftover lastPermissionMode
from an older build is ignored.

Leo-ask: ask-16f0b7197f

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d18-a2fb-7ac0-84d5-432ffea53b8b&host=Air-Blue

* Regenerate the settings exports for the per-chat mode help text

The help text change in AgentDefaultsSettingsSchema.swift and its catalog
was never exported, so the defaults stamp failed. These are the files the
PR autofix regenerated on the Mac for this head (its patch step came out
empty, so it pushed nothing); every stamp entry matches the head sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* acpmux: keep TOGGLE_MODES for the sub-session inherit path base added

The merge dropped its only declaration with the saved-pick code; #25155's
inherit path still skips a toggle mode such as plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a175da-93d2-7310-8727-b20e1d89afe5&host=huge-blue

* Regenerate the settings stamp and docs settings reference

The stamp comes from the PR autofix patch (run 40256227863), applied with
pr-autofix-apply.sh; the docs reference from export-next-docs.ts --settings,
which also picks up base's agentPane.display.environment.placement row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a175da-93d2-7310-8727-b20e1d89afe5&host=huge-blue

---------

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

This branch had an error being deployed

1 failed and 2 inactive deployments
artifacts — 04cc4de0 Deployed Oct 25, 2026 by teamleaderleo via publish to R2 #52918
sentry-dsyms — 04cc4de0 Deployed Oct 25, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #52918
cmux-tui-merge-tree — 04cc4de0 Deployed Oct 25, 2026 by teamleaderleo via publish the merge tree #5796
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