acpmux: no-ask start modes by inherit or an approved preset
A script, CLI or agent can now start a session in a mode that doesn't ask without the person approving each start, in two cases:
- Inherit. The caller is itself an acpmux session in that exact 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 per-run secret kept only in memory. It also getsACPMUX_SESSION_TOKEN_SOCKET, and the CLI sends the token only to that socket. - The token is read only from a unix-socket connection's first
initialize. - 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
- Person-approved preset.
_acpmux/presets {name, approveMode: true}from the person records a sha256 of the preset's contents.- The approval counts only while both the resolve and the spawn used exactly those contents, and only for the preset's own mode.
- A request that adds its own model, effort, policy or env needs the person; a folder is fine.
- Without the person, approving a preset's no-ask mode is refused.
A no-ask start mode never applies to an adopt without the person, because an adopt may find a session that's already running. A proven caller session that asks never gets the full-access default for the chats it starts. A refusal names where to approve: cmux NEXT on this host, or the preset to approve there once. error.data carries {reason, host, mode, preset}, and Web and peer clients never get the host name. The threat model and its residuals are at the top of hub/person.rs.
This is slice 2 of #20653's split, and it's the rest of that PR: (a), (b) and (d), the parts that were reviewed together. It builds on #21642, which has merged. Two changes from #20653's version:
tests/person_auto_start.rsruns in the hermetic test environment (daemon_env), whichhermetic_daemon_guardnow requires.- Its person-key pipe is close-on-exec (
std::io::pipe). With plainlibc::pipe, a daemon spawned by a parallel test could inherit another daemon's key write end. Two daemons holding each other's never got their key, and roughly one run in three hung. It passed 5 of 5 runs after the change.
Review fixes (e950712a169e)
- A request
routeon an approved preset now needs the person, like the request's own model, effort, policy or env, because a route swaps the provider's base URL and keys. - A profile's, preset's or request's env can no longer set
ACPMUX_SESSION_TOKENorACPMUX_SESSION_TOKEN_SOCKET. That env is layered aftercaller_env, so the spawn drops both keys from it. - The CLI's daemon readiness pipe is close-on-exec from creation (
std::io::pipe, which ispipe2(O_CLOEXEC)where the OS has it). Before, apipefollowed by a separatefcntlraced with a fork on another thread.
The regressions went in first, in 2a23d7b9555. On 116ab01ad85, cargo test -p acpmux --test person_auto_start gave 12 passed and 2 failed:
- the route request got past the gate (the handler answered "no route");
- the profile's forged token reached the agent.
On e950712a169e it gives 14 passed. The full cargo test -p acpmux --no-fail-fast fails only the same two tests that fail on feat-cmux-next, and clippy with -D warnings is clean.
Open question
An agent in a mode that asks can leave out its caller token and connect as an unproven unix-socket client. With the default start mode at full access, it then gets a full-access chat without the person, which undoes the inherit rule. Closing that means applying the default only to the person or app connections, which changes base behavior (cx-wx93i). That's waiting on Leo; nothing changes here.
Verification
-
cargo test -p acpmux --test person_auto_start: 13 passed, in each of 5 runs. -
cargo test -p acpmux --locked --no-fail-fast: every target passes except two:protocol::tests::protocol_ir_is_current: IR description drift from #21397.hermetic_daemon_guard: it flagsprompt_images.rs,complete_rpc.rsandsession_mcp_servers.rs.
Both fail the same way on
feat-cmux-next, and this change touches none of those files. -
cargo clippy -p acpmux --all-targets --locked -- -D warnings: clean. -
All of the above ran on Linux; macOS lanes run in CI.
Freeze WINDOW areas: CORE (cmux-tui/crates/acpmux and the acpmux protocol). No Cargo file changed, so not LOCK. The Air Blue coordinator merges this PR.
Changelog
Added: acpmux sessions can start new sessions in their own no-ask mode, and person-approved presets start in their mode without asking each time.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Lets a script, CLI, or agent start an acpmux session in a no-ask mode without the person approving each start, in two ways, and relays a peer's pending prompts to this daemon's watchers.
No-ask start modes
- Inherit: a caller that is itself an acpmux session in the same mode on the same harness family starts without an ask. The daemon proves the caller with
ACPMUX_SESSION_TOKEN, an HMAC of the session id keyed by a per-run in-memory secret, read only from a unix-socket connection's firstinitializeand sent only to the issuing daemon's socket. The prove-in-request env is acpmux's alone: a profile, preset, or request env never replaces either token variable. - Approved preset:
_acpmux/presets {approveMode: true}from the person records a sha256 of the preset's contents, counting only while both resolve and spawn used those contents and only for the preset's own mode. Presets gain amodefield,acpmux newa--modeflag, and a request that overrides model, effort, policy, env, orroutestill needs the person. - A no-ask start mode never applies to an adopt without the person, and a proven caller session whose mode asks keeps asking.
- A refusal names where to approve; Web and peer clients never get the host name.
Peer pending prompts
- A peer's
_acpmux/permission_pendingreaches unix-socket and app watchers of this daemon that did not attach the session. - Answers still go to the peer, which counts an allow only with its own person's proof; relays never go to a Web client or on to another peer daemon.
Also fixes a race where a daemon spawned by a parallel test could inherit another daemon's key pipe: the readiness pipe is now close-on-exec from creation.