cmux-host: relay acpmux hand-off reports to the Cloud API as the VM install
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 agithub_tokenask 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.
- session
- 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'suser. The report goes toPOST /v1/handoffs/<id>/report. - No new credential. The store already accepts the VM install bound to the hand-off's machine (
machineCredentialinroutes.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.
- That bearer never leaves the agent, which is root. acpmux gets only the reply:
- Off the poll loop.
CloudClient::handoff_reportshares the bearer-retry path withgithub_token, which is nowpost_as_install: a 401 or anauth.*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
busypast those.
- The GitHub minter thread now takes both kinds of ask (
- 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.rsonly acts onactivity,eventandresume). acpmux backs off from an agent that never answers. sha2moves from dev-dependencies to dependencies. It is already in the workspace.- Overlap: #25682 (austinywang, ACP trajectories outside the VM) adds a
post_jsonnext togithub_tokeninclient.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.rschecks:- 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.stalerefusal 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.
- the id matches the Worker's for the same owner and session (the expected value was computed with Node's
role_tests.rs: the real worker answers ahandoff_reportline on its own connection, withvm.unboundandrequest.invalidrefusals.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
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 toPOST /v1/handoffs/<id>/reportas 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.