@vellumai/cli 0.8.12 → 0.9.0-staging.1

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 (51) hide show
  1. package/bun.lock +49 -56
  2. package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +224 -0
  3. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +19 -0
  4. package/node_modules/@vellumai/local-mode/src/index.ts +8 -1
  5. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +0 -15
  6. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +8 -4
  7. package/node_modules/@vellumai/local-mode/src/sleep.ts +80 -0
  8. package/node_modules/@vellumai/local-mode/src/status.ts +342 -0
  9. package/node_modules/@vellumai/local-mode/src/wake.ts +12 -1
  10. package/package.json +3 -3
  11. package/src/__tests__/assistant-config.test.ts +1 -2
  12. package/src/__tests__/device-id.test.ts +6 -14
  13. package/src/__tests__/helpers/os-mock.ts +27 -0
  14. package/src/__tests__/login-loopback.test.ts +71 -0
  15. package/src/__tests__/multi-local.test.ts +2 -10
  16. package/src/__tests__/nginx-ingress-command.test.ts +69 -0
  17. package/src/__tests__/nginx-ingress.test.ts +401 -0
  18. package/src/__tests__/sleep.test.ts +4 -0
  19. package/src/__tests__/teleport.test.ts +6 -9
  20. package/src/__tests__/tunnel.test.ts +164 -0
  21. package/src/__tests__/wake.test.ts +15 -4
  22. package/src/__tests__/workos-pkce.test.ts +314 -0
  23. package/src/commands/flags.ts +1 -22
  24. package/src/commands/hatch.ts +90 -9
  25. package/src/commands/login.ts +123 -59
  26. package/src/commands/nginx-ingress.ts +291 -0
  27. package/src/commands/rollback.ts +0 -6
  28. package/src/commands/sleep.ts +17 -0
  29. package/src/commands/teleport.ts +23 -36
  30. package/src/commands/tunnel.ts +69 -11
  31. package/src/commands/upgrade.ts +0 -2
  32. package/src/commands/wake.ts +7 -5
  33. package/src/commands/workflows.ts +301 -0
  34. package/src/index.ts +8 -0
  35. package/src/lib/arg-utils.ts +48 -0
  36. package/src/lib/assistant-client.ts +2 -0
  37. package/src/lib/assistant-config.ts +0 -7
  38. package/src/lib/cloudflare-tunnel.ts +15 -2
  39. package/src/lib/docker.ts +103 -49
  40. package/src/lib/feature-flags.test.ts +157 -0
  41. package/src/lib/feature-flags.ts +38 -0
  42. package/src/lib/hatch-local.ts +0 -1
  43. package/src/lib/local.ts +5 -0
  44. package/src/lib/nginx-ingress.ts +574 -0
  45. package/src/lib/ngrok.ts +26 -4
  46. package/src/lib/platform-client.ts +0 -1
  47. package/src/lib/retire-local.ts +5 -0
  48. package/src/lib/statefulset.ts +73 -21
  49. package/src/lib/sync-cloud-assistants.ts +4 -17
  50. package/src/lib/upgrade-lifecycle.ts +1 -2
  51. package/src/lib/workos-pkce.ts +160 -0
@@ -272,12 +272,23 @@ describe("vellum wake", () => {
272
272
  expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
273
273
  });
274
274
 
275
- test("skips re-provision when a guardian token already exists", async () => {
275
+ test("re-provisions even when a guardian token already exists", async () => {
276
+ // A connect can 401 off a token whose local state looks healthy
277
+ // (revoked, mis-seeded, wrong principal). The user explicitly confirmed
278
+ // the destructive repair, so the flag forces a re-lease instead of
279
+ // guessing from local token state and recreating the no-op loop.
276
280
  process.argv = ["bun", "vellum", "wake", "--repair-guardian", "local-assistant"];
277
- // loadGuardianToken returns a token by default — recovery must not run.
281
+ // loadGuardianToken returns a healthy-looking token by default.
278
282
  await wake();
279
283
 
280
- expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
281
- expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
284
+ expect(resetGuardianBootstrapMock).toHaveBeenCalledWith(
285
+ "http://127.0.0.1:7830",
286
+ "generated-bootstrap-secret",
287
+ );
288
+ expect(leaseGuardianTokenMock).toHaveBeenCalledWith(
289
+ "http://127.0.0.1:7830",
290
+ "local-assistant",
291
+ "generated-bootstrap-secret",
292
+ );
282
293
  });
283
294
  });
@@ -0,0 +1,314 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import {
4
+ buildAuthorizeUrl,
5
+ exchangeAccessTokenForSession,
6
+ exchangeCodeWithWorkos,
7
+ fetchWorkosClientId,
8
+ generatePkcePair,
9
+ selectWorkosClientId,
10
+ } from "../lib/workos-pkce.js";
11
+
12
+ const originalFetch = globalThis.fetch;
13
+
14
+ afterEach(() => {
15
+ globalThis.fetch = originalFetch;
16
+ });
17
+
18
+ /**
19
+ * Route fetch responses by URL substring so each WorkOS/platform call can be
20
+ * inspected. The module talks to remote hosts via loopbackSafeFetch, which
21
+ * delegates to globalThis.fetch.
22
+ */
23
+ function mockFetchByUrl(responses: Record<string, () => Response>) {
24
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
25
+ globalThis.fetch = mock(
26
+ async (input: RequestInfo | URL, init?: RequestInit) => {
27
+ const url = String(input);
28
+ calls.push({ url, init });
29
+ const match = Object.entries(responses).find(([prefix]) =>
30
+ url.includes(prefix),
31
+ );
32
+ if (!match) throw new Error(`Unexpected fetch: ${url}`);
33
+ return match[1]();
34
+ },
35
+ ) as unknown as typeof globalThis.fetch;
36
+ return calls;
37
+ }
38
+
39
+ describe("generatePkcePair", () => {
40
+ test("challenge is the base64url sha256 of the verifier", async () => {
41
+ const { verifier, challenge } = generatePkcePair();
42
+ const digest = await crypto.subtle.digest(
43
+ "SHA-256",
44
+ new TextEncoder().encode(verifier),
45
+ );
46
+ const expected = Buffer.from(digest).toString("base64url");
47
+ expect(challenge).toBe(expected);
48
+ });
49
+
50
+ test("verifiers are unique", () => {
51
+ expect(generatePkcePair().verifier).not.toBe(generatePkcePair().verifier);
52
+ });
53
+ });
54
+
55
+ describe("buildAuthorizeUrl", () => {
56
+ const base = {
57
+ clientId: "client_123",
58
+ redirectUri: "http://127.0.0.1:4242/auth/callback",
59
+ challenge: "chal",
60
+ state: "st",
61
+ };
62
+
63
+ test("targets user_management/authorize with PKCE and authkit defaults", () => {
64
+ const url = new URL(buildAuthorizeUrl(base));
65
+ expect(url.origin).toBe("https://api.workos.com");
66
+ expect(url.pathname).toBe("/user_management/authorize");
67
+ expect(url.searchParams.get("client_id")).toBe("client_123");
68
+ expect(url.searchParams.get("redirect_uri")).toBe(base.redirectUri);
69
+ expect(url.searchParams.get("response_type")).toBe("code");
70
+ expect(url.searchParams.get("scope")).toBe("openid profile email");
71
+ expect(url.searchParams.get("code_challenge")).toBe("chal");
72
+ expect(url.searchParams.get("code_challenge_method")).toBe("S256");
73
+ expect(url.searchParams.get("state")).toBe("st");
74
+ expect(url.searchParams.get("provider")).toBe("authkit");
75
+ // Session reuse: never force a fresh IdP login.
76
+ expect(url.searchParams.has("prompt")).toBe(false);
77
+ });
78
+
79
+ test("provider hint replaces authkit", () => {
80
+ const url = new URL(
81
+ buildAuthorizeUrl({ ...base, providerHint: "GoogleOAuth" }),
82
+ );
83
+ expect(url.searchParams.get("provider")).toBe("GoogleOAuth");
84
+ });
85
+
86
+ test("login hint is forwarded", () => {
87
+ // generic-examples:ignore-next-line — reason: test fixture for URL encoding, not a real email
88
+ const url = new URL(buildAuthorizeUrl({ ...base, loginHint: "a@b.co" }));
89
+ // generic-examples:ignore-next-line — reason: test fixture for URL encoding, not a real email
90
+ expect(url.searchParams.get("login_hint")).toBe("a@b.co");
91
+
92
+ const noHint = new URL(buildAuthorizeUrl(base));
93
+ expect(noHint.searchParams.has("login_hint")).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe("selectWorkosClientId", () => {
98
+ // During coexistence the platform lists two providers that share the
99
+ // "workos-oidc" id; only the OAuth2 one (no discovery URL) is usable.
100
+ const legacy = {
101
+ id: "workos-oidc",
102
+ name: "WorkOS OIDC",
103
+ client_id: "client_connect",
104
+ flows: ["provider_redirect", "provider_token"],
105
+ openid_configuration_url:
106
+ "https://x.authkit.app/.well-known/openid-configuration",
107
+ };
108
+ const modern = {
109
+ id: "workos-oidc",
110
+ name: "WorkOS",
111
+ client_id: "client_um",
112
+ flows: ["provider_redirect", "provider_token"],
113
+ };
114
+
115
+ test("picks the OAuth2 entry during coexistence", () => {
116
+ expect(selectWorkosClientId([legacy, modern])).toBe("client_um");
117
+ expect(selectWorkosClientId([modern, legacy])).toBe("client_um");
118
+ });
119
+
120
+ test("returns null when the platform lacks token auth", () => {
121
+ const preTokenAuth = { ...modern, flows: ["provider_redirect"] };
122
+ expect(selectWorkosClientId([legacy, preTokenAuth])).toBeNull();
123
+ expect(selectWorkosClientId([])).toBeNull();
124
+ });
125
+
126
+ test("ignores an entry missing client_id", () => {
127
+ const noClientId = { id: "workos-oidc", flows: ["provider_token"] };
128
+ expect(selectWorkosClientId([noClientId])).toBeNull();
129
+ });
130
+ });
131
+
132
+ describe("fetchWorkosClientId", () => {
133
+ test("resolves the client id from the headless config", async () => {
134
+ const calls = mockFetchByUrl({
135
+ "/_allauth/app/v1/config": () =>
136
+ new Response(
137
+ JSON.stringify({
138
+ data: {
139
+ socialaccount: {
140
+ providers: [
141
+ {
142
+ id: "workos-oidc",
143
+ client_id: "client_um",
144
+ flows: ["provider_token"],
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ }),
150
+ { status: 200 },
151
+ ),
152
+ });
153
+
154
+ expect(await fetchWorkosClientId("https://platform.example")).toBe(
155
+ "client_um",
156
+ );
157
+ expect(calls[0]!.url).toBe(
158
+ "https://platform.example/_allauth/app/v1/config",
159
+ );
160
+ });
161
+
162
+ test("derives the config URL from the origin, ignoring any path", async () => {
163
+ const calls = mockFetchByUrl({
164
+ "/_allauth/app/v1/config": () =>
165
+ new Response(
166
+ JSON.stringify({
167
+ data: {
168
+ socialaccount: {
169
+ providers: [
170
+ { client_id: "client_um", flows: ["provider_token"] },
171
+ ],
172
+ },
173
+ },
174
+ }),
175
+ { status: 200 },
176
+ ),
177
+ });
178
+
179
+ await fetchWorkosClientId("https://platform.example/some/path");
180
+ expect(calls[0]!.url).toBe(
181
+ "https://platform.example/_allauth/app/v1/config",
182
+ );
183
+ });
184
+
185
+ test("throws a clear error when no token-auth provider is advertised", async () => {
186
+ mockFetchByUrl({
187
+ "/_allauth/app/v1/config": () =>
188
+ new Response(
189
+ JSON.stringify({ data: { socialaccount: { providers: [] } } }),
190
+ { status: 200 },
191
+ ),
192
+ });
193
+
194
+ await expect(
195
+ fetchWorkosClientId("https://platform.example"),
196
+ ).rejects.toThrow(/does not advertise/);
197
+ });
198
+
199
+ test("throws on a non-OK config response", async () => {
200
+ mockFetchByUrl({
201
+ "/_allauth/app/v1/config": () => new Response("nope", { status: 500 }),
202
+ });
203
+
204
+ await expect(
205
+ fetchWorkosClientId("https://platform.example"),
206
+ ).rejects.toThrow(/500/);
207
+ });
208
+ });
209
+
210
+ describe("exchangeCodeWithWorkos", () => {
211
+ test("posts a public-client PKCE exchange and returns the access token", async () => {
212
+ const calls = mockFetchByUrl({
213
+ "/user_management/authenticate": () =>
214
+ new Response(JSON.stringify({ access_token: "at_1", user: {} }), {
215
+ status: 200,
216
+ }),
217
+ });
218
+
219
+ const token = await exchangeCodeWithWorkos({
220
+ clientId: "client_um",
221
+ code: "c",
222
+ verifier: "v",
223
+ });
224
+
225
+ expect(token).toBe("at_1");
226
+ expect(calls[0]!.url).toBe(
227
+ "https://api.workos.com/user_management/authenticate",
228
+ );
229
+ const body = JSON.parse(String(calls[0]!.init?.body));
230
+ // Public client: no secret, no API key.
231
+ expect(body).toEqual({
232
+ client_id: "client_um",
233
+ grant_type: "authorization_code",
234
+ code: "c",
235
+ code_verifier: "v",
236
+ });
237
+ });
238
+
239
+ test("throws with upstream detail on failure", async () => {
240
+ mockFetchByUrl({
241
+ "/user_management/authenticate": () =>
242
+ new Response(JSON.stringify({ error: "invalid_grant" }), {
243
+ status: 400,
244
+ }),
245
+ });
246
+
247
+ await expect(
248
+ exchangeCodeWithWorkos({ clientId: "c", code: "x", verifier: "v" }),
249
+ ).rejects.toThrow(/400/);
250
+ });
251
+
252
+ test("throws when the exchange returns no access token", async () => {
253
+ mockFetchByUrl({
254
+ "/user_management/authenticate": () =>
255
+ new Response(JSON.stringify({}), { status: 200 }),
256
+ });
257
+
258
+ await expect(
259
+ exchangeCodeWithWorkos({ clientId: "c", code: "x", verifier: "v" }),
260
+ ).rejects.toThrow(/no access token/);
261
+ });
262
+ });
263
+
264
+ describe("exchangeAccessTokenForSession", () => {
265
+ test("posts the headless token payload and returns the session token", async () => {
266
+ const calls = mockFetchByUrl({
267
+ "/_allauth/app/v1/auth/provider/token": () =>
268
+ new Response(JSON.stringify({ meta: { session_token: "sess_1" } }), {
269
+ status: 200,
270
+ }),
271
+ });
272
+
273
+ const token = await exchangeAccessTokenForSession(
274
+ "https://platform.example",
275
+ "client_um",
276
+ "at_1",
277
+ );
278
+
279
+ expect(token).toBe("sess_1");
280
+ expect(calls[0]!.url).toBe(
281
+ "https://platform.example/_allauth/app/v1/auth/provider/token",
282
+ );
283
+ const body = JSON.parse(String(calls[0]!.init?.body));
284
+ expect(body).toEqual({
285
+ provider: "workos",
286
+ process: "login",
287
+ token: { client_id: "client_um", access_token: "at_1" },
288
+ });
289
+ });
290
+
291
+ test("throws on a rejected token", async () => {
292
+ mockFetchByUrl({
293
+ "/_allauth/app/v1/auth/provider/token": () =>
294
+ new Response(JSON.stringify({ errors: [{ code: "invalid_token" }] }), {
295
+ status: 400,
296
+ }),
297
+ });
298
+
299
+ await expect(
300
+ exchangeAccessTokenForSession("https://platform.example", "c", "bad"),
301
+ ).rejects.toThrow(/400/);
302
+ });
303
+
304
+ test("throws when the session exchange returns no session token", async () => {
305
+ mockFetchByUrl({
306
+ "/_allauth/app/v1/auth/provider/token": () =>
307
+ new Response(JSON.stringify({ meta: {} }), { status: 200 }),
308
+ });
309
+
310
+ await expect(
311
+ exchangeAccessTokenForSession("https://platform.example", "c", "at"),
312
+ ).rejects.toThrow(/no session token/);
313
+ });
314
+ });
@@ -1,3 +1,4 @@
1
+ import { extractAssistantFlag } from "../lib/arg-utils.js";
1
2
  import { AssistantClient } from "../lib/assistant-client.js";
2
3
  import {
3
4
  formatAssistantLookupError,
@@ -199,28 +200,6 @@ async function setFlag(
199
200
  console.log(`Flag "${key}" set to ${value}`);
200
201
  }
201
202
 
202
- /**
203
- * Strip `--assistant <name>` from argv and return the captured value.
204
- *
205
- * Mutates the input array so positional parsing downstream sees a clean
206
- * shape (subcommand + key + value). Returns `undefined` if the flag is
207
- * absent. Error-reports a missing value so the user gets a clear message
208
- * rather than the flag being silently swallowed as a positional.
209
- */
210
- function extractAssistantFlag(args: string[]): string | undefined {
211
- for (let i = 0; i < args.length; i++) {
212
- if (args[i] !== "--assistant") continue;
213
- const value = args[i + 1];
214
- if (!value || value.startsWith("-")) {
215
- console.error("Missing value for --assistant <name>");
216
- process.exit(1);
217
- }
218
- args.splice(i, 2);
219
- return value;
220
- }
221
- return undefined;
222
- }
223
-
224
203
  export async function flags(): Promise<void> {
225
204
  const args = process.argv.slice(3);
226
205
 
@@ -5,7 +5,6 @@ import cliPkg from "../../package.json";
5
5
 
6
6
  import { buildOpenclawStartupScript } from "../adapters/openclaw";
7
7
  import {
8
- normalizeVersion,
9
8
  saveAssistantEntry,
10
9
  setActiveAssistant,
11
10
  } from "../lib/assistant-config";
@@ -183,6 +182,9 @@ interface HatchArgs {
183
182
  flagEnvVars: Record<string, string>;
184
183
  analyze: boolean;
185
184
  disablePlatform: boolean;
185
+ netnsContainer: string | null;
186
+ gatewayPort: number | null;
187
+ assistantCaCert: string | null;
186
188
  }
187
189
 
188
190
  function parseArgs(): HatchArgs {
@@ -190,8 +192,10 @@ function parseArgs(): HatchArgs {
190
192
  process.argv.slice(3),
191
193
  );
192
194
  const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
193
- const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
194
- 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";
195
199
  let species: Species = DEFAULT_SPECIES;
196
200
  let detached = false;
197
201
  let keepAlive = false;
@@ -201,6 +205,9 @@ function parseArgs(): HatchArgs {
201
205
  let sourcePath: string | null = null;
202
206
  const configValues: Record<string, string> = {};
203
207
  let analyze = false;
208
+ let netnsContainer: string | null = null;
209
+ let gatewayPort: number | null = null;
210
+ let assistantCaCert: string | null = null;
204
211
 
205
212
  for (let i = 0; i < args.length; i++) {
206
213
  const arg = args[i];
@@ -240,6 +247,15 @@ function parseArgs(): HatchArgs {
240
247
  console.log(
241
248
  " --disable-platform Suppress all outbound platform API calls",
242
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
+ );
243
259
  process.exit(0);
244
260
  } else if (arg === "-d") {
245
261
  detached = true;
@@ -302,11 +318,38 @@ function parseArgs(): HatchArgs {
302
318
  i++;
303
319
  } else if (arg === "--disable-platform") {
304
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++;
305
348
  } else if (VALID_SPECIES.includes(arg as Species)) {
306
349
  species = arg as Species;
307
350
  } else {
308
351
  console.error(
309
- `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>`,
310
353
  );
311
354
  process.exit(1);
312
355
  }
@@ -324,6 +367,9 @@ function parseArgs(): HatchArgs {
324
367
  flagEnvVars,
325
368
  analyze,
326
369
  disablePlatform,
370
+ netnsContainer,
371
+ gatewayPort,
372
+ assistantCaCert,
327
373
  };
328
374
  }
329
375
 
@@ -560,6 +606,9 @@ export async function hatch(): Promise<void> {
560
606
  flagEnvVars,
561
607
  analyze,
562
608
  disablePlatform,
609
+ netnsContainer,
610
+ gatewayPort,
611
+ assistantCaCert,
563
612
  } = parseArgs();
564
613
 
565
614
  if (disablePlatform) {
@@ -581,6 +630,25 @@ export async function hatch(): Promise<void> {
581
630
  process.exit(1);
582
631
  }
583
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
+
584
652
  if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
585
653
  console.error(
586
654
  `Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
@@ -592,14 +660,30 @@ export async function hatch(): Promise<void> {
592
660
  }
593
661
 
594
662
  if (remote === "local") {
595
- await hatchLocal(species, name, watch, keepAlive, configValues, flagEnvVars);
663
+ await hatchLocal(
664
+ species,
665
+ name,
666
+ watch,
667
+ keepAlive,
668
+ configValues,
669
+ flagEnvVars,
670
+ );
596
671
  return;
597
672
  }
598
673
 
599
674
  if (remote === "docker") {
600
- await hatchDocker(species, detached, name, watch, configValues, flagEnvVars, {
675
+ await hatchDocker({
676
+ species,
677
+ detached,
678
+ name,
679
+ watch,
680
+ configValues,
681
+ flagEnvVars,
601
682
  sourcePath,
602
683
  analyze,
684
+ netnsContainer: netnsContainer ?? undefined,
685
+ gatewayPort: gatewayPort ?? undefined,
686
+ assistantCaCertPath: assistantCaCert ?? undefined,
603
687
  });
604
688
  return;
605
689
  }
@@ -639,9 +723,6 @@ async function hatchVellumPlatform(): Promise<void> {
639
723
  cloud: "vellum",
640
724
  species: "vellum",
641
725
  hatchedAt: new Date().toISOString(),
642
- ...(result.current_release_version != null && {
643
- version: normalizeVersion(result.current_release_version),
644
- }),
645
726
  });
646
727
  setActiveAssistant(result.id);
647
728