@vellumai/cli 0.12.3-dev.202609221316.175c45b → 0.12.3-dev.202609221714.83d90c3
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/package.json
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
mock,
|
|
8
|
+
spyOn,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// Lockfile isolation (mirrors backup.test.ts)
|
|
16
|
+
const testDir = mkdtempSync(join(tmpdir(), "cli-debug-bundle-test-"));
|
|
17
|
+
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
18
|
+
|
|
19
|
+
import * as assistantConfig from "../lib/assistant-config.js";
|
|
20
|
+
import * as guardianToken from "../lib/guardian-token.js";
|
|
21
|
+
import * as localRuntimeClient from "../lib/local-runtime-client.js";
|
|
22
|
+
import * as platformClient from "../lib/platform-client.js";
|
|
23
|
+
|
|
24
|
+
const resolveTargetMock = spyOn(
|
|
25
|
+
assistantConfig,
|
|
26
|
+
"resolveTargetAssistant",
|
|
27
|
+
).mockImplementation(() => {
|
|
28
|
+
throw new Error("process.exit:1");
|
|
29
|
+
});
|
|
30
|
+
const readPlatformTokenMock = spyOn(
|
|
31
|
+
platformClient,
|
|
32
|
+
"readPlatformToken",
|
|
33
|
+
).mockReturnValue("platform-token");
|
|
34
|
+
const exportMock = spyOn(
|
|
35
|
+
localRuntimeClient,
|
|
36
|
+
"localRuntimeExportToGcs",
|
|
37
|
+
).mockResolvedValue({ jobId: "job-1" });
|
|
38
|
+
const identityMock = spyOn(
|
|
39
|
+
localRuntimeClient,
|
|
40
|
+
"localRuntimeIdentity",
|
|
41
|
+
).mockResolvedValue({ version: "0.12.3" });
|
|
42
|
+
const pollMock = spyOn(
|
|
43
|
+
localRuntimeClient,
|
|
44
|
+
"localRuntimePollJobStatus",
|
|
45
|
+
).mockResolvedValue({
|
|
46
|
+
jobId: "job-1",
|
|
47
|
+
type: "export",
|
|
48
|
+
status: "complete",
|
|
49
|
+
} as unknown as Awaited<
|
|
50
|
+
ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
|
|
51
|
+
>);
|
|
52
|
+
const loadGuardianTokenSpy = spyOn(
|
|
53
|
+
guardianToken,
|
|
54
|
+
"loadGuardianToken",
|
|
55
|
+
).mockReturnValue({
|
|
56
|
+
accessToken: "local-token",
|
|
57
|
+
accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
|
58
|
+
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
59
|
+
const leaseGuardianTokenSpy = spyOn(
|
|
60
|
+
guardianToken,
|
|
61
|
+
"leaseGuardianToken",
|
|
62
|
+
).mockResolvedValue({
|
|
63
|
+
accessToken: "leased-token",
|
|
64
|
+
accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
|
65
|
+
} as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
|
|
66
|
+
|
|
67
|
+
const { debugBundle } = await import("../commands/debug-bundle.js");
|
|
68
|
+
|
|
69
|
+
const LOCAL_ENTRY = {
|
|
70
|
+
name: "mine",
|
|
71
|
+
assistantId: "local-asst-1",
|
|
72
|
+
runtimeUrl: "http://127.0.0.1:7821",
|
|
73
|
+
cloud: "local",
|
|
74
|
+
species: "local",
|
|
75
|
+
hatchedAt: new Date().toISOString(),
|
|
76
|
+
platformAssistantId: "11111111-2222-3333-4444-555555555555",
|
|
77
|
+
platformBaseUrl: "https://platform.vellum.ai",
|
|
78
|
+
platformOrganizationId: "org-1",
|
|
79
|
+
} as unknown as assistantConfig.AssistantEntry;
|
|
80
|
+
|
|
81
|
+
let originalFetch: typeof globalThis.fetch;
|
|
82
|
+
let originalExit: typeof process.exit;
|
|
83
|
+
let fetchCalls: { url: string; init?: RequestInit }[] = [];
|
|
84
|
+
let uploadUrlStatus = 201;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
process.argv = ["bun", "vellum", "debug-bundle"];
|
|
88
|
+
originalFetch = globalThis.fetch;
|
|
89
|
+
originalExit = process.exit;
|
|
90
|
+
process.exit = mock((code?: number) => {
|
|
91
|
+
throw new Error(`process.exit:${code}`);
|
|
92
|
+
}) as unknown as typeof process.exit;
|
|
93
|
+
fetchCalls = [];
|
|
94
|
+
uploadUrlStatus = 201;
|
|
95
|
+
globalThis.fetch = mock(
|
|
96
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
97
|
+
fetchCalls.push({ url: String(input), init });
|
|
98
|
+
return new Response(
|
|
99
|
+
JSON.stringify({ url: "https://storage.googleapis.com/b/signed" }),
|
|
100
|
+
{
|
|
101
|
+
status: uploadUrlStatus,
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
) as unknown as typeof globalThis.fetch;
|
|
107
|
+
resolveTargetMock.mockReset();
|
|
108
|
+
resolveTargetMock.mockReturnValue(LOCAL_ENTRY);
|
|
109
|
+
identityMock.mockReset();
|
|
110
|
+
identityMock.mockResolvedValue({ version: "0.12.3" });
|
|
111
|
+
leaseGuardianTokenSpy.mockClear();
|
|
112
|
+
readPlatformTokenMock.mockReset();
|
|
113
|
+
readPlatformTokenMock.mockReturnValue("platform-token");
|
|
114
|
+
exportMock.mockClear();
|
|
115
|
+
pollMock.mockClear();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
globalThis.fetch = originalFetch;
|
|
120
|
+
process.exit = originalExit;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterAll(() => {
|
|
124
|
+
resolveTargetMock.mockRestore();
|
|
125
|
+
identityMock.mockRestore();
|
|
126
|
+
leaseGuardianTokenSpy.mockRestore();
|
|
127
|
+
readPlatformTokenMock.mockRestore();
|
|
128
|
+
exportMock.mockRestore();
|
|
129
|
+
pollMock.mockRestore();
|
|
130
|
+
loadGuardianTokenSpy.mockRestore();
|
|
131
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("vellum debug-bundle", () => {
|
|
135
|
+
test("mints the URL on the platform, exports with the debug profile, and waits", async () => {
|
|
136
|
+
await debugBundle();
|
|
137
|
+
|
|
138
|
+
expect(fetchCalls).toHaveLength(1);
|
|
139
|
+
expect(fetchCalls[0].url).toBe(
|
|
140
|
+
"https://platform.vellum.ai/v1/assistants/11111111-2222-3333-4444-555555555555/debug-bundle-upload-url/",
|
|
141
|
+
);
|
|
142
|
+
expect(fetchCalls[0].init?.method).toBe("POST");
|
|
143
|
+
expect(
|
|
144
|
+
(fetchCalls[0].init?.headers as Record<string, string>)[
|
|
145
|
+
"Vellum-Organization-Id"
|
|
146
|
+
],
|
|
147
|
+
).toBe("org-1");
|
|
148
|
+
|
|
149
|
+
expect(exportMock).toHaveBeenCalledTimes(1);
|
|
150
|
+
const [entry, token, params] = exportMock.mock.calls[0];
|
|
151
|
+
expect(entry).toBe(LOCAL_ENTRY);
|
|
152
|
+
expect(token).toBe("local-token");
|
|
153
|
+
expect(params).toEqual({
|
|
154
|
+
uploadUrl: "https://storage.googleapis.com/b/signed",
|
|
155
|
+
description: "debug bundle for Vellum staff",
|
|
156
|
+
profile: "debug",
|
|
157
|
+
});
|
|
158
|
+
expect(pollMock).toHaveBeenCalled();
|
|
159
|
+
// The daemon was checked for the debug profile before the platform call.
|
|
160
|
+
expect(identityMock).toHaveBeenCalledTimes(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("passes an unquoted multi-word display name through to the shared resolver", async () => {
|
|
164
|
+
process.argv = ["bun", "vellum", "debug-bundle", "Support", "Bot"];
|
|
165
|
+
await debugBundle();
|
|
166
|
+
expect(resolveTargetMock).toHaveBeenCalledWith("Support Bot");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("refuses a daemon older than the debug profile before any URL is minted", async () => {
|
|
170
|
+
// Such a daemon strips `profile` and would upload the owner's
|
|
171
|
+
// credentials to the staff bucket.
|
|
172
|
+
identityMock.mockResolvedValue({ version: "0.12.2" });
|
|
173
|
+
const errors: string[] = [];
|
|
174
|
+
const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
|
|
175
|
+
errors.push(String(msg));
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
await expect(debugBundle()).rejects.toThrow("process.exit:1");
|
|
179
|
+
} finally {
|
|
180
|
+
errorSpy.mockRestore();
|
|
181
|
+
}
|
|
182
|
+
expect(errors.join("\n")).toContain("0.12.3");
|
|
183
|
+
expect(fetchCalls).toHaveLength(0);
|
|
184
|
+
expect(exportMock).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("re-leases the guardian token when the daemon answers 401 mid-poll", async () => {
|
|
188
|
+
pollMock.mockReset();
|
|
189
|
+
let polls = 0;
|
|
190
|
+
pollMock.mockImplementation(async (_entry, token) => {
|
|
191
|
+
polls += 1;
|
|
192
|
+
if (polls === 1) {
|
|
193
|
+
throw new Error("Local job status check failed: 401 Unauthorized");
|
|
194
|
+
}
|
|
195
|
+
expect(token).toBe("leased-token");
|
|
196
|
+
return {
|
|
197
|
+
jobId: "job-1",
|
|
198
|
+
type: "export",
|
|
199
|
+
status: "complete",
|
|
200
|
+
} as unknown as Awaited<
|
|
201
|
+
ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
|
|
202
|
+
>;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await debugBundle();
|
|
206
|
+
|
|
207
|
+
expect(leaseGuardianTokenSpy).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(polls).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("explains what to turn on when the platform refuses", async () => {
|
|
212
|
+
uploadUrlStatus = 403;
|
|
213
|
+
const errors: string[] = [];
|
|
214
|
+
const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
|
|
215
|
+
errors.push(String(msg));
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
await expect(debugBundle()).rejects.toThrow("process.exit:1");
|
|
219
|
+
} finally {
|
|
220
|
+
errorSpy.mockRestore();
|
|
221
|
+
}
|
|
222
|
+
expect(errors.join("\n")).toContain("Allow Staff Access");
|
|
223
|
+
expect(exportMock).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("refuses a Vellum-hosted assistant, which needs no bundle", async () => {
|
|
227
|
+
resolveTargetMock.mockReturnValue({
|
|
228
|
+
...LOCAL_ENTRY,
|
|
229
|
+
cloud: "vellum",
|
|
230
|
+
} as unknown as assistantConfig.AssistantEntry);
|
|
231
|
+
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
232
|
+
try {
|
|
233
|
+
await expect(debugBundle()).rejects.toThrow("process.exit:1");
|
|
234
|
+
} finally {
|
|
235
|
+
errorSpy.mockRestore();
|
|
236
|
+
}
|
|
237
|
+
expect(fetchCalls).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("asks for a login when the assistant is not registered", async () => {
|
|
241
|
+
resolveTargetMock.mockReturnValue({
|
|
242
|
+
...LOCAL_ENTRY,
|
|
243
|
+
platformAssistantId: undefined,
|
|
244
|
+
} as unknown as assistantConfig.AssistantEntry);
|
|
245
|
+
const errors: string[] = [];
|
|
246
|
+
const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
|
|
247
|
+
errors.push(String(msg));
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
await expect(debugBundle()).rejects.toThrow("process.exit:1");
|
|
251
|
+
} finally {
|
|
252
|
+
errorSpy.mockRestore();
|
|
253
|
+
}
|
|
254
|
+
expect(errors.join("\n")).toContain("vellum login");
|
|
255
|
+
expect(fetchCalls).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vellum debug-bundle [name]`
|
|
3
|
+
*
|
|
4
|
+
* Send Vellum staff a debug bundle of a self-hosted assistant: the
|
|
5
|
+
* workspace, its logs, and the gateway's own database, with no credentials.
|
|
6
|
+
* Staff open it on a throwaway debug clone. The platform only hands out the
|
|
7
|
+
* upload URL while "Allow Staff Access" is on, and the bundle is deleted
|
|
8
|
+
* after seven days.
|
|
9
|
+
*
|
|
10
|
+
* Steps: the daemon is checked for the debug profile, the platform mints
|
|
11
|
+
* the upload URL, the daemon exports with the debug profile, and this
|
|
12
|
+
* command waits for the job so the result is real.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
formatAssistantReference,
|
|
16
|
+
resolveTargetAssistant,
|
|
17
|
+
} from "../lib/assistant-config.js";
|
|
18
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
19
|
+
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
20
|
+
import { pollJobUntilDone } from "../lib/job-polling.js";
|
|
21
|
+
import {
|
|
22
|
+
MigrationInProgressError,
|
|
23
|
+
localRuntimeExportToGcs,
|
|
24
|
+
localRuntimeIdentity,
|
|
25
|
+
localRuntimePollJobStatus,
|
|
26
|
+
} from "../lib/local-runtime-client.js";
|
|
27
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
28
|
+
import {
|
|
29
|
+
authHeaders,
|
|
30
|
+
authHeadersForKnownOrganization,
|
|
31
|
+
getPlatformUrl,
|
|
32
|
+
readPlatformToken,
|
|
33
|
+
} from "../lib/platform-client.js";
|
|
34
|
+
import { compareVersions } from "../lib/version-compat.js";
|
|
35
|
+
|
|
36
|
+
// Matches the daemon's own upload deadline (EXPORT_TO_GCS_PUT_TIMEOUT_MS in
|
|
37
|
+
// assistant/src/runtime/routes/migration-routes.ts), so a slow but healthy
|
|
38
|
+
// export is never reported as failed while it is still uploading.
|
|
39
|
+
const EXPORT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
40
|
+
// The daemon release that added `profile: "debug"`. An older daemon strips
|
|
41
|
+
// the field and exports a normal teleport bundle, which carries the owner's
|
|
42
|
+
// credentials. That must never reach the staff bucket.
|
|
43
|
+
export const DEBUG_PROFILE_MIN_VERSION = "0.12.3";
|
|
44
|
+
// Renew a cached guardian token that is about to lapse instead of starting
|
|
45
|
+
// a long export on it.
|
|
46
|
+
const TOKEN_EXPIRY_MARGIN_MS = 60 * 1000;
|
|
47
|
+
|
|
48
|
+
function printHelp(): void {
|
|
49
|
+
console.log(`Usage: vellum debug-bundle [name]
|
|
50
|
+
|
|
51
|
+
Send Vellum staff a debug bundle of a self-hosted assistant so they can
|
|
52
|
+
inspect a copy of it. The bundle holds the assistant's workspace, its
|
|
53
|
+
logs, and the gateway's own database. It never holds credentials.
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
name Assistant ID or unique display name. Defaults to the active
|
|
57
|
+
assistant (see 'vellum use'). Multi-word names need no quotes.
|
|
58
|
+
|
|
59
|
+
Requirements:
|
|
60
|
+
- 'Allow Staff Access' is on for the assistant (Settings > Privacy).
|
|
61
|
+
- You are logged in ('vellum login'), which also registers the
|
|
62
|
+
assistant with the platform.
|
|
63
|
+
- The assistant runs version ${DEBUG_PROFILE_MIN_VERSION} or newer.
|
|
64
|
+
- The assistant is self-hosted. Vellum-hosted assistants need no bundle;
|
|
65
|
+
staff clone them directly.
|
|
66
|
+
|
|
67
|
+
Side effects:
|
|
68
|
+
- Uploads the bundle to Vellum's debug-bundle storage, where it is
|
|
69
|
+
deleted after 7 days. Nothing on this machine changes.
|
|
70
|
+
- Records a 'debug bundle exported' entry in your account's audit history.
|
|
71
|
+
- Waits for the upload to finish (up to 60 minutes for a large workspace).
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
vellum debug-bundle
|
|
75
|
+
vellum debug-bundle my-assistant
|
|
76
|
+
vellum debug-bundle Support Bot
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function debugBundle(): Promise<void> {
|
|
81
|
+
const args = process.argv.slice(3);
|
|
82
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
83
|
+
printHelp();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entry = resolveTargetAssistant(parseAssistantTargetArg(args));
|
|
88
|
+
const reference = formatAssistantReference(entry);
|
|
89
|
+
if (entry.cloud === "vellum") {
|
|
90
|
+
console.error(
|
|
91
|
+
`${reference} is hosted by Vellum, so staff can clone it directly. No bundle is needed.`,
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const platformToken = readPlatformToken();
|
|
97
|
+
if (!platformToken) {
|
|
98
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const platformAssistantId = entry.platformAssistantId?.trim();
|
|
102
|
+
if (!platformAssistantId) {
|
|
103
|
+
console.error(
|
|
104
|
+
`${reference} is not registered with the platform yet. Run 'vellum login' and retry.`,
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const platformUrl = entry.platformBaseUrl?.trim() || getPlatformUrl();
|
|
109
|
+
const organizationId = entry.platformOrganizationId?.trim();
|
|
110
|
+
|
|
111
|
+
// Step 1: a guardian token for the daemon, and a check that the daemon
|
|
112
|
+
// knows the debug profile. This comes before the platform call so a
|
|
113
|
+
// daemon that would export credentials is refused before any URL exists.
|
|
114
|
+
let accessToken = await daemonAccessToken(entry, reference, false);
|
|
115
|
+
const { version } = await localRuntimeIdentity(entry, accessToken);
|
|
116
|
+
const comparison = compareVersions(version, DEBUG_PROFILE_MIN_VERSION);
|
|
117
|
+
if (comparison === null || comparison < 0) {
|
|
118
|
+
console.error(
|
|
119
|
+
`${reference} runs version ${version || "unknown"}, which cannot build a debug bundle. Update it to ${DEBUG_PROFILE_MIN_VERSION} or newer ('vellum upgrade') and retry.`,
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 2: the platform mints the upload URL, and refuses unless the
|
|
125
|
+
// owner's staff access grant is active.
|
|
126
|
+
const headers = organizationId
|
|
127
|
+
? authHeadersForKnownOrganization(platformToken, organizationId)
|
|
128
|
+
: await authHeaders(platformToken, platformUrl);
|
|
129
|
+
const urlResponse = await loopbackSafeFetch(
|
|
130
|
+
`${platformUrl}/v1/assistants/${encodeURIComponent(platformAssistantId)}/debug-bundle-upload-url/`,
|
|
131
|
+
{ method: "POST", headers },
|
|
132
|
+
);
|
|
133
|
+
if (urlResponse.status === 403) {
|
|
134
|
+
console.error(
|
|
135
|
+
"Staff access is off for this assistant. Turn on 'Allow Staff Access' in Settings > Privacy, then retry.",
|
|
136
|
+
);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (!urlResponse.ok) {
|
|
140
|
+
const detail = await urlResponse.text().catch(() => "");
|
|
141
|
+
console.error(
|
|
142
|
+
`Error: Could not get an upload URL (${urlResponse.status}): ${detail || urlResponse.statusText}`,
|
|
143
|
+
);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const { url: uploadUrl } = (await urlResponse.json()) as { url: string };
|
|
147
|
+
|
|
148
|
+
// Step 3: the daemon builds and uploads the bundle.
|
|
149
|
+
let jobId: string;
|
|
150
|
+
try {
|
|
151
|
+
({ jobId } = await localRuntimeExportToGcs(entry, accessToken, {
|
|
152
|
+
uploadUrl,
|
|
153
|
+
description: "debug bundle for Vellum staff",
|
|
154
|
+
profile: "debug",
|
|
155
|
+
}));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err instanceof MigrationInProgressError) {
|
|
158
|
+
console.error(
|
|
159
|
+
`Error: Another export is already in progress (job ${err.existingJobId}). Wait for it to finish, then retry.`,
|
|
160
|
+
);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
console.log(`Export started (job ${jobId})...`);
|
|
166
|
+
|
|
167
|
+
// Step 4: wait for the upload so the owner sees a real result. A large
|
|
168
|
+
// export can outlive the guardian token, so a 401 mid-poll re-leases one.
|
|
169
|
+
const terminal = await pollJobUntilDone({
|
|
170
|
+
label: "debug bundle export",
|
|
171
|
+
poll: () => localRuntimePollJobStatus(entry, accessToken, jobId),
|
|
172
|
+
timeoutMs: EXPORT_TIMEOUT_MS,
|
|
173
|
+
refreshOn401: async () => {
|
|
174
|
+
accessToken = await daemonAccessToken(entry, reference, true);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (terminal.status === "failed") {
|
|
178
|
+
console.error(`Error: Export failed: ${terminal.error}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
console.log(
|
|
182
|
+
"Debug bundle sent to Vellum. Staff can open it for the next 7 days.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function daemonAccessToken(
|
|
187
|
+
entry: {
|
|
188
|
+
assistantId: string;
|
|
189
|
+
runtimeUrl: string;
|
|
190
|
+
guardianBootstrapSecret?: string;
|
|
191
|
+
},
|
|
192
|
+
reference: string,
|
|
193
|
+
forceRefresh: boolean,
|
|
194
|
+
): Promise<string> {
|
|
195
|
+
if (!forceRefresh) {
|
|
196
|
+
const cached = loadGuardianToken(entry.assistantId);
|
|
197
|
+
if (
|
|
198
|
+
cached &&
|
|
199
|
+
new Date(cached.accessTokenExpiresAt).getTime() - Date.now() >
|
|
200
|
+
TOKEN_EXPIRY_MARGIN_MS
|
|
201
|
+
) {
|
|
202
|
+
return cached.accessToken;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return (
|
|
207
|
+
await leaseGuardianToken(
|
|
208
|
+
entry.runtimeUrl,
|
|
209
|
+
entry.assistantId,
|
|
210
|
+
entry.guardianBootstrapSecret,
|
|
211
|
+
)
|
|
212
|
+
).accessToken;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
215
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
216
|
+
console.error(`Error: Could not connect to ${reference}. Is it running?`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { rollback } from "./commands/rollback";
|
|
|
31
31
|
import { setup } from "./commands/setup";
|
|
32
32
|
import { sleep } from "./commands/sleep";
|
|
33
33
|
import { ssh } from "./commands/ssh";
|
|
34
|
+
import { debugBundle } from "./commands/debug-bundle";
|
|
34
35
|
import { teleport } from "./commands/teleport";
|
|
35
36
|
import { terminal } from "./commands/terminal";
|
|
36
37
|
import { tunnel } from "./commands/tunnel";
|
|
@@ -45,6 +46,7 @@ import { loadGuardianToken } from "./lib/guardian-token";
|
|
|
45
46
|
import { checkHealth } from "./lib/health-check";
|
|
46
47
|
|
|
47
48
|
const commands = {
|
|
49
|
+
"debug-bundle": debugBundle,
|
|
48
50
|
backup,
|
|
49
51
|
clean,
|
|
50
52
|
client,
|
|
@@ -125,6 +127,9 @@ function printHelp(): void {
|
|
|
125
127
|
console.log(" setup Configure API keys interactively");
|
|
126
128
|
console.log(" sleep Stop the assistant process");
|
|
127
129
|
console.log(" ssh SSH into a remote assistant instance");
|
|
130
|
+
console.log(
|
|
131
|
+
" debug-bundle Send Vellum staff a debug bundle of a self-hosted assistant",
|
|
132
|
+
);
|
|
128
133
|
console.log(" teleport Transfer assistant data between environments");
|
|
129
134
|
console.log(" terminal Open a terminal into a managed assistant container");
|
|
130
135
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
@@ -116,12 +116,23 @@ async function throwIfInProgress(
|
|
|
116
116
|
export async function localRuntimeExportToGcs(
|
|
117
117
|
entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
|
|
118
118
|
token: string,
|
|
119
|
-
params: {
|
|
119
|
+
params: {
|
|
120
|
+
uploadUrl: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
/**
|
|
123
|
+
* `"debug"` builds a bundle for Vellum staff: no credentials, plus the
|
|
124
|
+
* gateway's database and logs. Omitted means a normal teleport bundle.
|
|
125
|
+
*/
|
|
126
|
+
profile?: "migration" | "debug";
|
|
127
|
+
},
|
|
120
128
|
): Promise<{ jobId: string }> {
|
|
121
129
|
const body: Record<string, unknown> = { upload_url: params.uploadUrl };
|
|
122
130
|
if (params.description !== undefined) {
|
|
123
131
|
body.description = params.description;
|
|
124
132
|
}
|
|
133
|
+
if (params.profile !== undefined) {
|
|
134
|
+
body.profile = params.profile;
|
|
135
|
+
}
|
|
125
136
|
|
|
126
137
|
const response = await loopbackSafeFetch(
|
|
127
138
|
resolveRuntimeMigrationUrl(entry, "export-to-gcs"),
|