@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.
@@ -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}`);
@@ -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 anthropicApiKey = process.env.ANTHROPIC_API_KEY;
414
- if (!anthropicApiKey) {
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: ANTHROPIC_API_KEY environment variable is not set.",
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
- anthropicApiKey,
448
+ providerApiKeys,
441
449
  instanceName,
442
450
  "aws",
443
451
  );
@@ -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",