@vellumai/cli 0.8.9-staging.1 → 0.8.9-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/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +88 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +3 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +15 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +33 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-client-refresh.test.ts +65 -4
- package/src/__tests__/client-tui-refresh.test.ts +50 -6
- package/src/__tests__/guardian-token.test.ts +130 -4
- package/src/__tests__/message.test.ts +86 -0
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +68 -9
- package/src/commands/client.ts +100 -58
- package/src/commands/hatch.ts +14 -4
- package/src/commands/message.ts +109 -19
- package/src/commands/teleport.ts +2 -0
- package/src/components/DefaultMainScreen.tsx +27 -2
- package/src/lib/__tests__/docker.test.ts +99 -0
- package/src/lib/assistant-client.ts +31 -13
- package/src/lib/docker.ts +97 -29
- package/src/lib/flag-args.test.ts +89 -0
- package/src/lib/flag-args.ts +74 -0
- package/src/lib/guardian-token.ts +54 -0
- package/src/lib/hatch-local.ts +2 -0
- package/src/lib/local.ts +6 -1
- package/src/lib/runtime-url.ts +90 -0
- package/src/lib/statefulset.ts +9 -0
package/src/lib/local.ts
CHANGED
|
@@ -1057,7 +1057,11 @@ export async function startLocalDaemon(
|
|
|
1057
1057
|
export async function startGateway(
|
|
1058
1058
|
watch: boolean = false,
|
|
1059
1059
|
resources?: LocalInstanceResources,
|
|
1060
|
-
options?: {
|
|
1060
|
+
options?: {
|
|
1061
|
+
signingKey?: string;
|
|
1062
|
+
bootstrapSecret?: string;
|
|
1063
|
+
envOverrides?: Record<string, string>;
|
|
1064
|
+
},
|
|
1061
1065
|
): Promise<string> {
|
|
1062
1066
|
const effectiveGatewayPort = resources?.gatewayPort ?? GATEWAY_PORT;
|
|
1063
1067
|
|
|
@@ -1083,6 +1087,7 @@ export async function startGateway(
|
|
|
1083
1087
|
|
|
1084
1088
|
const gatewayEnv: Record<string, string> = {
|
|
1085
1089
|
...(process.env as Record<string, string>),
|
|
1090
|
+
...options?.envOverrides,
|
|
1086
1091
|
RUNTIME_HTTP_PORT: String(effectiveDaemonPort),
|
|
1087
1092
|
GATEWAY_PORT: String(effectiveGatewayPort),
|
|
1088
1093
|
// Pass gateway operational settings via env vars so the CLI does not
|
package/src/lib/runtime-url.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
|
|
3
|
+
import { getLocalLanIPv4 } from "./local";
|
|
1
4
|
import type { AssistantEntry } from "./assistant-config.js";
|
|
2
5
|
|
|
3
6
|
/**
|
|
@@ -50,3 +53,90 @@ export function resolveRuntimeUrl(
|
|
|
50
53
|
}
|
|
51
54
|
return `${entry.runtimeUrl}/v1/${subpath}`;
|
|
52
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* If the hostname in `url` matches this machine's local DNS name, LAN IP, or
|
|
59
|
+
* raw hostname, replace it with 127.0.0.1 so the client avoids mDNS round-trips
|
|
60
|
+
* when talking to an assistant running on the same machine. Trailing slashes are
|
|
61
|
+
* stripped on a swap. Returns the input unchanged if it doesn't parse as a URL.
|
|
62
|
+
*/
|
|
63
|
+
function maybeSwapToLocalhost(url: string): string {
|
|
64
|
+
let parsed: URL;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(url);
|
|
67
|
+
} catch {
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const urlHost = parsed.hostname.toLowerCase();
|
|
72
|
+
|
|
73
|
+
const localNames: string[] = [];
|
|
74
|
+
|
|
75
|
+
const host = hostname();
|
|
76
|
+
if (host) {
|
|
77
|
+
localNames.push(host.toLowerCase());
|
|
78
|
+
// Also consider the bare name without .local suffix
|
|
79
|
+
if (host.toLowerCase().endsWith(".local")) {
|
|
80
|
+
localNames.push(host.toLowerCase().slice(0, -".local".length));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lanIp = getLocalLanIPv4();
|
|
85
|
+
if (lanIp) {
|
|
86
|
+
localNames.push(lanIp);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (localNames.includes(urlHost)) {
|
|
90
|
+
parsed.hostname = "127.0.0.1";
|
|
91
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return url;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Canonical form of a runtime/base URL used throughout the CLI: trailing
|
|
99
|
+
* slashes stripped, then localhost-swapped. This is exactly the transform
|
|
100
|
+
* `vellum client` applies to the runtime URL it hands the TUI, so comparing two
|
|
101
|
+
* URLs after passing both through this function is a like-for-like comparison.
|
|
102
|
+
*/
|
|
103
|
+
export function normalizeRuntimeUrl(url: string): string {
|
|
104
|
+
return maybeSwapToLocalhost(url.replace(/\/+$/, ""));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* SECURITY: decide whether a guardian-token refresh may be sent to
|
|
109
|
+
* `candidateUrl`, and to which URL it should actually go.
|
|
110
|
+
*
|
|
111
|
+
* `vellum client` lets `--url`/`-u` override the runtime URL while still reusing
|
|
112
|
+
* the selected entry's stored guardian token, so a victim pointed at an
|
|
113
|
+
* attacker-controlled (or poisoned/redirected) URL must NOT cause us to POST the
|
|
114
|
+
* long-lived refreshToken + deviceId there. Refresh is permitted only when
|
|
115
|
+
* `candidateUrl` normalizes to one of the entry's persisted URLs (`localUrl`,
|
|
116
|
+
* which the CLI prefers when present, or `runtimeUrl`).
|
|
117
|
+
*
|
|
118
|
+
* Returns the persisted URL that the candidate matched — never the
|
|
119
|
+
* caller-supplied `candidateUrl` verbatim — so credentials only ever reach a
|
|
120
|
+
* trusted origin even if a caller forgets to use this return value. The matched
|
|
121
|
+
* URL is preferred over always returning `runtimeUrl` so the refresh stays on
|
|
122
|
+
* the same interface the session is using: e.g. a local entry may persist both a
|
|
123
|
+
* loopback `localUrl` (which `vellum client` defaults to) and an externally
|
|
124
|
+
* discovered `runtimeUrl`, and refreshing the loopback session against the
|
|
125
|
+
* external address could be unreachable or needlessly cross the public
|
|
126
|
+
* interface. Returns `null` when the candidate is untrusted (caller must skip
|
|
127
|
+
* the refresh).
|
|
128
|
+
*/
|
|
129
|
+
export function trustedRefreshUrl(
|
|
130
|
+
entry: Pick<AssistantEntry, "runtimeUrl" | "localUrl">,
|
|
131
|
+
candidateUrl: string,
|
|
132
|
+
): string | null {
|
|
133
|
+
const candidate = normalizeRuntimeUrl(candidateUrl);
|
|
134
|
+
// localUrl first: it's what the CLI prefers when present, so the candidate is
|
|
135
|
+
// most likely to match it, and we want to keep the refresh on that interface.
|
|
136
|
+
for (const persisted of [entry.localUrl, entry.runtimeUrl]) {
|
|
137
|
+
if (persisted && normalizeRuntimeUrl(persisted) === candidate) {
|
|
138
|
+
return persisted;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
package/src/lib/statefulset.ts
CHANGED
|
@@ -257,6 +257,7 @@ export interface BuildServiceRunArgsOpts extends DockerRunSecrets {
|
|
|
257
257
|
instanceName: string;
|
|
258
258
|
res: DockerResourceNames;
|
|
259
259
|
extraAssistantEnv?: Record<string, string>;
|
|
260
|
+
extraGatewayEnv?: Record<string, string>;
|
|
260
261
|
/** Avatar device path, if available. Injected by `docker.ts` after resolving. */
|
|
261
262
|
avatarDevicePath?: string;
|
|
262
263
|
}
|
|
@@ -285,6 +286,7 @@ export function buildServiceRunArgs(
|
|
|
285
286
|
instanceName,
|
|
286
287
|
res,
|
|
287
288
|
extraAssistantEnv,
|
|
289
|
+
extraGatewayEnv,
|
|
288
290
|
avatarDevicePath,
|
|
289
291
|
} = opts;
|
|
290
292
|
|
|
@@ -346,6 +348,13 @@ export function buildServiceRunArgs(
|
|
|
346
348
|
}
|
|
347
349
|
}
|
|
348
350
|
|
|
351
|
+
// Gateway-only additions (e.g. feature flag env overrides)
|
|
352
|
+
if (svc === "gateway" && extraGatewayEnv) {
|
|
353
|
+
for (const [k, v] of Object.entries(extraGatewayEnv)) {
|
|
354
|
+
args.push("-e", `${k}=${v}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
349
358
|
// Assistant-only computed / optional additions
|
|
350
359
|
if (svc === "assistant") {
|
|
351
360
|
args.push(
|