@vellumai/cli 0.8.2 → 0.8.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.
@@ -8,7 +8,6 @@ import {
8
8
  saveAssistantEntry,
9
9
  setActiveAssistant,
10
10
  } from "../lib/assistant-config";
11
- import { hatchAws } from "../lib/aws";
12
11
  import {
13
12
  SPECIES_CONFIG,
14
13
  VALID_REMOTE_HOSTS,
@@ -17,7 +16,6 @@ import {
17
16
  import type { RemoteHost, Species } from "../lib/constants";
18
17
  import { buildNestedConfig } from "../lib/config-utils";
19
18
  import { hatchDocker } from "../lib/docker";
20
- import { hatchGcp } from "../lib/gcp";
21
19
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
22
20
  import { hatchLocal } from "../lib/hatch-local";
23
21
  import {
@@ -169,6 +167,7 @@ source ${INSTALL_SCRIPT_REMOTE_PATH}
169
167
  }
170
168
 
171
169
  const DEFAULT_REMOTE: RemoteHost = "local";
170
+ const UNSUPPORTED_REMOTE_HATCH_TARGETS = new Set<RemoteHost>(["aws", "gcp"]);
172
171
 
173
172
  interface HatchArgs {
174
173
  species: Species;
@@ -177,7 +176,9 @@ interface HatchArgs {
177
176
  name: string | null;
178
177
  remote: RemoteHost;
179
178
  watch: boolean;
179
+ sourcePath: string | null;
180
180
  configValues: Record<string, string>;
181
+ analyze: boolean;
181
182
  }
182
183
 
183
184
  function parseArgs(): HatchArgs {
@@ -188,7 +189,9 @@ function parseArgs(): HatchArgs {
188
189
  let name: string | null = null;
189
190
  let remote: RemoteHost = DEFAULT_REMOTE;
190
191
  let watch = false;
192
+ let sourcePath: string | null = null;
191
193
  const configValues: Record<string, string> = {};
194
+ let analyze = false;
192
195
 
193
196
  for (let i = 0; i < args.length; i++) {
194
197
  const arg = args[i];
@@ -210,17 +213,33 @@ function parseArgs(): HatchArgs {
210
213
  console.log(
211
214
  " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
212
215
  );
216
+ console.log(
217
+ " --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
218
+ );
213
219
  console.log(
214
220
  " --keep-alive Stay alive after hatch, exit when gateway stops",
215
221
  );
216
222
  console.log(
217
223
  " --config <key=value> Set a workspace config value (repeatable)",
218
224
  );
225
+ console.log(
226
+ " --analyze Emit a structured hatch-timing log line on stdout",
227
+ );
219
228
  process.exit(0);
220
229
  } else if (arg === "-d") {
221
230
  detached = true;
222
231
  } else if (arg === "--watch") {
223
232
  watch = true;
233
+ } else if (arg === "--analyze") {
234
+ analyze = true;
235
+ } else if (arg === "--source") {
236
+ const next = args[i + 1];
237
+ if (!next || next.startsWith("-")) {
238
+ console.error("Error: --source requires a path argument");
239
+ process.exit(1);
240
+ }
241
+ sourcePath = next;
242
+ i++;
224
243
  } else if (arg === "--keep-alive") {
225
244
  keepAlive = true;
226
245
  } else if (arg === "--name") {
@@ -270,7 +289,7 @@ function parseArgs(): HatchArgs {
270
289
  species = arg as Species;
271
290
  } else {
272
291
  console.error(
273
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
292
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --analyze`,
274
293
  );
275
294
  process.exit(1);
276
295
  }
@@ -283,7 +302,9 @@ function parseArgs(): HatchArgs {
283
302
  name,
284
303
  remote,
285
304
  watch,
305
+ sourcePath,
286
306
  configValues,
307
+ analyze,
287
308
  };
288
309
  }
289
310
 
@@ -508,8 +529,17 @@ export async function hatch(): Promise<void> {
508
529
  const cliVersion = getCliVersion();
509
530
  console.log(`@vellumai/cli v${cliVersion}`);
510
531
 
511
- const { species, detached, keepAlive, name, remote, watch, configValues } =
512
- parseArgs();
532
+ const {
533
+ species,
534
+ detached,
535
+ keepAlive,
536
+ name,
537
+ remote,
538
+ watch,
539
+ sourcePath,
540
+ configValues,
541
+ analyze,
542
+ } = parseArgs();
513
543
 
514
544
  if (watch && remote !== "local" && remote !== "docker") {
515
545
  console.error(
@@ -518,30 +548,33 @@ export async function hatch(): Promise<void> {
518
548
  process.exit(1);
519
549
  }
520
550
 
521
- if (remote === "local") {
522
- await hatchLocal(species, name, watch, keepAlive, configValues);
523
- return;
551
+ if (sourcePath !== null && remote !== "docker") {
552
+ console.error(
553
+ "Error: --source is only supported for docker hatch targets.",
554
+ );
555
+ process.exit(1);
524
556
  }
525
557
 
526
- if (remote === "gcp") {
527
- await hatchGcp(
528
- species,
529
- detached,
530
- name,
531
- buildStartupScript,
532
- watchHatching,
533
- configValues,
558
+ if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
559
+ console.error(
560
+ `Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
534
561
  );
535
- return;
562
+ console.error(
563
+ "No cloud resources were created. To self-host on AWS/GCP, SSH into the VM and run `vellum hatch` or `vellum hatch --remote docker` there.",
564
+ );
565
+ process.exit(1);
536
566
  }
537
567
 
538
- if (remote === "aws") {
539
- await hatchAws(species, detached, name, configValues);
568
+ if (remote === "local") {
569
+ await hatchLocal(species, name, watch, keepAlive, configValues);
540
570
  return;
541
571
  }
542
572
 
543
573
  if (remote === "docker") {
544
- await hatchDocker(species, detached, name, watch, configValues);
574
+ await hatchDocker(species, detached, name, watch, configValues, {
575
+ sourcePath,
576
+ analyze,
577
+ });
545
578
  return;
546
579
  }
547
580
 
@@ -1,5 +1,6 @@
1
1
  import { resolveAssistant } from "../lib/assistant-config.js";
2
2
  import {
3
+ leaseGuardianToken,
3
4
  loadGuardianToken,
4
5
  refreshGuardianToken,
5
6
  type GuardianTokenData,
@@ -41,6 +42,50 @@ function isGuardianAccessTokenUsable(
41
42
  return Number.isFinite(expiresAt) && expiresAt > Date.now();
42
43
  }
43
44
 
45
+ async function resolveSetupBearerToken(
46
+ entry: NonNullable<ReturnType<typeof resolveAssistant>>,
47
+ gatewayUrl: string,
48
+ ): Promise<string | undefined> {
49
+ const guardianToken = loadGuardianToken(entry.assistantId);
50
+ if (isGuardianAccessTokenUsable(guardianToken)) {
51
+ return guardianToken.accessToken;
52
+ }
53
+
54
+ if (guardianToken) {
55
+ const refreshedToken = await refreshGuardianToken(
56
+ gatewayUrl,
57
+ entry.assistantId,
58
+ );
59
+ if (isGuardianAccessTokenUsable(refreshedToken)) {
60
+ return refreshedToken.accessToken;
61
+ }
62
+ }
63
+
64
+ const canLeaseGuardianToken =
65
+ entry.cloud === "local" || entry.cloud === "docker" || entry.localUrl;
66
+ if (canLeaseGuardianToken) {
67
+ try {
68
+ const bootstrapSecret =
69
+ typeof entry.guardianBootstrapSecret === "string"
70
+ ? entry.guardianBootstrapSecret
71
+ : undefined;
72
+ const leasedToken = await leaseGuardianToken(
73
+ gatewayUrl,
74
+ entry.assistantId,
75
+ bootstrapSecret,
76
+ );
77
+ if (isGuardianAccessTokenUsable(leasedToken)) {
78
+ return leasedToken.accessToken;
79
+ }
80
+ } catch {
81
+ // Fall through to any lockfile bearer token, or let the setup request
82
+ // surface the gateway's auth error below.
83
+ }
84
+ }
85
+
86
+ return entry.bearerToken;
87
+ }
88
+
44
89
  export async function setup(): Promise<void> {
45
90
  const args = process.argv.slice(3);
46
91
 
@@ -85,18 +130,7 @@ export async function setup(): Promise<void> {
85
130
  }
86
131
 
87
132
  const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
88
- let bearerToken: string | undefined;
89
- const guardianToken = loadGuardianToken(entry.assistantId);
90
- if (isGuardianAccessTokenUsable(guardianToken)) {
91
- bearerToken = guardianToken.accessToken;
92
- } else {
93
- const refreshedToken = guardianToken
94
- ? await refreshGuardianToken(gatewayUrl, entry.assistantId)
95
- : null;
96
- bearerToken = isGuardianAccessTokenUsable(refreshedToken)
97
- ? refreshedToken.accessToken
98
- : entry.bearerToken;
99
- }
133
+ const bearerToken = await resolveSetupBearerToken(entry, gatewayUrl);
100
134
 
101
135
  console.log("Vellum Setup");
102
136
  console.log("============\n");
@@ -877,7 +877,16 @@ export async function resolveOrHatchTarget(
877
877
  // Hatch a new assistant in the target environment
878
878
  if (targetEnv === "local") {
879
879
  const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
880
- await hatchLocal("vellum", targetName ?? null, false, false, {});
880
+ await hatchLocal(
881
+ "vellum",
882
+ targetName ?? null,
883
+ false,
884
+ false,
885
+ {},
886
+ {
887
+ setupProviderCredentials: false,
888
+ },
889
+ );
881
890
  const entry = targetName
882
891
  ? findAssistantByName(targetName)
883
892
  : (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
@@ -892,7 +901,16 @@ export async function resolveOrHatchTarget(
892
901
 
893
902
  if (targetEnv === "docker") {
894
903
  const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
895
- await hatchDocker("vellum", false, targetName ?? null, false, {});
904
+ await hatchDocker(
905
+ "vellum",
906
+ false,
907
+ targetName ?? null,
908
+ false,
909
+ {},
910
+ {
911
+ setupProviderCredentials: false,
912
+ },
913
+ );
896
914
  const entry = targetName
897
915
  ? findAssistantByName(targetName)
898
916
  : (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
@@ -4,6 +4,8 @@ import {
4
4
  AVATAR_DEVICE_ENV_VAR,
5
5
  dockerResourceNames,
6
6
  resolveAvatarDevicePath,
7
+ resolveDockerHatchMode,
8
+ resolveDockerProviderCredentialSetupAction,
7
9
  type ServiceName,
8
10
  } from "../docker.js";
9
11
  import { buildServiceRunArgs } from "../statefulset.js";
@@ -103,6 +105,51 @@ describe("buildServiceRunArgs — assistant", () => {
103
105
  });
104
106
  });
105
107
 
108
+ describe("resolveDockerProviderCredentialSetupAction", () => {
109
+ test("defers provider setup in detached mode", () => {
110
+ expect(
111
+ resolveDockerProviderCredentialSetupAction({
112
+ provider: "anthropic",
113
+ detached: true,
114
+ }),
115
+ ).toBe("defer");
116
+ });
117
+
118
+ test("reports missing guardian token only when a lease was expected", () => {
119
+ expect(
120
+ resolveDockerProviderCredentialSetupAction({
121
+ provider: "anthropic",
122
+ detached: false,
123
+ }),
124
+ ).toBe("missing-token");
125
+ });
126
+
127
+ test("configures provider setup when a guardian token is available", () => {
128
+ expect(
129
+ resolveDockerProviderCredentialSetupAction({
130
+ provider: "anthropic",
131
+ guardianAccessToken: "guardian-token",
132
+ detached: false,
133
+ }),
134
+ ).toBe("configure");
135
+ });
136
+
137
+ test("skips provider setup for internal hatches and detached keyless hatches", () => {
138
+ expect(
139
+ resolveDockerProviderCredentialSetupAction({
140
+ provider: undefined,
141
+ detached: false,
142
+ }),
143
+ ).toBe("skip");
144
+ expect(
145
+ resolveDockerProviderCredentialSetupAction({
146
+ provider: null,
147
+ detached: true,
148
+ }),
149
+ ).toBe("skip");
150
+ });
151
+ });
152
+
106
153
  describe("buildServiceRunArgs — gateway", () => {
107
154
  const savedVelayBaseUrl = process.env.VELAY_BASE_URL;
108
155
 
@@ -171,3 +218,62 @@ describe("VELLUM_AVATAR_DEVICE passthrough", () => {
171
218
  );
172
219
  });
173
220
  });
221
+
222
+ describe("resolveDockerHatchMode", () => {
223
+ test("defaults to pulling published images when no source flag is set", () => {
224
+ expect(
225
+ resolveDockerHatchMode({
226
+ watch: false,
227
+ buildFromSource: false,
228
+ fullSourceTreeAvailable: true,
229
+ }),
230
+ ).toEqual({ build: false, watcher: false, fellBackToPull: false });
231
+ });
232
+
233
+ test("--source <path> builds without enabling the file watcher", () => {
234
+ expect(
235
+ resolveDockerHatchMode({
236
+ watch: false,
237
+ buildFromSource: true,
238
+ fullSourceTreeAvailable: true,
239
+ }),
240
+ ).toEqual({ build: true, watcher: false, fellBackToPull: false });
241
+ });
242
+
243
+ test("--watch builds and enables the file watcher", () => {
244
+ expect(
245
+ resolveDockerHatchMode({
246
+ watch: true,
247
+ buildFromSource: false,
248
+ fullSourceTreeAvailable: true,
249
+ }),
250
+ ).toEqual({ build: true, watcher: true, fellBackToPull: false });
251
+ });
252
+
253
+ test("--watch + --source <path> still enables the watcher (watch wins)", () => {
254
+ expect(
255
+ resolveDockerHatchMode({
256
+ watch: true,
257
+ buildFromSource: true,
258
+ fullSourceTreeAvailable: true,
259
+ }),
260
+ ).toEqual({ build: true, watcher: true, fellBackToPull: false });
261
+ });
262
+
263
+ test("falls back to pull when source flag is set but source tree is missing", () => {
264
+ expect(
265
+ resolveDockerHatchMode({
266
+ watch: false,
267
+ buildFromSource: true,
268
+ fullSourceTreeAvailable: false,
269
+ }),
270
+ ).toEqual({ build: false, watcher: false, fellBackToPull: true });
271
+ expect(
272
+ resolveDockerHatchMode({
273
+ watch: true,
274
+ buildFromSource: false,
275
+ fullSourceTreeAvailable: false,
276
+ }),
277
+ ).toEqual({ build: false, watcher: false, fellBackToPull: true });
278
+ });
279
+ });
@@ -89,6 +89,8 @@ export interface AssistantEntry {
89
89
  resources?: LocalInstanceResources;
90
90
  /** PID of the file watcher process for docker instances hatched with --watch. */
91
91
  watcherPid?: number;
92
+ /** Local bootstrap secret used to lease guardian tokens for Docker assistants after detached hatch. */
93
+ guardianBootstrapSecret?: string;
92
94
  /** Docker image metadata for rollback. Only present for docker topology entries. */
93
95
  containerInfo?: ContainerInfo;
94
96
  /** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
@@ -32,6 +32,24 @@ export function buildNestedConfig(
32
32
  return config;
33
33
  }
34
34
 
35
+ /**
36
+ * Ensure hatch always provides enough initial LLM config for the assistant to
37
+ * detect a fresh off-platform hatch and seed BYOK profiles.
38
+ */
39
+ export function buildHatchConfigValues(
40
+ configValues: Record<string, string>,
41
+ provider: string | null | undefined,
42
+ ): Record<string, string> {
43
+ if (!provider || configValues["llm.default.provider"]) {
44
+ return configValues;
45
+ }
46
+
47
+ return {
48
+ ...configValues,
49
+ "llm.default.provider": provider,
50
+ };
51
+ }
52
+
35
53
  /**
36
54
  * Write arbitrary key-value pairs to a temporary JSON file and return its
37
55
  * path. The caller passes this path to the daemon via the