@vellumai/cli 0.7.1 → 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/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +121 -5
- package/src/__tests__/teleport.test.ts +512 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +47 -6
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/restore.ts +7 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +111 -11
- package/src/commands/upgrade.ts +6 -0
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +40 -126
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
- package/src/lib/__tests__/runtime-url.test.ts +39 -1
- package/src/lib/assistant-client.ts +13 -5
- package/src/lib/assistant-config.ts +0 -25
- package/src/lib/backup-ops.ts +43 -17
- 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/local-runtime-client.ts +82 -1
- package/src/lib/local.ts +9 -7
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +96 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/commands/pair.ts +0 -212
package/src/commands/client.ts
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
} from "../lib/constants";
|
|
13
13
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
14
14
|
import { getLocalLanIPv4 } from "../lib/local";
|
|
15
|
+
import {
|
|
16
|
+
fetchOrganizationId,
|
|
17
|
+
readPlatformToken,
|
|
18
|
+
} from "../lib/platform-client";
|
|
15
19
|
import { tuiLog } from "../lib/tui-log";
|
|
16
20
|
|
|
17
21
|
const ANSI = {
|
|
@@ -26,6 +30,11 @@ interface ParsedArgs {
|
|
|
26
30
|
runtimeUrl: string;
|
|
27
31
|
assistantId: string;
|
|
28
32
|
species: Species;
|
|
33
|
+
/** "vellum" for platform-hosted assistants, undefined for local. */
|
|
34
|
+
cloud?: string;
|
|
35
|
+
/** Platform session token (X-Session-Token), set when cloud === "vellum". */
|
|
36
|
+
platformToken?: string;
|
|
37
|
+
/** Guardian JWT (Authorization: Bearer), set for local assistants. */
|
|
29
38
|
bearerToken?: string;
|
|
30
39
|
project?: string;
|
|
31
40
|
zone?: string;
|
|
@@ -89,10 +98,17 @@ function parseArgs(): ParsedArgs {
|
|
|
89
98
|
|
|
90
99
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
91
100
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
92
|
-
const
|
|
93
|
-
loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
|
|
101
|
+
const cloud = entry?.cloud;
|
|
94
102
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
95
103
|
|
|
104
|
+
// Platform-hosted assistants use a session token; local assistants use a guardian JWT.
|
|
105
|
+
const platformToken =
|
|
106
|
+
cloud === "vellum" ? (readPlatformToken() ?? undefined) : undefined;
|
|
107
|
+
const bearerToken =
|
|
108
|
+
cloud === "vellum"
|
|
109
|
+
? undefined
|
|
110
|
+
: (loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined);
|
|
111
|
+
|
|
96
112
|
for (let i = 0; i < flagArgs.length; i++) {
|
|
97
113
|
const flag = flagArgs[i];
|
|
98
114
|
if ((flag === "--url" || flag === "-u") && flagArgs[i + 1]) {
|
|
@@ -109,6 +125,8 @@ function parseArgs(): ParsedArgs {
|
|
|
109
125
|
runtimeUrl: maybeSwapToLocalhost(runtimeUrl.replace(/\/+$/, "")),
|
|
110
126
|
assistantId,
|
|
111
127
|
species,
|
|
128
|
+
cloud,
|
|
129
|
+
platformToken,
|
|
112
130
|
bearerToken,
|
|
113
131
|
project: entry?.project,
|
|
114
132
|
zone: entry?.zone,
|
|
@@ -181,11 +199,34 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
export async function client(): Promise<void> {
|
|
184
|
-
const {
|
|
185
|
-
|
|
202
|
+
const {
|
|
203
|
+
runtimeUrl,
|
|
204
|
+
assistantId,
|
|
205
|
+
species,
|
|
206
|
+
cloud,
|
|
207
|
+
platformToken,
|
|
208
|
+
bearerToken,
|
|
209
|
+
project,
|
|
210
|
+
zone,
|
|
211
|
+
} = parseArgs();
|
|
186
212
|
|
|
187
213
|
tuiLog.init();
|
|
188
|
-
tuiLog.info("session start", { runtimeUrl, assistantId, species });
|
|
214
|
+
tuiLog.info("session start", { runtimeUrl, assistantId, species, cloud });
|
|
215
|
+
|
|
216
|
+
// Build pre-constructed auth headers so all fetch sites share a single object.
|
|
217
|
+
let auth: Record<string, string> | undefined;
|
|
218
|
+
if (cloud === "vellum" && platformToken) {
|
|
219
|
+
const orgId = await fetchOrganizationId(platformToken).catch((err) => {
|
|
220
|
+
tuiLog.warn("failed to fetch organization id", { err: String(err) });
|
|
221
|
+
return undefined;
|
|
222
|
+
});
|
|
223
|
+
auth = {
|
|
224
|
+
"X-Session-Token": platformToken,
|
|
225
|
+
...(orgId ? { "Vellum-Organization-Id": orgId } : {}),
|
|
226
|
+
};
|
|
227
|
+
} else if (bearerToken) {
|
|
228
|
+
auth = { Authorization: `Bearer ${bearerToken}` };
|
|
229
|
+
}
|
|
189
230
|
|
|
190
231
|
const { renderChatApp } = await import("../components/DefaultMainScreen");
|
|
191
232
|
|
|
@@ -203,6 +244,6 @@ export async function client(): Promise<void> {
|
|
|
203
244
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
204
245
|
process.exit(0);
|
|
205
246
|
},
|
|
206
|
-
{
|
|
247
|
+
{ auth, project, zone },
|
|
207
248
|
);
|
|
208
249
|
}
|
package/src/commands/events.ts
CHANGED
package/src/commands/exec.ts
CHANGED
|
@@ -156,10 +156,19 @@ export async function exec(): Promise<void> {
|
|
|
156
156
|
const cloud = resolveCloud(entry);
|
|
157
157
|
|
|
158
158
|
if (cloud === "local") {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
const child = spawn(command[0], command.slice(1), { stdio: "inherit" });
|
|
160
|
+
await new Promise<void>((resolve) => {
|
|
161
|
+
child.on("close", (code) => {
|
|
162
|
+
process.exitCode = code ?? 0;
|
|
163
|
+
resolve();
|
|
164
|
+
});
|
|
165
|
+
child.on("error", (err) => {
|
|
166
|
+
console.error(`Error: ${err.message}`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
if (cloud === "apple-container") {
|
package/src/commands/hatch.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
|
32
32
|
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
33
33
|
|
|
34
34
|
const HATCH_TIMEOUT_MS: Record<Species, number> = {
|
|
35
|
-
vellum:
|
|
35
|
+
vellum: 5 * 60 * 1000,
|
|
36
36
|
openclaw: 10 * 60 * 1000,
|
|
37
37
|
};
|
|
38
38
|
const DEFAULT_SPECIES: Species = "vellum";
|
package/src/commands/restore.ts
CHANGED
|
@@ -179,7 +179,13 @@ async function restorePlatform(
|
|
|
179
179
|
process.exit(1);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// Step 1.5 — Upload to GCS via signed URL
|
|
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.
|
|
183
189
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
184
190
|
{ operation: "upload" },
|
|
185
191
|
token,
|
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/teleport.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
platformImportBundleFromGcs,
|
|
22
22
|
platformImportPreflightFromGcs,
|
|
23
23
|
platformRequestSignedUrl,
|
|
24
|
+
VersionMismatchError,
|
|
24
25
|
ensureSelfHostedLocalRegistration,
|
|
25
26
|
readGatewayCredential,
|
|
26
27
|
reprovisionAssistantApiKey,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
} from "../lib/platform-client.js";
|
|
31
32
|
import {
|
|
32
33
|
localRuntimeExportToGcs,
|
|
34
|
+
localRuntimeIdentity,
|
|
33
35
|
localRuntimeImportFromGcs,
|
|
34
36
|
localRuntimePollJobStatus,
|
|
35
37
|
MigrationInProgressError,
|
|
@@ -254,13 +256,17 @@ async function getAccessToken(
|
|
|
254
256
|
|
|
255
257
|
/**
|
|
256
258
|
* Detect a 401 Unauthorized raised by `localRuntimeExportToGcs` /
|
|
257
|
-
* `localRuntimeImportFromGcs`.
|
|
258
|
-
* `"Local runtime <op> failed (401): ..."`
|
|
259
|
-
*
|
|
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.
|
|
260
263
|
*/
|
|
261
264
|
function isRuntime401(err: unknown): boolean {
|
|
262
265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
263
|
-
return
|
|
266
|
+
return (
|
|
267
|
+
/Local runtime [^(]*failed \(401\)/.test(msg) ||
|
|
268
|
+
/Failed to fetch runtime identity: 401\b/.test(msg)
|
|
269
|
+
);
|
|
264
270
|
}
|
|
265
271
|
|
|
266
272
|
/**
|
|
@@ -367,13 +373,40 @@ async function exportFromAssistant(
|
|
|
367
373
|
}
|
|
368
374
|
|
|
369
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
|
+
|
|
370
399
|
// Request a signed upload URL from the platform instance that will
|
|
371
400
|
// eventually own the bundle (i.e. the one the importer will read from).
|
|
372
401
|
// Passing the target's runtime URL here keeps upload and download on
|
|
373
402
|
// the same platform — otherwise a non-default/stale platform URL would
|
|
374
403
|
// cause the import to look at an empty object.
|
|
375
404
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
376
|
-
{
|
|
405
|
+
{
|
|
406
|
+
operation: "upload",
|
|
407
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
408
|
+
maxRuntimeVersion: null,
|
|
409
|
+
},
|
|
377
410
|
platformToken,
|
|
378
411
|
bundlePlatformUrl,
|
|
379
412
|
);
|
|
@@ -440,6 +473,24 @@ async function exportFromAssistant(
|
|
|
440
473
|
}
|
|
441
474
|
|
|
442
475
|
if (cloud === "vellum") {
|
|
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
|
+
|
|
443
494
|
// Platform source — request a signed upload URL on the same platform
|
|
444
495
|
// instance the bundle will eventually be imported from, then ask the
|
|
445
496
|
// managed runtime to export directly to GCS. The runtime endpoint is
|
|
@@ -449,7 +500,11 @@ async function exportFromAssistant(
|
|
|
449
500
|
// pick that shape for `cloud === "vellum"` and `migrationRequestHeaders`
|
|
450
501
|
// to send platform-token auth (no guardian-token bootstrap).
|
|
451
502
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
452
|
-
{
|
|
503
|
+
{
|
|
504
|
+
operation: "upload",
|
|
505
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
506
|
+
maxRuntimeVersion: null,
|
|
507
|
+
},
|
|
453
508
|
platformToken,
|
|
454
509
|
bundlePlatformUrl,
|
|
455
510
|
);
|
|
@@ -659,11 +714,56 @@ async function importToAssistant(
|
|
|
659
714
|
// never touches the bytes. The URL must target the same platform the
|
|
660
715
|
// bundle was uploaded to; otherwise the object won't exist on this
|
|
661
716
|
// platform's GCS bucket.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
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
|
+
}
|
|
667
767
|
|
|
668
768
|
console.log("Importing data...");
|
|
669
769
|
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
buildStartingEvent,
|
|
41
41
|
buildUpgradeCommitMessage,
|
|
42
42
|
captureContainerEnv,
|
|
43
|
+
captureUpgradeFailureLogs,
|
|
43
44
|
commitWorkspaceViaGateway,
|
|
44
45
|
CONTAINER_ENV_EXCLUDE_KEYS,
|
|
45
46
|
rollbackMigrations,
|
|
@@ -511,6 +512,11 @@ async function upgradeDocker(
|
|
|
511
512
|
} else {
|
|
512
513
|
console.error(`\n❌ Containers failed to become ready within the timeout.`);
|
|
513
514
|
|
|
515
|
+
const logDir = await captureUpgradeFailureLogs(res, `${instanceName}-upgrade-failure`);
|
|
516
|
+
if (logDir) {
|
|
517
|
+
console.log(`📋 Container logs saved to: ${logDir}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
514
520
|
if (previousImageRefs) {
|
|
515
521
|
await broadcastUpgradeEvent(
|
|
516
522
|
entry.runtimeUrl,
|
package/src/commands/wake.ts
CHANGED
|
@@ -195,22 +195,11 @@ export async function wake(): Promise<void> {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
|
|
204
|
-
if (ngrokChild?.pid) {
|
|
205
|
-
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
206
|
-
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
207
|
-
}
|
|
208
|
-
} finally {
|
|
209
|
-
if (prevBaseDataDir !== undefined) {
|
|
210
|
-
process.env.BASE_DATA_DIR = prevBaseDataDir;
|
|
211
|
-
} else {
|
|
212
|
-
delete process.env.BASE_DATA_DIR;
|
|
213
|
-
}
|
|
198
|
+
const workspaceDir = join(resources.instanceDir, ".vellum", "workspace");
|
|
199
|
+
const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort, workspaceDir);
|
|
200
|
+
if (ngrokChild?.pid) {
|
|
201
|
+
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
202
|
+
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
214
203
|
}
|
|
215
204
|
|
|
216
205
|
console.log("Wake complete.");
|