backend: UserDO/TeamDO network identity and Native dial RPCs for the platform CloudDO
Summary
cmux-platform's CloudDO is taking over the Native backend (cloud.vm.network_policy, cloud.vm.receiver.lifecycle, Native mesh create, Native dials). UserDO and TeamDO stay in cmux-api, so the platform calls them through cross-script Durable Object RPCs. This PR adds those RPCs and the install-authority state they read. It adds no ops, routes or catalog entries, and nothing calls the new methods until the platform does.
Carried from #28950 (the HOLD aggregate), whose versions of domains/user-authority.ts and domains/team-authority.ts match the older stack #22281 → #22382 → #22388 → #22413 → #22451:
UserDO (each answer pinned to the caller's live grant: install not revoked, grant held by that install, not revoked or expired):
networkIdentity(entity, install, grant, team) → { key, kind, expires_at, authority_version, op_classes } | null: mac/ios/cli/daemon, grant withreadandmutate-own; a daemon only for itsbound_team.networkIdentities(entity, entries: {install, grant}[] (≤128), team) → Array<identity | null>(a malformed batch returns[]).vmNetworkIdentity(entity, install, grant, team, machine) → identity | null: kindvm,bound_teamandbound_machinemust match, grant hasvm-self.readIdentity(entity, install, grant) → identity | null: mac/ios/cli under a livereadgrant, forhost.network.target.deviceKey(entity, install, grant, opClass: "read" | "mutate-own" = "mutate-own"): existing callers are unchanged; any other op class getsnull.- Install authority state (
install_authority: a generation plus a per-install incarnation, written by the reducer).authority_versionhashes every admission input together with the incarnation, so a revoked, changed or re-granted install never matches an old version, even when it goes A → B → A.
TeamDO (the #28950 signatures):
authorizeCloudDial(entity, p, target: { vm, origin, network }, proof, services) → { ok: true, permit } | { ok: false, code, message }andrecheckCloudDial(entity, p, permit) → boolean.vmReceiverPeers(entity, { creator, origin, deadline, policy }) → { peers, expires_at, authority_version } | null.hostNetworkTarget(entity, p, { mesh? }) → ReadResult, the method cmux-platform #174'sTarget.tsalready calls. Around its mesh read it requires the same installauthority_version(and the socket'sinstall_authoritywhen one is set) and the same member incarnation before and after. A grant going read → none → read, or a member leaving and rejoining with the same role, is refused (review finding at 0d8f0fa3).- Member authority incarnations (
member_authority, now a private table) fence the creator and peer views. host.network.bindrecords the grant it was confirmed under (ServerNetworkBinding.install_grant). Native dials refuse a binding that has no grant or has a different one. Legacy bindings decode but authorize nothing until they are rebound.
Why the dial and peer logic stays in TeamDO instead of moving to the platform: recheckNativeDial and the two-pass peer stamp depend on no await between the final TeamDO state checks and the answer. Splitting that across a cross-script RPC would open a window where a membership, key or grant change goes unseen.
Not carried from #28950: the install.authority.changed / member-authority notices (their consumers are CloudDO and socket fan-out; the platform re-reads, and leases last at most 30 s), host.network.policy, the UserDO socket stamping, and every CloudDO-side file (owned by the platform port).
Differences from #28950: member_authority is in TEAM_PRIVATE_TABLES so subscribers never receive incarnations. hostNetworkTarget checks the caller's read grant class and decodes its own params, because no catalog op exists for it here. NetworkAllow lives in protocol/src/network-allow.ts. cloud-vm-network-policy.ts is taken from #28947's version (identical add).
Generated: backend/catalog/cloud-operations.json, clients/ts/cloud/src/generated.ts and cmux-cloud-wire types_02.rs were regenerated for the optional install_grant.
Tests
test/owner-network-rpcs.test.ts(new) covers the refusals: wrong user stream, wrong grant, missing op class, expired grant, revoked install, revoked grant, a daemon bound to another team, a VM install bound to another team or machine, a VM install withoutvm-self,networkIdentityrefusing a VM install,deviceKeyop classes, batch bounds, the install-authority A → B → A fence and a forged incarnation, TeamDO RPCs on an unheld team,hostNetworkTargetwithout a read grant or with a bad selector (no reads made), and thevmReceiverPeersdeadline window, creator membership, origin and tenant.- Live-authority tests: a successful
hostNetworkTargetread; a regrant, rejoin or leave during the mesh read (the regrant and rejoin cases fail without the fence); a populatedvmReceiverPeersjoin; an install authority change between the two identity snapshots; a creator rejoin or leave during the snapshots; a dial with an install or membership incarnation change during the Native read and after authorization. - Ported from #28950:
cloud-native-dial.test.ts(the dial authorization block),cloud-vm-api-user-network-identity.test.ts(revokes with the session, since install principals cannotinstall.revokeon base),team-authority.test.ts, and the protocol catalog round-trip.
Observed / Not run
- Run locally:
tsc -p backend/apps/apiis clean;vitest runpasses on the four files above (39 tests at the current head); protocolvitest runpasses (15);bun run catalog:checkpasses. - The full api suite runs on cmux-ci at this head (results will be in a comment).
- #28950's
cloud-vm-api-native-dial-owner.test.tswas not ported: it drivesCloudDO.mintLinkToken, which is the platform port's to test.
Changelog
none
Agent session: agent session
🤖 Generated with Claude Code