@vellumai/cli 0.4.14 → 0.4.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -135,15 +135,18 @@ async function runtimeRequest<T>(
135
135
  path: string,
136
136
  init?: RequestInit,
137
137
  bearerToken?: string,
138
- actorToken?: string,
138
+ accessToken?: string,
139
139
  ): Promise<T> {
140
140
  const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
141
+ // Prefer the JWT access token (from bootstrap) over the shared-secret
142
+ // bearer token. The JWT carries identity claims and is the canonical
143
+ // auth mechanism in the single-header auth model.
144
+ const authToken = accessToken ?? bearerToken;
141
145
  const response = await fetch(url, {
142
146
  ...init,
143
147
  headers: {
144
148
  "Content-Type": "application/json",
145
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
146
- ...(actorToken ? { "X-Actor-Token": actorToken } : {}),
149
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
147
150
  ...(init?.headers as Record<string, string> | undefined),
148
151
  },
149
152
  });
@@ -179,7 +182,7 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
179
182
  return response.json() as Promise<HealthResponse>;
180
183
  }
181
184
 
182
- async function bootstrapActorToken(baseUrl: string, bearerToken?: string): Promise<string> {
185
+ async function bootstrapAccessToken(baseUrl: string, bearerToken?: string): Promise<string> {
183
186
  if (!bearerToken) {
184
187
  throw new Error("Missing bearer token; cannot bootstrap actor identity");
185
188
  }
@@ -207,19 +210,19 @@ async function bootstrapActorToken(baseUrl: string, bearerToken?: string): Promi
207
210
  throw new Error("Invalid bootstrap response from gateway/runtime");
208
211
  }
209
212
 
210
- const actorToken = (json as Record<string, unknown>).actorToken;
211
- if (typeof actorToken !== "string" || actorToken.length === 0) {
213
+ const accessToken = (json as Record<string, unknown>).accessToken;
214
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
212
215
  throw new Error("Invalid bootstrap response from gateway/runtime");
213
216
  }
214
217
 
215
- return actorToken;
218
+ return accessToken;
216
219
  }
217
220
 
218
221
  async function pollMessages(
219
222
  baseUrl: string,
220
223
  assistantId: string,
221
224
  bearerToken?: string,
222
- actorToken?: string,
225
+ accessToken?: string,
223
226
  ): Promise<ListMessagesResponse> {
224
227
  const params = new URLSearchParams({ conversationKey: assistantId });
225
228
  return runtimeRequest<ListMessagesResponse>(
@@ -228,7 +231,7 @@ async function pollMessages(
228
231
  `/messages?${params.toString()}`,
229
232
  undefined,
230
233
  bearerToken,
231
- actorToken,
234
+ accessToken,
232
235
  );
233
236
  }
234
237
 
@@ -238,7 +241,7 @@ async function sendMessage(
238
241
  content: string,
239
242
  signal?: AbortSignal,
240
243
  bearerToken?: string,
241
- actorToken?: string,
244
+ accessToken?: string,
242
245
  ): Promise<SendMessageResponse> {
243
246
  return runtimeRequest<SendMessageResponse>(
244
247
  baseUrl,
@@ -250,7 +253,7 @@ async function sendMessage(
250
253
  signal,
251
254
  },
252
255
  bearerToken,
253
- actorToken,
256
+ accessToken,
254
257
  );
255
258
  }
256
259
 
@@ -260,7 +263,7 @@ async function submitDecision(
260
263
  requestId: string,
261
264
  decision: "allow" | "deny",
262
265
  bearerToken?: string,
263
- actorToken?: string,
266
+ accessToken?: string,
264
267
  ): Promise<SubmitDecisionResponse> {
265
268
  return runtimeRequest<SubmitDecisionResponse>(
266
269
  baseUrl,
@@ -271,7 +274,7 @@ async function submitDecision(
271
274
  body: JSON.stringify({ requestId, decision }),
272
275
  },
273
276
  bearerToken,
274
- actorToken,
277
+ accessToken,
275
278
  );
276
279
  }
277
280
 
@@ -283,7 +286,7 @@ async function addTrustRule(
283
286
  scope: string,
284
287
  decision: "allow" | "deny",
285
288
  bearerToken?: string,
286
- actorToken?: string,
289
+ accessToken?: string,
287
290
  ): Promise<AddTrustRuleResponse> {
288
291
  return runtimeRequest<AddTrustRuleResponse>(
289
292
  baseUrl,
@@ -294,7 +297,7 @@ async function addTrustRule(
294
297
  body: JSON.stringify({ requestId, pattern, scope, decision }),
295
298
  },
296
299
  bearerToken,
297
- actorToken,
300
+ accessToken,
298
301
  );
299
302
  }
300
303
 
@@ -302,7 +305,7 @@ async function pollPendingInteractions(
302
305
  baseUrl: string,
303
306
  assistantId: string,
304
307
  bearerToken?: string,
305
- actorToken?: string,
308
+ accessToken?: string,
306
309
  ): Promise<PendingInteractionsResponse> {
307
310
  const params = new URLSearchParams({ conversationKey: assistantId });
308
311
  return runtimeRequest<PendingInteractionsResponse>(
@@ -311,7 +314,7 @@ async function pollPendingInteractions(
311
314
  `/pending-interactions?${params.toString()}`,
312
315
  undefined,
313
316
  bearerToken,
314
- actorToken,
317
+ accessToken,
315
318
  );
316
319
  }
317
320
 
@@ -349,7 +352,7 @@ async function handleConfirmationPrompt(
349
352
  confirmation: PendingConfirmation,
350
353
  chatApp: ChatAppHandle,
351
354
  bearerToken?: string,
352
- actorToken?: string,
355
+ accessToken?: string,
353
356
  ): Promise<void> {
354
357
  const preview = formatConfirmationPreview(confirmation.toolName, confirmation.input);
355
358
  const allowlistOptions = confirmation.allowlistOptions ?? [];
@@ -369,7 +372,7 @@ async function handleConfirmationPrompt(
369
372
  const index = await chatApp.showSelection("Tool Approval", options);
370
373
 
371
374
  if (index === 0) {
372
- await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken, actorToken);
375
+ await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken, accessToken);
373
376
  chatApp.addStatus("\u2714 Allowed", "green");
374
377
  return;
375
378
  }
@@ -382,7 +385,7 @@ async function handleConfirmationPrompt(
382
385
  chatApp,
383
386
  "always_allow",
384
387
  bearerToken,
385
- actorToken,
388
+ accessToken,
386
389
  );
387
390
  return;
388
391
  }
@@ -395,12 +398,12 @@ async function handleConfirmationPrompt(
395
398
  chatApp,
396
399
  "always_deny",
397
400
  bearerToken,
398
- actorToken,
401
+ accessToken,
399
402
  );
400
403
  return;
401
404
  }
402
405
 
403
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
406
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
404
407
  chatApp.addStatus("\u2718 Denied", "yellow");
405
408
  }
406
409
 
@@ -412,7 +415,7 @@ async function handlePatternSelection(
412
415
  chatApp: ChatAppHandle,
413
416
  trustDecision: TrustDecision,
414
417
  bearerToken?: string,
415
- actorToken?: string,
418
+ accessToken?: string,
416
419
  ): Promise<void> {
417
420
  const allowlistOptions = confirmation.allowlistOptions ?? [];
418
421
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -431,12 +434,12 @@ async function handlePatternSelection(
431
434
  selectedPattern,
432
435
  trustDecision,
433
436
  bearerToken,
434
- actorToken,
437
+ accessToken,
435
438
  );
436
439
  return;
437
440
  }
438
441
 
439
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
442
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
440
443
  chatApp.addStatus("\u2718 Denied", "yellow");
441
444
  }
442
445
 
@@ -449,7 +452,7 @@ async function handleScopeSelection(
449
452
  selectedPattern: string,
450
453
  trustDecision: TrustDecision,
451
454
  bearerToken?: string,
452
- actorToken?: string,
455
+ accessToken?: string,
453
456
  ): Promise<void> {
454
457
  const scopeOptions = confirmation.scopeOptions ?? [];
455
458
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -467,7 +470,7 @@ async function handleScopeSelection(
467
470
  scopeOptions[index].scope,
468
471
  ruleDecision,
469
472
  bearerToken,
470
- actorToken,
473
+ accessToken,
471
474
  );
472
475
  await submitDecision(
473
476
  baseUrl,
@@ -475,7 +478,7 @@ async function handleScopeSelection(
475
478
  requestId,
476
479
  ruleDecision === "deny" ? "deny" : "allow",
477
480
  bearerToken,
478
- actorToken,
481
+ accessToken,
479
482
  );
480
483
  const ruleLabel = trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
481
484
  const ruleColor = trustDecision === "always_deny" ? "yellow" : "green";
@@ -486,7 +489,7 @@ async function handleScopeSelection(
486
489
  return;
487
490
  }
488
491
 
489
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
492
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
490
493
  chatApp.addStatus("\u2718 Denied", "yellow");
491
494
  }
492
495
 
@@ -1081,7 +1084,7 @@ function ChatApp({
1081
1084
  const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
1082
1085
  const doctorSessionIdRef = useRef(randomUUID());
1083
1086
  const handleRef_ = useRef<ChatAppHandle | null>(null);
1084
- const actorTokenRef = useRef<string | undefined>(undefined);
1087
+ const accessTokenRef = useRef<string | undefined>(undefined);
1085
1088
 
1086
1089
  const { stdout } = useStdout();
1087
1090
  const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
@@ -1307,8 +1310,8 @@ function ChatApp({
1307
1310
 
1308
1311
  try {
1309
1312
  const health = await checkHealthRuntime(runtimeUrl);
1310
- if (!actorTokenRef.current) {
1311
- actorTokenRef.current = await bootstrapActorToken(runtimeUrl, bearerToken);
1313
+ if (!accessTokenRef.current) {
1314
+ accessTokenRef.current = await bootstrapAccessToken(runtimeUrl, bearerToken);
1312
1315
  }
1313
1316
  h.hideSpinner();
1314
1317
  h.updateHealthStatus(health.status);
@@ -1329,7 +1332,7 @@ function ChatApp({
1329
1332
  runtimeUrl,
1330
1333
  assistantId,
1331
1334
  bearerToken,
1332
- actorTokenRef.current,
1335
+ accessTokenRef.current,
1333
1336
  );
1334
1337
  h.hideSpinner();
1335
1338
  if (historyResponse.messages.length > 0) {
@@ -1348,7 +1351,7 @@ function ChatApp({
1348
1351
  runtimeUrl,
1349
1352
  assistantId,
1350
1353
  bearerToken,
1351
- actorTokenRef.current,
1354
+ accessTokenRef.current,
1352
1355
  );
1353
1356
  for (const msg of response.messages) {
1354
1357
  if (!seenMessageIdsRef.current.has(msg.id)) {
@@ -1632,7 +1635,7 @@ function ChatApp({
1632
1635
  trimmed,
1633
1636
  controller.signal,
1634
1637
  bearerToken,
1635
- actorTokenRef.current,
1638
+ accessTokenRef.current,
1636
1639
  );
1637
1640
  clearTimeout(timeoutId);
1638
1641
  if (!sendResult.accepted) {
@@ -1662,7 +1665,7 @@ function ChatApp({
1662
1665
  runtimeUrl,
1663
1666
  assistantId,
1664
1667
  bearerToken,
1665
- actorTokenRef.current,
1668
+ accessTokenRef.current,
1666
1669
  );
1667
1670
 
1668
1671
  if (pending.pendingConfirmation) {
@@ -1674,7 +1677,7 @@ function ChatApp({
1674
1677
  pending.pendingConfirmation,
1675
1678
  h,
1676
1679
  bearerToken,
1677
- actorTokenRef.current,
1680
+ accessTokenRef.current,
1678
1681
  );
1679
1682
  h.showSpinner("Working...");
1680
1683
  continue;
@@ -1693,7 +1696,7 @@ function ChatApp({
1693
1696
  body: JSON.stringify({ requestId: secretRequestId, value, delivery }),
1694
1697
  },
1695
1698
  bearerToken,
1696
- actorTokenRef.current,
1699
+ accessTokenRef.current,
1697
1700
  );
1698
1701
  });
1699
1702
  h.showSpinner("Working...");
@@ -1709,7 +1712,7 @@ function ChatApp({
1709
1712
  runtimeUrl,
1710
1713
  assistantId,
1711
1714
  bearerToken,
1712
- actorTokenRef.current,
1715
+ accessTokenRef.current,
1713
1716
  );
1714
1717
  for (const msg of pollResult.messages) {
1715
1718
  if (!seenMessageIdsRef.current.has(msg.id)) {
@@ -1,9 +1,32 @@
1
+ import { execFileSync } from "child_process";
1
2
  import { existsSync, readFileSync, unlinkSync } from "fs";
2
3
 
4
+ /**
5
+ * Verify that a PID belongs to a vellum-related process by inspecting its
6
+ * command line via `ps`. Prevents killing unrelated processes when a PID file
7
+ * is stale and the OS has reused the PID.
8
+ */
9
+ function isVellumProcess(pid: number): boolean {
10
+ try {
11
+ const output = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
12
+ encoding: "utf-8",
13
+ timeout: 3000,
14
+ stdio: ["ignore", "pipe", "ignore"],
15
+ }).trim();
16
+ // Match daemon binary, gateway binary, or bun-run source invocations
17
+ return /vellum|@vellumai/.test(output);
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
3
23
  /**
4
24
  * Check if a PID file's process is alive.
5
25
  */
6
- export function isProcessAlive(pidFile: string): { alive: boolean; pid: number | null } {
26
+ export function isProcessAlive(pidFile: string): {
27
+ alive: boolean;
28
+ pid: number | null;
29
+ } {
7
30
  if (!existsSync(pidFile)) {
8
31
  return { alive: false, pid: null };
9
32
  }
@@ -26,7 +49,10 @@ export function isProcessAlive(pidFile: string): { alive: boolean; pid: number |
26
49
  * Stop a process by PID: SIGTERM, wait up to 2s, then SIGKILL if still alive.
27
50
  * Returns true if the process was stopped, false if it wasn't alive.
28
51
  */
29
- export async function stopProcess(pid: number, label: string): Promise<boolean> {
52
+ export async function stopProcess(
53
+ pid: number,
54
+ label: string,
55
+ ): Promise<boolean> {
30
56
  try {
31
57
  process.kill(pid, 0);
32
58
  } catch {
@@ -70,11 +96,35 @@ export async function stopProcessByPidFile(
70
96
 
71
97
  if (!alive || pid === null) {
72
98
  if (existsSync(pidFile)) {
73
- try { unlinkSync(pidFile); } catch {}
99
+ try {
100
+ unlinkSync(pidFile);
101
+ } catch {}
74
102
  }
75
103
  if (cleanupFiles) {
76
104
  for (const f of cleanupFiles) {
77
- try { unlinkSync(f); } catch {}
105
+ try {
106
+ unlinkSync(f);
107
+ } catch {}
108
+ }
109
+ }
110
+ return false;
111
+ }
112
+
113
+ // Verify the PID actually belongs to a vellum process before killing.
114
+ // If the PID file is stale and the OS reused the PID, skip the kill
115
+ // and clean up the stale files instead.
116
+ if (!isVellumProcess(pid)) {
117
+ console.log(
118
+ `PID ${pid} is not a vellum process — cleaning up stale ${label} PID file.`,
119
+ );
120
+ try {
121
+ unlinkSync(pidFile);
122
+ } catch {}
123
+ if (cleanupFiles) {
124
+ for (const f of cleanupFiles) {
125
+ try {
126
+ unlinkSync(f);
127
+ } catch {}
78
128
  }
79
129
  }
80
130
  return false;
@@ -82,10 +132,14 @@ export async function stopProcessByPidFile(
82
132
 
83
133
  const stopped = await stopProcess(pid, label);
84
134
 
85
- try { unlinkSync(pidFile); } catch {}
135
+ try {
136
+ unlinkSync(pidFile);
137
+ } catch {}
86
138
  if (cleanupFiles) {
87
139
  for (const f of cleanupFiles) {
88
- try { unlinkSync(f); } catch {}
140
+ try {
141
+ unlinkSync(f);
142
+ } catch {}
89
143
  }
90
144
  }
91
145