Cloud attach-endpoint vmKind: owner for the creator's machine on a one-member team
HOLD: security policy (who counts as owner on classic attach) awaits Leo
Why
On the classic Cloud route (/api/vm, every release and nightly build of cmux NEXT), cmux cloud start-session and the app's agent chats on a Cloud machine are refused with agent_session.host_unverified ("could not confirm what kind of Cloud machine it is").
The cause is the attach answer. POST /api/vm puts a new machine on the caller's selected Stack team (web/services/vms/auth.ts resolves billingTeamId to the Stack team, and falls back to the user id only for an account with no Stack team at all). So a person working on their own personal Stack team gets ownerTeamId = that team's id. vmKindForCaller answered team for every Stack team, so the Mac app never sent vm_kind: "owner", and cmux-tui's start gate refused. The machine's own record said owner (resolveVmGuestKind already treats a one-member team as owner). That combination is exactly host_unverified and not team_vm_blocked.
What changes (reverses #26222's one-member choice)
#26222 made the attach answer stricter than the guest's: "Every Stack team, a one-member team included, is team". This PR reverses that one point:
vmKindForCaller(row, caller, members?)answersownerfor a machine the caller created (row.userId === caller, exact) on a Stack team whose member list, read for this request, is exactly[caller].- The list comes from the same bounded read
resolveVmGuestKinduses (listTeamMemberIdsWithTimeout, 3 s). It is read on every attach and never stored, so a second member joining makes the next attach answerteam. - Fail closed: no directory, a failed read, a timeout, a missing team, an empty list, or any other member gives
team, neverowner. - Everything else stays as it was. A personal-account machine (
ownerTeamId === caller) is stillownerwithout asking Stack. A machine the caller did not create isteamwhatever its team. A blankownerTeamIdgivesnull. Ids compare exactly. - Stack is asked only for a machine the caller created on a Stack team (
vmKindForCallerNeedsMembers). The read is forked as soon as the machine is known and joined at the end, so it overlaps the provider endpoint open instead of adding to it. - The attach-endpoint route (cmux-remote and websocket) passes
vmTeamDirectory(). The sessions route does not returnvmKindand is unchanged.
Why it is acceptable: owner still means "the caller created it and nobody else can reach it", the same rule the guest already applies to /etc/cmux/vm-kind. The one-member check runs live per attach, like the guest's per-minute re-read. A team that gains a member turns team on the next link. The Mac app reads the answer per link and never saves it.
Risk (security policy): this decides when the Mac app gives up its local sandbox for its own session on a Cloud machine. There is a window between a second member joining and the person's next attach, during which an already-running link keeps its owner answer. The guest has the same window, up to its one-minute re-read.
Tests
web/tests/vm-attach-vm-kind.test.ts, through both openVmCmuxRemote and openAttachEndpoint plus vmKindForCaller directly:
- the caller's machine on a one-member own team:
owner(the list is read per attach, for the machine's own team); - a two-member team:
team; - a machine the caller did not create, on the caller's one-member team:
team, and Stack is not asked; - an empty
ownerTeamId:null; - an unread, failed, missing (
null), empty or timed-out member list:team; - the personal machine never asks Stack.
web/tests/vm-route-auth.test.ts: the cmux-remote route passes the team directory.
Results are posted in a comment below (cmux-ci, Linux).
Refs #26222
🤖 Generated with Claude Code
Agent session: agent session
![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
Fixes the Cloud attach endpoint answering team for a caller's own machine on a one-member Stack team, which made cmux-tui refuse agent sessions on the classic route with agent_session.host_unverified.
vmKindForCallernow answersowneronly when the caller created the machine and the team's member list, read per attach with a bounded timeout, is exactly them; a second member, missing team, failed read, or a machine the caller did not create still answersteam.- The member read runs only for machines the caller created on Stack teams and overlaps the endpoint open; personal machines never ask Stack.
- Both cmux-remote and websocket attach routes pass the team directory; the sessions route is unchanged.
- Tests cover the same machine flipping from
ownertoteamwhen a second member joins on the next attach. - The route auth test now asserts the team directory type-safely.
Summary by CodeRabbit
- Bug Fixes
- VM ownership is now identified more accurately for machines in Stack teams. A creator’s machine is shown as personally owned only when the membership information read during attachment lists that creator as the team’s sole member; otherwise, it remains team-owned.
- If team membership information is unavailable or cannot be retrieved, the machine is treated as team-owned.
- Personal machines continue to be identified without requiring team membership information, and machines attached by someone other than their creator remain team-owned.