@vellumai/cli 0.7.0 → 0.7.2
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/AGENTS.md +3 -11
- package/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
package/src/commands/ps.ts
CHANGED
|
@@ -28,6 +28,10 @@ import { pgrepExact } from "../lib/pgrep";
|
|
|
28
28
|
import { probePort } from "../lib/port-probe";
|
|
29
29
|
import { withStatusEmoji } from "../lib/status-emoji";
|
|
30
30
|
import { execOutput } from "../lib/step-runner";
|
|
31
|
+
import {
|
|
32
|
+
syncCloudAssistants,
|
|
33
|
+
type SyncLogger,
|
|
34
|
+
} from "../lib/sync-cloud-assistants";
|
|
31
35
|
|
|
32
36
|
// ── Table formatting helpers ────────────────────────────────────
|
|
33
37
|
|
|
@@ -468,7 +472,7 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
468
472
|
|
|
469
473
|
// ── List all assistants (no arg) ────────────────────────────────
|
|
470
474
|
|
|
471
|
-
async function listAllAssistants(): Promise<void> {
|
|
475
|
+
async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
472
476
|
const { name: envName, source: envSource } = resolveEnvironmentSource();
|
|
473
477
|
const sourceLabels: Record<typeof envSource, string> = {
|
|
474
478
|
flag: "--environment flag",
|
|
@@ -476,7 +480,31 @@ async function listAllAssistants(): Promise<void> {
|
|
|
476
480
|
config: "~/.config/vellum/environment",
|
|
477
481
|
default: "default",
|
|
478
482
|
};
|
|
479
|
-
console.log(`Environment: ${envName} (${sourceLabels[envSource]})
|
|
483
|
+
console.log(`Environment: ${envName} (${sourceLabels[envSource]})`);
|
|
484
|
+
|
|
485
|
+
const log: SyncLogger | undefined = verbose
|
|
486
|
+
? (msg) => console.log(` [verbose] ${msg}`)
|
|
487
|
+
: undefined;
|
|
488
|
+
|
|
489
|
+
// Refresh cloud assistants from the platform before listing.
|
|
490
|
+
const syncResult = await syncCloudAssistants({ log });
|
|
491
|
+
|
|
492
|
+
// Show platform login status
|
|
493
|
+
if (syncResult) {
|
|
494
|
+
const parts = [`Platform: logged in`];
|
|
495
|
+
if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
|
|
496
|
+
if (syncResult.added > 0 || syncResult.removed > 0) {
|
|
497
|
+
const changes: string[] = [];
|
|
498
|
+
if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
|
|
499
|
+
if (syncResult.removed > 0)
|
|
500
|
+
changes.push(`${syncResult.removed} removed`);
|
|
501
|
+
parts.push(`(${changes.join(", ")})`);
|
|
502
|
+
}
|
|
503
|
+
console.log(parts.join(" "));
|
|
504
|
+
} else {
|
|
505
|
+
console.log("Platform: not logged in");
|
|
506
|
+
}
|
|
507
|
+
console.log("");
|
|
480
508
|
|
|
481
509
|
const assistants = loadAllAssistants();
|
|
482
510
|
const activeId = getActiveAssistant();
|
|
@@ -599,21 +627,28 @@ async function listAllAssistants(): Promise<void> {
|
|
|
599
627
|
export async function ps(): Promise<void> {
|
|
600
628
|
const args = process.argv.slice(3);
|
|
601
629
|
if (args.includes("--help") || args.includes("-h")) {
|
|
602
|
-
console.log("Usage: vellum ps [<name>]");
|
|
630
|
+
console.log("Usage: vellum ps [<name>] [--verbose]");
|
|
603
631
|
console.log("");
|
|
604
632
|
console.log(
|
|
605
633
|
"List all assistants, or show processes for a specific assistant.",
|
|
606
634
|
);
|
|
607
635
|
console.log("");
|
|
608
636
|
console.log("Arguments:");
|
|
609
|
-
console.log(" <name>
|
|
637
|
+
console.log(" <name> Show processes for the named assistant");
|
|
638
|
+
console.log("");
|
|
639
|
+
console.log("Options:");
|
|
640
|
+
console.log(
|
|
641
|
+
" --verbose Show diagnostic logs (platform sync, auth issues)",
|
|
642
|
+
);
|
|
610
643
|
process.exit(0);
|
|
611
644
|
}
|
|
612
645
|
|
|
613
|
-
const
|
|
646
|
+
const verbose = args.includes("--verbose");
|
|
647
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
648
|
+
const assistantId = positional[0];
|
|
614
649
|
|
|
615
650
|
if (!assistantId) {
|
|
616
|
-
await listAllAssistants();
|
|
651
|
+
await listAllAssistants(verbose);
|
|
617
652
|
return;
|
|
618
653
|
}
|
|
619
654
|
|
package/src/commands/restore.ts
CHANGED
|
@@ -9,13 +9,11 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
readPlatformToken,
|
|
11
11
|
rollbackPlatformAssistant,
|
|
12
|
-
|
|
13
|
-
platformImportBundle,
|
|
14
|
-
platformRequestUploadUrl,
|
|
12
|
+
platformRequestSignedUrl,
|
|
15
13
|
platformUploadToSignedUrl,
|
|
16
14
|
platformImportPreflightFromGcs,
|
|
17
15
|
platformImportBundleFromGcs,
|
|
18
|
-
|
|
16
|
+
platformPollJobStatus,
|
|
19
17
|
} from "../lib/platform-client.js";
|
|
20
18
|
import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
|
|
21
19
|
|
|
@@ -181,24 +179,20 @@ async function restorePlatform(
|
|
|
181
179
|
process.exit(1);
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
// Step 1.5 — Upload to GCS via signed URL
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else {
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
182
|
+
// Step 1.5 — Upload to GCS via signed URL.
|
|
183
|
+
// We deliberately omit min/max runtime version here: restore uploads an
|
|
184
|
+
// arbitrary .vbundle from disk (often produced by a different runtime
|
|
185
|
+
// than the one we'd query right now), and the bundle's own manifest is
|
|
186
|
+
// the authority on its compatibility band. The platform skips the
|
|
187
|
+
// version gate when these fields are absent and re-derives compat from
|
|
188
|
+
// the manifest when it processes the import.
|
|
189
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
190
|
+
{ operation: "upload" },
|
|
191
|
+
token,
|
|
192
|
+
entry.runtimeUrl,
|
|
193
|
+
);
|
|
194
|
+
console.log("Uploading bundle...");
|
|
195
|
+
await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
|
|
202
196
|
|
|
203
197
|
// Step 2 — Dry-run path
|
|
204
198
|
if (opts.dryRun) {
|
|
@@ -213,17 +207,11 @@ async function restorePlatform(
|
|
|
213
207
|
|
|
214
208
|
let preflightResult: { statusCode: number; body: Record<string, unknown> };
|
|
215
209
|
try {
|
|
216
|
-
preflightResult =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
: await platformImportPreflight(
|
|
223
|
-
new Uint8Array(bundleData),
|
|
224
|
-
token,
|
|
225
|
-
entry.runtimeUrl,
|
|
226
|
-
);
|
|
210
|
+
preflightResult = await platformImportPreflightFromGcs(
|
|
211
|
+
bundleKey,
|
|
212
|
+
token,
|
|
213
|
+
entry.runtimeUrl,
|
|
214
|
+
);
|
|
227
215
|
} catch (err) {
|
|
228
216
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
229
217
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -353,13 +341,11 @@ async function restorePlatform(
|
|
|
353
341
|
|
|
354
342
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
355
343
|
try {
|
|
356
|
-
importResult =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
entry.runtimeUrl,
|
|
362
|
-
);
|
|
344
|
+
importResult = await platformImportBundleFromGcs(
|
|
345
|
+
bundleKey,
|
|
346
|
+
token,
|
|
347
|
+
entry.runtimeUrl,
|
|
348
|
+
);
|
|
363
349
|
} catch (err) {
|
|
364
350
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
365
351
|
console.error("Error: Import request timed out after 5 minutes.");
|
|
@@ -420,13 +406,9 @@ async function restorePlatform(
|
|
|
420
406
|
while (Date.now() < deadline) {
|
|
421
407
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
422
408
|
|
|
423
|
-
let status:
|
|
424
|
-
status: string;
|
|
425
|
-
result?: Record<string, unknown>;
|
|
426
|
-
error?: string;
|
|
427
|
-
};
|
|
409
|
+
let status: Awaited<ReturnType<typeof platformPollJobStatus>>;
|
|
428
410
|
try {
|
|
429
|
-
status = await
|
|
411
|
+
status = await platformPollJobStatus(jobId, token, entry.runtimeUrl);
|
|
430
412
|
} catch (err) {
|
|
431
413
|
const msg = err instanceof Error ? err.message : String(err);
|
|
432
414
|
if (msg.includes("not found")) {
|
|
@@ -451,7 +433,10 @@ async function restorePlatform(
|
|
|
451
433
|
}
|
|
452
434
|
|
|
453
435
|
if (status.status === "complete") {
|
|
454
|
-
importResult = {
|
|
436
|
+
importResult = {
|
|
437
|
+
statusCode: 200,
|
|
438
|
+
body: (status.result as Record<string, unknown>) ?? {},
|
|
439
|
+
};
|
|
455
440
|
break;
|
|
456
441
|
}
|
|
457
442
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,43 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "readline";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
const base = process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
8
|
-
return join(base, ".vellum");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function getEnvFilePath(): string {
|
|
12
|
-
return join(getVellumDir(), ".env");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readEnvFile(): Record<string, string> {
|
|
16
|
-
const envPath = getEnvFilePath();
|
|
17
|
-
const vars: Record<string, string> = {};
|
|
18
|
-
if (!existsSync(envPath)) return vars;
|
|
19
|
-
|
|
20
|
-
const content = readFileSync(envPath, "utf-8");
|
|
21
|
-
for (const line of content.split("\n")) {
|
|
22
|
-
const trimmed = line.trim();
|
|
23
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
24
|
-
const eqIdx = trimmed.indexOf("=");
|
|
25
|
-
if (eqIdx === -1) continue;
|
|
26
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
27
|
-
const value = trimmed.slice(eqIdx + 1).trim();
|
|
28
|
-
vars[key] = value;
|
|
29
|
-
}
|
|
30
|
-
return vars;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeEnvFile(vars: Record<string, string>): void {
|
|
34
|
-
const envPath = getEnvFilePath();
|
|
35
|
-
const dir = dirname(envPath);
|
|
36
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
-
|
|
38
|
-
const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`);
|
|
39
|
-
writeFileSync(envPath, lines.join("\n") + "\n", { mode: 0o600 });
|
|
40
|
-
}
|
|
3
|
+
import { resolveAssistant } from "../lib/assistant-config.js";
|
|
41
4
|
|
|
42
5
|
async function promptMasked(prompt: string): Promise<string> {
|
|
43
6
|
return new Promise((resolve) => {
|
|
@@ -46,7 +9,6 @@ async function promptMasked(prompt: string): Promise<string> {
|
|
|
46
9
|
output: process.stdout,
|
|
47
10
|
});
|
|
48
11
|
|
|
49
|
-
// Disable echoing by writing the prompt manually and intercepting keystrokes
|
|
50
12
|
process.stdout.write(prompt);
|
|
51
13
|
|
|
52
14
|
const stdin = process.stdin;
|
|
@@ -60,7 +22,6 @@ async function promptMasked(prompt: string): Promise<string> {
|
|
|
60
22
|
const char = key.toString("utf-8");
|
|
61
23
|
|
|
62
24
|
if (char === "\r" || char === "\n") {
|
|
63
|
-
// Enter pressed
|
|
64
25
|
stdin.removeListener("data", onData);
|
|
65
26
|
if (stdin.isTTY) {
|
|
66
27
|
stdin.setRawMode(wasRaw ?? false);
|
|
@@ -69,11 +30,9 @@ async function promptMasked(prompt: string): Promise<string> {
|
|
|
69
30
|
rl.close();
|
|
70
31
|
resolve(input);
|
|
71
32
|
} else if (char === "\u0003") {
|
|
72
|
-
// Ctrl+C
|
|
73
33
|
process.stdout.write("\n");
|
|
74
34
|
process.exit(1);
|
|
75
35
|
} else if (char === "\u007F" || char === "\b") {
|
|
76
|
-
// Backspace
|
|
77
36
|
if (input.length > 0) {
|
|
78
37
|
input = input.slice(0, -1);
|
|
79
38
|
process.stdout.write("\b \b");
|
|
@@ -111,39 +70,23 @@ export async function setup(): Promise<void> {
|
|
|
111
70
|
console.log("");
|
|
112
71
|
console.log("Interactive wizard to configure API keys.");
|
|
113
72
|
console.log(
|
|
114
|
-
"
|
|
73
|
+
"Injects secrets into your running assistant via the gateway API.",
|
|
115
74
|
);
|
|
116
75
|
process.exit(0);
|
|
117
76
|
}
|
|
118
77
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
78
|
+
const entry = resolveAssistant();
|
|
79
|
+
if (!entry) {
|
|
80
|
+
console.error(
|
|
81
|
+
"Error: No active assistant found. Run `vellum hatch` first.",
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
124
85
|
|
|
125
|
-
|
|
126
|
-
const masked =
|
|
127
|
-
existingVars.ANTHROPIC_API_KEY.slice(0, 7) +
|
|
128
|
-
"..." +
|
|
129
|
-
existingVars.ANTHROPIC_API_KEY.slice(-4);
|
|
130
|
-
console.log(`Anthropic API key is already configured (${masked}).`);
|
|
86
|
+
const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
|
|
131
87
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
output: process.stdout,
|
|
135
|
-
});
|
|
136
|
-
const answer = await new Promise<string>((resolve) => {
|
|
137
|
-
rl.question("Overwrite? [y/N] ", resolve);
|
|
138
|
-
});
|
|
139
|
-
rl.close();
|
|
140
|
-
|
|
141
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
142
|
-
console.log("\nSetup complete. No changes made.");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
console.log("");
|
|
146
|
-
}
|
|
88
|
+
console.log("Vellum Setup");
|
|
89
|
+
console.log("============\n");
|
|
147
90
|
|
|
148
91
|
const apiKey = await promptMasked(
|
|
149
92
|
"Enter your Anthropic API key (sk-ant-...): ",
|
|
@@ -164,9 +107,31 @@ export async function setup(): Promise<void> {
|
|
|
164
107
|
process.exit(1);
|
|
165
108
|
}
|
|
166
109
|
|
|
167
|
-
|
|
168
|
-
|
|
110
|
+
const headers: Record<string, string> = {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
Accept: "application/json",
|
|
113
|
+
};
|
|
114
|
+
if (entry.bearerToken) {
|
|
115
|
+
headers["Authorization"] = `Bearer ${entry.bearerToken}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await fetch(`${gatewayUrl}/v1/secrets`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
type: "credential",
|
|
123
|
+
name: "ANTHROPIC_API_KEY",
|
|
124
|
+
value: apiKey.trim(),
|
|
125
|
+
}),
|
|
126
|
+
signal: AbortSignal.timeout(10_000),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
console.error(
|
|
131
|
+
`Error: Failed to store API key in assistant (${response.status}).`,
|
|
132
|
+
);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
169
135
|
|
|
170
|
-
console.log(
|
|
171
|
-
console.log("Setup complete.");
|
|
136
|
+
console.log("\nAPI key saved to assistant. Setup complete.");
|
|
172
137
|
}
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
findAssistantByName,
|
|
5
|
-
loadLatestAssistant,
|
|
6
|
-
} from "../lib/assistant-config";
|
|
3
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
7
4
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
5
|
import { dockerResourceNames } from "../lib/docker";
|
|
9
6
|
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
@@ -58,7 +55,7 @@ export async function ssh(): Promise<void> {
|
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
const name = process.argv[3];
|
|
61
|
-
const entry =
|
|
58
|
+
const entry = resolveAssistant(name);
|
|
62
59
|
|
|
63
60
|
if (!entry) {
|
|
64
61
|
if (name) {
|
package/src/commands/teleport.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
getPlatformUrl,
|
|
18
18
|
hatchAssistant,
|
|
19
19
|
checkExistingPlatformAssistant,
|
|
20
|
-
platformInitiateExport,
|
|
21
20
|
platformPollJobStatus,
|
|
22
21
|
platformImportBundleFromGcs,
|
|
23
22
|
platformImportPreflightFromGcs,
|
|
24
23
|
platformRequestSignedUrl,
|
|
24
|
+
VersionMismatchError,
|
|
25
25
|
ensureSelfHostedLocalRegistration,
|
|
26
26
|
readGatewayCredential,
|
|
27
27
|
reprovisionAssistantApiKey,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../lib/platform-client.js";
|
|
32
32
|
import {
|
|
33
33
|
localRuntimeExportToGcs,
|
|
34
|
+
localRuntimeIdentity,
|
|
34
35
|
localRuntimeImportFromGcs,
|
|
35
36
|
localRuntimePollJobStatus,
|
|
36
37
|
MigrationInProgressError,
|
|
@@ -255,13 +256,17 @@ async function getAccessToken(
|
|
|
255
256
|
|
|
256
257
|
/**
|
|
257
258
|
* Detect a 401 Unauthorized raised by `localRuntimeExportToGcs` /
|
|
258
|
-
* `localRuntimeImportFromGcs`.
|
|
259
|
-
* `"Local runtime <op> failed (401): ..."`
|
|
260
|
-
*
|
|
259
|
+
* `localRuntimeImportFromGcs` / `localRuntimeIdentity`. They throw Error
|
|
260
|
+
* with a message of the form `"Local runtime <op> failed (401): ..."` or
|
|
261
|
+
* `"Failed to fetch runtime identity: 401 ..."` when the gateway rejects
|
|
262
|
+
* the cached guardian token.
|
|
261
263
|
*/
|
|
262
264
|
function isRuntime401(err: unknown): boolean {
|
|
263
265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
-
return
|
|
266
|
+
return (
|
|
267
|
+
/Local runtime [^(]*failed \(401\)/.test(msg) ||
|
|
268
|
+
/Failed to fetch runtime identity: 401\b/.test(msg)
|
|
269
|
+
);
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
/**
|
|
@@ -368,13 +373,40 @@ async function exportFromAssistant(
|
|
|
368
373
|
}
|
|
369
374
|
|
|
370
375
|
if (cloud === "local" || cloud === "docker") {
|
|
376
|
+
// Ask the source runtime which version it's running before requesting
|
|
377
|
+
// the signed upload URL. The bundle is produced by the daemon (not the
|
|
378
|
+
// CLI), so the daemon's version is what defines the bundle's
|
|
379
|
+
// `min_runtime_version`. Stamping with `cliPkg.version` instead would
|
|
380
|
+
// record an inaccurate compatibility band whenever the CLI/daemon have
|
|
381
|
+
// drifted (a normal case in real usage — `vellum upgrade` swaps the
|
|
382
|
+
// daemon, the CLI is updated separately).
|
|
383
|
+
let sourceRuntimeVersion: string;
|
|
384
|
+
try {
|
|
385
|
+
const identity = await callRuntimeWithAuthRetry(
|
|
386
|
+
entry.runtimeUrl,
|
|
387
|
+
entry.assistantId,
|
|
388
|
+
async (token) => localRuntimeIdentity(entry, token),
|
|
389
|
+
);
|
|
390
|
+
sourceRuntimeVersion = identity.version;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
393
|
+
console.error(
|
|
394
|
+
`Error: Could not fetch runtime identity from '${entry.assistantId}': ${msg}`,
|
|
395
|
+
);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
371
399
|
// Request a signed upload URL from the platform instance that will
|
|
372
400
|
// eventually own the bundle (i.e. the one the importer will read from).
|
|
373
401
|
// Passing the target's runtime URL here keeps upload and download on
|
|
374
402
|
// the same platform — otherwise a non-default/stale platform URL would
|
|
375
403
|
// cause the import to look at an empty object.
|
|
376
404
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
377
|
-
{
|
|
405
|
+
{
|
|
406
|
+
operation: "upload",
|
|
407
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
408
|
+
maxRuntimeVersion: null,
|
|
409
|
+
},
|
|
378
410
|
platformToken,
|
|
379
411
|
bundlePlatformUrl,
|
|
380
412
|
);
|
|
@@ -391,7 +423,7 @@ async function exportFromAssistant(
|
|
|
391
423
|
entry.runtimeUrl,
|
|
392
424
|
entry.assistantId,
|
|
393
425
|
async (token) => {
|
|
394
|
-
const r = await localRuntimeExportToGcs(entry
|
|
426
|
+
const r = await localRuntimeExportToGcs(entry, token, {
|
|
395
427
|
uploadUrl,
|
|
396
428
|
description: "teleport export",
|
|
397
429
|
});
|
|
@@ -418,8 +450,7 @@ async function exportFromAssistant(
|
|
|
418
450
|
|
|
419
451
|
const terminal = await pollJobUntilDone({
|
|
420
452
|
label: "local-runtime export",
|
|
421
|
-
poll: () =>
|
|
422
|
-
localRuntimePollJobStatus(entry.runtimeUrl, accessToken, jobId),
|
|
453
|
+
poll: () => localRuntimePollJobStatus(entry, accessToken, jobId),
|
|
423
454
|
// Large exports can take longer than a guardian-token lease. If the
|
|
424
455
|
// runtime returns 401 mid-poll, re-lease a fresh token and rebind the
|
|
425
456
|
// closure variable so the next poll uses it.
|
|
@@ -442,22 +473,68 @@ async function exportFromAssistant(
|
|
|
442
473
|
}
|
|
443
474
|
|
|
444
475
|
if (cloud === "vellum") {
|
|
445
|
-
//
|
|
446
|
-
// the bundle
|
|
447
|
-
//
|
|
448
|
-
|
|
476
|
+
// Ask the managed runtime which version it's running so the signed-URL
|
|
477
|
+
// request records the bundle's actual `min_runtime_version`. The
|
|
478
|
+
// platform-managed runtime is the exporter; the CLI version is
|
|
479
|
+
// unrelated. Routed via the wildcard proxy with platform-token auth
|
|
480
|
+
// (resolveRuntimeUrl + migrationRequestHeaders inside
|
|
481
|
+
// localRuntimeIdentity).
|
|
482
|
+
let sourceRuntimeVersion: string;
|
|
483
|
+
try {
|
|
484
|
+
const identity = await localRuntimeIdentity(entry, platformToken);
|
|
485
|
+
sourceRuntimeVersion = identity.version;
|
|
486
|
+
} catch (err) {
|
|
487
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
488
|
+
console.error(
|
|
489
|
+
`Error: Could not fetch runtime identity from '${entry.assistantId}': ${msg}`,
|
|
490
|
+
);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Platform source — request a signed upload URL on the same platform
|
|
495
|
+
// instance the bundle will eventually be imported from, then ask the
|
|
496
|
+
// managed runtime to export directly to GCS. The runtime endpoint is
|
|
497
|
+
// reached via the platform's wildcard runtime proxy at
|
|
498
|
+
// `/v1/assistants/<id>/migrations/export-to-gcs` — the
|
|
499
|
+
// `localRuntimeExportToGcs` helper uses `resolveRuntimeMigrationUrl` to
|
|
500
|
+
// pick that shape for `cloud === "vellum"` and `migrationRequestHeaders`
|
|
501
|
+
// to send platform-token auth (no guardian-token bootstrap).
|
|
502
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
503
|
+
{
|
|
504
|
+
operation: "upload",
|
|
505
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
506
|
+
maxRuntimeVersion: null,
|
|
507
|
+
},
|
|
449
508
|
platformToken,
|
|
450
|
-
|
|
451
|
-
entry.runtimeUrl,
|
|
509
|
+
bundlePlatformUrl,
|
|
452
510
|
);
|
|
453
511
|
|
|
512
|
+
let jobId: string;
|
|
513
|
+
let exportPlatformToken = platformToken;
|
|
514
|
+
try {
|
|
515
|
+
({ jobId } = await localRuntimeExportToGcs(entry, exportPlatformToken, {
|
|
516
|
+
uploadUrl,
|
|
517
|
+
description: "teleport export",
|
|
518
|
+
}));
|
|
519
|
+
} catch (err) {
|
|
520
|
+
if (err instanceof MigrationInProgressError) {
|
|
521
|
+
console.error(
|
|
522
|
+
`Error: Another teleport export is already in progress on '${entry.assistantId}' (job ${err.existingJobId}). Wait for it to finish or check its status, then re-run.`,
|
|
523
|
+
);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
|
|
454
529
|
console.log(`Export started (job ${jobId})...`);
|
|
455
530
|
|
|
456
|
-
|
|
531
|
+
// Polling also goes through the wildcard proxy — `localRuntimePollJobStatus`
|
|
532
|
+
// builds `/v1/assistants/<id>/migrations/jobs/<jobId>` for `cloud === "vellum"`
|
|
533
|
+
// (the dedicated `/v1/migrations/jobs/{id}/` endpoint queries platform-side
|
|
534
|
+
// ImportJob records and 404s on runtime-created job IDs).
|
|
457
535
|
const terminal = await pollJobUntilDone({
|
|
458
536
|
label: "platform export",
|
|
459
|
-
poll: () =>
|
|
460
|
-
platformPollJobStatus(jobId, exportPlatformToken, entry.runtimeUrl),
|
|
537
|
+
poll: () => localRuntimePollJobStatus(entry, exportPlatformToken, jobId),
|
|
461
538
|
// The platform token is normally static per-process, but re-reading the
|
|
462
539
|
// on-disk credential covers the case where the user ran `vellum login`
|
|
463
540
|
// in another terminal during a long migration. A persistent 401 after
|
|
@@ -478,14 +555,7 @@ async function exportFromAssistant(
|
|
|
478
555
|
process.exit(1);
|
|
479
556
|
}
|
|
480
557
|
|
|
481
|
-
|
|
482
|
-
console.error(
|
|
483
|
-
"Export completed but the platform did not return a bundle_key. Is the platform up to date?",
|
|
484
|
-
);
|
|
485
|
-
process.exit(1);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return { bundleKey: terminal.bundleKey };
|
|
558
|
+
return { bundleKey };
|
|
489
559
|
}
|
|
490
560
|
|
|
491
561
|
console.error(
|
|
@@ -644,11 +714,56 @@ async function importToAssistant(
|
|
|
644
714
|
// never touches the bytes. The URL must target the same platform the
|
|
645
715
|
// bundle was uploaded to; otherwise the object won't exist on this
|
|
646
716
|
// platform's GCS bucket.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
)
|
|
717
|
+
//
|
|
718
|
+
// The platform's vbundle version gate compares the **target runtime's**
|
|
719
|
+
// version against the bundle's compatibility range. The CLI and the
|
|
720
|
+
// target assistant's daemon can diverge (assistants upgrade
|
|
721
|
+
// independently), so we MUST query the target runtime's `/v1/identity`
|
|
722
|
+
// for its version rather than sending `cliPkg.version`. Sending the CLI
|
|
723
|
+
// version here would falsely 422 a valid import (or pass a bundle the
|
|
724
|
+
// target can't actually load) whenever the two drift apart.
|
|
725
|
+
let targetRuntimeVersion: string;
|
|
726
|
+
try {
|
|
727
|
+
const identity = await callRuntimeWithAuthRetry(
|
|
728
|
+
entry.runtimeUrl,
|
|
729
|
+
entry.assistantId,
|
|
730
|
+
(token) => localRuntimeIdentity(entry, token),
|
|
731
|
+
);
|
|
732
|
+
targetRuntimeVersion = identity.version;
|
|
733
|
+
} catch (err) {
|
|
734
|
+
// Surface and abort — silently falling back to `cliPkg.version` would
|
|
735
|
+
// re-introduce the bug this code is fixing. If the runtime is
|
|
736
|
+
// unreachable, the import would fail downstream anyway.
|
|
737
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
738
|
+
console.error(
|
|
739
|
+
`Error: Could not read target runtime version from '${entry.assistantId}': ${msg}`,
|
|
740
|
+
);
|
|
741
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let bundleUrl: string;
|
|
746
|
+
try {
|
|
747
|
+
const result = await platformRequestSignedUrl(
|
|
748
|
+
{
|
|
749
|
+
operation: "download",
|
|
750
|
+
bundleKey,
|
|
751
|
+
targetRuntimeVersion,
|
|
752
|
+
},
|
|
753
|
+
platformToken,
|
|
754
|
+
bundlePlatformUrl,
|
|
755
|
+
);
|
|
756
|
+
bundleUrl = result.url;
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (err instanceof VersionMismatchError) {
|
|
759
|
+
// 422 version_mismatch is terminal — the bundle's runtime range and
|
|
760
|
+
// the target runtime's version don't overlap. Surface the
|
|
761
|
+
// platform-formatted message and exit; do NOT retry.
|
|
762
|
+
console.error(`Error: ${err.message}`);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
652
767
|
|
|
653
768
|
console.log("Importing data...");
|
|
654
769
|
|
|
@@ -659,7 +774,7 @@ async function importToAssistant(
|
|
|
659
774
|
entry.runtimeUrl,
|
|
660
775
|
entry.assistantId,
|
|
661
776
|
async (token) => {
|
|
662
|
-
const r = await localRuntimeImportFromGcs(entry
|
|
777
|
+
const r = await localRuntimeImportFromGcs(entry, token, {
|
|
663
778
|
bundleUrl,
|
|
664
779
|
});
|
|
665
780
|
return { jobId: r.jobId, token };
|
|
@@ -682,8 +797,7 @@ async function importToAssistant(
|
|
|
682
797
|
|
|
683
798
|
const terminal = await pollJobUntilDone({
|
|
684
799
|
label: "local-runtime import",
|
|
685
|
-
poll: () =>
|
|
686
|
-
localRuntimePollJobStatus(entry.runtimeUrl, accessToken, jobId),
|
|
800
|
+
poll: () => localRuntimePollJobStatus(entry, accessToken, jobId),
|
|
687
801
|
refreshOn401: async () => {
|
|
688
802
|
accessToken = await getAccessToken(
|
|
689
803
|
entry.runtimeUrl,
|