@vellumai/cli 0.5.15 → 0.6.0
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/bun.lock +46 -52
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +1005 -391
- package/src/commands/hatch.ts +17 -388
- package/src/commands/retire.ts +2 -120
- package/src/commands/rollback.ts +6 -0
- package/src/commands/teleport.ts +757 -198
- package/src/commands/upgrade.ts +7 -0
- package/src/lib/aws.ts +2 -1
- package/src/lib/constants.ts +0 -11
- package/src/lib/docker.ts +27 -13
- package/src/lib/gcp.ts +2 -5
- package/src/lib/hatch-local.ts +403 -0
- package/src/lib/local.ts +9 -120
- package/src/lib/platform-client.ts +142 -8
- package/src/lib/retire-local.ts +124 -0
- package/src/lib/upgrade-lifecycle.ts +8 -0
- package/src/shared/provider-env-vars.ts +19 -0
package/src/lib/local.ts
CHANGED
|
@@ -474,108 +474,6 @@ function resolveGatewayDir(): string {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
function normalizeIngressUrl(value: unknown): string | undefined {
|
|
478
|
-
if (typeof value !== "string") return undefined;
|
|
479
|
-
const normalized = value.trim().replace(/\/+$/, "");
|
|
480
|
-
return normalized || undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ── Workspace config helpers ──
|
|
484
|
-
|
|
485
|
-
function getWorkspaceConfigPath(instanceDir?: string): string {
|
|
486
|
-
const baseDataDir =
|
|
487
|
-
instanceDir ??
|
|
488
|
-
(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir()));
|
|
489
|
-
return join(baseDataDir, ".vellum", "workspace", "config.json");
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function loadWorkspaceConfig(instanceDir?: string): Record<string, unknown> {
|
|
493
|
-
const configPath = getWorkspaceConfigPath(instanceDir);
|
|
494
|
-
try {
|
|
495
|
-
if (!existsSync(configPath)) return {};
|
|
496
|
-
return JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
497
|
-
string,
|
|
498
|
-
unknown
|
|
499
|
-
>;
|
|
500
|
-
} catch {
|
|
501
|
-
return {};
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function saveWorkspaceConfig(
|
|
506
|
-
config: Record<string, unknown>,
|
|
507
|
-
instanceDir?: string,
|
|
508
|
-
): void {
|
|
509
|
-
const configPath = getWorkspaceConfigPath(instanceDir);
|
|
510
|
-
const dir = dirname(configPath);
|
|
511
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
512
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Write gateway operational settings to the workspace config file so the
|
|
517
|
-
* gateway reads them at startup via its config.ts readWorkspaceConfig().
|
|
518
|
-
*/
|
|
519
|
-
function writeGatewayConfig(
|
|
520
|
-
instanceDir?: string,
|
|
521
|
-
opts?: {
|
|
522
|
-
runtimeProxyEnabled?: boolean;
|
|
523
|
-
runtimeProxyRequireAuth?: boolean;
|
|
524
|
-
unmappedPolicy?: "reject" | "default";
|
|
525
|
-
defaultAssistantId?: string;
|
|
526
|
-
routingEntries?: Array<{
|
|
527
|
-
type: "conversation_id" | "actor_id";
|
|
528
|
-
key: string;
|
|
529
|
-
assistantId: string;
|
|
530
|
-
}>;
|
|
531
|
-
},
|
|
532
|
-
): void {
|
|
533
|
-
const config = loadWorkspaceConfig(instanceDir);
|
|
534
|
-
const gateway = (config.gateway ?? {}) as Record<string, unknown>;
|
|
535
|
-
|
|
536
|
-
if (opts?.runtimeProxyEnabled !== undefined) {
|
|
537
|
-
gateway.runtimeProxyEnabled = opts.runtimeProxyEnabled;
|
|
538
|
-
}
|
|
539
|
-
if (opts?.runtimeProxyRequireAuth !== undefined) {
|
|
540
|
-
gateway.runtimeProxyRequireAuth = opts.runtimeProxyRequireAuth;
|
|
541
|
-
}
|
|
542
|
-
if (opts?.unmappedPolicy !== undefined) {
|
|
543
|
-
gateway.unmappedPolicy = opts.unmappedPolicy;
|
|
544
|
-
}
|
|
545
|
-
if (opts?.defaultAssistantId !== undefined) {
|
|
546
|
-
gateway.defaultAssistantId = opts.defaultAssistantId;
|
|
547
|
-
}
|
|
548
|
-
if (opts?.routingEntries !== undefined) {
|
|
549
|
-
gateway.routingEntries = opts.routingEntries;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
config.gateway = gateway;
|
|
553
|
-
saveWorkspaceConfig(config, instanceDir);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function readWorkspaceIngressPublicBaseUrl(
|
|
557
|
-
instanceDir?: string,
|
|
558
|
-
): string | undefined {
|
|
559
|
-
const baseDataDir =
|
|
560
|
-
instanceDir ??
|
|
561
|
-
(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir()));
|
|
562
|
-
const workspaceConfigPath = join(
|
|
563
|
-
baseDataDir,
|
|
564
|
-
".vellum",
|
|
565
|
-
"workspace",
|
|
566
|
-
"config.json",
|
|
567
|
-
);
|
|
568
|
-
try {
|
|
569
|
-
const raw = JSON.parse(
|
|
570
|
-
readFileSync(workspaceConfigPath, "utf-8"),
|
|
571
|
-
) as Record<string, unknown>;
|
|
572
|
-
const ingress = raw.ingress as Record<string, unknown> | undefined;
|
|
573
|
-
return normalizeIngressUrl(ingress?.publicBaseUrl);
|
|
574
|
-
} catch {
|
|
575
|
-
return undefined;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
477
|
/**
|
|
580
478
|
* Check if the daemon is responsive by hitting its HTTP `/healthz` endpoint.
|
|
581
479
|
* This replaces the socket-based `isSocketResponsive()` check.
|
|
@@ -973,6 +871,7 @@ export async function startLocalDaemon(
|
|
|
973
871
|
"VELLUM_DEBUG",
|
|
974
872
|
"VELLUM_DEV",
|
|
975
873
|
"VELLUM_DESKTOP_APP",
|
|
874
|
+
"VELLUM_WORKSPACE_DIR",
|
|
976
875
|
]) {
|
|
977
876
|
if (process.env[key]) {
|
|
978
877
|
daemonEnv[key] = process.env[key]!;
|
|
@@ -1131,19 +1030,16 @@ export async function startGateway(
|
|
|
1131
1030
|
const effectiveDaemonPort =
|
|
1132
1031
|
resources?.daemonPort ?? Number(process.env.RUNTIME_HTTP_PORT || "7821");
|
|
1133
1032
|
|
|
1134
|
-
// Write gateway operational settings to workspace config before starting
|
|
1135
|
-
// the gateway process. The gateway reads these at startup from config.json.
|
|
1136
|
-
writeGatewayConfig(resources?.instanceDir, {
|
|
1137
|
-
runtimeProxyEnabled: true,
|
|
1138
|
-
runtimeProxyRequireAuth: true,
|
|
1139
|
-
unmappedPolicy: "default",
|
|
1140
|
-
defaultAssistantId: "self",
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
1033
|
const gatewayEnv: Record<string, string> = {
|
|
1144
1034
|
...(process.env as Record<string, string>),
|
|
1145
1035
|
RUNTIME_HTTP_PORT: String(effectiveDaemonPort),
|
|
1146
1036
|
GATEWAY_PORT: String(effectiveGatewayPort),
|
|
1037
|
+
// Pass gateway operational settings via env vars so the CLI does not
|
|
1038
|
+
// need direct access to the workspace config file.
|
|
1039
|
+
RUNTIME_PROXY_ENABLED: "true",
|
|
1040
|
+
RUNTIME_PROXY_REQUIRE_AUTH: "true",
|
|
1041
|
+
UNMAPPED_POLICY: "default",
|
|
1042
|
+
DEFAULT_ASSISTANT_ID: "self",
|
|
1147
1043
|
...(options?.signingKey
|
|
1148
1044
|
? { ACTOR_TOKEN_SIGNING_KEY: options.signingKey }
|
|
1149
1045
|
: {}),
|
|
@@ -1152,15 +1048,8 @@ export async function startGateway(
|
|
|
1152
1048
|
// workspace config for this instance (mirrors the daemon env setup).
|
|
1153
1049
|
...(resources ? { BASE_DATA_DIR: resources.instanceDir } : {}),
|
|
1154
1050
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
// for diagnostic visibility during startup.
|
|
1158
|
-
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(
|
|
1159
|
-
resources?.instanceDir,
|
|
1160
|
-
);
|
|
1161
|
-
const ingressPublicBaseUrl = workspaceIngressPublicBaseUrl ?? publicUrl;
|
|
1162
|
-
if (ingressPublicBaseUrl) {
|
|
1163
|
-
console.log(` Ingress URL: ${ingressPublicBaseUrl}`);
|
|
1051
|
+
if (publicUrl) {
|
|
1052
|
+
console.log(` Ingress URL: ${publicUrl}`);
|
|
1164
1053
|
}
|
|
1165
1054
|
|
|
1166
1055
|
let gateway;
|
|
@@ -87,14 +87,20 @@ export interface HatchedAssistant {
|
|
|
87
87
|
status: string;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export async function hatchAssistant(
|
|
91
|
-
|
|
90
|
+
export async function hatchAssistant(
|
|
91
|
+
token: string,
|
|
92
|
+
orgId: string,
|
|
93
|
+
platformUrl?: string,
|
|
94
|
+
): Promise<HatchedAssistant> {
|
|
95
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
96
|
+
const url = `${resolvedUrl}/v1/assistants/hatch/`;
|
|
92
97
|
|
|
93
98
|
const response = await fetch(url, {
|
|
94
99
|
method: "POST",
|
|
95
100
|
headers: {
|
|
96
101
|
"Content-Type": "application/json",
|
|
97
102
|
...authHeaders(token),
|
|
103
|
+
"Vellum-Organization-Id": orgId,
|
|
98
104
|
},
|
|
99
105
|
body: JSON.stringify({}),
|
|
100
106
|
});
|
|
@@ -143,7 +149,7 @@ export async function fetchOrganizationId(
|
|
|
143
149
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
144
150
|
const url = `${resolvedUrl}/v1/organizations/`;
|
|
145
151
|
const response = await fetch(url, {
|
|
146
|
-
headers: {
|
|
152
|
+
headers: { ...authHeaders(token) },
|
|
147
153
|
});
|
|
148
154
|
|
|
149
155
|
if (!response.ok) {
|
|
@@ -213,7 +219,7 @@ export async function rollbackPlatformAssistant(
|
|
|
213
219
|
method: "POST",
|
|
214
220
|
headers: {
|
|
215
221
|
"Content-Type": "application/json",
|
|
216
|
-
|
|
222
|
+
...authHeaders(token),
|
|
217
223
|
"Vellum-Organization-Id": orgId,
|
|
218
224
|
},
|
|
219
225
|
body: JSON.stringify(version ? { version } : {}),
|
|
@@ -258,7 +264,7 @@ export async function platformInitiateExport(
|
|
|
258
264
|
method: "POST",
|
|
259
265
|
headers: {
|
|
260
266
|
"Content-Type": "application/json",
|
|
261
|
-
|
|
267
|
+
...authHeaders(token),
|
|
262
268
|
"Vellum-Organization-Id": orgId,
|
|
263
269
|
},
|
|
264
270
|
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
@@ -292,7 +298,7 @@ export async function platformPollExportStatus(
|
|
|
292
298
|
`${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
|
|
293
299
|
{
|
|
294
300
|
headers: {
|
|
295
|
-
|
|
301
|
+
...authHeaders(token),
|
|
296
302
|
"Vellum-Organization-Id": orgId,
|
|
297
303
|
},
|
|
298
304
|
},
|
|
@@ -349,7 +355,7 @@ export async function platformImportPreflight(
|
|
|
349
355
|
method: "POST",
|
|
350
356
|
headers: {
|
|
351
357
|
"Content-Type": "application/octet-stream",
|
|
352
|
-
|
|
358
|
+
...authHeaders(token),
|
|
353
359
|
"Vellum-Organization-Id": orgId,
|
|
354
360
|
},
|
|
355
361
|
body: new Blob([bundleData]),
|
|
@@ -375,7 +381,7 @@ export async function platformImportBundle(
|
|
|
375
381
|
method: "POST",
|
|
376
382
|
headers: {
|
|
377
383
|
"Content-Type": "application/octet-stream",
|
|
378
|
-
|
|
384
|
+
...authHeaders(token),
|
|
379
385
|
"Vellum-Organization-Id": orgId,
|
|
380
386
|
},
|
|
381
387
|
body: new Blob([bundleData]),
|
|
@@ -388,3 +394,131 @@ export async function platformImportBundle(
|
|
|
388
394
|
>;
|
|
389
395
|
return { statusCode: response.status, body };
|
|
390
396
|
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Signed-URL upload flow
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
export async function platformRequestUploadUrl(
|
|
403
|
+
token: string,
|
|
404
|
+
orgId: string,
|
|
405
|
+
platformUrl?: string,
|
|
406
|
+
): Promise<{ uploadUrl: string; bundleKey: string; expiresAt: string }> {
|
|
407
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
408
|
+
const response = await fetch(`${resolvedUrl}/v1/migrations/upload-url/`, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
...authHeaders(token),
|
|
413
|
+
"Vellum-Organization-Id": orgId,
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify({ content_type: "application/octet-stream" }),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (response.status === 201) {
|
|
419
|
+
const body = (await response.json()) as {
|
|
420
|
+
upload_url: string;
|
|
421
|
+
bundle_key: string;
|
|
422
|
+
expires_at: string;
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
uploadUrl: body.upload_url,
|
|
426
|
+
bundleKey: body.bundle_key,
|
|
427
|
+
expiresAt: body.expires_at,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (response.status === 404 || response.status === 503) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"Signed uploads are not available on this platform instance",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
438
|
+
detail?: string;
|
|
439
|
+
};
|
|
440
|
+
throw new Error(
|
|
441
|
+
errorBody.detail ??
|
|
442
|
+
`Failed to request upload URL: ${response.status} ${response.statusText}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function platformUploadToSignedUrl(
|
|
447
|
+
uploadUrl: string,
|
|
448
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
const response = await fetch(uploadUrl, {
|
|
451
|
+
method: "PUT",
|
|
452
|
+
headers: {
|
|
453
|
+
"Content-Type": "application/octet-stream",
|
|
454
|
+
},
|
|
455
|
+
body: new Blob([bundleData]),
|
|
456
|
+
signal: AbortSignal.timeout(600_000),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Upload to signed URL failed: ${response.status} ${response.statusText}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function platformImportPreflightFromGcs(
|
|
467
|
+
bundleKey: string,
|
|
468
|
+
token: string,
|
|
469
|
+
orgId: string,
|
|
470
|
+
platformUrl?: string,
|
|
471
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
472
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
473
|
+
const response = await fetch(
|
|
474
|
+
`${resolvedUrl}/v1/migrations/import-preflight-from-gcs/`,
|
|
475
|
+
{
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
...authHeaders(token),
|
|
480
|
+
"Vellum-Organization-Id": orgId,
|
|
481
|
+
},
|
|
482
|
+
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
483
|
+
signal: AbortSignal.timeout(120_000),
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
488
|
+
string,
|
|
489
|
+
unknown
|
|
490
|
+
>;
|
|
491
|
+
return { statusCode: response.status, body };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function platformImportBundleFromGcs(
|
|
495
|
+
bundleKey: string,
|
|
496
|
+
token: string,
|
|
497
|
+
orgId: string,
|
|
498
|
+
platformUrl?: string,
|
|
499
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
500
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
501
|
+
const response = await fetch(
|
|
502
|
+
`${resolvedUrl}/v1/migrations/import-from-gcs/`,
|
|
503
|
+
{
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
...authHeaders(token),
|
|
508
|
+
"Vellum-Organization-Id": orgId,
|
|
509
|
+
},
|
|
510
|
+
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
511
|
+
signal: AbortSignal.timeout(120_000),
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (response.status === 413) {
|
|
516
|
+
throw new Error("Bundle too large to import");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
520
|
+
string,
|
|
521
|
+
unknown
|
|
522
|
+
>;
|
|
523
|
+
return { statusCode: response.status, body };
|
|
524
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
|
+
import { basename, dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
import { getBaseDir, loadAllAssistants } from "./assistant-config.js";
|
|
6
|
+
import type { AssistantEntry } from "./assistant-config.js";
|
|
7
|
+
import {
|
|
8
|
+
stopOrphanedDaemonProcesses,
|
|
9
|
+
stopProcessByPidFile,
|
|
10
|
+
} from "./process.js";
|
|
11
|
+
import { getArchivePath, getMetadataPath } from "./retire-archive.js";
|
|
12
|
+
|
|
13
|
+
export async function retireLocal(
|
|
14
|
+
name: string,
|
|
15
|
+
entry: AssistantEntry,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
console.log("\u{1F5D1}\ufe0f Stopping local assistant...\n");
|
|
18
|
+
|
|
19
|
+
if (!entry.resources) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Local assistant '${name}' is missing resource configuration. Re-hatch to fix.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const resources = entry.resources;
|
|
25
|
+
const vellumDir = join(resources.instanceDir, ".vellum");
|
|
26
|
+
|
|
27
|
+
// Check whether another local assistant shares the same data directory.
|
|
28
|
+
const otherSharesDir = loadAllAssistants().some((other) => {
|
|
29
|
+
if (other.cloud !== "local") return false;
|
|
30
|
+
if (other.assistantId === name) return false;
|
|
31
|
+
if (!other.resources) return false;
|
|
32
|
+
const otherVellumDir = join(other.resources.instanceDir, ".vellum");
|
|
33
|
+
return otherVellumDir === vellumDir;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (otherSharesDir) {
|
|
37
|
+
console.log(
|
|
38
|
+
` Skipping process stop and archive — another local assistant shares ${vellumDir}.`,
|
|
39
|
+
);
|
|
40
|
+
console.log("\u2705 Local instance retired (config entry removed only).");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const daemonPidFile = resources.pidFile;
|
|
45
|
+
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon");
|
|
46
|
+
|
|
47
|
+
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
48
|
+
// drain window (5s) before it exits.
|
|
49
|
+
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
50
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
51
|
+
|
|
52
|
+
// Stop Qdrant — the daemon's graceful shutdown tries to stop it via
|
|
53
|
+
// qdrantManager.stop(), but if the daemon was SIGKILL'd (after 2s timeout)
|
|
54
|
+
// Qdrant may still be running as an orphan. Check both the current PID file
|
|
55
|
+
// location and the legacy location.
|
|
56
|
+
const qdrantPidFile = join(
|
|
57
|
+
vellumDir,
|
|
58
|
+
"workspace",
|
|
59
|
+
"data",
|
|
60
|
+
"qdrant",
|
|
61
|
+
"qdrant.pid",
|
|
62
|
+
);
|
|
63
|
+
const qdrantLegacyPidFile = join(vellumDir, "qdrant.pid");
|
|
64
|
+
await stopProcessByPidFile(qdrantPidFile, "qdrant", undefined, 5000);
|
|
65
|
+
await stopProcessByPidFile(qdrantLegacyPidFile, "qdrant", undefined, 5000);
|
|
66
|
+
|
|
67
|
+
// If the PID file didn't track a running daemon, scan for orphaned
|
|
68
|
+
// daemon processes that may have been started without writing a PID.
|
|
69
|
+
if (!daemonStopped) {
|
|
70
|
+
await stopOrphanedDaemonProcesses();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// For named instances (instanceDir differs from the base directory),
|
|
74
|
+
// archive and remove the entire instance directory. For the default
|
|
75
|
+
// instance, archive only the .vellum subdirectory.
|
|
76
|
+
const isNamedInstance = resources.instanceDir !== getBaseDir();
|
|
77
|
+
const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
|
|
78
|
+
|
|
79
|
+
// Move the data directory out of the way so the path is immediately available
|
|
80
|
+
// for the next hatch, then kick off the tar archive in the background.
|
|
81
|
+
const archivePath = getArchivePath(name);
|
|
82
|
+
const metadataPath = getMetadataPath(name);
|
|
83
|
+
const stagingDir = `${archivePath}.staging`;
|
|
84
|
+
|
|
85
|
+
if (!existsSync(dirToArchive)) {
|
|
86
|
+
console.log(
|
|
87
|
+
` No data directory at ${dirToArchive} — nothing to archive.`,
|
|
88
|
+
);
|
|
89
|
+
console.log("\u2705 Local instance retired.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Ensure the retired archive directory exists before attempting the rename
|
|
94
|
+
mkdirSync(dirname(stagingDir), { recursive: true });
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
renameSync(dirToArchive, stagingDir);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Re-throw so the caller (and the desktop app) knows the archive failed.
|
|
100
|
+
// If the rename fails, old workspace data stays in place and a subsequent
|
|
101
|
+
// hatch would inherit stale SOUL.md, IDENTITY.md, and memories.
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Failed to archive ${dirToArchive}: ${err instanceof Error ? err.message : err}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeFileSync(metadataPath, JSON.stringify(entry, null, 2) + "\n");
|
|
108
|
+
|
|
109
|
+
// Spawn tar + cleanup in the background and detach so the CLI can exit
|
|
110
|
+
// immediately. The staging directory is removed once the archive is written.
|
|
111
|
+
const tarCmd = [
|
|
112
|
+
`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(dirname(stagingDir))} ${JSON.stringify(basename(stagingDir))}`,
|
|
113
|
+
`rm -rf ${JSON.stringify(stagingDir)}`,
|
|
114
|
+
].join(" && ");
|
|
115
|
+
|
|
116
|
+
const child = spawn("sh", ["-c", tarCmd], {
|
|
117
|
+
stdio: "ignore",
|
|
118
|
+
detached: true,
|
|
119
|
+
});
|
|
120
|
+
child.unref();
|
|
121
|
+
|
|
122
|
+
console.log(`📦 Archiving to ${archivePath} in the background.`);
|
|
123
|
+
console.log("\u2705 Local instance retired.");
|
|
124
|
+
}
|
|
@@ -90,6 +90,7 @@ export function buildUpgradeCommitMessage(options: {
|
|
|
90
90
|
*/
|
|
91
91
|
export const CONTAINER_ENV_EXCLUDE_KEYS: ReadonlySet<string> = new Set([
|
|
92
92
|
"CES_SERVICE_TOKEN",
|
|
93
|
+
"GUARDIAN_BOOTSTRAP_SECRET",
|
|
93
94
|
"VELLUM_ASSISTANT_NAME",
|
|
94
95
|
"RUNTIME_HTTP_HOST",
|
|
95
96
|
"PATH",
|
|
@@ -467,6 +468,11 @@ export async function performDockerRollback(
|
|
|
467
468
|
` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
|
|
468
469
|
);
|
|
469
470
|
|
|
471
|
+
// Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
|
|
472
|
+
// set on gateway, not assistant) so it persists across container restarts.
|
|
473
|
+
const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
|
|
474
|
+
const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
|
|
475
|
+
|
|
470
476
|
const cesServiceToken =
|
|
471
477
|
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
472
478
|
|
|
@@ -575,6 +581,7 @@ export async function performDockerRollback(
|
|
|
575
581
|
await startContainers(
|
|
576
582
|
{
|
|
577
583
|
signingKey,
|
|
584
|
+
bootstrapSecret,
|
|
578
585
|
cesServiceToken,
|
|
579
586
|
extraAssistantEnv,
|
|
580
587
|
gatewayPort,
|
|
@@ -695,6 +702,7 @@ export async function performDockerRollback(
|
|
|
695
702
|
await startContainers(
|
|
696
703
|
{
|
|
697
704
|
signingKey,
|
|
705
|
+
bootstrapSecret,
|
|
698
706
|
cesServiceToken,
|
|
699
707
|
extraAssistantEnv,
|
|
700
708
|
gatewayPort,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider API key environment variable names, keyed by provider ID.
|
|
3
|
+
*
|
|
4
|
+
* Keep in sync with:
|
|
5
|
+
* - assistant/src/shared/provider-env-vars.ts
|
|
6
|
+
* - meta/provider-env-vars.json (consumed by the macOS client build)
|
|
7
|
+
*
|
|
8
|
+
* Once a consolidated shared package exists in packages/, all three
|
|
9
|
+
* copies can be replaced by a single import.
|
|
10
|
+
*/
|
|
11
|
+
export const PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
|
|
12
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
13
|
+
openai: "OPENAI_API_KEY",
|
|
14
|
+
gemini: "GEMINI_API_KEY",
|
|
15
|
+
fireworks: "FIREWORKS_API_KEY",
|
|
16
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
17
|
+
brave: "BRAVE_API_KEY",
|
|
18
|
+
perplexity: "PERPLEXITY_API_KEY",
|
|
19
|
+
};
|