Skip to content

Commit 0673602

Browse files
committed
fix(runtime-host): harden remote onboarding recovery
Align managed development releases with the generated package version grammar. Preserve published credential authority after uncertain commits, persist pending Desktop pairing transactions across process loss, and dismiss interactive SSH presentation when cancellation begins. Generated-by: Codex
1 parent b2543c8 commit 0673602

18 files changed

Lines changed: 829 additions & 225 deletions

apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@ import type { DesktopRuntimeHostProfileService } from '../runtime-host-profile-s
77
test('persists a verified SSH profile without projecting its credential', async () => {
88
const handlers = new Map<string, (...args: unknown[]) => unknown>();
99
const events: unknown[] = [];
10+
let resetAfterCompletion: Promise<unknown> | undefined;
1011
let saved: DesktopRuntimeHostProfileAddInput | undefined;
11-
const profiles: DesktopRuntimeHostProfileService = {
12-
getSnapshot: async () => ({ defaultProfileId: 'local', entries: [] }),
13-
addAndEnable: async () => assert.fail('manual profile path must not be used'),
12+
const profiles: Pick<DesktopRuntimeHostProfileService, 'addAndEnableVerified'> = {
1413
addAndEnableVerified: async (input) => {
1514
saved = input;
1615
return { profileId: input.profile.id };
1716
},
18-
setEnabled: async () => assert.fail('not used'),
19-
setDefault: async () => assert.fail('not used'),
20-
remove: async () => assert.fail('not used'),
2117
};
2218
const onboarding = createDesktopRuntimeHostOnboarding({
2319
ipcMain: {
@@ -35,7 +31,14 @@ test('persists a verified SSH profile without projecting its credential', async
3531
credential: 'secret-access-token',
3632
};
3733
},
38-
send: (snapshot) => events.push(snapshot),
34+
send: (snapshot) => {
35+
events.push(snapshot);
36+
if (snapshot.kind === 'complete') {
37+
resetAfterCompletion = handlers.get('runtime-host-onboarding:reset')?.({}) as
38+
| Promise<unknown>
39+
| undefined;
40+
}
41+
},
3942
});
4043
const start = handlers.get('runtime-host-onboarding:start');
4144
assert.ok(start);
@@ -55,10 +58,46 @@ test('persists a verified SSH profile without projecting its credential', async
5558
});
5659
assert.equal(saved?.credential, 'secret-access-token');
5760
assert.doesNotMatch(JSON.stringify(events), /secret-access-token/u);
61+
await resetAfterCompletion;
62+
const getSnapshot = handlers.get('runtime-host-onboarding:getSnapshot');
63+
assert.ok(getSnapshot);
64+
assert.equal((await getSnapshot({}) as { kind?: string }).kind, 'idle');
5865
await onboarding.close();
5966
assert.equal(handlers.size, 0);
6067
});
6168

69+
test('projects invalid setup input as a recoverable failure', async () => {
70+
const handlers = new Map<string, (...args: unknown[]) => unknown>();
71+
const onboarding = createDesktopRuntimeHostOnboarding({
72+
ipcMain: {
73+
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
74+
removeHandler: (channel) => handlers.delete(channel),
75+
},
76+
clientInstanceId: 'stable-client',
77+
profiles: {
78+
addAndEnableVerified: async () => assert.fail('not used'),
79+
},
80+
setupPackage: { kind: 'npm', specifier: 'maka-agent@0.2.0' },
81+
runSetup: async () => assert.fail('invalid input must not start SSH'),
82+
send: () => undefined,
83+
});
84+
85+
const result = await handlers.get('runtime-host-onboarding:start')?.({}, {
86+
destination: '',
87+
});
88+
assert.deepEqual(result, {
89+
kind: 'failed',
90+
message: 'Remote Runtime Host setup input is invalid',
91+
revision: 1,
92+
});
93+
await handlers.get('runtime-host-onboarding:reset')?.({});
94+
assert.deepEqual(await handlers.get('runtime-host-onboarding:getSnapshot')?.({}), {
95+
kind: 'idle',
96+
revision: 2,
97+
});
98+
await onboarding.close();
99+
});
100+
62101
test('finishes Host pairing after the cancellable SSH phase has completed', async () => {
63102
const handlers = new Map<string, (...args: unknown[]) => unknown>();
64103
let finishPairing!: (value: { profileId: string }) => void;
@@ -79,16 +118,11 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
79118
}>((resolve) => {
80119
finishSetup = resolve;
81120
});
82-
const profiles: DesktopRuntimeHostProfileService = {
83-
getSnapshot: async () => ({ defaultProfileId: 'local', entries: [] }),
84-
addAndEnable: async () => assert.fail('manual profile path must not be used'),
121+
const profiles: Pick<DesktopRuntimeHostProfileService, 'addAndEnableVerified'> = {
85122
addAndEnableVerified: async () => {
86123
pairingStarted = true;
87124
return pairing;
88125
},
89-
setEnabled: async () => assert.fail('not used'),
90-
setDefault: async () => assert.fail('not used'),
91-
remove: async () => assert.fail('not used'),
92126
};
93127
const onboarding = createDesktopRuntimeHostOnboarding({
94128
ipcMain: {

apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { afterEach, test } from "node:test";
66
import {
77
createClientRuntimeHostProfileCatalog,
88
LOCAL_RUNTIME_HOST_PROFILE,
9+
RuntimeHostPermanentReconnectError,
910
type ResolvedRuntimeHostProfile,
1011
} from "@maka/runtime-host/client";
1112
import {
@@ -306,6 +307,57 @@ test("refreshes the existing profile when managed setup pairs the same Host agai
306307
assert.deepEqual((await catalog.read()).profiles.map((profile) => profile.id), [PROFILE.id]);
307308
});
308309

310+
test("finishes a persisted pairing after Desktop restarts before finalization", async () => {
311+
const root = await clientRoot();
312+
const catalog = await stageInterruptedPairing(root);
313+
const startup = await resolveDesktopRuntimeHostStartup(root, { catalog });
314+
assert.equal(startup.pairingIntents.length, 1);
315+
const finalized: string[] = [];
316+
const service = createDesktopRuntimeHostProfileService({
317+
clientDataRoot: root,
318+
startup,
319+
catalog,
320+
states: () => [connectingLocal()],
321+
enable: async () => undefined,
322+
disable: async () => undefined,
323+
setDefault: () => undefined,
324+
finalizePairing: async (profileId) => {
325+
finalized.push(profileId);
326+
},
327+
});
328+
329+
await service.recoverPendingPairings();
330+
331+
assert.deepEqual(finalized, [PROFILE.id]);
332+
assert.equal((await catalog.resolve(PROFILE.id)).credential, "new-token");
333+
assert.equal((await resolveDesktopRuntimeHostStartup(root, { catalog })).pairingIntents.length, 0);
334+
});
335+
336+
test("restores the previous credential when an interrupted pairing can no longer authenticate", async () => {
337+
const root = await clientRoot();
338+
const catalog = await stageInterruptedPairing(root);
339+
const startup = await resolveDesktopRuntimeHostStartup(root, { catalog });
340+
const service = createDesktopRuntimeHostProfileService({
341+
clientDataRoot: root,
342+
startup,
343+
catalog,
344+
states: () => [connectingLocal()],
345+
enable: async (target) => {
346+
if (target.credential === "new-token") {
347+
throw new RuntimeHostPermanentReconnectError("pairing credential expired");
348+
}
349+
},
350+
disable: async () => undefined,
351+
setDefault: () => undefined,
352+
finalizePairing: async () => assert.fail("an expired credential cannot be finalized"),
353+
});
354+
355+
await service.recoverPendingPairings();
356+
357+
assert.equal((await catalog.resolve(PROFILE.id)).credential, "old-token");
358+
assert.equal((await resolveDesktopRuntimeHostStartup(root, { catalog })).pairingIntents.length, 0);
359+
});
360+
309361
for (const failureAt of ["connection", "finalization"] as const) {
310362
test(`restores an existing profile when replacement ${failureAt} fails`, async () => {
311363
const root = await clientRoot();
@@ -401,6 +453,43 @@ async function clientRoot(): Promise<string> {
401453
return root;
402454
}
403455

456+
async function stageInterruptedPairing(root: string) {
457+
const catalog = createClientRuntimeHostProfileCatalog(root);
458+
await catalog.create(PROFILE, "old-token");
459+
await writeFile(
460+
join(root, "runtime-host-profile-selection.json"),
461+
`${JSON.stringify({
462+
schemaVersion: 2,
463+
defaultProfileId: "local",
464+
enabledRemoteProfileIds: [PROFILE.id],
465+
})}\n`,
466+
);
467+
const startup = await resolveDesktopRuntimeHostStartup(root, { catalog });
468+
let finalizationStarted!: () => void;
469+
const started = new Promise<void>((resolve) => {
470+
finalizationStarted = resolve;
471+
});
472+
const service = createDesktopRuntimeHostProfileService({
473+
clientDataRoot: root,
474+
startup,
475+
catalog,
476+
states: () => [connectingLocal()],
477+
enable: async () => undefined,
478+
disable: async () => undefined,
479+
setDefault: () => undefined,
480+
finalizePairing: async () => {
481+
finalizationStarted();
482+
await new Promise<never>(() => undefined);
483+
},
484+
});
485+
void service.addAndEnableVerified({
486+
profile: { ...PROFILE, name: "Updated office" },
487+
credential: "new-token",
488+
});
489+
await started;
490+
return catalog;
491+
}
492+
404493
function connectingLocal(): RuntimeHostDesktopTargetState {
405494
return connecting({ profile: LOCAL_RUNTIME_HOST_PROFILE });
406495
}

apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test('does not reopen a cancelled SSH prompt for late process output', async ()
5959
await assert.rejects(opening, /SSH exited/u);
6060

6161
assert.deepEqual(harness.pty.killSignals, ['SIGTERM', 'SIGKILL']);
62-
assert.deepEqual(harness.eventKinds(), ['opened', 'data']);
62+
assert.deepEqual(harness.eventKinds(), ['opened', 'data', 'dismissed']);
6363
assert.deepEqual(await harness.getSnapshot(), { kind: 'idle', revision: 3 });
6464
await harness.terminal.close();
6565
});
@@ -126,11 +126,14 @@ test('force-stops a cancelled setup when SSH ignores graceful termination', asyn
126126
() => undefined,
127127
);
128128
await waitFor(() => harness.pty.hasDataListener());
129+
harness.pty.emitData('Password: ');
129130

130131
controller.abort();
131132

132133
await assert.rejects(setup, /aborted/u);
133134
assert.deepEqual(harness.pty.killSignals, ['SIGTERM', 'SIGKILL']);
135+
assert.deepEqual(harness.eventKinds(), ['opened', 'data', 'dismissed']);
136+
assert.deepEqual(await harness.getSnapshot(), { kind: 'idle', revision: 3 });
134137
await harness.terminal.close();
135138
});
136139

@@ -164,7 +167,10 @@ test('uploads a development release archive before running the same remote setup
164167
await waitFor(() => launches.length === 1);
165168
assert.equal(launches[0]?.file, 'scp');
166169
assert.match(launches[0]?.args.at(-2) ?? '', /maka-agent-development\.tgz$/u);
167-
assert.match(launches[0]?.args.at(-1) ?? '', /^operator@example\.com:\/tmp\/maka-runtime-host-setup-.+\.tgz$/u);
170+
assert.match(
171+
launches[0]?.args.at(-1) ?? '',
172+
/^operator@example\.com:\.\/\.maka-runtime-host-setup-.+\.tgz$/u,
173+
);
168174
launches[0]?.pty.exit(0);
169175

170176
await waitFor(() => launches.length === 2);
@@ -175,10 +181,10 @@ test('uploads a development release archive before running the same remote setup
175181
/npx.*--package.*maka-runtime-host-setup-.+\.tgz.*maka.*runtime-host.*setup/u,
176182
);
177183
assert.match(remoteCommand, /--defer-pairing-commit/u);
184+
assert.match(remoteCommand, /cd.*\$HOME/u);
178185
assert.match(remoteCommand, /rm -f/u);
179186
assert.match(remoteCommand, /exec \/bin\/sh -c/u);
180187
assert.match(remoteCommand, /maka_setup_exit/u);
181-
assert.doesNotMatch(remoteCommand, /\bstatus=/u);
182188
launches[1]?.pty.exit(255);
183189
await assert.rejects(setup, /exited with code 255/u);
184190

apps/desktop/src/main/runtime-host-boot.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,12 @@ runtimeHostManager = await startRuntimeHostDesktopManager(
708708
});
709709
wireLifecycle();
710710
runtimeHostManager.setDefaultProfile(runtimeHostStartup.preferences.defaultProfileId);
711+
const pendingPairingProfileIds = new Set(
712+
runtimeHostStartup.pairingIntents.map((intent) => intent.target.profile.id),
713+
);
714+
void runtimeHostProfileService
715+
.recoverPendingPairings()
716+
.catch((error) => console.error("[runtime-host] pairing recovery failed:", error));
711717
const unavailableDefault = runtimeHostStartup.unavailable.get(
712718
runtimeHostStartup.preferences.defaultProfileId,
713719
);
@@ -731,6 +737,7 @@ if (unavailableDefault) {
731737
// enabled Host behind it. Other SSH targets fail batch-mode and remain retryable.
732738
for (const target of runtimeHostStartup.remotes) {
733739
if (target.profile.kind !== "remote" || !target.credential) continue;
740+
if (pendingPairingProfileIds.has(target.profile.id)) continue;
734741
void runtimeHostManager
735742
.enable({
736743
profile: target.profile,

apps/desktop/src/main/runtime-host-onboarding.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type OnboardingState = DesktopRuntimeHostOnboardingSnapshot extends infer Snapsh
2020
export function createDesktopRuntimeHostOnboarding(input: {
2121
readonly ipcMain: Pick<IpcMain, 'handle' | 'removeHandler'>;
2222
readonly clientInstanceId: string;
23-
readonly profiles: DesktopRuntimeHostProfileService;
23+
readonly profiles: Pick<DesktopRuntimeHostProfileService, 'addAndEnableVerified'>;
2424
readonly runSetup: (
2525
input: DesktopRuntimeHostSshSetupInput,
2626
onProgress: (frame: { readonly phase: RuntimeHostSetupPhase }) => void,
@@ -53,8 +53,16 @@ export function createDesktopRuntimeHostOnboarding(input: {
5353
};
5454

5555
const start = (value: unknown): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
56-
if (active) throw new Error('A remote Runtime Host setup is already in progress');
57-
const request = requireOnboardingInput(value);
56+
if (active) return active.task;
57+
let request: DesktopRuntimeHostOnboardingInput;
58+
try {
59+
request = requireOnboardingInput(value);
60+
} catch (error) {
61+
return Promise.resolve(publish({
62+
kind: 'failed',
63+
message: error instanceof Error ? error.message : String(error),
64+
}));
65+
}
5866
const abort = new AbortController();
5967
publish({ kind: 'running', phase: 'connecting_ssh' });
6068
const task = Promise.resolve().then(() => run(request, abort.signal)).finally(() => {
@@ -145,8 +153,9 @@ export function createDesktopRuntimeHostOnboarding(input: {
145153
await current.task;
146154
return true;
147155
});
148-
input.ipcMain.handle(channels[3], () => {
149-
if (active) throw new Error('Remote Runtime Host setup is still running');
156+
input.ipcMain.handle(channels[3], async () => {
157+
if (snapshot.kind === 'running') return;
158+
await active?.task;
150159
publish({ kind: 'idle' });
151160
});
152161

0 commit comments

Comments
 (0)