cmux NEXT Cloud: caller-ownership answer on connect_info, bound to the link token (cmux-platform#89 A)

Merged
TE
6d ago
+336-29

Leo, 2026-10-30, on building the app-server Cloud path now instead of after the demo: "well why can't we just.... do our best now?"

Part of manaflow-ai/cmux-platform#89 (section 1, the caller-ownership answer). This is the Worker half. #27922 wires the app, and a follow-up passes the answer through the Cloud app server's cloud.machine.connect. cmux#27778 stays on HOLD until those land.

What changes

The cmux-next API Worker (backend/apps/api, CloudDO) now says whether the signed-in caller owns a Cloud machine, with the classic attach answer's rule (web/services/vms/vmKind.ts vmKindForCaller):

  • cloud.machine.connect_info and cloud.machine.link_token results carry vm_kind: "owner" | "team", computed per request and never stored.
  • owner only when the caller created the machine (exact user id, never an agent token) and the machine is on the creator's personal team. Every shared-team machine is team, its creator included. A teammate is team. Another team is refused with auth.forbidden before any answer (unchanged).
  • The link token signs the same value as claim vmk. The owner answer therefore belongs to one dial: verifyLinkToken gives no claims for an expired or replayed token, and refuses a vmk that is neither owner nor team as malformed. Tokens minted before this have no vmk and grant no owner answer.
  • callerVmKind(entity, row, principal) in cloud-connect.ts is the one check. The connect/link audit rows record vm_kind.
  • Regenerated: backend/catalog/cloud-operations.json, cloud-vectors.json (connect_info and link_token cases carry vm_kind), schemas/link-token/vectors.json (new cases owner_claim and bad_vmk), the TS client, the Rust cmux-cloud-wire types and the app-host typings.

Why it's here and not in cmux-platform: the brief said cmux-platform, but connect_info, link_token and CloudDO live only in cmux, and cmux-platform has no port of them. docs/port-cloud-worker.md rule 1 says "A fix goes to cmux first and comes along with the port."

Compatibility: the deployed Rust decoders (cmux-link ConnectInfo / LinkTokenGrant and the cloud app server's api::models::ConnectInfo) ignore unknown fields. The generated cmux-cloud-wire types are used only inside that crate. Only the TS reference verifier checks link tokens today.

Piece B (lane/cloud-np-b-owner-exec) keeps CloudDO's mayUse for exec/fs, which is classic's /api/vm/{id}/fs rule (any member of the billing team). The owner answer is a strict subset of it.

Tests

backend/apps/api/test/cloud-owner-answer.test.ts: the owner (connect_info, mint, signed claim, audit), an agent token acting as the creator (team), a shared-team machine for its creator and a teammate (team, also in the teammate's token), another team (refused, also when it claims this team's id), an expired token and a replayed token.

  • Red on base (fix files reverted, test kept): bunx vitest run test/cloud-owner-answer.test.ts → 5 failed, 1 passed (the other-team refusal already held).
  • Green on head: 6 passed.
  • On head, 15 files covering connect, link token, bind, keyset vectors, routes and idle: 55 passed.
  • bun run catalog:check, tsc -p . (api, protocol), lint:size, gen-cmux-global.ts --check and scripts/cmux-next/prepush-gate.py all clean.

Observed / Not run

  • Observed: the runs above, on a Linux lane host, before and after merging base.
  • Also observed: cargo test -p cmux-cloud-wire (cloud_vectors decodes the regenerated vectors): ok.
  • Not run: the full api suite (CI runs it). No staging run yet; that is the issue's "done when", after the app wiring lands.

Changelog

none

risk: high (auth answer). Lands on an independent review receipt at the exact head plus a green compile.

šŸ¤– Generated with Claude Code

Agent session: agent session