@vellumai/cli 0.7.1 → 0.7.3
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 +515 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +90 -7
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/login.ts +11 -0
- package/src/commands/restore.ts +7 -1
- package/src/commands/rollback.ts +1 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +122 -12
- package/src/commands/upgrade.ts +8 -2
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +42 -130
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +53 -35
- 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/client-identity.ts +9 -5
- package/src/lib/docker.ts +6 -267
- package/src/lib/environments/paths.ts +20 -0
- 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 +100 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/statefulset.ts +375 -0
- package/src/lib/upgrade-lifecycle.ts +97 -1
- package/src/commands/pair.ts +0 -212
package/src/commands/client.ts
CHANGED
|
@@ -12,8 +12,19 @@ import {
|
|
|
12
12
|
} from "../lib/constants";
|
|
13
13
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
14
14
|
import { getLocalLanIPv4 } from "../lib/local";
|
|
15
|
+
import {
|
|
16
|
+
CLI_INTERFACE_ID,
|
|
17
|
+
getClientRegistrationHeaders,
|
|
18
|
+
} from "../lib/client-identity";
|
|
19
|
+
import {
|
|
20
|
+
fetchOrganizationId,
|
|
21
|
+
readPlatformToken,
|
|
22
|
+
} from "../lib/platform-client";
|
|
15
23
|
import { tuiLog } from "../lib/tui-log";
|
|
16
24
|
|
|
25
|
+
const SUPPORTED_INTERFACES = ["cli"] as const;
|
|
26
|
+
type SupportedInterface = (typeof SUPPORTED_INTERFACES)[number];
|
|
27
|
+
|
|
17
28
|
const ANSI = {
|
|
18
29
|
reset: "\x1b[0m",
|
|
19
30
|
bold: "\x1b[1m",
|
|
@@ -26,7 +37,14 @@ interface ParsedArgs {
|
|
|
26
37
|
runtimeUrl: string;
|
|
27
38
|
assistantId: string;
|
|
28
39
|
species: Species;
|
|
40
|
+
/** "vellum" for platform-hosted assistants, undefined for local. */
|
|
41
|
+
cloud?: string;
|
|
42
|
+
/** Platform session token (X-Session-Token), set when cloud === "vellum". */
|
|
43
|
+
platformToken?: string;
|
|
44
|
+
/** Guardian JWT (Authorization: Bearer), set for local assistants. */
|
|
29
45
|
bearerToken?: string;
|
|
46
|
+
/** Interface identifier sent as X-Vellum-Interface-Id on all requests. */
|
|
47
|
+
interfaceId: SupportedInterface;
|
|
30
48
|
project?: string;
|
|
31
49
|
zone?: string;
|
|
32
50
|
}
|
|
@@ -45,7 +63,9 @@ function parseArgs(): ParsedArgs {
|
|
|
45
63
|
(arg === "--url" ||
|
|
46
64
|
arg === "-u" ||
|
|
47
65
|
arg === "--assistant-id" ||
|
|
48
|
-
arg === "-a"
|
|
66
|
+
arg === "-a" ||
|
|
67
|
+
arg === "--interface" ||
|
|
68
|
+
arg === "-i") &&
|
|
49
69
|
args[i + 1]
|
|
50
70
|
) {
|
|
51
71
|
flagArgs.push(arg, args[++i]);
|
|
@@ -89,10 +109,19 @@ function parseArgs(): ParsedArgs {
|
|
|
89
109
|
|
|
90
110
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
91
111
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
92
|
-
const
|
|
93
|
-
loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
|
|
112
|
+
const cloud = entry?.cloud;
|
|
94
113
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
95
114
|
|
|
115
|
+
// Platform-hosted assistants use a session token; local assistants use a guardian JWT.
|
|
116
|
+
const platformToken =
|
|
117
|
+
cloud === "vellum" ? (readPlatformToken() ?? undefined) : undefined;
|
|
118
|
+
const bearerToken =
|
|
119
|
+
cloud === "vellum"
|
|
120
|
+
? undefined
|
|
121
|
+
: (loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined);
|
|
122
|
+
|
|
123
|
+
let interfaceId: SupportedInterface = CLI_INTERFACE_ID;
|
|
124
|
+
|
|
96
125
|
for (let i = 0; i < flagArgs.length; i++) {
|
|
97
126
|
const flag = flagArgs[i];
|
|
98
127
|
if ((flag === "--url" || flag === "-u") && flagArgs[i + 1]) {
|
|
@@ -102,6 +131,21 @@ function parseArgs(): ParsedArgs {
|
|
|
102
131
|
flagArgs[i + 1]
|
|
103
132
|
) {
|
|
104
133
|
assistantId = flagArgs[++i];
|
|
134
|
+
} else if ((flag === "--interface" || flag === "-i") && flagArgs[i + 1]) {
|
|
135
|
+
const value = flagArgs[++i];
|
|
136
|
+
if (value === "web") {
|
|
137
|
+
console.error(
|
|
138
|
+
`--interface web is not yet supported. Coming soon.`,
|
|
139
|
+
);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (!(SUPPORTED_INTERFACES as readonly string[]).includes(value)) {
|
|
143
|
+
console.error(
|
|
144
|
+
`Unknown interface '${value}'. Supported: ${SUPPORTED_INTERFACES.join(", ")}.`,
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
interfaceId = value as SupportedInterface;
|
|
105
149
|
}
|
|
106
150
|
}
|
|
107
151
|
|
|
@@ -109,7 +153,10 @@ function parseArgs(): ParsedArgs {
|
|
|
109
153
|
runtimeUrl: maybeSwapToLocalhost(runtimeUrl.replace(/\/+$/, "")),
|
|
110
154
|
assistantId,
|
|
111
155
|
species,
|
|
156
|
+
cloud,
|
|
157
|
+
platformToken,
|
|
112
158
|
bearerToken,
|
|
159
|
+
interfaceId,
|
|
113
160
|
project: entry?.project,
|
|
114
161
|
zone: entry?.zone,
|
|
115
162
|
};
|
|
@@ -166,6 +213,7 @@ ${ANSI.bold}ARGUMENTS:${ANSI.reset}
|
|
|
166
213
|
${ANSI.bold}OPTIONS:${ANSI.reset}
|
|
167
214
|
-u, --url <url> Runtime URL
|
|
168
215
|
-a, --assistant-id <id> Assistant ID
|
|
216
|
+
-i, --interface <id> Interface identifier (default: cli)
|
|
169
217
|
-h, --help Show this help message
|
|
170
218
|
|
|
171
219
|
${ANSI.bold}DEFAULTS:${ANSI.reset}
|
|
@@ -181,11 +229,46 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
|
181
229
|
}
|
|
182
230
|
|
|
183
231
|
export async function client(): Promise<void> {
|
|
184
|
-
const {
|
|
185
|
-
|
|
232
|
+
const {
|
|
233
|
+
runtimeUrl,
|
|
234
|
+
assistantId,
|
|
235
|
+
species,
|
|
236
|
+
cloud,
|
|
237
|
+
platformToken,
|
|
238
|
+
bearerToken,
|
|
239
|
+
interfaceId,
|
|
240
|
+
project,
|
|
241
|
+
zone,
|
|
242
|
+
} = parseArgs();
|
|
186
243
|
|
|
187
244
|
tuiLog.init();
|
|
188
|
-
tuiLog.info("session start", {
|
|
245
|
+
tuiLog.info("session start", {
|
|
246
|
+
runtimeUrl,
|
|
247
|
+
assistantId,
|
|
248
|
+
species,
|
|
249
|
+
cloud,
|
|
250
|
+
interfaceId,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Build pre-constructed request headers merged from auth + client registration.
|
|
254
|
+
// Spreading into every fetch site ensures consistency across REST and SSE endpoints.
|
|
255
|
+
let auth: Record<string, string> | undefined;
|
|
256
|
+
if (cloud === "vellum" && platformToken) {
|
|
257
|
+
const orgId = await fetchOrganizationId(platformToken).catch((err) => {
|
|
258
|
+
tuiLog.warn("failed to fetch organization id", { err: String(err) });
|
|
259
|
+
return undefined;
|
|
260
|
+
});
|
|
261
|
+
auth = {
|
|
262
|
+
"X-Session-Token": platformToken,
|
|
263
|
+
...(orgId ? { "Vellum-Organization-Id": orgId } : {}),
|
|
264
|
+
...getClientRegistrationHeaders(interfaceId),
|
|
265
|
+
};
|
|
266
|
+
} else {
|
|
267
|
+
auth = {
|
|
268
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
269
|
+
...getClientRegistrationHeaders(interfaceId),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
189
272
|
|
|
190
273
|
const { renderChatApp } = await import("../components/DefaultMainScreen");
|
|
191
274
|
|
|
@@ -203,6 +286,6 @@ export async function client(): Promise<void> {
|
|
|
203
286
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
204
287
|
process.exit(0);
|
|
205
288
|
},
|
|
206
|
-
{
|
|
289
|
+
{ auth, project, zone },
|
|
207
290
|
);
|
|
208
291
|
}
|
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/login.ts
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
setActiveAssistant,
|
|
11
11
|
} from "../lib/assistant-config";
|
|
12
12
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
13
|
+
import {
|
|
14
|
+
fetchAssistantIngressUrl,
|
|
15
|
+
fetchCurrentVersion,
|
|
16
|
+
} from "../lib/upgrade-lifecycle.js";
|
|
13
17
|
import {
|
|
14
18
|
clearPlatformToken,
|
|
15
19
|
ensureSelfHostedLocalRegistration,
|
|
@@ -210,12 +214,19 @@ export async function login(): Promise<void> {
|
|
|
210
214
|
if (entry && entry.cloud !== "vellum") {
|
|
211
215
|
const orgId = await fetchOrganizationId(token);
|
|
212
216
|
const clientInstallationId = computeDeviceId();
|
|
217
|
+
const [assistantVersion, ingressUrl] = await Promise.all([
|
|
218
|
+
fetchCurrentVersion(entry.runtimeUrl),
|
|
219
|
+
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
220
|
+
]);
|
|
213
221
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
214
222
|
token,
|
|
215
223
|
orgId,
|
|
216
224
|
clientInstallationId,
|
|
217
225
|
entry.assistantId,
|
|
218
226
|
"cli",
|
|
227
|
+
assistantVersion,
|
|
228
|
+
getPlatformUrl(),
|
|
229
|
+
ingressUrl,
|
|
219
230
|
);
|
|
220
231
|
console.log(
|
|
221
232
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
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/rollback.ts
CHANGED
|
@@ -340,7 +340,7 @@ export async function rollback(): Promise<void> {
|
|
|
340
340
|
const signingKey =
|
|
341
341
|
capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
|
|
342
342
|
|
|
343
|
-
// Build extra env vars, excluding keys managed by
|
|
343
|
+
// Build extra env vars, excluding keys managed by buildServiceRunArgs
|
|
344
344
|
const envKeysSetByRunArgs = new Set(CONTAINER_ENV_EXCLUDE_KEYS);
|
|
345
345
|
for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
|
|
346
346
|
if (process.env[envVar]) {
|
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,
|
|
@@ -45,7 +47,10 @@ import { hatchLocal } from "../lib/hatch-local.js";
|
|
|
45
47
|
import { retireLocal } from "../lib/retire-local.js";
|
|
46
48
|
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
47
49
|
import { stopProcessByPidFile } from "../lib/process.js";
|
|
48
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
fetchAssistantIngressUrl,
|
|
52
|
+
fetchCurrentVersion,
|
|
53
|
+
} from "../lib/upgrade-lifecycle.js";
|
|
49
54
|
import { compareVersions } from "../lib/version-compat.js";
|
|
50
55
|
import { join } from "node:path";
|
|
51
56
|
|
|
@@ -254,13 +259,17 @@ async function getAccessToken(
|
|
|
254
259
|
|
|
255
260
|
/**
|
|
256
261
|
* Detect a 401 Unauthorized raised by `localRuntimeExportToGcs` /
|
|
257
|
-
* `localRuntimeImportFromGcs`.
|
|
258
|
-
* `"Local runtime <op> failed (401): ..."`
|
|
259
|
-
*
|
|
262
|
+
* `localRuntimeImportFromGcs` / `localRuntimeIdentity`. They throw Error
|
|
263
|
+
* with a message of the form `"Local runtime <op> failed (401): ..."` or
|
|
264
|
+
* `"Failed to fetch runtime identity: 401 ..."` when the gateway rejects
|
|
265
|
+
* the cached guardian token.
|
|
260
266
|
*/
|
|
261
267
|
function isRuntime401(err: unknown): boolean {
|
|
262
268
|
const msg = err instanceof Error ? err.message : String(err);
|
|
263
|
-
return
|
|
269
|
+
return (
|
|
270
|
+
/Local runtime [^(]*failed \(401\)/.test(msg) ||
|
|
271
|
+
/Failed to fetch runtime identity: 401\b/.test(msg)
|
|
272
|
+
);
|
|
264
273
|
}
|
|
265
274
|
|
|
266
275
|
/**
|
|
@@ -367,13 +376,40 @@ async function exportFromAssistant(
|
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
if (cloud === "local" || cloud === "docker") {
|
|
379
|
+
// Ask the source runtime which version it's running before requesting
|
|
380
|
+
// the signed upload URL. The bundle is produced by the daemon (not the
|
|
381
|
+
// CLI), so the daemon's version is what defines the bundle's
|
|
382
|
+
// `min_runtime_version`. Stamping with `cliPkg.version` instead would
|
|
383
|
+
// record an inaccurate compatibility band whenever the CLI/daemon have
|
|
384
|
+
// drifted (a normal case in real usage — `vellum upgrade` swaps the
|
|
385
|
+
// daemon, the CLI is updated separately).
|
|
386
|
+
let sourceRuntimeVersion: string;
|
|
387
|
+
try {
|
|
388
|
+
const identity = await callRuntimeWithAuthRetry(
|
|
389
|
+
entry.runtimeUrl,
|
|
390
|
+
entry.assistantId,
|
|
391
|
+
async (token) => localRuntimeIdentity(entry, token),
|
|
392
|
+
);
|
|
393
|
+
sourceRuntimeVersion = identity.version;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
396
|
+
console.error(
|
|
397
|
+
`Error: Could not fetch runtime identity from '${entry.assistantId}': ${msg}`,
|
|
398
|
+
);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
370
402
|
// Request a signed upload URL from the platform instance that will
|
|
371
403
|
// eventually own the bundle (i.e. the one the importer will read from).
|
|
372
404
|
// Passing the target's runtime URL here keeps upload and download on
|
|
373
405
|
// the same platform — otherwise a non-default/stale platform URL would
|
|
374
406
|
// cause the import to look at an empty object.
|
|
375
407
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
376
|
-
{
|
|
408
|
+
{
|
|
409
|
+
operation: "upload",
|
|
410
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
411
|
+
maxRuntimeVersion: null,
|
|
412
|
+
},
|
|
377
413
|
platformToken,
|
|
378
414
|
bundlePlatformUrl,
|
|
379
415
|
);
|
|
@@ -440,6 +476,24 @@ async function exportFromAssistant(
|
|
|
440
476
|
}
|
|
441
477
|
|
|
442
478
|
if (cloud === "vellum") {
|
|
479
|
+
// Ask the managed runtime which version it's running so the signed-URL
|
|
480
|
+
// request records the bundle's actual `min_runtime_version`. The
|
|
481
|
+
// platform-managed runtime is the exporter; the CLI version is
|
|
482
|
+
// unrelated. Routed via the wildcard proxy with platform-token auth
|
|
483
|
+
// (resolveRuntimeUrl + migrationRequestHeaders inside
|
|
484
|
+
// localRuntimeIdentity).
|
|
485
|
+
let sourceRuntimeVersion: string;
|
|
486
|
+
try {
|
|
487
|
+
const identity = await localRuntimeIdentity(entry, platformToken);
|
|
488
|
+
sourceRuntimeVersion = identity.version;
|
|
489
|
+
} catch (err) {
|
|
490
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
491
|
+
console.error(
|
|
492
|
+
`Error: Could not fetch runtime identity from '${entry.assistantId}': ${msg}`,
|
|
493
|
+
);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
443
497
|
// Platform source — request a signed upload URL on the same platform
|
|
444
498
|
// instance the bundle will eventually be imported from, then ask the
|
|
445
499
|
// managed runtime to export directly to GCS. The runtime endpoint is
|
|
@@ -449,7 +503,11 @@ async function exportFromAssistant(
|
|
|
449
503
|
// pick that shape for `cloud === "vellum"` and `migrationRequestHeaders`
|
|
450
504
|
// to send platform-token auth (no guardian-token bootstrap).
|
|
451
505
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
452
|
-
{
|
|
506
|
+
{
|
|
507
|
+
operation: "upload",
|
|
508
|
+
minRuntimeVersion: sourceRuntimeVersion,
|
|
509
|
+
maxRuntimeVersion: null,
|
|
510
|
+
},
|
|
453
511
|
platformToken,
|
|
454
512
|
bundlePlatformUrl,
|
|
455
513
|
);
|
|
@@ -659,11 +717,56 @@ async function importToAssistant(
|
|
|
659
717
|
// never touches the bytes. The URL must target the same platform the
|
|
660
718
|
// bundle was uploaded to; otherwise the object won't exist on this
|
|
661
719
|
// platform's GCS bucket.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
720
|
+
//
|
|
721
|
+
// The platform's vbundle version gate compares the **target runtime's**
|
|
722
|
+
// version against the bundle's compatibility range. The CLI and the
|
|
723
|
+
// target assistant's daemon can diverge (assistants upgrade
|
|
724
|
+
// independently), so we MUST query the target runtime's `/v1/identity`
|
|
725
|
+
// for its version rather than sending `cliPkg.version`. Sending the CLI
|
|
726
|
+
// version here would falsely 422 a valid import (or pass a bundle the
|
|
727
|
+
// target can't actually load) whenever the two drift apart.
|
|
728
|
+
let targetRuntimeVersion: string;
|
|
729
|
+
try {
|
|
730
|
+
const identity = await callRuntimeWithAuthRetry(
|
|
731
|
+
entry.runtimeUrl,
|
|
732
|
+
entry.assistantId,
|
|
733
|
+
(token) => localRuntimeIdentity(entry, token),
|
|
734
|
+
);
|
|
735
|
+
targetRuntimeVersion = identity.version;
|
|
736
|
+
} catch (err) {
|
|
737
|
+
// Surface and abort — silently falling back to `cliPkg.version` would
|
|
738
|
+
// re-introduce the bug this code is fixing. If the runtime is
|
|
739
|
+
// unreachable, the import would fail downstream anyway.
|
|
740
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
741
|
+
console.error(
|
|
742
|
+
`Error: Could not read target runtime version from '${entry.assistantId}': ${msg}`,
|
|
743
|
+
);
|
|
744
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let bundleUrl: string;
|
|
749
|
+
try {
|
|
750
|
+
const result = await platformRequestSignedUrl(
|
|
751
|
+
{
|
|
752
|
+
operation: "download",
|
|
753
|
+
bundleKey,
|
|
754
|
+
targetRuntimeVersion,
|
|
755
|
+
},
|
|
756
|
+
platformToken,
|
|
757
|
+
bundlePlatformUrl,
|
|
758
|
+
);
|
|
759
|
+
bundleUrl = result.url;
|
|
760
|
+
} catch (err) {
|
|
761
|
+
if (err instanceof VersionMismatchError) {
|
|
762
|
+
// 422 version_mismatch is terminal — the bundle's runtime range and
|
|
763
|
+
// the target runtime's version don't overlap. Surface the
|
|
764
|
+
// platform-formatted message and exit; do NOT retry.
|
|
765
|
+
console.error(`Error: ${err.message}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
throw err;
|
|
769
|
+
}
|
|
667
770
|
|
|
668
771
|
console.log("Importing data...");
|
|
669
772
|
|
|
@@ -966,12 +1069,19 @@ async function tryInjectPlatformCredentials(
|
|
|
966
1069
|
const user = await fetchCurrentUser(token);
|
|
967
1070
|
const orgId = await fetchOrganizationId(token);
|
|
968
1071
|
const clientInstallationId = computeDeviceId();
|
|
1072
|
+
const [assistantVersion, ingressUrl] = await Promise.all([
|
|
1073
|
+
fetchCurrentVersion(entry.runtimeUrl),
|
|
1074
|
+
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
1075
|
+
]);
|
|
969
1076
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
970
1077
|
token,
|
|
971
1078
|
orgId,
|
|
972
1079
|
clientInstallationId,
|
|
973
1080
|
entry.assistantId,
|
|
974
1081
|
"cli",
|
|
1082
|
+
assistantVersion,
|
|
1083
|
+
getPlatformUrl(),
|
|
1084
|
+
ingressUrl,
|
|
975
1085
|
);
|
|
976
1086
|
|
|
977
1087
|
// Resolve the API key: 1) fresh from registration, 2) existing from
|
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,
|
|
@@ -428,9 +429,9 @@ async function upgradeDocker(
|
|
|
428
429
|
|
|
429
430
|
// Build the set of extra env vars to replay on the new assistant container.
|
|
430
431
|
// Captured env vars serve as the base; keys already managed by
|
|
431
|
-
//
|
|
432
|
+
// buildServiceRunArgs are excluded to avoid duplicates.
|
|
432
433
|
const envKeysSetByRunArgs = new Set(CONTAINER_ENV_EXCLUDE_KEYS);
|
|
433
|
-
// Only exclude keys that
|
|
434
|
+
// Only exclude keys that buildServiceRunArgs will actually set
|
|
434
435
|
for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
|
|
435
436
|
if (process.env[envVar]) {
|
|
436
437
|
envKeysSetByRunArgs.add(envVar);
|
|
@@ -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.");
|