From 3a8f25557e10c23f59520ed44fbd8a40e2eae855 Mon Sep 17 00:00:00 2001 From: Wang Date: Wed, 2 Sep 2026 12:12:30 +0800 Subject: [PATCH] fix(runtime-host): retain authenticated owner routes Persist authenticated direct-peer route rotations in the existing Owner profile so reconnects and restarts do not fall back to stale connection-code hints. Fence updates by profile incarnation and leave credentials and authority unchanged. Generated-by: Codex --- .../runtime-host-profile-service.test.ts | 57 +++++++++++++++++ apps/desktop/src/main/runtime-host-boot.ts | 23 ++++--- .../src/main/runtime-host-desktop-manager.ts | 4 +- .../src/main/runtime-host-profile-service.ts | 64 ++++++++++++++++++- .../runtime-host-cli-context.test.ts | 4 ++ .../runtime-host-profile-command.test.ts | 1 + .../src/__tests__/host-profile.test.ts | 9 +++ .../runtime-host/src/client/host-profile.ts | 41 ++++++++++++ 8 files changed, 193 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts b/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts index 12a9886d42..c51e8d7ad8 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts @@ -679,6 +679,63 @@ test('classifies connection-code failures without exposing transport errors to t ); }); +test('persists authenticated Owner routes imported through a connection code', async () => { + const root = await clientRoot(); + const catalog = createClientRuntimeHostProfileCatalog(root); + const startup = await resolveDesktopRuntimeHostStartup(root, { catalog }); + const staleTransport = { + kind: 'libp2p-direct' as const, + peerId: '12D3KooWpeer', + routeHints: ['/ip4/192.0.2.8/udp/44001/quic-v1'], + coordinationRelays: ['/memory/stale-relay'], + }; + const freshEndpoint = { + peerId: staleTransport.peerId, + routeHints: ['/ip4/198.51.100.9/udp/44002/quic-v1'], + coordinationRelays: ['/memory/fresh-relay'], + }; + let observedIncarnation: string | undefined; + const service = createDesktopRuntimeHostProfileService({ + clientDataRoot: root, + startup, + catalog, + states: () => [connectingLocal()], + enable: async (target, _sshInteraction, onPeerEndpoint) => { + observedIncarnation = target.profileIncarnationId; + assert.deepEqual( + target.profile.kind === 'remote' ? target.profile.transport : undefined, + staleTransport, + ); + assert.ok(onPeerEndpoint); + onPeerEndpoint(freshEndpoint); + }, + disable: async () => undefined, + setDefault: () => undefined, + finalizePairing: async () => undefined, + }); + + const result = await service.importConnectionCode( + encodeRuntimeHostOwnerConnectionCode({ + name: 'Other computer', + rootId: ROOT_ID, + transport: staleTransport, + credential: 'pending-owner-token', + }), + ); + + assert.equal(result.kind, 'connected'); + if (result.kind !== 'connected') return; + const persisted = await catalog.resolve(result.profileId); + assert.equal(persisted.credential, 'pending-owner-token'); + assert.equal(persisted.profileIncarnationId, observedIncarnation); + assert.deepEqual( + persisted.profile.kind === 'remote' ? persisted.profile.transport : undefined, + { kind: 'libp2p-direct', ...freshEndpoint }, + ); + const restarted = await resolveDesktopRuntimeHostStartup(root, { catalog }); + assert.deepEqual(restarted.remotes, [persisted]); +}); + test("finishes a persisted pairing after Desktop restarts before finalization", async () => { const root = await clientRoot(); const catalog = await stageInterruptedPairing(root); diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 8ac6146168..7a1c7577e6 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -545,7 +545,7 @@ const runtimeHostProfileService = createDesktopRuntimeHostProfileService({ catalog: runtimeHostProfileCatalog, credentialStore: runtimeHostCredentialStore, states: () => runtimeHostManager?.entries() ?? [], - enable: async (target, sshInteraction) => { + enable: async (target, sshInteraction, onPeerEndpoint) => { if (target.profile.kind === 'local') { throw new Error('A resolved non-local Runtime Host profile is required'); } @@ -553,13 +553,20 @@ const runtimeHostProfileService = createDesktopRuntimeHostProfileService({ throw new Error('A remote Runtime Host profile requires an access credential'); } if (!runtimeHostManager) throw new Error("Runtime Host manager is unavailable"); - await runtimeHostManager.enable({ - profile: target.profile, - ...(target.credential ? { credential: target.credential } : {}), - ...(target.profile.kind === 'remote' && target.profile.transport.kind === "ssh" - ? { sshInteraction } - : {}), - }); + await runtimeHostManager.enable( + { + profile: target.profile, + ...(target.credential ? { credential: target.credential } : {}), + ...(target.profile.kind === 'remote' && target.profile.transport.kind === "ssh" + ? { sshInteraction } + : {}), + }, + onPeerEndpoint + ? (status) => { + if (status.peerEndpoint) onPeerEndpoint(status.peerEndpoint); + } + : undefined, + ); }, disable: async (profileId) => { if (!runtimeHostManager) throw new Error("Runtime Host manager is unavailable"); diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts index 60dd17f72d..914aeb1b5b 100644 --- a/apps/desktop/src/main/runtime-host-desktop-manager.ts +++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts @@ -71,6 +71,7 @@ export interface RuntimeHostDesktopManager { unobserveSession(observerId: string): Promise; enable( profileTarget: DesktopRuntimeHostCandidateStartInput['profileTarget'], + onHostStatus?: (status: HostStatusResult) => void, ): Promise; mountGuest( profileTarget: NonNullable, @@ -493,13 +494,14 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { async enable( profileTarget: DesktopRuntimeHostCandidateStartInput['profileTarget'], + onHostStatus?: (status: HostStatusResult) => void, ): Promise { if (!profileTarget) throw new Error('A non-local Runtime Host profile is required'); if (isSessionGuestProfile(profileTarget.profile)) { throw new Error('Session Guest targets must be mounted instead of enabled as profiles'); } return this.#mutateTarget(profileTarget.profile.id, () => - this.#enable(profileTarget, false), + this.#enable(profileTarget, false, undefined, undefined, onHostStatus), ); } diff --git a/apps/desktop/src/main/runtime-host-profile-service.ts b/apps/desktop/src/main/runtime-host-profile-service.ts index a310141439..b75137439c 100644 --- a/apps/desktop/src/main/runtime-host-profile-service.ts +++ b/apps/desktop/src/main/runtime-host-profile-service.ts @@ -40,6 +40,7 @@ import { type RuntimeHostProfileCatalog, } from "@maka/runtime-host/client"; import { runtimeHostAccessCredentialFingerprint } from "@maka/runtime-host/operator"; +import type { HostPeerEndpoint } from '@maka/runtime-host/protocol'; import type { CredentialStore } from "@maka/storage/credential-store"; import { withFileUpdateLock } from "@maka/storage/file-update-lock"; import type { @@ -295,6 +296,7 @@ export function createDesktopRuntimeHostProfileService(input: { readonly enable: ( target: ResolvedRuntimeHostProfile, sshInteraction: "terminal" | "batch", + onPeerEndpoint?: (endpoint: HostPeerEndpoint) => void, ) => Promise; readonly disable: (profileId: string) => Promise; readonly finalizePairing: (profileId: string) => Promise; @@ -488,7 +490,9 @@ export function createDesktopRuntimeHostProfileService(input: { ): Promise => { try { const activationTarget = await resolveActivationTarget(target); - await input.enable(activationTarget, sshInteraction); + const routeObserver = createAuthenticatedPeerRouteObserver(catalog, activationTarget); + await input.enable(activationTarget, sshInteraction, routeObserver?.observe); + await routeObserver?.flush(); const current = await catalog.resolve(activationTarget.profile.id).catch(() => undefined); if (!current || !sameResolvedRuntimeHostProfileTarget(current, activationTarget)) { await input.disable(activationTarget.profile.id); @@ -1325,6 +1329,64 @@ export function createDesktopRuntimeHostProfileService(input: { }; } +interface AuthenticatedPeerRouteObserver { + readonly observe: (endpoint: HostPeerEndpoint) => void; + readonly flush: () => Promise; +} + +function createAuthenticatedPeerRouteObserver( + catalog: RuntimeHostProfileCatalog, + target: ResolvedRuntimeHostProfile, +): AuthenticatedPeerRouteObserver | undefined { + if ( + target.profile.kind !== 'remote' || + target.profile.transport.kind !== 'libp2p-direct' || + !target.profileIncarnationId + ) return undefined; + const expectedPeerId = target.profile.transport.peerId; + const incarnation = { + profile: target.profile, + profileIncarnationId: target.profileIncarnationId, + } as const; + let pending = Promise.resolve(); + const observe = (endpoint: HostPeerEndpoint): void => { + if ( + endpoint.peerId !== expectedPeerId || + (endpoint.routeHints.length === 0 && endpoint.coordinationRelays.length === 0) + ) return; + pending = pending + .then(async () => { + await catalog.updateRemoteProfileIfCurrent(incarnation, (current) => { + if (current.transport.kind !== 'libp2p-direct') return current; + if ( + sameStrings(current.transport.routeHints, endpoint.routeHints) && + sameStrings(current.transport.coordinationRelays, endpoint.coordinationRelays) + ) return current; + return { + ...current, + transport: { + kind: 'libp2p-direct', + peerId: expectedPeerId, + routeHints: endpoint.routeHints, + coordinationRelays: endpoint.coordinationRelays, + }, + }; + }); + }) + .catch((error: unknown) => { + console.warn( + `[runtime-host] authenticated routes for profile ${target.profile.id} could not be saved:`, + asError(error), + ); + }); + }; + return { observe, flush: () => pending }; +} + +function sameStrings(left: readonly string[], right: readonly string[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); +} + function managedDirectPeerProfileId(sourceProfileId: string): string { const digest = createHash('sha256').update(sourceProfileId).digest('hex').slice(0, 32); return `direct-${digest}`; diff --git a/packages/cli/src/__tests__/runtime-host-cli-context.test.ts b/packages/cli/src/__tests__/runtime-host-cli-context.test.ts index 83fe1dad99..10c6bf6a29 100644 --- a/packages/cli/src/__tests__/runtime-host-cli-context.test.ts +++ b/packages/cli/src/__tests__/runtime-host-cli-context.test.ts @@ -303,6 +303,9 @@ test('remote CLI profiles pin root identity and resolve credential outside the p rebindIfCurrent: async () => { throw new Error('unexpected write'); }, + updateRemoteProfileIfCurrent: async () => { + throw new Error('unexpected write'); + }, mutateRemoteProfileIfCurrent: async () => { throw new Error('unexpected write'); }, @@ -573,6 +576,7 @@ function singleRemoteProfileCatalog(profile: RemoteRuntimeHostProfile): RuntimeH remove: async () => assert.fail('unexpected write'), removeIfCurrent: async () => assert.fail('unexpected write'), rebindIfCurrent: async () => assert.fail('unexpected write'), + updateRemoteProfileIfCurrent: async () => assert.fail('unexpected write'), mutateRemoteProfileIfCurrent: async () => assert.fail('unexpected write'), readRemoteProfileIfCurrent: async () => assert.fail('unexpected read'), }; diff --git a/packages/cli/src/__tests__/runtime-host-profile-command.test.ts b/packages/cli/src/__tests__/runtime-host-profile-command.test.ts index bf972eec2d..15fee776b2 100644 --- a/packages/cli/src/__tests__/runtime-host-profile-command.test.ts +++ b/packages/cli/src/__tests__/runtime-host-profile-command.test.ts @@ -236,6 +236,7 @@ function createProfileCatalogCapture(): { remove: async () => assert.fail('unexpected profile removal'), removeIfCurrent: async () => assert.fail('unexpected conditional profile removal'), rebindIfCurrent: async () => assert.fail('unexpected conditional profile rebind'), + updateRemoteProfileIfCurrent: async () => assert.fail('unexpected conditional update'), mutateRemoteProfileIfCurrent: async () => assert.fail('unexpected conditional mutation'), readRemoteProfileIfCurrent: async () => assert.fail('unexpected conditional read'), }; diff --git a/packages/runtime-host/src/__tests__/host-profile.test.ts b/packages/runtime-host/src/__tests__/host-profile.test.ts index fcf5fd00fa..0e8c6b05ce 100644 --- a/packages/runtime-host/src/__tests__/host-profile.test.ts +++ b/packages/runtime-host/src/__tests__/host-profile.test.ts @@ -632,6 +632,15 @@ describe('Runtime Host profiles', () => { false, ); assert.equal(staleMutationRan, false); + let staleUpdateRan = false; + assert.equal( + await catalog.updateRemoteProfileIfCurrent(firstIncarnation, (current) => { + staleUpdateRan = true; + return current; + }), + false, + ); + assert.equal(staleUpdateRan, false); }); test('pins a direct-peer profile to its PeerId while allowing route discovery to change', () => { diff --git a/packages/runtime-host/src/client/host-profile.ts b/packages/runtime-host/src/client/host-profile.ts index 1e77cb64bb..4ac5566570 100644 --- a/packages/runtime-host/src/client/host-profile.ts +++ b/packages/runtime-host/src/client/host-profile.ts @@ -225,6 +225,11 @@ export interface RuntimeHostProfileCatalog { readonly rebound: boolean; readonly document: RuntimeHostProfileDocument; }>; + /** Update mutable profile metadata while this exact profile lifetime remains current. */ + updateRemoteProfileIfCurrent( + target: RuntimeHostRemoteProfileIncarnation, + update: (profile: RemoteRuntimeHostProfile) => RemoteRuntimeHostProfile, + ): Promise; /** Serialize one sidecar mutation with catalog updates while this profile lifetime remains current. */ mutateRemoteProfileIfCurrent( target: RuntimeHostRemoteProfileIncarnation, @@ -1008,6 +1013,42 @@ class FileRuntimeHostProfileCatalog implements RuntimeHostProfileCatalog { }); } + updateRemoteProfileIfCurrent( + target: RuntimeHostRemoteProfileIncarnation, + update: (profile: RemoteRuntimeHostProfile) => RemoteRuntimeHostProfile, + ): Promise { + const expectedProfile = decodeRemoteRuntimeHostProfile(target.profile); + const expectedIncarnationId = requireProfileIncarnationId(target.profileIncarnationId); + return this.#exclusive(async () => { + const current = await this.#readSnapshot(); + const profile = current.profiles.find( + (candidate): candidate is RemoteRuntimeHostProfile => + candidate.id === expectedProfile.id && candidate.kind === 'remote', + ); + if (!profile || !sameRemoteRuntimeHostProfileTarget(profile, expectedProfile)) return false; + const credential = await this.credentials.get(profile); + if (credential?.profileIncarnationId !== expectedIncarnationId) return false; + const value = update(profile); + if (value === profile) return true; + const updated = decodeRemoteRuntimeHostProfile(value); + if ( + updated.id !== profile.id || + !sameRemoteRuntimeHostProfileTarget(updated, profile) || + runtimeHostProfileAccess(updated) !== runtimeHostProfileAccess(profile) + ) { + throw new Error('A Runtime Host profile metadata update must retain its connection'); + } + const next = decodeRuntimeHostProfileDocument({ + schemaVersion: PROFILE_SCHEMA_VERSION, + profiles: current.profiles.map((candidate) => + candidate.id === updated.id ? updated : candidate, + ), + }); + await writeProfileDocument(this.path, next); + return true; + }); + } + mutateRemoteProfileIfCurrent( target: RuntimeHostRemoteProfileIncarnation, mutation: (profile: RemoteRuntimeHostProfile) => Promise,