@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +6 -4
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +1 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +15 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -78
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +46 -34
- package/src/lib/nginx-ingress.ts +40 -15
- package/src/lib/pair.ts +1 -1
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/sync-cloud-assistants.ts +19 -19
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
collectWatchTargets,
|
|
9
9
|
dockerResourceNames,
|
|
10
10
|
ensureDockerDaemonRunning,
|
|
11
|
+
pullImage,
|
|
11
12
|
resolveAvatarDevicePath,
|
|
12
13
|
resolveDockerHatchMode,
|
|
13
14
|
resolveDockerProviderCredentialSetupAction,
|
|
@@ -158,6 +159,38 @@ describe("resolveDockerProviderCredentialSetupAction", () => {
|
|
|
158
159
|
}),
|
|
159
160
|
).toBe("skip");
|
|
160
161
|
});
|
|
162
|
+
|
|
163
|
+
test("detached hatch with a pre-collected key configures instead of deferring", () => {
|
|
164
|
+
expect(
|
|
165
|
+
resolveDockerProviderCredentialSetupAction({
|
|
166
|
+
provider: "fireworks",
|
|
167
|
+
guardianAccessToken: "guardian-token",
|
|
168
|
+
detached: true,
|
|
169
|
+
precollectedKey: "fw-key-123",
|
|
170
|
+
}),
|
|
171
|
+
).toBe("configure");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("pre-collected key without a guardian token reports missing-token (no silent defer)", () => {
|
|
175
|
+
expect(
|
|
176
|
+
resolveDockerProviderCredentialSetupAction({
|
|
177
|
+
provider: "fireworks",
|
|
178
|
+
detached: true,
|
|
179
|
+
precollectedKey: "fw-key-123",
|
|
180
|
+
}),
|
|
181
|
+
).toBe("missing-token");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("an explicitly skipped key still counts as answered — configure runs and reports the gap", () => {
|
|
185
|
+
expect(
|
|
186
|
+
resolveDockerProviderCredentialSetupAction({
|
|
187
|
+
provider: "fireworks",
|
|
188
|
+
guardianAccessToken: "guardian-token",
|
|
189
|
+
detached: true,
|
|
190
|
+
precollectedKey: "",
|
|
191
|
+
}),
|
|
192
|
+
).toBe("configure");
|
|
193
|
+
});
|
|
161
194
|
});
|
|
162
195
|
|
|
163
196
|
describe("buildServiceRunArgs — gateway", () => {
|
|
@@ -384,25 +417,48 @@ describe("collectWatchTargets", () => {
|
|
|
384
417
|
});
|
|
385
418
|
|
|
386
419
|
/**
|
|
387
|
-
* Build a fake `exec`
|
|
388
|
-
*
|
|
389
|
-
* (
|
|
390
|
-
*
|
|
420
|
+
* Build a STATEFUL fake `exec` modeling the Docker daemon + Colima interplay:
|
|
421
|
+
* `docker info` succeeds only while `daemonUp`; a `colima start` heals the
|
|
422
|
+
* daemon (unless the fake is told to model the zombie state, where Colima's
|
|
423
|
+
* state file says "already running, ignoring" but the daemon stays dead —
|
|
424
|
+
* only the stop → start cycle recovers). Injected into
|
|
425
|
+
* `ensureDockerDaemonRunning` so these cases need no `mock.module` (which is
|
|
426
|
+
* process-global and would leak across the CLI's single-process test run) and
|
|
427
|
+
* no real Docker/Colima subprocesses.
|
|
391
428
|
*/
|
|
392
|
-
function fakeExec(
|
|
429
|
+
function fakeExec(
|
|
430
|
+
opts: {
|
|
431
|
+
/** Daemon already reachable when the test starts. */
|
|
432
|
+
daemonUpAtStart?: boolean;
|
|
433
|
+
/** `colima start` exits nonzero instead of healing. */
|
|
434
|
+
startFails?: boolean;
|
|
435
|
+
/** First `colima start` is a zombie no-op that does NOT heal the daemon. */
|
|
436
|
+
zombieFirstStart?: boolean;
|
|
437
|
+
} = {},
|
|
438
|
+
) {
|
|
393
439
|
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
440
|
+
let daemonUp = opts.daemonUpAtStart ?? false;
|
|
441
|
+
let starts = 0;
|
|
394
442
|
const exec = async (cmd: string, args: string[] = []): Promise<void> => {
|
|
395
443
|
calls.push({ cmd, args });
|
|
396
|
-
if (cmd === "docker" && args[0] === "info"
|
|
397
|
-
throw new Error("Cannot connect to the Docker daemon");
|
|
444
|
+
if (cmd === "docker" && args[0] === "info") {
|
|
445
|
+
if (!daemonUp) throw new Error("Cannot connect to the Docker daemon");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (cmd === "colima" && args[0] === "start") {
|
|
449
|
+
starts++;
|
|
450
|
+
if (opts.startFails) throw new Error("colima start exited 1");
|
|
451
|
+
if (!opts.zombieFirstStart || starts > 1) daemonUp = true;
|
|
452
|
+
return;
|
|
398
453
|
}
|
|
399
|
-
if (cmd === "colima" && args[0] === "
|
|
400
|
-
|
|
454
|
+
if (cmd === "colima" && args[0] === "stop") {
|
|
455
|
+
daemonUp = false;
|
|
456
|
+
return;
|
|
401
457
|
}
|
|
402
458
|
};
|
|
403
459
|
const ran = (cmd: string, sub: string): boolean =>
|
|
404
460
|
calls.some((c) => c.cmd === cmd && c.args[0] === sub);
|
|
405
|
-
return { exec
|
|
461
|
+
return { exec, ran };
|
|
406
462
|
}
|
|
407
463
|
|
|
408
464
|
describe("ensureDockerDaemonRunning", () => {
|
|
@@ -410,30 +466,122 @@ describe("ensureDockerDaemonRunning", () => {
|
|
|
410
466
|
// Colima, because that switches the active `docker context` and points
|
|
411
467
|
// `docker start` at a VM that never held the instance's containers.
|
|
412
468
|
test("reuses an already-reachable daemon (e.g. Docker Desktop) and never touches Colima", async () => {
|
|
413
|
-
const { exec, ran } = fakeExec({
|
|
469
|
+
const { exec, ran } = fakeExec({ daemonUpAtStart: true });
|
|
414
470
|
|
|
415
471
|
await ensureDockerDaemonRunning(exec);
|
|
416
472
|
|
|
417
473
|
expect(ran("docker", "info")).toBe(true);
|
|
418
|
-
expect(ran("colima", "status")).toBe(false);
|
|
419
474
|
expect(ran("colima", "start")).toBe(false);
|
|
475
|
+
expect(ran("colima", "stop")).toBe(false);
|
|
420
476
|
});
|
|
421
477
|
|
|
422
|
-
test("starts Colima
|
|
423
|
-
const { exec, ran } = fakeExec(
|
|
478
|
+
test("starts Colima when no daemon is reachable and confirms the daemon answered", async () => {
|
|
479
|
+
const { exec, ran } = fakeExec();
|
|
424
480
|
|
|
425
|
-
await ensureDockerDaemonRunning(exec);
|
|
481
|
+
await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
|
|
426
482
|
|
|
427
483
|
expect(ran("docker", "info")).toBe(true);
|
|
428
484
|
expect(ran("colima", "start")).toBe(true);
|
|
485
|
+
expect(ran("colima", "stop")).toBe(false);
|
|
429
486
|
});
|
|
430
487
|
|
|
431
|
-
|
|
432
|
-
|
|
488
|
+
// Regression (v0.12.8 release feedback): a zombie Colima — stale state file
|
|
489
|
+
// saying "already running" while the daemon inside is dead — used to sail
|
|
490
|
+
// through wake (no `colima status` oracle anymore) and leave every later
|
|
491
|
+
// docker command dying on the unreachable socket.
|
|
492
|
+
test("recovers a zombie Colima: start no-ops, daemon stays dead → stop --force → start → daemon up", async () => {
|
|
493
|
+
const { exec, ran } = fakeExec({ zombieFirstStart: true });
|
|
433
494
|
|
|
434
|
-
await ensureDockerDaemonRunning(exec);
|
|
495
|
+
await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
|
|
435
496
|
|
|
436
|
-
expect(ran("colima", "
|
|
437
|
-
expect(ran("colima", "
|
|
497
|
+
expect(ran("colima", "start")).toBe(true);
|
|
498
|
+
expect(ran("colima", "stop")).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("never deletes the Colima VM — the VM disk holds every image and volume", async () => {
|
|
502
|
+
const { exec, ran } = fakeExec({ zombieFirstStart: true });
|
|
503
|
+
|
|
504
|
+
await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
|
|
505
|
+
|
|
506
|
+
expect(ran("colima", "delete")).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("throws an actionable error when even the restart cannot bring the daemon up", () => {
|
|
510
|
+
// A start that always exits nonzero: the first failure surfaces its
|
|
511
|
+
// output, the recovery stop+start runs, and the final wait still finds
|
|
512
|
+
// no daemon → the helper throws instead of letting hatch proceed.
|
|
513
|
+
const { exec } = fakeExec({ startFails: true });
|
|
514
|
+
expect(
|
|
515
|
+
ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 }),
|
|
516
|
+
).rejects.toThrow(/Failed to restart Colima/);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("pullImage", () => {
|
|
521
|
+
function fakePull(
|
|
522
|
+
opts: {
|
|
523
|
+
failFirstPullWith?: string;
|
|
524
|
+
failAllPullsWith?: string;
|
|
525
|
+
} = {},
|
|
526
|
+
) {
|
|
527
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
528
|
+
let pulls = 0;
|
|
529
|
+
const exec = async (cmd: string, args: string[] = []): Promise<void> => {
|
|
530
|
+
calls.push({ cmd, args });
|
|
531
|
+
if (cmd === "docker" && args[0] === "pull") {
|
|
532
|
+
pulls++;
|
|
533
|
+
if (opts.failAllPullsWith) throw new Error(opts.failAllPullsWith);
|
|
534
|
+
if (opts.failFirstPullWith && pulls === 1) {
|
|
535
|
+
throw new Error(opts.failFirstPullWith);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// `docker logout` (and anything else) succeeds silently.
|
|
539
|
+
};
|
|
540
|
+
const ran = (sub: string): boolean =>
|
|
541
|
+
calls.some((c) => c.cmd === "docker" && c.args[0] === sub);
|
|
542
|
+
return { exec, ran };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
test("pulls without touching stored credentials on success", async () => {
|
|
546
|
+
const { exec, ran } = fakePull();
|
|
547
|
+
|
|
548
|
+
await pullImage("mycadet/mycadet-assistant:v1", () => {}, exec);
|
|
549
|
+
|
|
550
|
+
expect(ran("pull")).toBe(true);
|
|
551
|
+
expect(ran("logout")).toBe(false);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("clears stale Docker Hub credentials and retries once on an expired-token error", async () => {
|
|
555
|
+
const { exec, ran } = fakePull({
|
|
556
|
+
failFirstPullWith:
|
|
557
|
+
"Error response from daemon: authentication required - personal access token is expired",
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await pullImage("mycadet/mycadet-assistant:v1", () => {}, exec);
|
|
561
|
+
|
|
562
|
+
expect(ran("logout")).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("wraps the failure with actionable copy when the auth retry also fails", () => {
|
|
566
|
+
const { exec } = fakePull({
|
|
567
|
+
failAllPullsWith:
|
|
568
|
+
"Error response from daemon: authentication required - personal access token is expired",
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
expect(
|
|
572
|
+
pullImage("mycadet/mycadet-assistant:v1", () => {}, exec),
|
|
573
|
+
).rejects.toThrow("Failed to pull mycadet/mycadet-assistant:v1");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("rethrows non-auth pull errors raw and never logs out", () => {
|
|
577
|
+
const { exec, ran } = fakePull({
|
|
578
|
+
failAllPullsWith:
|
|
579
|
+
"Cannot connect to the Docker daemon at unix:///var/run/docker.sock",
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(
|
|
583
|
+
pullImage("mycadet/mycadet-assistant:v1", () => {}, exec),
|
|
584
|
+
).rejects.toThrow("Cannot connect to the Docker daemon");
|
|
585
|
+
expect(ran("logout")).toBe(false);
|
|
438
586
|
});
|
|
439
587
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { pollJobUntilDone } from "../job-polling.js";
|
|
4
|
-
import type { UnifiedJobStatus } from "../
|
|
4
|
+
import type { UnifiedJobStatus } from "../cloud-client.js";
|
|
5
5
|
|
|
6
6
|
describe("pollJobUntilDone", () => {
|
|
7
7
|
test("returns terminal 'complete' after N processing polls", async () => {
|
|
@@ -203,7 +203,7 @@ describe("pollJobUntilDone", () => {
|
|
|
203
203
|
expect(refreshes).toBe(1);
|
|
204
204
|
expect(calls).toBe(2);
|
|
205
205
|
// The 401 branch logs its own distinct warning (not the generic
|
|
206
|
-
// "polling failed
|
|
206
|
+
// "polling failed:" one) so operators can distinguish an
|
|
207
207
|
// auth refresh from a transient-error retry in the output.
|
|
208
208
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
209
209
|
expect.stringContaining("refreshing auth"),
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { LocalInstanceResources } from "../assistant-config.js";
|
|
4
|
+
import { gatewayCliInvocation } from "../local.js";
|
|
5
|
+
|
|
6
|
+
function fakeResources(instanceDir: string): LocalInstanceResources {
|
|
7
|
+
// gatewayCliInvocation only reads instanceDir; the rest of the resources
|
|
8
|
+
// shape is irrelevant here.
|
|
9
|
+
return { instanceDir } as unknown as LocalInstanceResources;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("gatewayCliInvocation", () => {
|
|
13
|
+
test("runs the gateway package's CLI entry through bun", () => {
|
|
14
|
+
const invocation = gatewayCliInvocation();
|
|
15
|
+
expect(invocation.args[0]).toBe("run");
|
|
16
|
+
expect(invocation.args[1]).toMatch(/gateway\/src\/cli\/index\.ts$/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("pins CADET_WORKSPACE_DIR to the instance dir for named instances", () => {
|
|
20
|
+
const invocation = gatewayCliInvocation(
|
|
21
|
+
fakeResources("/data/assistants/testy"),
|
|
22
|
+
);
|
|
23
|
+
expect(invocation.env.CADET_WORKSPACE_DIR).toBe(
|
|
24
|
+
"/data/assistants/testy/workspace",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("pins GATEWAY_SECURITY_DIR to the instance dir for named instances", () => {
|
|
29
|
+
const invocation = gatewayCliInvocation(
|
|
30
|
+
fakeResources("/data/assistants/testy"),
|
|
31
|
+
);
|
|
32
|
+
expect(invocation.env.GATEWAY_SECURITY_DIR).toBe(
|
|
33
|
+
"/data/assistants/testy/gateway-security",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("leaves the workspace resolution untouched for the default instance", () => {
|
|
38
|
+
delete process.env.CADET_WORKSPACE_DIR;
|
|
39
|
+
const invocation = gatewayCliInvocation();
|
|
40
|
+
expect(invocation.env.CADET_WORKSPACE_DIR).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -14,7 +14,7 @@ const TOKEN = "local-bearer-token";
|
|
|
14
14
|
|
|
15
15
|
// All tests in this file exercise the local/docker code path (cloud="local"),
|
|
16
16
|
// which builds `{runtimeUrl}/v1/migrations/<subpath>` URLs and uses
|
|
17
|
-
// guardian-token bearer auth. The
|
|
17
|
+
// guardian-token bearer auth. The cloud path (cloud="cadet") is covered
|
|
18
18
|
// by `runtime-url.test.ts` (URL construction) and the teleport tests
|
|
19
19
|
// (call-site wiring).
|
|
20
20
|
const ENTRY: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId"> = {
|
|
@@ -384,7 +384,7 @@ describe("localRuntimePollJobStatus", () => {
|
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
// ---------------------------------------------------------------------------
|
|
387
|
-
//
|
|
387
|
+
// Cloud assistants (cloud="cadet") route through the cloud's
|
|
388
388
|
// wildcard runtime proxy at `/v1/assistants/<id>/migrations/...` with
|
|
389
389
|
// platform-token auth (NOT guardian-token bearer). This block asserts the
|
|
390
390
|
// actual URL and headers built by the helpers — not mocked, not abstracted.
|
|
@@ -588,7 +588,7 @@ describe("localRuntimeIdentity", () => {
|
|
|
588
588
|
});
|
|
589
589
|
|
|
590
590
|
// ---------------------------------------------------------------------
|
|
591
|
-
// 401-retry parity with
|
|
591
|
+
// 401-retry parity with cloudRequestSignedUrl (Codex P2 regression
|
|
592
592
|
// guard). localRuntimeIdentity is the first network call in the
|
|
593
593
|
// backup/teleport export flow for cadet-cloud assistants, so a stale
|
|
594
594
|
// Cadet-Organization-Id cache entry would surface as a hard abort
|
|
@@ -613,7 +613,7 @@ describe("localRuntimeIdentity", () => {
|
|
|
613
613
|
orgIdFetchedAs.push(`org-${orgIdFetchedAs.length + 1}`);
|
|
614
614
|
return new Response(
|
|
615
615
|
JSON.stringify({
|
|
616
|
-
results: [{ id: orgIdFetchedAs[orgIdFetchedAs.length - 1]
|
|
616
|
+
results: [{ id: orgIdFetchedAs[orgIdFetchedAs.length - 1]!, name: "My Cadet" }],
|
|
617
617
|
}),
|
|
618
618
|
{ status: 200 },
|
|
619
619
|
);
|
|
@@ -49,7 +49,7 @@ describe("resolveRuntimeMigrationUrl", () => {
|
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
test("cadet (
|
|
52
|
+
test("cadet (cloud-managed) cloud uses wildcard-proxy /v1/assistants/<id>/migrations/<subpath>", () => {
|
|
53
53
|
const entry = makeEntry({
|
|
54
54
|
cloud: "cadet",
|
|
55
55
|
runtimeUrl: "https://cloud.mycadet.ai",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import {
|
|
9
9
|
readGatewayCredential,
|
|
10
10
|
reprovisionAssistantApiKey,
|
|
11
|
-
|
|
12
|
-
} from "./
|
|
11
|
+
verifyGatewayCloudCredential,
|
|
12
|
+
} from "./cloud-client.js";
|
|
13
13
|
|
|
14
14
|
/** Where the key came from, so callers can report what they did. */
|
|
15
15
|
export type AssistantApiKeySource =
|
|
@@ -39,13 +39,13 @@ export interface ResolvedAssistantApiKey {
|
|
|
39
39
|
* Three steps, in order: the key the registration just issued, the key the
|
|
40
40
|
* assistant already holds, then a rotation.
|
|
41
41
|
*
|
|
42
|
-
* A stored key the
|
|
42
|
+
* A stored key the cloud has rejected is skipped, because reinjecting it
|
|
43
43
|
* repairs nothing while looking like a repair. The verdict is asked for
|
|
44
44
|
* before the key is read, so the key returned is one the verdict covers: a
|
|
45
45
|
* repair that lands between the two requests is then read, not overwritten
|
|
46
46
|
* by the value it replaced. Rotation is still withheld when the gateway is
|
|
47
47
|
* merely unreachable: rotating without being able to store the replacement
|
|
48
|
-
* would start the
|
|
48
|
+
* would start the cloud's grace clock on a key the assistant still needs,
|
|
49
49
|
* with nothing to hand it in exchange.
|
|
50
50
|
*/
|
|
51
51
|
export async function resolveAssistantApiKeyForInjection(
|
|
@@ -55,7 +55,7 @@ export async function resolveAssistantApiKeyForInjection(
|
|
|
55
55
|
return { apiKey: args.registrationApiKey, source: "registration" };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const status = await
|
|
58
|
+
const status = await verifyGatewayCloudCredential(
|
|
59
59
|
args.runtimeUrl,
|
|
60
60
|
args.bearerToken,
|
|
61
61
|
);
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* The auth scheme follows the assistant's topology, so callers never pick it.
|
|
15
15
|
* Platform-managed entries in particular have no local guardian token: they
|
|
16
16
|
* authenticate against the wildcard runtime proxy at
|
|
17
|
-
* `{
|
|
17
|
+
* `{cloudUrl}/v1/assistants/<id>/<rest>` with the stored cloud credential.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { isCloudHome } from "./cloud-values.js";
|
|
21
21
|
import { resolveAssistant } from "./assistant-config.js";
|
|
22
22
|
import { GATEWAY_PORT } from "./constants.js";
|
|
23
23
|
import {
|
|
@@ -29,8 +29,8 @@ import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
|
29
29
|
import {
|
|
30
30
|
authHeaders,
|
|
31
31
|
invalidateOrgIdCache,
|
|
32
|
-
|
|
33
|
-
} from "./
|
|
32
|
+
readCloudToken,
|
|
33
|
+
} from "./cloud-client.js";
|
|
34
34
|
|
|
35
35
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
36
36
|
const FALLBACK_RUNTIME_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
@@ -54,7 +54,7 @@ export interface AssistantClientOpts {
|
|
|
54
54
|
* sent as `X-Session-Token: <sessionToken>` and the org id is
|
|
55
55
|
* sent via the `Cadet-Organization-Id` header.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
57
|
+
* Cloud assistants do NOT need this: they are detected from the
|
|
58
58
|
* lockfile entry and authenticated from the stored platform token.
|
|
59
59
|
*/
|
|
60
60
|
sessionToken?: string;
|
|
@@ -75,7 +75,7 @@ export class AssistantClient {
|
|
|
75
75
|
private readonly _assistantId: string;
|
|
76
76
|
/**
|
|
77
77
|
* The credential for the current mode. Mutable: a 401 on the guardian path
|
|
78
|
-
* refreshes it in place (see request), and the
|
|
78
|
+
* refreshes it in place (see request), and the cloud path fills it in on
|
|
79
79
|
* first use (see resolveAuthHeaders).
|
|
80
80
|
*/
|
|
81
81
|
private token: string | undefined;
|
|
@@ -103,7 +103,7 @@ export class AssistantClient {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
this._assistantId = entry.assistantId;
|
|
106
|
-
const platformManaged = !opts?.sessionToken &&
|
|
106
|
+
const platformManaged = !opts?.sessionToken && isCloudHome(entry.cloud);
|
|
107
107
|
const platformHost = stripTrailingSlashes(entry.runtimeUrl);
|
|
108
108
|
|
|
109
109
|
// SECURITY: a cloud credential is account-scoped, far broader than the
|
|
@@ -155,7 +155,7 @@ export class AssistantClient {
|
|
|
155
155
|
* error rather than letting an anonymous request fall through to a bare 403.
|
|
156
156
|
*/
|
|
157
157
|
private requirePlatformToken(): string {
|
|
158
|
-
const token =
|
|
158
|
+
const token = readCloudToken();
|
|
159
159
|
if (!token) {
|
|
160
160
|
throw new Error(
|
|
161
161
|
"Not logged in. Run `cadet login` first to authenticate with Cadet Cloud.",
|
|
@@ -63,19 +63,11 @@ export type LocalInstanceResources = Omit<
|
|
|
63
63
|
[key: string]: unknown;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
/** Docker image metadata for the service group. Enables rollback to known-good
|
|
66
|
+
/** Docker image metadata for the service group. Enables rollback to known-good refs. */
|
|
67
67
|
export interface ContainerInfo {
|
|
68
68
|
assistantImage: string;
|
|
69
69
|
gatewayImage: string;
|
|
70
70
|
cesImage: string;
|
|
71
|
-
/** sha256 digest of the assistant image at time of hatch/upgrade */
|
|
72
|
-
assistantDigest?: string;
|
|
73
|
-
/** sha256 digest of the gateway image at time of hatch/upgrade */
|
|
74
|
-
gatewayDigest?: string;
|
|
75
|
-
/** sha256 digest of the CES image at time of hatch/upgrade */
|
|
76
|
-
cesDigest?: string;
|
|
77
|
-
/** Docker network name for the service group */
|
|
78
|
-
networkName?: string;
|
|
79
71
|
/**
|
|
80
72
|
* Host-side port the assistant HTTP API is published on. Dynamically
|
|
81
73
|
* allocated at hatch time so concurrent instances don't collide on the
|
|
@@ -159,10 +151,29 @@ export type AssistantLookupResult =
|
|
|
159
151
|
interface LockfileData {
|
|
160
152
|
assistants?: Record<string, unknown>[];
|
|
161
153
|
activeAssistant?: string;
|
|
162
|
-
|
|
154
|
+
cloudBaseUrl?: string;
|
|
155
|
+
tunnel?: RootProcessRecord;
|
|
156
|
+
app?: RootProcessRecord;
|
|
163
157
|
[key: string]: unknown;
|
|
164
158
|
}
|
|
165
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A CLI-managed OS process recorded at the lockfile root, keyed by what it is
|
|
162
|
+
* for — `tunnel` (the auto-tunnel agent fronting the edge) and `app` (the
|
|
163
|
+
* nginx ingress edge). `cadet ps` renders these alongside the per-assistant
|
|
164
|
+
* process trees, and the stop paths verify the pid before signaling it.
|
|
165
|
+
*/
|
|
166
|
+
export interface RootProcessRecord {
|
|
167
|
+
pid: number;
|
|
168
|
+
/** Which tunnel provider owns the process; "tunnel" entries only. */
|
|
169
|
+
provider?: string;
|
|
170
|
+
/** The assistant the process fronts, so `cadet ps` can nest the row. */
|
|
171
|
+
assistantId?: string;
|
|
172
|
+
startedAt: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type RootProcessKind = "tunnel" | "app";
|
|
176
|
+
|
|
166
177
|
/**
|
|
167
178
|
* Derive the daemon PID file path from a resources object. The PID file
|
|
168
179
|
* lives inside the instance's workspace directory. When no resources are
|
|
@@ -224,6 +235,40 @@ function withCliLockfileLock<T>(fn: () => T): T {
|
|
|
224
235
|
return locked.value;
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/** Record a CLI-managed root process (tunnel agent, app edge) by pid. */
|
|
239
|
+
export function stampRootProcess(
|
|
240
|
+
kind: RootProcessKind,
|
|
241
|
+
record: RootProcessRecord,
|
|
242
|
+
): void {
|
|
243
|
+
withCliLockfileLock(() => {
|
|
244
|
+
const data = readLockfile();
|
|
245
|
+
data[kind] = record;
|
|
246
|
+
writeLockfileUnlocked(data);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Drop the recorded root process entry, e.g. once the process is stopped. */
|
|
251
|
+
export function clearRootProcess(kind: RootProcessKind): void {
|
|
252
|
+
withCliLockfileLock(() => {
|
|
253
|
+
const data = readLockfile();
|
|
254
|
+
if (data[kind] === undefined) return;
|
|
255
|
+
delete data[kind];
|
|
256
|
+
writeLockfileUnlocked(data);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read the root-level process entries for `cadet ps`. Read-only: no lock, so
|
|
262
|
+
* a concurrently-updating lockfile is fine — the pid is verified separately.
|
|
263
|
+
*/
|
|
264
|
+
export function readRootProcesses(): {
|
|
265
|
+
tunnel?: RootProcessRecord;
|
|
266
|
+
app?: RootProcessRecord;
|
|
267
|
+
} {
|
|
268
|
+
const data = readLockfile();
|
|
269
|
+
return { tunnel: data.tunnel, app: data.app };
|
|
270
|
+
}
|
|
271
|
+
|
|
227
272
|
/**
|
|
228
273
|
* Try to extract a port number from a URL string (e.g. `http://localhost:7830`).
|
|
229
274
|
* Returns undefined if the URL is malformed or has no explicit port.
|
|
@@ -746,13 +791,14 @@ export async function allocateLocalResources(
|
|
|
746
791
|
}
|
|
747
792
|
|
|
748
793
|
/**
|
|
749
|
-
* Return `
|
|
794
|
+
* Return `cloudBaseUrl` from the lockfile, if set. This is the value
|
|
750
795
|
* persisted by {@link syncConfigToLockfile} the last time the active
|
|
751
796
|
* assistant was hatched/waked, and is the source of truth for "which
|
|
752
797
|
* platform does the currently-active assistant target".
|
|
753
798
|
*/
|
|
754
799
|
export function getLockfilePlatformBaseUrl(): string | undefined {
|
|
755
|
-
const
|
|
800
|
+
const lockfile = readLockfile();
|
|
801
|
+
const url = lockfile.cloudBaseUrl ?? lockfile.platformBaseUrl;
|
|
756
802
|
if (typeof url === "string" && url.trim()) return url.trim();
|
|
757
803
|
return undefined;
|
|
758
804
|
}
|