@mycadet/cli 0.12.7 → 0.12.8

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.
Files changed (165) hide show
  1. package/README.md +2 -2
  2. package/knip.json +1 -1
  3. package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
  4. package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
  5. package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
  6. package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
  7. package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
  8. package/node_modules/@mycadet/assistant-api/events/{show-platform-login.ts → show-cloud-login.ts} +3 -5
  9. package/node_modules/@mycadet/assistant-api/index.ts +11 -11
  10. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
  11. package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  12. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  13. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  14. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  15. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  16. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  17. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  18. package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
  19. package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
  20. package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  21. package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  22. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  23. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  24. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  25. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  26. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  27. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  28. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  29. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
  30. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
  31. 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
  32. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  33. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  34. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  35. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  36. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  37. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  38. package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
  39. package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
  40. package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
  41. package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
  42. package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
  43. package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
  44. package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
  45. package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
  46. package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
  47. package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
  48. package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
  49. package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
  50. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  51. package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
  52. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  53. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  54. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  55. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  56. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  57. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  58. package/package.json +3 -2
  59. package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
  60. package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
  61. package/src/__tests__/assistant-client-refresh.test.ts +2 -2
  62. package/src/__tests__/backup.test.ts +86 -91
  63. package/src/__tests__/client-token.test.ts +1 -1
  64. package/src/__tests__/client-tui-refresh.test.ts +1 -1
  65. package/src/__tests__/client-web-config.test.ts +2 -2
  66. package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
  67. package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +5 -5
  68. package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
  69. package/src/__tests__/connect-import.test.ts +55 -39
  70. package/src/__tests__/debug.test.ts +38 -38
  71. package/src/__tests__/guardian-token.test.ts +149 -56
  72. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  73. package/src/__tests__/helpers/guardian-token.ts +22 -0
  74. package/src/__tests__/helpers/http.ts +10 -0
  75. package/src/__tests__/nginx-ingress.test.ts +18 -18
  76. package/src/__tests__/orphan-detection.test.ts +1 -1
  77. package/src/__tests__/pair.test.ts +1 -8
  78. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
  79. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  80. package/src/__tests__/retire.test.ts +58 -199
  81. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  82. package/src/__tests__/sleep.test.ts +19 -19
  83. package/src/__tests__/statefulset.test.ts +8 -8
  84. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
  85. package/src/__tests__/teleport-backup.test.ts +3 -3
  86. package/src/__tests__/teleport.test.ts +180 -196
  87. package/src/__tests__/tunnel.test.ts +255 -227
  88. package/src/__tests__/uninstall.test.ts +13 -4
  89. package/src/__tests__/upgrade-local.test.ts +3 -3
  90. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  91. package/src/__tests__/wake.test.ts +258 -41
  92. package/src/__tests__/workos-pkce.test.ts +2 -2
  93. package/src/commands/backup.ts +22 -22
  94. package/src/commands/client.ts +76 -76
  95. package/src/commands/debug.ts +20 -20
  96. package/src/commands/exec.ts +5 -5
  97. package/src/commands/hatch.ts +22 -22
  98. package/src/commands/login.ts +22 -22
  99. package/src/commands/logs.ts +1 -1
  100. package/src/commands/ps.ts +188 -79
  101. package/src/commands/restart.ts +30 -0
  102. package/src/commands/restore.ts +138 -20
  103. package/src/commands/retire.ts +46 -148
  104. package/src/commands/rollback.ts +18 -24
  105. package/src/commands/setup.ts +2 -2
  106. package/src/commands/teleport.ts +93 -93
  107. package/src/commands/terminal.ts +157 -17
  108. package/src/commands/tunnel.ts +80 -122
  109. package/src/commands/uninstall.ts +55 -7
  110. package/src/commands/unpair.ts +3 -3
  111. package/src/commands/upgrade.ts +22 -46
  112. package/src/commands/wake.ts +162 -46
  113. package/src/index.ts +3 -9
  114. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  115. package/src/lib/__tests__/job-polling.test.ts +1 -1
  116. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  117. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  118. package/src/lib/assistant-api-key-resolution.ts +5 -5
  119. package/src/lib/assistant-client.ts +8 -8
  120. package/src/lib/assistant-config.ts +58 -12
  121. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  122. package/src/lib/cloud-login.ts +24 -24
  123. package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
  124. package/src/lib/cloud-values.ts +3 -0
  125. package/src/lib/config-utils.ts +1 -1
  126. package/src/lib/detached-process.ts +1 -1
  127. package/src/lib/docker.ts +111 -10
  128. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  129. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  130. package/src/lib/environments/resolve.ts +8 -8
  131. package/src/lib/guardian-token.ts +36 -4
  132. package/src/lib/hatch-local.ts +19 -23
  133. package/src/lib/health-check.ts +6 -6
  134. package/src/lib/ingress-config.ts +8 -35
  135. package/src/lib/job-polling.ts +1 -1
  136. package/src/lib/live-voice/connection.ts +12 -12
  137. package/src/lib/live-voice/tunnel.test.ts +11 -11
  138. package/src/lib/live-voice/tunnel.ts +15 -15
  139. package/src/lib/local-runtime-client.ts +20 -20
  140. package/src/lib/local.ts +9 -30
  141. package/src/lib/nginx-ingress.ts +41 -14
  142. package/src/lib/process.ts +1 -1
  143. package/src/lib/provider-secrets.ts +3 -3
  144. package/src/lib/retire-apple-container.ts +1 -1
  145. package/src/lib/runtime-url.ts +8 -8
  146. package/src/lib/statefulset.ts +2 -2
  147. package/src/lib/sync-cloud-assistants.ts +18 -18
  148. package/src/lib/teleport-backup.ts +4 -4
  149. package/src/lib/terminal-client.ts +17 -17
  150. package/src/lib/terminal-session.ts +24 -24
  151. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  152. package/src/lib/tunnels/cadet.ts +29 -0
  153. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
  154. package/src/lib/tunnels/gateway-notify.ts +61 -0
  155. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
  156. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
  157. package/src/lib/upgrade-lifecycle.ts +3 -17
  158. package/src/lib/upgrade-preflight.ts +2 -2
  159. package/src/lib/workos-pkce.ts +5 -5
  160. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  161. package/src/commands/recover.ts +0 -131
  162. package/src/commands/roadmap.ts +0 -459
  163. package/src/commands/ssh.ts +0 -144
  164. package/src/commands/workflows.ts +0 -305
  165. package/src/lib/managed-cloud.ts +0 -3
@@ -10,7 +10,7 @@ import { randomBytes } from "crypto";
10
10
  import { createServer } from "http";
11
11
  import type { AddressInfo } from "net";
12
12
 
13
- import { isManagedCloud } from "./managed-cloud.js";
13
+ import { isCloudHome } from "./cloud-values.js";
14
14
  import {
15
15
  resolveAssistant,
16
16
  saveAssistantEntry,
@@ -22,11 +22,11 @@ import {
22
22
  fetchCurrentUser,
23
23
  fetchOrganizationId,
24
24
  ensureSelfHostedLocalRegistration,
25
- getPlatformUrl,
25
+ getCloudUrl,
26
26
  injectCredentialsIntoAssistant,
27
- readPlatformToken,
28
- savePlatformToken,
29
- } from "./platform-client.js";
27
+ readCloudToken,
28
+ saveCloudToken,
29
+ } from "./cloud-client.js";
30
30
  import { fetchAssistantIngressUrl, fetchCurrentVersion } from "./upgrade-lifecycle.js";
31
31
  import {
32
32
  CALLBACK_PATH,
@@ -257,8 +257,8 @@ export function startLoopbackListener(
257
257
  }
258
258
 
259
259
  /** App-held WorkOS PKCE login */
260
- export async function workosPkceLogin(platformUrl: string): Promise<string> {
261
- const clientId = await fetchWorkosClientId(platformUrl);
260
+ export async function workosPkceLogin(cloudUrl: string): Promise<string> {
261
+ const clientId = await fetchWorkosClientId(cloudUrl);
262
262
  const { verifier, challenge } = generatePkcePair();
263
263
  const state = randomBytes(32).toString("hex");
264
264
 
@@ -286,7 +286,7 @@ export async function workosPkceLogin(platformUrl: string): Promise<string> {
286
286
  verifier,
287
287
  });
288
288
  return await exchangeAccessTokenForSession(
289
- platformUrl,
289
+ cloudUrl,
290
290
  clientId,
291
291
  accessToken,
292
292
  );
@@ -307,7 +307,7 @@ export interface CloudLoginSession {
307
307
  * validates the resulting token, and persists it.
308
308
  */
309
309
  export async function ensureCloudLoginInteractive(): Promise<CloudLoginSession> {
310
- const existingToken = readPlatformToken();
310
+ const existingToken = readCloudToken();
311
311
  if (existingToken) {
312
312
  try {
313
313
  const user = await fetchCurrentUser(existingToken);
@@ -317,16 +317,16 @@ export async function ensureCloudLoginInteractive(): Promise<CloudLoginSession>
317
317
  }
318
318
  }
319
319
 
320
- const token = await workosPkceLogin(getPlatformUrl());
320
+ const token = await workosPkceLogin(getCloudUrl());
321
321
  const user = await fetchCurrentUser(token);
322
- savePlatformToken(token);
322
+ saveCloudToken(token);
323
323
  return { token, user };
324
324
  }
325
325
 
326
326
  /**
327
327
  * TLS failures surface as opaque messages like "unknown certificate
328
328
  * verification error" from Bun's fetch. Translate the recognizable ones into
329
- * something actionable — most commonly the platform host's managed
329
+ * something actionable — most commonly the cloud host's managed
330
330
  * certificate is still provisioning.
331
331
  */
332
332
  export function describeLoginError(error: unknown): string {
@@ -335,13 +335,13 @@ export function describeLoginError(error: unknown): string {
335
335
  if (!tlsish) return raw;
336
336
  return (
337
337
  `Cadet Cloud is unreachable (${raw}). This usually means ` +
338
- `${getPlatformUrl()}'s TLS certificate is missing or still provisioning. ` +
339
- "Retry in a few minutes, or point CADET_PLATFORM_URL at a reachable host."
338
+ `${getCloudUrl()}'s TLS certificate is missing or still provisioning. ` +
339
+ "Retry in a few minutes, or point CADET_CLOUD_URL at a reachable host."
340
340
  );
341
341
  }
342
342
 
343
343
  /**
344
- * Register the active self-hosted assistant with the platform and inject
344
+ * Register the active self-hosted assistant with the cloud and inject
345
345
  * cloud credentials into its runtime — the post-login half of the desktop
346
346
  * app's LocalAssistantBootstrapService flow. Extracted so `cadet hatch` can
347
347
  * complete the same flow right after an interactive login on the Cadet Cloud
@@ -355,8 +355,8 @@ export async function registerLocalAssistantWithPlatform(
355
355
  const log = options.log ?? console.log;
356
356
  const entry = resolveAssistant();
357
357
 
358
- // Skip managed ("cadet") assistants — they are handled by the platform.
359
- if (!entry || isManagedCloud(entry.cloud)) return;
358
+ // Skip managed ("cadet") assistants — they are handled by the cloud.
359
+ if (!entry || isCloudHome(entry.cloud)) return;
360
360
 
361
361
  const orgId = await fetchOrganizationId(token);
362
362
  const clientInstallationId = computeDeviceId();
@@ -364,7 +364,7 @@ export async function registerLocalAssistantWithPlatform(
364
364
  fetchCurrentVersion(entry.runtimeUrl),
365
365
  fetchAssistantIngressUrl(entry.runtimeUrl, options.bearerToken ?? entry.bearerToken),
366
366
  ]);
367
- const platformUrl = getPlatformUrl();
367
+ const cloudUrl = getCloudUrl();
368
368
  const registration = await ensureSelfHostedLocalRegistration(
369
369
  token,
370
370
  orgId,
@@ -372,14 +372,14 @@ export async function registerLocalAssistantWithPlatform(
372
372
  entry.assistantId,
373
373
  "cli",
374
374
  assistantVersion,
375
- platformUrl,
375
+ cloudUrl,
376
376
  ingressUrl,
377
377
  );
378
378
  saveAssistantEntry({
379
379
  ...entry,
380
- platformAssistantId: registration.assistant.id,
381
- platformBaseUrl: platformUrl,
382
- platformOrganizationId: orgId,
380
+ cloudAssistantId: registration.assistant.id,
381
+ cloudBaseUrl: cloudUrl,
382
+ cloudOrganizationId: orgId,
383
383
  });
384
384
  log(
385
385
  `Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
@@ -408,8 +408,8 @@ export async function registerLocalAssistantWithPlatform(
408
408
  gatewayUrl: entry.runtimeUrl,
409
409
  bearerToken: options.bearerToken ?? entry.bearerToken,
410
410
  assistantApiKey,
411
- platformAssistantId: registration.assistant.id,
412
- platformBaseUrl: platformUrl,
411
+ cloudAssistantId: registration.assistant.id,
412
+ cloudBaseUrl: cloudUrl,
413
413
  organizationId: orgId,
414
414
  userId: user.id,
415
415
  webhookSecret: registration.webhook_secret,
@@ -1,4 +1,4 @@
1
- import { getPlatformUrl } from "./platform-client.js";
1
+ import { getCloudUrl } from "./cloud-client.js";
2
2
  import { dockerhubImages } from "./docker.js";
3
3
  import type { ServiceName } from "./docker.js";
4
4
  import { loopbackSafeFetch } from "./loopback-fetch.js";
@@ -40,20 +40,20 @@ const RELEASES_FETCH_LIMIT = 100;
40
40
  /**
41
41
  * Fetch the releases list from Cadet Cloud, optionally filtered by channel
42
42
  * (the `channel` param takes precedence over `stable` server-side).
43
- * `platformUrl` overrides the lockfile/env-resolved default — pass the
43
+ * `cloudUrl` overrides the lockfile/env-resolved default — pass the
44
44
  * target assistant's platform URL when it may differ from the active one.
45
45
  * Returns `null` when Cadet Cloud is unreachable or responds non-OK —
46
46
  * distinct from `[]` (it answered, no releases).
47
47
  */
48
48
  export async function fetchReleases(opts?: {
49
49
  channel?: "stable" | "preview";
50
- platformUrl?: string;
50
+ cloudUrl?: string;
51
51
  }): Promise<ReleaseListItem[] | null> {
52
52
  try {
53
- const platformUrl = opts?.platformUrl || getPlatformUrl();
53
+ const cloudUrl = opts?.cloudUrl || getCloudUrl();
54
54
  const filter = opts?.channel ? `channel=${opts.channel}` : "stable=true";
55
55
  const response = await loopbackSafeFetch(
56
- `${platformUrl}/v1/releases/?${filter}&limit=${RELEASES_FETCH_LIMIT}`,
56
+ `${cloudUrl}/v1/releases/?${filter}&limit=${RELEASES_FETCH_LIMIT}`,
57
57
  { signal: AbortSignal.timeout(10_000) },
58
58
  );
59
59
  if (!response.ok) return null;
@@ -0,0 +1,3 @@
1
+ export function isCloudHome(cloud: unknown): boolean {
2
+ return cloud === "cloud" || cloud === "cadet";
3
+ }
@@ -34,7 +34,7 @@ export function buildNestedConfig(
34
34
 
35
35
  /**
36
36
  * Ensure hatch always provides enough initial LLM config for the assistant to
37
- * detect a fresh off-platform hatch and seed BYOK profiles.
37
+ * detect a fresh off-cloud hatch and seed BYOK profiles.
38
38
  *
39
39
  * @deprecated Part of the workspace-config overlay path — a CLI→Assistant
40
40
  * side channel that bypasses the Assistant's public APIs and has no
@@ -6,7 +6,7 @@ import { isCompiledCli } from "./local.js";
6
6
  import { getLogDir, openLogFile, resetLogFile } from "./xdg-log.js";
7
7
 
8
8
  export interface RelaunchDetachedOptions {
9
- /** argv for the re-invoked CLI command, e.g. ["tunnel", "--provider", "ngrok"]. */
9
+ /** argv for the re-invoked CLI command, e.g. ["tunnel", "--provider", "cloudflare"]. */
10
10
  args: string[];
11
11
  /** Log file basename under the XDG log dir; truncated before spawn. */
12
12
  logFile: string;
package/src/lib/docker.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  isLocalBuildRef,
38
38
  loadImageViaHost,
39
39
  } from "./host-image-loader.js";
40
- import { resolveOwnImageRefs } from "./platform-releases.js";
40
+ import { resolveOwnImageRefs } from "./cloud-releases.js";
41
41
  import { isDevVersion } from "./version-compat.js";
42
42
  import {
43
43
  configureHatchProviderApiKey,
@@ -785,6 +785,113 @@ export async function wakeContainers(
785
785
  }
786
786
  }
787
787
 
788
+ /** The `docker start` failure for a container that was never created. */
789
+ export function isNoSuchContainerError(err: unknown): boolean {
790
+ const msg = err instanceof Error ? err.message : String(err);
791
+ return msg.includes("No such container");
792
+ }
793
+
794
+ /**
795
+ * Recreate a docker instance's service group from its lockfile entry — the
796
+ * wake path for containers that don't exist yet. A migrated install has its
797
+ * data in the volumes but no containers: nothing ever ran `docker run` for
798
+ * it (v0.0.10 run feedback: `wake --all` died on "No such container:
799
+ * <name>-assistant").
800
+ *
801
+ * Network and volume creates are idempotent; the three service containers
802
+ * come up from the entry's recorded images (the migrator points those at
803
+ * the installed cadet's release). The guardian bootstrap secret rides
804
+ * through from the entry; the signing key and CES token are deliberately
805
+ * NOT invented — with no env value the gateway and assistant load the
806
+ * persisted trust material from their security volumes (a migrated install
807
+ * keeps its own), matching the gateway's resolution order (env → file →
808
+ * generate). Empty volumes just get fresh state on first boot.
809
+ */
810
+ export async function recreateDockerContainers(entry: AssistantEntry): Promise<void> {
811
+ const info = entry.containerInfo;
812
+ if (!info?.assistantImage || !info.gatewayImage || !info.cesImage) {
813
+ throw new Error(
814
+ `no container images recorded for '${entry.assistantId}' — re-hatch or run \`cadet upgrade\` to set them`,
815
+ );
816
+ }
817
+ const res = dockerResourceNames(entry.assistantId);
818
+ const log = (msg: string): void => console.log(msg);
819
+
820
+ // Clean slate for the create: an existing container (even a stopped one)
821
+ // would collide with `docker run --name` (the upgrade path does the same).
822
+ await stopContainers(res);
823
+
824
+ try {
825
+ await exec("docker", ["network", "inspect", res.network]);
826
+ } catch {
827
+ await exec("docker", ["network", "create", res.network]);
828
+ }
829
+ for (const vol of [
830
+ res.socketVolume,
831
+ res.assistantIpcVolume,
832
+ res.gatewayIpcVolume,
833
+ res.workspaceVolume,
834
+ res.gatewaySecurityVolume,
835
+ res.cesSecurityVolume,
836
+ ]) {
837
+ try {
838
+ await exec("docker", ["volume", "inspect", vol]);
839
+ } catch {
840
+ await exec("docker", ["volume", "create", vol]);
841
+ }
842
+ }
843
+
844
+ // The recorded images may not be on the machine yet (a migrated install
845
+ // gets its refs written before anything pulled them) — a pull of an
846
+ // already-present image is a cheap no-op.
847
+ for (const image of [info.assistantImage, info.gatewayImage, info.cesImage]) {
848
+ log(`📦 Pulling ${image}...`);
849
+ await exec("docker", ["pull", image]);
850
+ }
851
+
852
+ const gatewayPort = parseGatewayPort(entry.runtimeUrl) ??
853
+ getDefaultPorts(getCurrentEnvironment()).gateway;
854
+ const assistantPort = info.assistantPort ?? ASSISTANT_INTERNAL_PORT;
855
+ const device = getOrCreateHostDeviceId();
856
+ await startContainers(
857
+ {
858
+ bootstrapSecret: entry.guardianBootstrapSecret,
859
+ extraAssistantEnv: { CADET_DEVICE_ID: device },
860
+ extraGatewayEnv: { CADET_DEVICE_ID: device },
861
+ gatewayPort,
862
+ assistantPort,
863
+ imageTags: {
864
+ assistant: info.assistantImage,
865
+ gateway: info.gatewayImage,
866
+ "credential-executor": info.cesImage,
867
+ },
868
+ instanceName: entry.assistantId,
869
+ res,
870
+ },
871
+ log,
872
+ );
873
+
874
+ // Persist what the recreation decided (the gateway port in the runtime
875
+ // url, the assistant port when it defaulted) so plain `docker start`
876
+ // wakes work and clients reach the right endpoints.
877
+ saveAssistantEntry({
878
+ ...entry,
879
+ runtimeUrl: `http://localhost:${gatewayPort}`,
880
+ containerInfo: { ...info, assistantPort },
881
+ });
882
+ }
883
+
884
+ /** The port of the CLI's own runtime url form (`http://localhost:7830`). */
885
+ function parseGatewayPort(runtimeUrl: string): number | null {
886
+ try {
887
+ const port = new URL(runtimeUrl).port;
888
+ const parsed = parseInt(port, 10);
889
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
890
+ } catch {
891
+ return null;
892
+ }
893
+ }
894
+
788
895
  /**
789
896
  * Ensure a Docker daemon is reachable before waking an instance's containers
790
897
  * (macOS only — on Linux the daemon runs natively).
@@ -1434,9 +1541,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1434
1541
  : ownSecret;
1435
1542
 
1436
1543
  emitProgress(4, 6, "Starting containers...");
1437
- if (flagEnvVars.CADET_DISABLE_PLATFORM) {
1438
- extraAssistantEnv.CADET_DISABLE_PLATFORM =
1439
- flagEnvVars.CADET_DISABLE_PLATFORM;
1544
+ if (flagEnvVars.CADET_DISABLE_CLOUD) {
1545
+ extraAssistantEnv.CADET_DISABLE_CLOUD =
1546
+ flagEnvVars.CADET_DISABLE_CLOUD;
1440
1547
  }
1441
1548
  const hostDeviceId = getOrCreateHostDeviceId();
1442
1549
  extraAssistantEnv.CADET_DEVICE_ID = hostDeviceId;
@@ -1478,8 +1585,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1478
1585
  );
1479
1586
  const containersUpAt = Date.now();
1480
1587
 
1481
- const imageDigests = await captureImageRefs(res);
1482
-
1483
1588
  const runtimeUrl = `http://localhost:${gatewayPort}`;
1484
1589
  const dockerEntry: AssistantEntry = {
1485
1590
  assistantId: instanceName,
@@ -1491,10 +1596,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1491
1596
  assistantImage: imageTags.assistant,
1492
1597
  gatewayImage: imageTags.gateway,
1493
1598
  cesImage: imageTags["credential-executor"],
1494
- assistantDigest: imageDigests?.assistant,
1495
- gatewayDigest: imageDigests?.gateway,
1496
- cesDigest: imageDigests?.["credential-executor"],
1497
- networkName: res.network,
1498
1599
  assistantPort,
1499
1600
  },
1500
1601
  };
@@ -48,13 +48,13 @@ type EnvironmentDefinition =
48
48
 
49
49
  const prod: EnvironmentDefinition = {
50
50
  name: "production",
51
- platformUrl: "https://cloud.mycadet.ai",
51
+ cloudUrl: "https://cloud.mycadet.ai",
52
52
  webUrl: "https://mycadet.ai",
53
53
  };
54
54
 
55
55
  const dev: EnvironmentDefinition = {
56
56
  name: "dev",
57
- platformUrl: "https://dev-cloud.mycadet.ai",
57
+ cloudUrl: "https://dev-cloud.mycadet.ai",
58
58
  webUrl: "https://dev.mycadet.ai",
59
59
  };
60
60
 
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  const ENV_VARS_TO_SAVE = [
10
10
  "CADET_ENVIRONMENT",
11
- "CADET_PLATFORM_URL",
11
+ "CADET_CLOUD_URL",
12
12
  "CADET_ASSISTANT_PLATFORM_URL",
13
13
  "CADET_LOCKFILE_DIR",
14
14
  ] as const;
@@ -37,33 +37,33 @@ describe("getCurrentEnvironment", () => {
37
37
  test("returns production seed when no override, no env var", () => {
38
38
  const env = getCurrentEnvironment();
39
39
  expect(env.name).toBe("production");
40
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
40
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
41
41
  });
42
42
 
43
43
  test("returns dev seed when CADET_ENVIRONMENT=dev", () => {
44
44
  process.env.CADET_ENVIRONMENT = "dev";
45
45
  const env = getCurrentEnvironment();
46
46
  expect(env.name).toBe("dev");
47
- expect(env.platformUrl).toBe("https://dev-cloud.mycadet.ai");
47
+ expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
48
48
  });
49
49
 
50
50
  test("returns staging seed when CADET_ENVIRONMENT=staging", () => {
51
51
  process.env.CADET_ENVIRONMENT = "staging";
52
- expect(getCurrentEnvironment().platformUrl).toBe(
52
+ expect(getCurrentEnvironment().cloudUrl).toBe(
53
53
  "https://staging-cloud.mycadet.ai",
54
54
  );
55
55
  });
56
56
 
57
57
  test("returns local seed with localhost URL", () => {
58
58
  process.env.CADET_ENVIRONMENT = "local";
59
- expect(getCurrentEnvironment().platformUrl).toBe("http://localhost:8000");
59
+ expect(getCurrentEnvironment().cloudUrl).toBe("http://localhost:8000");
60
60
  });
61
61
 
62
62
  test("override argument takes priority over CADET_ENVIRONMENT env var", () => {
63
63
  process.env.CADET_ENVIRONMENT = "staging";
64
64
  const env = getCurrentEnvironment("dev");
65
65
  expect(env.name).toBe("dev");
66
- expect(env.platformUrl).toBe("https://dev-cloud.mycadet.ai");
66
+ expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
67
67
  });
68
68
 
69
69
  test("empty override argument falls through to env var", () => {
@@ -85,7 +85,7 @@ describe("getCurrentEnvironment", () => {
85
85
  try {
86
86
  const env = getCurrentEnvironment("no-such-env");
87
87
  expect(env.name).toBe("production");
88
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
88
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
89
89
  expect(stderr).toHaveBeenCalledWith(
90
90
  expect.stringContaining('unknown environment "no-such-env"'),
91
91
  );
@@ -105,7 +105,7 @@ describe("getCurrentEnvironment", () => {
105
105
  try {
106
106
  const env = getCurrentEnvironment();
107
107
  expect(env.name).toBe("production");
108
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
108
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
109
109
  expect(stderr).toHaveBeenCalledWith(
110
110
  expect.stringContaining('unknown environment "nope"'),
111
111
  );
@@ -128,49 +128,49 @@ describe("getCurrentEnvironment", () => {
128
128
  }
129
129
  });
130
130
 
131
- test("CADET_PLATFORM_URL overrides platformUrl on the resolved definition", () => {
131
+ test("CADET_CLOUD_URL overrides cloudUrl on the resolved definition", () => {
132
132
  process.env.CADET_ENVIRONMENT = "dev";
133
- process.env.CADET_PLATFORM_URL = "https://custom.example.com";
133
+ process.env.CADET_CLOUD_URL = "https://custom.example.com";
134
134
  const env = getCurrentEnvironment();
135
135
  expect(env.name).toBe("dev");
136
- expect(env.platformUrl).toBe("https://custom.example.com");
136
+ expect(env.cloudUrl).toBe("https://custom.example.com");
137
137
  });
138
138
 
139
- test("CADET_PLATFORM_URL override does not affect the seed table", () => {
140
- process.env.CADET_PLATFORM_URL = "https://custom.example.com";
139
+ test("CADET_CLOUD_URL override does not affect the seed table", () => {
140
+ process.env.CADET_CLOUD_URL = "https://custom.example.com";
141
141
  getCurrentEnvironment();
142
- delete process.env.CADET_PLATFORM_URL;
142
+ delete process.env.CADET_CLOUD_URL;
143
143
  const env = getCurrentEnvironment();
144
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
144
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
145
145
  });
146
146
 
147
- test("CADET_ASSISTANT_PLATFORM_URL overrides assistantPlatformUrl", () => {
147
+ test("CADET_ASSISTANT_PLATFORM_URL overrides assistantCloudUrl", () => {
148
148
  process.env.CADET_ASSISTANT_PLATFORM_URL =
149
149
  "http://host.docker.internal:8000";
150
150
  const env = getCurrentEnvironment();
151
- expect(env.assistantPlatformUrl).toBe("http://host.docker.internal:8000");
151
+ expect(env.assistantCloudUrl).toBe("http://host.docker.internal:8000");
152
152
  });
153
153
 
154
- test("CADET_ASSISTANT_PLATFORM_URL does not shadow platformUrl", () => {
154
+ test("CADET_ASSISTANT_PLATFORM_URL does not shadow cloudUrl", () => {
155
155
  process.env.CADET_ASSISTANT_PLATFORM_URL = "http://override";
156
156
  const env = getCurrentEnvironment();
157
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
157
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
158
158
  });
159
159
 
160
- test("does not auto-materialize a new environment from CADET_PLATFORM_URL alone", () => {
160
+ test("does not auto-materialize a new environment from CADET_CLOUD_URL alone", () => {
161
161
  // Unknown env names fall back to production (parity with daemon + Swift).
162
- // The per-field CADET_PLATFORM_URL override is intentionally dropped on
162
+ // The per-field CADET_CLOUD_URL override is intentionally dropped on
163
163
  // the fallback path — fallback returns a pristine production seed so a
164
164
  // typo'd env var can't accidentally stitch together a new environment.
165
165
  process.env.CADET_ENVIRONMENT = "my-custom";
166
- process.env.CADET_PLATFORM_URL = "https://my-custom.example.com";
166
+ process.env.CADET_CLOUD_URL = "https://my-custom.example.com";
167
167
  const stderr = spyOn(process.stderr, "write").mockImplementation(
168
168
  () => true,
169
169
  );
170
170
  try {
171
171
  const env = getCurrentEnvironment();
172
172
  expect(env.name).toBe("production");
173
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
173
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
174
174
  expect(stderr).toHaveBeenCalledWith(
175
175
  expect.stringContaining('unknown environment "my-custom"'),
176
176
  );
@@ -232,7 +232,7 @@ describe("getSeed", () => {
232
232
  const seed = getSeed("dev");
233
233
  expect(seed).toBeDefined();
234
234
  expect(seed?.name).toBe("dev");
235
- expect(seed?.platformUrl).toBe("https://dev-cloud.mycadet.ai");
235
+ expect(seed?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
236
236
  });
237
237
 
238
238
  test("returns undefined for an unknown name", () => {
@@ -242,10 +242,10 @@ describe("getSeed", () => {
242
242
  test("returned seed is a copy — mutations do not affect the table", () => {
243
243
  const seed = getSeed("dev");
244
244
  if (seed) {
245
- seed.platformUrl = "mutated";
245
+ seed.cloudUrl = "mutated";
246
246
  }
247
247
  const second = getSeed("dev");
248
- expect(second?.platformUrl).toBe("https://dev-cloud.mycadet.ai");
248
+ expect(second?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
249
249
  });
250
250
 
251
251
  test("all five canonical seeds exist", () => {
@@ -43,9 +43,9 @@ export function environmentFromVersion(version: string): string {
43
43
  *
44
44
  * Per-field env-var overrides are honored on the resolved definition as
45
45
  * ad-hoc escape hatches (they do not materialize new environments):
46
- * - `CADET_PLATFORM_URL` overrides `platformUrl`
46
+ * - `CADET_CLOUD_URL` overrides `cloudUrl`
47
47
  * - `CADET_WEB_URL` overrides `webUrl`
48
- * - `CADET_ASSISTANT_PLATFORM_URL` overrides `assistantPlatformUrl`
48
+ * - `CADET_ASSISTANT_PLATFORM_URL` overrides `assistantCloudUrl`
49
49
  * - `CADET_LOCKFILE_DIR` overrides `lockfileDirOverride` (legacy e2e
50
50
  * test hook)
51
51
  *
@@ -87,9 +87,9 @@ export function getCurrentEnvironment(
87
87
 
88
88
  const resolved: EnvironmentDefinition = { ...seed };
89
89
 
90
- const platformUrlOverride = process.env.CADET_PLATFORM_URL?.trim();
91
- if (platformUrlOverride) {
92
- resolved.platformUrl = platformUrlOverride;
90
+ const cloudUrlOverride = process.env.CADET_CLOUD_URL?.trim();
91
+ if (cloudUrlOverride) {
92
+ resolved.cloudUrl = cloudUrlOverride;
93
93
  }
94
94
 
95
95
  const webUrlOverride = process.env.CADET_WEB_URL?.trim();
@@ -97,10 +97,10 @@ export function getCurrentEnvironment(
97
97
  resolved.webUrl = webUrlOverride;
98
98
  }
99
99
 
100
- const assistantPlatformUrlOverride =
100
+ const assistantCloudUrlOverride =
101
101
  process.env.CADET_ASSISTANT_PLATFORM_URL?.trim();
102
- if (assistantPlatformUrlOverride) {
103
- resolved.assistantPlatformUrl = assistantPlatformUrlOverride;
102
+ if (assistantCloudUrlOverride) {
103
+ resolved.assistantCloudUrl = assistantCloudUrlOverride;
104
104
  }
105
105
 
106
106
  const lockfileDirOverride = process.env.CADET_LOCKFILE_DIR?.trim();
@@ -49,7 +49,11 @@ const GuardianTokenFieldsSchema = z
49
49
  .union([z.string(), z.number()])
50
50
  .optional()
51
51
  .catch(undefined),
52
- refreshAfter: z.string().optional().catch(undefined),
52
+ refreshAfter: z
53
+ .union([z.string(), z.number()])
54
+ .transform((v) => (typeof v === "number" ? new Date(v).toISOString() : v))
55
+ .optional()
56
+ .catch(undefined),
53
57
  isNew: z.boolean().optional().catch(undefined),
54
58
  deviceId: z.string().optional().catch(undefined),
55
59
  })
@@ -590,6 +594,20 @@ async function waitForGatewayReady(gatewayUrl: string): Promise<void> {
590
594
  }
591
595
  }
592
596
 
597
+ export class GuardianLeasePermanentError extends Error {
598
+ constructor(message: string) {
599
+ super(message);
600
+ this.name = "GuardianLeasePermanentError";
601
+ }
602
+ }
603
+
604
+ function describeJsonType(value: unknown): string {
605
+ if (value === undefined) return "absent";
606
+ if (value === null) return "null";
607
+ if (Array.isArray(value)) return "array";
608
+ return typeof value;
609
+ }
610
+
593
611
  export async function leaseGuardianToken(
594
612
  gatewayUrl: string,
595
613
  assistantId: string,
@@ -621,7 +639,8 @@ export async function leaseGuardianToken(
621
639
  throw new Error(`guardian/init failed (${response.status}): ${body}`);
622
640
  }
623
641
 
624
- const parsed = parseGuardianTokenFields(await response.json());
642
+ const rawBody: unknown = await response.json();
643
+ const parsed = parseGuardianTokenFields(rawBody);
625
644
  if (
626
645
  parsed.guardianPrincipalId == null ||
627
646
  parsed.accessToken == null ||
@@ -631,8 +650,21 @@ export async function leaseGuardianToken(
631
650
  parsed.refreshAfter == null ||
632
651
  parsed.isNew == null
633
652
  ) {
634
- throw new Error(
635
- `guardian/init returned a malformed token response (${response.status})`,
653
+ const raw = z.record(z.string(), z.unknown()).catch({}).parse(rawBody);
654
+ const requiredFields: (keyof typeof parsed)[] = [
655
+ "guardianPrincipalId",
656
+ "accessToken",
657
+ "accessTokenExpiresAt",
658
+ "refreshToken",
659
+ "refreshTokenExpiresAt",
660
+ "refreshAfter",
661
+ "isNew",
662
+ ];
663
+ const unusable = requiredFields
664
+ .filter((field) => parsed[field] == null)
665
+ .map((field) => `${field} arrived as ${describeJsonType(raw[field])}`);
666
+ throw new GuardianLeasePermanentError(
667
+ `guardian/init returned a malformed token response (${response.status}) — ${unusable.join(", ")}`,
636
668
  );
637
669
  }
638
670
  const tokenData: GuardianTokenData = {