@vellumai/cli 0.11.2 → 0.11.3-staging.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/knip.json +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/ipc-framing.ts +295 -0
- package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +444 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +236 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/LICENSE +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/README.md +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/bin/nanoid.js +55 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.browser.js +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.d.ts +106 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.js +47 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/nanoid.js +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.d.ts +48 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.js +21 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/package.json +46 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/url-alphabet/index.js +2 -0
- package/node_modules/@vellumai/local-mode/package.json +1 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +63 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +503 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token.test.ts +203 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +46 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +6 -4
- package/node_modules/@vellumai/local-mode/src/__tests__/pair.test.ts +621 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +5 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +232 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +126 -20
- package/node_modules/@vellumai/local-mode/src/environment.ts +36 -15
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +313 -25
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +222 -13
- package/node_modules/@vellumai/local-mode/src/index.ts +35 -3
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +18 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +72 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +130 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +134 -45
- package/node_modules/@vellumai/local-mode/src/pair.ts +354 -0
- package/node_modules/@vellumai/local-mode/src/paths.ts +49 -0
- package/node_modules/@vellumai/local-mode/src/status.ts +10 -8
- package/node_modules/@vellumai/local-mode/src/unpair.ts +101 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +34 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/package.json +3 -1
- package/src/__tests__/client-token.test.ts +26 -1
- package/src/__tests__/guardian-token.test.ts +4 -0
- package/src/__tests__/nginx-ingress-command.test.ts +176 -16
- package/src/__tests__/nginx-ingress.test.ts +1821 -101
- package/src/__tests__/pair.test.ts +104 -2
- package/src/__tests__/sleep.test.ts +7 -3
- package/src/__tests__/tailscale-tunnel.test.ts +0 -1
- package/src/__tests__/tunnel.test.ts +1099 -55
- package/src/__tests__/wake.test.ts +166 -99
- package/src/commands/client.ts +182 -27
- package/src/commands/connect/import.ts +35 -156
- package/src/commands/nginx-ingress.ts +87 -110
- package/src/commands/pair.ts +84 -21
- package/src/commands/tunnel.ts +196 -85
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/wake.ts +5 -163
- package/src/index.ts +1 -1
- package/src/lib/__tests__/local-ces.test.ts +10 -1
- package/src/lib/cloudflare-tunnel.ts +3 -16
- package/src/lib/environments/__tests__/paths.test.ts +20 -1
- package/src/lib/environments/paths.ts +29 -41
- package/src/lib/environments/resolve.ts +7 -5
- package/src/lib/guardian-token.ts +22 -62
- package/src/lib/ingress-config.ts +25 -0
- package/src/lib/local.ts +77 -20
- package/src/lib/nginx-ingress.ts +523 -76
- package/src/lib/ngrok.ts +154 -61
- package/src/lib/orphan-detection.test.ts +167 -0
- package/src/lib/orphan-detection.ts +40 -0
- package/src/lib/pair.test.ts +87 -0
- package/src/lib/pair.ts +55 -0
- package/src/lib/platform-client.ts +8 -5
- package/src/lib/tailscale-tunnel.ts +7 -18
- package/src/lib/tunnel-edge.ts +114 -0
- package/src/lib/xdg-log.ts +2 -2
package/src/lib/pair.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `vellum pair`'s remote-web approval flow.
|
|
3
|
+
*
|
|
4
|
+
* Pairing challenge stores are in-memory per gateway process, so approving a
|
|
5
|
+
* code against a different assistant or environment than the one that served
|
|
6
|
+
* the pair page looks identical to a typo'd or expired code. These helpers
|
|
7
|
+
* turn the gateway's error envelope into a diagnostic that names the gateway
|
|
8
|
+
* actually asked.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { REMOTE_WEB_PAIRING_CODE_TTL_MS } from "@vellumai/service-contracts/remote-web-pairing";
|
|
12
|
+
|
|
13
|
+
const CODE_TTL_MINUTES = Math.round(REMOTE_WEB_PAIRING_CODE_TTL_MS / 60_000);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract the `error.code` from a gateway JSON error envelope
|
|
17
|
+
* (`{"error":{"code":...,"message":...}}`). Returns null when the body is not
|
|
18
|
+
* JSON or carries no string code.
|
|
19
|
+
*/
|
|
20
|
+
export function parseGatewayErrorCode(body: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(body) as { error?: { code?: unknown } };
|
|
23
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Diagnostic for a failed `--web-approve`, naming which gateway rejected the
|
|
31
|
+
* code and hinting at cross-assistant / cross-environment mismatches. Returns
|
|
32
|
+
* null for error codes this helper doesn't recognize, so the caller falls
|
|
33
|
+
* back to the generic HTTP error text. `assistantReference` should carry the
|
|
34
|
+
* stable assistant ID (see `formatAssistantReference`); `envName` should be
|
|
35
|
+
* the effective environment actually used (see `getCurrentEnvironment`).
|
|
36
|
+
*/
|
|
37
|
+
export function formatWebApproveFailure(
|
|
38
|
+
gatewayUrl: string,
|
|
39
|
+
assistantReference: string,
|
|
40
|
+
envName: string,
|
|
41
|
+
errorCode: string | null,
|
|
42
|
+
): string | null {
|
|
43
|
+
if (errorCode !== "INVALID_USER_CODE" && errorCode !== "EXPIRED_USER_CODE") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const rejection =
|
|
47
|
+
errorCode === "EXPIRED_USER_CODE"
|
|
48
|
+
? "Pairing code expired on"
|
|
49
|
+
: "No such pairing code on";
|
|
50
|
+
return [
|
|
51
|
+
`${rejection} ${gatewayUrl} (assistant "${assistantReference}", environment "${envName}").`,
|
|
52
|
+
`Codes are minted by the gateway that serves the pair page, expire after ${CODE_TTL_MINUTES} minutes, and are single-use.`,
|
|
53
|
+
"If the pair page came from a different assistant or environment (e.g. the desktop app's), re-run with that environment's VELLUM_ENVIRONMENT and that assistant's name.",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { join, dirname } from "path";
|
|
10
10
|
|
|
11
11
|
import { getLockfilePlatformBaseUrl } from "./assistant-config.js";
|
|
12
|
-
import { getConfigDir } from "./environments/paths.js";
|
|
12
|
+
import { getConfigDir, getConfigDirs } from "./environments/paths.js";
|
|
13
13
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
14
14
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
15
15
|
|
|
@@ -55,11 +55,14 @@ export function getWebUrl(): string {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function readPlatformToken(): string | null {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
for (const dir of getConfigDirs(getCurrentEnvironment())) {
|
|
59
|
+
try {
|
|
60
|
+
return readFileSync(join(dir, "platform-token"), "utf-8").trim();
|
|
61
|
+
} catch {
|
|
62
|
+
// Try the next compatible location.
|
|
63
|
+
}
|
|
62
64
|
}
|
|
65
|
+
return null;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export function savePlatformToken(token: string): void {
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
getDefaultWorkspaceDir,
|
|
7
7
|
saveIngressUrl,
|
|
8
8
|
} from "./ingress-config.js";
|
|
9
|
-
import { resolveTunnelTargetPort } from "./nginx-ingress.js";
|
|
10
9
|
|
|
11
10
|
// ── Tailscale CLI discovery + invocation ────────────────────────────────────
|
|
12
11
|
|
|
@@ -161,12 +160,10 @@ export function shouldClearIngressUrl(
|
|
|
161
160
|
// ── Tailscale serve lifecycle ───────────────────────────────────────────────
|
|
162
161
|
|
|
163
162
|
export interface RunTailscaleTunnelOptions {
|
|
164
|
-
/**
|
|
163
|
+
/** Local edge port to serve. Defaults to the global GATEWAY_PORT. */
|
|
165
164
|
port?: number;
|
|
166
165
|
/** Workspace directory for config read/write. Defaults to ~/.vellum/workspace. */
|
|
167
166
|
workspaceDir?: string;
|
|
168
|
-
/** Prefer nginx ingress over the gateway port when it is running. */
|
|
169
|
-
preferNginxIngress?: boolean;
|
|
170
167
|
/** Lockfile entry to mirror the ingress URL onto (`ingressUrl`). */
|
|
171
168
|
assistantId?: string;
|
|
172
169
|
}
|
|
@@ -174,7 +171,6 @@ export interface RunTailscaleTunnelOptions {
|
|
|
174
171
|
export interface TailscaleServeInfo {
|
|
175
172
|
publicUrl: string;
|
|
176
173
|
port: number;
|
|
177
|
-
viaIngress: boolean;
|
|
178
174
|
binary: string;
|
|
179
175
|
workspaceDir: string;
|
|
180
176
|
}
|
|
@@ -210,12 +206,7 @@ export async function startTailscaleServe(
|
|
|
210
206
|
const publicUrl = `https://${hostname}`;
|
|
211
207
|
|
|
212
208
|
const workspaceDir = opts.workspaceDir ?? getDefaultWorkspaceDir();
|
|
213
|
-
const
|
|
214
|
-
const { port, viaIngress } = resolveTunnelTargetPort(
|
|
215
|
-
workspaceDir,
|
|
216
|
-
gatewayPort,
|
|
217
|
-
{ preferNginxIngress: opts.preferNginxIngress === true },
|
|
218
|
-
);
|
|
209
|
+
const port = opts.port ?? GATEWAY_PORT;
|
|
219
210
|
|
|
220
211
|
const serveResult = deps.run(binary, ["serve", "--bg", String(port)]);
|
|
221
212
|
if (serveResult.status !== 0) {
|
|
@@ -224,7 +215,7 @@ export async function startTailscaleServe(
|
|
|
224
215
|
|
|
225
216
|
saveIngressUrl(workspaceDir, publicUrl, opts.assistantId);
|
|
226
217
|
|
|
227
|
-
return { publicUrl, port,
|
|
218
|
+
return { publicUrl, port, binary, workspaceDir };
|
|
228
219
|
}
|
|
229
220
|
|
|
230
221
|
/**
|
|
@@ -252,12 +243,10 @@ export async function runTailscaleTunnel(
|
|
|
252
243
|
const deps = realTailscaleDeps();
|
|
253
244
|
|
|
254
245
|
console.log("Setting up tailscale serve...");
|
|
255
|
-
const { publicUrl, port,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.log(`nginx ingress detected — serving it on 127.0.0.1:${port}.`);
|
|
260
|
-
}
|
|
246
|
+
const { publicUrl, port, binary, workspaceDir } = await startTailscaleServe(
|
|
247
|
+
opts,
|
|
248
|
+
deps,
|
|
249
|
+
);
|
|
261
250
|
|
|
262
251
|
console.log("");
|
|
263
252
|
console.log(`Tunnel established: ${publicUrl}`);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ChildProcess } from "child_process";
|
|
2
|
+
|
|
3
|
+
import { loadRawConfig } from "./ingress-config.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureTunnelEdge,
|
|
6
|
+
formatEdgeMode,
|
|
7
|
+
isIngressRunning,
|
|
8
|
+
readIngressState,
|
|
9
|
+
type TunnelEdge,
|
|
10
|
+
} from "./nginx-ingress.js";
|
|
11
|
+
import { hasWebhookIntegrations, maybeStartNgrokTunnel } from "./ngrok.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retry policy for the flag lookup inside the tunnel-edge restore. The gateway
|
|
15
|
+
* has typically been up for milliseconds at this point and answers
|
|
16
|
+
* `503 {"status":"starting"}` (or refuses connections) until its startup
|
|
17
|
+
* completes, so a single lookup races it.
|
|
18
|
+
*/
|
|
19
|
+
export const WEB_INGRESS_FLAG_RETRY = {
|
|
20
|
+
attempts: 15,
|
|
21
|
+
intervalMs: 2_000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether the workspace ingress config wants the remote-web edge: explicitly
|
|
26
|
+
* enabled with a saved public URL.
|
|
27
|
+
*/
|
|
28
|
+
function wantsWebIngress(config: Record<string, unknown>): boolean {
|
|
29
|
+
const ingress = config.ingress as
|
|
30
|
+
{ enabled?: unknown; publicBaseUrl?: unknown } | undefined;
|
|
31
|
+
return (
|
|
32
|
+
ingress?.enabled === true &&
|
|
33
|
+
typeof ingress.publicBaseUrl === "string" &&
|
|
34
|
+
ingress.publicBaseUrl.trim() !== ""
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether anything wants the nginx edge: webhook integrations or the
|
|
40
|
+
* remote-web ingress config. One config read covers both checks;
|
|
41
|
+
* `maybeStartNgrokTunnel` keeps its own webhook gate for its other callers.
|
|
42
|
+
*/
|
|
43
|
+
function wantsTunnelEdge(workspaceDir: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const config = loadRawConfig(workspaceDir);
|
|
46
|
+
return hasWebhookIntegrations(config) || wantsWebIngress(config);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bring the nginx edge back up after a wake or local upgrade and point the
|
|
54
|
+
* webhook auto-tunnel at it. The edge is wanted when webhook integrations are
|
|
55
|
+
* configured or the workspace ingress config is enabled with a saved public
|
|
56
|
+
* URL; `ensureTunnelEdge` picks SPA vs webhooks-only mode off the
|
|
57
|
+
* `web-remote-ingress` flag, retrying the lookup through the gateway's startup
|
|
58
|
+
* window. A healthy edge whose recorded state already targets the requested
|
|
59
|
+
* gateway port is reused without the flag lookup or the `remoteWebConfigHash`
|
|
60
|
+
* comparison `startRemoteWebIngress` performs; both flag-driven mode drift
|
|
61
|
+
* and injected-config drift (a renamed assistant, a changed hub URL) are
|
|
62
|
+
* repaired by the next explicit `vellum tunnel` or `vellum nginx-ingress up`,
|
|
63
|
+
* not by background wakes. Edge failures warn
|
|
64
|
+
* (with the error's install or diagnostic text) and fall back to tunneling the
|
|
65
|
+
* gateway port directly, which `maybeStartNgrokTunnel` only does when webhook
|
|
66
|
+
* integrations are configured, so webhook channels on nginx-less machines
|
|
67
|
+
* keep working, and the caller never fails because of edge problems.
|
|
68
|
+
*
|
|
69
|
+
* Returns the spawned ngrok child (for PID tracking) or null.
|
|
70
|
+
*/
|
|
71
|
+
export async function restoreTunnelEdgeAndAutoTunnel(
|
|
72
|
+
assistantId: string,
|
|
73
|
+
gatewayPort: number,
|
|
74
|
+
workspaceDir: string,
|
|
75
|
+
): Promise<ChildProcess | null> {
|
|
76
|
+
let tunnelTargetPort = gatewayPort;
|
|
77
|
+
if (wantsTunnelEdge(workspaceDir)) {
|
|
78
|
+
const recorded = isIngressRunning(workspaceDir)
|
|
79
|
+
? readIngressState(workspaceDir)
|
|
80
|
+
: null;
|
|
81
|
+
let edge: TunnelEdge | null = null;
|
|
82
|
+
if (recorded !== null && recorded.gatewayPort === gatewayPort) {
|
|
83
|
+
edge = {
|
|
84
|
+
port: recorded.listenPort,
|
|
85
|
+
started: false,
|
|
86
|
+
includesWebApp: recorded.includeWebApp,
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
try {
|
|
90
|
+
edge = await ensureTunnelEdge({
|
|
91
|
+
assistantId,
|
|
92
|
+
workspaceDir,
|
|
93
|
+
gatewayPort,
|
|
94
|
+
flagRetry: WEB_INGRESS_FLAG_RETRY,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.warn(
|
|
98
|
+
` Could not restore the tunnel edge: ${
|
|
99
|
+
err instanceof Error ? err.message : String(err)
|
|
100
|
+
} Bring it up manually with \`vellum nginx-ingress up\`.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (edge) {
|
|
105
|
+
tunnelTargetPort = edge.port;
|
|
106
|
+
console.log(
|
|
107
|
+
` Tunnel edge ${edge.started ? "started" : "already running"} on 127.0.0.1:${edge.port} (${formatEdgeMode(
|
|
108
|
+
edge.includesWebApp,
|
|
109
|
+
)}).`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return maybeStartNgrokTunnel(tunnelTargetPort, workspaceDir);
|
|
114
|
+
}
|
package/src/lib/xdg-log.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { getLogDir as getEnvironmentLogDir } from "./environments/paths.js";
|
|
15
15
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
16
16
|
|
|
17
17
|
/** Regex matching pino-pretty's short time prefix, e.g. `[12:07:37.467] `. */
|
|
@@ -24,7 +24,7 @@ const PINO_TIME_RE = /^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\s*/;
|
|
|
24
24
|
* non-production environments use `$XDG_CONFIG_HOME/vellum-<env>/logs`.
|
|
25
25
|
*/
|
|
26
26
|
export function getLogDir(): string {
|
|
27
|
-
return
|
|
27
|
+
return getEnvironmentLogDir(getCurrentEnvironment());
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Open (or create) a log file in append mode, returning the file descriptor.
|