@vellumai/cli 0.7.1 → 0.7.3
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/AGENTS.md +3 -11
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +121 -5
- package/src/__tests__/teleport.test.ts +515 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +90 -7
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/login.ts +11 -0
- package/src/commands/restore.ts +7 -1
- package/src/commands/rollback.ts +1 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +122 -12
- package/src/commands/upgrade.ts +8 -2
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +42 -130
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +53 -35
- package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
- package/src/lib/__tests__/runtime-url.test.ts +39 -1
- package/src/lib/assistant-client.ts +13 -5
- package/src/lib/assistant-config.ts +0 -25
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/client-identity.ts +9 -5
- package/src/lib/docker.ts +6 -267
- package/src/lib/environments/paths.ts +20 -0
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/local-runtime-client.ts +82 -1
- package/src/lib/local.ts +9 -7
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +100 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/statefulset.ts +375 -0
- package/src/lib/upgrade-lifecycle.ts +97 -1
- package/src/commands/pair.ts +0 -212
package/src/lib/local.ts
CHANGED
|
@@ -111,7 +111,9 @@ function computeIpcSocketDirOverride(workspaceDir: string): string | undefined {
|
|
|
111
111
|
* a short override directory and set all IPC socket env vars on the target
|
|
112
112
|
* env object. No-op on non-macOS or when paths are within limits.
|
|
113
113
|
*/
|
|
114
|
-
function applyIpcSocketDirOverride(
|
|
114
|
+
function applyIpcSocketDirOverride(
|
|
115
|
+
env: Record<string, string | undefined>,
|
|
116
|
+
): void {
|
|
115
117
|
const workspaceDir =
|
|
116
118
|
env.VELLUM_WORKSPACE_DIR || join(homedir(), ".vellum", "workspace");
|
|
117
119
|
const override = computeIpcSocketDirOverride(workspaceDir);
|
|
@@ -417,6 +419,8 @@ async function startDaemonFromSource(
|
|
|
417
419
|
options.defaultWorkspaceConfigPath;
|
|
418
420
|
}
|
|
419
421
|
|
|
422
|
+
applyIpcSocketDirOverride(env);
|
|
423
|
+
|
|
420
424
|
// Write a sentinel PID file before spawning so concurrent hatch() calls
|
|
421
425
|
// detect the in-progress spawn and wait instead of racing.
|
|
422
426
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
@@ -552,6 +556,8 @@ async function startDaemonWatchFromSource(
|
|
|
552
556
|
options.defaultWorkspaceConfigPath;
|
|
553
557
|
}
|
|
554
558
|
|
|
559
|
+
applyIpcSocketDirOverride(env);
|
|
560
|
+
|
|
555
561
|
// Write a sentinel PID file before spawning so concurrent hatch() calls
|
|
556
562
|
// detect the in-progress spawn and wait instead of racing.
|
|
557
563
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
@@ -1183,10 +1189,6 @@ export async function startGateway(
|
|
|
1183
1189
|
|
|
1184
1190
|
applyIpcSocketDirOverride(gatewayEnv);
|
|
1185
1191
|
|
|
1186
|
-
if (publicUrl) {
|
|
1187
|
-
console.log(` HTTP URL: ${publicUrl}`);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
1192
|
let gateway;
|
|
1191
1193
|
|
|
1192
1194
|
const gatewayBinary = join(dirname(process.execPath), "vellum-gateway");
|
|
@@ -1232,8 +1234,8 @@ export async function startGateway(
|
|
|
1232
1234
|
const gatewayUrl = publicUrl || `http://localhost:${effectiveGatewayPort}`;
|
|
1233
1235
|
|
|
1234
1236
|
// Wait for the gateway to be responsive before returning. Without this,
|
|
1235
|
-
// callers
|
|
1236
|
-
//
|
|
1237
|
+
// callers may try to connect before the HTTP server is listening and get
|
|
1238
|
+
// connection-refused errors.
|
|
1237
1239
|
const start = Date.now();
|
|
1238
1240
|
const timeoutMs = 30000;
|
|
1239
1241
|
let ready = false;
|
package/src/lib/ngrok.ts
CHANGED
|
@@ -12,13 +12,19 @@ import { dirname, join } from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { GATEWAY_PORT } from "./constants";
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function getDefaultWorkspaceDir(): string {
|
|
16
|
+
return (
|
|
17
|
+
process.env.VELLUM_WORKSPACE_DIR?.trim() ||
|
|
18
|
+
join(homedir(), ".vellum", "workspace")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getConfigPath(workspaceDir: string): string {
|
|
23
|
+
return join(workspaceDir, "config.json");
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
function loadRawConfig(): Record<string, unknown> {
|
|
21
|
-
const configPath = getConfigPath();
|
|
26
|
+
function loadRawConfig(workspaceDir: string): Record<string, unknown> {
|
|
27
|
+
const configPath = getConfigPath(workspaceDir);
|
|
22
28
|
if (!existsSync(configPath)) return {};
|
|
23
29
|
return JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
24
30
|
string,
|
|
@@ -26,8 +32,11 @@ function loadRawConfig(): Record<string, unknown> {
|
|
|
26
32
|
>;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
function saveRawConfig(
|
|
30
|
-
|
|
35
|
+
function saveRawConfig(
|
|
36
|
+
workspaceDir: string,
|
|
37
|
+
config: Record<string, unknown>,
|
|
38
|
+
): void {
|
|
39
|
+
const configPath = getConfigPath(workspaceDir);
|
|
31
40
|
const dir = dirname(configPath);
|
|
32
41
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
33
42
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
@@ -182,33 +191,33 @@ export async function waitForNgrokUrl(
|
|
|
182
191
|
/**
|
|
183
192
|
* Persist a public ingress URL to the workspace config and enable ingress.
|
|
184
193
|
*/
|
|
185
|
-
function saveIngressUrl(publicUrl: string): void {
|
|
186
|
-
const config = loadRawConfig();
|
|
194
|
+
function saveIngressUrl(workspaceDir: string, publicUrl: string): void {
|
|
195
|
+
const config = loadRawConfig(workspaceDir);
|
|
187
196
|
const ingress = (config.ingress ?? {}) as Record<string, unknown>;
|
|
188
197
|
ingress.publicBaseUrl = publicUrl;
|
|
189
198
|
ingress.enabled = true;
|
|
190
199
|
config.ingress = ingress;
|
|
191
|
-
saveRawConfig(config);
|
|
200
|
+
saveRawConfig(workspaceDir, config);
|
|
192
201
|
}
|
|
193
202
|
|
|
194
203
|
/**
|
|
195
204
|
* Clear the ingress public base URL from the workspace config.
|
|
196
205
|
*/
|
|
197
|
-
function clearIngressUrl(): void {
|
|
198
|
-
const config = loadRawConfig();
|
|
206
|
+
function clearIngressUrl(workspaceDir: string): void {
|
|
207
|
+
const config = loadRawConfig(workspaceDir);
|
|
199
208
|
const ingress = (config.ingress ?? {}) as Record<string, unknown>;
|
|
200
209
|
delete ingress.publicBaseUrl;
|
|
201
210
|
config.ingress = ingress;
|
|
202
|
-
saveRawConfig(config);
|
|
211
|
+
saveRawConfig(workspaceDir, config);
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
/**
|
|
206
215
|
* Check whether any webhook-based integrations (e.g. Telegram, Twilio) are
|
|
207
216
|
* configured that require a public ingress URL.
|
|
208
217
|
*/
|
|
209
|
-
function hasWebhookIntegrationsConfigured(): boolean {
|
|
218
|
+
function hasWebhookIntegrationsConfigured(workspaceDir: string): boolean {
|
|
210
219
|
try {
|
|
211
|
-
const config = loadRawConfig();
|
|
220
|
+
const config = loadRawConfig(workspaceDir);
|
|
212
221
|
const telegram = config.telegram as Record<string, unknown> | undefined;
|
|
213
222
|
if (telegram?.botUsername) return true;
|
|
214
223
|
const twilio = config.twilio as Record<string, unknown> | undefined;
|
|
@@ -223,9 +232,9 @@ function hasWebhookIntegrationsConfigured(): boolean {
|
|
|
223
232
|
* Check whether a non-ngrok ingress URL is already configured (e.g. custom
|
|
224
233
|
* domain or cloud deployment), meaning ngrok is not needed.
|
|
225
234
|
*/
|
|
226
|
-
function hasNonNgrokIngressUrl(): boolean {
|
|
235
|
+
function hasNonNgrokIngressUrl(workspaceDir: string): boolean {
|
|
227
236
|
try {
|
|
228
|
-
const config = loadRawConfig();
|
|
237
|
+
const config = loadRawConfig(workspaceDir);
|
|
229
238
|
const ingress = config.ingress as Record<string, unknown> | undefined;
|
|
230
239
|
const publicBaseUrl = ingress?.publicBaseUrl;
|
|
231
240
|
if (!publicBaseUrl || typeof publicBaseUrl !== "string") return false;
|
|
@@ -244,6 +253,7 @@ function hasNonNgrokIngressUrl(): boolean {
|
|
|
244
253
|
*/
|
|
245
254
|
export async function maybeStartNgrokTunnel(
|
|
246
255
|
targetPort: number,
|
|
256
|
+
workspaceDir: string,
|
|
247
257
|
): Promise<ChildProcess | null> {
|
|
248
258
|
// Managed/containerized deployments route webhooks through the platform's
|
|
249
259
|
// callback proxy. ngrok is not needed and would not be reachable from the
|
|
@@ -252,8 +262,8 @@ export async function maybeStartNgrokTunnel(
|
|
|
252
262
|
process.env.IS_CONTAINERIZED === "true" ||
|
|
253
263
|
process.env.IS_CONTAINERIZED === "1";
|
|
254
264
|
if (isContainerized) return null;
|
|
255
|
-
if (!hasWebhookIntegrationsConfigured()) return null;
|
|
256
|
-
if (hasNonNgrokIngressUrl()) return null;
|
|
265
|
+
if (!hasWebhookIntegrationsConfigured(workspaceDir)) return null;
|
|
266
|
+
if (hasNonNgrokIngressUrl(workspaceDir)) return null;
|
|
257
267
|
|
|
258
268
|
const version = getNgrokVersion();
|
|
259
269
|
if (!version) return null;
|
|
@@ -262,7 +272,7 @@ export async function maybeStartNgrokTunnel(
|
|
|
262
272
|
const existingUrl = await findExistingTunnel(targetPort);
|
|
263
273
|
if (existingUrl) {
|
|
264
274
|
console.log(` Found existing ngrok tunnel: ${existingUrl}`);
|
|
265
|
-
saveIngressUrl(existingUrl);
|
|
275
|
+
saveIngressUrl(workspaceDir, existingUrl);
|
|
266
276
|
return null;
|
|
267
277
|
}
|
|
268
278
|
|
|
@@ -274,14 +284,13 @@ export async function maybeStartNgrokTunnel(
|
|
|
274
284
|
// 2. If pipe handles are destroyed, SIGPIPE kills ngrok on its next write.
|
|
275
285
|
// Writing to a log file sidesteps both issues — the file descriptor is
|
|
276
286
|
// inherited by the detached ngrok process and remains valid after CLI exit.
|
|
277
|
-
const
|
|
278
|
-
const ngrokLogPath = join(root, "workspace", "data", "logs", "ngrok.log");
|
|
287
|
+
const ngrokLogPath = join(workspaceDir, "data", "logs", "ngrok.log");
|
|
279
288
|
const ngrokProcess = startNgrokProcess(targetPort, ngrokLogPath);
|
|
280
289
|
ngrokProcess.unref();
|
|
281
290
|
|
|
282
291
|
try {
|
|
283
292
|
const publicUrl = await waitForNgrokUrl();
|
|
284
|
-
saveIngressUrl(publicUrl);
|
|
293
|
+
saveIngressUrl(workspaceDir, publicUrl);
|
|
285
294
|
console.log(` Tunnel established: ${publicUrl}`);
|
|
286
295
|
|
|
287
296
|
return ngrokProcess;
|
|
@@ -317,12 +326,13 @@ export async function runNgrokTunnel(): Promise<void> {
|
|
|
317
326
|
console.log(`Using ${version}`);
|
|
318
327
|
|
|
319
328
|
const port = GATEWAY_PORT;
|
|
329
|
+
const workspaceDir = getDefaultWorkspaceDir();
|
|
320
330
|
|
|
321
331
|
// Check for an existing ngrok tunnel pointing at the gateway
|
|
322
332
|
const existingUrl = await findExistingTunnel(port);
|
|
323
333
|
if (existingUrl) {
|
|
324
334
|
console.log(`Found existing ngrok tunnel: ${existingUrl}`);
|
|
325
|
-
saveIngressUrl(existingUrl);
|
|
335
|
+
saveIngressUrl(workspaceDir, existingUrl);
|
|
326
336
|
console.log("Ingress URL saved to config.");
|
|
327
337
|
console.log("");
|
|
328
338
|
console.log(
|
|
@@ -349,7 +359,7 @@ export async function runNgrokTunnel(): Promise<void> {
|
|
|
349
359
|
}
|
|
350
360
|
if (publicUrl) {
|
|
351
361
|
console.log("\nClearing ingress URL from config...");
|
|
352
|
-
clearIngressUrl();
|
|
362
|
+
clearIngressUrl(workspaceDir);
|
|
353
363
|
}
|
|
354
364
|
};
|
|
355
365
|
|
|
@@ -398,7 +408,7 @@ export async function runNgrokTunnel(): Promise<void> {
|
|
|
398
408
|
console.log(`Tunnel established: ${publicUrl}`);
|
|
399
409
|
console.log(`Forwarding to: localhost:${port}`);
|
|
400
410
|
|
|
401
|
-
saveIngressUrl(publicUrl);
|
|
411
|
+
saveIngressUrl(workspaceDir, publicUrl);
|
|
402
412
|
console.log("Ingress URL saved to config.");
|
|
403
413
|
console.log("");
|
|
404
414
|
console.log("Press Ctrl+C to stop the tunnel and clear the ingress URL.");
|
|
@@ -99,6 +99,23 @@ function tokenAuthHeader(token: string): Record<string, string> {
|
|
|
99
99
|
const orgIdCache = new Map<string, { orgId: string; expiresAt: number }>();
|
|
100
100
|
const ORG_ID_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Drop the cached org ID for a given (token, platformUrl) pair. Used by the
|
|
104
|
+
* one-shot 401-retry path: a 401 on a session-token request frequently means
|
|
105
|
+
* the cached `Vellum-Organization-Id` header is stale (e.g. user switched
|
|
106
|
+
* orgs in another tab). Clearing the entry forces the next `authHeaders`
|
|
107
|
+
* call to refetch the org ID from the platform.
|
|
108
|
+
*
|
|
109
|
+
* Exported so other modules (e.g. local-runtime-client) can implement the
|
|
110
|
+
* same retry pattern without needing direct access to the cache map.
|
|
111
|
+
*/
|
|
112
|
+
export function invalidateOrgIdCache(
|
|
113
|
+
token: string,
|
|
114
|
+
platformUrl?: string,
|
|
115
|
+
): void {
|
|
116
|
+
orgIdCache.delete(`${token}::${platformUrl ?? ""}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Returns the full set of headers needed for an authenticated platform
|
|
104
121
|
* API request:
|
|
@@ -196,6 +213,7 @@ export async function ensureSelfHostedLocalRegistration(
|
|
|
196
213
|
clientPlatform: string,
|
|
197
214
|
assistantVersion?: string,
|
|
198
215
|
platformUrl?: string,
|
|
216
|
+
publicBaseUrl?: string,
|
|
199
217
|
): Promise<EnsureRegistrationResponse> {
|
|
200
218
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
201
219
|
const body: Record<string, string> = {
|
|
@@ -206,6 +224,9 @@ export async function ensureSelfHostedLocalRegistration(
|
|
|
206
224
|
if (assistantVersion) {
|
|
207
225
|
body.assistant_version = assistantVersion;
|
|
208
226
|
}
|
|
227
|
+
if (publicBaseUrl) {
|
|
228
|
+
body.public_ingress_url = publicBaseUrl;
|
|
229
|
+
}
|
|
209
230
|
|
|
210
231
|
const response = await fetch(
|
|
211
232
|
`${resolvedUrl}/v1/assistants/self-hosted-local/ensure-registration/`,
|
|
@@ -468,6 +489,7 @@ export async function hatchAssistant(
|
|
|
468
489
|
method: "POST",
|
|
469
490
|
headers: await authHeaders(token, platformUrl),
|
|
470
491
|
body: JSON.stringify({}),
|
|
492
|
+
signal: AbortSignal.timeout(300_000),
|
|
471
493
|
});
|
|
472
494
|
|
|
473
495
|
if (response.ok) {
|
|
@@ -805,6 +827,45 @@ export function parseUnifiedJobStatus(
|
|
|
805
827
|
};
|
|
806
828
|
}
|
|
807
829
|
|
|
830
|
+
export interface BundleCompatibility {
|
|
831
|
+
min_runtime_version: string;
|
|
832
|
+
max_runtime_version: string | null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Thrown by platformRequestSignedUrl when the platform rejects a download
|
|
837
|
+
* signed-URL request because the target runtime version is outside the
|
|
838
|
+
* ExportJob's [min_runtime_version, max_runtime_version] band. Terminal
|
|
839
|
+
* — callers must NOT retry; surface to the user and abort the
|
|
840
|
+
* teleport/restore wizard.
|
|
841
|
+
*/
|
|
842
|
+
export class VersionMismatchError extends Error {
|
|
843
|
+
readonly bundleCompat: BundleCompatibility;
|
|
844
|
+
readonly targetRuntimeVersion: string;
|
|
845
|
+
|
|
846
|
+
constructor(bundleCompat: BundleCompatibility, targetRuntimeVersion: string) {
|
|
847
|
+
super(
|
|
848
|
+
VersionMismatchError.formatMessage(bundleCompat, targetRuntimeVersion),
|
|
849
|
+
);
|
|
850
|
+
this.name = "VersionMismatchError";
|
|
851
|
+
this.bundleCompat = bundleCompat;
|
|
852
|
+
this.targetRuntimeVersion = targetRuntimeVersion;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
static formatMessage(
|
|
856
|
+
compat: BundleCompatibility,
|
|
857
|
+
targetRuntimeVersion: string,
|
|
858
|
+
): string {
|
|
859
|
+
const range = compat.max_runtime_version
|
|
860
|
+
? `${compat.min_runtime_version}–${compat.max_runtime_version}`
|
|
861
|
+
: `${compat.min_runtime_version}+`;
|
|
862
|
+
return (
|
|
863
|
+
`Cannot import: bundle requires runtime ${range}, but this runtime is ${targetRuntimeVersion}. ` +
|
|
864
|
+
`Update your runtime before importing.`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
808
869
|
/**
|
|
809
870
|
* Request a signed URL from the platform for either uploading a new bundle
|
|
810
871
|
* or downloading an existing one. Calls `POST /v1/migrations/signed-url/`.
|
|
@@ -816,6 +877,9 @@ export function parseUnifiedJobStatus(
|
|
|
816
877
|
*
|
|
817
878
|
* Retries once with a fresh org-ID cache on 401 to match the retry pattern
|
|
818
879
|
* used by other authenticated platform helpers.
|
|
880
|
+
*
|
|
881
|
+
* Throws {@link VersionMismatchError} on a 422 `version_mismatch` response,
|
|
882
|
+
* which is terminal — callers must NOT retry.
|
|
819
883
|
*/
|
|
820
884
|
export async function platformRequestSignedUrl(
|
|
821
885
|
params: {
|
|
@@ -823,6 +887,11 @@ export async function platformRequestSignedUrl(
|
|
|
823
887
|
bundleKey?: string;
|
|
824
888
|
contentType?: string;
|
|
825
889
|
contentLength?: number;
|
|
890
|
+
// Source-side, upload only: runtime version that produced the bundle.
|
|
891
|
+
minRuntimeVersion?: string;
|
|
892
|
+
maxRuntimeVersion?: string | null;
|
|
893
|
+
// Target-side, download only: runtime version that will import.
|
|
894
|
+
targetRuntimeVersion?: string;
|
|
826
895
|
},
|
|
827
896
|
token: string,
|
|
828
897
|
platformUrl?: string,
|
|
@@ -839,6 +908,17 @@ export async function platformRequestSignedUrl(
|
|
|
839
908
|
if (params.contentLength !== undefined) {
|
|
840
909
|
body.content_length = params.contentLength;
|
|
841
910
|
}
|
|
911
|
+
if (params.minRuntimeVersion !== undefined) {
|
|
912
|
+
body.min_runtime_version = params.minRuntimeVersion;
|
|
913
|
+
}
|
|
914
|
+
if (params.maxRuntimeVersion !== undefined) {
|
|
915
|
+
// Explicit null is the documented "no upper bound" sentinel; keep it
|
|
916
|
+
// in the payload rather than stripping to undefined.
|
|
917
|
+
body.max_runtime_version = params.maxRuntimeVersion;
|
|
918
|
+
}
|
|
919
|
+
if (params.targetRuntimeVersion !== undefined) {
|
|
920
|
+
body.target_runtime_version = params.targetRuntimeVersion;
|
|
921
|
+
}
|
|
842
922
|
|
|
843
923
|
const doRequest = async (): Promise<Response> =>
|
|
844
924
|
fetch(`${resolvedUrl}/v1/migrations/signed-url/`, {
|
|
@@ -854,7 +934,7 @@ export async function platformRequestSignedUrl(
|
|
|
854
934
|
// lookup. For session-token callers, a 401 frequently means the
|
|
855
935
|
// cached org ID is stale — calling doRequest() again without clearing
|
|
856
936
|
// the cache would just send the same stale header and fail again.
|
|
857
|
-
|
|
937
|
+
invalidateOrgIdCache(token, platformUrl);
|
|
858
938
|
response = await doRequest();
|
|
859
939
|
}
|
|
860
940
|
|
|
@@ -873,9 +953,28 @@ export async function platformRequestSignedUrl(
|
|
|
873
953
|
};
|
|
874
954
|
}
|
|
875
955
|
|
|
956
|
+
// Non-success body. Read once and reuse for both the 422 version-mismatch
|
|
957
|
+
// branch and the generic-error fallthrough — `response.json()` consumes
|
|
958
|
+
// the body, so a second read would always return undefined.
|
|
876
959
|
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
877
960
|
detail?: string;
|
|
961
|
+
reason?: string;
|
|
962
|
+
bundle_compat?: BundleCompatibility;
|
|
963
|
+
target_runtime_version?: string;
|
|
878
964
|
};
|
|
965
|
+
|
|
966
|
+
if (
|
|
967
|
+
response.status === 422 &&
|
|
968
|
+
errorBody.reason === "version_mismatch" &&
|
|
969
|
+
errorBody.bundle_compat &&
|
|
970
|
+
typeof errorBody.target_runtime_version === "string"
|
|
971
|
+
) {
|
|
972
|
+
throw new VersionMismatchError(
|
|
973
|
+
errorBody.bundle_compat,
|
|
974
|
+
errorBody.target_runtime_version,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
879
978
|
throw new Error(
|
|
880
979
|
errorBody.detail ??
|
|
881
980
|
`Failed to request signed URL: ${response.status} ${response.statusText}`,
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import { homedir } from "os";
|
|
2
3
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
4
|
import { basename, dirname, join } from "path";
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
|
-
getBaseDir,
|
|
7
7
|
getDaemonPidPath,
|
|
8
8
|
loadAllAssistants,
|
|
9
9
|
} from "./assistant-config.js";
|
|
@@ -77,7 +77,7 @@ export async function retireLocal(
|
|
|
77
77
|
// For named instances (instanceDir differs from the base directory),
|
|
78
78
|
// archive and remove the entire instance directory. For the default
|
|
79
79
|
// instance, archive only the .vellum subdirectory.
|
|
80
|
-
const isNamedInstance = resources.instanceDir !==
|
|
80
|
+
const isNamedInstance = resources.instanceDir !== homedir();
|
|
81
81
|
const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
|
|
82
82
|
|
|
83
83
|
// Move the data directory out of the way so the path is immediately available
|
package/src/lib/runtime-url.ts
CHANGED
|
@@ -28,3 +28,25 @@ export function resolveRuntimeMigrationUrl(
|
|
|
28
28
|
}
|
|
29
29
|
return `${entry.runtimeUrl}/v1/migrations/${subpath}`;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the URL for a generic runtime endpoint under `/v1/<subpath>`,
|
|
34
|
+
* taking the assistant's topology into account.
|
|
35
|
+
*
|
|
36
|
+
* - For local/docker assistants, `runtimeUrl` is the loopback gateway and
|
|
37
|
+
* the runtime serves `/v1/<subpath>` directly.
|
|
38
|
+
* - For platform-managed (cloud="vellum") assistants the path is rewritten
|
|
39
|
+
* to the wildcard runtime proxy:
|
|
40
|
+
* `{platformUrl}/v1/assistants/<assistantId>/<subpath>`.
|
|
41
|
+
*
|
|
42
|
+
* The `subpath` is appended verbatim (e.g. `"identity"`).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveRuntimeUrl(
|
|
45
|
+
entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
|
|
46
|
+
subpath: string,
|
|
47
|
+
): string {
|
|
48
|
+
if (entry.cloud === "vellum") {
|
|
49
|
+
return `${entry.runtimeUrl}/v1/assistants/${entry.assistantId}/${subpath}`;
|
|
50
|
+
}
|
|
51
|
+
return `${entry.runtimeUrl}/v1/${subpath}`;
|
|
52
|
+
}
|