@vellumai/cli 0.9.1-staging.1 → 0.10.0-staging.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/package.json
CHANGED
|
@@ -127,8 +127,6 @@ describe("buildIngressNginxConfig", () => {
|
|
|
127
127
|
"location = /v1/pair/ { return 404; }",
|
|
128
128
|
"location = /v1/pair/web-init { return 404; }",
|
|
129
129
|
"location = /v1/pair/web-init/ { return 404; }",
|
|
130
|
-
"location = /v1/remote-web/pairing-challenge { return 404; }",
|
|
131
|
-
"location = /v1/remote-web/pairing-challenge/ { return 404; }",
|
|
132
130
|
"location = /v1/devices { return 404; }",
|
|
133
131
|
"location = /v1/devices/ { return 404; }",
|
|
134
132
|
"location = /v1/devices/revoke { return 404; }",
|
|
@@ -268,4 +268,203 @@ describe("pair command", () => {
|
|
|
268
268
|
const out = JSON.parse(logs.join("\n"));
|
|
269
269
|
expect(out.gatewayUrl).toBe(OVERRIDE);
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
test("--web creates a browser pairing URL without printing tokens", async () => {
|
|
273
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
274
|
+
const origFetch = globalThis.fetch;
|
|
275
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
276
|
+
calls.push([url, init]);
|
|
277
|
+
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
278
|
+
return new Response(
|
|
279
|
+
JSON.stringify({
|
|
280
|
+
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
281
|
+
}),
|
|
282
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
286
|
+
return new Response(
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
deviceCode: "device-code",
|
|
289
|
+
userCode: "ABCD-EFGH",
|
|
290
|
+
verificationUri:
|
|
291
|
+
"https://abc123.ngrok.app/assistant-123/assistant/pair",
|
|
292
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
293
|
+
expiresInSeconds: 600,
|
|
294
|
+
intervalSeconds: 5,
|
|
295
|
+
}),
|
|
296
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return new Response("not found", { status: 404 });
|
|
300
|
+
}) as unknown as typeof fetch;
|
|
301
|
+
|
|
302
|
+
const logs: string[] = [];
|
|
303
|
+
const logSpy = spyOn(console, "log").mockImplementation(
|
|
304
|
+
(...a: unknown[]) => {
|
|
305
|
+
logs.push(a.join(" "));
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
process.argv = [
|
|
310
|
+
"bun",
|
|
311
|
+
"vellum",
|
|
312
|
+
"pair",
|
|
313
|
+
"--web",
|
|
314
|
+
"--url",
|
|
315
|
+
"https://abc123.ngrok.app/assistant-123/assistant/",
|
|
316
|
+
"--json",
|
|
317
|
+
];
|
|
318
|
+
try {
|
|
319
|
+
await pair();
|
|
320
|
+
} finally {
|
|
321
|
+
logSpy.mockRestore();
|
|
322
|
+
globalThis.fetch = origFetch;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
expect(calls).toHaveLength(2);
|
|
326
|
+
expect(calls[1][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
|
|
327
|
+
expect(JSON.parse(calls[1][1]?.body as string)).toEqual({
|
|
328
|
+
publicBaseUrl: "https://abc123.ngrok.app/assistant-123",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const out = JSON.parse(logs.join("\n"));
|
|
332
|
+
expect(out).toEqual({
|
|
333
|
+
pairUrl:
|
|
334
|
+
"https://abc123.ngrok.app/assistant-123/assistant/pair#device_code=device-code",
|
|
335
|
+
userCode: "ABCD-EFGH",
|
|
336
|
+
verificationUri: "https://abc123.ngrok.app/assistant-123/assistant/pair",
|
|
337
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
338
|
+
expiresInSeconds: 600,
|
|
339
|
+
});
|
|
340
|
+
expect(logs.join("\n")).not.toContain("access");
|
|
341
|
+
expect(logs.join("\n")).not.toContain("refresh");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("--web refuses when the web remote ingress feature flag is off", async () => {
|
|
345
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
346
|
+
const origFetch = globalThis.fetch;
|
|
347
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
348
|
+
calls.push([url, init]);
|
|
349
|
+
return new Response(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
flags: [{ key: "web-remote-ingress", enabled: false }],
|
|
352
|
+
}),
|
|
353
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
354
|
+
);
|
|
355
|
+
}) as unknown as typeof fetch;
|
|
356
|
+
|
|
357
|
+
const errors: string[] = [];
|
|
358
|
+
const errSpy = spyOn(console, "error").mockImplementation(
|
|
359
|
+
(...a: unknown[]) => {
|
|
360
|
+
errors.push(a.join(" "));
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(((
|
|
364
|
+
code?: number,
|
|
365
|
+
) => {
|
|
366
|
+
throw new Error(`exit:${code}`);
|
|
367
|
+
}) as never);
|
|
368
|
+
|
|
369
|
+
process.argv = [
|
|
370
|
+
"bun",
|
|
371
|
+
"vellum",
|
|
372
|
+
"pair",
|
|
373
|
+
"--web",
|
|
374
|
+
"--url",
|
|
375
|
+
"https://abc123.ngrok.app",
|
|
376
|
+
];
|
|
377
|
+
let exited = false;
|
|
378
|
+
try {
|
|
379
|
+
await pair();
|
|
380
|
+
} catch (e) {
|
|
381
|
+
exited = (e as Error).message === "exit:1";
|
|
382
|
+
} finally {
|
|
383
|
+
errSpy.mockRestore();
|
|
384
|
+
exitSpy.mockRestore();
|
|
385
|
+
globalThis.fetch = origFetch;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
expect(exited).toBe(true);
|
|
389
|
+
expect(errors.join("\n")).toContain("web-remote-ingress");
|
|
390
|
+
expect(calls).toHaveLength(1);
|
|
391
|
+
expect(calls[0][0]).toBe(
|
|
392
|
+
`${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("--web-approve approves a browser pairing code over loopback", async () => {
|
|
397
|
+
writeFileSync(
|
|
398
|
+
join(testDir, ".vellum.lock.json"),
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
assistants: [
|
|
401
|
+
{
|
|
402
|
+
assistantId: "pair-test",
|
|
403
|
+
runtimeUrl: LOCAL_URL,
|
|
404
|
+
localUrl: LOCAL_URL,
|
|
405
|
+
cloud: "local",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
activeAssistant: "pair-test",
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
413
|
+
const origFetch = globalThis.fetch;
|
|
414
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
415
|
+
calls.push([url, init]);
|
|
416
|
+
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
417
|
+
return new Response(
|
|
418
|
+
JSON.stringify({
|
|
419
|
+
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
420
|
+
}),
|
|
421
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
425
|
+
return new Response(
|
|
426
|
+
JSON.stringify({
|
|
427
|
+
status: "approved",
|
|
428
|
+
verificationUri: "https://abc123.ngrok.app/assistant/pair",
|
|
429
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
430
|
+
}),
|
|
431
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return new Response("not found", { status: 404 });
|
|
435
|
+
}) as unknown as typeof fetch;
|
|
436
|
+
|
|
437
|
+
const logs: string[] = [];
|
|
438
|
+
const logSpy = spyOn(console, "log").mockImplementation(
|
|
439
|
+
(...a: unknown[]) => {
|
|
440
|
+
logs.push(a.join(" "));
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
process.argv = [
|
|
445
|
+
"bun",
|
|
446
|
+
"vellum",
|
|
447
|
+
"pair",
|
|
448
|
+
"--web-approve",
|
|
449
|
+
"ABCD-EFGH",
|
|
450
|
+
"--json",
|
|
451
|
+
];
|
|
452
|
+
try {
|
|
453
|
+
await pair();
|
|
454
|
+
} finally {
|
|
455
|
+
logSpy.mockRestore();
|
|
456
|
+
globalThis.fetch = origFetch;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
expect(calls).toHaveLength(2);
|
|
460
|
+
expect(calls[1][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-verification`);
|
|
461
|
+
expect(JSON.parse(calls[1][1]?.body as string)).toEqual({
|
|
462
|
+
userCode: "ABCD-EFGH",
|
|
463
|
+
});
|
|
464
|
+
expect(JSON.parse(logs.join("\n"))).toEqual({
|
|
465
|
+
status: "approved",
|
|
466
|
+
verificationUri: "https://abc123.ngrok.app/assistant/pair",
|
|
467
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
468
|
+
});
|
|
469
|
+
});
|
|
271
470
|
});
|
package/src/commands/pair.ts
CHANGED
|
@@ -25,6 +25,11 @@ import {
|
|
|
25
25
|
getClientRegistrationHeaders,
|
|
26
26
|
} from "../lib/client-identity.js";
|
|
27
27
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
28
|
+
import {
|
|
29
|
+
formatFeatureFlagGateMessage,
|
|
30
|
+
isAssistantFeatureFlagEnabled,
|
|
31
|
+
WEB_REMOTE_INGRESS_FLAG,
|
|
32
|
+
} from "../lib/feature-flags.js";
|
|
28
33
|
import { getLocalLanIPv4 } from "../lib/local.js";
|
|
29
34
|
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
30
35
|
|
|
@@ -50,12 +55,17 @@ OPTIONS:
|
|
|
50
55
|
--url <url> Reachable gateway URL to advertise in the bundle
|
|
51
56
|
(default: the assistant's runtime URL, not loopback)
|
|
52
57
|
--label <name> Human label for this pairing (echoed in the output)
|
|
58
|
+
--web Create a browser pairing URL for remote web access
|
|
59
|
+
--web-approve <code>
|
|
60
|
+
Approve a browser pairing code shown by /assistant/pair
|
|
53
61
|
--json Output the raw bundle as JSON
|
|
54
62
|
|
|
55
63
|
EXAMPLES:
|
|
56
64
|
vellum pair
|
|
57
65
|
vellum pair "My Assistant" --label "phone"
|
|
58
66
|
vellum pair --url https://abc123.ngrok.app
|
|
67
|
+
vellum pair --web --url https://abc123.ngrok.app
|
|
68
|
+
vellum pair --web-approve ABCD-EFGH
|
|
59
69
|
vellum pair --json
|
|
60
70
|
`);
|
|
61
71
|
}
|
|
@@ -72,6 +82,71 @@ interface PairResponse {
|
|
|
72
82
|
refreshAfter?: string;
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
interface RemoteWebPairingChallengeResponse {
|
|
86
|
+
deviceCode: string;
|
|
87
|
+
userCode: string;
|
|
88
|
+
verificationUri: string;
|
|
89
|
+
expiresAt: string;
|
|
90
|
+
expiresInSeconds: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RemoteWebPairingApprovalResponse {
|
|
94
|
+
status: "approved";
|
|
95
|
+
verificationUri: string;
|
|
96
|
+
expiresAt: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizePublicBaseUrl(value: string): string {
|
|
100
|
+
const url = new URL(value);
|
|
101
|
+
url.search = "";
|
|
102
|
+
url.hash = "";
|
|
103
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
104
|
+
const assistantIndex = parts.indexOf("assistant");
|
|
105
|
+
if (assistantIndex >= 0) {
|
|
106
|
+
parts.splice(assistantIndex);
|
|
107
|
+
}
|
|
108
|
+
url.pathname = parts.length ? `/${parts.join("/")}` : "/";
|
|
109
|
+
return url.toString().replace(/\/+$/, "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildRemoteWebPairingUrl(
|
|
113
|
+
challenge: RemoteWebPairingChallengeResponse,
|
|
114
|
+
): string {
|
|
115
|
+
const url = new URL(challenge.verificationUri);
|
|
116
|
+
url.hash = new URLSearchParams({
|
|
117
|
+
device_code: challenge.deviceCode,
|
|
118
|
+
}).toString();
|
|
119
|
+
return url.toString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function assertWebRemoteIngressEnabled(
|
|
123
|
+
assistantId: string,
|
|
124
|
+
runtimeUrl: string,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
let enabled: boolean;
|
|
127
|
+
try {
|
|
128
|
+
enabled = await isAssistantFeatureFlagEnabled(
|
|
129
|
+
assistantId,
|
|
130
|
+
WEB_REMOTE_INGRESS_FLAG,
|
|
131
|
+
{ runtimeUrl },
|
|
132
|
+
);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(
|
|
135
|
+
`Error: could not verify the \`${WEB_REMOTE_INGRESS_FLAG}\` feature flag. Is the assistant running? Try \`vellum wake\` and retry. ${
|
|
136
|
+
err instanceof Error ? err.message : String(err)
|
|
137
|
+
}`,
|
|
138
|
+
);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!enabled) {
|
|
143
|
+
console.error(
|
|
144
|
+
`Error: ${formatFeatureFlagGateMessage(WEB_REMOTE_INGRESS_FLAG)}`,
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
75
150
|
export async function pair(): Promise<void> {
|
|
76
151
|
const rawArgs = process.argv.slice(3);
|
|
77
152
|
|
|
@@ -81,12 +156,27 @@ export async function pair(): Promise<void> {
|
|
|
81
156
|
}
|
|
82
157
|
|
|
83
158
|
const jsonOutput = rawArgs.includes("--json");
|
|
84
|
-
|
|
159
|
+
const webPairing = rawArgs.includes("--web");
|
|
160
|
+
const webApproval = rawArgs.includes("--web-approve");
|
|
161
|
+
let args = rawArgs.filter((a) => a !== "--json" && a !== "--web");
|
|
85
162
|
|
|
86
163
|
const [label, afterLabel] = extractFlag(args, "--label");
|
|
87
|
-
const [
|
|
164
|
+
const [webApproveCode, afterWebApprove] = extractFlag(
|
|
165
|
+
afterLabel,
|
|
166
|
+
"--web-approve",
|
|
167
|
+
);
|
|
168
|
+
const [urlOverride, afterUrl] = extractFlag(afterWebApprove, "--url");
|
|
88
169
|
args = afterUrl;
|
|
89
170
|
|
|
171
|
+
if (webPairing && webApproveCode) {
|
|
172
|
+
console.error("Error: use either --web or --web-approve, not both.");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
if (webApproval && !webApproveCode) {
|
|
176
|
+
console.error("Error: --web-approve requires a pairing code.");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
90
180
|
// Resolve the target. An explicit argument is matched by display name OR id
|
|
91
181
|
// (with the standard ambiguity error); no argument falls back to the active
|
|
92
182
|
// assistant. Join positional tokens so multi-word display names work even
|
|
@@ -126,7 +216,7 @@ export async function pair(): Promise<void> {
|
|
|
126
216
|
// so without an explicit --url the bundle would point the other machine at
|
|
127
217
|
// its own localhost. Refuse to advertise a loopback URL unless the user
|
|
128
218
|
// explicitly passed one. (An explicit --url is trusted as-is.)
|
|
129
|
-
if (!urlOverride && isLoopbackHost(advertisedUrl)) {
|
|
219
|
+
if (!urlOverride && !webApproveCode && isLoopbackHost(advertisedUrl)) {
|
|
130
220
|
const lan = getLocalLanIPv4();
|
|
131
221
|
// Use THIS assistant's gateway port (not the global default) — second
|
|
132
222
|
// local instances listen on a different port.
|
|
@@ -149,6 +239,126 @@ export async function pair(): Promise<void> {
|
|
|
149
239
|
process.exit(1);
|
|
150
240
|
}
|
|
151
241
|
|
|
242
|
+
if (webApproveCode) {
|
|
243
|
+
await assertWebRemoteIngressEnabled(entry.assistantId, mintUrl);
|
|
244
|
+
|
|
245
|
+
let response: Response;
|
|
246
|
+
try {
|
|
247
|
+
response = await loopbackSafeFetch(
|
|
248
|
+
`${mintUrl}/v1/remote-web/pairing-verification`,
|
|
249
|
+
{
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json" },
|
|
252
|
+
body: JSON.stringify({ userCode: webApproveCode }),
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error(
|
|
257
|
+
`Error: could not reach the gateway at ${mintUrl} ` +
|
|
258
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
259
|
+
);
|
|
260
|
+
console.error("Is the assistant running? Try `vellum wake`.");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const body = await response.text().catch(() => "");
|
|
266
|
+
console.error(
|
|
267
|
+
`Error: HTTP ${response.status}: ${body || response.statusText}`,
|
|
268
|
+
);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = (await response.json()) as RemoteWebPairingApprovalResponse;
|
|
273
|
+
if (jsonOutput) {
|
|
274
|
+
console.log(JSON.stringify(result, null, 2));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log("Remote web pairing approved.");
|
|
278
|
+
console.log(`Expires: ${result.expiresAt}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (webPairing) {
|
|
283
|
+
await assertWebRemoteIngressEnabled(entry.assistantId, mintUrl);
|
|
284
|
+
|
|
285
|
+
let publicBaseUrl: string;
|
|
286
|
+
try {
|
|
287
|
+
publicBaseUrl = normalizePublicBaseUrl(advertisedUrl);
|
|
288
|
+
} catch {
|
|
289
|
+
console.error(`Error: invalid --url value '${advertisedUrl}'.`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let response: Response;
|
|
294
|
+
try {
|
|
295
|
+
response = await loopbackSafeFetch(
|
|
296
|
+
`${mintUrl}/v1/remote-web/pairing-challenge`,
|
|
297
|
+
{
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: { "Content-Type": "application/json" },
|
|
300
|
+
body: JSON.stringify({ publicBaseUrl }),
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(
|
|
305
|
+
`Error: could not reach the gateway at ${mintUrl} ` +
|
|
306
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
307
|
+
);
|
|
308
|
+
console.error("Is the assistant running? Try `vellum wake`.");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
const body = await response.text().catch(() => "");
|
|
314
|
+
console.error(
|
|
315
|
+
`Error: HTTP ${response.status}: ${body || response.statusText}`,
|
|
316
|
+
);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const challenge =
|
|
321
|
+
(await response.json()) as RemoteWebPairingChallengeResponse;
|
|
322
|
+
const pairUrl = buildRemoteWebPairingUrl(challenge);
|
|
323
|
+
|
|
324
|
+
if (jsonOutput) {
|
|
325
|
+
console.log(
|
|
326
|
+
JSON.stringify(
|
|
327
|
+
{
|
|
328
|
+
pairUrl,
|
|
329
|
+
userCode: challenge.userCode,
|
|
330
|
+
verificationUri: challenge.verificationUri,
|
|
331
|
+
expiresAt: challenge.expiresAt,
|
|
332
|
+
expiresInSeconds: challenge.expiresInSeconds,
|
|
333
|
+
},
|
|
334
|
+
null,
|
|
335
|
+
2,
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const displayName = entry.name || entry.assistantName || entry.assistantId;
|
|
342
|
+
console.log(`Created remote web pairing for ${displayName}.`);
|
|
343
|
+
console.log("");
|
|
344
|
+
console.log("Open this URL in the browser:");
|
|
345
|
+
console.log("");
|
|
346
|
+
console.log(` ${pairUrl}`);
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log("Approve this pairing locally when you're ready:");
|
|
349
|
+
console.log("");
|
|
350
|
+
const approveTarget = assistantName
|
|
351
|
+
? `${JSON.stringify(assistantName)} `
|
|
352
|
+
: "";
|
|
353
|
+
console.log(` Code: ${challenge.userCode}`);
|
|
354
|
+
console.log(
|
|
355
|
+
` Run: vellum pair ${approveTarget}--web-approve ${challenge.userCode}`,
|
|
356
|
+
);
|
|
357
|
+
console.log("");
|
|
358
|
+
console.log(`Expires: ${challenge.expiresAt}`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
152
362
|
// Fresh per-pairing device identity — each `vellum pair` is independently
|
|
153
363
|
// revocable.
|
|
154
364
|
const deviceId = nanoid();
|
package/src/lib/nginx-ingress.ts
CHANGED
|
@@ -255,8 +255,6 @@ function buildRemoteWebIngressLocations(opts: {
|
|
|
255
255
|
location = /v1/pair/ { return 404; }
|
|
256
256
|
location = /v1/pair/web-init { return 404; }
|
|
257
257
|
location = /v1/pair/web-init/ { return 404; }
|
|
258
|
-
location = /v1/remote-web/pairing-challenge { return 404; }
|
|
259
|
-
location = /v1/remote-web/pairing-challenge/ { return 404; }
|
|
260
258
|
location = /v1/devices { return 404; }
|
|
261
259
|
location = /v1/devices/ { return 404; }
|
|
262
260
|
location = /v1/devices/revoke { return 404; }
|
|
@@ -265,6 +263,8 @@ function buildRemoteWebIngressLocations(opts: {
|
|
|
265
263
|
location = /v1/guardian/init/ { return 404; }
|
|
266
264
|
location = /v1/guardian/reset-bootstrap { return 404; }
|
|
267
265
|
location = /v1/guardian/reset-bootstrap/ { return 404; }
|
|
266
|
+
location = /v1/remote-web/pairing-verification { return 404; }
|
|
267
|
+
location = /v1/remote-web/pairing-verification/ { return 404; }
|
|
268
268
|
location ^~ /assistant/__local/ { return 404; }
|
|
269
269
|
location ^~ /assistant/__gateway/ { return 404; }
|
|
270
270
|
|
|
@@ -27,6 +27,7 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
|
|
|
27
27
|
fireworks: "FIREWORKS_API_KEY",
|
|
28
28
|
openrouter: "OPENROUTER_API_KEY",
|
|
29
29
|
minimax: "MINIMAX_API_KEY",
|
|
30
|
+
atlascloud: "ATLASCLOUD_API_KEY",
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/** Search-provider env var names. Mirrors `SEARCH_PROVIDER_CATALOG` BYOK entries. */
|
|
@@ -34,6 +35,7 @@ export const SEARCH_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
|
|
|
34
35
|
perplexity: "PERPLEXITY_API_KEY",
|
|
35
36
|
brave: "BRAVE_API_KEY",
|
|
36
37
|
tavily: "TAVILY_API_KEY",
|
|
38
|
+
firecrawl: "FIRECRAWL_API_KEY",
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
/**
|