Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 15 additions & 8 deletions apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,21 +545,28 @@ 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');
}
if (target.profile.kind === 'remote' && !target.credential) {
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");
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/runtime-host-desktop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface RuntimeHostDesktopManager {
unobserveSession(observerId: string): Promise<void>;
enable(
profileTarget: DesktopRuntimeHostCandidateStartInput['profileTarget'],
onHostStatus?: (status: HostStatusResult) => void,
): Promise<void>;
mountGuest(
profileTarget: NonNullable<DesktopRuntimeHostCandidateStartInput['profileTarget']>,
Expand Down Expand Up @@ -493,13 +494,14 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {

async enable(
profileTarget: DesktopRuntimeHostCandidateStartInput['profileTarget'],
onHostStatus?: (status: HostStatusResult) => void,
): Promise<void> {
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),
);
}

Expand Down
64 changes: 63 additions & 1 deletion apps/desktop/src/main/runtime-host-profile-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -295,6 +296,7 @@ export function createDesktopRuntimeHostProfileService(input: {
readonly enable: (
target: ResolvedRuntimeHostProfile,
sshInteraction: "terminal" | "batch",
onPeerEndpoint?: (endpoint: HostPeerEndpoint) => void,
) => Promise<void>;
readonly disable: (profileId: string) => Promise<void>;
readonly finalizePairing: (profileId: string) => Promise<void>;
Expand Down Expand Up @@ -488,7 +490,9 @@ export function createDesktopRuntimeHostProfileService(input: {
): Promise<void> => {
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);
Expand Down Expand Up @@ -1325,6 +1329,64 @@ export function createDesktopRuntimeHostProfileService(input: {
};
}

interface AuthenticatedPeerRouteObserver {
readonly observe: (endpoint: HostPeerEndpoint) => void;
readonly flush: () => Promise<void>;
}

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}`;
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/__tests__/runtime-host-cli-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand Down Expand Up @@ -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'),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
};
Expand Down
9 changes: 9 additions & 0 deletions packages/runtime-host/src/__tests__/host-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
41 changes: 41 additions & 0 deletions packages/runtime-host/src/client/host-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
/** Serialize one sidecar mutation with catalog updates while this profile lifetime remains current. */
mutateRemoteProfileIfCurrent(
target: RuntimeHostRemoteProfileIncarnation,
Expand Down Expand Up @@ -1008,6 +1013,42 @@ class FileRuntimeHostProfileCatalog implements RuntimeHostProfileCatalog {
});
}

updateRemoteProfileIfCurrent(
target: RuntimeHostRemoteProfileIncarnation,
update: (profile: RemoteRuntimeHostProfile) => RemoteRuntimeHostProfile,
): Promise<boolean> {
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<void>,
Expand Down