acpmux: no-ask start modes by inherit or an approved preset

Merged
TE
19d ago
+1631-79

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 gets ACPMUX_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.
  • 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.rs runs in the hermetic test environment (daemon_env), which hermetic_daemon_guard now requires.
  • Its person-key pipe is close-on-exec (std::io::pipe). With plain libc::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 route on 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_TOKEN or ACPMUX_SESSION_TOKEN_SOCKET. That env is layered after caller_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 is pipe2(O_CLOEXEC) where the OS has it). Before, a pipe followed by a separate fcntl raced 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 flags prompt_images.rs, complete_rpc.rs and session_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


View with [code]smith Autofix with [code]smith
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 first initialize and 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 a mode field, acpmux new a --mode flag, and a request that overrides model, effort, policy, env, or route still 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_pending reaches 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.