@vellumai/cli 0.8.4 → 0.8.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.
Files changed (43) hide show
  1. package/AGENTS.md +17 -1
  2. package/knip.json +2 -1
  3. package/package.json +1 -1
  4. package/src/__tests__/api-key-check.test.ts +78 -0
  5. package/src/__tests__/backup.test.ts +38 -0
  6. package/src/__tests__/recover.test.ts +307 -0
  7. package/src/__tests__/retire.test.ts +241 -0
  8. package/src/__tests__/wake.test.ts +215 -0
  9. package/src/commands/backup.ts +2 -0
  10. package/src/commands/client.ts +62 -32
  11. package/src/commands/flags.ts +197 -0
  12. package/src/commands/gateway/token.ts +73 -0
  13. package/src/commands/gateway.ts +29 -0
  14. package/src/commands/logs.ts +6 -18
  15. package/src/commands/ps.ts +41 -41
  16. package/src/commands/recover.ts +47 -9
  17. package/src/commands/restore.ts +8 -1
  18. package/src/commands/retire.ts +145 -55
  19. package/src/commands/roadmap.ts +449 -0
  20. package/src/commands/rollback.ts +2 -14
  21. package/src/commands/ssh.ts +5 -24
  22. package/src/commands/teleport.ts +34 -26
  23. package/src/commands/upgrade.ts +8 -16
  24. package/src/commands/wake.ts +68 -45
  25. package/src/index.ts +9 -0
  26. package/src/lib/__tests__/port-allocator.test.ts +117 -0
  27. package/src/lib/__tests__/step-runner.test.ts +133 -0
  28. package/src/lib/api-key-check.ts +40 -0
  29. package/src/lib/assistant-config.ts +13 -0
  30. package/src/lib/config-utils.ts +24 -3
  31. package/src/lib/docker.ts +72 -8
  32. package/src/lib/hatch-local.ts +15 -2
  33. package/src/lib/http-client.ts +1 -3
  34. package/src/lib/local.ts +173 -292
  35. package/src/lib/orphan-detection.ts +9 -5
  36. package/src/lib/pgrep.ts +5 -1
  37. package/src/lib/platform-client.ts +97 -49
  38. package/src/lib/port-allocator.ts +93 -0
  39. package/src/lib/process.ts +109 -39
  40. package/src/lib/statefulset.ts +0 -10
  41. package/src/lib/step-runner.ts +102 -9
  42. package/src/lib/sync-cloud-assistants.ts +17 -0
  43. package/src/shared/provider-env-vars.ts +1 -0
package/src/lib/docker.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { randomBytes } from "crypto";
2
- import { chmodSync, existsSync, mkdirSync, watch as fsWatch } from "fs";
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ watch as fsWatch,
8
+ } from "fs";
3
9
  import { arch, platform } from "os";
4
10
  import { dirname, join, resolve } from "path";
5
11
 
@@ -35,7 +41,8 @@ import {
35
41
  formatProviderName,
36
42
  resolveHatchProvider,
37
43
  } from "./provider-secrets.js";
38
- import { exec, execOutput } from "./step-runner";
44
+ import { findOpenPort } from "./port-allocator.js";
45
+ import { exec, execOutput, execWithStdin } from "./step-runner";
39
46
  import {
40
47
  closeLogFile,
41
48
  openLogFile,
@@ -645,7 +652,6 @@ export async function startContainers(
645
652
  extraAssistantEnv?: Record<string, string>;
646
653
  gatewayPort: number;
647
654
  imageTags: Record<ServiceName, string>;
648
- defaultWorkspaceConfigPath?: string;
649
655
  instanceName: string;
650
656
  res: ReturnType<typeof dockerResourceNames>;
651
657
  },
@@ -981,7 +987,22 @@ export async function hatchDocker(
981
987
  await ensureDockerInstalled();
982
988
 
983
989
  const instanceName = generateInstanceName(species, name);
984
- const gatewayPort = getDefaultPorts(getCurrentEnvironment()).gateway;
990
+ // Resolve the gateway's host port dynamically. The env-default
991
+ // (production 7830 / non-prod overrides) is just the *preferred*
992
+ // starting point — if it's taken by another local assistant, eval
993
+ // run, or unrelated process, we walk upward until we find a free
994
+ // port. This replaces the previous "first one in wins, everyone
995
+ // else gets a docker bind error" behavior and removes the need for
996
+ // an orphan-cleanup pre-flight in the evals harness.
997
+ const preferredGatewayPort = getDefaultPorts(
998
+ getCurrentEnvironment(),
999
+ ).gateway;
1000
+ const gatewayPort = await findOpenPort(preferredGatewayPort);
1001
+ if (gatewayPort !== preferredGatewayPort) {
1002
+ log(
1003
+ `Preferred gateway port ${preferredGatewayPort} is in use; allocated ${gatewayPort} for this instance.`,
1004
+ );
1005
+ }
985
1006
 
986
1007
  const imageTags: Record<ServiceName, string> = {
987
1008
  assistant: "",
@@ -1150,10 +1171,53 @@ export async function hatchDocker(
1150
1171
  "chown 1001:1001 /workspace /run/assistant-ipc /run/gateway-ipc",
1151
1172
  ]);
1152
1173
 
1153
- // Write --config key=value pairs to a temp file that gets bind-mounted
1154
- // into the assistant container and read via VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH.
1174
+ // Stage the BYOK default-workspace-config overlay *inside* the workspace
1175
+ // volume so the daemon's startup loader can consume it. The loader
1176
+ // (`mergeDefaultWorkspaceConfig` in assistant/src/config/loader.ts) does:
1177
+ // 1. JSON.parse + deep-merge into the workspace's config.json.
1178
+ // 2. `renameSync(defaultConfigPath, <workspace>/default-config.json)`
1179
+ // to mark the overlay consumed so subsequent restarts skip it.
1180
+ //
1181
+ // The rename must be same-filesystem. Bind-mounting the host file into
1182
+ // `/tmp/...` (the pre-#32025 design) crossed filesystems and silently
1183
+ // failed with EXDEV, so every `docker start` re-applied the overlay.
1184
+ // Staging into the workspace volume keeps the rename in-place.
1185
+ //
1186
+ // Streaming the JSON via stdin (instead of bind-mounting the host file
1187
+ // into the staging container) sidesteps macOS Colima's virtiofs share,
1188
+ // which doesn't expose `/var/folders/...` (where `os.tmpdir()` resolves
1189
+ // on macOS) and would otherwise materialize an empty directory at the
1190
+ // bind-mount target.
1191
+ //
1192
+ // @deprecated stopgap. Replacement direction is one of:
1193
+ // 1. Post-hatch API calls (POST /v1/secrets + a small endpoint that
1194
+ // returns canonical inference-profile templates).
1195
+ // 2. Move inference-profile seeds out of workspace config and into
1196
+ // Assistant code, eliminating the overlay entirely.
1197
+ // See `cli/src/lib/config-utils.ts` JSDoc for context.
1155
1198
  const hatchConfigValues = buildHatchConfigValues(configValues, provider);
1156
- const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
1199
+ const hostOverlayPath = writeInitialConfig(hatchConfigValues);
1200
+ const stagedOverlayInContainer = "/workspace/.default-config-overlay.json";
1201
+ const extraAssistantEnv: Record<string, string> = {};
1202
+ if (hostOverlayPath) {
1203
+ await execWithStdin(
1204
+ "docker",
1205
+ [
1206
+ "run",
1207
+ "--rm",
1208
+ "-i",
1209
+ "-v",
1210
+ `${res.workspaceVolume}:/workspace`,
1211
+ "busybox",
1212
+ "sh",
1213
+ "-c",
1214
+ `cat > ${stagedOverlayInContainer} && chown 1001:1001 ${stagedOverlayInContainer}`,
1215
+ ],
1216
+ readFileSync(hostOverlayPath, "utf-8"),
1217
+ );
1218
+ extraAssistantEnv.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
1219
+ stagedOverlayInContainer;
1220
+ }
1157
1221
 
1158
1222
  const cesServiceToken = randomBytes(32).toString("hex");
1159
1223
  const signingKey = randomBytes(32).toString("hex");
@@ -1175,9 +1239,9 @@ export async function hatchDocker(
1175
1239
  signingKey,
1176
1240
  bootstrapSecret,
1177
1241
  cesServiceToken,
1242
+ extraAssistantEnv,
1178
1243
  gatewayPort,
1179
1244
  imageTags,
1180
- defaultWorkspaceConfigPath,
1181
1245
  instanceName,
1182
1246
  res,
1183
1247
  },
@@ -40,6 +40,7 @@ import {
40
40
  resolveHatchProvider,
41
41
  } from "./provider-secrets.js";
42
42
  import { logHatchNextSteps } from "./hatch-next-steps.js";
43
+ import { checkProviderApiKey } from "./api-key-check.js";
43
44
 
44
45
  /**
45
46
  * Attempts to place a symlink at the given path pointing to cliBinary.
@@ -178,6 +179,16 @@ export async function hatchLocal(
178
179
  console.log(` Species: ${species}`);
179
180
  console.log("");
180
181
 
182
+ const apiKeyCheck = checkProviderApiKey();
183
+ if (!apiKeyCheck.hasKey) {
184
+ console.warn(
185
+ "Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
186
+ );
187
+ console.warn(" To fix, export your key before running vellum hatch:");
188
+ console.warn(" export ANTHROPIC_API_KEY=<your-key>");
189
+ console.warn("");
190
+ }
191
+
181
192
  if (!process.env.APP_VERSION) {
182
193
  process.env.APP_VERSION = cliPkg.version;
183
194
  }
@@ -188,6 +199,7 @@ export async function hatchLocal(
188
199
 
189
200
  emitProgress(3, 6, "Starting assistant...");
190
201
  const signingKey = generateLocalSigningKey();
202
+ const bootstrapSecret = generateLocalSigningKey();
191
203
  await startLocalDaemon(watch, resources, {
192
204
  defaultWorkspaceConfigPath,
193
205
  signingKey,
@@ -196,7 +208,7 @@ export async function hatchLocal(
196
208
  emitProgress(4, 6, "Starting gateway...");
197
209
  let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
198
210
  try {
199
- runtimeUrl = await startGateway(watch, resources, { signingKey });
211
+ runtimeUrl = await startGateway(watch, resources, { signingKey, bootstrapSecret });
200
212
  } catch (error) {
201
213
  // Gateway failed — stop the daemon we just started so we don't leave
202
214
  // orphaned processes with no lock file entry.
@@ -217,7 +229,7 @@ export async function hatchLocal(
217
229
  let guardianAccessToken: string | undefined;
218
230
  for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
219
231
  try {
220
- const tokenData = await leaseGuardianToken(loopbackUrl, instanceName);
232
+ const tokenData = await leaseGuardianToken(loopbackUrl, instanceName, bootstrapSecret);
221
233
  guardianAccessToken = tokenData.accessToken;
222
234
  break;
223
235
  } catch (err) {
@@ -246,6 +258,7 @@ export async function hatchLocal(
246
258
  species,
247
259
  hatchedAt: new Date().toISOString(),
248
260
  resources: { ...resources, signingKey },
261
+ guardianBootstrapSecret: bootstrapSecret,
249
262
  };
250
263
 
251
264
  emitProgress(6, 6, "Saving configuration...");
@@ -8,8 +8,6 @@ export function buildDaemonUrl(port: number): string {
8
8
  /**
9
9
  * Perform an HTTP health check against the daemon's `/healthz` endpoint.
10
10
  * Returns true if the daemon responds with HTTP 200, false otherwise.
11
- *
12
- * This replaces the socket-based `isSocketResponsive()` check.
13
11
  */
14
12
  export async function httpHealthCheck(
15
13
  port: number,
@@ -28,7 +26,7 @@ export async function httpHealthCheck(
28
26
 
29
27
  /**
30
28
  * Poll the daemon's `/healthz` endpoint until it responds with 200 or the
31
- * timeout is reached. This replaces `waitForSocketFile()`.
29
+ * timeout is reached.
32
30
  *
33
31
  * Returns true if the daemon became healthy within the timeout, false otherwise.
34
32
  */