@vellumai/cli 0.10.4-dev.202607022227.14c7770 → 0.10.4-staging.2
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/index.ts +3 -12
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +2 -11
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +0 -6
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +1 -29
- package/node_modules/@vellumai/local-mode/src/retire.ts +5 -27
- package/package.json +1 -1
- package/src/__tests__/platform-releases.test.ts +0 -42
- package/src/__tests__/retire.test.ts +0 -353
- package/src/__tests__/teleport.test.ts +0 -8
- package/src/__tests__/workos-pkce.test.ts +7 -0
- package/src/commands/hatch.ts +1 -9
- package/src/commands/login.ts +2 -10
- package/src/commands/retire.ts +24 -197
- package/src/commands/teleport.ts +2 -9
- package/src/lib/docker.ts +4 -22
- package/src/lib/platform-client.ts +0 -14
- package/src/lib/platform-releases.ts +6 -38
- package/src/lib/workos-pkce.ts +2 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +0 -79
|
@@ -76,6 +76,13 @@ 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
|
+
|
|
79
86
|
test("login hint is forwarded", () => {
|
|
80
87
|
const url = new URL(
|
|
81
88
|
buildAuthorizeUrl({ ...base, loginHint: "user@example.com" }),
|
package/src/commands/hatch.ts
CHANGED
|
@@ -178,7 +178,6 @@ interface HatchArgs {
|
|
|
178
178
|
remote: RemoteHost;
|
|
179
179
|
watch: boolean;
|
|
180
180
|
sourcePath: string | null;
|
|
181
|
-
preview: boolean;
|
|
182
181
|
configValues: Record<string, string>;
|
|
183
182
|
flagEnvVars: Record<string, string>;
|
|
184
183
|
analyze: boolean;
|
|
@@ -204,7 +203,6 @@ function parseArgs(): HatchArgs {
|
|
|
204
203
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
205
204
|
let watch = false;
|
|
206
205
|
let sourcePath: string | null = null;
|
|
207
|
-
let preview = false;
|
|
208
206
|
const configValues: Record<string, string> = {};
|
|
209
207
|
let analyze = false;
|
|
210
208
|
let netnsContainer: string | null = null;
|
|
@@ -233,7 +231,6 @@ function parseArgs(): HatchArgs {
|
|
|
233
231
|
);
|
|
234
232
|
console.log(
|
|
235
233
|
" --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.",
|
|
237
234
|
);
|
|
238
235
|
console.log(
|
|
239
236
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
@@ -266,8 +263,6 @@ function parseArgs(): HatchArgs {
|
|
|
266
263
|
watch = true;
|
|
267
264
|
} else if (arg === "--analyze") {
|
|
268
265
|
analyze = true;
|
|
269
|
-
} else if (arg === "--preview") {
|
|
270
|
-
preview = true;
|
|
271
266
|
} else if (arg === "--source") {
|
|
272
267
|
const next = args[i + 1];
|
|
273
268
|
if (!next || next.startsWith("-")) {
|
|
@@ -354,7 +349,7 @@ function parseArgs(): HatchArgs {
|
|
|
354
349
|
species = arg as Species;
|
|
355
350
|
} else {
|
|
356
351
|
console.error(
|
|
357
|
-
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --
|
|
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>`,
|
|
358
353
|
);
|
|
359
354
|
process.exit(1);
|
|
360
355
|
}
|
|
@@ -368,7 +363,6 @@ function parseArgs(): HatchArgs {
|
|
|
368
363
|
remote,
|
|
369
364
|
watch,
|
|
370
365
|
sourcePath,
|
|
371
|
-
preview,
|
|
372
366
|
configValues,
|
|
373
367
|
flagEnvVars,
|
|
374
368
|
analyze,
|
|
@@ -608,7 +602,6 @@ export async function hatch(): Promise<void> {
|
|
|
608
602
|
remote,
|
|
609
603
|
watch,
|
|
610
604
|
sourcePath,
|
|
611
|
-
preview,
|
|
612
605
|
configValues,
|
|
613
606
|
flagEnvVars,
|
|
614
607
|
analyze,
|
|
@@ -688,7 +681,6 @@ export async function hatch(): Promise<void> {
|
|
|
688
681
|
flagEnvVars,
|
|
689
682
|
sourcePath,
|
|
690
683
|
analyze,
|
|
691
|
-
channel: preview ? "preview" : undefined,
|
|
692
684
|
netnsContainer: netnsContainer ?? undefined,
|
|
693
685
|
gatewayPort: gatewayPort ?? undefined,
|
|
694
686
|
assistantCaCertPath: assistantCaCert ?? undefined,
|
package/src/commands/login.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
loadAllAssistants,
|
|
8
8
|
removeAssistantEntry,
|
|
9
9
|
resolveAssistant,
|
|
10
|
-
saveAssistantEntry,
|
|
11
10
|
setActiveAssistant,
|
|
12
11
|
} from "../lib/assistant-config";
|
|
13
12
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
@@ -384,7 +383,6 @@ export async function login(): Promise<void> {
|
|
|
384
383
|
fetchCurrentVersion(entry.runtimeUrl),
|
|
385
384
|
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
386
385
|
]);
|
|
387
|
-
const platformUrl = getPlatformUrl();
|
|
388
386
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
389
387
|
token,
|
|
390
388
|
orgId,
|
|
@@ -392,15 +390,9 @@ export async function login(): Promise<void> {
|
|
|
392
390
|
entry.assistantId,
|
|
393
391
|
"cli",
|
|
394
392
|
assistantVersion,
|
|
395
|
-
|
|
393
|
+
getPlatformUrl(),
|
|
396
394
|
ingressUrl,
|
|
397
395
|
);
|
|
398
|
-
saveAssistantEntry({
|
|
399
|
-
...entry,
|
|
400
|
-
platformAssistantId: registration.assistant.id,
|
|
401
|
-
platformBaseUrl: platformUrl,
|
|
402
|
-
platformOrganizationId: orgId,
|
|
403
|
-
});
|
|
404
396
|
console.log(
|
|
405
397
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
406
398
|
);
|
|
@@ -441,7 +433,7 @@ export async function login(): Promise<void> {
|
|
|
441
433
|
bearerToken: entry.bearerToken,
|
|
442
434
|
assistantApiKey,
|
|
443
435
|
platformAssistantId: registration.assistant.id,
|
|
444
|
-
platformBaseUrl:
|
|
436
|
+
platformBaseUrl: getPlatformUrl(),
|
|
445
437
|
organizationId: orgId,
|
|
446
438
|
userId: user.id,
|
|
447
439
|
webhookSecret: registration.webhook_secret,
|
package/src/commands/retire.ts
CHANGED
|
@@ -20,9 +20,7 @@ import { getConfigDir } from "../lib/environments/paths.js";
|
|
|
20
20
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
21
21
|
import {
|
|
22
22
|
authHeaders,
|
|
23
|
-
authHeadersForKnownOrganization,
|
|
24
23
|
getPlatformUrl,
|
|
25
|
-
readGatewayCredential,
|
|
26
24
|
readPlatformToken,
|
|
27
25
|
} from "../lib/platform-client.js";
|
|
28
26
|
import { retireInstance as retireAwsInstance } from "../lib/aws.js";
|
|
@@ -47,180 +45,6 @@ interface RetireArgs {
|
|
|
47
45
|
yes: boolean;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
interface SelfHostedPlatformRegistration {
|
|
51
|
-
platformAssistantId: string;
|
|
52
|
-
platformBaseUrl: string | null;
|
|
53
|
-
organizationId: string | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const PLATFORM_TOKEN_ENV = "VELLUM_PLATFORM_TOKEN";
|
|
57
|
-
|
|
58
|
-
function readPlatformRetireToken(): string | null {
|
|
59
|
-
const envToken = process.env[PLATFORM_TOKEN_ENV]?.trim();
|
|
60
|
-
if (envToken) return envToken;
|
|
61
|
-
return readPlatformToken();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function platformOrigin(platformUrl: string): string | null {
|
|
65
|
-
try {
|
|
66
|
-
return new URL(platformUrl).origin;
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function resolveTrustedSelfHostedPlatformUrl(
|
|
73
|
-
storedPlatformUrl: string | null,
|
|
74
|
-
): string | null {
|
|
75
|
-
const configuredPlatformUrl = getPlatformUrl();
|
|
76
|
-
if (!storedPlatformUrl) return configuredPlatformUrl;
|
|
77
|
-
|
|
78
|
-
const storedOrigin = platformOrigin(storedPlatformUrl);
|
|
79
|
-
const configuredOrigin = platformOrigin(configuredPlatformUrl);
|
|
80
|
-
if (storedOrigin && configuredOrigin && storedOrigin === configuredOrigin) {
|
|
81
|
-
return configuredPlatformUrl;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.warn(
|
|
85
|
-
"⚠️ Stored platform URL does not match the configured platform URL; skipping platform unregister.",
|
|
86
|
-
);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function nonEmptyString(value: string | null | undefined): string | null {
|
|
91
|
-
const trimmed = value?.trim();
|
|
92
|
-
return trimmed ? trimmed : null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function lockfileSelfHostedPlatformRegistration(
|
|
96
|
-
entry: AssistantEntry,
|
|
97
|
-
): SelfHostedPlatformRegistration | null {
|
|
98
|
-
const platformAssistantId = nonEmptyString(entry.platformAssistantId);
|
|
99
|
-
if (!platformAssistantId) return null;
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
platformAssistantId,
|
|
103
|
-
platformBaseUrl: nonEmptyString(entry.platformBaseUrl),
|
|
104
|
-
organizationId: nonEmptyString(entry.platformOrganizationId),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function deletePlatformAssistant(
|
|
109
|
-
assistantId: string,
|
|
110
|
-
options: {
|
|
111
|
-
token: string;
|
|
112
|
-
platformUrl?: string;
|
|
113
|
-
organizationId?: string;
|
|
114
|
-
label: string;
|
|
115
|
-
successMessage: string;
|
|
116
|
-
alreadyGoneMessage: string;
|
|
117
|
-
},
|
|
118
|
-
): Promise<void> {
|
|
119
|
-
const platformUrl = options.platformUrl || getPlatformUrl();
|
|
120
|
-
const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
|
|
121
|
-
const headers = options.organizationId
|
|
122
|
-
? authHeadersForKnownOrganization(options.token, options.organizationId)
|
|
123
|
-
: await authHeaders(options.token, platformUrl);
|
|
124
|
-
|
|
125
|
-
const response = await loopbackSafeFetch(url, {
|
|
126
|
-
method: "DELETE",
|
|
127
|
-
headers,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (!response.ok && response.status !== 404) {
|
|
131
|
-
const body = await response.text();
|
|
132
|
-
throw new Error(`${options.label} failed (${response.status}): ${body}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (response.status === 404) {
|
|
136
|
-
console.log(options.alreadyGoneMessage);
|
|
137
|
-
} else {
|
|
138
|
-
console.log(options.successMessage);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function readSelfHostedPlatformRegistration(
|
|
143
|
-
entry: AssistantEntry,
|
|
144
|
-
): Promise<SelfHostedPlatformRegistration | null> {
|
|
145
|
-
const fallback = lockfileSelfHostedPlatformRegistration(entry);
|
|
146
|
-
const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
|
|
147
|
-
const platformAssistant = await readGatewayCredential(
|
|
148
|
-
gatewayUrl,
|
|
149
|
-
"vellum:platform_assistant_id",
|
|
150
|
-
entry.bearerToken,
|
|
151
|
-
);
|
|
152
|
-
if (platformAssistant.unreachable) {
|
|
153
|
-
if (fallback) {
|
|
154
|
-
console.warn(
|
|
155
|
-
"\u26a0\ufe0f Could not read platform registration from the local assistant; using saved platform registration.",
|
|
156
|
-
);
|
|
157
|
-
return fallback;
|
|
158
|
-
}
|
|
159
|
-
console.warn(
|
|
160
|
-
"\u26a0\ufe0f Could not read platform registration from the local assistant and no saved platform registration is available; skipping platform unregister.",
|
|
161
|
-
);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const platformAssistantId =
|
|
166
|
-
nonEmptyString(platformAssistant.value) ?? fallback?.platformAssistantId;
|
|
167
|
-
if (!platformAssistantId) return null;
|
|
168
|
-
|
|
169
|
-
const [platformBaseUrl, organizationId] = await Promise.all([
|
|
170
|
-
readGatewayCredential(
|
|
171
|
-
gatewayUrl,
|
|
172
|
-
"vellum:platform_base_url",
|
|
173
|
-
entry.bearerToken,
|
|
174
|
-
),
|
|
175
|
-
readGatewayCredential(
|
|
176
|
-
gatewayUrl,
|
|
177
|
-
"vellum:platform_organization_id",
|
|
178
|
-
entry.bearerToken,
|
|
179
|
-
),
|
|
180
|
-
]);
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
platformAssistantId,
|
|
184
|
-
platformBaseUrl:
|
|
185
|
-
nonEmptyString(platformBaseUrl.value) ??
|
|
186
|
-
fallback?.platformBaseUrl ??
|
|
187
|
-
null,
|
|
188
|
-
organizationId:
|
|
189
|
-
nonEmptyString(organizationId.value) ?? fallback?.organizationId ?? null,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function unregisterSelfHostedLocalPlatformRecord(
|
|
194
|
-
entry: AssistantEntry,
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
const token = readPlatformRetireToken();
|
|
197
|
-
if (!token) return;
|
|
198
|
-
|
|
199
|
-
const registration = await readSelfHostedPlatformRegistration(entry);
|
|
200
|
-
if (!registration) return;
|
|
201
|
-
|
|
202
|
-
const platformUrl = resolveTrustedSelfHostedPlatformUrl(
|
|
203
|
-
registration.platformBaseUrl,
|
|
204
|
-
);
|
|
205
|
-
if (!platformUrl) return;
|
|
206
|
-
if (!registration.organizationId) {
|
|
207
|
-
console.warn(
|
|
208
|
-
"⚠️ Could not read platform organization ID from the local assistant; skipping platform unregister.",
|
|
209
|
-
);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
await deletePlatformAssistant(registration.platformAssistantId, {
|
|
214
|
-
token,
|
|
215
|
-
platformUrl,
|
|
216
|
-
organizationId: registration.organizationId,
|
|
217
|
-
label: "Platform self-hosted unregister",
|
|
218
|
-
successMessage: "\u2705 Platform self-hosted registration unregistered.",
|
|
219
|
-
alreadyGoneMessage:
|
|
220
|
-
"\u2705 Platform self-hosted registration already unregistered (404).",
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
48
|
async function retireCustom(entry: AssistantEntry): Promise<void> {
|
|
225
49
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
226
50
|
const sshUser = entry.sshUser ?? "root";
|
|
@@ -262,7 +86,7 @@ async function retireVellum(
|
|
|
262
86
|
): Promise<void> {
|
|
263
87
|
console.log("\u{1F5D1}\ufe0f Retiring platform-hosted instance...\n");
|
|
264
88
|
|
|
265
|
-
const token =
|
|
89
|
+
const token = readPlatformToken();
|
|
266
90
|
if (!token) {
|
|
267
91
|
console.error(
|
|
268
92
|
"Error: Not logged in. Run `vellum login --token <token>` first.",
|
|
@@ -270,21 +94,33 @@ async function retireVellum(
|
|
|
270
94
|
process.exit(1);
|
|
271
95
|
}
|
|
272
96
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
97
|
+
const platformUrl = runtimeUrl || getPlatformUrl();
|
|
98
|
+
const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
|
|
99
|
+
const response = await loopbackSafeFetch(url, {
|
|
100
|
+
method: "DELETE",
|
|
101
|
+
headers: await authHeaders(token, runtimeUrl),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Treat 404 as success: the assistant is already gone from the platform
|
|
105
|
+
// (previously retired, deleted from the web UI, or retired from another
|
|
106
|
+
// device) so the caller's job is done. Falling through to the lockfile
|
|
107
|
+
// cleanup avoids leaving a stale entry that would otherwise wedge the
|
|
108
|
+
// macOS app in a permanent health-check loop.
|
|
109
|
+
if (!response.ok && response.status !== 404) {
|
|
110
|
+
const body = await response.text();
|
|
283
111
|
console.error(
|
|
284
|
-
`Error:
|
|
112
|
+
`Error: Platform retire failed (${response.status}): ${body}`,
|
|
285
113
|
);
|
|
286
114
|
process.exit(1);
|
|
287
115
|
}
|
|
116
|
+
|
|
117
|
+
if (response.status === 404) {
|
|
118
|
+
console.log(
|
|
119
|
+
"\u2705 Platform-hosted instance already retired (404) — cleaning up local state.",
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
console.log("\u2705 Platform-hosted instance retired.");
|
|
123
|
+
}
|
|
288
124
|
}
|
|
289
125
|
|
|
290
126
|
function parseRetireArgs(args: string[]): RetireArgs {
|
|
@@ -467,15 +303,6 @@ async function retireInner(): Promise<void> {
|
|
|
467
303
|
} else if (cloud === "docker") {
|
|
468
304
|
await retireDocker(assistantId);
|
|
469
305
|
} else if (cloud === "local") {
|
|
470
|
-
try {
|
|
471
|
-
await unregisterSelfHostedLocalPlatformRecord(entry);
|
|
472
|
-
} catch (error) {
|
|
473
|
-
console.warn(
|
|
474
|
-
`⚠️ Platform self-hosted unregister failed; continuing local retire: ${
|
|
475
|
-
error instanceof Error ? error.message : String(error)
|
|
476
|
-
}`,
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
306
|
await retireLocal(assistantId, entry);
|
|
480
307
|
} else if (cloud === "custom") {
|
|
481
308
|
await retireCustom(entry);
|
package/src/commands/teleport.ts
CHANGED
|
@@ -1123,7 +1123,6 @@ async function tryInjectPlatformCredentials(
|
|
|
1123
1123
|
fetchCurrentVersion(entry.runtimeUrl),
|
|
1124
1124
|
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
1125
1125
|
]);
|
|
1126
|
-
const platformUrl = getPlatformUrl();
|
|
1127
1126
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
1128
1127
|
token,
|
|
1129
1128
|
orgId,
|
|
@@ -1131,15 +1130,9 @@ async function tryInjectPlatformCredentials(
|
|
|
1131
1130
|
entry.assistantId,
|
|
1132
1131
|
"cli",
|
|
1133
1132
|
assistantVersion,
|
|
1134
|
-
|
|
1133
|
+
getPlatformUrl(),
|
|
1135
1134
|
ingressUrl,
|
|
1136
1135
|
);
|
|
1137
|
-
saveAssistantEntry({
|
|
1138
|
-
...entry,
|
|
1139
|
-
platformAssistantId: registration.assistant.id,
|
|
1140
|
-
platformBaseUrl: platformUrl,
|
|
1141
|
-
platformOrganizationId: orgId,
|
|
1142
|
-
});
|
|
1143
1136
|
|
|
1144
1137
|
// Resolve the API key: 1) fresh from registration, 2) existing from
|
|
1145
1138
|
// daemon credential store, 3) reprovision as last resort (revokes old key).
|
|
@@ -1171,7 +1164,7 @@ async function tryInjectPlatformCredentials(
|
|
|
1171
1164
|
bearerToken: entry.bearerToken,
|
|
1172
1165
|
assistantApiKey,
|
|
1173
1166
|
platformAssistantId: registration.assistant.id,
|
|
1174
|
-
platformBaseUrl:
|
|
1167
|
+
platformBaseUrl: getPlatformUrl(),
|
|
1175
1168
|
organizationId: orgId,
|
|
1176
1169
|
userId: user.id,
|
|
1177
1170
|
webhookSecret: registration.webhook_secret,
|
package/src/lib/docker.ts
CHANGED
|
@@ -35,9 +35,8 @@ import {
|
|
|
35
35
|
loadImageViaHost,
|
|
36
36
|
} from "./host-image-loader.js";
|
|
37
37
|
import {
|
|
38
|
-
|
|
38
|
+
fetchLatestStableVersion,
|
|
39
39
|
resolveImageRefs,
|
|
40
|
-
type ReleaseChannel,
|
|
41
40
|
} from "./platform-releases.js";
|
|
42
41
|
import {
|
|
43
42
|
configureHatchProviderApiKey,
|
|
@@ -322,15 +321,6 @@ export interface HatchDockerParams {
|
|
|
322
321
|
* connection.
|
|
323
322
|
*/
|
|
324
323
|
assistantCaCertPath?: string;
|
|
325
|
-
/**
|
|
326
|
-
* Release channel to resolve published images from when hatching without a
|
|
327
|
-
* local source tree (the image-pull fallback). `stable` (default) keeps the
|
|
328
|
-
* latest-stable behavior; `preview` pulls the latest preview release. Only
|
|
329
|
-
* affects the pull path — a local source build ignores it. Falls back to
|
|
330
|
-
* the `VELLUM_HATCH_CHANNEL` env var when unset, so callers that hatch via
|
|
331
|
-
* env (e.g. evals) can opt in without changing the invocation.
|
|
332
|
-
*/
|
|
333
|
-
channel?: ReleaseChannel;
|
|
334
324
|
}
|
|
335
325
|
|
|
336
326
|
export type DockerProviderCredentialSetupAction =
|
|
@@ -1103,14 +1093,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1103
1093
|
flagEnvVars = {},
|
|
1104
1094
|
} = params;
|
|
1105
1095
|
let watch = params.watch ?? false;
|
|
1106
|
-
// Resolve the release channel for the image-pull fallback: explicit param
|
|
1107
|
-
// wins, then the VELLUM_HATCH_CHANNEL env var, else stable. Any value other
|
|
1108
|
-
// than "preview" (case-insensitive) is treated as stable.
|
|
1109
|
-
const channel: ReleaseChannel =
|
|
1110
|
-
params.channel ??
|
|
1111
|
-
(process.env.VELLUM_HATCH_CHANNEL?.trim().toLowerCase() === "preview"
|
|
1112
|
-
? "preview"
|
|
1113
|
-
: "stable");
|
|
1114
1096
|
|
|
1115
1097
|
resetLogFile("hatch.log");
|
|
1116
1098
|
const provider =
|
|
@@ -1268,8 +1250,8 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1268
1250
|
// Resolve image refs from a remote source that may have dev/local
|
|
1269
1251
|
// builds. If resolution is unavailable, fall back to the CLI's own
|
|
1270
1252
|
// version so a default tag can still be resolved.
|
|
1271
|
-
log(
|
|
1272
|
-
const latestVersion = await
|
|
1253
|
+
log("🔍 Fetching latest stable release...");
|
|
1254
|
+
const latestVersion = await fetchLatestStableVersion();
|
|
1273
1255
|
let versionTag: string;
|
|
1274
1256
|
if (latestVersion) {
|
|
1275
1257
|
versionTag = latestVersion.startsWith("v")
|
|
@@ -1283,7 +1265,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1283
1265
|
);
|
|
1284
1266
|
}
|
|
1285
1267
|
log("🔍 Resolving image references...");
|
|
1286
|
-
const resolved = await resolveImageRefs(versionTag, log
|
|
1268
|
+
const resolved = await resolveImageRefs(versionTag, log);
|
|
1287
1269
|
imageTags.assistant = resolved.imageTags.assistant;
|
|
1288
1270
|
imageTags.gateway = resolved.imageTags.gateway;
|
|
1289
1271
|
imageTags["credential-executor"] =
|
|
@@ -96,20 +96,6 @@ function tokenAuthHeader(token: string): Record<string, string> {
|
|
|
96
96
|
return { "X-Session-Token": token };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export function authHeadersForKnownOrganization(
|
|
100
|
-
token: string,
|
|
101
|
-
organizationId: string,
|
|
102
|
-
): Record<string, string> {
|
|
103
|
-
const headers: Record<string, string> = {
|
|
104
|
-
"Content-Type": "application/json",
|
|
105
|
-
...tokenAuthHeader(token),
|
|
106
|
-
};
|
|
107
|
-
if (!token.startsWith(VAK_PREFIX)) {
|
|
108
|
-
headers["Vellum-Organization-Id"] = organizationId;
|
|
109
|
-
}
|
|
110
|
-
return headers;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
99
|
/** Module-level cache for org IDs to avoid redundant fetches in polling loops. */
|
|
114
100
|
const orgIdCache = new Map<string, { orgId: string; expiresAt: number }>();
|
|
115
101
|
const ORG_ID_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
@@ -9,25 +9,6 @@ export interface ResolvedImageRefs {
|
|
|
9
9
|
source: "platform" | "dockerhub";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Release channel to resolve images from. `stable` is the default and maps
|
|
14
|
-
* to the server's `stable=true` filter (unchanged behavior). `preview` opts
|
|
15
|
-
* into the platform's preview channel — the newest release regardless of
|
|
16
|
-
* stability — used by callers (e.g. evals) that want to always hatch the
|
|
17
|
-
* latest preview image instead of latest-stable.
|
|
18
|
-
*/
|
|
19
|
-
export type ReleaseChannel = "stable" | "preview";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Map a channel to `fetchReleases` options. `stable` passes no `channel`
|
|
23
|
-
* param so the server keeps its `stable=true` default (we don't rely on an
|
|
24
|
-
* unverified `channel=stable` filter); `preview` requests the preview
|
|
25
|
-
* channel explicitly.
|
|
26
|
-
*/
|
|
27
|
-
function releasesOptsFor(channel: ReleaseChannel): { channel?: "preview" } {
|
|
28
|
-
return channel === "preview" ? { channel: "preview" } : {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
12
|
export interface ReleaseListItem {
|
|
32
13
|
version: string;
|
|
33
14
|
is_stable?: boolean;
|
|
@@ -70,24 +51,13 @@ export async function fetchReleases(opts?: {
|
|
|
70
51
|
}
|
|
71
52
|
|
|
72
53
|
/**
|
|
73
|
-
* Fetch the latest release version
|
|
54
|
+
* Fetch the latest stable release version from the platform API.
|
|
74
55
|
* Returns the version string (e.g. "0.7.0") or null if unavailable.
|
|
75
|
-
* The releases endpoint returns entries ordered newest-first
|
|
76
|
-
* entry is the latest for the requested channel.
|
|
77
|
-
*/
|
|
78
|
-
export async function fetchLatestVersion(
|
|
79
|
-
channel: ReleaseChannel = "stable",
|
|
80
|
-
): Promise<string | null> {
|
|
81
|
-
const releases = await fetchReleases(releasesOptsFor(channel));
|
|
82
|
-
return releases?.[0]?.version ?? null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Fetch the latest stable release version. Thin back-compat wrapper around
|
|
87
|
-
* {@link fetchLatestVersion} for existing callers.
|
|
56
|
+
* The releases endpoint returns entries ordered newest-first.
|
|
88
57
|
*/
|
|
89
58
|
export async function fetchLatestStableVersion(): Promise<string | null> {
|
|
90
|
-
|
|
59
|
+
const releases = await fetchReleases();
|
|
60
|
+
return releases?.[0]?.version ?? null;
|
|
91
61
|
}
|
|
92
62
|
|
|
93
63
|
export type ImageRefResolution =
|
|
@@ -117,11 +87,10 @@ function dockerhubImageTags(version: string): Record<ServiceName, string> {
|
|
|
117
87
|
export async function resolveImageRefsDetailed(
|
|
118
88
|
version: string,
|
|
119
89
|
log?: (msg: string) => void,
|
|
120
|
-
channel: ReleaseChannel = "stable",
|
|
121
90
|
): Promise<ImageRefResolution> {
|
|
122
91
|
log?.("Resolving image references...");
|
|
123
92
|
|
|
124
|
-
const releases = await fetchReleases(
|
|
93
|
+
const releases = await fetchReleases();
|
|
125
94
|
if (releases === null) {
|
|
126
95
|
log?.("Platform unreachable — falling back to DockerHub tags");
|
|
127
96
|
return {
|
|
@@ -182,9 +151,8 @@ export async function resolveImageRefsDetailed(
|
|
|
182
151
|
export async function resolveImageRefs(
|
|
183
152
|
version: string,
|
|
184
153
|
log?: (msg: string) => void,
|
|
185
|
-
channel: ReleaseChannel = "stable",
|
|
186
154
|
): Promise<ResolvedImageRefs> {
|
|
187
|
-
const resolution = await resolveImageRefsDetailed(version, log
|
|
155
|
+
const resolution = await resolveImageRefsDetailed(version, log);
|
|
188
156
|
if (resolution.status === "platform") {
|
|
189
157
|
log?.("Resolved image refs from platform API");
|
|
190
158
|
return { imageTags: resolution.imageTags, source: "platform" };
|
package/src/lib/workos-pkce.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface AuthorizeUrlOptions {
|
|
|
33
33
|
challenge: string;
|
|
34
34
|
state: string;
|
|
35
35
|
loginHint?: string;
|
|
36
|
+
providerHint?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
|
|
@@ -45,7 +46,7 @@ export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
|
|
|
45
46
|
url.searchParams.set("code_challenge_method", "S256");
|
|
46
47
|
url.searchParams.set("state", options.state);
|
|
47
48
|
// No `prompt`: lets the browser's existing IdP session be reused.
|
|
48
|
-
url.searchParams.set("provider", "authkit");
|
|
49
|
+
url.searchParams.set("provider", options.providerHint || "authkit");
|
|
49
50
|
if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
|
|
50
51
|
return url.toString();
|
|
51
52
|
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
|
|
4
|
-
import type { CliInvocation } from "../util";
|
|
5
|
-
|
|
6
|
-
class FakeChild extends EventEmitter {
|
|
7
|
-
stdout = new EventEmitter();
|
|
8
|
-
stderr = new EventEmitter();
|
|
9
|
-
kill = mock(() => true);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let lastChild: FakeChild;
|
|
13
|
-
const spawnArgs: Array<
|
|
14
|
-
[string, string[], { env?: NodeJS.ProcessEnv; stdio?: unknown }]
|
|
15
|
-
> = [];
|
|
16
|
-
const spawnMock = mock(
|
|
17
|
-
(
|
|
18
|
-
command: string,
|
|
19
|
-
args: string[],
|
|
20
|
-
options: { env?: NodeJS.ProcessEnv; stdio?: unknown },
|
|
21
|
-
) => {
|
|
22
|
-
spawnArgs.push([command, args, options]);
|
|
23
|
-
lastChild = new FakeChild();
|
|
24
|
-
return lastChild;
|
|
25
|
-
},
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
mock.module("node:child_process", () => ({ spawn: spawnMock }));
|
|
29
|
-
|
|
30
|
-
let runRetire: typeof import("../retire").runRetire;
|
|
31
|
-
|
|
32
|
-
beforeAll(async () => {
|
|
33
|
-
({ runRetire } = await import("../retire"));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
spawnArgs.length = 0;
|
|
38
|
-
spawnMock.mockClear();
|
|
39
|
-
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const invocation: CliInvocation = { command: "bun", baseArgs: ["run", "cli"] };
|
|
43
|
-
|
|
44
|
-
describe("runRetire", () => {
|
|
45
|
-
test("spawns the CLI retire command", async () => {
|
|
46
|
-
const pending = runRetire(invocation, "asst-42");
|
|
47
|
-
lastChild.emit("close", 0);
|
|
48
|
-
|
|
49
|
-
expect(await pending).toEqual({ ok: true });
|
|
50
|
-
expect(spawnArgs[0]).toEqual([
|
|
51
|
-
"bun",
|
|
52
|
-
["run", "cli", "retire", "asst-42", "--yes"],
|
|
53
|
-
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
54
|
-
]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("passes a host platform token to the CLI subprocess", async () => {
|
|
58
|
-
const pending = runRetire(invocation, "asst-42", {
|
|
59
|
-
platformToken: "session-token",
|
|
60
|
-
});
|
|
61
|
-
lastChild.emit("close", 0);
|
|
62
|
-
|
|
63
|
-
expect(await pending).toEqual({ ok: true });
|
|
64
|
-
expect(spawnArgs[0]?.[2].env?.VELLUM_PLATFORM_TOKEN).toBe("session-token");
|
|
65
|
-
expect(process.env.VELLUM_PLATFORM_TOKEN).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a non-zero exit resolves to a failure carrying the CLI output", async () => {
|
|
69
|
-
const pending = runRetire(invocation, "asst-42");
|
|
70
|
-
lastChild.stderr.emit("data", Buffer.from("retire failed"));
|
|
71
|
-
lastChild.emit("close", 1);
|
|
72
|
-
|
|
73
|
-
expect(await pending).toEqual({
|
|
74
|
-
ok: false,
|
|
75
|
-
status: 500,
|
|
76
|
-
error: "retire failed",
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|