@vellumai/cli 0.8.12-dev.202606131247.4d64b75 → 0.8.12-dev.202606131450.c6f2ffd

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.8.12-dev.202606131247.4d64b75",
3
+ "version": "0.8.12-dev.202606131450.c6f2ffd",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -773,15 +773,12 @@ describe("resolveOrHatchTarget", () => {
773
773
  });
774
774
 
775
775
  const result = await resolveOrHatchTarget("docker", "new-one");
776
- expect(hatchDockerMock).toHaveBeenCalledWith(
777
- "vellum",
778
- false,
779
- "new-one",
780
- false,
781
- {},
782
- {},
783
- { setupProviderCredentials: false },
784
- );
776
+ expect(hatchDockerMock).toHaveBeenCalledWith({
777
+ species: "vellum",
778
+ detached: false,
779
+ name: "new-one",
780
+ setupProviderCredentials: false,
781
+ });
785
782
  expect(result).toBe(newEntry);
786
783
  });
787
784
 
@@ -182,6 +182,9 @@ interface HatchArgs {
182
182
  flagEnvVars: Record<string, string>;
183
183
  analyze: boolean;
184
184
  disablePlatform: boolean;
185
+ netnsContainer: string | null;
186
+ gatewayPort: number | null;
187
+ assistantCaCert: string | null;
185
188
  }
186
189
 
187
190
  function parseArgs(): HatchArgs {
@@ -189,8 +192,10 @@ function parseArgs(): HatchArgs {
189
192
  process.argv.slice(3),
190
193
  );
191
194
  const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
192
- const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
193
- let disablePlatform = disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
195
+ const disablePlatformAmbient =
196
+ process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
197
+ let disablePlatform =
198
+ disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
194
199
  let species: Species = DEFAULT_SPECIES;
195
200
  let detached = false;
196
201
  let keepAlive = false;
@@ -200,6 +205,9 @@ function parseArgs(): HatchArgs {
200
205
  let sourcePath: string | null = null;
201
206
  const configValues: Record<string, string> = {};
202
207
  let analyze = false;
208
+ let netnsContainer: string | null = null;
209
+ let gatewayPort: number | null = null;
210
+ let assistantCaCert: string | null = null;
203
211
 
204
212
  for (let i = 0; i < args.length; i++) {
205
213
  const arg = args[i];
@@ -239,6 +247,15 @@ function parseArgs(): HatchArgs {
239
247
  console.log(
240
248
  " --disable-platform Suppress all outbound platform API calls",
241
249
  );
250
+ console.log(
251
+ " --netns-container <name> Join an existing container's network namespace (docker target only) instead of creating a per-instance network. The namespace owner publishes host ports, so --gateway-port is required.",
252
+ );
253
+ console.log(
254
+ " --gateway-port <port> Use an explicit host port for the gateway runtime URL instead of auto-allocating. Required with --netns-container.",
255
+ );
256
+ console.log(
257
+ " --assistant-ca-cert <path> Trust an extra PEM CA bundle in the assistant container (NODE_EXTRA_CA_CERTS) from process start. Useful behind a TLS-terminating egress proxy.",
258
+ );
242
259
  process.exit(0);
243
260
  } else if (arg === "-d") {
244
261
  detached = true;
@@ -301,11 +318,38 @@ function parseArgs(): HatchArgs {
301
318
  i++;
302
319
  } else if (arg === "--disable-platform") {
303
320
  disablePlatform = true;
321
+ } else if (arg === "--netns-container") {
322
+ const next = args[i + 1];
323
+ if (!next || next.startsWith("-")) {
324
+ console.error("Error: --netns-container requires a container name");
325
+ process.exit(1);
326
+ }
327
+ netnsContainer = next;
328
+ i++;
329
+ } else if (arg === "--gateway-port") {
330
+ const next = args[i + 1];
331
+ const parsed = next ? Number(next) : NaN;
332
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
333
+ console.error(
334
+ "Error: --gateway-port requires an integer port in 1-65535",
335
+ );
336
+ process.exit(1);
337
+ }
338
+ gatewayPort = parsed;
339
+ i++;
340
+ } else if (arg === "--assistant-ca-cert") {
341
+ const next = args[i + 1];
342
+ if (!next || next.startsWith("-")) {
343
+ console.error("Error: --assistant-ca-cert requires a path argument");
344
+ process.exit(1);
345
+ }
346
+ assistantCaCert = next;
347
+ i++;
304
348
  } else if (VALID_SPECIES.includes(arg as Species)) {
305
349
  species = arg as Species;
306
350
  } else {
307
351
  console.error(
308
- `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>, --flag <key=value>, --analyze, --disable-platform`,
352
+ `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>, --flag <key=value>, --analyze, --disable-platform, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
309
353
  );
310
354
  process.exit(1);
311
355
  }
@@ -323,6 +367,9 @@ function parseArgs(): HatchArgs {
323
367
  flagEnvVars,
324
368
  analyze,
325
369
  disablePlatform,
370
+ netnsContainer,
371
+ gatewayPort,
372
+ assistantCaCert,
326
373
  };
327
374
  }
328
375
 
@@ -559,6 +606,9 @@ export async function hatch(): Promise<void> {
559
606
  flagEnvVars,
560
607
  analyze,
561
608
  disablePlatform,
609
+ netnsContainer,
610
+ gatewayPort,
611
+ assistantCaCert,
562
612
  } = parseArgs();
563
613
 
564
614
  if (disablePlatform) {
@@ -580,6 +630,25 @@ export async function hatch(): Promise<void> {
580
630
  process.exit(1);
581
631
  }
582
632
 
633
+ if (
634
+ (netnsContainer !== null ||
635
+ gatewayPort !== null ||
636
+ assistantCaCert !== null) &&
637
+ remote !== "docker"
638
+ ) {
639
+ console.error(
640
+ "Error: --netns-container, --gateway-port, and --assistant-ca-cert are only supported for docker hatch targets.",
641
+ );
642
+ process.exit(1);
643
+ }
644
+
645
+ if (netnsContainer !== null && gatewayPort === null) {
646
+ console.error(
647
+ "Error: --gateway-port is required with --netns-container (the namespace owner publishes the port before hatch runs).",
648
+ );
649
+ process.exit(1);
650
+ }
651
+
583
652
  if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
584
653
  console.error(
585
654
  `Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
@@ -591,14 +660,30 @@ export async function hatch(): Promise<void> {
591
660
  }
592
661
 
593
662
  if (remote === "local") {
594
- await hatchLocal(species, name, watch, keepAlive, configValues, flagEnvVars);
663
+ await hatchLocal(
664
+ species,
665
+ name,
666
+ watch,
667
+ keepAlive,
668
+ configValues,
669
+ flagEnvVars,
670
+ );
595
671
  return;
596
672
  }
597
673
 
598
674
  if (remote === "docker") {
599
- await hatchDocker(species, detached, name, watch, configValues, flagEnvVars, {
675
+ await hatchDocker({
676
+ species,
677
+ detached,
678
+ name,
679
+ watch,
680
+ configValues,
681
+ flagEnvVars,
600
682
  sourcePath,
601
683
  analyze,
684
+ netnsContainer: netnsContainer ?? undefined,
685
+ gatewayPort: gatewayPort ?? undefined,
686
+ assistantCaCertPath: assistantCaCert ?? undefined,
602
687
  });
603
688
  return;
604
689
  }
@@ -391,9 +391,8 @@ async function exportFromAssistant(
391
391
  // daemon, the CLI is updated separately).
392
392
  let sourceRuntimeVersion: string;
393
393
  try {
394
- const identity = await callRuntimeWithAuthRetry(
395
- entry,
396
- async (token) => localRuntimeIdentity(entry, token),
394
+ const identity = await callRuntimeWithAuthRetry(entry, async (token) =>
395
+ localRuntimeIdentity(entry, token),
397
396
  );
398
397
  sourceRuntimeVersion = identity.version;
399
398
  } catch (err) {
@@ -427,16 +426,13 @@ async function exportFromAssistant(
427
426
  let jobId: string;
428
427
  let accessToken: string;
429
428
  try {
430
- const result = await callRuntimeWithAuthRetry(
431
- entry,
432
- async (token) => {
433
- const r = await localRuntimeExportToGcs(entry, token, {
434
- uploadUrl,
435
- description: "teleport export",
436
- });
437
- return { jobId: r.jobId, token };
438
- },
439
- );
429
+ const result = await callRuntimeWithAuthRetry(entry, async (token) => {
430
+ const r = await localRuntimeExportToGcs(entry, token, {
431
+ uploadUrl,
432
+ description: "teleport export",
433
+ });
434
+ return { jobId: r.jobId, token };
435
+ });
440
436
  jobId = result.jobId;
441
437
  accessToken = result.token;
442
438
  } catch (err) {
@@ -734,9 +730,8 @@ async function importToAssistant(
734
730
  // target can't actually load) whenever the two drift apart.
735
731
  let targetRuntimeVersion: string;
736
732
  try {
737
- const identity = await callRuntimeWithAuthRetry(
738
- entry,
739
- (token) => localRuntimeIdentity(entry, token),
733
+ const identity = await callRuntimeWithAuthRetry(entry, (token) =>
734
+ localRuntimeIdentity(entry, token),
740
735
  );
741
736
  targetRuntimeVersion = identity.version;
742
737
  } catch (err) {
@@ -779,15 +774,12 @@ async function importToAssistant(
779
774
  let jobId: string;
780
775
  let accessToken: string;
781
776
  try {
782
- const result = await callRuntimeWithAuthRetry(
783
- entry,
784
- async (token) => {
785
- const r = await localRuntimeImportFromGcs(entry, token, {
786
- bundleUrl,
787
- });
788
- return { jobId: r.jobId, token };
789
- },
790
- );
777
+ const result = await callRuntimeWithAuthRetry(entry, async (token) => {
778
+ const r = await localRuntimeImportFromGcs(entry, token, {
779
+ bundleUrl,
780
+ });
781
+ return { jobId: r.jobId, token };
782
+ });
791
783
  jobId = result.jobId;
792
784
  accessToken = result.token;
793
785
  } catch (err) {
@@ -910,17 +902,12 @@ export async function resolveOrHatchTarget(
910
902
 
911
903
  if (targetEnv === "docker") {
912
904
  const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
913
- await hatchDocker(
914
- "vellum",
915
- false,
916
- targetName ?? null,
917
- false,
918
- {},
919
- {},
920
- {
921
- setupProviderCredentials: false,
922
- },
923
- );
905
+ await hatchDocker({
906
+ species: "vellum",
907
+ detached: false,
908
+ name: targetName ?? null,
909
+ setupProviderCredentials: false,
910
+ });
924
911
  const entry = targetName
925
912
  ? findAssistantByName(targetName)
926
913
  : (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
package/src/lib/docker.ts CHANGED
@@ -274,8 +274,53 @@ function ensureLocalBinOnPath(): void {
274
274
  }
275
275
  }
276
276
 
277
- export interface HatchDockerOptions {
277
+ export interface HatchDockerParams {
278
+ /** Assistant species to hatch (e.g. `"vellum"`). */
279
+ species: Species;
280
+ /** Run detached without attaching to logs or interactive setup. */
281
+ detached?: boolean;
282
+ /** Instance display name. Defaults to an auto-generated name. */
283
+ name?: string | null;
284
+ /** Build from a local source tree and hot-reload on change. */
285
+ watch?: boolean;
286
+ /** Hatch-time config values (key → value). */
287
+ configValues?: Record<string, string>;
288
+ /** Extra env vars forwarded into the assistant container. */
289
+ flagEnvVars?: Record<string, string>;
278
290
  setupProviderCredentials?: boolean;
291
+ /**
292
+ * Path to a local source tree to build images from before hatching. When
293
+ * provided, this path is used directly as the repo root and no file
294
+ * watcher is started — useful for callers (e.g. evals) that want each
295
+ * run to pick up local CLI changes without keeping a long-lived watcher
296
+ * process around. `--watch` independently auto-detects the repo root and
297
+ * also enables hot-reload.
298
+ */
299
+ sourcePath?: string | null;
300
+ analyze?: boolean;
301
+ /**
302
+ * Name of an existing container whose network namespace the assistant,
303
+ * gateway, and credential-executor join (`--network=container:<name>`),
304
+ * instead of the assistant owning a freshly-created per-instance network.
305
+ * When set, hatch creates no Docker network and publishes no host ports —
306
+ * the namespace owner is responsible for publishing the gateway port — so
307
+ * `gatewayPort` must also be supplied (the owner had to publish it before
308
+ * hatch ran).
309
+ */
310
+ netnsContainer?: string;
311
+ /**
312
+ * Explicit host port to record as the gateway's `runtimeUrl` instead of
313
+ * auto-allocating a free port. Required alongside `netnsContainer`, where
314
+ * the namespace owner — not hatch — owns port allocation and publishing.
315
+ */
316
+ gatewayPort?: number;
317
+ /**
318
+ * Host path to a PEM CA bundle bind-mounted into the assistant container
319
+ * and trusted at process start via `NODE_EXTRA_CA_CERTS`. Lets the daemon
320
+ * trust a TLS-terminating egress proxy from its very first outbound
321
+ * connection.
322
+ */
323
+ assistantCaCertPath?: string;
279
324
  }
280
325
 
281
326
  export type DockerProviderCredentialSetupAction =
@@ -669,6 +714,8 @@ export async function startContainers(
669
714
  imageTags: Record<ServiceName, string>;
670
715
  instanceName: string;
671
716
  res: ReturnType<typeof dockerResourceNames>;
717
+ netnsContainer?: string;
718
+ assistantCaCertPath?: string;
672
719
  },
673
720
  log: (msg: string) => void,
674
721
  ): Promise<void> {
@@ -891,6 +938,8 @@ function startFileWatcher(opts: {
891
938
  instanceName: string;
892
939
  repoRoot: string;
893
940
  res: ReturnType<typeof dockerResourceNames>;
941
+ netnsContainer?: string;
942
+ assistantCaCertPath?: string;
894
943
  }): () => void {
895
944
  const { gatewayPort, imageTags, instanceName, repoRoot, res } = opts;
896
945
 
@@ -912,6 +961,8 @@ function startFileWatcher(opts: {
912
961
  instanceName,
913
962
  res,
914
963
  avatarDevicePath: resolveAvatarDevicePath(),
964
+ netnsContainer: opts.netnsContainer,
965
+ assistantCaCertPath: opts.assistantCaCertPath,
915
966
  });
916
967
  const containerForService: Record<ServiceName, string> = {
917
968
  assistant: res.assistantContainer,
@@ -1030,31 +1081,19 @@ function startFileWatcher(opts: {
1030
1081
  };
1031
1082
  }
1032
1083
 
1033
- export interface HatchDockerOptions {
1034
- /**
1035
- * Path to a local source tree to build images from before hatching. When
1036
- * provided, this path is used directly as the repo root and no file
1037
- * watcher is started — useful for callers (e.g. evals) that want each
1038
- * run to pick up local CLI changes without keeping a long-lived watcher
1039
- * process around. `--watch` independently auto-detects the repo root and
1040
- * also enables hot-reload.
1041
- */
1042
- sourcePath?: string | null;
1043
- analyze?: boolean;
1044
- }
1084
+ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1085
+ const {
1086
+ species,
1087
+ detached = false,
1088
+ name = null,
1089
+ configValues = {},
1090
+ flagEnvVars = {},
1091
+ } = params;
1092
+ let watch = params.watch ?? false;
1045
1093
 
1046
- export async function hatchDocker(
1047
- species: Species,
1048
- detached: boolean,
1049
- name: string | null,
1050
- watch: boolean = false,
1051
- configValues: Record<string, string> = {},
1052
- flagEnvVars: Record<string, string> = {},
1053
- options: HatchDockerOptions = {},
1054
- ): Promise<void> {
1055
1094
  resetLogFile("hatch.log");
1056
1095
  const provider =
1057
- options.setupProviderCredentials === false
1096
+ params.setupProviderCredentials === false
1058
1097
  ? undefined
1059
1098
  : resolveHatchProvider(configValues);
1060
1099
 
@@ -1069,21 +1108,32 @@ export async function hatchDocker(
1069
1108
  await ensureDockerInstalled();
1070
1109
 
1071
1110
  const instanceName = generateInstanceName(species, name);
1072
- // Resolve the gateway's host port dynamically. The env-default
1111
+ // Resolve the gateway's host port. When joining an externally-owned
1112
+ // network namespace, the owner has already published the gateway port,
1113
+ // so the caller — not hatch — owns port allocation; use the supplied
1114
+ // port verbatim. Otherwise resolve it dynamically: the env-default
1073
1115
  // (production 7830 / non-prod overrides) is just the *preferred*
1074
- // starting point — if it's taken by another local assistant, eval
1075
- // run, or unrelated process, we walk upward until we find a free
1076
- // port. This replaces the previous "first one in wins, everyone
1077
- // else gets a docker bind error" behavior and removes the need for
1078
- // an orphan-cleanup pre-flight in the evals harness.
1079
- const preferredGatewayPort = getDefaultPorts(
1080
- getCurrentEnvironment(),
1081
- ).gateway;
1082
- const gatewayPort = await findOpenPort(preferredGatewayPort);
1083
- if (gatewayPort !== preferredGatewayPort) {
1084
- log(
1085
- `Preferred gateway port ${preferredGatewayPort} is in use; allocated ${gatewayPort} for this instance.`,
1086
- );
1116
+ // starting point — if it's taken by another local assistant, eval run,
1117
+ // or unrelated process, we walk upward until we find a free port, so
1118
+ // concurrent instances don't collide on a docker bind error.
1119
+ let gatewayPort: number;
1120
+ if (params.netnsContainer) {
1121
+ if (params.gatewayPort === undefined) {
1122
+ throw new Error(
1123
+ "hatchDocker: gatewayPort is required when netnsContainer is set (the namespace owner publishes the port before hatch runs)",
1124
+ );
1125
+ }
1126
+ gatewayPort = params.gatewayPort;
1127
+ } else {
1128
+ const preferredGatewayPort = getDefaultPorts(
1129
+ getCurrentEnvironment(),
1130
+ ).gateway;
1131
+ gatewayPort = await findOpenPort(preferredGatewayPort);
1132
+ if (gatewayPort !== preferredGatewayPort) {
1133
+ log(
1134
+ `Preferred gateway port ${preferredGatewayPort} is in use; allocated ${gatewayPort} for this instance.`,
1135
+ );
1136
+ }
1087
1137
  }
1088
1138
 
1089
1139
  const imageTags: Record<ServiceName, string> = {
@@ -1093,8 +1143,8 @@ export async function hatchDocker(
1093
1143
  };
1094
1144
 
1095
1145
  const sourcePath =
1096
- typeof options.sourcePath === "string" && options.sourcePath.length > 0
1097
- ? options.sourcePath
1146
+ typeof params.sourcePath === "string" && params.sourcePath.length > 0
1147
+ ? params.sourcePath
1098
1148
  : null;
1099
1149
  const buildFromSource = sourcePath !== null;
1100
1150
  let repoRoot: string | undefined;
@@ -1241,8 +1291,15 @@ export async function hatchDocker(
1241
1291
  const res = dockerResourceNames(instanceName);
1242
1292
 
1243
1293
  emitProgress(3, 6, "Creating volumes...");
1244
- log("📁 Creating network and volumes...");
1245
- await exec("docker", ["network", "create", res.network]);
1294
+ // When joining an externally-owned network namespace, the owner already
1295
+ // provides the network stack — creating a per-instance network here would
1296
+ // be unused and leak on teardown.
1297
+ if (params.netnsContainer) {
1298
+ log("📁 Joining existing network namespace; creating volumes...");
1299
+ } else {
1300
+ log("📁 Creating network and volumes...");
1301
+ await exec("docker", ["network", "create", res.network]);
1302
+ }
1246
1303
  await exec("docker", ["volume", "create", res.socketVolume]);
1247
1304
  await exec("docker", ["volume", "create", res.assistantIpcVolume]);
1248
1305
  await exec("docker", ["volume", "create", res.gatewayIpcVolume]);
@@ -1350,6 +1407,8 @@ export async function hatchDocker(
1350
1407
  imageTags,
1351
1408
  instanceName,
1352
1409
  res,
1410
+ netnsContainer: params.netnsContainer,
1411
+ assistantCaCertPath: params.assistantCaCertPath,
1353
1412
  },
1354
1413
  log,
1355
1414
  );
@@ -1389,7 +1448,7 @@ export async function hatchDocker(
1389
1448
  logFd,
1390
1449
  runtimeUrl,
1391
1450
  containersUpAt,
1392
- analyze: options.analyze ?? false,
1451
+ analyze: params.analyze ?? false,
1393
1452
  });
1394
1453
 
1395
1454
  if (!ready && !(watch && repoRoot)) {
@@ -1447,6 +1506,8 @@ export async function hatchDocker(
1447
1506
  instanceName,
1448
1507
  repoRoot,
1449
1508
  res,
1509
+ netnsContainer: params.netnsContainer,
1510
+ assistantCaCertPath: params.assistantCaCertPath,
1450
1511
  });
1451
1512
 
1452
1513
  await new Promise<void>((resolve) => {
@@ -16,22 +16,29 @@ import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
16
16
 
17
17
  const AVATAR_DEVICE_ENV_VAR = "VELLUM_AVATAR_DEVICE";
18
18
 
19
+ /**
20
+ * In-container path the assistant's extra CA bundle is mounted at when
21
+ * `assistantCaCertPath` is supplied. Read-only and outside the
22
+ * `update-ca-certificates` directory so it's consumed purely via
23
+ * `NODE_EXTRA_CA_CERTS` without touching the system trust store.
24
+ */
25
+ const ASSISTANT_EXTRA_CA_TARGET = "/etc/vellum/extra-ca-cert.pem";
26
+
19
27
  /** Logical service name used throughout the CLI. */
20
28
  export type ServiceName = "assistant" | "gateway" | "credential-executor";
21
29
 
22
30
  /**
23
- /**
24
- * The four fields from `dockerResourceNames()` that the builder actually uses.
25
- * Container/network names come from here; volume names are generated by the
26
- * `volumeClaimTemplates` in the spec. `ReturnType<typeof dockerResourceNames>`
27
- * structurally satisfies this interface (it has all these fields plus more).
28
- */
29
- export interface DockerResourceNames {
30
- assistantContainer: string;
31
- cesContainer: string;
32
- gatewayContainer: string;
33
- network: string;
34
- }
31
+ * The four fields from `dockerResourceNames()` that the builder actually uses.
32
+ * Container/network names come from here; volume names are generated by the
33
+ * `volumeClaimTemplates` in the spec. `ReturnType<typeof dockerResourceNames>`
34
+ * structurally satisfies this interface (it has all these fields plus more).
35
+ */
36
+ export interface DockerResourceNames {
37
+ assistantContainer: string;
38
+ cesContainer: string;
39
+ gatewayContainer: string;
40
+ network: string;
41
+ }
35
42
 
36
43
  // ---------------------------------------------------------------------------
37
44
  // Types
@@ -133,6 +140,7 @@ export interface DockerRunSecrets {
133
140
  // Spec
134
141
  // ---------------------------------------------------------------------------
135
142
 
143
+ // prettier-ignore
136
144
  export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
137
145
  startOrder: ["assistant", "gateway", "credential-executor"],
138
146
 
@@ -260,6 +268,21 @@ export interface BuildServiceRunArgsOpts extends DockerRunSecrets {
260
268
  extraGatewayEnv?: Record<string, string>;
261
269
  /** Avatar device path, if available. Injected by `docker.ts` after resolving. */
262
270
  avatarDevicePath?: string;
271
+ /**
272
+ * Name of an existing container whose network namespace every service
273
+ * joins via `--network=container:<name>`, instead of the assistant owning
274
+ * the namespace. When set, the assistant publishes no host ports — the
275
+ * namespace owner is responsible for port publishing — and no per-instance
276
+ * Docker network is referenced.
277
+ */
278
+ netnsContainer?: string;
279
+ /**
280
+ * Host path to a PEM CA bundle to bind-mount into the assistant container
281
+ * and trust at process start via `NODE_EXTRA_CA_CERTS`. Used when the
282
+ * assistant's outbound TLS is terminated by a proxy whose certificate is
283
+ * signed by a CA outside the default trust set.
284
+ */
285
+ assistantCaCertPath?: string;
263
286
  }
264
287
 
265
288
  interface BuilderManagedEnvKeys {
@@ -283,13 +306,17 @@ export function getBuilderManagedEnvKeys(
283
306
  spec = DOCKER_STATEFUL_SET_SPEC,
284
307
  ): BuilderManagedEnvKeys {
285
308
  const container = spec.containers.find((c) => c.internalName === service);
286
- if (!container) throw new Error(`docker-statefulset: unknown service "${service}"`);
309
+ if (!container)
310
+ throw new Error(`docker-statefulset: unknown service "${service}"`);
287
311
 
288
312
  const always = new Set<string>(["PATH"]);
289
313
  const hostForwarded: Array<{ name: string; hostVar: string }> = [];
290
314
  for (const entry of container.env) {
291
315
  if (entry.kind === "host") {
292
- hostForwarded.push({ name: entry.name, hostVar: entry.hostVar ?? entry.name });
316
+ hostForwarded.push({
317
+ name: entry.name,
318
+ hostVar: entry.hostVar ?? entry.name,
319
+ });
293
320
  } else {
294
321
  always.add(entry.name);
295
322
  }
@@ -311,7 +338,8 @@ function resolveVolume(
311
338
  volumeName: string,
312
339
  ): string {
313
340
  const claim = spec.volumeClaimTemplates.find((v) => v.name === volumeName);
314
- if (!claim) throw new Error(`docker-statefulset: unknown volume "${volumeName}"`);
341
+ if (!claim)
342
+ throw new Error(`docker-statefulset: unknown volume "${volumeName}"`);
315
343
  return claim.dockerVolume(instanceName);
316
344
  }
317
345
 
@@ -331,6 +359,8 @@ export function buildServiceRunArgs(
331
359
  extraAssistantEnv,
332
360
  extraGatewayEnv,
333
361
  avatarDevicePath,
362
+ netnsContainer,
363
+ assistantCaCertPath,
334
364
  } = opts;
335
365
 
336
366
  const result = {} as Record<ServiceName, () => string[]>;
@@ -356,7 +386,11 @@ export function buildServiceRunArgs(
356
386
  }
357
387
 
358
388
  // Network
359
- if (container.network === "bridge") {
389
+ if (netnsContainer) {
390
+ // Every service joins an externally-owned namespace. The owner
391
+ // publishes host ports, so nothing is published here.
392
+ args.push(`--network=container:${netnsContainer}`);
393
+ } else if (container.network === "bridge") {
360
394
  args.push(`--network=${res.network}`);
361
395
  for (const port of container.ports ?? []) {
362
396
  const hostSide =
@@ -374,7 +408,12 @@ export function buildServiceRunArgs(
374
408
  // Volume mounts
375
409
  for (const mount of container.volumeMounts) {
376
410
  const vol = resolveVolume(spec, instanceName, mount.volumeName);
377
- args.push("-v", mount.readOnly ? `${vol}:${mount.mountPath}:ro` : `${vol}:${mount.mountPath}`);
411
+ args.push(
412
+ "-v",
413
+ mount.readOnly
414
+ ? `${vol}:${mount.mountPath}:ro`
415
+ : `${vol}:${mount.mountPath}`,
416
+ );
378
417
  }
379
418
 
380
419
  // Env vars from spec
@@ -401,8 +440,10 @@ export function buildServiceRunArgs(
401
440
  // Assistant-only computed / optional additions
402
441
  if (svc === "assistant") {
403
442
  args.push(
404
- "-e", `VELLUM_ASSISTANT_NAME=${instanceName}`,
405
- "-e", `GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
443
+ "-e",
444
+ `VELLUM_ASSISTANT_NAME=${instanceName}`,
445
+ "-e",
446
+ `GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
406
447
  );
407
448
 
408
449
  if (extraAssistantEnv) {
@@ -413,8 +454,19 @@ export function buildServiceRunArgs(
413
454
 
414
455
  if (avatarDevicePath && existsSync(avatarDevicePath)) {
415
456
  args.push(
416
- "--device", `${avatarDevicePath}:${avatarDevicePath}`,
417
- "-e", `${AVATAR_DEVICE_ENV_VAR}=${avatarDevicePath}`,
457
+ "--device",
458
+ `${avatarDevicePath}:${avatarDevicePath}`,
459
+ "-e",
460
+ `${AVATAR_DEVICE_ENV_VAR}=${avatarDevicePath}`,
461
+ );
462
+ }
463
+
464
+ if (assistantCaCertPath) {
465
+ args.push(
466
+ "-v",
467
+ `${assistantCaCertPath}:${ASSISTANT_EXTRA_CA_TARGET}:ro`,
468
+ "-e",
469
+ `NODE_EXTRA_CA_CERTS=${ASSISTANT_EXTRA_CA_TARGET}`,
418
470
  );
419
471
  }
420
472
  }