@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.
Files changed (45) hide show
  1. package/AGENTS.md +8 -2
  2. package/package.json +1 -1
  3. package/src/__tests__/assistant-config.test.ts +1 -7
  4. package/src/__tests__/config-utils.test.ts +159 -0
  5. package/src/__tests__/env-drift.test.ts +10 -32
  6. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  7. package/src/__tests__/multi-local.test.ts +0 -5
  8. package/src/__tests__/sleep.test.ts +1 -2
  9. package/src/__tests__/teleport.test.ts +919 -1255
  10. package/src/commands/env.ts +93 -0
  11. package/src/commands/events.ts +2 -0
  12. package/src/commands/exec.ts +40 -8
  13. package/src/commands/hatch.ts +6 -2
  14. package/src/commands/login.ts +89 -6
  15. package/src/commands/ps.ts +104 -20
  16. package/src/commands/sleep.ts +5 -2
  17. package/src/commands/ssh.ts +15 -2
  18. package/src/commands/teleport.ts +447 -583
  19. package/src/commands/terminal.ts +9 -221
  20. package/src/commands/wake.ts +2 -1
  21. package/src/components/DefaultMainScreen.tsx +304 -152
  22. package/src/index.ts +3 -0
  23. package/src/lib/__tests__/docker.test.ts +50 -74
  24. package/src/lib/__tests__/job-polling.test.ts +278 -0
  25. package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
  26. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  27. package/src/lib/assistant-config.ts +12 -8
  28. package/src/lib/client-identity.ts +67 -0
  29. package/src/lib/config-utils.ts +97 -1
  30. package/src/lib/docker.ts +73 -75
  31. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  32. package/src/lib/environments/resolve.ts +89 -7
  33. package/src/lib/environments/seeds.ts +8 -5
  34. package/src/lib/environments/types.ts +10 -0
  35. package/src/lib/hatch-local.ts +15 -120
  36. package/src/lib/health-check.ts +98 -0
  37. package/src/lib/job-polling.ts +195 -0
  38. package/src/lib/local-runtime-client.ts +178 -0
  39. package/src/lib/local.ts +139 -15
  40. package/src/lib/orphan-detection.ts +2 -35
  41. package/src/lib/platform-client.ts +215 -0
  42. package/src/lib/retire-local.ts +6 -2
  43. package/src/lib/terminal-session.ts +457 -0
  44. package/src/shared/provider-env-vars.ts +2 -3
  45. 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
- * Default virtual-camera device path when the Meet avatar feature is
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
- * 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";
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
- * Override for the virtual-camera device path. Defaults to
85
- * {@link DEFAULT_MEET_AVATAR_DEVICE_PATH}.
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 const MEET_AVATAR_DEVICE_ENV_VAR = "VELLUM_MEET_AVATAR_DEVICE";
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 | null {
97
- const flag = env[MEET_AVATAR_ENV_VAR];
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
- : DEFAULT_MEET_AVATAR_DEVICE_PATH;
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
- // - `--privileged` so the inner dockerd can manage cgroups, iptables,
639
- // overlayfs mounts, etc.
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
- "--privileged",
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
- // Optional Meet avatar (v4l2loopback) passthrough. When
730
- // `VELLUM_MEET_AVATAR=1` is set in the caller's environment, bind the
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
- `${MEET_AVATAR_ENV_VAR}=1`,
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 workspace volume ownership so non-root containers (UID 1001) can write.
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
- "chown",
1273
- "1001:1001",
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. (future) user context file
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 = resolveEnvironmentName(override);
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
- function resolveEnvironmentName(override: string | undefined): string {
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
- return DEFAULT_ENVIRONMENT_NAME;
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
- * Two other TS sites duplicate the name list:
31
+ * One other TS site duplicates the name list:
32
32
  * - `assistant/src/util/platform.ts` (`KNOWN_ENVIRONMENTS`)
33
- * - `clients/chrome-extension/native-host/src/lockfile.ts`
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 all three sites import
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