Cloud attach-endpoint: report vmKind (owner only for the caller's own personal VM)

Merged
TE
9d ago
+192-6

What

POST /api/vm/:id/attach-endpoint now answers a top-level vmKind: "owner" | "team" | null on every transport (cmux-remote, websocket).

The Mac app will use it to give up its local sandbox for the caller's own session only when the Cloud API says the machine is the caller's own personal machine. The guest has passwordless sudo, so no in-guest marker can be trusted for this. The app reads the field per link and never caches it; a missing, null or other value stays sandboxed. The app and acpmux sides are separate PRs.

The rule (strict)

New vmKindForCaller(row, callerUserId) in web/services/vms/vmKind.ts:

  • "owner" only when the machine is on the caller's own personal account (owner_team_id equals the caller's user id, the synthetic personal team) and the caller created it (user_id equals the caller).
  • "team" for every Stack team, a one-member team included, and for any machine the caller can reach but did not create. A team member attaching to a teammate's machine gets "team".
  • null when the row has no usable billing owner (blank owner_team_id). The access check already refuses such a row, so in practice clients see owner or team.

resolveVmGuestKind and its callers (the guest's /etc/cmux/vm-kind via /api/vm/reflection/kind) are unchanged. On main that function calls a one-member team owner and re-asks every minute; the attach answer deliberately does not reuse it, so it can't turn into owner for a team.

owner_team_id is text NOT NULL DEFAULT '' on main and feat-cmux-next alike, so "personal account" means owner_team_id = user_id on both. There is no null case.

Where production serves this

Both the stable app and the nightly-next app call https://cmux.com/api/vm/:id/attach-endpoint:

  • classic: Packages/macOS/CmuxCloud/Sources/CmuxCloud/Environment/AuthEnvironment.swift:324-336, where a Release build is always https://cmux.com;
  • next: Packages/macOS/CmuxNext/Sources/CmuxNextCloud/Config/CloudConfiguration.swift:69,95-99 and API/CloudAPIClient.swift:117-122, where a Release build uses apiBaseURL = https://cmux.com.

cmux.com is the Vercel web/ app. web/vercel.json deploys main only, and a live request answers server: Vercel, x-matched-path: /api/vm/[id]/attach-endpoint. So this lands on main for production. A twin PR carries the same change to feat-cmux-next so the branches don't drift.

Tests

web/tests/vm-attach-vm-kind.test.ts, through openVmCmuxRemote and openAttachEndpoint, plus the pure function:

  • the creator on their personal machine gets owner;
  • a team machine gets team, for its creator too;
  • another account that can attach gets team, never owner;
  • a one-member team gets team;
  • an unusable owner gets null, and its attach is refused.

Three existing vm-workflows equality checks now expect the extra field.

Linux CI: 14 related files, 325 pass and 0 fail; tsgo, eslint and complexity are clean.

Each test fails without its part of the change:

  • with the workflow wiring removed, 7 tests fail;
  • with owner for any personal machine whoever calls, the "another account" tests fail (2);
  • with a one-member team as owner, the one-member test fails (1).

Risk

The response gains a field; Swift decoders ignore unknown keys. A wrong owner would let a session skip the local sandbox, so the rule fails closed: only the exact creator-on-personal-account case is owner.

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 CodeRabbit

  • New Features
    • VM attach responses now include a caller-specific classification: a VM is labeled “owner” when the caller matches both its user and billing owner; otherwise, it is labeled “team.”
    • The classification appears in standard and remote attach responses, including successful recovery results.
  • Bug Fixes
    • VMs with a blank or whitespace-only billing owner are handled as unavailable during attach workflows.