@vellumai/cli 0.7.1 → 0.7.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.
Files changed (39) hide show
  1. package/AGENTS.md +3 -11
  2. package/bun.lock +0 -15
  3. package/package.json +1 -6
  4. package/src/__tests__/backup.test.ts +121 -5
  5. package/src/__tests__/teleport.test.ts +515 -10
  6. package/src/commands/backup.ts +35 -2
  7. package/src/commands/client.ts +90 -7
  8. package/src/commands/exec.ts +13 -4
  9. package/src/commands/hatch.ts +1 -1
  10. package/src/commands/login.ts +11 -0
  11. package/src/commands/restore.ts +7 -1
  12. package/src/commands/rollback.ts +1 -1
  13. package/src/commands/setup.ts +38 -73
  14. package/src/commands/teleport.ts +122 -12
  15. package/src/commands/upgrade.ts +8 -2
  16. package/src/commands/wake.ts +5 -16
  17. package/src/components/DefaultMainScreen.tsx +42 -130
  18. package/src/index.ts +1 -7
  19. package/src/lib/__tests__/docker.test.ts +53 -35
  20. package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
  21. package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
  22. package/src/lib/__tests__/runtime-url.test.ts +39 -1
  23. package/src/lib/assistant-client.ts +13 -5
  24. package/src/lib/assistant-config.ts +0 -25
  25. package/src/lib/backup-ops.ts +43 -17
  26. package/src/lib/client-identity.ts +9 -5
  27. package/src/lib/docker.ts +6 -267
  28. package/src/lib/environments/paths.ts +20 -0
  29. package/src/lib/guardian-token.ts +56 -6
  30. package/src/lib/hatch-local.ts +3 -26
  31. package/src/lib/local-runtime-client.ts +82 -1
  32. package/src/lib/local.ts +9 -7
  33. package/src/lib/ngrok.ts +36 -26
  34. package/src/lib/platform-client.ts +100 -1
  35. package/src/lib/retire-local.ts +2 -2
  36. package/src/lib/runtime-url.ts +22 -0
  37. package/src/lib/statefulset.ts +375 -0
  38. package/src/lib/upgrade-lifecycle.ts +97 -1
  39. package/src/commands/pair.ts +0 -212
@@ -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,
@@ -14,7 +12,7 @@ import {
14
12
  import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
15
13
 
16
14
  import { removeAssistantEntry } from "../lib/assistant-config";
17
- import { getClientRegistrationHeaders } from "../lib/client-identity";
15
+
18
16
  import { SPECIES_CONFIG, type Species } from "../lib/constants";
19
17
  import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
20
18
  import { checkHealth } from "../lib/health-check";
@@ -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,17 +348,15 @@ 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()}`;
358
- const clientHeaders = getClientRegistrationHeaders();
359
- tuiLog.info("sse connect", { url, clientHeaders });
355
+ tuiLog.info("sse connect", { url, authHeaders: Object.keys(auth ?? {}) });
360
356
  const response = await fetch(url, {
361
357
  headers: {
362
358
  Accept: "text/event-stream",
363
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
364
- ...clientHeaders,
359
+ ...auth,
365
360
  },
366
361
  signal,
367
362
  });
@@ -457,7 +452,7 @@ async function handleConfirmationPrompt(
457
452
  requestId: string,
458
453
  confirmation: PendingConfirmation,
459
454
  chatApp: ChatAppHandle,
460
- bearerToken?: string,
455
+ auth?: Record<string, string>,
461
456
  ): Promise<void> {
462
457
  const preview = formatConfirmationPreview(
463
458
  confirmation.toolName,
@@ -483,7 +478,7 @@ async function handleConfirmationPrompt(
483
478
  const index = await chatApp.showSelection("Tool Approval", options);
484
479
 
485
480
  if (index === 0) {
486
- await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken);
481
+ await submitDecision(baseUrl, assistantId, requestId, "allow", auth);
487
482
  chatApp.addStatus("\u2714 Allowed", "green");
488
483
  return;
489
484
  }
@@ -495,7 +490,7 @@ async function handleConfirmationPrompt(
495
490
  confirmation,
496
491
  chatApp,
497
492
  "always_allow",
498
- bearerToken,
493
+ auth,
499
494
  );
500
495
  return;
501
496
  }
@@ -507,12 +502,12 @@ async function handleConfirmationPrompt(
507
502
  confirmation,
508
503
  chatApp,
509
504
  "always_deny",
510
- bearerToken,
505
+ auth,
511
506
  );
512
507
  return;
513
508
  }
514
509
 
515
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
510
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
516
511
  chatApp.addStatus("\u2718 Denied", "yellow");
517
512
  }
518
513
 
@@ -523,7 +518,7 @@ async function handlePatternSelection(
523
518
  confirmation: PendingConfirmation,
524
519
  chatApp: ChatAppHandle,
525
520
  trustDecision: TrustDecision,
526
- bearerToken?: string,
521
+ auth?: Record<string, string>,
527
522
  ): Promise<void> {
528
523
  const allowlistOptions = confirmation.allowlistOptions ?? [];
529
524
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -544,12 +539,12 @@ async function handlePatternSelection(
544
539
  chatApp,
545
540
  selectedPattern,
546
541
  trustDecision,
547
- bearerToken,
542
+ auth,
548
543
  );
549
544
  return;
550
545
  }
551
546
 
552
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
547
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
553
548
  chatApp.addStatus("\u2718 Denied", "yellow");
554
549
  }
555
550
 
@@ -561,7 +556,7 @@ async function handleScopeSelection(
561
556
  chatApp: ChatAppHandle,
562
557
  selectedPattern: string,
563
558
  trustDecision: TrustDecision,
564
- bearerToken?: string,
559
+ auth?: Record<string, string>,
565
560
  ): Promise<void> {
566
561
  const scopeOptions = confirmation.scopeOptions ?? [];
567
562
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -578,14 +573,14 @@ async function handleScopeSelection(
578
573
  selectedPattern,
579
574
  scopeOptions[index].scope,
580
575
  ruleDecision,
581
- bearerToken,
576
+ auth,
582
577
  );
583
578
  await submitDecision(
584
579
  baseUrl,
585
580
  assistantId,
586
581
  requestId,
587
582
  ruleDecision === "deny" ? "deny" : "allow",
588
- bearerToken,
583
+ auth,
589
584
  );
590
585
  const ruleLabel =
591
586
  trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
@@ -597,7 +592,7 @@ async function handleScopeSelection(
597
592
  return;
598
593
  }
599
594
 
600
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
595
+ await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
601
596
  chatApp.addStatus("\u2718 Denied", "yellow");
602
597
  }
603
598
 
@@ -753,10 +748,6 @@ function HelpDisplay(): ReactElement {
753
748
  {" /clear "}
754
749
  <Text dimColor>Clear the screen</Text>
755
750
  </Text>
756
- <Text>
757
- {" /pair "}
758
- <Text dimColor>Generate a QR code for mobile device pairing</Text>
759
- </Text>
760
751
  <Text>
761
752
  {" /help, ? "}
762
753
  <Text dimColor>Show this help</Text>
@@ -1368,7 +1359,9 @@ interface ChatAppProps {
1368
1359
  runtimeUrl: string;
1369
1360
  assistantId: string;
1370
1361
  species: Species;
1371
- bearerToken?: string;
1362
+ /** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
1363
+ * { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
1364
+ auth?: Record<string, string>;
1372
1365
  project?: string;
1373
1366
  zone?: string;
1374
1367
  onExit: () => void;
@@ -1379,7 +1372,7 @@ function ChatApp({
1379
1372
  runtimeUrl,
1380
1373
  assistantId,
1381
1374
  species,
1382
- bearerToken,
1375
+ auth,
1383
1376
  project,
1384
1377
  zone,
1385
1378
  onExit,
@@ -1692,7 +1685,7 @@ function ChatApp({
1692
1685
  const historyResponse = await pollMessages(
1693
1686
  runtimeUrl,
1694
1687
  assistantId,
1695
- bearerToken,
1688
+ auth,
1696
1689
  );
1697
1690
  h.hideSpinner();
1698
1691
  if (historyResponse.messages.length > 0) {
@@ -1717,7 +1710,7 @@ function ChatApp({
1717
1710
  assistantId,
1718
1711
  assistantId,
1719
1712
  sseAc.signal,
1720
- bearerToken,
1713
+ auth,
1721
1714
  )) {
1722
1715
  const hRef = handleRef_.current;
1723
1716
  if (!hRef) continue;
@@ -1788,7 +1781,7 @@ function ChatApp({
1788
1781
  event.persistentDecisionsAllowed,
1789
1782
  },
1790
1783
  hRef,
1791
- bearerToken,
1784
+ auth,
1792
1785
  );
1793
1786
  hRef.showSpinner("Working...");
1794
1787
  break;
@@ -1819,7 +1812,7 @@ function ChatApp({
1819
1812
  delivery,
1820
1813
  }),
1821
1814
  },
1822
- bearerToken,
1815
+ auth,
1823
1816
  );
1824
1817
  },
1825
1818
  );
@@ -1903,7 +1896,7 @@ function ChatApp({
1903
1896
  );
1904
1897
  return false;
1905
1898
  }
1906
- }, [runtimeUrl, assistantId, bearerToken]);
1899
+ }, [runtimeUrl, assistantId, auth]);
1907
1900
 
1908
1901
  const handleInput = useCallback(
1909
1902
  async (input: string): Promise<void> => {
@@ -2110,9 +2103,7 @@ function ChatApp({
2110
2103
  method: "POST",
2111
2104
  headers: {
2112
2105
  "Content-Type": "application/json",
2113
- ...(bearerToken
2114
- ? { Authorization: `Bearer ${bearerToken}` }
2115
- : {}),
2106
+ ...auth,
2116
2107
  },
2117
2108
  body: JSON.stringify({
2118
2109
  conversationKey: assistantId,
@@ -2171,85 +2162,6 @@ function ChatApp({
2171
2162
  return;
2172
2163
  }
2173
2164
 
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
2165
  if (busyRef.current) {
2254
2166
  // /btw is already handled above this block
2255
2167
  if (!trimmed.startsWith("/")) {
@@ -2278,7 +2190,7 @@ function ChatApp({
2278
2190
  assistantId,
2279
2191
  trimmed,
2280
2192
  controller.signal,
2281
- bearerToken,
2193
+ auth,
2282
2194
  );
2283
2195
  clearTimeout(timeoutId);
2284
2196
  if (sendResult.accepted) {
@@ -2329,7 +2241,7 @@ function ChatApp({
2329
2241
  assistantId,
2330
2242
  trimmed,
2331
2243
  controller.signal,
2332
- bearerToken,
2244
+ auth,
2333
2245
  );
2334
2246
  clearTimeout(timeoutId);
2335
2247
  if (!sendResult.accepted) {
@@ -2356,7 +2268,7 @@ function ChatApp({
2356
2268
  [
2357
2269
  runtimeUrl,
2358
2270
  assistantId,
2359
- bearerToken,
2271
+ auth,
2360
2272
  project,
2361
2273
  zone,
2362
2274
  cleanup,
@@ -2670,7 +2582,7 @@ export function renderChatApp(
2670
2582
  assistantId: string,
2671
2583
  species: Species,
2672
2584
  onExit: () => void,
2673
- options?: { bearerToken?: string; project?: string; zone?: string },
2585
+ options?: { auth?: Record<string, string>; project?: string; zone?: string },
2674
2586
  ): ChatAppInstance {
2675
2587
  let chatHandle: ChatAppHandle | null = null;
2676
2588
 
@@ -2679,7 +2591,7 @@ export function renderChatApp(
2679
2591
  runtimeUrl={runtimeUrl}
2680
2592
  assistantId={assistantId}
2681
2593
  species={species}
2682
- bearerToken={options?.bearerToken}
2594
+ auth={options?.auth}
2683
2595
  project={options?.project}
2684
2596
  zone={options?.zone}
2685
2597
  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
  );
@@ -4,9 +4,9 @@ import {
4
4
  AVATAR_DEVICE_ENV_VAR,
5
5
  dockerResourceNames,
6
6
  resolveAvatarDevicePath,
7
- serviceDockerRunArgs,
8
7
  type ServiceName,
9
8
  } from "../docker.js";
9
+ import { buildServiceRunArgs } from "../statefulset.js";
10
10
 
11
11
  const instanceName = "test-instance";
12
12
  const imageTags: Record<ServiceName, string> = {
@@ -16,10 +16,10 @@ const imageTags: Record<ServiceName, string> = {
16
16
  };
17
17
 
18
18
  function buildAssistantArgs(
19
- overrides: Partial<Parameters<typeof serviceDockerRunArgs>[0]> = {},
19
+ overrides: Partial<Parameters<typeof buildServiceRunArgs>[0]> = {},
20
20
  ): string[] {
21
21
  const res = dockerResourceNames(instanceName);
22
- const builders = serviceDockerRunArgs({
22
+ const builders = buildServiceRunArgs({
23
23
  gatewayPort: 7830,
24
24
  imageTags,
25
25
  instanceName,
@@ -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 buildServiceRunArgs>[0]> = {},
34
+ ): string[] {
35
+ const res = dockerResourceNames(instanceName);
36
+ const builders = buildServiceRunArgs({
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("buildServiceRunArgs 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("buildServiceRunArgs — 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