@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 +1 -1
- package/src/__tests__/teleport.test.ts +6 -9
- package/src/commands/hatch.ts +90 -5
- package/src/commands/teleport.ts +23 -36
- package/src/lib/docker.ts +103 -42
- package/src/lib/statefulset.ts +73 -21
package/package.json
CHANGED
|
@@ -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
|
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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 =
|
|
193
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/commands/teleport.ts
CHANGED
|
@@ -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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
1097
|
-
?
|
|
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
|
-
|
|
1245
|
-
|
|
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:
|
|
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) => {
|
package/src/lib/statefulset.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
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({
|
|
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)
|
|
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 (
|
|
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(
|
|
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",
|
|
405
|
-
|
|
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",
|
|
417
|
-
|
|
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
|
}
|