acpmux MCP tools: agents message lanes with acpmux_send, drawn as a message card

Merged
TE
10d ago
+568-6

Agents message other lanes by piping text into a shell, printf … | timeout 60 acpmux send --no-wait NAME -. The chat then draws that as a command block. This PR gives them real tools for it.

  • New tools. Every local agent session gets an acpmux MCP server (acpmux acpmux-mcp, acpmux_mcp.rs). Behind the cmux server it offers:
    • acpmux_send {to, text, steer?} messages a session by name, id or host/name. Like acpmux send --no-wait, it hands the message to this daemon's outbox (_acpmux/send_queue), which delivers it as this session and retries a slow target or a peer that is down. It returns the promptId and delivery id once the delivery settles (steered in, queued, or a new turn), or after 5 s with the message on its way. It never waits for the reply. steer defaults to true.
    • acpmux_ls {status?}, acpmux_last {name, count?} and acpmux_info {name} read the sessions.
    • Each tool description says it messages or reads another lane or session, and to use it instead of acpmux send in a shell.
  • How the pane draws it. The agent pane draws an acpmux_send call as the collapsed "Sent to NAME" message row (AgentMessageRow), never as an MCP row. This works however the harness names the call: mcp__cmux__acpmux_send, Codex's {server: "cmux", tool: "acpmux_send", arguments}, mcp__acpmux__send, or the bare or dotted name. (A shell acpmux send is drawn by base's acpmuxSends.ts, #25045.)
  • Gallery. New lanes variant on agent-pane.agent-message-row.
  • Docs. The README documents the tools.

Risk: risk:normal. This adds no new socket method and no policy change. The authorization analysis follows.

Authorization

  • Same power as the CLI. Each tool makes the same request (session/prompt with _acpmux/steer, _acpmux/sessions, _acpmux/info, the event read behind last) that the agent's own acpmux CLI already makes. It goes over this daemon's unix socket with the session's caller token (ACPMUX_SESSION_ID/_TOKEN/_TOKEN_SOCKET from hub::person::caller_env). The subsession and forum servers get their token the same way.
  • Stamped as the agent's message. The token makes the daemon stamp a message as session_message from this session (prompt_provenance.rs). It can never pass as the person's words.
  • Remote and Web sessions get nothing new. A remote-origin session gets none of cmux's agent tools (agent_tools::left_out), so this server is left out as well. A Web client can't reach this stdio server at all.
  • One difference: sandboxed agents. An agent whose shell sandbox can't reach the daemon socket can now message other sessions through MCP. Its harness asks permission for an MCP tool the same way it does for a shell command. That is the point of the change.

Changelog

Added: agents message other lanes and sessions with the acpmux_send tool, and the chat shows each message as a "Sent to" line instead of a shell command.

Tests

  • Rust:
    • cargo test -p acpmux --lib acpmux_mcp (4 tests, against a fake agent over a real socket): send delivers and returns its promptId; ls, last and info read the sessions; an unknown session or a missing argument is an error the agent reads; the descriptions are checked.
    • --test agent_tools: every local session gets the acpmux upstream with the caller token.
    • chief_builtin_presets and tool_inventory list the new server.
  • Web:
    • agentMessages.test.ts: every MCP naming, Codex's nested call, the shell forms, and the negatives (another server's send, no recipient, a waiting send).
    • test/transcript-row-message.test.tsx: an acpmux_send call renders as the collapsed .cv-agentmsg.

Red/green, hosted ./scripts/verify-cmux-tui-hosted.sh --filter local_sessions_get_the_browser_and_cua_tools:

Locally on huge-blue, the 3 new webview assertions failed on the test commit and pass on the head.

Observed / Not run

  • Observed on huge-blue.
    • Rust: cargo test -p acpmux --lib acpmux_mcp and the agent_tools, agent_displays, chief_builtin_presets, tool_inventory and forum integration tests pass with umask 022. With this host's default umask, chief_builtin_presets stops on "/tmp/acb-tools-* is writable by another user". That is environmental, and it happens on base too.
    • Checks: cargo clippy -p acpmux --all-targets -D warnings and prepush-gate.py are clean.
    • Web: the two webview test files pass. vp check finds no format or lint errors in the touched files.
  • Not run locally.
    • The full cargo test -p acpmux filled the disk, so it was stopped; PR CI runs it.
    • bun run check's generated-copy checks and gallery-coverage.test.ts need the Swift sources, which this sparse worktree doesn't have; CI runs them.
    • scripts/gallery-compare.sh can't reach the gallery host (cmux-lawrence) from huge-blue. Someone off huge-blue can run it to see the new lanes variant.
    • No app build: there is no Swift change.
  • Known limits.
    • A Codex session in code mode calls tools from a cmux_exec script, so its sends there stay a script row.
    • When the async send --no-wait daemon request lands (cmux/acpmux-send-async, not pushed yet), acpmux_send should move onto it. For now it uses the same queue_prompt that send --no-wait uses today.

Adopted and rebased (2026-10-25)

The stale-pr-reconciler, word for word: "These PRs in your area have no live owner (Leo, 2026-10-25: "these should never have been this stale for this long"). Adopt each: pr-owner claim N (in that repo), rebase onto its base, an independent review at the new head, then land it".

  • Not superseded. cmux-subsessions.send_message reaches only one's own sub-sessions or parent. acpmux_send reaches any session by name or host/name.
  • Re-applied onto feat-cmux-next (it was 4,101 commits behind). Base's #25045 (acpmuxSends.ts) now draws a shell acpmux send, so this PR's shell parser (withoutTimeout and acpmuxMessage) and its tests were dropped. Only the MCP call's row is left.
  • Codex review (gpt-6-astra xhigh), BLOCK: 1. send called queue_prompt directly and bypassed base's durable outbox (_acpmux/send_queue), so a peer that was down or a slow target got no retry and no restart survival. 2. The 10-minute wrapper dropped a JoinHandle and left the nested session/prompt attached. Fixed: send now submits _acpmux/send_queue (unix socket only, delivered as this session with its caller token, retried for about 34 minutes) and reads _acpmux/send_status for at most 5 s. Nothing in the MCP server outlives the call. Test first: send_goes_through_the_outbox_and_names_its_delivery (red: "no delivery id").
  • Rebased again past CollectOpts.steer. The gallery and tests now say build-box, not a fleet host (prepush gate). The re-review said SHIP at ba3998d5b32 ("Outbox delivery remains unix-local, caller-stamped, restart/retry durable, and attachment-checked").

Tests on the rebased branch (fast-checks on Blacksmith)

  • Red at the test commits: agent_tools (no acpmux upstream), the two new webview tests, then the outbox test.
  • Green at ba3998d5b32: --lib acpmux_mcp 5 pass; --test agent_tools, chief_builtin_presets and tool_inventory pass; clippy -D warnings (acpmux, cmux-tui) and fmt are clean. Webviews agentMessages.test.ts and transcript-row-message.test.tsx: 22 pass, 1 fail ("opening it grows the message in place"), which fails the same way on base 5e6cb133c60.
  • PR CI reds that aren't from this PR: Script tests (base), Swift test reds in tooltip and split-focus code (no Swift change here), generated files (base's open-remote), gallery health, and GitHub API rate limits in the status, publish and same-tree jobs.

No timers or observers are left: the old SEND_MAX task is gone. No capture: the new gallery variant lanes shows the row.

Agent session: big-red
šŸ¤– Generated with Claude Code