@vellumai/cli 0.7.0 → 0.7.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/AGENTS.md +3 -11
- package/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { hostname, userInfo } from "os";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
4
3
|
import { basename } from "path";
|
|
5
|
-
import qrcode from "qrcode-terminal";
|
|
6
4
|
import {
|
|
7
5
|
useCallback,
|
|
8
6
|
useEffect,
|
|
@@ -19,6 +17,7 @@ import { SPECIES_CONFIG, type Species } from "../lib/constants";
|
|
|
19
17
|
import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
|
|
20
18
|
import { checkHealth } from "../lib/health-check";
|
|
21
19
|
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
20
|
+
import { tuiLog } from "../lib/tui-log";
|
|
22
21
|
import { statusEmoji, withStatusEmoji } from "../lib/status-emoji";
|
|
23
22
|
import {
|
|
24
23
|
getTerminalCapabilities,
|
|
@@ -45,7 +44,6 @@ export const SLASH_COMMANDS = [
|
|
|
45
44
|
"/doctor",
|
|
46
45
|
"/exit",
|
|
47
46
|
"/help",
|
|
48
|
-
"/pair",
|
|
49
47
|
"/q",
|
|
50
48
|
"/quit",
|
|
51
49
|
"/retire",
|
|
@@ -172,14 +170,14 @@ async function runtimeRequest<T>(
|
|
|
172
170
|
assistantId: string,
|
|
173
171
|
path: string,
|
|
174
172
|
init?: RequestInit,
|
|
175
|
-
|
|
173
|
+
auth?: Record<string, string>,
|
|
176
174
|
): Promise<T> {
|
|
177
175
|
const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
|
|
178
176
|
const response = await fetch(url, {
|
|
179
177
|
...init,
|
|
180
178
|
headers: {
|
|
181
179
|
"Content-Type": "application/json",
|
|
182
|
-
...
|
|
180
|
+
...auth,
|
|
183
181
|
...(init?.headers as Record<string, string> | undefined),
|
|
184
182
|
},
|
|
185
183
|
});
|
|
@@ -218,7 +216,7 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
|
|
|
218
216
|
async function pollMessages(
|
|
219
217
|
baseUrl: string,
|
|
220
218
|
assistantId: string,
|
|
221
|
-
|
|
219
|
+
auth?: Record<string, string>,
|
|
222
220
|
): Promise<ListMessagesResponse> {
|
|
223
221
|
const params = new URLSearchParams({ conversationKey: assistantId });
|
|
224
222
|
return runtimeRequest<ListMessagesResponse>(
|
|
@@ -226,7 +224,7 @@ async function pollMessages(
|
|
|
226
224
|
assistantId,
|
|
227
225
|
`/messages?${params.toString()}`,
|
|
228
226
|
undefined,
|
|
229
|
-
|
|
227
|
+
auth,
|
|
230
228
|
);
|
|
231
229
|
}
|
|
232
230
|
|
|
@@ -235,7 +233,7 @@ async function sendMessage(
|
|
|
235
233
|
assistantId: string,
|
|
236
234
|
content: string,
|
|
237
235
|
signal?: AbortSignal,
|
|
238
|
-
|
|
236
|
+
auth?: Record<string, string>,
|
|
239
237
|
): Promise<SendMessageResponse> {
|
|
240
238
|
return runtimeRequest<SendMessageResponse>(
|
|
241
239
|
baseUrl,
|
|
@@ -251,7 +249,7 @@ async function sendMessage(
|
|
|
251
249
|
}),
|
|
252
250
|
signal,
|
|
253
251
|
},
|
|
254
|
-
|
|
252
|
+
auth,
|
|
255
253
|
);
|
|
256
254
|
}
|
|
257
255
|
|
|
@@ -260,7 +258,7 @@ async function submitDecision(
|
|
|
260
258
|
assistantId: string,
|
|
261
259
|
requestId: string,
|
|
262
260
|
decision: "allow" | "deny",
|
|
263
|
-
|
|
261
|
+
auth?: Record<string, string>,
|
|
264
262
|
): Promise<SubmitDecisionResponse> {
|
|
265
263
|
return runtimeRequest<SubmitDecisionResponse>(
|
|
266
264
|
baseUrl,
|
|
@@ -270,7 +268,7 @@ async function submitDecision(
|
|
|
270
268
|
method: "POST",
|
|
271
269
|
body: JSON.stringify({ requestId, decision }),
|
|
272
270
|
},
|
|
273
|
-
|
|
271
|
+
auth,
|
|
274
272
|
);
|
|
275
273
|
}
|
|
276
274
|
|
|
@@ -281,7 +279,7 @@ async function addTrustRule(
|
|
|
281
279
|
pattern: string,
|
|
282
280
|
scope: string,
|
|
283
281
|
decision: "allow" | "deny",
|
|
284
|
-
|
|
282
|
+
auth?: Record<string, string>,
|
|
285
283
|
): Promise<AddTrustRuleResponse> {
|
|
286
284
|
return runtimeRequest<AddTrustRuleResponse>(
|
|
287
285
|
baseUrl,
|
|
@@ -291,7 +289,7 @@ async function addTrustRule(
|
|
|
291
289
|
method: "POST",
|
|
292
290
|
body: JSON.stringify({ requestId, pattern, scope, decision }),
|
|
293
291
|
},
|
|
294
|
-
|
|
292
|
+
auth,
|
|
295
293
|
);
|
|
296
294
|
}
|
|
297
295
|
|
|
@@ -350,26 +348,39 @@ async function* streamEvents(
|
|
|
350
348
|
assistantId: string,
|
|
351
349
|
conversationKey: string,
|
|
352
350
|
signal: AbortSignal,
|
|
353
|
-
|
|
351
|
+
auth?: Record<string, string>,
|
|
354
352
|
): AsyncGenerator<SseEvent> {
|
|
355
353
|
const params = new URLSearchParams({ conversationKey });
|
|
356
354
|
const url = `${baseUrl}/v1/assistants/${assistantId}/events?${params.toString()}`;
|
|
355
|
+
const clientHeaders = getClientRegistrationHeaders();
|
|
356
|
+
tuiLog.info("sse connect", { url, clientHeaders });
|
|
357
357
|
const response = await fetch(url, {
|
|
358
358
|
headers: {
|
|
359
359
|
Accept: "text/event-stream",
|
|
360
|
-
...
|
|
361
|
-
...
|
|
360
|
+
...auth,
|
|
361
|
+
...clientHeaders,
|
|
362
362
|
},
|
|
363
363
|
signal,
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
tuiLog.info("sse response", {
|
|
367
|
+
status: response.status,
|
|
368
|
+
statusText: response.statusText,
|
|
369
|
+
contentType: response.headers.get("content-type"),
|
|
370
|
+
});
|
|
371
|
+
|
|
366
372
|
if (!response.ok) {
|
|
367
373
|
const body = await response.text().catch(() => "");
|
|
374
|
+
tuiLog.error("sse connection failed", {
|
|
375
|
+
status: response.status,
|
|
376
|
+
body: body.slice(0, 500),
|
|
377
|
+
});
|
|
368
378
|
throw new Error(
|
|
369
379
|
`SSE connection failed (${response.status}): ${body || response.statusText}`,
|
|
370
380
|
);
|
|
371
381
|
}
|
|
372
382
|
if (!response.body) {
|
|
383
|
+
tuiLog.error("sse response has no body");
|
|
373
384
|
throw new Error("No response body from SSE endpoint");
|
|
374
385
|
}
|
|
375
386
|
|
|
@@ -443,7 +454,7 @@ async function handleConfirmationPrompt(
|
|
|
443
454
|
requestId: string,
|
|
444
455
|
confirmation: PendingConfirmation,
|
|
445
456
|
chatApp: ChatAppHandle,
|
|
446
|
-
|
|
457
|
+
auth?: Record<string, string>,
|
|
447
458
|
): Promise<void> {
|
|
448
459
|
const preview = formatConfirmationPreview(
|
|
449
460
|
confirmation.toolName,
|
|
@@ -469,7 +480,7 @@ async function handleConfirmationPrompt(
|
|
|
469
480
|
const index = await chatApp.showSelection("Tool Approval", options);
|
|
470
481
|
|
|
471
482
|
if (index === 0) {
|
|
472
|
-
await submitDecision(baseUrl, assistantId, requestId, "allow",
|
|
483
|
+
await submitDecision(baseUrl, assistantId, requestId, "allow", auth);
|
|
473
484
|
chatApp.addStatus("\u2714 Allowed", "green");
|
|
474
485
|
return;
|
|
475
486
|
}
|
|
@@ -481,7 +492,7 @@ async function handleConfirmationPrompt(
|
|
|
481
492
|
confirmation,
|
|
482
493
|
chatApp,
|
|
483
494
|
"always_allow",
|
|
484
|
-
|
|
495
|
+
auth,
|
|
485
496
|
);
|
|
486
497
|
return;
|
|
487
498
|
}
|
|
@@ -493,12 +504,12 @@ async function handleConfirmationPrompt(
|
|
|
493
504
|
confirmation,
|
|
494
505
|
chatApp,
|
|
495
506
|
"always_deny",
|
|
496
|
-
|
|
507
|
+
auth,
|
|
497
508
|
);
|
|
498
509
|
return;
|
|
499
510
|
}
|
|
500
511
|
|
|
501
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny",
|
|
512
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
|
|
502
513
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
503
514
|
}
|
|
504
515
|
|
|
@@ -509,7 +520,7 @@ async function handlePatternSelection(
|
|
|
509
520
|
confirmation: PendingConfirmation,
|
|
510
521
|
chatApp: ChatAppHandle,
|
|
511
522
|
trustDecision: TrustDecision,
|
|
512
|
-
|
|
523
|
+
auth?: Record<string, string>,
|
|
513
524
|
): Promise<void> {
|
|
514
525
|
const allowlistOptions = confirmation.allowlistOptions ?? [];
|
|
515
526
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -530,12 +541,12 @@ async function handlePatternSelection(
|
|
|
530
541
|
chatApp,
|
|
531
542
|
selectedPattern,
|
|
532
543
|
trustDecision,
|
|
533
|
-
|
|
544
|
+
auth,
|
|
534
545
|
);
|
|
535
546
|
return;
|
|
536
547
|
}
|
|
537
548
|
|
|
538
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny",
|
|
549
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
|
|
539
550
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
540
551
|
}
|
|
541
552
|
|
|
@@ -547,7 +558,7 @@ async function handleScopeSelection(
|
|
|
547
558
|
chatApp: ChatAppHandle,
|
|
548
559
|
selectedPattern: string,
|
|
549
560
|
trustDecision: TrustDecision,
|
|
550
|
-
|
|
561
|
+
auth?: Record<string, string>,
|
|
551
562
|
): Promise<void> {
|
|
552
563
|
const scopeOptions = confirmation.scopeOptions ?? [];
|
|
553
564
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -564,14 +575,14 @@ async function handleScopeSelection(
|
|
|
564
575
|
selectedPattern,
|
|
565
576
|
scopeOptions[index].scope,
|
|
566
577
|
ruleDecision,
|
|
567
|
-
|
|
578
|
+
auth,
|
|
568
579
|
);
|
|
569
580
|
await submitDecision(
|
|
570
581
|
baseUrl,
|
|
571
582
|
assistantId,
|
|
572
583
|
requestId,
|
|
573
584
|
ruleDecision === "deny" ? "deny" : "allow",
|
|
574
|
-
|
|
585
|
+
auth,
|
|
575
586
|
);
|
|
576
587
|
const ruleLabel =
|
|
577
588
|
trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
|
|
@@ -583,7 +594,7 @@ async function handleScopeSelection(
|
|
|
583
594
|
return;
|
|
584
595
|
}
|
|
585
596
|
|
|
586
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny",
|
|
597
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", auth);
|
|
587
598
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
588
599
|
}
|
|
589
600
|
|
|
@@ -739,10 +750,6 @@ function HelpDisplay(): ReactElement {
|
|
|
739
750
|
{" /clear "}
|
|
740
751
|
<Text dimColor>Clear the screen</Text>
|
|
741
752
|
</Text>
|
|
742
|
-
<Text>
|
|
743
|
-
{" /pair "}
|
|
744
|
-
<Text dimColor>Generate a QR code for mobile device pairing</Text>
|
|
745
|
-
</Text>
|
|
746
753
|
<Text>
|
|
747
754
|
{" /help, ? "}
|
|
748
755
|
<Text dimColor>Show this help</Text>
|
|
@@ -1354,7 +1361,9 @@ interface ChatAppProps {
|
|
|
1354
1361
|
runtimeUrl: string;
|
|
1355
1362
|
assistantId: string;
|
|
1356
1363
|
species: Species;
|
|
1357
|
-
|
|
1364
|
+
/** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
|
|
1365
|
+
* { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
|
|
1366
|
+
auth?: Record<string, string>;
|
|
1358
1367
|
project?: string;
|
|
1359
1368
|
zone?: string;
|
|
1360
1369
|
onExit: () => void;
|
|
@@ -1365,7 +1374,7 @@ function ChatApp({
|
|
|
1365
1374
|
runtimeUrl,
|
|
1366
1375
|
assistantId,
|
|
1367
1376
|
species,
|
|
1368
|
-
|
|
1377
|
+
auth,
|
|
1369
1378
|
project,
|
|
1370
1379
|
zone,
|
|
1371
1380
|
onExit,
|
|
@@ -1653,6 +1662,10 @@ function ChatApp({
|
|
|
1653
1662
|
|
|
1654
1663
|
try {
|
|
1655
1664
|
const health = await checkHealthRuntime(runtimeUrl);
|
|
1665
|
+
tuiLog.info("health check", {
|
|
1666
|
+
status: health.status,
|
|
1667
|
+
message: health.message,
|
|
1668
|
+
});
|
|
1656
1669
|
h.hideSpinner();
|
|
1657
1670
|
h.updateHealthStatus(health.status);
|
|
1658
1671
|
if (health.status === "healthy" || health.status === "ok") {
|
|
@@ -1674,7 +1687,7 @@ function ChatApp({
|
|
|
1674
1687
|
const historyResponse = await pollMessages(
|
|
1675
1688
|
runtimeUrl,
|
|
1676
1689
|
assistantId,
|
|
1677
|
-
|
|
1690
|
+
auth,
|
|
1678
1691
|
);
|
|
1679
1692
|
h.hideSpinner();
|
|
1680
1693
|
if (historyResponse.messages.length > 0) {
|
|
@@ -1699,7 +1712,7 @@ function ChatApp({
|
|
|
1699
1712
|
assistantId,
|
|
1700
1713
|
assistantId,
|
|
1701
1714
|
sseAc.signal,
|
|
1702
|
-
|
|
1715
|
+
auth,
|
|
1703
1716
|
)) {
|
|
1704
1717
|
const hRef = handleRef_.current;
|
|
1705
1718
|
if (!hRef) continue;
|
|
@@ -1770,7 +1783,7 @@ function ChatApp({
|
|
|
1770
1783
|
event.persistentDecisionsAllowed,
|
|
1771
1784
|
},
|
|
1772
1785
|
hRef,
|
|
1773
|
-
|
|
1786
|
+
auth,
|
|
1774
1787
|
);
|
|
1775
1788
|
hRef.showSpinner("Working...");
|
|
1776
1789
|
break;
|
|
@@ -1801,7 +1814,7 @@ function ChatApp({
|
|
|
1801
1814
|
delivery,
|
|
1802
1815
|
}),
|
|
1803
1816
|
},
|
|
1804
|
-
|
|
1817
|
+
auth,
|
|
1805
1818
|
);
|
|
1806
1819
|
},
|
|
1807
1820
|
);
|
|
@@ -1850,9 +1863,12 @@ function ChatApp({
|
|
|
1850
1863
|
break;
|
|
1851
1864
|
}
|
|
1852
1865
|
}
|
|
1853
|
-
} catch {
|
|
1866
|
+
} catch (sseErr) {
|
|
1854
1867
|
// Stream ended — only report if not intentionally aborted
|
|
1855
1868
|
if (!sseAc.signal.aborted) {
|
|
1869
|
+
tuiLog.warn("sse stream disconnected", {
|
|
1870
|
+
error: String(sseErr),
|
|
1871
|
+
});
|
|
1856
1872
|
handleRef_.current?.addStatus(
|
|
1857
1873
|
"SSE stream disconnected — will reconnect on next message",
|
|
1858
1874
|
"yellow",
|
|
@@ -1869,10 +1885,11 @@ function ChatApp({
|
|
|
1869
1885
|
setConnectionState("connected");
|
|
1870
1886
|
return true;
|
|
1871
1887
|
} catch (err) {
|
|
1888
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1889
|
+
tuiLog.error("connection failed", { error: msg });
|
|
1872
1890
|
h.hideSpinner();
|
|
1873
1891
|
connectingRef.current = false;
|
|
1874
1892
|
h.updateHealthStatus("unreachable");
|
|
1875
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1876
1893
|
setConnectionState("error");
|
|
1877
1894
|
setConnectionError(msg);
|
|
1878
1895
|
h.addStatus(
|
|
@@ -1881,7 +1898,7 @@ function ChatApp({
|
|
|
1881
1898
|
);
|
|
1882
1899
|
return false;
|
|
1883
1900
|
}
|
|
1884
|
-
}, [runtimeUrl, assistantId,
|
|
1901
|
+
}, [runtimeUrl, assistantId, auth]);
|
|
1885
1902
|
|
|
1886
1903
|
const handleInput = useCallback(
|
|
1887
1904
|
async (input: string): Promise<void> => {
|
|
@@ -2088,9 +2105,7 @@ function ChatApp({
|
|
|
2088
2105
|
method: "POST",
|
|
2089
2106
|
headers: {
|
|
2090
2107
|
"Content-Type": "application/json",
|
|
2091
|
-
...
|
|
2092
|
-
? { Authorization: `Bearer ${bearerToken}` }
|
|
2093
|
-
: {}),
|
|
2108
|
+
...auth,
|
|
2094
2109
|
},
|
|
2095
2110
|
body: JSON.stringify({
|
|
2096
2111
|
conversationKey: assistantId,
|
|
@@ -2149,85 +2164,6 @@ function ChatApp({
|
|
|
2149
2164
|
return;
|
|
2150
2165
|
}
|
|
2151
2166
|
|
|
2152
|
-
if (trimmed === "/pair") {
|
|
2153
|
-
h.showSpinner("Generating pairing credentials...");
|
|
2154
|
-
|
|
2155
|
-
const isConnected = await ensureConnected();
|
|
2156
|
-
if (!isConnected) {
|
|
2157
|
-
h.hideSpinner();
|
|
2158
|
-
h.showError("Cannot pair — not connected to the assistant runtime.");
|
|
2159
|
-
return;
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
try {
|
|
2163
|
-
const pairingRequestId = randomUUID();
|
|
2164
|
-
const pairingSecret = randomBytes(32).toString("hex");
|
|
2165
|
-
const gatewayUrl = runtimeUrl;
|
|
2166
|
-
|
|
2167
|
-
// Call /pairing/register on the gateway (dedicated pairing proxy route)
|
|
2168
|
-
const registerUrl = `${runtimeUrl}/pairing/register`;
|
|
2169
|
-
const registerRes = await fetch(registerUrl, {
|
|
2170
|
-
method: "POST",
|
|
2171
|
-
headers: {
|
|
2172
|
-
"Content-Type": "application/json",
|
|
2173
|
-
...(bearerToken
|
|
2174
|
-
? { Authorization: `Bearer ${bearerToken}` }
|
|
2175
|
-
: {}),
|
|
2176
|
-
},
|
|
2177
|
-
body: JSON.stringify({
|
|
2178
|
-
pairingRequestId,
|
|
2179
|
-
pairingSecret,
|
|
2180
|
-
gatewayUrl,
|
|
2181
|
-
}),
|
|
2182
|
-
});
|
|
2183
|
-
|
|
2184
|
-
if (!registerRes.ok) {
|
|
2185
|
-
const body = await registerRes.text().catch(() => "");
|
|
2186
|
-
throw new Error(
|
|
2187
|
-
`HTTP ${registerRes.status}: ${body || registerRes.statusText}`,
|
|
2188
|
-
);
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
let username: string;
|
|
2192
|
-
try {
|
|
2193
|
-
username = userInfo().username;
|
|
2194
|
-
} catch {
|
|
2195
|
-
username = "";
|
|
2196
|
-
}
|
|
2197
|
-
const hostId = createHash("sha256")
|
|
2198
|
-
.update(hostname() + username)
|
|
2199
|
-
.digest("hex");
|
|
2200
|
-
const payload = JSON.stringify({
|
|
2201
|
-
type: "vellum-assistant",
|
|
2202
|
-
v: 4,
|
|
2203
|
-
id: hostId,
|
|
2204
|
-
g: gatewayUrl,
|
|
2205
|
-
pairingRequestId,
|
|
2206
|
-
pairingSecret,
|
|
2207
|
-
});
|
|
2208
|
-
|
|
2209
|
-
const qrString = await new Promise<string>((resolve) => {
|
|
2210
|
-
qrcode.generate(payload, { small: true }, (code: string) => {
|
|
2211
|
-
resolve(code);
|
|
2212
|
-
});
|
|
2213
|
-
});
|
|
2214
|
-
|
|
2215
|
-
h.hideSpinner();
|
|
2216
|
-
h.addStatus(
|
|
2217
|
-
`Pairing Ready\n\n` +
|
|
2218
|
-
`Scan this QR code with the Vellum iOS app:\n\n` +
|
|
2219
|
-
`${qrString}\n` +
|
|
2220
|
-
`This pairing request expires in 5 minutes. Run /pair again to generate a new one.`,
|
|
2221
|
-
);
|
|
2222
|
-
} catch (err) {
|
|
2223
|
-
h.hideSpinner();
|
|
2224
|
-
h.showError(
|
|
2225
|
-
`Pairing failed: ${err instanceof Error ? err.message : err}`,
|
|
2226
|
-
);
|
|
2227
|
-
}
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
2167
|
if (busyRef.current) {
|
|
2232
2168
|
// /btw is already handled above this block
|
|
2233
2169
|
if (!trimmed.startsWith("/")) {
|
|
@@ -2256,7 +2192,7 @@ function ChatApp({
|
|
|
2256
2192
|
assistantId,
|
|
2257
2193
|
trimmed,
|
|
2258
2194
|
controller.signal,
|
|
2259
|
-
|
|
2195
|
+
auth,
|
|
2260
2196
|
);
|
|
2261
2197
|
clearTimeout(timeoutId);
|
|
2262
2198
|
if (sendResult.accepted) {
|
|
@@ -2307,7 +2243,7 @@ function ChatApp({
|
|
|
2307
2243
|
assistantId,
|
|
2308
2244
|
trimmed,
|
|
2309
2245
|
controller.signal,
|
|
2310
|
-
|
|
2246
|
+
auth,
|
|
2311
2247
|
);
|
|
2312
2248
|
clearTimeout(timeoutId);
|
|
2313
2249
|
if (!sendResult.accepted) {
|
|
@@ -2334,7 +2270,7 @@ function ChatApp({
|
|
|
2334
2270
|
[
|
|
2335
2271
|
runtimeUrl,
|
|
2336
2272
|
assistantId,
|
|
2337
|
-
|
|
2273
|
+
auth,
|
|
2338
2274
|
project,
|
|
2339
2275
|
zone,
|
|
2340
2276
|
cleanup,
|
|
@@ -2648,7 +2584,7 @@ export function renderChatApp(
|
|
|
2648
2584
|
assistantId: string,
|
|
2649
2585
|
species: Species,
|
|
2650
2586
|
onExit: () => void,
|
|
2651
|
-
options?: {
|
|
2587
|
+
options?: { auth?: Record<string, string>; project?: string; zone?: string },
|
|
2652
2588
|
): ChatAppInstance {
|
|
2653
2589
|
let chatHandle: ChatAppHandle | null = null;
|
|
2654
2590
|
|
|
@@ -2657,7 +2593,7 @@ export function renderChatApp(
|
|
|
2657
2593
|
runtimeUrl={runtimeUrl}
|
|
2658
2594
|
assistantId={assistantId}
|
|
2659
2595
|
species={species}
|
|
2660
|
-
|
|
2596
|
+
auth={options?.auth}
|
|
2661
2597
|
project={options?.project}
|
|
2662
2598
|
zone={options?.zone}
|
|
2663
2599
|
onExit={onExit}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { hatch } from "./commands/hatch";
|
|
|
11
11
|
import { login, logout, whoami } from "./commands/login";
|
|
12
12
|
import { logs } from "./commands/logs";
|
|
13
13
|
import { message } from "./commands/message";
|
|
14
|
-
import { pair } from "./commands/pair";
|
|
15
14
|
import { ps } from "./commands/ps";
|
|
16
15
|
import { recover } from "./commands/recover";
|
|
17
16
|
import { restore } from "./commands/restore";
|
|
@@ -26,12 +25,7 @@ import { tunnel } from "./commands/tunnel";
|
|
|
26
25
|
import { upgrade } from "./commands/upgrade";
|
|
27
26
|
import { use } from "./commands/use";
|
|
28
27
|
import { wake } from "./commands/wake";
|
|
29
|
-
import {
|
|
30
|
-
getActiveAssistant,
|
|
31
|
-
findAssistantByName,
|
|
32
|
-
loadLatestAssistant,
|
|
33
|
-
setActiveAssistant,
|
|
34
|
-
} from "./lib/assistant-config";
|
|
28
|
+
import { resolveAssistant, setActiveAssistant } from "./lib/assistant-config";
|
|
35
29
|
import { loadGuardianToken } from "./lib/guardian-token";
|
|
36
30
|
import { checkHealth } from "./lib/health-check";
|
|
37
31
|
|
|
@@ -47,7 +41,6 @@ const commands = {
|
|
|
47
41
|
logout,
|
|
48
42
|
logs,
|
|
49
43
|
message,
|
|
50
|
-
pair,
|
|
51
44
|
ps,
|
|
52
45
|
recover,
|
|
53
46
|
restore,
|
|
@@ -82,7 +75,6 @@ function printHelp(): void {
|
|
|
82
75
|
console.log(" login Log in to the Vellum platform");
|
|
83
76
|
console.log(" logout Log out of the Vellum platform");
|
|
84
77
|
console.log(" message Send a message to a running assistant");
|
|
85
|
-
console.log(" pair Pair with a remote assistant via QR code");
|
|
86
78
|
console.log(
|
|
87
79
|
" ps List assistants (or processes for a specific assistant)",
|
|
88
80
|
);
|
|
@@ -129,10 +121,7 @@ function applyNoColorFlags(argv: string[]): void {
|
|
|
129
121
|
* Otherwise return false so the caller can fall back to help text.
|
|
130
122
|
*/
|
|
131
123
|
async function tryLaunchClient(): Promise<boolean> {
|
|
132
|
-
const
|
|
133
|
-
const entry = activeName
|
|
134
|
-
? findAssistantByName(activeName)
|
|
135
|
-
: loadLatestAssistant();
|
|
124
|
+
const entry = resolveAssistant();
|
|
136
125
|
|
|
137
126
|
if (!entry) return false;
|
|
138
127
|
|
|
@@ -29,50 +29,41 @@ function buildAssistantArgs(
|
|
|
29
29
|
return builders.assistant();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
expect(netAdminIdx).toBeGreaterThan(0);
|
|
43
|
-
expect(args[netAdminIdx - 1]).toBe("--cap-add");
|
|
32
|
+
function buildGatewayArgs(
|
|
33
|
+
overrides: Partial<Parameters<typeof serviceDockerRunArgs>[0]> = {},
|
|
34
|
+
): string[] {
|
|
35
|
+
const res = dockerResourceNames(instanceName);
|
|
36
|
+
const builders = serviceDockerRunArgs({
|
|
37
|
+
gatewayPort: 7830,
|
|
38
|
+
imageTags,
|
|
39
|
+
instanceName,
|
|
40
|
+
res,
|
|
41
|
+
...overrides,
|
|
44
42
|
});
|
|
43
|
+
return builders.gateway();
|
|
44
|
+
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
describe("serviceDockerRunArgs — assistant", () => {
|
|
47
|
+
test("does not grant elevated capabilities or disable security profiles", () => {
|
|
47
48
|
const args = buildAssistantArgs();
|
|
48
|
-
|
|
49
|
-
expect(
|
|
50
|
-
expect(args
|
|
51
|
-
|
|
52
|
-
expect(
|
|
53
|
-
expect(args
|
|
49
|
+
expect(args).not.toContain("--privileged");
|
|
50
|
+
expect(args).not.toContain("--cap-add");
|
|
51
|
+
expect(args).not.toContain("SYS_ADMIN");
|
|
52
|
+
expect(args).not.toContain("NET_ADMIN");
|
|
53
|
+
expect(args).not.toContain("seccomp=unconfined");
|
|
54
|
+
expect(args).not.toContain("apparmor=unconfined");
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
test("
|
|
57
|
+
test("does not mount a dockerd data volume", () => {
|
|
57
58
|
const args = buildAssistantArgs();
|
|
58
|
-
|
|
59
|
-
const mountIndex = args.indexOf(spec);
|
|
60
|
-
expect(mountIndex).toBeGreaterThan(0);
|
|
61
|
-
expect(args[mountIndex - 1]).toBe("-v");
|
|
59
|
+
expect(args.some((a) => a.includes("/var/lib/docker"))).toBe(false);
|
|
62
60
|
});
|
|
63
61
|
|
|
64
|
-
test("does NOT bind-mount the host Docker socket
|
|
62
|
+
test("does NOT bind-mount the host Docker socket", () => {
|
|
65
63
|
const args = buildAssistantArgs();
|
|
66
64
|
expect(args).not.toContain("/var/run/docker.sock:/var/run/docker.sock");
|
|
67
65
|
});
|
|
68
66
|
|
|
69
|
-
test("does NOT set VELLUM_WORKSPACE_VOLUME_NAME (legacy Phase 1.8 hint, no longer needed in DinD)", () => {
|
|
70
|
-
const args = buildAssistantArgs();
|
|
71
|
-
expect(
|
|
72
|
-
args.some((a) => a.startsWith("VELLUM_WORKSPACE_VOLUME_NAME=")),
|
|
73
|
-
).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
67
|
test("keeps existing workspace and socket volume mounts intact", () => {
|
|
77
68
|
const args = buildAssistantArgs();
|
|
78
69
|
expect(args).toContain(`${instanceName}-workspace:/workspace`);
|
|
@@ -112,6 +103,33 @@ describe("serviceDockerRunArgs — assistant", () => {
|
|
|
112
103
|
});
|
|
113
104
|
});
|
|
114
105
|
|
|
106
|
+
describe("serviceDockerRunArgs — gateway", () => {
|
|
107
|
+
const savedVelayBaseUrl = process.env.VELAY_BASE_URL;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
delete process.env.VELAY_BASE_URL;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
if (savedVelayBaseUrl === undefined) delete process.env.VELAY_BASE_URL;
|
|
115
|
+
else process.env.VELAY_BASE_URL = savedVelayBaseUrl;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("passes VELAY_BASE_URL into the gateway container when set", () => {
|
|
119
|
+
process.env.VELAY_BASE_URL = "http://host.docker.internal:8501";
|
|
120
|
+
|
|
121
|
+
expect(buildGatewayArgs()).toContain(
|
|
122
|
+
"VELAY_BASE_URL=http://host.docker.internal:8501",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("omits VELAY_BASE_URL from gateway args when unset", () => {
|
|
127
|
+
expect(
|
|
128
|
+
buildGatewayArgs().some((arg) => arg.startsWith("VELAY_BASE_URL=")),
|
|
129
|
+
).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
115
133
|
describe("VELLUM_AVATAR_DEVICE passthrough", () => {
|
|
116
134
|
const savedValue = process.env[AVATAR_DEVICE_ENV_VAR];
|
|
117
135
|
|