@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.
- 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-platform-login.ts → show-cloud-login.ts} +3 -5
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +3 -2
- 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 +2 -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} +5 -5
- 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__/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 +18 -18
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- 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} +1 -1
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -196
- package/src/__tests__/tunnel.test.ts +255 -227
- package/src/__tests__/uninstall.test.ts +13 -4
- 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 +258 -41
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +22 -22
- package/src/commands/client.ts +76 -76
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +5 -5
- package/src/commands/hatch.ts +22 -22
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +1 -1
- package/src/commands/ps.ts +188 -79
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +138 -20
- package/src/commands/retire.ts +46 -148
- package/src/commands/rollback.ts +18 -24
- package/src/commands/setup.ts +2 -2
- package/src/commands/teleport.ts +93 -93
- package/src/commands/terminal.ts +157 -17
- package/src/commands/tunnel.ts +80 -122
- package/src/commands/uninstall.ts +55 -7
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +22 -46
- package/src/commands/wake.ts +162 -46
- package/src/index.ts +3 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/job-polling.test.ts +1 -1
- 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} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +111 -10
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/guardian-token.ts +36 -4
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +1 -1
- 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 +9 -30
- package/src/lib/nginx-ingress.ts +41 -14
- 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/sync-cloud-assistants.ts +18 -18
- 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} +15 -4
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
- package/src/lib/upgrade-lifecycle.ts +3 -17
- 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/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
package/src/lib/cloud-login.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
25
|
+
getCloudUrl,
|
|
26
26
|
injectCredentialsIntoAssistant,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "./
|
|
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(
|
|
261
|
-
const clientId = await fetchWorkosClientId(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
320
|
+
const token = await workosPkceLogin(getCloudUrl());
|
|
321
321
|
const user = await fetchCurrentUser(token);
|
|
322
|
-
|
|
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
|
|
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
|
-
`${
|
|
339
|
-
"Retry in a few minutes, or point
|
|
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
|
|
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
|
|
359
|
-
if (!entry ||
|
|
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
|
|
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
|
-
|
|
375
|
+
cloudUrl,
|
|
376
376
|
ingressUrl,
|
|
377
377
|
);
|
|
378
378
|
saveAssistantEntry({
|
|
379
379
|
...entry,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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 {
|
|
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
|
-
* `
|
|
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
|
-
|
|
50
|
+
cloudUrl?: string;
|
|
51
51
|
}): Promise<ReleaseListItem[] | null> {
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
53
|
+
const cloudUrl = opts?.cloudUrl || getCloudUrl();
|
|
54
54
|
const filter = opts?.channel ? `channel=${opts.channel}` : "stable=true";
|
|
55
55
|
const response = await loopbackSafeFetch(
|
|
56
|
-
`${
|
|
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;
|
package/src/lib/config-utils.ts
CHANGED
|
@@ -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-
|
|
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", "
|
|
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 "./
|
|
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.
|
|
1438
|
-
extraAssistantEnv.
|
|
1439
|
-
flagEnvVars.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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().
|
|
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().
|
|
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.
|
|
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.
|
|
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.
|
|
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("
|
|
131
|
+
test("CADET_CLOUD_URL overrides cloudUrl on the resolved definition", () => {
|
|
132
132
|
process.env.CADET_ENVIRONMENT = "dev";
|
|
133
|
-
process.env.
|
|
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.
|
|
136
|
+
expect(env.cloudUrl).toBe("https://custom.example.com");
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
test("
|
|
140
|
-
process.env.
|
|
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.
|
|
142
|
+
delete process.env.CADET_CLOUD_URL;
|
|
143
143
|
const env = getCurrentEnvironment();
|
|
144
|
-
expect(env.
|
|
144
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test("CADET_ASSISTANT_PLATFORM_URL overrides
|
|
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.
|
|
151
|
+
expect(env.assistantCloudUrl).toBe("http://host.docker.internal:8000");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("CADET_ASSISTANT_PLATFORM_URL does not shadow
|
|
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.
|
|
157
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test("does not auto-materialize a new environment from
|
|
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
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
245
|
+
seed.cloudUrl = "mutated";
|
|
246
246
|
}
|
|
247
247
|
const second = getSeed("dev");
|
|
248
|
-
expect(second?.
|
|
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
|
-
* - `
|
|
46
|
+
* - `CADET_CLOUD_URL` overrides `cloudUrl`
|
|
47
47
|
* - `CADET_WEB_URL` overrides `webUrl`
|
|
48
|
-
* - `CADET_ASSISTANT_PLATFORM_URL` overrides `
|
|
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
|
|
91
|
-
if (
|
|
92
|
-
resolved.
|
|
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
|
|
100
|
+
const assistantCloudUrlOverride =
|
|
101
101
|
process.env.CADET_ASSISTANT_PLATFORM_URL?.trim();
|
|
102
|
-
if (
|
|
103
|
-
resolved.
|
|
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
|
|
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
|
|
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
|
-
|
|
635
|
-
|
|
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 = {
|