acpmux: no-ask start modes by inherit or an approved preset; pending prompts reach peers
acpmux: approval paths for no-ask start modes that don't need the person each time, plus pending prompts that reach peered apps.
When a script or agent asked acpmux for a mode that doesn't ask, the request needed the person's approval. That approval was only visible in cmux NEXT on the same host, and the error didn't say where to give it. The threat model is written at the top of hub/person.rs.
- (a) Inherit. A session can start new sessions in its own current no-ask mode, on the same harness family.
- The daemon proves which session is calling. Each harness it starts gets
ACPMUX_SESSION_TOKEN, an HMAC of the session id keyed by a secret made at each daemon start and kept only in memory. - The token is checked on the connection's first
initialize, and only over the unix socket. - The caller's mode is read at request time, so a session the person moved back to asking inherits nothing.
- Peer and Web connections never have a caller.
- The daemon proves which session is calling. Each harness it starts gets
- (b) Person-approved preset.
_acpmux/presets {name, approveMode: true}from the person records a sha256 of the preset's contents.- The approval holds only while the preset still hashes to it, and only for that preset's own mode.
- Any edit to the preset drops it.
- Without the person, approving a preset's no-ask mode is refused.
- (c) Pending prompts across peers. A peer's
_acpmux/permission_pendingreaches this daemon's watchers that did not attach the session (via: "peer").- Answers still go to the peer, which counts an allow only with its own person's proof.
- (d) Refusal text. The error names where to approve: cmux NEXT on this host, or the preset to approve there once.
error.datacarries{reason, host, mode, preset}so an app can show localized text.
- Fits with the base start mode (cx-wx93i).
- A request that names a mode skips the default start mode, and (a) and (b) gate it.
- A proven caller session that asks never gets the full-access default for its chats: they start in the harness's own mode.
Review fixes
From the security review:
- Adopt plus a start mode. A start mode that does not ask, on a
session/newthat adopts, now needs the person. The request is refused before anything starts, because an adopt may find a session that is already running (ifLive: "open"), and only the person widens a running session. - Peer-relayed pending prompts. A peer's
_acpmux/permission_pendingis relayed only when all of these hold:- the session is one that peer listed as its own, and it is not a session of this daemon;
- the prompt is in the daemon run the peer named at its handshake;
- that run is neither this daemon's nor one another peer named.
It goes only to unix-socket and app watchers, never to a Web client and never on to another peer daemon. - Content binding (decision): not done. The decision proof covers the prompt's ids, not its text, and binding it to the text needs the app to hash what it shows. The checks above keep this daemon's own prompts, which the app on this host can prove, from arriving with a peer's text. The residual case is in the threat model.
- Preset approval only gated the start. One
session/newreads the preset's contents at three points: the request check; the resolve of model, effort and env; and the spawn of args, env and system prompt.- The resolve and the spawn each record the digest of what they used.
- The start counts the approval only if both digests equal the approved one. An edit that is put back between reads gains nothing.
- A preset with a mode or an approval never takes a pooled harness.
- The session records the approved digest. A respawn from other contents starts in the harness's own mode instead of replaying the approved one.
- Harness name. Documented: the approval covers the preset's
harnessstring, not the profile that name resolves to. - Overrides (decision): a request that adds its own model, effort, policy or env to an approved preset needs the person. A folder (
cwd) is fine. modeApproval. Documented: it is an unkeyed sha256, so same-uid writes to config.json are out of scope.- Host name. A Web or peer client never gets this host's name, in the refusal text or in
error.data.host. - Caller token. The daemon also sets
ACPMUX_SESSION_TOKEN_SOCKET. The CLI sends the caller token only when it connects to that socket.
Out of scope, and pre-existing on the base: a peer can send session/update for a local session id. Tracked as bead cx-r492l.
Second review
- A request during the start rebound the approval. The spawn digest was written before
spawn_lock, so a request on the session while an approved preset was starting could overwrite it.ensure_childnow records the digest it spawned with, underspawn_lock, and only when it starts a new harness. A live or re-adopted child never writes it. - The saved mode was cleared while the harness kept its no-ask mode. The saved mode is now dropped only when a new harness actually starts from other contents. For a live or re-adopted child, the saved mode still says what the harness runs.
- A peer can name a third daemon's run. Documented as a residual in
person.rsand below in open questions. Tracked as bead cx-i4rte. ACPMUX_SOCKETis made absolute, so a relative path cannot point the CLI and the daemon at different sockets.- Forgetting a mode now clears every mode field (
MODE_FIELDS), not onlymodeId.
Open questions for review
- Unproven unix-socket clients. With the base default start mode, any unix-socket client that is not a proven session gets a full-access chat without the person. That includes an agent in an asking mode that just leaves its token out. Applying the default only to the person or app connections would close that, but it changes base behavior, so it is Lawrence's call. Not changed here.
- Readable token. A caller token can be read by any process of the same user (
/proc/<pid>/environ,ps -E). It grants only that session's current mode, which its own agent can already use. Binding the token to the connecting process's ancestry is later hardening. - No cross-peer inherit.
- Binding a decision proof to the prompt text would need the app to hash the text it shows (see Review fixes, 2).
- A peer naming a third daemon's run. A peer can name, as its own run, the
personInstanceof another daemon the app enrolled directly. The app must refuse to prove avia: "peer"prompt whosepersonInstanceis a daemon it enrolled directly. This needs an app change (bead cx-i4rte). - Restoring a preset restores its approval. Reverting a preset to the exact contents the person approved makes the approval valid again.
Not in this PR
- The cmux NEXT notification for a peer-forwarded pending prompt.
- Live verification that a pending prompt shows in the pane on another Mac.
Tests
The tests are at the daemon boundary: tests/person_auto_start.rs (auto_start_*) and a peer test in tests/peer_integration.rs for the pending prompt (c).
- Red on 4814e6a (
auto_start_):left: "full-access",right: "normal". A chat started by an agent in an asking mode came up in full access. - Hosted Linux green on f04c419 for
auto_start_,peer,start_modeandpreset. - Review fixes, boundary tests committed red first (d3ba5e7):
auto_start_, red on Linux: 3 failed, 9 passed. An adopt moved a running session to bypass, a request model override started without the person, and a respawned edited preset came back in bypass.peer_pending, red on Linux: 3 failed, 1 passed (the legit relay). A forged prompt (local session, this daemon's run, unknown session) reached the watcher.
- Green on Linux at 0f34db0:
auto_start_: 12 passed (run 38592681245).peer: 75 passed (run 38600221583).preset: 26 passed (run 38601708940).
- Second review, boundary test committed red first (14f2ddb):
auto_start_a_request_during_the_start_never_rebinds_the_approval, red on Linux (run 38611185844). The start was refused withpermission.person_required. - Green on Linux at 68914f2:
auto_start_: 13 passed (run 38618842274).peer: 75 passed (run 38626137238).preset: 26 passed (run 38635193026).
- In the 68914f2 runs, macOS failed on a base clippy lint in
cmux-tui/tests/replace_unresponsive_owner.rs(unused qualification), which this PR does not touch. - After merging base (915952c, which picks up the base clippy fix from #20654), hosted green on Linux and macOS:
auto_start_: Linux 19 passed, macOS 44 passed (run 38640660645).peer: Linux 77 passed, macOS 101 passed (run 38643120684).preset: Linux 26 passed (Linux job rerun after a cancel), macOS 51 passed (run 38646576474).
- macOS was queued because the pool is starved, so these runs were cancelled after their Linux test job.
Changelog
- Changed: acpmux sessions can start sessions in their own no-ask mode, the person can approve a preset's mode once, and a pending approval reaches peered apps. The refusal names where to approve.
๐ค Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds approval paths for starting acpmux sessions in a no-ask mode without the person, relays pending approval prompts to peered apps, and keeps the motion system and Chief work the base merge brought in.
- Inherit. A proven caller session (an HMAC token the daemon places in its env, checked at the connection's first
initialize) can start sessions in its own current no-ask mode on the same harness family; claims in request params are ignored. The CLI sends its caller token only to the socket of the issuing daemon (ACPMUX_SESSION_TOKEN_SOCKET), andACPMUX_SOCKETis made absolute. - Approved preset. The person can approve a preset's no-ask mode once; the approval binds to the preset's contents via sha256, so any edit drops it. A request that adds its own model, effort, policy or env to an approved preset still needs the person.
- Pending prompts across peers. A peer's pending prompt reaches this daemon's watchers that didn't attach the session; answers still go to the peer and need its own person's proof.
- Refusals. Name where to approve โ the host's cmux NEXT or a preset โ with
host,modeandpresetinerror.data.
Security boundaries
- A no-ask mode never applies to an adopt, and approved-preset starts never take pooled harnesses; the approval counts only when both the resolved and spawned contents hash to the approved digest, recorded only on a cold start under the spawn lock so a racing request can't rebind it.
- The default full-access start mode never widens a caller session that asks.
- A peer's pending prompt is relayed only for a session the peer listed as its own, in the run the peer named at its handshake, to unix-socket and app watchers only.
Rode in from the base merge
- Chrome motion (hq-1d): closed-form springs on one
MotionClockper window,ui.animations.newTab(on by default) andui.animations.newWorkspace, bounce and Reduce Motion switches; a new tab grows in at its slot on thetabOpenspring, structural changes snap under Reduce Motion. Palette rows for "Window Style" preview the style live while highlighted. - Chief:
chief.profile.adoptmakes an existing agent participant a Chief record under its own id (refuses a bound participant, revokes its earlier credentials); a brain started by the supervisor gets the account's home; every Chief profile page call answers within a deadline on a relay connection to the current owner.labs.classicHomekeeps classic Home one release; the agent CLI card'sharness.update/harness.restartrun on this Mac's daemon with a gesture.