@vellumai/cli 0.5.4 → 0.5.6
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 +12 -0
- package/knip.json +2 -1
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +46 -0
- package/src/__tests__/health-check.test.ts +26 -1
- package/src/adapters/openclaw.ts +4 -2
- package/src/commands/backup.ts +151 -0
- package/src/commands/hatch.ts +14 -4
- package/src/commands/ps.ts +6 -1
- package/src/commands/restore.ts +330 -0
- package/src/commands/rollback.ts +280 -0
- package/src/commands/upgrade.ts +171 -2
- package/src/commands/wake.ts +8 -0
- package/src/index.ts +11 -0
- package/src/lib/assistant-config.ts +57 -0
- package/src/lib/aws.ts +13 -5
- package/src/lib/constants.ts +12 -0
- package/src/lib/docker.ts +299 -18
- package/src/lib/gcp.ts +18 -6
- package/src/lib/guardian-token.ts +46 -1
- package/src/lib/health-check.ts +4 -0
- package/src/lib/platform-client.ts +3 -2
- package/src/lib/version-compat.ts +45 -0
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
|
|
1
3
|
import cliPkg from "../../package.json";
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
findAssistantByName,
|
|
5
7
|
getActiveAssistant,
|
|
6
8
|
loadAllAssistants,
|
|
9
|
+
saveAssistantEntry,
|
|
7
10
|
} from "../lib/assistant-config";
|
|
8
11
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
9
12
|
import {
|
|
10
13
|
captureImageRefs,
|
|
14
|
+
clearSigningKeyBootstrapLock,
|
|
11
15
|
DOCKERHUB_IMAGES,
|
|
12
16
|
DOCKER_READY_TIMEOUT_MS,
|
|
13
17
|
GATEWAY_INTERNAL_PORT,
|
|
14
18
|
dockerResourceNames,
|
|
19
|
+
migrateCesSecurityFiles,
|
|
20
|
+
migrateGatewaySecurityFiles,
|
|
15
21
|
startContainers,
|
|
16
22
|
stopContainers,
|
|
17
23
|
} from "../lib/docker";
|
|
@@ -21,6 +27,7 @@ import {
|
|
|
21
27
|
getPlatformUrl,
|
|
22
28
|
readPlatformToken,
|
|
23
29
|
} from "../lib/platform-client";
|
|
30
|
+
import { loadBootstrapSecret, loadGuardianToken } from "../lib/guardian-token";
|
|
24
31
|
import { exec, execOutput } from "../lib/step-runner";
|
|
25
32
|
|
|
26
33
|
interface UpgradeArgs {
|
|
@@ -132,7 +139,7 @@ function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
|
|
|
132
139
|
* Capture environment variables from a running Docker container so they
|
|
133
140
|
* can be replayed onto the replacement container after upgrade.
|
|
134
141
|
*/
|
|
135
|
-
async function captureContainerEnv(
|
|
142
|
+
export async function captureContainerEnv(
|
|
136
143
|
containerName: string,
|
|
137
144
|
): Promise<Record<string, string>> {
|
|
138
145
|
const captured: Record<string, string> = {};
|
|
@@ -160,7 +167,7 @@ async function captureContainerEnv(
|
|
|
160
167
|
* Poll the gateway `/readyz` endpoint until it returns 200 or the timeout
|
|
161
168
|
* elapses. Returns whether the assistant became ready.
|
|
162
169
|
*/
|
|
163
|
-
async function waitForReady(runtimeUrl: string): Promise<boolean> {
|
|
170
|
+
export async function waitForReady(runtimeUrl: string): Promise<boolean> {
|
|
164
171
|
const readyUrl = `${runtimeUrl}/readyz`;
|
|
165
172
|
const start = Date.now();
|
|
166
173
|
|
|
@@ -194,6 +201,35 @@ async function waitForReady(runtimeUrl: string): Promise<boolean> {
|
|
|
194
201
|
return false;
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Best-effort broadcast of an upgrade lifecycle event to connected clients
|
|
206
|
+
* via the gateway's upgrade-broadcast proxy. Uses guardian token auth.
|
|
207
|
+
* Failures are logged but never block the upgrade flow.
|
|
208
|
+
*/
|
|
209
|
+
export async function broadcastUpgradeEvent(
|
|
210
|
+
gatewayUrl: string,
|
|
211
|
+
assistantId: string,
|
|
212
|
+
event: Record<string, unknown>,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
const token = loadGuardianToken(assistantId);
|
|
216
|
+
const headers: Record<string, string> = {
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
};
|
|
219
|
+
if (token?.accessToken) {
|
|
220
|
+
headers["Authorization"] = `Bearer ${token.accessToken}`;
|
|
221
|
+
}
|
|
222
|
+
await fetch(`${gatewayUrl}/v1/admin/upgrade-broadcast`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers,
|
|
225
|
+
body: JSON.stringify(event),
|
|
226
|
+
signal: AbortSignal.timeout(3000),
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
// Best-effort — gateway/daemon may already be shutting down or not yet ready
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
197
233
|
async function upgradeDocker(
|
|
198
234
|
entry: AssistantEntry,
|
|
199
235
|
version: string | null,
|
|
@@ -229,6 +265,18 @@ async function upgradeDocker(
|
|
|
229
265
|
);
|
|
230
266
|
}
|
|
231
267
|
|
|
268
|
+
// Persist rollback state to lockfile BEFORE any destructive changes.
|
|
269
|
+
// This enables the `vellum rollback` command to restore the previous version.
|
|
270
|
+
if (entry.serviceGroupVersion && entry.containerInfo) {
|
|
271
|
+
const rollbackEntry: AssistantEntry = {
|
|
272
|
+
...entry,
|
|
273
|
+
previousServiceGroupVersion: entry.serviceGroupVersion,
|
|
274
|
+
previousContainerInfo: { ...entry.containerInfo },
|
|
275
|
+
};
|
|
276
|
+
saveAssistantEntry(rollbackEntry);
|
|
277
|
+
console.log(` Saved rollback state: ${entry.serviceGroupVersion}\n`);
|
|
278
|
+
}
|
|
279
|
+
|
|
232
280
|
console.log("💾 Capturing existing container environment...");
|
|
233
281
|
const capturedEnv = await captureContainerEnv(res.assistantContainer);
|
|
234
282
|
console.log(
|
|
@@ -241,6 +289,16 @@ async function upgradeDocker(
|
|
|
241
289
|
await exec("docker", ["pull", imageTags["credential-executor"]]);
|
|
242
290
|
console.log("✅ Docker images pulled\n");
|
|
243
291
|
|
|
292
|
+
// Notify connected clients that an upgrade is about to begin.
|
|
293
|
+
console.log("📢 Notifying connected clients...");
|
|
294
|
+
await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
|
|
295
|
+
type: "starting",
|
|
296
|
+
targetVersion: versionTag,
|
|
297
|
+
expectedDowntimeSeconds: 60,
|
|
298
|
+
});
|
|
299
|
+
// Brief pause to allow SSE delivery before containers stop.
|
|
300
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
301
|
+
|
|
244
302
|
console.log("🛑 Stopping existing containers...");
|
|
245
303
|
await stopContainers(res);
|
|
246
304
|
console.log("✅ Containers stopped\n");
|
|
@@ -257,10 +315,23 @@ async function upgradeDocker(
|
|
|
257
315
|
// use default
|
|
258
316
|
}
|
|
259
317
|
|
|
318
|
+
// Extract CES_SERVICE_TOKEN from the captured env so it can be passed via
|
|
319
|
+
// the dedicated cesServiceToken parameter (which propagates it to all three
|
|
320
|
+
// containers). If the old instance predates CES_SERVICE_TOKEN, generate a
|
|
321
|
+
// fresh one so gateway and CES can authenticate.
|
|
322
|
+
const cesServiceToken =
|
|
323
|
+
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
324
|
+
|
|
325
|
+
// Retrieve or generate a bootstrap secret for the gateway. The secret was
|
|
326
|
+
// persisted to disk during hatch; older instances won't have one yet.
|
|
327
|
+
const bootstrapSecret =
|
|
328
|
+
loadBootstrapSecret(instanceName) || randomBytes(32).toString("hex");
|
|
329
|
+
|
|
260
330
|
// Build the set of extra env vars to replay on the new assistant container.
|
|
261
331
|
// Captured env vars serve as the base; keys already managed by
|
|
262
332
|
// serviceDockerRunArgs are excluded to avoid duplicates.
|
|
263
333
|
const envKeysSetByRunArgs = new Set([
|
|
334
|
+
"CES_SERVICE_TOKEN",
|
|
264
335
|
"VELLUM_ASSISTANT_NAME",
|
|
265
336
|
"RUNTIME_HTTP_HOST",
|
|
266
337
|
"PATH",
|
|
@@ -278,9 +349,20 @@ async function upgradeDocker(
|
|
|
278
349
|
}
|
|
279
350
|
}
|
|
280
351
|
|
|
352
|
+
console.log("🔄 Migrating security files to gateway volume...");
|
|
353
|
+
await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
|
|
354
|
+
|
|
355
|
+
console.log("🔄 Migrating credential files to CES security volume...");
|
|
356
|
+
await migrateCesSecurityFiles(res, (msg) => console.log(msg));
|
|
357
|
+
|
|
358
|
+
console.log("🔑 Clearing signing key bootstrap lock...");
|
|
359
|
+
await clearSigningKeyBootstrapLock(res);
|
|
360
|
+
|
|
281
361
|
console.log("🚀 Starting upgraded containers...");
|
|
282
362
|
await startContainers(
|
|
283
363
|
{
|
|
364
|
+
bootstrapSecret,
|
|
365
|
+
cesServiceToken,
|
|
284
366
|
extraAssistantEnv,
|
|
285
367
|
gatewayPort,
|
|
286
368
|
imageTags,
|
|
@@ -294,6 +376,32 @@ async function upgradeDocker(
|
|
|
294
376
|
console.log("Waiting for assistant to become ready...");
|
|
295
377
|
const ready = await waitForReady(entry.runtimeUrl);
|
|
296
378
|
if (ready) {
|
|
379
|
+
// Update lockfile with new service group topology
|
|
380
|
+
const newDigests = await captureImageRefs(res);
|
|
381
|
+
const updatedEntry: AssistantEntry = {
|
|
382
|
+
...entry,
|
|
383
|
+
serviceGroupVersion: versionTag,
|
|
384
|
+
containerInfo: {
|
|
385
|
+
assistantImage: imageTags.assistant,
|
|
386
|
+
gatewayImage: imageTags.gateway,
|
|
387
|
+
cesImage: imageTags["credential-executor"],
|
|
388
|
+
assistantDigest: newDigests?.assistant,
|
|
389
|
+
gatewayDigest: newDigests?.gateway,
|
|
390
|
+
cesDigest: newDigests?.["credential-executor"],
|
|
391
|
+
networkName: res.network,
|
|
392
|
+
},
|
|
393
|
+
previousServiceGroupVersion: entry.serviceGroupVersion,
|
|
394
|
+
previousContainerInfo: entry.containerInfo,
|
|
395
|
+
};
|
|
396
|
+
saveAssistantEntry(updatedEntry);
|
|
397
|
+
|
|
398
|
+
// Notify clients on the new service group that the upgrade succeeded.
|
|
399
|
+
await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
|
|
400
|
+
type: "complete",
|
|
401
|
+
installedVersion: versionTag,
|
|
402
|
+
success: true,
|
|
403
|
+
});
|
|
404
|
+
|
|
297
405
|
console.log(
|
|
298
406
|
`\n✅ Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
|
|
299
407
|
);
|
|
@@ -307,6 +415,8 @@ async function upgradeDocker(
|
|
|
307
415
|
|
|
308
416
|
await startContainers(
|
|
309
417
|
{
|
|
418
|
+
bootstrapSecret,
|
|
419
|
+
cesServiceToken,
|
|
310
420
|
extraAssistantEnv,
|
|
311
421
|
gatewayPort,
|
|
312
422
|
imageTags: previousImageRefs,
|
|
@@ -318,6 +428,43 @@ async function upgradeDocker(
|
|
|
318
428
|
|
|
319
429
|
const rollbackReady = await waitForReady(entry.runtimeUrl);
|
|
320
430
|
if (rollbackReady) {
|
|
431
|
+
// Restore previous container info in lockfile after rollback.
|
|
432
|
+
// previousImageRefs contains sha256 digests from `docker inspect
|
|
433
|
+
// --format {{.Image}}`. The *Image fields should hold
|
|
434
|
+
// human-readable image:tag names, so prefer the pre-upgrade
|
|
435
|
+
// containerInfo values and store digests in the *Digest fields.
|
|
436
|
+
if (previousImageRefs) {
|
|
437
|
+
const rolledBackEntry: AssistantEntry = {
|
|
438
|
+
...entry,
|
|
439
|
+
containerInfo: {
|
|
440
|
+
assistantImage:
|
|
441
|
+
entry.containerInfo?.assistantImage ??
|
|
442
|
+
previousImageRefs.assistant,
|
|
443
|
+
gatewayImage:
|
|
444
|
+
entry.containerInfo?.gatewayImage ??
|
|
445
|
+
previousImageRefs.gateway,
|
|
446
|
+
cesImage:
|
|
447
|
+
entry.containerInfo?.cesImage ??
|
|
448
|
+
previousImageRefs["credential-executor"],
|
|
449
|
+
assistantDigest: previousImageRefs.assistant,
|
|
450
|
+
gatewayDigest: previousImageRefs.gateway,
|
|
451
|
+
cesDigest: previousImageRefs["credential-executor"],
|
|
452
|
+
networkName: res.network,
|
|
453
|
+
},
|
|
454
|
+
previousServiceGroupVersion: undefined,
|
|
455
|
+
previousContainerInfo: undefined,
|
|
456
|
+
};
|
|
457
|
+
saveAssistantEntry(rolledBackEntry);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Notify clients that the upgrade failed and rolled back.
|
|
461
|
+
await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
|
|
462
|
+
type: "complete",
|
|
463
|
+
installedVersion: entry.serviceGroupVersion ?? "unknown",
|
|
464
|
+
success: false,
|
|
465
|
+
rolledBackToVersion: entry.serviceGroupVersion,
|
|
466
|
+
});
|
|
467
|
+
|
|
321
468
|
console.log(
|
|
322
469
|
`\n⚠️ Rolled back to previous version. Upgrade to ${versionTag} failed.`,
|
|
323
470
|
);
|
|
@@ -380,6 +527,15 @@ async function upgradePlatform(
|
|
|
380
527
|
body.version = version;
|
|
381
528
|
}
|
|
382
529
|
|
|
530
|
+
// Notify connected clients that an upgrade is about to begin.
|
|
531
|
+
const targetVersion = version ?? `v${cliPkg.version}`;
|
|
532
|
+
console.log("📢 Notifying connected clients...");
|
|
533
|
+
await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
|
|
534
|
+
type: "starting",
|
|
535
|
+
targetVersion,
|
|
536
|
+
expectedDowntimeSeconds: 90,
|
|
537
|
+
});
|
|
538
|
+
|
|
383
539
|
const response = await fetch(url, {
|
|
384
540
|
method: "POST",
|
|
385
541
|
headers: {
|
|
@@ -395,10 +551,23 @@ async function upgradePlatform(
|
|
|
395
551
|
console.error(
|
|
396
552
|
`Error: Platform upgrade failed (${response.status}): ${text}`,
|
|
397
553
|
);
|
|
554
|
+
await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
|
|
555
|
+
type: "complete",
|
|
556
|
+
installedVersion: entry.serviceGroupVersion ?? "unknown",
|
|
557
|
+
success: false,
|
|
558
|
+
});
|
|
398
559
|
process.exit(1);
|
|
399
560
|
}
|
|
400
561
|
|
|
401
562
|
const result = (await response.json()) as UpgradeApiResponse;
|
|
563
|
+
|
|
564
|
+
// NOTE: We intentionally do NOT broadcast a "complete" event here.
|
|
565
|
+
// The platform API returning 200 only means "upgrade request accepted" —
|
|
566
|
+
// the service group has not yet restarted with the new version. The
|
|
567
|
+
// completion signal will come from the client's health-check
|
|
568
|
+
// version-change detection (DaemonConnection.swift) once the new
|
|
569
|
+
// version actually appears after the platform restarts the service group.
|
|
570
|
+
|
|
402
571
|
console.log(`✅ ${result.detail}`);
|
|
403
572
|
if (result.version) {
|
|
404
573
|
console.log(` Version: ${result.version}`);
|
package/src/commands/wake.ts
CHANGED
|
@@ -40,6 +40,14 @@ export async function wake(): Promise<void> {
|
|
|
40
40
|
const entry = resolveTargetAssistant(nameArg);
|
|
41
41
|
|
|
42
42
|
if (entry.cloud === "docker") {
|
|
43
|
+
if (watch || foreground) {
|
|
44
|
+
const ignored = [watch && "--watch", foreground && "--foreground"]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" and ");
|
|
47
|
+
console.warn(
|
|
48
|
+
`Warning: ${ignored} ignored for Docker instances (not supported).`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
43
51
|
const res = dockerResourceNames(entry.assistantId);
|
|
44
52
|
await wakeContainers(res);
|
|
45
53
|
console.log("Docker containers started.");
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import cliPkg from "../package.json";
|
|
4
|
+
import { backup } from "./commands/backup";
|
|
4
5
|
import { clean } from "./commands/clean";
|
|
5
6
|
import { client } from "./commands/client";
|
|
6
7
|
import { hatch } from "./commands/hatch";
|
|
@@ -8,7 +9,9 @@ import { login, logout, whoami } from "./commands/login";
|
|
|
8
9
|
import { pair } from "./commands/pair";
|
|
9
10
|
import { ps } from "./commands/ps";
|
|
10
11
|
import { recover } from "./commands/recover";
|
|
12
|
+
import { restore } from "./commands/restore";
|
|
11
13
|
import { retire } from "./commands/retire";
|
|
14
|
+
import { rollback } from "./commands/rollback";
|
|
12
15
|
import { setup } from "./commands/setup";
|
|
13
16
|
import { sleep } from "./commands/sleep";
|
|
14
17
|
import { ssh } from "./commands/ssh";
|
|
@@ -26,6 +29,7 @@ import { loadGuardianToken } from "./lib/guardian-token";
|
|
|
26
29
|
import { checkHealth } from "./lib/health-check";
|
|
27
30
|
|
|
28
31
|
const commands = {
|
|
32
|
+
backup,
|
|
29
33
|
clean,
|
|
30
34
|
client,
|
|
31
35
|
hatch,
|
|
@@ -34,7 +38,9 @@ const commands = {
|
|
|
34
38
|
pair,
|
|
35
39
|
ps,
|
|
36
40
|
recover,
|
|
41
|
+
restore,
|
|
37
42
|
retire,
|
|
43
|
+
rollback,
|
|
38
44
|
setup,
|
|
39
45
|
sleep,
|
|
40
46
|
ssh,
|
|
@@ -51,6 +57,7 @@ function printHelp(): void {
|
|
|
51
57
|
console.log("Usage: vellum <command> [options]");
|
|
52
58
|
console.log("");
|
|
53
59
|
console.log("Commands:");
|
|
60
|
+
console.log(" backup Export a backup of a running assistant");
|
|
54
61
|
console.log(" clean Kill orphaned vellum processes");
|
|
55
62
|
console.log(" client Connect to a hatched assistant");
|
|
56
63
|
console.log(" hatch Create a new assistant instance");
|
|
@@ -61,7 +68,11 @@ function printHelp(): void {
|
|
|
61
68
|
" ps List assistants (or processes for a specific assistant)",
|
|
62
69
|
);
|
|
63
70
|
console.log(" recover Restore a previously retired local assistant");
|
|
71
|
+
console.log(" restore Restore a .vbundle backup into a running assistant");
|
|
64
72
|
console.log(" retire Delete an assistant instance");
|
|
73
|
+
console.log(
|
|
74
|
+
" rollback Roll back a Docker assistant to the previous version",
|
|
75
|
+
);
|
|
65
76
|
console.log(" setup Configure API keys interactively");
|
|
66
77
|
console.log(" sleep Stop the assistant process");
|
|
67
78
|
console.log(" ssh SSH into a remote assistant instance");
|
|
@@ -4,6 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
7
|
+
DEFAULT_CES_PORT,
|
|
7
8
|
DEFAULT_DAEMON_PORT,
|
|
8
9
|
DEFAULT_GATEWAY_PORT,
|
|
9
10
|
DEFAULT_QDRANT_PORT,
|
|
@@ -29,11 +30,28 @@ export interface LocalInstanceResources {
|
|
|
29
30
|
gatewayPort: number;
|
|
30
31
|
/** HTTP port for the Qdrant vector store */
|
|
31
32
|
qdrantPort: number;
|
|
33
|
+
/** HTTP port for the CES (Claude Extension Server) */
|
|
34
|
+
cesPort: number;
|
|
32
35
|
/** Absolute path to the daemon PID file */
|
|
33
36
|
pidFile: string;
|
|
34
37
|
[key: string]: unknown;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
/** Docker image metadata for the service group. Enables rollback to known-good digests. */
|
|
41
|
+
export interface ContainerInfo {
|
|
42
|
+
assistantImage: string;
|
|
43
|
+
gatewayImage: string;
|
|
44
|
+
cesImage: string;
|
|
45
|
+
/** sha256 digest of the assistant image at time of hatch/upgrade */
|
|
46
|
+
assistantDigest?: string;
|
|
47
|
+
/** sha256 digest of the gateway image at time of hatch/upgrade */
|
|
48
|
+
gatewayDigest?: string;
|
|
49
|
+
/** sha256 digest of the CES image at time of hatch/upgrade */
|
|
50
|
+
cesDigest?: string;
|
|
51
|
+
/** Docker network name for the service group */
|
|
52
|
+
networkName?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
export interface AssistantEntry {
|
|
38
56
|
assistantId: string;
|
|
39
57
|
runtimeUrl: string;
|
|
@@ -56,6 +74,14 @@ export interface AssistantEntry {
|
|
|
56
74
|
resources?: LocalInstanceResources;
|
|
57
75
|
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
58
76
|
watcherPid?: number;
|
|
77
|
+
/** Last-known version of the service group, populated at hatch and updated by health checks. */
|
|
78
|
+
serviceGroupVersion?: string;
|
|
79
|
+
/** Docker image metadata for rollback. Only present for docker topology entries. */
|
|
80
|
+
containerInfo?: ContainerInfo;
|
|
81
|
+
/** The service group version that was running before the last upgrade. */
|
|
82
|
+
previousServiceGroupVersion?: string;
|
|
83
|
+
/** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
|
|
84
|
+
previousContainerInfo?: ContainerInfo;
|
|
59
85
|
[key: string]: unknown;
|
|
60
86
|
}
|
|
61
87
|
|
|
@@ -166,6 +192,7 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
166
192
|
daemonPort: DEFAULT_DAEMON_PORT,
|
|
167
193
|
gatewayPort,
|
|
168
194
|
qdrantPort: DEFAULT_QDRANT_PORT,
|
|
195
|
+
cesPort: DEFAULT_CES_PORT,
|
|
169
196
|
pidFile: join(instanceDir, ".vellum", "vellum.pid"),
|
|
170
197
|
};
|
|
171
198
|
mutated = true;
|
|
@@ -198,6 +225,10 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
198
225
|
res.qdrantPort = DEFAULT_QDRANT_PORT;
|
|
199
226
|
mutated = true;
|
|
200
227
|
}
|
|
228
|
+
if (typeof res.cesPort !== "number") {
|
|
229
|
+
res.cesPort = DEFAULT_CES_PORT;
|
|
230
|
+
mutated = true;
|
|
231
|
+
}
|
|
201
232
|
if (typeof res.pidFile !== "string") {
|
|
202
233
|
res.pidFile = join(res.instanceDir as string, ".vellum", "vellum.pid");
|
|
203
234
|
mutated = true;
|
|
@@ -333,6 +364,23 @@ export function saveAssistantEntry(entry: AssistantEntry): void {
|
|
|
333
364
|
writeAssistants(entries);
|
|
334
365
|
}
|
|
335
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Update just the serviceGroupVersion field on a lockfile entry.
|
|
369
|
+
* Reads the current entry, updates the version if changed, and writes back.
|
|
370
|
+
* No-op if the entry doesn't exist or the version hasn't changed.
|
|
371
|
+
*/
|
|
372
|
+
export function updateServiceGroupVersion(
|
|
373
|
+
assistantId: string,
|
|
374
|
+
version: string,
|
|
375
|
+
): void {
|
|
376
|
+
const entries = readAssistants();
|
|
377
|
+
const entry = entries.find((e) => e.assistantId === assistantId);
|
|
378
|
+
if (!entry) return;
|
|
379
|
+
if (entry.serviceGroupVersion === version) return;
|
|
380
|
+
entry.serviceGroupVersion = version;
|
|
381
|
+
writeAssistants(entries);
|
|
382
|
+
}
|
|
383
|
+
|
|
336
384
|
/**
|
|
337
385
|
* Scan upward from `basePort` to find an available port. A port is considered
|
|
338
386
|
* available when `probePort()` returns false (nothing listening). Scans up to
|
|
@@ -373,6 +421,7 @@ export async function allocateLocalResources(
|
|
|
373
421
|
daemonPort: DEFAULT_DAEMON_PORT,
|
|
374
422
|
gatewayPort: DEFAULT_GATEWAY_PORT,
|
|
375
423
|
qdrantPort: DEFAULT_QDRANT_PORT,
|
|
424
|
+
cesPort: DEFAULT_CES_PORT,
|
|
376
425
|
pidFile: join(vellumDir, "vellum.pid"),
|
|
377
426
|
};
|
|
378
427
|
}
|
|
@@ -398,6 +447,7 @@ export async function allocateLocalResources(
|
|
|
398
447
|
entry.resources.daemonPort,
|
|
399
448
|
entry.resources.gatewayPort,
|
|
400
449
|
entry.resources.qdrantPort,
|
|
450
|
+
entry.resources.cesPort,
|
|
401
451
|
);
|
|
402
452
|
}
|
|
403
453
|
}
|
|
@@ -417,12 +467,19 @@ export async function allocateLocalResources(
|
|
|
417
467
|
daemonPort,
|
|
418
468
|
gatewayPort,
|
|
419
469
|
]);
|
|
470
|
+
const cesPort = await findAvailablePort(DEFAULT_CES_PORT, [
|
|
471
|
+
...reservedPorts,
|
|
472
|
+
daemonPort,
|
|
473
|
+
gatewayPort,
|
|
474
|
+
qdrantPort,
|
|
475
|
+
]);
|
|
420
476
|
|
|
421
477
|
return {
|
|
422
478
|
instanceDir,
|
|
423
479
|
daemonPort,
|
|
424
480
|
gatewayPort,
|
|
425
481
|
qdrantPort,
|
|
482
|
+
cesPort,
|
|
426
483
|
pidFile: join(instanceDir, ".vellum", "vellum.pid"),
|
|
427
484
|
};
|
|
428
485
|
}
|
package/src/lib/aws.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { buildStartupScript, watchHatching } from "../commands/hatch";
|
|
|
6
6
|
import type { PollResult } from "../commands/hatch";
|
|
7
7
|
import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
8
8
|
import type { AssistantEntry } from "./assistant-config";
|
|
9
|
-
import { GATEWAY_PORT } from "./constants";
|
|
9
|
+
import { GATEWAY_PORT, PROVIDER_ENV_VAR_NAMES } from "./constants";
|
|
10
10
|
import type { Species } from "./constants";
|
|
11
11
|
import { leaseGuardianToken } from "./guardian-token";
|
|
12
12
|
import { generateInstanceName } from "./random-name";
|
|
@@ -410,10 +410,18 @@ export async function hatchAws(
|
|
|
410
410
|
|
|
411
411
|
const sshUser = userInfo().username;
|
|
412
412
|
const hatchedBy = process.env.VELLUM_HATCHED_BY;
|
|
413
|
-
const
|
|
414
|
-
|
|
413
|
+
const providerApiKeys: Record<string, string> = {};
|
|
414
|
+
for (const [, envVar] of Object.entries(PROVIDER_ENV_VAR_NAMES)) {
|
|
415
|
+
const value = process.env[envVar];
|
|
416
|
+
if (value) {
|
|
417
|
+
providerApiKeys[envVar] = value;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (Object.keys(providerApiKeys).length === 0) {
|
|
415
421
|
console.error(
|
|
416
|
-
"Error:
|
|
422
|
+
"Error: No provider API key environment variable is set. " +
|
|
423
|
+
"Set at least one of: " +
|
|
424
|
+
Object.values(PROVIDER_ENV_VAR_NAMES).join(", "),
|
|
417
425
|
);
|
|
418
426
|
process.exit(1);
|
|
419
427
|
}
|
|
@@ -437,7 +445,7 @@ export async function hatchAws(
|
|
|
437
445
|
const startupScript = await buildStartupScript(
|
|
438
446
|
species,
|
|
439
447
|
sshUser,
|
|
440
|
-
|
|
448
|
+
providerApiKeys,
|
|
441
449
|
instanceName,
|
|
442
450
|
"aws",
|
|
443
451
|
);
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Canonical internal assistant ID used as the default/fallback across the CLI
|
|
3
5
|
* and daemon. Mirrors `DAEMON_INTERNAL_ASSISTANT_ID` from
|
|
@@ -14,6 +16,16 @@ export const GATEWAY_PORT = process.env.GATEWAY_PORT
|
|
|
14
16
|
export const DEFAULT_DAEMON_PORT = 7821;
|
|
15
17
|
export const DEFAULT_GATEWAY_PORT = 7830;
|
|
16
18
|
export const DEFAULT_QDRANT_PORT = 6333;
|
|
19
|
+
export const DEFAULT_CES_PORT = 8090;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Environment variable names for provider API keys, keyed by provider ID.
|
|
23
|
+
* Loaded from the shared registry at `meta/provider-env-vars.json` — the
|
|
24
|
+
* single source of truth also consumed by the assistant runtime and the
|
|
25
|
+
* macOS client.
|
|
26
|
+
*/
|
|
27
|
+
export const PROVIDER_ENV_VAR_NAMES: Record<string, string> =
|
|
28
|
+
providerEnvVarsRegistry.providers;
|
|
17
29
|
|
|
18
30
|
export const VALID_REMOTE_HOSTS = [
|
|
19
31
|
"local",
|