@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 +1 -1
- package/src/components/DefaultMainScreen.tsx +42 -39
- package/src/lib/process.ts +60 -6
package/package.json
CHANGED
|
@@ -135,15 +135,18 @@ async function runtimeRequest<T>(
|
|
|
135
135
|
path: string,
|
|
136
136
|
init?: RequestInit,
|
|
137
137
|
bearerToken?: string,
|
|
138
|
-
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
211
|
-
if (typeof
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
401
|
+
accessToken,
|
|
399
402
|
);
|
|
400
403
|
return;
|
|
401
404
|
}
|
|
402
405
|
|
|
403
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken,
|
|
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
|
-
|
|
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
|
-
|
|
437
|
+
accessToken,
|
|
435
438
|
);
|
|
436
439
|
return;
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 (!
|
|
1311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1715
|
+
accessTokenRef.current,
|
|
1713
1716
|
);
|
|
1714
1717
|
for (const msg of pollResult.messages) {
|
|
1715
1718
|
if (!seenMessageIdsRef.current.has(msg.id)) {
|
package/src/lib/process.ts
CHANGED
|
@@ -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): {
|
|
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(
|
|
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 {
|
|
99
|
+
try {
|
|
100
|
+
unlinkSync(pidFile);
|
|
101
|
+
} catch {}
|
|
74
102
|
}
|
|
75
103
|
if (cleanupFiles) {
|
|
76
104
|
for (const f of cleanupFiles) {
|
|
77
|
-
try {
|
|
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 {
|
|
135
|
+
try {
|
|
136
|
+
unlinkSync(pidFile);
|
|
137
|
+
} catch {}
|
|
86
138
|
if (cleanupFiles) {
|
|
87
139
|
for (const f of cleanupFiles) {
|
|
88
|
-
try {
|
|
140
|
+
try {
|
|
141
|
+
unlinkSync(f);
|
|
142
|
+
} catch {}
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
|