cmux-tui: agent message delivery pump, launch credentials and unsend (agent messaging 4/6)

Merged
TE
21d ago
+2109-207

Agent messaging 4/6 (cx-55d27). Agents' messages now actually reach acpmux recipients without anyone re-running cmux agent message, acpmux agents send as verified senders, and a sender can take a message back.

What changes

  • Launch credentials for acpmux agents. When acpmux spawns an agent session, it calls credential.mint {acp_session} on the cmux daemon its own CMUX_TUI_SOCKET names. It then sets CMUX_LAUNCH_CREDENTIAL, plus that same CMUX_TUI_SOCKET, in the agent's environment.
    • Inherited, preset and Chief credential variables are always removed first. This now happens inside launch_credential::apply, so acpmux/src/agent.rs shrinks from 1179 to 1175 lines.
    • If no daemon is set, none answers, or the answer takes longer than 2 s, the agent starts without a credential and its sends stay unverified. Launch never blocks on this.
    • The daemon's own acpmux starter now passes its socket.
  • Behavior change: acpmux agents act as AcpSession, not the local user. An agent's cmux calls now reach the daemon as AcpSession, wherever any call checks the actor. For example, workspace_status.set with owner.pid is refused for them (state/router.rs status_set_meta: trusted local callers only), as it already is for terminal agents.
  • Daemon delivery pump (mux/agent_message_pump.rs). This is the one acpmux delivery path.
    • It sends a queued acp:<id> delivery to the session as a prompt whose id is the message id, with _meta.acpmux.provenance.
    • It hands over and records each delivery under the lock unsend takes.
    • When acpmux accepts, the delivery becomes delivered (via acp.prompt). When acpmux refuses, it becomes failed.
    • A prompt acpmux never accepts (timeout, or the link closes after the send) stays queued with the attempt counted. acpmux runs a prompt id once, so the retry is safe.
    • Each pass reads only queued acpmux deliveries, in SQL, 200 per page, with a (sequence, position) cursor. Queued terminal messages can't hide them.
    • It wakes on send, release, daemon start and each agent-chat-watch reconnect. While a delivery is waiting it also wakes on a backoff timer: 1 s, doubling to a 60 s cap, with no cap on the number of retries. Waiting means acpmux is down, doesn't run the session yet, refuses with transfer.*, or never accepted the prompt.
    • Held deliveries wait for agent.message.release.
  • The CLI no longer hands over to acpmux. cmux agent message waits up to 15 s for the pump and reports a delivery that failed or is still waiting. Previously the CLI prompted acpmux outside the delivery lock and recorded the result afterwards, so an unsend could record unsent for a message acpmux then ran. Codex terminal delivery is unchanged.
  • agent.message.unsend {message_id, recipient?} returns {outcome, outcomes:[{recipient, outcome, error?}], message}. The outcome is unsent, retracted, too_late or failed.
    • Who (fail closed, coordinator decision, pending Leo): only the verified sender, or the person in the verified cmux app (Frontend), may unsend.
      • A plain local connection is refused (operation.failed, reason unsend_needs_app), as agent.message.release refuses it. Any local connection may be an agent that dropped CMUX_LAUNCH_CREDENTIAL.
      • Another agent is refused with unsend_not_sender. There is no time limit.
      • Alternative for Leo: the originally approved default (1) also allowed the plain local user (User{local}). That lets an agent that drops its credential unsend anyone's message. It's a one-match-arm change if Leo prefers it.
    • Replays are checked first. A retry under the same key replays, and a reused key or a stale expected_revision is refused, before acpmux is asked anything.
    • Each acpmux recipient's outcome is acpmux's _acpmux/prompt_withdraw answer, and every answer commits:
      • A refusal is that recipient's failed, with error.reason withdraw_refused and error.acpmux_reason.
      • A silent acpmux on a delivery a hand-over may have reached is failed, with acpmux_unavailable. That covers delivered, failed, or queued with attempts. It is never unsent.
      • Only a queued delivery that no hand-over was ever tried for is unsent without acpmux.
    • The overall outcome is failed if any delivery failed, else too_late, else retracted, else unsent.
    • Wiring: catalog (mutation, idempotency required), spec docs, the 7 SDK descriptors (new catalog sha), the app-host NEVER list (regenerated cmux-app.d.ts), and page access hard-denied. Not allowlisted for the remote relay.
  • CLI cmux agent message unsend <id> [--recipient <agent>] has one noun-first case, which now exercises --recipient. It is CLI-only in MCP and documented in cli.md.

Testing

All tests are behavior tests against a real daemon: tests/agent_message_pump.rs (14) and tests/agent_message_launch_credential.rs. The fake acpmux now has prompt gates, transfer.moving refusals, withdraw refusals and hang-ups.

Red, on the old code with the new tests (fleet):

  • f67896f9, a31dfb33 (first pass, on 09560b51): launch credential missing; pump 0/6.
  • 09d74ea1 (on 8da02bb8). Each of these is red for the expected reason:
    • partial unsend: the whole request was refused
    • silent acpmux: the whole request was refused
    • unaccepted prompt: became failed
    • 205 queued terminal messages hid an acpmux delivery
  • 3428f9ce (scratch commit e2cc058b: 8da02bb8 plus the rewritten tests):
    • replay: acpmux was asked twice
    • transfer.moving: became failed and was never retried
  • 2da3969f (scratch commit 580882a4): CLI racing an unsend. The CLI's own hand-over marked the message failed while the pump held it at the gate, and the CLI exited 1.
  • ed28865c (on f1ade20c, before the fail-closed fix): a plain local unsend was accepted.
  • c8efeadc (on 28cc8a66): every_safe_transport_operation_has_a_noun_first_path reported that unsend's recipient field was never exercised.

Green, on 1e29acb7e1 (fleet):

  • b3d46706 (test agent_message_): launch_credential 1/1, agent_message_policy 16/16, agent_message_pump 14/14.
  • 784e341f: every_safe_transport_operation_has_a_noun_first_path.
  • dd51f1c0 (all): fmt and clippy pass. Tests stop at acpmux --test router_free_tier (6 of 7 fail), a known base flake. On earlier heads the same step stopped at the base red codex_bridge_config, which also fails on the merge base (bf8774d1 base, 1afdf179 head).
  • Targeted follow-ups on 28cc8a66. The only later commit changes cases.rs.
    • 53669e67 (launch_credential): acpmux, cmux-tui and agent_message tests pass.
    • fad09876 (chief): 93 pass.
    • 5ddfcf04 (state::): 155 pass.

Local checks:

  • check-resource-api-boundary.py, check-spec-inventory.py, check-sdk-schema.py and gen-cmux-global.ts --check pass.
  • crash_ratchet.py passes, with no new unwrap.
  • The god-file check on every touched file finds no failures and no growth: agent.rs is 1175 vs 1179 on base, and mux.rs and router.rs are unchanged.
  • rustfmt is clean.

Not run:

  • The test binaries after router_free_tier beyond the targeted filters above.
  • A Windows cargo check. Non-unix paths have stubs.
  • Pump and withdraw end-to-end against a real acpmux.

Changelog

  • Added: Agents can take back a message they sent with cmux agent message unsend, and the daemon delivers queued messages to acpmux agents on its own.

🤖 Generated with Claude Code