@vellumai/cli 0.7.1 → 0.7.2

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,8 +1,6 @@
1
1
  import { spawn } from "child_process";
2
- import { createHash, randomBytes, randomUUID } from "crypto";
3
- import { hostname, userInfo } from "os";
2
+ import { randomUUID } from "crypto";
4
3
  import { basename } from "path";
5
- import qrcode from "qrcode-terminal";
6
4
  import {
7
5
  useCallback,
8
6
  useEffect,
@@ -46,7 +44,6 @@ export const SLASH_COMMANDS = [
46
44
  "/doctor",
47
45
  "/exit",
48
46
  "/help",
49
- "/pair",
50
47
  "/q",
51
48
  "/quit",
52
49
  "/retire",
@@ -173,14 +170,14 @@ async function runtimeRequest<T>(
173
170
  assistantId: string,
174
171
  path: string,
175
172
  init?: RequestInit,
176
- bearerToken?: string,
173
+ auth?: Record<string, string>,
177
174
  ): Promise<T> {
178
175
  const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
179
176
  const response = await fetch(url, {
180
177
  ...init,
181
178
  headers: {
182
179
  "Content-Type": "application/json",
183
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
180
+ ...auth,
184
181
  ...(init?.headers as Record<string, string> | undefined),
185
182
  },
186
183
  });
@@ -219,7 +216,7 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
219
216
  async function pollMessages(
220
217
  baseUrl: string,
221
218
  assistantId: string,
222
- bearerToken?: string,
219
+ auth?: Record<string, string>,
223
220
  ): Promise<ListMessagesResponse> {
224
221
  const params = new URLSearchParams({ conversationKey: assistantId });
225
222
  return runtimeRequest<ListMessagesResponse>(
@@ -227,7 +224,7 @@ async function pollMessages(
227
224
  assistantId,
228
225
  `/messages?${params.toString()}`,
229
226
  undefined,
230
- bearerToken,
227
+ auth,
231
228
  );
232
229
  }
233
230
 
@@ -236,7 +233,7 @@ async function sendMessage(
236
233
  assistantId: string,
237
234
  content: string,
238
235
  signal?: AbortSignal,
239
- bearerToken?: string,
236
+ auth?: Record<string, string>,
240
237
  ): Promise<SendMessageResponse> {
241
238
  return runtimeRequest<SendMessageResponse>(
242
239
  baseUrl,
@@ -252,7 +249,7 @@ async function sendMessage(
252
249
  }),
253
250
  signal,
254
251
  },
255
- bearerToken,
252
+ auth,
256
253
  );
257
254
  }
258
255
 
@@ -261,7 +258,7 @@ async function submitDecision(
261
258
  assistantId: string,
262
259
  requestId: string,
263
260
  decision: "allow" | "deny",
264
- bearerToken?: string,
261
+ auth?: Record<string, string>,
265
262
  ): Promise<SubmitDecisionResponse> {
266
263
  return runtimeRequest<SubmitDecisionResponse>(
267
264
  baseUrl,
@@ -271,7 +268,7 @@ async function submitDecision(
271
268
  method: "POST",
272
269
  body: JSON.stringify({ requestId, decision }),
273
270
  },
274
- bearerToken,
271
+ auth,
275
272
  );
276
273
  }
277
274
 
@@ -282,7 +279,7 @@ async function addTrustRule(
282
279
  pattern: string,
283
280
  scope: string,
284
281
  decision: "allow" | "deny",
285
- bearerToken?: string,
282
+ auth?: Record<string, string>,
286
283
  ): Promise<AddTrustRuleResponse> {
287
284
  return runtimeRequest<AddTrustRuleResponse>(
288
285
  baseUrl,
@@ -292,7 +289,7 @@ async function addTrustRule(
292
289
  method: "POST",
293
290
  body: JSON.stringify({ requestId, pattern, scope, decision }),
294
291
  },
295
- bearerToken,
292
+ auth,
296
293
  );
297
294
  }
298
295
 
@@ -351,7 +348,7 @@ async function* streamEvents(
351
348
  assistantId: string,
352
349
  conversationKey: string,
353
350
  signal: AbortSignal,
354
- bearerToken?: string,
351
+ auth?: Record<string, string>,
355
352
  ): AsyncGenerator<SseEvent> {
356
353
  const params = new URLSearchParams({ conversationKey });
357
354
  const url = `${baseUrl}/v1/assistants/${assistantId}/events?${params.toString()}`;
@@ -360,7 +357,7 @@ async function* streamEvents(
360
357
  const response = await fetch(url, {
361
358
  headers: {
362
359
  Accept: "text/event-stream",
363
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
360
+ ...auth,
364
361
  ...clientHeaders,
365
362
  },
366
363
  signal,
@@ -457,7 +454,7 @@ async function handleConfirmationPrompt(
457
454
  requestId: string,
458
455
  confirmation: PendingConfirmation,
459
456
  chatApp: ChatAppHandle,
460
- bearerToken?: string,
457
+ auth?: Record<string, string>,
461
458
  ): Promise<void> {
462
459
  const preview = formatConfirmationPreview(
463
460
  confirmation.toolName,
@@ -483,7 +480,7 @@ async function handleConfirmationPrompt(
483
480
  const index = await chatApp.showSelection("Tool Approval", options);
484
481
 
485
482
  if (index === 0) {
486
- await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken);
483
+ await submitDecision(baseUrl, assistantId, requestId, "allow", auth);
487
484
  chatApp.addStatus("\u2714 Allowed", "green");
488
485
  return;
489
486
  }
@@ -495,7 +492,7 @@ async function handleConfirmationPrompt(
495
492
  confirmation,
496
493
  chatApp,
497
494
  "always_allow",
498
- bearerToken,
495
+ auth,
499
496
  );
500
497
  return;
501
498
  }
@@ -507,12 +504,12 @@ async function handleConfirmationPrompt(
507
504
  confirmation,
508
505
  chatApp,
509
506
  "always_deny",
510
- bearerToken,
507
+ auth,
511
508
  );
512
509
  return;
513
510
  }
514
511
 
515
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
512
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
516
513
  chatApp.addStatus("\u2718 Denied", "yellow");
517
514
  }
518
515
 
@@ -523,7 +520,7 @@ async function handlePatternSelection(
523
520
  confirmation: PendingConfirmation,
524
521
  chatApp: ChatAppHandle,
525
522
  trustDecision: TrustDecision,
526
- bearerToken?: string,
523
+ auth?: Record<string, string>,
527
524
  ): Promise<void> {
528
525
  const allowlistOptions = confirmation.allowlistOptions ?? [];
529
526
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -544,12 +541,12 @@ async function handlePatternSelection(
544
541
  chatApp,
545
542
  selectedPattern,
546
543
  trustDecision,
547
- bearerToken,
544
+ auth,
548
545
  );
549
546
  return;
550
547
  }
551
548
 
552
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
549
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
553
550
  chatApp.addStatus("\u2718 Denied", "yellow");
554
551
  }
555
552
 
@@ -561,7 +558,7 @@ async function handleScopeSelection(
561
558
  chatApp: ChatAppHandle,
562
559
  selectedPattern: string,
563
560
  trustDecision: TrustDecision,
564
- bearerToken?: string,
561
+ auth?: Record<string, string>,
565
562
  ): Promise<void> {
566
563
  const scopeOptions = confirmation.scopeOptions ?? [];
567
564
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -578,14 +575,14 @@ async function handleScopeSelection(
578
575
  selectedPattern,
579
576
  scopeOptions[index].scope,
580
577
  ruleDecision,
581
- bearerToken,
578
+ auth,
582
579
  );
583
580
  await submitDecision(
584
581
  baseUrl,
585
582
  assistantId,
586
583
  requestId,
587
584
  ruleDecision === "deny" ? "deny" : "allow",
588
- bearerToken,
585
+ auth,
589
586
  );
590
587
  const ruleLabel =
591
588
  trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
@@ -597,7 +594,7 @@ async function handleScopeSelection(
597
594
  return;
598
595
  }
599
596
 
600
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
597
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
601
598
  chatApp.addStatus("\u2718 Denied", "yellow");
602
599
  }
603
600
 
@@ -753,10 +750,6 @@ function HelpDisplay(): ReactElement {
753
750
  {" /clear "}
754
751
  <Text dimColor>Clear the screen</Text>
755
752
  </Text>
756
- <Text>
757
- {" /pair "}
758
- <Text dimColor>Generate a QR code for mobile device pairing</Text>
759
- </Text>
760
753
  <Text>
761
754
  {" /help, ? "}
762
755
  <Text dimColor>Show this help</Text>
@@ -1368,7 +1361,9 @@ interface ChatAppProps {
1368
1361
  runtimeUrl: string;
1369
1362
  assistantId: string;
1370
1363
  species: Species;
1371
- bearerToken?: string;
1364
+ /** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
1365
+ * { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
1366
+ auth?: Record<string, string>;
1372
1367
  project?: string;
1373
1368
  zone?: string;
1374
1369
  onExit: () => void;
@@ -1379,7 +1374,7 @@ function ChatApp({
1379
1374
  runtimeUrl,
1380
1375
  assistantId,
1381
1376
  species,
1382
- bearerToken,
1377
+ auth,
1383
1378
  project,
1384
1379
  zone,
1385
1380
  onExit,
@@ -1692,7 +1687,7 @@ function ChatApp({
1692
1687
  const historyResponse = await pollMessages(
1693
1688
  runtimeUrl,
1694
1689
  assistantId,
1695
- bearerToken,
1690
+ auth,
1696
1691
  );
1697
1692
  h.hideSpinner();
1698
1693
  if (historyResponse.messages.length > 0) {
@@ -1717,7 +1712,7 @@ function ChatApp({
1717
1712
  assistantId,
1718
1713
  assistantId,
1719
1714
  sseAc.signal,
1720
- bearerToken,
1715
+ auth,
1721
1716
  )) {
1722
1717
  const hRef = handleRef_.current;
1723
1718
  if (!hRef) continue;
@@ -1788,7 +1783,7 @@ function ChatApp({
1788
1783
  event.persistentDecisionsAllowed,
1789
1784
  },
1790
1785
  hRef,
1791
- bearerToken,
1786
+ auth,
1792
1787
  );
1793
1788
  hRef.showSpinner("Working...");
1794
1789
  break;
@@ -1819,7 +1814,7 @@ function ChatApp({
1819
1814
  delivery,
1820
1815
  }),
1821
1816
  },
1822
- bearerToken,
1817
+ auth,
1823
1818
  );
1824
1819
  },
1825
1820
  );
@@ -1903,7 +1898,7 @@ function ChatApp({
1903
1898
  );
1904
1899
  return false;
1905
1900
  }
1906
- }, [runtimeUrl, assistantId, bearerToken]);
1901
+ }, [runtimeUrl, assistantId, auth]);
1907
1902
 
1908
1903
  const handleInput = useCallback(
1909
1904
  async (input: string): Promise<void> => {
@@ -2110,9 +2105,7 @@ function ChatApp({
2110
2105
  method: "POST",
2111
2106
  headers: {
2112
2107
  "Content-Type": "application/json",
2113
- ...(bearerToken
2114
- ? { Authorization: `Bearer ${bearerToken}` }
2115
- : {}),
2108
+ ...auth,
2116
2109
  },
2117
2110
  body: JSON.stringify({
2118
2111
  conversationKey: assistantId,
@@ -2171,85 +2164,6 @@ function ChatApp({
2171
2164
  return;
2172
2165
  }
2173
2166
 
2174
- if (trimmed === "/pair") {
2175
- h.showSpinner("Generating pairing credentials...");
2176
-
2177
- const isConnected = await ensureConnected();
2178
- if (!isConnected) {
2179
- h.hideSpinner();
2180
- h.showError("Cannot pair — not connected to the assistant runtime.");
2181
- return;
2182
- }
2183
-
2184
- try {
2185
- const pairingRequestId = randomUUID();
2186
- const pairingSecret = randomBytes(32).toString("hex");
2187
- const gatewayUrl = runtimeUrl;
2188
-
2189
- // Call /pairing/register on the gateway (dedicated pairing proxy route)
2190
- const registerUrl = `${runtimeUrl}/pairing/register`;
2191
- const registerRes = await fetch(registerUrl, {
2192
- method: "POST",
2193
- headers: {
2194
- "Content-Type": "application/json",
2195
- ...(bearerToken
2196
- ? { Authorization: `Bearer ${bearerToken}` }
2197
- : {}),
2198
- },
2199
- body: JSON.stringify({
2200
- pairingRequestId,
2201
- pairingSecret,
2202
- gatewayUrl,
2203
- }),
2204
- });
2205
-
2206
- if (!registerRes.ok) {
2207
- const body = await registerRes.text().catch(() => "");
2208
- throw new Error(
2209
- `HTTP ${registerRes.status}: ${body || registerRes.statusText}`,
2210
- );
2211
- }
2212
-
2213
- let username: string;
2214
- try {
2215
- username = userInfo().username;
2216
- } catch {
2217
- username = "";
2218
- }
2219
- const hostId = createHash("sha256")
2220
- .update(hostname() + username)
2221
- .digest("hex");
2222
- const payload = JSON.stringify({
2223
- type: "vellum-assistant",
2224
- v: 4,
2225
- id: hostId,
2226
- g: gatewayUrl,
2227
- pairingRequestId,
2228
- pairingSecret,
2229
- });
2230
-
2231
- const qrString = await new Promise<string>((resolve) => {
2232
- qrcode.generate(payload, { small: true }, (code: string) => {
2233
- resolve(code);
2234
- });
2235
- });
2236
-
2237
- h.hideSpinner();
2238
- h.addStatus(
2239
- `Pairing Ready\n\n` +
2240
- `Scan this QR code with the Vellum iOS app:\n\n` +
2241
- `${qrString}\n` +
2242
- `This pairing request expires in 5 minutes. Run /pair again to generate a new one.`,
2243
- );
2244
- } catch (err) {
2245
- h.hideSpinner();
2246
- h.showError(
2247
- `Pairing failed: ${err instanceof Error ? err.message : err}`,
2248
- );
2249
- }
2250
- return;
2251
- }
2252
-
2253
2167
  if (busyRef.current) {
2254
2168
  // /btw is already handled above this block
2255
2169
  if (!trimmed.startsWith("/")) {
@@ -2278,7 +2192,7 @@ function ChatApp({
2278
2192
  assistantId,
2279
2193
  trimmed,
2280
2194
  controller.signal,
2281
- bearerToken,
2195
+ auth,
2282
2196
  );
2283
2197
  clearTimeout(timeoutId);
2284
2198
  if (sendResult.accepted) {
@@ -2329,7 +2243,7 @@ function ChatApp({
2329
2243
  assistantId,
2330
2244
  trimmed,
2331
2245
  controller.signal,
2332
- bearerToken,
2246
+ auth,
2333
2247
  );
2334
2248
  clearTimeout(timeoutId);
2335
2249
  if (!sendResult.accepted) {
@@ -2356,7 +2270,7 @@ function ChatApp({
2356
2270
  [
2357
2271
  runtimeUrl,
2358
2272
  assistantId,
2359
- bearerToken,
2273
+ auth,
2360
2274
  project,
2361
2275
  zone,
2362
2276
  cleanup,
@@ -2670,7 +2584,7 @@ export function renderChatApp(
2670
2584
  assistantId: string,
2671
2585
  species: Species,
2672
2586
  onExit: () => void,
2673
- options?: { bearerToken?: string; project?: string; zone?: string },
2587
+ options?: { auth?: Record<string, string>; project?: string; zone?: string },
2674
2588
  ): ChatAppInstance {
2675
2589
  let chatHandle: ChatAppHandle | null = null;
2676
2590
 
@@ -2679,7 +2593,7 @@ export function renderChatApp(
2679
2593
  runtimeUrl={runtimeUrl}
2680
2594
  assistantId={assistantId}
2681
2595
  species={species}
2682
- bearerToken={options?.bearerToken}
2596
+ auth={options?.auth}
2683
2597
  project={options?.project}
2684
2598
  zone={options?.zone}
2685
2599
  onExit={onExit}
package/src/index.ts CHANGED
@@ -11,7 +11,6 @@ import { hatch } from "./commands/hatch";
11
11
  import { login, logout, whoami } from "./commands/login";
12
12
  import { logs } from "./commands/logs";
13
13
  import { message } from "./commands/message";
14
- import { pair } from "./commands/pair";
15
14
  import { ps } from "./commands/ps";
16
15
  import { recover } from "./commands/recover";
17
16
  import { restore } from "./commands/restore";
@@ -26,10 +25,7 @@ import { tunnel } from "./commands/tunnel";
26
25
  import { upgrade } from "./commands/upgrade";
27
26
  import { use } from "./commands/use";
28
27
  import { wake } from "./commands/wake";
29
- import {
30
- resolveAssistant,
31
- setActiveAssistant,
32
- } from "./lib/assistant-config";
28
+ import { resolveAssistant, setActiveAssistant } from "./lib/assistant-config";
33
29
  import { loadGuardianToken } from "./lib/guardian-token";
34
30
  import { checkHealth } from "./lib/health-check";
35
31
 
@@ -45,7 +41,6 @@ const commands = {
45
41
  logout,
46
42
  logs,
47
43
  message,
48
- pair,
49
44
  ps,
50
45
  recover,
51
46
  restore,
@@ -80,7 +75,6 @@ function printHelp(): void {
80
75
  console.log(" login Log in to the Vellum platform");
81
76
  console.log(" logout Log out of the Vellum platform");
82
77
  console.log(" message Send a message to a running assistant");
83
- console.log(" pair Pair with a remote assistant via QR code");
84
78
  console.log(
85
79
  " ps List assistants (or processes for a specific assistant)",
86
80
  );
@@ -29,50 +29,41 @@ function buildAssistantArgs(
29
29
  return builders.assistant();
30
30
  }
31
31
 
32
- describe("serviceDockerRunArgs — assistant", () => {
33
- test("grants the minimum capability set needed for DinD (SYS_ADMIN + NET_ADMIN) rather than --privileged", () => {
34
- const args = buildAssistantArgs();
35
- expect(args).not.toContain("--privileged");
36
- // --cap-add SYS_ADMIN and --cap-add NET_ADMIN are each passed as two
37
- // adjacent args: "--cap-add" followed by the capability name.
38
- const sysAdminIdx = args.indexOf("SYS_ADMIN");
39
- expect(sysAdminIdx).toBeGreaterThan(0);
40
- expect(args[sysAdminIdx - 1]).toBe("--cap-add");
41
- const netAdminIdx = args.indexOf("NET_ADMIN");
42
- expect(netAdminIdx).toBeGreaterThan(0);
43
- expect(args[netAdminIdx - 1]).toBe("--cap-add");
32
+ function buildGatewayArgs(
33
+ overrides: Partial<Parameters<typeof serviceDockerRunArgs>[0]> = {},
34
+ ): string[] {
35
+ const res = dockerResourceNames(instanceName);
36
+ const builders = serviceDockerRunArgs({
37
+ gatewayPort: 7830,
38
+ imageTags,
39
+ instanceName,
40
+ res,
41
+ ...overrides,
44
42
  });
43
+ return builders.gateway();
44
+ }
45
45
 
46
- test("disables the default seccomp and AppArmor profiles so the inner dockerd can mount overlayfs and run pivot_root", () => {
46
+ describe("serviceDockerRunArgs assistant", () => {
47
+ test("does not grant elevated capabilities or disable security profiles", () => {
47
48
  const args = buildAssistantArgs();
48
- const seccompIdx = args.indexOf("seccomp=unconfined");
49
- expect(seccompIdx).toBeGreaterThan(0);
50
- expect(args[seccompIdx - 1]).toBe("--security-opt");
51
- const apparmorIdx = args.indexOf("apparmor=unconfined");
52
- expect(apparmorIdx).toBeGreaterThan(0);
53
- expect(args[apparmorIdx - 1]).toBe("--security-opt");
49
+ expect(args).not.toContain("--privileged");
50
+ expect(args).not.toContain("--cap-add");
51
+ expect(args).not.toContain("SYS_ADMIN");
52
+ expect(args).not.toContain("NET_ADMIN");
53
+ expect(args).not.toContain("seccomp=unconfined");
54
+ expect(args).not.toContain("apparmor=unconfined");
54
55
  });
55
56
 
56
- test("mounts a dedicated named volume at /var/lib/docker for the inner dockerd data store", () => {
57
+ test("does not mount a dockerd data volume", () => {
57
58
  const args = buildAssistantArgs();
58
- const spec = `${instanceName}-dockerd-data:/var/lib/docker`;
59
- const mountIndex = args.indexOf(spec);
60
- expect(mountIndex).toBeGreaterThan(0);
61
- expect(args[mountIndex - 1]).toBe("-v");
59
+ expect(args.some((a) => a.includes("/var/lib/docker"))).toBe(false);
62
60
  });
63
61
 
64
- test("does NOT bind-mount the host Docker socket (DinD replaces host-socket access)", () => {
62
+ test("does NOT bind-mount the host Docker socket", () => {
65
63
  const args = buildAssistantArgs();
66
64
  expect(args).not.toContain("/var/run/docker.sock:/var/run/docker.sock");
67
65
  });
68
66
 
69
- test("does NOT set VELLUM_WORKSPACE_VOLUME_NAME (legacy Phase 1.8 hint, no longer needed in DinD)", () => {
70
- const args = buildAssistantArgs();
71
- expect(
72
- args.some((a) => a.startsWith("VELLUM_WORKSPACE_VOLUME_NAME=")),
73
- ).toBe(false);
74
- });
75
-
76
67
  test("keeps existing workspace and socket volume mounts intact", () => {
77
68
  const args = buildAssistantArgs();
78
69
  expect(args).toContain(`${instanceName}-workspace:/workspace`);
@@ -112,6 +103,33 @@ describe("serviceDockerRunArgs — assistant", () => {
112
103
  });
113
104
  });
114
105
 
106
+ describe("serviceDockerRunArgs — gateway", () => {
107
+ const savedVelayBaseUrl = process.env.VELAY_BASE_URL;
108
+
109
+ beforeEach(() => {
110
+ delete process.env.VELAY_BASE_URL;
111
+ });
112
+
113
+ afterEach(() => {
114
+ if (savedVelayBaseUrl === undefined) delete process.env.VELAY_BASE_URL;
115
+ else process.env.VELAY_BASE_URL = savedVelayBaseUrl;
116
+ });
117
+
118
+ test("passes VELAY_BASE_URL into the gateway container when set", () => {
119
+ process.env.VELAY_BASE_URL = "http://host.docker.internal:8501";
120
+
121
+ expect(buildGatewayArgs()).toContain(
122
+ "VELAY_BASE_URL=http://host.docker.internal:8501",
123
+ );
124
+ });
125
+
126
+ test("omits VELAY_BASE_URL from gateway args when unset", () => {
127
+ expect(
128
+ buildGatewayArgs().some((arg) => arg.startsWith("VELAY_BASE_URL=")),
129
+ ).toBe(false);
130
+ });
131
+ });
132
+
115
133
  describe("VELLUM_AVATAR_DEVICE passthrough", () => {
116
134
  const savedValue = process.env[AVATAR_DEVICE_ENV_VAR];
117
135