cmux-host: relay acpmux hand-off reports to the Cloud API as the VM install

Merged
TE
8d ago
+529-29

The Cloud machine's agent (cmux host run's cloud role) now relays a session's hand-off report to the hand-off store as the machine's VM install. acpmux sends the reports in the companion PR. This PR is the half that holds the credential.

Leo, 2026-10-28: "whatever's convenient for cloud, like that can be productionized, operationalized, we ought to just put out there". It is piece (a) of the cloud build gaps: give the hand-off store (backend/apps/api/src/handoffs/, cmux#26578 and cmux#26666) a caller.

What changed (cmux-tui/crates/cmux-host/src/cloud/)

  • New socket line. The agent socket (/run/cmux-vm-agent/agent.sock, root:cmux 0660) takes {"handoff_report": {session, turn, status, summary?}}. It is answered on its own connection with one reply line, the same way a github_token ask is.
  • Validation (handoff.rs) matches the store's shapes:
    • session [A-Za-z0-9_.:-]{1,128};
    • turn ≄ 1;
    • status running|needs-you|done|failed;
    • summary at most 2,000 UTF-16 units.
  • The hand-off is named the way the store names it. The id is ho_ + sha256(owner NUL session)[:20], where the owner is the bound install's user. The report goes to POST /v1/handoffs/<id>/report.
  • No new credential. The store already accepts the VM install bound to the hand-off's machine (machineCredential in routes.ts), so the agent reuses its install bearer (/v1/auth/challenge + /v1/auth/token).
    • That bearer never leaves the agent, which is root. acpmux gets only the reply: ok, plus the hand-off's id, turn, state and status, or the store's error code with the current turn.
    • The reply never carries the summary.
  • Off the poll loop. CloudClient::handoff_report shares the bearer-retry path with github_token, which is now post_as_install: a 401 or an auth.* 403 drops the cached bearer and retries once.
    • The GitHub minter thread now takes both kinds of ask (Ask::Github | Ask::Handoff), so an API call never holds the worker's poll loop.
    • Its limits are unchanged: 8 pending, a 60 s queue wait, and busy past those.
  • The journal line names the hand-off, turn, status and outcome, never the summary.

Risk: low.

  • Nothing sends the new line until the acpmux PR lands.
  • An agent built before this change reads such a line as a session line and ignores it (session.rs only acts on activity, event and resume). acpmux backs off from an agent that never answers.
  • sha2 moves from dev-dependencies to dependencies. It is already in the workspace.
  • Overlap: #25682 (austinywang, ACP trajectories outside the VM) adds a post_json next to github_token in client.rs. The changes are textually adjacent and do different things.

Tests (cmux-ci Linux, cargo test -p cmux-host --lib cloud::: 22 passed; clippy -D warnings and fmt --check are clean):

  • handoff_tests.rs checks:
    • the id matches the Worker's for the same owner and session (the expected value was computed with Node's crypto);
    • the route, the bearer and the body;
    • a handoff.stale refusal passes through the store's current turn;
    • 404, transport and bad-200 answers;
    • a stale bearer is retried once;
    • every malformed field is refused before the API is called;
    • the minter answers a report off the poll loop.
  • role_tests.rs: the real worker answers a handoff_report line on its own connection, with vm.unbound and request.invalid refusals.

Agent session: agent session

šŸ¤– 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

Relays a session's hand-off report from the Cloud agent to the hand-off store as the machine's VM install, giving the hand-off store its first caller.

  • The agent socket takes a new {"handoff_report": {session, turn, status, summary?}} line, answered on its own connection; the report posts to POST /v1/handoffs/<id>/report as the VM install, with the id named as the store names it.
  • No new credential: the store accepts the VM install bound to the machine, and the bearer never leaves the agent; the reply carries only the hand-off's id, turn, state, status and a length-bounded statusAt, never the summary, so a stale refusal's newer turn is distinguishable from acpmux's own. Field bounds cite the store's shapes.
  • Reports join GitHub token asks on the minter thread off the poll loop, sharing the bearer-retry path; malformed fields are rejected before any API call.
  • Risk is low: nothing sends the new line until the acpmux PR lands, and older agents read it as a session line and ignore it.