@vellumai/cli 0.6.6 → 0.7.1
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 +8 -2
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
getActiveAssistant,
|
|
18
|
-
loadLatestAssistant,
|
|
16
|
+
resolveAssistant,
|
|
19
17
|
} from "./assistant-config.js";
|
|
20
18
|
import { GATEWAY_PORT } from "./constants.js";
|
|
21
19
|
import { loadGuardianToken } from "./guardian-token.js";
|
|
@@ -58,27 +56,13 @@ export class AssistantClient {
|
|
|
58
56
|
* @throws If no matching assistant is found.
|
|
59
57
|
*/
|
|
60
58
|
constructor(opts?: AssistantClientOpts) {
|
|
61
|
-
const
|
|
62
|
-
let entry = nameOrId ? findAssistantByName(nameOrId) : null;
|
|
63
|
-
|
|
64
|
-
if (nameOrId && !entry) {
|
|
65
|
-
throw new Error(`No assistant found with name '${nameOrId}'.`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!entry) {
|
|
69
|
-
const active = getActiveAssistant();
|
|
70
|
-
if (active) {
|
|
71
|
-
entry = findAssistantByName(active);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!entry) {
|
|
76
|
-
entry = loadLatestAssistant();
|
|
77
|
-
}
|
|
59
|
+
const entry = resolveAssistant(opts?.assistantId);
|
|
78
60
|
|
|
79
61
|
if (!entry) {
|
|
80
62
|
throw new Error(
|
|
81
|
-
|
|
63
|
+
opts?.assistantId
|
|
64
|
+
? `No assistant found with name '${opts.assistantId}'.`
|
|
65
|
+
: "No assistant found. Hatch one first with 'vellum hatch'.",
|
|
82
66
|
);
|
|
83
67
|
}
|
|
84
68
|
|
|
@@ -42,8 +42,6 @@ export interface LocalInstanceResources {
|
|
|
42
42
|
qdrantPort: number;
|
|
43
43
|
/** HTTP port for the CES (Claude Extension Server) */
|
|
44
44
|
cesPort: number;
|
|
45
|
-
/** Absolute path to the daemon PID file */
|
|
46
|
-
pidFile: string;
|
|
47
45
|
/** Persisted HMAC signing key (hex). Survives daemon/gateway restarts so
|
|
48
46
|
* client actor tokens remain valid across `wake` cycles. */
|
|
49
47
|
signingKey?: string;
|
|
@@ -114,6 +112,18 @@ export function getBaseDir(): string {
|
|
|
114
112
|
return process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
115
113
|
}
|
|
116
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Derive the daemon PID file path from a resources object. The PID file
|
|
117
|
+
* lives inside the instance's workspace directory. When no resources are
|
|
118
|
+
* available, falls back to `~/.vellum/workspace/vellum.pid`.
|
|
119
|
+
*/
|
|
120
|
+
export function getDaemonPidPath(resources?: LocalInstanceResources): string {
|
|
121
|
+
const vellumDir = resources
|
|
122
|
+
? join(resources.instanceDir, ".vellum")
|
|
123
|
+
: join(homedir(), ".vellum");
|
|
124
|
+
return join(vellumDir, "workspace", "vellum.pid");
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
function readLockfile(): LockfileData {
|
|
118
128
|
for (const lockfilePath of getLockfilePaths(getCurrentEnvironment())) {
|
|
119
129
|
if (!existsSync(lockfilePath)) continue;
|
|
@@ -215,7 +225,6 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
215
225
|
gatewayPort,
|
|
216
226
|
qdrantPort: defaultPorts.qdrant,
|
|
217
227
|
cesPort: defaultPorts.ces,
|
|
218
|
-
pidFile: join(instanceDir, ".vellum", "vellum.pid"),
|
|
219
228
|
};
|
|
220
229
|
mutated = true;
|
|
221
230
|
} else {
|
|
@@ -247,10 +256,6 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
247
256
|
res.cesPort = defaultPorts.ces;
|
|
248
257
|
mutated = true;
|
|
249
258
|
}
|
|
250
|
-
if (typeof res.pidFile !== "string") {
|
|
251
|
-
res.pidFile = join(res.instanceDir as string, ".vellum", "vellum.pid");
|
|
252
|
-
mutated = true;
|
|
253
|
-
}
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
return mutated;
|
|
@@ -338,19 +343,17 @@ export function setActiveAssistant(assistantId: string): void {
|
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
/**
|
|
341
|
-
*
|
|
346
|
+
* Best-effort resolution of the target assistant. Returns null when no
|
|
347
|
+
* match is found — callers decide how to handle the absence.
|
|
348
|
+
*
|
|
349
|
+
* Priority:
|
|
342
350
|
* 1. Explicit name argument
|
|
343
351
|
* 2. Active assistant set via `vellum use`
|
|
344
|
-
* 3. Sole
|
|
352
|
+
* 3. Sole lockfile entry (any cloud)
|
|
345
353
|
*/
|
|
346
|
-
export function
|
|
354
|
+
export function resolveAssistant(nameArg?: string): AssistantEntry | null {
|
|
347
355
|
if (nameArg) {
|
|
348
|
-
|
|
349
|
-
if (!entry) {
|
|
350
|
-
console.error(`No assistant found with name '${nameArg}'.`);
|
|
351
|
-
process.exit(1);
|
|
352
|
-
}
|
|
353
|
-
return entry;
|
|
356
|
+
return findAssistantByName(nameArg);
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
const active = getActiveAssistant();
|
|
@@ -361,15 +364,35 @@ export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
|
|
|
361
364
|
}
|
|
362
365
|
|
|
363
366
|
const all = readAssistants();
|
|
364
|
-
|
|
365
|
-
if (locals.length === 1) return locals[0];
|
|
367
|
+
if (all.length === 1) return all[0];
|
|
366
368
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve which assistant to target for a command, exiting the process
|
|
374
|
+
* with a user-facing error when resolution fails.
|
|
375
|
+
*
|
|
376
|
+
* Priority:
|
|
377
|
+
* 1. Explicit name argument
|
|
378
|
+
* 2. Active assistant set via `vellum use`
|
|
379
|
+
* 3. Sole lockfile entry (any cloud)
|
|
380
|
+
*/
|
|
381
|
+
export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
|
|
382
|
+
const entry = resolveAssistant(nameArg);
|
|
383
|
+
if (entry) return entry;
|
|
384
|
+
|
|
385
|
+
if (nameArg) {
|
|
386
|
+
console.error(`No assistant found with name '${nameArg}'.`);
|
|
369
387
|
} else {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
388
|
+
const all = readAssistants();
|
|
389
|
+
if (all.length === 0) {
|
|
390
|
+
console.error("No assistant found. Run 'vellum hatch' first.");
|
|
391
|
+
} else {
|
|
392
|
+
console.error(
|
|
393
|
+
`Multiple assistants found. Set an active assistant with 'vellum use <name>'.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
373
396
|
}
|
|
374
397
|
process.exit(1);
|
|
375
398
|
}
|
|
@@ -474,7 +497,6 @@ export async function allocateLocalResources(
|
|
|
474
497
|
gatewayPort,
|
|
475
498
|
qdrantPort,
|
|
476
499
|
cesPort,
|
|
477
|
-
pidFile: join(instanceDir, ".vellum", "vellum.pid"),
|
|
478
500
|
};
|
|
479
501
|
}
|
|
480
502
|
|
package/src/lib/cli-error.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable per-install client identity for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Generates a UUID on first use and persists it to
|
|
5
|
+
* `~/.config/vellum/client-id` so the daemon's event hub can
|
|
6
|
+
* track this terminal across SSE reconnects and CLI restarts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
|
|
14
|
+
const CLI_INTERFACE_ID = "cli";
|
|
15
|
+
|
|
16
|
+
let cached: string | null = null;
|
|
17
|
+
|
|
18
|
+
function getConfigDir(): string {
|
|
19
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
20
|
+
return join(configHome, "vellum");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns a stable UUID identifying this CLI installation.
|
|
25
|
+
* Generated once and persisted to `~/.config/vellum/client-id`.
|
|
26
|
+
*/
|
|
27
|
+
export function getClientId(): string {
|
|
28
|
+
if (cached) return cached;
|
|
29
|
+
|
|
30
|
+
const configDir = getConfigDir();
|
|
31
|
+
const idFile = join(configDir, "client-id");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (existsSync(idFile)) {
|
|
35
|
+
const stored = readFileSync(idFile, "utf-8").trim();
|
|
36
|
+
if (stored) {
|
|
37
|
+
cached = stored;
|
|
38
|
+
return stored;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
/* best-effort read */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const id = randomUUID();
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(configDir, { recursive: true });
|
|
48
|
+
writeFileSync(idFile, id, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
/* best-effort persist — transient id still works for this session */
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cached = id;
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Headers that identify this CLI client to the assistant daemon.
|
|
59
|
+
* Attach to SSE streaming connections so the ClientRegistry can
|
|
60
|
+
* track connected clients and their capabilities.
|
|
61
|
+
*/
|
|
62
|
+
export function getClientRegistrationHeaders(): Record<string, string> {
|
|
63
|
+
return {
|
|
64
|
+
"X-Vellum-Client-Id": getClientId(),
|
|
65
|
+
"X-Vellum-Interface-Id": CLI_INTERFACE_ID,
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/lib/docker.ts
CHANGED
|
@@ -6,13 +6,6 @@ import { dirname, join } from "path";
|
|
|
6
6
|
// Direct import — bun embeds this at compile time so it works in compiled binaries.
|
|
7
7
|
import cliPkg from "../../package.json";
|
|
8
8
|
|
|
9
|
-
// Pulled from skills/ — the Meet avatar device-path default is owned by the
|
|
10
|
-
// meet-join skill; importing here keeps the CLI's Docker wiring locked to the
|
|
11
|
-
// same value the bot and config schema use. The shared module is deliberately
|
|
12
|
-
// zero-dep so this import cannot drag unrelated surface into the compiled CLI
|
|
13
|
-
// binary.
|
|
14
|
-
import { AVATAR_DEVICE_PATH_DEFAULT } from "../../../skills/meet-join/shared/avatar-device-path.js";
|
|
15
|
-
|
|
16
9
|
import {
|
|
17
10
|
findAssistantByName,
|
|
18
11
|
saveAssistantEntry,
|
|
@@ -53,62 +46,24 @@ export const GATEWAY_INTERNAL_PORT = 7830;
|
|
|
53
46
|
/** Max time to wait for the assistant container to emit the readiness sentinel. */
|
|
54
47
|
export const DOCKER_READY_TIMEOUT_MS = 3 * 60 * 1000;
|
|
55
48
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
* enabled. Re-exports the shared
|
|
59
|
-
* {@link ../../../skills/meet-join/shared/avatar-device-path.js AVATAR_DEVICE_PATH_DEFAULT}
|
|
60
|
-
* so the CLI's device-passthrough wiring cannot drift from the bot's
|
|
61
|
-
* Chrome-flag wiring or the workspace config default. Matches the
|
|
62
|
-
* `video_nr=10` value in the README's host-setup section
|
|
63
|
-
* (`skills/meet-join/bot/README.md`). Operators can override the path by
|
|
64
|
-
* setting `VELLUM_MEET_AVATAR_DEVICE` to something other than the default
|
|
65
|
-
* (e.g. `/dev/video11` if a different `video_nr` was used).
|
|
66
|
-
*/
|
|
67
|
-
export const DEFAULT_MEET_AVATAR_DEVICE_PATH = AVATAR_DEVICE_PATH_DEFAULT;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Env-var opt-in for bind-mounting the v4l2loopback virtual-camera device
|
|
71
|
-
* into the assistant container. Set to a truthy value (`1`, `true`, `yes`)
|
|
72
|
-
* to enable; unset or falsy disables the passthrough entirely.
|
|
73
|
-
*
|
|
74
|
-
* Kept as an env-var rather than a config-schema field because the Meet
|
|
75
|
-
* avatar config schema lands in a later PR (PR 5 of the phase-4 plan) —
|
|
76
|
-
* threading the config through the CLI's boot flow now would force a
|
|
77
|
-
* forward dependency. Once the schema lands, the CLI can either keep this
|
|
78
|
-
* env-var as a pre-config override or move the opt-in into the workspace
|
|
79
|
-
* config.
|
|
80
|
-
*/
|
|
81
|
-
export const MEET_AVATAR_ENV_VAR = "VELLUM_MEET_AVATAR";
|
|
49
|
+
/** Default virtual-camera device path. Overridable via `VELLUM_AVATAR_DEVICE`. */
|
|
50
|
+
const DEFAULT_AVATAR_DEVICE_PATH = "/dev/video10";
|
|
82
51
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
* {@link DEFAULT_MEET_AVATAR_DEVICE_PATH}.
|
|
86
|
-
*/
|
|
87
|
-
export const MEET_AVATAR_DEVICE_ENV_VAR = "VELLUM_MEET_AVATAR_DEVICE";
|
|
52
|
+
/** Env var the assistant reads to discover its virtual-camera device path. */
|
|
53
|
+
export const AVATAR_DEVICE_ENV_VAR = "VELLUM_AVATAR_DEVICE";
|
|
88
54
|
|
|
89
55
|
/**
|
|
90
|
-
* Resolve the
|
|
91
|
-
*
|
|
92
|
-
*
|
|
56
|
+
* Resolve the avatar device path from the environment. Always returns a
|
|
57
|
+
* value — the CLI unconditionally passes the device path to the assistant
|
|
58
|
+
* container; the skill decides whether to use it.
|
|
93
59
|
*/
|
|
94
|
-
export function
|
|
60
|
+
export function resolveAvatarDevicePath(
|
|
95
61
|
env: NodeJS.ProcessEnv = process.env,
|
|
96
|
-
): string
|
|
97
|
-
const
|
|
98
|
-
if (!flag) return null;
|
|
99
|
-
const normalized = flag.trim().toLowerCase();
|
|
100
|
-
if (
|
|
101
|
-
normalized === "" ||
|
|
102
|
-
normalized === "0" ||
|
|
103
|
-
normalized === "false" ||
|
|
104
|
-
normalized === "no"
|
|
105
|
-
) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
const override = env[MEET_AVATAR_DEVICE_ENV_VAR];
|
|
62
|
+
): string {
|
|
63
|
+
const override = env[AVATAR_DEVICE_ENV_VAR];
|
|
109
64
|
return override && override.length > 0
|
|
110
65
|
? override
|
|
111
|
-
:
|
|
66
|
+
: DEFAULT_AVATAR_DEVICE_PATH;
|
|
112
67
|
}
|
|
113
68
|
|
|
114
69
|
/** Default memory (GiB) allocated to the Colima VM. */
|
|
@@ -405,10 +360,12 @@ async function ensureDockerInstalled(): Promise<void> {
|
|
|
405
360
|
export function dockerResourceNames(instanceName: string) {
|
|
406
361
|
return {
|
|
407
362
|
assistantContainer: `${instanceName}-assistant`,
|
|
363
|
+
assistantIpcVolume: `${instanceName}-assistant-ipc`,
|
|
408
364
|
cesContainer: `${instanceName}-credential-executor`,
|
|
409
365
|
cesSecurityVolume: `${instanceName}-ces-sec`,
|
|
410
366
|
dockerdDataVolume: `${instanceName}-dockerd-data`,
|
|
411
367
|
gatewayContainer: `${instanceName}-gateway`,
|
|
368
|
+
gatewayIpcVolume: `${instanceName}-gateway-ipc`,
|
|
412
369
|
gatewaySecurityVolume: `${instanceName}-gateway-sec`,
|
|
413
370
|
network: `${instanceName}-net`,
|
|
414
371
|
socketVolume: `${instanceName}-socket`,
|
|
@@ -464,6 +421,8 @@ export async function retireDocker(name: string): Promise<void> {
|
|
|
464
421
|
}
|
|
465
422
|
for (const vol of [
|
|
466
423
|
res.socketVolume,
|
|
424
|
+
res.assistantIpcVolume,
|
|
425
|
+
res.gatewayIpcVolume,
|
|
467
426
|
res.workspaceVolume,
|
|
468
427
|
res.cesSecurityVolume,
|
|
469
428
|
res.gatewaySecurityVolume,
|
|
@@ -635,8 +594,19 @@ export function serviceDockerRunArgs(opts: {
|
|
|
635
594
|
// container runs its own `dockerd` so the Meet subsystem can spawn
|
|
636
595
|
// sibling meet-bot containers without needing access to the host's
|
|
637
596
|
// Docker engine. This requires:
|
|
638
|
-
// -
|
|
639
|
-
//
|
|
597
|
+
// - `CAP_SYS_ADMIN` + `CAP_NET_ADMIN` so the inner dockerd can
|
|
598
|
+
// configure cgroups, overlay mounts, network namespaces, and
|
|
599
|
+
// iptables. We deliberately avoid `--privileged` (which grants the
|
|
600
|
+
// full host capability set and access to every host device node)
|
|
601
|
+
// to shrink the escape surface from any code running inside the
|
|
602
|
+
// assistant container. See the "Security tradeoff for Docker mode"
|
|
603
|
+
// note in AGENTS.md.
|
|
604
|
+
// - `seccomp=unconfined` + `apparmor=unconfined` because Docker's
|
|
605
|
+
// default seccomp profile blocks syscalls dockerd needs (e.g.
|
|
606
|
+
// certain clone/unshare and pivot_root flags) and the default
|
|
607
|
+
// AppArmor profile on Debian/Ubuntu hosts denies the mount
|
|
608
|
+
// operations dockerd performs while launching bot containers. On
|
|
609
|
+
// hosts where these LSMs are inactive, the options are no-ops.
|
|
640
610
|
// - A dedicated named volume mounted at `/var/lib/docker` so the
|
|
641
611
|
// inner Docker image cache and container state survive restarts of
|
|
642
612
|
// the assistant container.
|
|
@@ -646,7 +616,14 @@ export function serviceDockerRunArgs(opts: {
|
|
|
646
616
|
"run",
|
|
647
617
|
"--init",
|
|
648
618
|
"-d",
|
|
649
|
-
"--
|
|
619
|
+
"--cap-add",
|
|
620
|
+
"SYS_ADMIN",
|
|
621
|
+
"--cap-add",
|
|
622
|
+
"NET_ADMIN",
|
|
623
|
+
"--security-opt",
|
|
624
|
+
"seccomp=unconfined",
|
|
625
|
+
"--security-opt",
|
|
626
|
+
"apparmor=unconfined",
|
|
650
627
|
"--name",
|
|
651
628
|
res.assistantContainer,
|
|
652
629
|
`--network=${res.network}`,
|
|
@@ -677,10 +654,16 @@ export function serviceDockerRunArgs(opts: {
|
|
|
677
654
|
"-v",
|
|
678
655
|
`${res.socketVolume}:/run/ces-bootstrap`,
|
|
679
656
|
"-v",
|
|
657
|
+
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
658
|
+
"-v",
|
|
659
|
+
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
660
|
+
"-v",
|
|
680
661
|
`${res.dockerdDataVolume}:/var/lib/docker`,
|
|
681
662
|
"-e",
|
|
682
663
|
"IS_CONTAINERIZED=true",
|
|
683
664
|
"-e",
|
|
665
|
+
"DEBUG_STDOUT_LOGS=1",
|
|
666
|
+
"-e",
|
|
684
667
|
`VELLUM_ASSISTANT_NAME=${instanceName}`,
|
|
685
668
|
"-e",
|
|
686
669
|
"VELLUM_CLOUD=docker",
|
|
@@ -696,6 +679,10 @@ export function serviceDockerRunArgs(opts: {
|
|
|
696
679
|
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
697
680
|
"-e",
|
|
698
681
|
`GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
|
|
682
|
+
"-e",
|
|
683
|
+
"GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
|
|
684
|
+
"-e",
|
|
685
|
+
"ASSISTANT_IPC_SOCKET_DIR=/run/assistant-ipc",
|
|
699
686
|
];
|
|
700
687
|
if (defaultWorkspaceConfigPath) {
|
|
701
688
|
const containerPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
@@ -712,6 +699,14 @@ export function serviceDockerRunArgs(opts: {
|
|
|
712
699
|
if (opts.signingKey) {
|
|
713
700
|
args.push("-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`);
|
|
714
701
|
}
|
|
702
|
+
if (opts.bootstrapSecret) {
|
|
703
|
+
// Mirror the secret into the assistant container so the runtime's
|
|
704
|
+
// guardian-bootstrap handler can validate the x-bootstrap-secret
|
|
705
|
+
// header forwarded by the gateway. Without this, the published
|
|
706
|
+
// runtime port would expose an unauthenticated token-minting
|
|
707
|
+
// endpoint reachable from the host bypassing the gateway's gate.
|
|
708
|
+
args.push("-e", `GUARDIAN_BOOTSTRAP_SECRET=${opts.bootstrapSecret}`);
|
|
709
|
+
}
|
|
715
710
|
for (const envVar of [
|
|
716
711
|
...Object.values(PROVIDER_ENV_VAR_NAMES),
|
|
717
712
|
"VELLUM_ENVIRONMENT",
|
|
@@ -726,22 +721,13 @@ export function serviceDockerRunArgs(opts: {
|
|
|
726
721
|
args.push("-e", `${key}=${value}`);
|
|
727
722
|
}
|
|
728
723
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// virtual-camera device node (default `/dev/video10`) into the
|
|
732
|
-
// assistant container so the nested `dockerd` can in turn pass it
|
|
733
|
-
// through to the Meet-bot container. The daemon-side equivalent of
|
|
734
|
-
// this opt-in lives on `DockerRunner.run()`'s `avatarDevicePath`
|
|
735
|
-
// option (see `skills/meet-join/daemon/docker-runner.ts`).
|
|
736
|
-
const avatarDevice = resolveMeetAvatarDevicePath();
|
|
737
|
-
if (avatarDevice) {
|
|
724
|
+
const avatarDevice = resolveAvatarDevicePath();
|
|
725
|
+
if (existsSync(avatarDevice)) {
|
|
738
726
|
args.push(
|
|
739
727
|
"--device",
|
|
740
728
|
`${avatarDevice}:${avatarDevice}`,
|
|
741
729
|
"-e",
|
|
742
|
-
`${
|
|
743
|
-
"-e",
|
|
744
|
-
`${MEET_AVATAR_DEVICE_ENV_VAR}=${avatarDevice}`,
|
|
730
|
+
`${AVATAR_DEVICE_ENV_VAR}=${avatarDevice}`,
|
|
745
731
|
);
|
|
746
732
|
}
|
|
747
733
|
args.push(imageTags.assistant);
|
|
@@ -758,6 +744,10 @@ export function serviceDockerRunArgs(opts: {
|
|
|
758
744
|
`${res.workspaceVolume}:/workspace`,
|
|
759
745
|
"-v",
|
|
760
746
|
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
747
|
+
"-v",
|
|
748
|
+
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
749
|
+
"-v",
|
|
750
|
+
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
761
751
|
"-e",
|
|
762
752
|
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
763
753
|
"-e",
|
|
@@ -769,9 +759,11 @@ export function serviceDockerRunArgs(opts: {
|
|
|
769
759
|
"-e",
|
|
770
760
|
`RUNTIME_HTTP_PORT=${ASSISTANT_INTERNAL_PORT}`,
|
|
771
761
|
"-e",
|
|
772
|
-
"RUNTIME_PROXY_ENABLED=true",
|
|
773
|
-
"-e",
|
|
774
762
|
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
763
|
+
"-e",
|
|
764
|
+
"GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
|
|
765
|
+
"-e",
|
|
766
|
+
"ASSISTANT_IPC_SOCKET_DIR=/run/assistant-ipc",
|
|
775
767
|
...(cesServiceToken
|
|
776
768
|
? ["-e", `CES_SERVICE_TOKEN=${cesServiceToken}`]
|
|
777
769
|
: []),
|
|
@@ -1257,21 +1249,27 @@ export async function hatchDocker(
|
|
|
1257
1249
|
log("📁 Creating network and volumes...");
|
|
1258
1250
|
await exec("docker", ["network", "create", res.network]);
|
|
1259
1251
|
await exec("docker", ["volume", "create", res.socketVolume]);
|
|
1252
|
+
await exec("docker", ["volume", "create", res.assistantIpcVolume]);
|
|
1253
|
+
await exec("docker", ["volume", "create", res.gatewayIpcVolume]);
|
|
1260
1254
|
await exec("docker", ["volume", "create", res.workspaceVolume]);
|
|
1261
1255
|
await exec("docker", ["volume", "create", res.cesSecurityVolume]);
|
|
1262
1256
|
await exec("docker", ["volume", "create", res.gatewaySecurityVolume]);
|
|
1263
1257
|
await exec("docker", ["volume", "create", res.dockerdDataVolume]);
|
|
1264
1258
|
|
|
1265
|
-
// Set
|
|
1259
|
+
// Set volume ownership so non-root containers (UID 1001) can write.
|
|
1266
1260
|
await exec("docker", [
|
|
1267
1261
|
"run",
|
|
1268
1262
|
"--rm",
|
|
1269
1263
|
"-v",
|
|
1270
1264
|
`${res.workspaceVolume}:/workspace`,
|
|
1265
|
+
"-v",
|
|
1266
|
+
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
1267
|
+
"-v",
|
|
1268
|
+
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
1271
1269
|
"busybox",
|
|
1272
|
-
"
|
|
1273
|
-
"
|
|
1274
|
-
"/workspace",
|
|
1270
|
+
"sh",
|
|
1271
|
+
"-c",
|
|
1272
|
+
"chown 1001:1001 /workspace /run/assistant-ipc /run/gateway-ipc",
|
|
1275
1273
|
]);
|
|
1276
1274
|
|
|
1277
1275
|
// Write --config key=value pairs to a temp file that gets bind-mounted
|
|
@@ -32,11 +32,13 @@ type EnvironmentDefinition = import("../types.js").EnvironmentDefinition;
|
|
|
32
32
|
const prod: EnvironmentDefinition = {
|
|
33
33
|
name: "production",
|
|
34
34
|
platformUrl: "https://platform.vellum.ai",
|
|
35
|
+
webUrl: "https://www.vellum.ai",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const dev: EnvironmentDefinition = {
|
|
38
39
|
name: "dev",
|
|
39
40
|
platformUrl: "https://dev-platform.vellum.ai",
|
|
41
|
+
webUrl: "https://dev-assistant.vellum.ai",
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
const XDG_ENV_VARS = ["XDG_DATA_HOME", "XDG_CONFIG_HOME"] as const;
|
|
@@ -1,8 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
|
|
1
11
|
import { SEEDS } from "./seeds.js";
|
|
2
12
|
import type { EnvironmentDefinition } from "./types.js";
|
|
3
13
|
|
|
4
14
|
const DEFAULT_ENVIRONMENT_NAME = "production";
|
|
5
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Path to the user's persisted default environment file.
|
|
18
|
+
* Lives at `~/.config/vellum/environment` — a fixed, environment-agnostic
|
|
19
|
+
* location so it can be read before the environment is resolved.
|
|
20
|
+
*/
|
|
21
|
+
function getDefaultEnvironmentPath(): string {
|
|
22
|
+
const xdgConfig =
|
|
23
|
+
process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
24
|
+
return join(xdgConfig, "vellum", "environment");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read the persisted default environment name, if any.
|
|
29
|
+
* Returns `undefined` if no file exists or the file is empty.
|
|
30
|
+
*/
|
|
31
|
+
export function readDefaultEnvironment(): string | undefined {
|
|
32
|
+
const filePath = getDefaultEnvironmentPath();
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(filePath)) return undefined;
|
|
35
|
+
const content = readFileSync(filePath, "utf-8").trim();
|
|
36
|
+
return content.length > 0 ? content : undefined;
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Persist a default environment name to the user config file.
|
|
44
|
+
*/
|
|
45
|
+
export function writeDefaultEnvironment(name: string): void {
|
|
46
|
+
const filePath = getDefaultEnvironmentPath();
|
|
47
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
48
|
+
writeFileSync(filePath, name + "\n", "utf-8");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove the persisted default environment file, falling back to production.
|
|
53
|
+
*/
|
|
54
|
+
export function clearDefaultEnvironment(): void {
|
|
55
|
+
const filePath = getDefaultEnvironmentPath();
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(filePath);
|
|
58
|
+
} catch {
|
|
59
|
+
// Already absent — nothing to do.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
6
63
|
/**
|
|
7
64
|
* Look up a seed entry by name. Returns `undefined` if no seed matches.
|
|
8
65
|
* Callers that need the full resolution stack (env-var overrides, default
|
|
@@ -22,12 +79,13 @@ export function getSeed(name: string): EnvironmentDefinition | undefined {
|
|
|
22
79
|
* Priority:
|
|
23
80
|
* 1. `override` argument (from a `--environment` CLI flag, when wired)
|
|
24
81
|
* 2. `VELLUM_ENVIRONMENT` env var
|
|
25
|
-
* 3. (
|
|
82
|
+
* 3. User config file (`~/.config/vellum/environment`, set via `vellum env set`)
|
|
26
83
|
* 4. Default: `production`
|
|
27
84
|
*
|
|
28
85
|
* Per-field env-var overrides are honored on the resolved definition as
|
|
29
86
|
* ad-hoc escape hatches (they do not materialize new environments):
|
|
30
87
|
* - `VELLUM_PLATFORM_URL` overrides `platformUrl`
|
|
88
|
+
* - `VELLUM_WEB_URL` overrides `webUrl`
|
|
31
89
|
* - `VELLUM_ASSISTANT_PLATFORM_URL` overrides `assistantPlatformUrl`
|
|
32
90
|
* - `VELLUM_LOCKFILE_DIR` overrides `lockfileDirOverride` (legacy e2e
|
|
33
91
|
* test hook)
|
|
@@ -38,7 +96,15 @@ export function getSeed(name: string): EnvironmentDefinition | undefined {
|
|
|
38
96
|
export function getCurrentEnvironment(
|
|
39
97
|
override?: string,
|
|
40
98
|
): EnvironmentDefinition {
|
|
41
|
-
const name =
|
|
99
|
+
const { name, source } = resolveEnvironmentSource(override);
|
|
100
|
+
|
|
101
|
+
// When the environment was resolved from the config file, propagate it
|
|
102
|
+
// into process.env so child processes (daemon, gateway) inherit the same
|
|
103
|
+
// environment without needing to read the config file themselves.
|
|
104
|
+
if (source === "config" && !process.env.VELLUM_ENVIRONMENT) {
|
|
105
|
+
process.env.VELLUM_ENVIRONMENT = name;
|
|
106
|
+
}
|
|
107
|
+
|
|
42
108
|
const seed = SEEDS[name];
|
|
43
109
|
if (!seed) {
|
|
44
110
|
if (name !== DEFAULT_ENVIRONMENT_NAME) {
|
|
@@ -68,6 +134,11 @@ export function getCurrentEnvironment(
|
|
|
68
134
|
resolved.platformUrl = platformUrlOverride;
|
|
69
135
|
}
|
|
70
136
|
|
|
137
|
+
const webUrlOverride = process.env.VELLUM_WEB_URL?.trim();
|
|
138
|
+
if (webUrlOverride) {
|
|
139
|
+
resolved.webUrl = webUrlOverride;
|
|
140
|
+
}
|
|
141
|
+
|
|
71
142
|
const assistantPlatformUrlOverride =
|
|
72
143
|
process.env.VELLUM_ASSISTANT_PLATFORM_URL?.trim();
|
|
73
144
|
if (assistantPlatformUrlOverride) {
|
|
@@ -82,15 +153,26 @@ export function getCurrentEnvironment(
|
|
|
82
153
|
return resolved;
|
|
83
154
|
}
|
|
84
155
|
|
|
85
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the environment name and its source for diagnostics.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveEnvironmentSource(override?: string): {
|
|
160
|
+
name: string;
|
|
161
|
+
source: "flag" | "env" | "config" | "default";
|
|
162
|
+
} {
|
|
86
163
|
const trimmedOverride = override?.trim();
|
|
87
164
|
if (trimmedOverride && trimmedOverride.length > 0) {
|
|
88
|
-
return trimmedOverride;
|
|
165
|
+
return { name: trimmedOverride, source: "flag" };
|
|
89
166
|
}
|
|
90
167
|
const envVar = process.env.VELLUM_ENVIRONMENT?.trim();
|
|
91
168
|
if (envVar && envVar.length > 0) {
|
|
92
|
-
return envVar;
|
|
169
|
+
return { name: envVar, source: "env" };
|
|
93
170
|
}
|
|
94
|
-
|
|
95
|
-
|
|
171
|
+
const configDefault = readDefaultEnvironment();
|
|
172
|
+
if (configDefault) {
|
|
173
|
+
return { name: configDefault, source: "config" };
|
|
174
|
+
}
|
|
175
|
+
return { name: DEFAULT_ENVIRONMENT_NAME, source: "default" };
|
|
96
176
|
}
|
|
177
|
+
|
|
178
|
+
|