@vellumai/cli 0.8.12-staging.2 → 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.
- package/bun.lock +49 -56
- package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +224 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +19 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +8 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +0 -15
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +8 -4
- package/node_modules/@vellumai/local-mode/src/sleep.ts +80 -0
- package/node_modules/@vellumai/local-mode/src/status.ts +342 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +12 -1
- package/package.json +3 -3
- package/src/__tests__/assistant-config.test.ts +1 -2
- package/src/__tests__/device-id.test.ts +6 -14
- package/src/__tests__/helpers/os-mock.ts +27 -0
- package/src/__tests__/login-loopback.test.ts +71 -0
- package/src/__tests__/multi-local.test.ts +2 -10
- package/src/__tests__/nginx-ingress-command.test.ts +69 -0
- package/src/__tests__/nginx-ingress.test.ts +401 -0
- package/src/__tests__/sleep.test.ts +4 -0
- package/src/__tests__/teleport.test.ts +6 -9
- package/src/__tests__/tunnel.test.ts +164 -0
- package/src/__tests__/wake.test.ts +15 -4
- package/src/__tests__/workos-pkce.test.ts +314 -0
- package/src/commands/flags.ts +1 -22
- package/src/commands/hatch.ts +90 -9
- package/src/commands/login.ts +123 -59
- package/src/commands/nginx-ingress.ts +291 -0
- package/src/commands/rollback.ts +0 -6
- package/src/commands/sleep.ts +17 -0
- package/src/commands/teleport.ts +23 -36
- package/src/commands/tunnel.ts +69 -11
- package/src/commands/upgrade.ts +0 -2
- package/src/commands/wake.ts +7 -5
- package/src/commands/workflows.ts +301 -0
- package/src/index.ts +8 -0
- package/src/lib/arg-utils.ts +48 -0
- package/src/lib/assistant-client.ts +2 -0
- package/src/lib/assistant-config.ts +0 -7
- package/src/lib/cloudflare-tunnel.ts +15 -2
- package/src/lib/docker.ts +103 -49
- package/src/lib/feature-flags.test.ts +157 -0
- package/src/lib/feature-flags.ts +38 -0
- package/src/lib/hatch-local.ts +0 -1
- package/src/lib/local.ts +5 -0
- package/src/lib/nginx-ingress.ts +574 -0
- package/src/lib/ngrok.ts +26 -4
- package/src/lib/platform-client.ts +0 -1
- package/src/lib/retire-local.ts +5 -0
- package/src/lib/statefulset.ts +73 -21
- package/src/lib/sync-cloud-assistants.ts +4 -17
- package/src/lib/upgrade-lifecycle.ts +1 -2
- 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("
|
|
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
|
|
281
|
+
// loadGuardianToken returns a healthy-looking token by default.
|
|
278
282
|
await wake();
|
|
279
283
|
|
|
280
|
-
expect(resetGuardianBootstrapMock).
|
|
281
|
-
|
|
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
|
+
});
|
package/src/commands/flags.ts
CHANGED
|
@@ -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
|
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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 =
|
|
194
|
-
|
|
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(
|
|
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(
|
|
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
|
|