@vellumai/cli 0.10.4-staging.2 → 0.10.5-dev.202607022248.91fa053
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/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +12 -3
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +11 -2
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +29 -1
- package/node_modules/@vellumai/local-mode/src/retire.ts +27 -5
- package/package.json +1 -1
- package/src/__tests__/platform-releases.test.ts +42 -0
- package/src/__tests__/retire.test.ts +353 -0
- package/src/__tests__/teleport.test.ts +8 -0
- package/src/__tests__/workos-pkce.test.ts +0 -7
- package/src/commands/hatch.ts +9 -1
- package/src/commands/login.ts +10 -2
- package/src/commands/retire.ts +197 -24
- package/src/commands/teleport.ts +9 -2
- package/src/lib/docker.ts +22 -4
- package/src/lib/platform-client.ts +14 -0
- package/src/lib/platform-releases.ts +38 -6
- package/src/lib/workos-pkce.ts +1 -2
|
@@ -20,30 +20,74 @@ import { join } from "node:path";
|
|
|
20
20
|
|
|
21
21
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
22
22
|
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
23
|
+
import * as loopbackFetchModule from "../lib/loopback-fetch.js";
|
|
24
|
+
import * as platformClientModule from "../lib/platform-client.js";
|
|
23
25
|
import * as retireLocalModule from "../lib/retire-local.js";
|
|
24
26
|
|
|
25
27
|
const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
|
|
26
28
|
const originalArgv = [...process.argv];
|
|
27
29
|
const originalExit = process.exit;
|
|
28
30
|
const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
|
|
31
|
+
const originalPlatformToken = process.env.VELLUM_PLATFORM_TOKEN;
|
|
29
32
|
const originalStdinIsTTY = process.stdin.isTTY;
|
|
30
33
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
|
31
34
|
const originalStdinIsRaw = process.stdin.isRaw;
|
|
32
35
|
const originalSetRawMode = process.stdin.setRawMode;
|
|
33
36
|
const originalStdoutWrite = process.stdout.write;
|
|
34
37
|
const realRetireLocalModule = { ...retireLocalModule };
|
|
38
|
+
const realPlatformClientModule = { ...platformClientModule };
|
|
39
|
+
const realLoopbackFetchModule = { ...loopbackFetchModule };
|
|
35
40
|
|
|
36
41
|
const retireLocalMock = mock(async () => {});
|
|
42
|
+
const readPlatformTokenMock = mock((): string | null => null);
|
|
43
|
+
const authHeadersMock = mock(async () => ({
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"X-Session-Token": "session-token",
|
|
46
|
+
}));
|
|
47
|
+
const authHeadersForKnownOrganizationMock = mock(
|
|
48
|
+
(token: string, organizationId: string) => ({
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"X-Session-Token": token,
|
|
51
|
+
"Vellum-Organization-Id": organizationId,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const getPlatformUrlMock = mock(() => "https://platform.example.com");
|
|
55
|
+
const readGatewayCredentialMock = mock(
|
|
56
|
+
async (
|
|
57
|
+
_gatewayUrl: string,
|
|
58
|
+
_name: string,
|
|
59
|
+
_bearerToken?: string,
|
|
60
|
+
): Promise<{ value: string | null; unreachable: boolean }> => ({
|
|
61
|
+
value: null,
|
|
62
|
+
unreachable: false,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
const loopbackSafeFetchMock = mock(
|
|
66
|
+
async (): Promise<Response> => new Response(null, { status: 204 }),
|
|
67
|
+
);
|
|
37
68
|
|
|
38
69
|
mock.module("../lib/retire-local.js", () => ({
|
|
39
70
|
...realRetireLocalModule,
|
|
40
71
|
retireLocal: retireLocalMock,
|
|
41
72
|
}));
|
|
42
73
|
|
|
74
|
+
mock.module("../lib/platform-client.js", () => ({
|
|
75
|
+
authHeaders: authHeadersMock,
|
|
76
|
+
authHeadersForKnownOrganization: authHeadersForKnownOrganizationMock,
|
|
77
|
+
getPlatformUrl: getPlatformUrlMock,
|
|
78
|
+
readGatewayCredential: readGatewayCredentialMock,
|
|
79
|
+
readPlatformToken: readPlatformTokenMock,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
mock.module("../lib/loopback-fetch.js", () => ({
|
|
83
|
+
loopbackSafeFetch: loopbackSafeFetchMock,
|
|
84
|
+
}));
|
|
85
|
+
|
|
43
86
|
import { retire } from "../commands/retire.js";
|
|
44
87
|
|
|
45
88
|
let consoleLogSpy: ReturnType<typeof spyOn>;
|
|
46
89
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
90
|
+
let consoleWarnSpy: ReturnType<typeof spyOn>;
|
|
47
91
|
|
|
48
92
|
function makeEntry(
|
|
49
93
|
assistantId: string,
|
|
@@ -123,6 +167,7 @@ function restoreTerminal(): void {
|
|
|
123
167
|
describe("vellum retire", () => {
|
|
124
168
|
beforeEach(() => {
|
|
125
169
|
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
170
|
+
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
126
171
|
rmSync(join(testDir, ".vellum.lock.json"), { force: true });
|
|
127
172
|
process.argv = ["bun", "vellum", "retire"];
|
|
128
173
|
process.exit = ((code?: number) => {
|
|
@@ -130,9 +175,36 @@ describe("vellum retire", () => {
|
|
|
130
175
|
}) as typeof process.exit;
|
|
131
176
|
retireLocalMock.mockReset();
|
|
132
177
|
retireLocalMock.mockResolvedValue(undefined);
|
|
178
|
+
readPlatformTokenMock.mockReset();
|
|
179
|
+
readPlatformTokenMock.mockReturnValue(null);
|
|
180
|
+
authHeadersMock.mockReset();
|
|
181
|
+
authHeadersMock.mockResolvedValue({
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"X-Session-Token": "session-token",
|
|
184
|
+
});
|
|
185
|
+
authHeadersForKnownOrganizationMock.mockReset();
|
|
186
|
+
authHeadersForKnownOrganizationMock.mockImplementation(
|
|
187
|
+
(token: string, organizationId: string) => ({
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"X-Session-Token": token,
|
|
190
|
+
"Vellum-Organization-Id": organizationId,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
getPlatformUrlMock.mockReset();
|
|
194
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.com");
|
|
195
|
+
readGatewayCredentialMock.mockReset();
|
|
196
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
197
|
+
value: null,
|
|
198
|
+
unreachable: false,
|
|
199
|
+
});
|
|
200
|
+
loopbackSafeFetchMock.mockReset();
|
|
201
|
+
loopbackSafeFetchMock.mockResolvedValue(
|
|
202
|
+
new Response(null, { status: 204 }),
|
|
203
|
+
);
|
|
133
204
|
setTerminalMode(false);
|
|
134
205
|
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
135
206
|
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
207
|
+
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
136
208
|
});
|
|
137
209
|
|
|
138
210
|
afterEach(() => {
|
|
@@ -141,15 +213,23 @@ describe("vellum retire", () => {
|
|
|
141
213
|
restoreTerminal();
|
|
142
214
|
consoleLogSpy.mockRestore();
|
|
143
215
|
consoleErrorSpy.mockRestore();
|
|
216
|
+
consoleWarnSpy.mockRestore();
|
|
144
217
|
});
|
|
145
218
|
|
|
146
219
|
afterAll(() => {
|
|
147
220
|
mock.module("../lib/retire-local.js", () => realRetireLocalModule);
|
|
221
|
+
mock.module("../lib/platform-client.js", () => realPlatformClientModule);
|
|
222
|
+
mock.module("../lib/loopback-fetch.js", () => realLoopbackFetchModule);
|
|
148
223
|
if (originalLockfileDir === undefined) {
|
|
149
224
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
150
225
|
} else {
|
|
151
226
|
process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
|
|
152
227
|
}
|
|
228
|
+
if (originalPlatformToken === undefined) {
|
|
229
|
+
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
230
|
+
} else {
|
|
231
|
+
process.env.VELLUM_PLATFORM_TOKEN = originalPlatformToken;
|
|
232
|
+
}
|
|
153
233
|
rmSync(testDir, { recursive: true, force: true });
|
|
154
234
|
});
|
|
155
235
|
|
|
@@ -170,6 +250,279 @@ describe("vellum retire", () => {
|
|
|
170
250
|
);
|
|
171
251
|
});
|
|
172
252
|
|
|
253
|
+
test("local retire unregisters the self-hosted platform record first", async () => {
|
|
254
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
255
|
+
writeLockfile([entry]);
|
|
256
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
257
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test/api");
|
|
258
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
259
|
+
if (name === "vellum:platform_assistant_id") {
|
|
260
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
261
|
+
}
|
|
262
|
+
if (name === "vellum:platform_base_url") {
|
|
263
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
264
|
+
}
|
|
265
|
+
if (name === "vellum:platform_organization_id") {
|
|
266
|
+
return { value: "org-123", unreachable: false };
|
|
267
|
+
}
|
|
268
|
+
return { value: null, unreachable: false };
|
|
269
|
+
});
|
|
270
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
271
|
+
|
|
272
|
+
await retire();
|
|
273
|
+
|
|
274
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
275
|
+
entry.localUrl ?? entry.runtimeUrl,
|
|
276
|
+
"vellum:platform_assistant_id",
|
|
277
|
+
entry.bearerToken,
|
|
278
|
+
);
|
|
279
|
+
expect(authHeadersMock).not.toHaveBeenCalled();
|
|
280
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
281
|
+
"https://platform.example.test/api/v1/assistants/platform-assistant-1/retire/",
|
|
282
|
+
{
|
|
283
|
+
method: "DELETE",
|
|
284
|
+
headers: {
|
|
285
|
+
"Content-Type": "application/json",
|
|
286
|
+
"X-Session-Token": "session-token",
|
|
287
|
+
"Vellum-Organization-Id": "org-123",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
292
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("local retire reads platform registration through the loopback gateway URL", async () => {
|
|
296
|
+
const localGatewayUrl = "http://127.0.0.1:7831";
|
|
297
|
+
const entry = makeEntry("assistant-1", {
|
|
298
|
+
name: "Example Assistant",
|
|
299
|
+
runtimeUrl: "http://assistant-1.local:7831",
|
|
300
|
+
localUrl: localGatewayUrl,
|
|
301
|
+
});
|
|
302
|
+
writeLockfile([entry]);
|
|
303
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
304
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
305
|
+
readGatewayCredentialMock.mockImplementation(async (gatewayUrl, name) => {
|
|
306
|
+
expect(gatewayUrl).toBe(localGatewayUrl);
|
|
307
|
+
if (name === "vellum:platform_assistant_id") {
|
|
308
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
309
|
+
}
|
|
310
|
+
if (name === "vellum:platform_base_url") {
|
|
311
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
312
|
+
}
|
|
313
|
+
if (name === "vellum:platform_organization_id") {
|
|
314
|
+
return { value: "org-123", unreachable: false };
|
|
315
|
+
}
|
|
316
|
+
return { value: null, unreachable: false };
|
|
317
|
+
});
|
|
318
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
319
|
+
|
|
320
|
+
await retire();
|
|
321
|
+
|
|
322
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
323
|
+
localGatewayUrl,
|
|
324
|
+
"vellum:platform_assistant_id",
|
|
325
|
+
entry.bearerToken,
|
|
326
|
+
);
|
|
327
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
328
|
+
localGatewayUrl,
|
|
329
|
+
"vellum:platform_base_url",
|
|
330
|
+
entry.bearerToken,
|
|
331
|
+
);
|
|
332
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
333
|
+
localGatewayUrl,
|
|
334
|
+
"vellum:platform_organization_id",
|
|
335
|
+
entry.bearerToken,
|
|
336
|
+
);
|
|
337
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
338
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
339
|
+
expect.any(Object),
|
|
340
|
+
);
|
|
341
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("local retire uses saved platform registration when local credentials are unreachable", async () => {
|
|
345
|
+
const entry = makeEntry("assistant-1", {
|
|
346
|
+
name: "Example Assistant",
|
|
347
|
+
platformAssistantId: "platform-assistant-1",
|
|
348
|
+
platformBaseUrl: "https://platform.example.test",
|
|
349
|
+
platformOrganizationId: "org-123",
|
|
350
|
+
});
|
|
351
|
+
writeLockfile([entry]);
|
|
352
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
353
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
354
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
355
|
+
value: null,
|
|
356
|
+
unreachable: true,
|
|
357
|
+
});
|
|
358
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
359
|
+
|
|
360
|
+
await retire();
|
|
361
|
+
|
|
362
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
363
|
+
entry.localUrl ?? entry.runtimeUrl,
|
|
364
|
+
"vellum:platform_assistant_id",
|
|
365
|
+
entry.bearerToken,
|
|
366
|
+
);
|
|
367
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
368
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
369
|
+
{
|
|
370
|
+
method: "DELETE",
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
"X-Session-Token": "session-token",
|
|
374
|
+
"Vellum-Organization-Id": "org-123",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
379
|
+
"using saved platform registration",
|
|
380
|
+
);
|
|
381
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
382
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("local retire continues when local credentials are unreachable and no saved platform registration exists", async () => {
|
|
386
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
387
|
+
writeLockfile([entry]);
|
|
388
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
389
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
390
|
+
value: null,
|
|
391
|
+
unreachable: true,
|
|
392
|
+
});
|
|
393
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
394
|
+
|
|
395
|
+
await retire();
|
|
396
|
+
|
|
397
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
398
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
399
|
+
"no saved platform registration is available",
|
|
400
|
+
);
|
|
401
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
402
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("local retire skips platform unregister when stored platform URL origin mismatches", async () => {
|
|
406
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
407
|
+
writeLockfile([entry]);
|
|
408
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
409
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.com");
|
|
410
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
411
|
+
if (name === "vellum:platform_assistant_id") {
|
|
412
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
413
|
+
}
|
|
414
|
+
if (name === "vellum:platform_base_url") {
|
|
415
|
+
return { value: "https://malicious.example.test", unreachable: false };
|
|
416
|
+
}
|
|
417
|
+
if (name === "vellum:platform_organization_id") {
|
|
418
|
+
return { value: "org-123", unreachable: false };
|
|
419
|
+
}
|
|
420
|
+
return { value: null, unreachable: false };
|
|
421
|
+
});
|
|
422
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
423
|
+
|
|
424
|
+
await retire();
|
|
425
|
+
|
|
426
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
427
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
428
|
+
"Stored platform URL does not match the configured platform URL",
|
|
429
|
+
);
|
|
430
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("local retire skips platform unregister when stored organization ID is missing", async () => {
|
|
434
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
435
|
+
writeLockfile([entry]);
|
|
436
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
437
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
438
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
439
|
+
if (name === "vellum:platform_assistant_id") {
|
|
440
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
441
|
+
}
|
|
442
|
+
if (name === "vellum:platform_base_url") {
|
|
443
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
444
|
+
}
|
|
445
|
+
return { value: null, unreachable: false };
|
|
446
|
+
});
|
|
447
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
448
|
+
|
|
449
|
+
await retire();
|
|
450
|
+
|
|
451
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
452
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
453
|
+
"Could not read platform organization ID",
|
|
454
|
+
);
|
|
455
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
456
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("local retire uses injected platform token from the app host", async () => {
|
|
460
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
461
|
+
writeLockfile([entry]);
|
|
462
|
+
process.env.VELLUM_PLATFORM_TOKEN = "host-session-token";
|
|
463
|
+
readPlatformTokenMock.mockReturnValue(null);
|
|
464
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
465
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
466
|
+
if (name === "vellum:platform_assistant_id") {
|
|
467
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
468
|
+
}
|
|
469
|
+
if (name === "vellum:platform_base_url") {
|
|
470
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
471
|
+
}
|
|
472
|
+
if (name === "vellum:platform_organization_id") {
|
|
473
|
+
return { value: "org-123", unreachable: false };
|
|
474
|
+
}
|
|
475
|
+
return { value: null, unreachable: false };
|
|
476
|
+
});
|
|
477
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
478
|
+
|
|
479
|
+
await retire();
|
|
480
|
+
|
|
481
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
482
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
483
|
+
{
|
|
484
|
+
method: "DELETE",
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
"X-Session-Token": "host-session-token",
|
|
488
|
+
"Vellum-Organization-Id": "org-123",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("local retire continues when self-hosted platform unregister fails", async () => {
|
|
496
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
497
|
+
writeLockfile([entry]);
|
|
498
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
499
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
500
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
501
|
+
if (name === "vellum:platform_assistant_id") {
|
|
502
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
503
|
+
}
|
|
504
|
+
if (name === "vellum:platform_base_url") {
|
|
505
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
506
|
+
}
|
|
507
|
+
if (name === "vellum:platform_organization_id") {
|
|
508
|
+
return { value: "org-123", unreachable: false };
|
|
509
|
+
}
|
|
510
|
+
return { value: null, unreachable: false };
|
|
511
|
+
});
|
|
512
|
+
loopbackSafeFetchMock.mockResolvedValue(
|
|
513
|
+
new Response("platform unavailable", { status: 503 }),
|
|
514
|
+
);
|
|
515
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
516
|
+
|
|
517
|
+
await retire();
|
|
518
|
+
|
|
519
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
520
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
521
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
522
|
+
"Platform self-hosted unregister failed (503): platform unavailable",
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
173
526
|
test("non-interactive retire without --yes fails before deleting", async () => {
|
|
174
527
|
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
175
528
|
writeLockfile([entry]);
|
|
@@ -2266,6 +2266,14 @@ describe("platform credential injection", () => {
|
|
|
2266
2266
|
userId: "user-1",
|
|
2267
2267
|
webhookSecret: "webhook-secret-123",
|
|
2268
2268
|
});
|
|
2269
|
+
expect(saveAssistantEntryMock).toHaveBeenCalledWith(
|
|
2270
|
+
expect.objectContaining({
|
|
2271
|
+
assistantId: "my-local",
|
|
2272
|
+
platformAssistantId: "platform-assistant-1",
|
|
2273
|
+
platformBaseUrl: "https://platform.vellum.ai",
|
|
2274
|
+
platformOrganizationId: "org-1",
|
|
2275
|
+
}),
|
|
2276
|
+
);
|
|
2269
2277
|
} finally {
|
|
2270
2278
|
restoreFetch();
|
|
2271
2279
|
}
|
|
@@ -76,13 +76,6 @@ describe("buildAuthorizeUrl", () => {
|
|
|
76
76
|
expect(url.searchParams.has("prompt")).toBe(false);
|
|
77
77
|
});
|
|
78
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
79
|
test("login hint is forwarded", () => {
|
|
87
80
|
const url = new URL(
|
|
88
81
|
buildAuthorizeUrl({ ...base, loginHint: "user@example.com" }),
|
package/src/commands/hatch.ts
CHANGED
|
@@ -178,6 +178,7 @@ interface HatchArgs {
|
|
|
178
178
|
remote: RemoteHost;
|
|
179
179
|
watch: boolean;
|
|
180
180
|
sourcePath: string | null;
|
|
181
|
+
preview: boolean;
|
|
181
182
|
configValues: Record<string, string>;
|
|
182
183
|
flagEnvVars: Record<string, string>;
|
|
183
184
|
analyze: boolean;
|
|
@@ -203,6 +204,7 @@ function parseArgs(): HatchArgs {
|
|
|
203
204
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
204
205
|
let watch = false;
|
|
205
206
|
let sourcePath: string | null = null;
|
|
207
|
+
let preview = false;
|
|
206
208
|
const configValues: Record<string, string> = {};
|
|
207
209
|
let analyze = false;
|
|
208
210
|
let netnsContainer: string | null = null;
|
|
@@ -231,6 +233,7 @@ function parseArgs(): HatchArgs {
|
|
|
231
233
|
);
|
|
232
234
|
console.log(
|
|
233
235
|
" --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
|
|
236
|
+
" --preview When pulling published images (no local source), resolve from the preview channel (latest preview release) instead of latest-stable. Also settable via VELLUM_HATCH_CHANNEL=preview.",
|
|
234
237
|
);
|
|
235
238
|
console.log(
|
|
236
239
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
@@ -263,6 +266,8 @@ function parseArgs(): HatchArgs {
|
|
|
263
266
|
watch = true;
|
|
264
267
|
} else if (arg === "--analyze") {
|
|
265
268
|
analyze = true;
|
|
269
|
+
} else if (arg === "--preview") {
|
|
270
|
+
preview = true;
|
|
266
271
|
} else if (arg === "--source") {
|
|
267
272
|
const next = args[i + 1];
|
|
268
273
|
if (!next || next.startsWith("-")) {
|
|
@@ -349,7 +354,7 @@ function parseArgs(): HatchArgs {
|
|
|
349
354
|
species = arg as Species;
|
|
350
355
|
} else {
|
|
351
356
|
console.error(
|
|
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>`,
|
|
357
|
+
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --preview, --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>`,
|
|
353
358
|
);
|
|
354
359
|
process.exit(1);
|
|
355
360
|
}
|
|
@@ -363,6 +368,7 @@ function parseArgs(): HatchArgs {
|
|
|
363
368
|
remote,
|
|
364
369
|
watch,
|
|
365
370
|
sourcePath,
|
|
371
|
+
preview,
|
|
366
372
|
configValues,
|
|
367
373
|
flagEnvVars,
|
|
368
374
|
analyze,
|
|
@@ -602,6 +608,7 @@ export async function hatch(): Promise<void> {
|
|
|
602
608
|
remote,
|
|
603
609
|
watch,
|
|
604
610
|
sourcePath,
|
|
611
|
+
preview,
|
|
605
612
|
configValues,
|
|
606
613
|
flagEnvVars,
|
|
607
614
|
analyze,
|
|
@@ -681,6 +688,7 @@ export async function hatch(): Promise<void> {
|
|
|
681
688
|
flagEnvVars,
|
|
682
689
|
sourcePath,
|
|
683
690
|
analyze,
|
|
691
|
+
channel: preview ? "preview" : undefined,
|
|
684
692
|
netnsContainer: netnsContainer ?? undefined,
|
|
685
693
|
gatewayPort: gatewayPort ?? undefined,
|
|
686
694
|
assistantCaCertPath: assistantCaCert ?? undefined,
|
package/src/commands/login.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
loadAllAssistants,
|
|
8
8
|
removeAssistantEntry,
|
|
9
9
|
resolveAssistant,
|
|
10
|
+
saveAssistantEntry,
|
|
10
11
|
setActiveAssistant,
|
|
11
12
|
} from "../lib/assistant-config";
|
|
12
13
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
@@ -383,6 +384,7 @@ export async function login(): Promise<void> {
|
|
|
383
384
|
fetchCurrentVersion(entry.runtimeUrl),
|
|
384
385
|
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
385
386
|
]);
|
|
387
|
+
const platformUrl = getPlatformUrl();
|
|
386
388
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
387
389
|
token,
|
|
388
390
|
orgId,
|
|
@@ -390,9 +392,15 @@ export async function login(): Promise<void> {
|
|
|
390
392
|
entry.assistantId,
|
|
391
393
|
"cli",
|
|
392
394
|
assistantVersion,
|
|
393
|
-
|
|
395
|
+
platformUrl,
|
|
394
396
|
ingressUrl,
|
|
395
397
|
);
|
|
398
|
+
saveAssistantEntry({
|
|
399
|
+
...entry,
|
|
400
|
+
platformAssistantId: registration.assistant.id,
|
|
401
|
+
platformBaseUrl: platformUrl,
|
|
402
|
+
platformOrganizationId: orgId,
|
|
403
|
+
});
|
|
396
404
|
console.log(
|
|
397
405
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
398
406
|
);
|
|
@@ -433,7 +441,7 @@ export async function login(): Promise<void> {
|
|
|
433
441
|
bearerToken: entry.bearerToken,
|
|
434
442
|
assistantApiKey,
|
|
435
443
|
platformAssistantId: registration.assistant.id,
|
|
436
|
-
platformBaseUrl:
|
|
444
|
+
platformBaseUrl: platformUrl,
|
|
437
445
|
organizationId: orgId,
|
|
438
446
|
userId: user.id,
|
|
439
447
|
webhookSecret: registration.webhook_secret,
|