@vellumai/cli 0.10.7-dev.202607102035.64f07ea → 0.10.7-staging.1

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,12 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
2
 
6
- import {
7
- readAllowedGatewayPorts,
8
- resolveGatewayProxyTarget,
9
- } from "../gateway-proxy";
3
+ import { resolveGatewayProxyTarget } from "../gateway-proxy";
10
4
 
11
5
  const allow =
12
6
  (...ports: number[]) =>
@@ -70,41 +64,6 @@ describe("resolveGatewayProxyTarget", () => {
70
64
  });
71
65
  });
72
66
 
73
- test("allowlists ports from resources, loopback URLs, and docker runtimeUrls — never remote URLs", () => {
74
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gateway-proxy-test-"));
75
- const lockfilePath = path.join(dir, "assistants.json");
76
- try {
77
- fs.writeFileSync(
78
- lockfilePath,
79
- JSON.stringify({
80
- assistants: [
81
- { assistantId: "local-a", resources: { gatewayPort: 7830 } },
82
- { assistantId: "local-b", localUrl: "http://127.0.0.1:7831" },
83
- // Docker entries record their published gateway only as a
84
- // loopback runtimeUrl.
85
- {
86
- assistantId: "docker-a",
87
- cloud: "docker",
88
- runtimeUrl: "http://localhost:7930",
89
- },
90
- // Remote runtimeUrls (managed / gcp / paired) must never widen
91
- // the allowlist.
92
- {
93
- assistantId: "remote-a",
94
- cloud: "gcp",
95
- runtimeUrl: "https://assistant.example.com:8443",
96
- },
97
- ],
98
- }),
99
- );
100
- expect(readAllowedGatewayPorts([lockfilePath])).toEqual(
101
- new Set([7830, 7831, 7930]),
102
- );
103
- } finally {
104
- fs.rmSync(dir, { recursive: true, force: true });
105
- }
106
- });
107
-
108
67
  test("never reads the allowlist for non-gateway or invalid-port paths", () => {
109
68
  let reads = 0;
110
69
  const counting = () => {
@@ -87,7 +87,6 @@ export function readAllowedGatewayPorts(lockfilePaths: string[]): Set<number> {
87
87
  assistants?: Array<{
88
88
  gatewayUrl?: unknown;
89
89
  localUrl?: unknown;
90
- runtimeUrl?: unknown;
91
90
  resources?: { gatewayPort?: unknown };
92
91
  }>;
93
92
  };
@@ -96,10 +95,6 @@ export function readAllowedGatewayPorts(lockfilePaths: string[]): Set<number> {
96
95
  if (!assistant) continue;
97
96
  addPortFromUrl(assistant.gatewayUrl, ports);
98
97
  addPortFromUrl(assistant.localUrl, ports);
99
- // Docker entries record their published gateway as a loopback
100
- // `runtimeUrl` with no `resources` block; the loopback-hostname filter
101
- // in addPortFromUrl keeps remote runtimeUrls out of the allowlist.
102
- addPortFromUrl(assistant.runtimeUrl, ports);
103
98
  const gp = assistant.resources?.gatewayPort;
104
99
  if (typeof gp === "number" && Number.isInteger(gp) && gp >= 1024 && gp <= 65535) {
105
100
  ports.add(gp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.7-dev.202607102035.64f07ea",
3
+ "version": "0.10.7-staging.1",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,14 +30,12 @@ const realLocal = {
30
30
  generateLocalSigningKey: localModule.generateLocalSigningKey,
31
31
  startLocalDaemon: localModule.startLocalDaemon,
32
32
  startGateway: localModule.startGateway,
33
- startCes: localModule.startCes,
34
33
  };
35
34
  const realExec = stepRunnerModule.exec;
36
35
 
37
- // Prevent real daemon / gateway / CES from starting
36
+ // Prevent real daemon / gateway from starting
38
37
  const startLocalDaemonMock = mock(async () => {});
39
38
  const startGatewayMock = mock(async () => {});
40
- const startCesMock = mock(async () => {});
41
39
 
42
40
  // Capture exec calls without running real tar
43
41
  const execMock = mock(async (_cmd: string, _args: string[]) => {});
@@ -47,7 +45,6 @@ beforeAll(() => {
47
45
  generateLocalSigningKey: () => "deadbeefdeadbeefdeadbeefdeadbeef",
48
46
  startLocalDaemon: startLocalDaemonMock,
49
47
  startGateway: startGatewayMock,
50
- startCes: startCesMock,
51
48
  }));
52
49
  mock.module("../lib/step-runner.js", () => ({ exec: execMock }));
53
50
  });
@@ -131,7 +128,6 @@ beforeEach(() => {
131
128
  execMock.mockClear();
132
129
  startLocalDaemonMock.mockClear();
133
130
  startGatewayMock.mockClear();
134
- startCesMock.mockClear();
135
131
  });
136
132
 
137
133
  afterEach(() => {
@@ -172,7 +172,8 @@ describe("sleep command", () => {
172
172
  undefined,
173
173
  7000,
174
174
  );
175
- // The CES sibling is stopped by its PID file; a no-op when absent.
175
+ // The CES sibling (CES_STANDALONE opt-in) is stopped by its PID file; a
176
+ // no-op when absent on the default topology.
176
177
  expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
177
178
  3,
178
179
  join(assistantRootDir, "ces.pid"),
@@ -119,7 +119,6 @@ const startGatewayMock = mock<typeof local.startGateway>(
119
119
  const stopLocalProcessesMock = mock<typeof local.stopLocalProcesses>(
120
120
  async () => {},
121
121
  );
122
- const startCesMock = mock<typeof local.startCes>(async () => {});
123
122
 
124
123
  mock.module("../lib/local.js", () => ({
125
124
  ...realLocal,
@@ -128,7 +127,6 @@ mock.module("../lib/local.js", () => ({
128
127
  startLocalDaemon: startLocalDaemonMock,
129
128
  startGateway: startGatewayMock,
130
129
  stopLocalProcesses: stopLocalProcessesMock,
131
- startCes: startCesMock,
132
130
  }));
133
131
 
134
132
  const loopbackSafeFetchMock = mock<typeof loopbackFetch.loopbackSafeFetch>(
@@ -258,8 +256,6 @@ beforeEach(() => {
258
256
  startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
259
257
  stopLocalProcessesMock.mockReset();
260
258
  stopLocalProcessesMock.mockResolvedValue(undefined);
261
- startCesMock.mockReset();
262
- startCesMock.mockResolvedValue(undefined);
263
259
  loopbackSafeFetchMock.mockReset();
264
260
  loopbackSafeFetchMock.mockResolvedValue({
265
261
  ok: true,
@@ -118,9 +118,9 @@ export async function recover(): Promise<void> {
118
118
  entry.guardianBootstrapSecret = bootstrapSecret;
119
119
  saveAssistantEntry(entry);
120
120
 
121
- // 8. Start CES sibling + daemon + gateway in parallel, the way the
122
- // Docker topology brings its sibling processes up together. startCes
123
- // always launches the CES sibling.
121
+ // 8. Start CES sibling (opt-in) + daemon + gateway in parallel, the way the
122
+ // Docker topology brings its sibling processes up together. startCes is a
123
+ // no-op unless CES_STANDALONE is set.
124
124
  await Promise.all([
125
125
  startCes(false, entry.resources),
126
126
  startLocalDaemon(false, entry.resources, { signingKey }),
@@ -168,8 +168,9 @@ export async function sleep(): Promise<void> {
168
168
  console.log("Gateway stopped.");
169
169
  }
170
170
 
171
- // Stop the CES sibling it is stopped by its PID file, a no-op when the
172
- // PID file is absent (e.g. the sibling was never started or already exited).
171
+ // Stop the CES sibling if one was launched (CES_STANDALONE). No-op when the
172
+ // PID file is absent on the default topology the assistant owns CES as an
173
+ // stdio child and it exits with the daemon.
173
174
  const cesPidFile = join(vellumDir, "ces.pid");
174
175
  const cesStopped = await stopProcessByPidFile(
175
176
  cesPidFile,
@@ -408,18 +408,11 @@ async function exportFromAssistant(
408
408
  // Passing the target's runtime URL here keeps upload and download on
409
409
  // the same platform — otherwise a non-default/stale platform URL would
410
410
  // cause the import to look at an empty object.
411
- //
412
- // The PUT is performed by the source daemon, not this CLI. A bare-metal
413
- // local daemon shares the host's network view, so the client-signed URL
414
- // is reachable; a docker daemon runs inside a container and reaches the
415
- // host the same way managed pods do, so its URL must be signed for the
416
- // runtime-reachable storage endpoint.
417
411
  const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
418
412
  {
419
413
  operation: "upload",
420
414
  minRuntimeVersion: sourceRuntimeVersion,
421
415
  maxRuntimeVersion: null,
422
- ...(cloud === "docker" ? { consumer: "runtime" as const } : {}),
423
416
  },
424
417
  platformToken,
425
418
  bundlePlatformUrl,
@@ -517,9 +510,6 @@ async function exportFromAssistant(
517
510
  operation: "upload",
518
511
  minRuntimeVersion: sourceRuntimeVersion,
519
512
  maxRuntimeVersion: null,
520
- // The managed pod PUTs the bundle, not this CLI — the URL must be
521
- // signed against the runtime-reachable storage endpoint.
522
- consumer: "runtime",
523
513
  },
524
514
  platformToken,
525
515
  bundlePlatformUrl,
@@ -1018,8 +1018,8 @@ async function upgradeLocal(
1018
1018
  process.env.APP_VERSION = stripVersionPrefix(targetVersion);
1019
1019
  try {
1020
1020
  // Bring CES, daemon, and gateway up in parallel, the way the Docker
1021
- // topology starts its sibling processes together. startCes always
1022
- // launches the CES sibling.
1021
+ // topology starts its sibling processes together. startCes is a no-op
1022
+ // unless CES_STANDALONE is set.
1023
1023
  await Promise.all([
1024
1024
  startCes(false, entry.resources),
1025
1025
  startLocalDaemon(false, entry.resources, { signingKey }),
@@ -210,8 +210,9 @@ export async function wake(): Promise<void> {
210
210
  // CES socket during startup (discoverCesWithRetry), so it tolerates CES
211
211
  // still binding. CES's lifecycle tracks the daemon (its only consumer):
212
212
  // restarting it under a live daemon would sever the daemon's open
213
- // connection, so it is only (re)started alongside the daemon. startCes
214
- // always launches the CES sibling unconditionally.
213
+ // connection, so it is only (re)started alongside the daemon. startCes is a
214
+ // no-op unless CES_STANDALONE is set, in which case the assistant spawns
215
+ // CES itself as today.
215
216
  await Promise.all([
216
217
  startCes(watch, resources),
217
218
  startLocalDaemon(watch, resources, { foreground, signingKey }),
@@ -228,7 +229,7 @@ export async function wake(): Promise<void> {
228
229
  // died independently (crash, OOM kill). A dead ces.pid under a live daemon
229
230
  // means credential operations will fail until the next wake. Relaunch the
230
231
  // sibling so the daemon's lazy reconnect (secure-keys.ts) picks it up on
231
- // the next credential read. startCes always launches the sibling.
232
+ // the next credential read. startCes is a no-op unless CES_STANDALONE.
232
233
  const vellumDir = join(resources.instanceDir, ".vellum");
233
234
  const cesPidFile = join(vellumDir, "ces.pid");
234
235
  const cesAlive = isProcessAlive(cesPidFile).alive;
package/src/lib/docker.ts CHANGED
@@ -1442,21 +1442,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1442
1442
  }
1443
1443
  const hostDeviceId = getOrCreateHostDeviceId();
1444
1444
  extraAssistantEnv.VELLUM_DEVICE_ID = hostDeviceId;
1445
- // Forward the migration URL allowlists so a daemon inside the container
1446
- // can PUT/GET teleport bundles against a local (non-GCS) platform.
1447
- // Pass-through only: unset in normal use, preserving the strict
1448
- // GCS-only validator default. A containerized daemon reaches the host
1449
- // via host.docker.internal, so that is the value to export when
1450
- // teleporting docker assistants against a local platform.
1451
- for (const key of [
1452
- "VELLUM_MIGRATION_EXPORT_ALLOWED_HOSTS",
1453
- "VELLUM_MIGRATION_IMPORT_ALLOWED_HOSTS",
1454
- ] as const) {
1455
- const value = process.env[key];
1456
- if (value) {
1457
- extraAssistantEnv[key] = value;
1458
- }
1459
- }
1460
1445
  const extraGatewayEnv = {
1461
1446
  ...flagEnvVars,
1462
1447
  VELLUM_DEVICE_ID: hostDeviceId,
@@ -1,4 +1,4 @@
1
- /** Only allow simple kebab-case keys (e.g. "voice-mode", "browser"). */
1
+ /** Only allow simple kebab-case keys (e.g. "voice-mode", "ces-tools"). */
2
2
  const ALLOWED_KEY_RE = /^[a-z0-9][a-z0-9-]*$/;
3
3
 
4
4
  /**
@@ -227,9 +227,9 @@ export async function hatchLocal(
227
227
  const signingKey = generateLocalSigningKey();
228
228
  const bootstrapSecret = generateLocalSigningKey();
229
229
  // Launch the CES sibling alongside the daemon, in parallel — matching the
230
- // Docker topology. The assistant does not spawn its own CES, so a freshly
231
- // hatched instance would otherwise come up with CES unavailable.
232
- // startCes always launches the CES sibling.
230
+ // Docker topology. Under the opt-in the assistant does not spawn its own CES,
231
+ // so a freshly hatched instance would otherwise come up with CES unavailable.
232
+ // startCes is a no-op unless CES_STANDALONE is set.
233
233
  await Promise.all([
234
234
  startCes(watch, resources),
235
235
  startLocalDaemon(watch, resources, {
package/src/lib/local.ts CHANGED
@@ -94,26 +94,9 @@ function hasLocalRuntimeComponents(installDir: string): boolean {
94
94
  );
95
95
  }
96
96
 
97
- /**
98
- * True when this process is a compiled standalone binary (desktop app or
99
- * `bun build --compile` CLI) rather than a script executed by a plain `bun`
100
- * binary (source tree, bunx, npm/global install).
101
- *
102
- * Only a compiled binary may trust product siblings in
103
- * `dirname(process.execPath)`: under plain bun that directory is bun's own
104
- * bin dir (e.g. `~/.bun/bin`), where bin links of globally-installed packages
105
- * (`assistant`, `credential-executor`) collide with app-bundle binary names
106
- * and point at whatever version happens to be installed globally.
107
- */
108
- function isCompiledCli(): boolean {
109
- const execBase = basename(process.execPath);
110
- return (
111
- execBase !== "bun" && execBase !== "bunx" && !execBase.startsWith("bun-")
112
- );
113
- }
114
-
115
97
  function resolveBunExecutable(): string {
116
- if (!isCompiledCli()) {
98
+ const execBase = basename(process.execPath);
99
+ if (execBase === "bun" || execBase.startsWith("bun-")) {
117
100
  return process.execPath;
118
101
  }
119
102
 
@@ -590,11 +573,14 @@ function applyDaemonEnvOverrides(
590
573
  env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
591
574
  options.defaultWorkspaceConfigPath;
592
575
  }
593
- // Pin the daemon to the exact socket the sibling binds so the two agree
594
- // regardless of any stale CES_LOCAL_SOCKET inherited from the parent
595
- // environment. The assistant connects to the sibling instead of spawning
596
- // its own CES.
597
- env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
576
+ // When the CLI launches CES as a sibling (CES_STANDALONE), pin the daemon to
577
+ // the exact socket the sibling binds so the two agree regardless of any stale
578
+ // CES_LOCAL_SOCKET inherited from the parent environment. The assistant then
579
+ // connects to the sibling instead of spawning its own CES.
580
+ if (isCesSiblingOptIn()) {
581
+ env.CES_STANDALONE = "1";
582
+ env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
583
+ }
598
584
  applyIpcSocketDirOverride(env);
599
585
  }
600
586
 
@@ -798,7 +784,7 @@ function resolveGatewayDir(resources?: LocalInstanceResources): string {
798
784
 
799
785
  // Compiled binary: gateway/ bundled adjacent to the CLI executable.
800
786
  const binGateway = join(dirname(process.execPath), "gateway");
801
- if (isCompiledCli() && isGatewaySourceDir(binGateway)) {
787
+ if (isGatewaySourceDir(binGateway)) {
802
788
  return binGateway;
803
789
  }
804
790
 
@@ -864,18 +850,31 @@ function resolveCesSocketPath(resources?: LocalInstanceResources): string {
864
850
  }
865
851
 
866
852
  /**
867
- * Launch the local CES sibling over a Unix socket. The sibling model is now
868
- * the default topology for local (non-containerized) instances, matching how
869
- * containerized homes already run CES.
853
+ * Whether the CLI should launch CES as an independent sibling process instead
854
+ * of leaving the assistant to spawn it as an stdio child. Temporary opt-in
855
+ * (`CES_STANDALONE=1`) while local CES converges onto the sibling model that
856
+ * containerized homes already use.
857
+ */
858
+ function isCesSiblingOptIn(): boolean {
859
+ return process.env.CES_STANDALONE === "1";
860
+ }
861
+
862
+ /**
863
+ * Launch the local CES sibling over a Unix socket (opted into via
864
+ * `CES_STANDALONE=1`). No-op unless the opt-in is set, in which case the
865
+ * assistant continues to spawn CES itself as today.
870
866
  *
871
- * The sibling runs as an independent process with its lifecycle anchored to
872
- * SIGTERM, mirroring the gateway: a CLI-owned process with a PID file under
873
- * `.vellum/ces.pid`, started by `wake` and stopped by `sleep`.
867
+ * The sibling runs with `CES_STANDALONE=1` so its lifecycle is anchored to
868
+ * SIGTERM rather than stdin EOF, mirroring the gateway: a CLI-owned process
869
+ * with a PID file under `.vellum/ces.pid`, started by `wake` and stopped by
870
+ * `sleep`.
874
871
  */
875
872
  export async function startCes(
876
873
  watch: boolean = false,
877
874
  resources?: LocalInstanceResources,
878
875
  ): Promise<void> {
876
+ if (!isCesSiblingOptIn()) return;
877
+
879
878
  const vellumDir = resources
880
879
  ? join(resources.instanceDir, ".vellum")
881
880
  : join(homedir(), ".vellum");
@@ -906,6 +905,7 @@ export async function startCes(
906
905
 
907
906
  const cesEnv: Record<string, string | undefined> = {
908
907
  ...process.env,
908
+ CES_STANDALONE: "1",
909
909
  CES_LOCAL_SOCKET: socketPath,
910
910
  CREDENTIAL_SECURITY_DIR: securityDir,
911
911
  VELLUM_WORKSPACE_DIR: workspaceDir,
@@ -914,7 +914,7 @@ export async function startCes(
914
914
  let ces;
915
915
  const runtimeCesDir = !watch ? localRuntimeCesDir(resources) : undefined;
916
916
  const cesBinary = join(dirname(process.execPath), "credential-executor");
917
- if (!runtimeCesDir && isCompiledCli() && existsSync(cesBinary) && !watch) {
917
+ if (!runtimeCesDir && existsSync(cesBinary) && !watch) {
918
918
  // Compiled binary alongside the CLI (desktop app / compiled CLI).
919
919
  const cesLogFd = openLogFile("hatch.log");
920
920
  ces = spawn(cesBinary, [], {
@@ -1270,7 +1270,7 @@ export function isGatewayWatchModeAvailable(): boolean {
1270
1270
  */
1271
1271
  function writeAssistantWrapper(resources: LocalInstanceResources): void {
1272
1272
  const assistantBinary = join(dirname(process.execPath), "assistant");
1273
- if (!isCompiledCli() || !existsSync(assistantBinary)) return;
1273
+ if (!existsSync(assistantBinary)) return;
1274
1274
 
1275
1275
  const workspaceDir = join(resources.instanceDir, ".vellum", "workspace");
1276
1276
  const protectedDir = join(resources.instanceDir, ".vellum", "protected");
@@ -1332,7 +1332,7 @@ export async function startLocalDaemon(
1332
1332
  // the user runs the compiled CLI directly from the terminal (e.g. via a
1333
1333
  // /usr/local/bin/vellum symlink into the app bundle).
1334
1334
  const daemonBinary = join(dirname(process.execPath), "vellum-daemon");
1335
- if (isCompiledCli() && existsSync(daemonBinary) && !watch) {
1335
+ if (existsSync(daemonBinary) && !watch) {
1336
1336
  // In watch mode, skip the bundled binary and use source (bun --watch
1337
1337
  // only works with source files, not compiled binaries).
1338
1338
 
@@ -1413,8 +1413,6 @@ export async function startLocalDaemon(
1413
1413
  "VELLUM_DEV",
1414
1414
  "VELLUM_DESKTOP_APP",
1415
1415
  "VELLUM_DISABLE_PLATFORM",
1416
- "VELLUM_MIGRATION_EXPORT_ALLOWED_HOSTS",
1417
- "VELLUM_MIGRATION_IMPORT_ALLOWED_HOSTS",
1418
1416
  "VELLUM_WORKSPACE_DIR",
1419
1417
  ]) {
1420
1418
  if (process.env[key]) {
@@ -1626,12 +1624,7 @@ export async function startGateway(
1626
1624
  ? localRuntimeGatewayDir(resources)
1627
1625
  : undefined;
1628
1626
  const gatewayBinary = join(dirname(process.execPath), "vellum-gateway");
1629
- if (
1630
- !runtimeGatewayDir &&
1631
- isCompiledCli() &&
1632
- existsSync(gatewayBinary) &&
1633
- !watch
1634
- ) {
1627
+ if (!runtimeGatewayDir && existsSync(gatewayBinary) && !watch) {
1635
1628
  // Use the compiled gateway binary when available (desktop app or compiled
1636
1629
  // CLI invoked from the terminal). In watch mode, skip the bundled binary
1637
1630
  // and use source (bun --watch only works with source files).
@@ -1720,8 +1713,9 @@ export async function stopLocalProcesses(
1720
1713
  const gatewayPidFile = join(vellumDir, "gateway.pid");
1721
1714
  await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
1722
1715
 
1723
- // Stop the CES sibling if one was launched. No-op when the
1724
- // PID file is absent.
1716
+ // Stop the CES sibling if one was launched (CES_STANDALONE). No-op when the
1717
+ // PID file is absent, so this is safe on the default topology where the
1718
+ // assistant owns CES as an stdio child.
1725
1719
  const cesPidFile = join(vellumDir, "ces.pid");
1726
1720
  await stopProcessByPidFile(cesPidFile, "credential-executor");
1727
1721
 
@@ -1027,12 +1027,6 @@ export async function platformRequestSignedUrl(
1027
1027
  maxRuntimeVersion?: string | null;
1028
1028
  // Target-side, download only: runtime version that will import.
1029
1029
  targetRuntimeVersion?: string;
1030
- // Upload only: who will PUT to the URL. "runtime" signs against the
1031
- // runtime-reachable storage endpoint so a managed assistant pod can
1032
- // upload its export bundle (platform→local teleport); defaults to
1033
- // "client" server-side. No effect in production, where both endpoints
1034
- // are the same.
1035
- consumer?: "client" | "runtime";
1036
1030
  },
1037
1031
  token: string,
1038
1032
  platformUrl?: string,
@@ -1060,9 +1054,6 @@ export async function platformRequestSignedUrl(
1060
1054
  if (params.targetRuntimeVersion !== undefined) {
1061
1055
  body.target_runtime_version = params.targetRuntimeVersion;
1062
1056
  }
1063
- if (params.consumer !== undefined) {
1064
- body.consumer = params.consumer;
1065
- }
1066
1057
 
1067
1058
  const doRequest = async (): Promise<Response> =>
1068
1059
  loopbackSafeFetch(`${resolvedUrl}/v1/migrations/signed-url/`, {
@@ -71,10 +71,6 @@ const PROVIDER_LABELS: Record<LlmProviderId, string> = {
71
71
  gemini: "Gemini",
72
72
  fireworks: "Fireworks",
73
73
  openrouter: "OpenRouter",
74
- "vercel-ai-gateway": "Vercel AI Gateway",
75
- minimax: "MiniMax",
76
- atlascloud: "Atlas Cloud",
77
- together: "Together AI",
78
74
  };
79
75
 
80
76
  export function formatProviderName(provider: LlmProviderId): string {
@@ -155,42 +151,13 @@ function readConfigValue(
155
151
  return value && value.length > 0 ? value : undefined;
156
152
  }
157
153
 
158
- /**
159
- * Infer the provider a bare model ID implies, mirroring the assistant
160
- * resolver's `getCatalogProviderForModel`: an ID listed by multiple catalog
161
- * providers resolves to the FIRST one in catalog order. Vendor prefixes unique
162
- * to one provider map to it; shared gateway IDs (e.g. `anthropic/*`) fall
163
- * through to openrouter, the earlier catalog entry. Drift guard:
164
- * `cli/src/__tests__/provider-inference-parity.test.ts`. Exported for tests.
165
- */
166
- export function inferProviderFromModel(model: string): string | undefined {
167
- if (model.startsWith("claude-")) {
168
- return "anthropic";
169
- }
170
- if (model.startsWith("gpt-")) {
171
- return "openai";
172
- }
173
- if (model.startsWith("gemini-")) {
174
- return "gemini";
175
- }
176
- if (model.startsWith("accounts/fireworks/models/")) {
177
- return "fireworks";
178
- }
179
- if (model.startsWith("openai/") || model.startsWith("xai/")) {
180
- return "vercel-ai-gateway";
181
- }
182
- if (model.startsWith("MiniMaxAI/")) {
183
- return "together";
184
- }
185
- if (model.startsWith("deepseek-ai/")) {
186
- return "atlascloud";
187
- }
188
- if (model.includes("/")) {
189
- return "openrouter";
190
- }
191
- if (model === "llama3.2" || model === "mistral") {
192
- return "ollama";
193
- }
154
+ function inferProviderFromModel(model: string): string | undefined {
155
+ if (model.startsWith("claude-")) return "anthropic";
156
+ if (model.startsWith("gpt-")) return "openai";
157
+ if (model.startsWith("gemini-")) return "gemini";
158
+ if (model.startsWith("accounts/fireworks/models/")) return "fireworks";
159
+ if (model.includes("/")) return "openrouter";
160
+ if (model === "llama3.2" || model === "mistral") return "ollama";
194
161
  return undefined;
195
162
  }
196
163
 
@@ -66,8 +66,9 @@ export async function retireLocal(
66
66
  const gatewayPidFile = join(vellumDir, "gateway.pid");
67
67
  await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
68
68
 
69
- // Stop the CES sibling it is stopped by its PID file, a no-op when the
70
- // PID file is absent (e.g. the sibling was never started or already exited).
69
+ // Stop the CES sibling if one was launched (CES_STANDALONE). No-op when the
70
+ // PID file is absent on the default topology the assistant owns CES as an
71
+ // stdio child and it exits with the daemon.
71
72
  const cesPidFile = join(vellumDir, "ces.pid");
72
73
  const cesStopped = await stopProcessByPidFile(
73
74
  cesPidFile,
@@ -26,7 +26,6 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
26
26
  gemini: "GEMINI_API_KEY",
27
27
  fireworks: "FIREWORKS_API_KEY",
28
28
  openrouter: "OPENROUTER_API_KEY",
29
- "vercel-ai-gateway": "AI_GATEWAY_API_KEY",
30
29
  minimax: "MINIMAX_API_KEY",
31
30
  atlascloud: "ATLASCLOUD_API_KEY",
32
31
  together: "TOGETHER_API_KEY",
@@ -1,57 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { describe, expect, test } from "bun:test";
4
-
5
- import { inferProviderFromModel } from "../lib/provider-secrets.js";
6
-
7
- /**
8
- * Drift guard for the CLI's model → provider inference heuristic.
9
- *
10
- * `inferProviderFromModel` mirrors the assistant resolver's
11
- * `getCatalogProviderForModel` semantics without importing from
12
- * `assistant/src/`: a model ID listed by multiple catalog providers resolves
13
- * to the FIRST provider in catalog order (e.g. `anthropic/*` IDs shared by
14
- * OpenRouter and the Vercel AI Gateway resolve to openrouter), while an ID
15
- * unique to one provider resolves to that provider (e.g. `openai/gpt-5.5` and
16
- * `xai/grok-4.3` to vercel-ai-gateway). This test recomputes that expectation
17
- * from `meta/llm-provider-catalog.json` for every vendor-prefixed
18
- * (slash-containing) model ID and fails if a catalog change breaks the
19
- * heuristic.
20
- */
21
-
22
- const REPO_ROOT = join(import.meta.dir, "..", "..", "..");
23
-
24
- interface LlmCatalog {
25
- providers: Array<{ id: string; models: Array<{ id: string }> }>;
26
- }
27
-
28
- function loadLlmCatalog(): LlmCatalog {
29
- const path = join(REPO_ROOT, "meta", "llm-provider-catalog.json");
30
- return JSON.parse(readFileSync(path, "utf-8"));
31
- }
32
-
33
- describe("CLI provider inference parity", () => {
34
- test("inferProviderFromModel matches first-catalog-provider resolution for every vendor-prefixed model ID", () => {
35
- const catalog = loadLlmCatalog();
36
- const expected: Record<string, string> = {};
37
- for (const provider of catalog.providers) {
38
- for (const model of provider.models) {
39
- if (!model.id.includes("/")) {
40
- continue;
41
- }
42
- expected[model.id] ??= provider.id;
43
- }
44
- }
45
-
46
- // Sanity: the catalog still exercises both the unique-ID and shared-ID
47
- // paths this guard exists for.
48
- expect(Object.values(expected)).toContain("vercel-ai-gateway");
49
- expect(Object.values(expected)).toContain("openrouter");
50
-
51
- const actual: Record<string, string | undefined> = {};
52
- for (const modelId of Object.keys(expected)) {
53
- actual[modelId] = inferProviderFromModel(modelId);
54
- }
55
- expect(actual).toEqual(expected);
56
- });
57
- });
@@ -1,119 +0,0 @@
1
- import {
2
- mkdtempSync,
3
- rmSync,
4
- writeFileSync,
5
- existsSync,
6
- mkdirSync,
7
- } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
- import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
11
-
12
- import { startCes } from "../local.js";
13
-
14
- // ---------------------------------------------------------------------------
15
- // Mocks
16
- // ---------------------------------------------------------------------------
17
-
18
- // Capture spawn calls so we can assert on cmd/env.
19
- let lastSpawnCall: {
20
- cmd: string[];
21
- options: {
22
- detached?: boolean;
23
- env?: Record<string, string | undefined>;
24
- cwd?: string;
25
- };
26
- } | null = null;
27
-
28
- mock.module("node:child_process", () => ({
29
- spawn: mock((cmd: string, args: string[], options: object) => {
30
- lastSpawnCall = { cmd: [cmd, ...args], options };
31
- // Return a fake subprocess with a pid and no-op methods.
32
- return {
33
- pid: 42,
34
- unref: () => {},
35
- stdout: { on: () => {} },
36
- stderr: { on: () => {} },
37
- on: () => {},
38
- };
39
- }),
40
- execSync: () => "",
41
- execFileSync: () => "",
42
- spawnSync: () => ({ status: 0, stdout: "", stderr: "" }),
43
- }));
44
-
45
- // Mock xdg-log so we don't open real log files.
46
- mock.module("../xdg-log.js", () => ({
47
- openLogFile: mock(() => 42),
48
- pipeToLogFile: mock(() => {}),
49
- }));
50
-
51
- // Mock process helpers so stopProcessByPidFile is a no-op.
52
- mock.module("../process.js", () => ({
53
- stopProcessByPidFile: mock(async () => {}),
54
- isProcessAlive: mock(() => false),
55
- stopProcessGracefully: mock(async () => {}),
56
- }));
57
-
58
- // ---------------------------------------------------------------------------
59
- // Tests
60
- // ---------------------------------------------------------------------------
61
-
62
- describe("startCes", () => {
63
- let tempDir: string;
64
-
65
- beforeAll(() => {
66
- tempDir = mkdtempSync(join(tmpdir(), "ces-test-"));
67
- });
68
-
69
- afterAll(() => {
70
- rmSync(tempDir, { recursive: true, force: true });
71
- });
72
-
73
- test("spawns CES with correct env vars and writes PID file", async () => {
74
- // Create the socket path before calling startCes so the wait loop exits.
75
- // startCes unlinks it first, then waits for it to reappear. We create it
76
- // after a short delay to simulate CES binding the socket.
77
- const resources = {
78
- instanceDir: tempDir,
79
- name: "test-assistant",
80
- } as unknown as Parameters<typeof startCes>[1];
81
-
82
- // Create the socket file asynchronously after startCes unlinks it.
83
- // We use a small setTimeout to create it during the wait loop.
84
- const vellumDir = join(tempDir, ".vellum");
85
- mkdirSync(vellumDir, { recursive: true });
86
-
87
- // Pre-create the socket so it exists when startCes checks after unlinking.
88
- // startCes unlinks the stale socket, then polls for it. We create it with
89
- // a slight delay so the poll catches it.
90
- setTimeout(() => {
91
- const socketDir = join(vellumDir, "workspace");
92
- mkdirSync(socketDir, { recursive: true });
93
- writeFileSync(join(socketDir, "ces.sock"), "");
94
- }, 50);
95
-
96
- lastSpawnCall = null;
97
- await startCes(false, resources);
98
-
99
- // Verify spawn was called
100
- expect(lastSpawnCall).not.toBeNull();
101
- expect(lastSpawnCall!.options.detached).toBe(true);
102
-
103
- // Under plain bun (source tree / tests), CES must run via `bun run
104
- // src/main.ts` — never an adjacent `credential-executor` binary, which in
105
- // bun's own bin dir is an unrelated globally-installed package bin.
106
- expect(lastSpawnCall!.cmd[0]).toBe(process.execPath);
107
- expect(lastSpawnCall!.cmd).toContain("src/main.ts");
108
-
109
- // Verify env vars
110
- const env = lastSpawnCall!.options.env!;
111
- expect(env["CES_LOCAL_SOCKET"]).toBeDefined();
112
- expect(env["CREDENTIAL_SECURITY_DIR"]).toBeDefined();
113
- expect(env["VELLUM_WORKSPACE_DIR"]).toBeDefined();
114
-
115
- // Verify PID file was written
116
- const cesPidFile = join(vellumDir, "ces.pid");
117
- expect(existsSync(cesPidFile)).toBe(true);
118
- }, 15_000);
119
- });