@vellumai/cli 0.6.6 → 0.7.0
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/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/config-utils.test.ts +159 -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 +919 -1255
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +40 -8
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +89 -6
- package/src/commands/ps.ts +104 -20
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +15 -2
- package/src/commands/teleport.ts +447 -583
- package/src/commands/terminal.ts +9 -221
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +304 -152
- package/src/index.ts +3 -0
- 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 +383 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/assistant-config.ts +12 -8
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/config-utils.ts +97 -1
- package/src/lib/docker.ts +73 -75
- 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 +178 -0
- package/src/lib/local.ts +139 -15
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +215 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/terminal-session.ts +457 -0
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
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;
|
|
49
|
+
/** Default virtual-camera device path. Overridable via `VELLUM_AVATAR_DEVICE`. */
|
|
50
|
+
const DEFAULT_AVATAR_DEVICE_PATH = "/dev/video10";
|
|
68
51
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
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";
|
|
52
|
+
/** Env var the assistant reads to discover its virtual-camera device path. */
|
|
53
|
+
export const AVATAR_DEVICE_ENV_VAR = "VELLUM_AVATAR_DEVICE";
|
|
82
54
|
|
|
83
55
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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.
|
|
86
59
|
*/
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Resolve the Meet avatar device path to pass through to the assistant
|
|
91
|
-
* container, or `null` if the feature is not opted into. Exported so tests
|
|
92
|
-
* can assert against the env-var parsing without reaching into the shell.
|
|
93
|
-
*/
|
|
94
|
-
export function resolveMeetAvatarDevicePath(
|
|
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,6 +654,10 @@ 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",
|
|
@@ -696,6 +677,10 @@ export function serviceDockerRunArgs(opts: {
|
|
|
696
677
|
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
697
678
|
"-e",
|
|
698
679
|
`GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
|
|
680
|
+
"-e",
|
|
681
|
+
"GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
|
|
682
|
+
"-e",
|
|
683
|
+
"ASSISTANT_IPC_SOCKET_DIR=/run/assistant-ipc",
|
|
699
684
|
];
|
|
700
685
|
if (defaultWorkspaceConfigPath) {
|
|
701
686
|
const containerPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
@@ -712,6 +697,14 @@ export function serviceDockerRunArgs(opts: {
|
|
|
712
697
|
if (opts.signingKey) {
|
|
713
698
|
args.push("-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`);
|
|
714
699
|
}
|
|
700
|
+
if (opts.bootstrapSecret) {
|
|
701
|
+
// Mirror the secret into the assistant container so the runtime's
|
|
702
|
+
// guardian-bootstrap handler can validate the x-bootstrap-secret
|
|
703
|
+
// header forwarded by the gateway. Without this, the published
|
|
704
|
+
// runtime port would expose an unauthenticated token-minting
|
|
705
|
+
// endpoint reachable from the host bypassing the gateway's gate.
|
|
706
|
+
args.push("-e", `GUARDIAN_BOOTSTRAP_SECRET=${opts.bootstrapSecret}`);
|
|
707
|
+
}
|
|
715
708
|
for (const envVar of [
|
|
716
709
|
...Object.values(PROVIDER_ENV_VAR_NAMES),
|
|
717
710
|
"VELLUM_ENVIRONMENT",
|
|
@@ -726,22 +719,13 @@ export function serviceDockerRunArgs(opts: {
|
|
|
726
719
|
args.push("-e", `${key}=${value}`);
|
|
727
720
|
}
|
|
728
721
|
}
|
|
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) {
|
|
722
|
+
const avatarDevice = resolveAvatarDevicePath();
|
|
723
|
+
if (existsSync(avatarDevice)) {
|
|
738
724
|
args.push(
|
|
739
725
|
"--device",
|
|
740
726
|
`${avatarDevice}:${avatarDevice}`,
|
|
741
727
|
"-e",
|
|
742
|
-
`${
|
|
743
|
-
"-e",
|
|
744
|
-
`${MEET_AVATAR_DEVICE_ENV_VAR}=${avatarDevice}`,
|
|
728
|
+
`${AVATAR_DEVICE_ENV_VAR}=${avatarDevice}`,
|
|
745
729
|
);
|
|
746
730
|
}
|
|
747
731
|
args.push(imageTags.assistant);
|
|
@@ -758,6 +742,10 @@ export function serviceDockerRunArgs(opts: {
|
|
|
758
742
|
`${res.workspaceVolume}:/workspace`,
|
|
759
743
|
"-v",
|
|
760
744
|
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
745
|
+
"-v",
|
|
746
|
+
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
747
|
+
"-v",
|
|
748
|
+
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
761
749
|
"-e",
|
|
762
750
|
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
763
751
|
"-e",
|
|
@@ -772,6 +760,10 @@ export function serviceDockerRunArgs(opts: {
|
|
|
772
760
|
"RUNTIME_PROXY_ENABLED=true",
|
|
773
761
|
"-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
|
+
|
|
@@ -28,13 +28,11 @@ function portBlock(base: number): PortMap {
|
|
|
28
28
|
* Built-in environment definitions. Mirrors Swift's
|
|
29
29
|
* `clients/macos/vellum-assistant/App/VellumEnvironment.swift` enum and is
|
|
30
30
|
* the TS-side source of truth for the set of known environment names.
|
|
31
|
-
*
|
|
31
|
+
* One other TS site duplicates the name list:
|
|
32
32
|
* - `assistant/src/util/platform.ts` (`KNOWN_ENVIRONMENTS`)
|
|
33
|
-
*
|
|
34
|
-
* (`NON_PRODUCTION_ENVIRONMENTS`, excludes `production`)
|
|
35
|
-
* Drift between these three sites is caught at test time by
|
|
33
|
+
* Drift between these two sites is caught at test time by
|
|
36
34
|
* `cli/src/__tests__/env-drift.test.ts`. Fast follow: hoist the shared
|
|
37
|
-
* list into a `packages/environments` package so
|
|
35
|
+
* list into a `packages/environments` package so both sites import
|
|
38
36
|
* from one place.
|
|
39
37
|
*
|
|
40
38
|
* Custom environments via a user config file are a future phase — see the
|
|
@@ -45,10 +43,12 @@ export const SEEDS: Record<string, EnvironmentDefinition> = {
|
|
|
45
43
|
production: {
|
|
46
44
|
name: "production",
|
|
47
45
|
platformUrl: "https://platform.vellum.ai",
|
|
46
|
+
webUrl: "https://www.vellum.ai",
|
|
48
47
|
},
|
|
49
48
|
staging: {
|
|
50
49
|
name: "staging",
|
|
51
50
|
platformUrl: "https://staging-platform.vellum.ai",
|
|
51
|
+
webUrl: "https://staging-assistant.vellum.ai",
|
|
52
52
|
portsOverride: portBlock(17000),
|
|
53
53
|
},
|
|
54
54
|
test: {
|
|
@@ -56,16 +56,19 @@ export const SEEDS: Record<string, EnvironmentDefinition> = {
|
|
|
56
56
|
// Non-functional URL — used only by unit tests for URL resolution, never
|
|
57
57
|
// hit in production.
|
|
58
58
|
platformUrl: "https://test-platform.vellum.ai",
|
|
59
|
+
webUrl: "https://dev-assistant.vellum.ai",
|
|
59
60
|
portsOverride: portBlock(19000),
|
|
60
61
|
},
|
|
61
62
|
dev: {
|
|
62
63
|
name: "dev",
|
|
63
64
|
platformUrl: "https://dev-platform.vellum.ai",
|
|
65
|
+
webUrl: "https://dev-assistant.vellum.ai",
|
|
64
66
|
portsOverride: portBlock(18000),
|
|
65
67
|
},
|
|
66
68
|
local: {
|
|
67
69
|
name: "local",
|
|
68
70
|
platformUrl: "http://localhost:8000",
|
|
71
|
+
webUrl: "http://localhost:3000",
|
|
69
72
|
// assistantPlatformUrl: "http://host.docker.internal:8000",
|
|
70
73
|
// ^ uncomment this once dockerized hatch path is live.
|
|
71
74
|
// The assistant runs in a different network namespace than the host.
|
|
@@ -30,6 +30,16 @@ export interface EnvironmentDefinition {
|
|
|
30
30
|
name: string;
|
|
31
31
|
platformUrl: string;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The web app (Next.js) base URL for browser-facing pages like
|
|
35
|
+
* `/account/login`. In production this is separate from the API backend
|
|
36
|
+
* (e.g. `www.vellum.ai` vs `platform.vellum.ai`); locally it's
|
|
37
|
+
* `localhost:3000` vs `localhost:8000`.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors `VellumEnvironment.webURL` on the Swift side.
|
|
40
|
+
*/
|
|
41
|
+
webUrl: string;
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Override for the platform URL the assistant process itself uses. Only
|
|
35
45
|
* differs from `platformUrl` when the assistant runs in a different network
|