@vellumai/cli 0.7.0 → 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.
Files changed (40) hide show
  1. package/README.md +49 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/backup.test.ts +475 -0
  4. package/src/__tests__/config-utils.test.ts +35 -48
  5. package/src/__tests__/teleport.test.ts +86 -28
  6. package/src/commands/backup.ts +117 -71
  7. package/src/commands/client.ts +10 -9
  8. package/src/commands/exec.ts +21 -8
  9. package/src/commands/hatch.ts +2 -6
  10. package/src/commands/login.ts +15 -33
  11. package/src/commands/logs.ts +2 -7
  12. package/src/commands/ps.ts +41 -6
  13. package/src/commands/restore.ts +26 -47
  14. package/src/commands/ssh.ts +2 -5
  15. package/src/commands/teleport.ts +38 -24
  16. package/src/commands/tunnel.ts +2 -7
  17. package/src/commands/upgrade.ts +108 -7
  18. package/src/components/DefaultMainScreen.tsx +25 -3
  19. package/src/index.ts +2 -7
  20. package/src/lib/__tests__/local-runtime-client.test.ts +122 -25
  21. package/src/lib/__tests__/platform-client-signed-url.test.ts +2 -2
  22. package/src/lib/__tests__/runtime-url.test.ts +87 -0
  23. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  24. package/src/lib/assistant-client.ts +5 -21
  25. package/src/lib/assistant-config.ts +34 -16
  26. package/src/lib/cli-error.ts +1 -0
  27. package/src/lib/client-identity.ts +1 -1
  28. package/src/lib/config-utils.ts +1 -97
  29. package/src/lib/docker.ts +2 -2
  30. package/src/lib/job-polling.ts +1 -1
  31. package/src/lib/local-runtime-client.ts +81 -28
  32. package/src/lib/local.ts +27 -58
  33. package/src/lib/platform-client.ts +1 -220
  34. package/src/lib/platform-releases.ts +23 -0
  35. package/src/lib/runtime-url.ts +30 -0
  36. package/src/lib/sync-cloud-assistants.ts +126 -0
  37. package/src/lib/terminal-client.ts +6 -1
  38. package/src/lib/terminal-session.ts +127 -48
  39. package/src/lib/tui-log.ts +60 -0
  40. package/src/lib/xdg-log.ts +10 -4
@@ -13,9 +13,7 @@
13
13
  */
14
14
 
15
15
  import {
16
- findAssistantByName,
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 nameOrId = opts?.assistantId;
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
- "No assistant found. Hatch one first with 'vellum hatch'.",
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
 
@@ -343,19 +343,17 @@ export function setActiveAssistant(assistantId: string): void {
343
343
  }
344
344
 
345
345
  /**
346
- * Resolve which assistant to target for a command. Priority:
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:
347
350
  * 1. Explicit name argument
348
351
  * 2. Active assistant set via `vellum use`
349
- * 3. Sole local assistant (when exactly one exists)
352
+ * 3. Sole lockfile entry (any cloud)
350
353
  */
351
- export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
354
+ export function resolveAssistant(nameArg?: string): AssistantEntry | null {
352
355
  if (nameArg) {
353
- const entry = findAssistantByName(nameArg);
354
- if (!entry) {
355
- console.error(`No assistant found with name '${nameArg}'.`);
356
- process.exit(1);
357
- }
358
- return entry;
356
+ return findAssistantByName(nameArg);
359
357
  }
360
358
 
361
359
  const active = getActiveAssistant();
@@ -366,15 +364,35 @@ export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
366
364
  }
367
365
 
368
366
  const all = readAssistants();
369
- const locals = all.filter((e) => e.cloud === "local");
370
- if (locals.length === 1) return locals[0];
367
+ if (all.length === 1) return all[0];
368
+
369
+ return null;
370
+ }
371
371
 
372
- if (locals.length === 0) {
373
- console.error("No local assistant found. Run 'vellum hatch local' first.");
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}'.`);
374
387
  } else {
375
- console.error(
376
- `Multiple assistants found. Set an active assistant with 'vellum use <name>'.`,
377
- );
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
+ }
378
396
  }
379
397
  process.exit(1);
380
398
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  /** Known error categories emitted by CLI commands. */
11
11
  export type CliErrorCategory =
12
+ | "CLI_UPDATE_FAILED"
12
13
  | "DOCKER_NOT_RUNNING"
13
14
  | "IMAGE_PULL_FAILED"
14
15
  | "MISSING_VERSION"
@@ -2,7 +2,7 @@
2
2
  * Stable per-install client identity for the CLI.
3
3
  *
4
4
  * Generates a UUID on first use and persists it to
5
- * `~/.config/vellum/client-id` so the daemon's ClientRegistry can
5
+ * `~/.config/vellum/client-id` so the daemon's event hub can
6
6
  * track this terminal across SSE reconnects and CLI restarts.
7
7
  */
8
8
 
@@ -2,11 +2,6 @@ import { writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
 
5
- const ANTHROPIC_PROVIDER = "anthropic";
6
- const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-7";
7
- const MAIN_AGENT_OPUS_MODEL = "claude-opus-4-7";
8
- const MAIN_AGENT_OPUS_MAX_TOKENS = 32000;
9
-
10
5
  /**
11
6
  * Convert flat dot-notation key=value pairs into a nested config object.
12
7
  *
@@ -37,20 +32,6 @@ export function buildNestedConfig(
37
32
  return config;
38
33
  }
39
34
 
40
- /**
41
- * Build the first-boot workspace config overlay passed to the assistant during
42
- * hatch. Anthropic onboarding sets `llm.default.model` to Sonnet so background
43
- * fallback work stays cheaper, while the main conversation thread should remain
44
- * on Opus via the same call-site override seeded by workspace migration 050.
45
- */
46
- export function buildInitialConfig(
47
- configValues: Record<string, string>,
48
- ): Record<string, unknown> {
49
- const config = buildNestedConfig(configValues);
50
- seedAnthropicMainAgentCallSite(config);
51
- return config;
52
- }
53
-
54
35
  /**
55
36
  * Write arbitrary key-value pairs to a temporary JSON file and return its
56
37
  * path. The caller passes this path to the daemon via the
@@ -68,7 +49,7 @@ export function writeInitialConfig(
68
49
  ): string | undefined {
69
50
  if (Object.keys(configValues).length === 0) return undefined;
70
51
 
71
- const config = buildInitialConfig(configValues);
52
+ const config = buildNestedConfig(configValues);
72
53
  const tempPath = join(
73
54
  tmpdir(),
74
55
  `vellum-default-workspace-config-${process.pid}-${Date.now()}.json`,
@@ -76,80 +57,3 @@ export function writeInitialConfig(
76
57
  writeFileSync(tempPath, JSON.stringify(config, null, 2) + "\n");
77
58
  return tempPath;
78
59
  }
79
-
80
- function seedAnthropicMainAgentCallSite(config: Record<string, unknown>): void {
81
- const llm = ensureObject(config, "llm");
82
-
83
- const existingCallSites = readObject(llm.callSites);
84
- if (existingCallSites !== null && "mainAgent" in existingCallSites) return;
85
-
86
- const { provider, model } = resolveInitialMainAgentBaseSelection(llm);
87
- if (provider !== ANTHROPIC_PROVIDER) return;
88
-
89
- if (
90
- model !== undefined &&
91
- model !== ANTHROPIC_DEFAULT_MODEL &&
92
- model !== MAIN_AGENT_OPUS_MODEL
93
- ) {
94
- return;
95
- }
96
-
97
- const callSites = ensureObject(llm, "callSites");
98
-
99
- callSites.mainAgent = {
100
- model: MAIN_AGENT_OPUS_MODEL,
101
- maxTokens: MAIN_AGENT_OPUS_MAX_TOKENS,
102
- };
103
- }
104
-
105
- function resolveInitialMainAgentBaseSelection(llm: Record<string, unknown>): {
106
- provider: string;
107
- model?: string;
108
- } {
109
- const defaultBlock = readObject(llm.default);
110
- let provider = readString(defaultBlock?.provider) ?? ANTHROPIC_PROVIDER;
111
- let model = readString(defaultBlock?.model);
112
-
113
- const profiles = readObject(llm.profiles);
114
- const activeProfileName = readString(llm.activeProfile);
115
- const activeProfile =
116
- profiles !== null && activeProfileName !== undefined
117
- ? readObject(profiles[activeProfileName])
118
- : null;
119
-
120
- if (activeProfile !== null) {
121
- provider = readString(activeProfile.provider) ?? provider;
122
- model = readString(activeProfile.model) ?? model;
123
- }
124
-
125
- return model === undefined ? { provider } : { provider, model };
126
- }
127
-
128
- function ensureObject(
129
- parent: Record<string, unknown>,
130
- key: string,
131
- ): Record<string, unknown> {
132
- const existing = parent[key];
133
- if (
134
- existing != null &&
135
- typeof existing === "object" &&
136
- !Array.isArray(existing)
137
- ) {
138
- return existing as Record<string, unknown>;
139
- }
140
-
141
- const next: Record<string, unknown> = {};
142
- parent[key] = next;
143
- return next;
144
- }
145
-
146
- function readObject(value: unknown): Record<string, unknown> | null {
147
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
148
- return null;
149
- }
150
- return value as Record<string, unknown>;
151
- }
152
-
153
- function readString(value: unknown): string | undefined {
154
- return typeof value === "string" && value.length > 0 ? value : undefined;
155
- }
package/src/lib/docker.ts CHANGED
@@ -662,6 +662,8 @@ export function serviceDockerRunArgs(opts: {
662
662
  "-e",
663
663
  "IS_CONTAINERIZED=true",
664
664
  "-e",
665
+ "DEBUG_STDOUT_LOGS=1",
666
+ "-e",
665
667
  `VELLUM_ASSISTANT_NAME=${instanceName}`,
666
668
  "-e",
667
669
  "VELLUM_CLOUD=docker",
@@ -757,8 +759,6 @@ export function serviceDockerRunArgs(opts: {
757
759
  "-e",
758
760
  `RUNTIME_HTTP_PORT=${ASSISTANT_INTERNAL_PORT}`,
759
761
  "-e",
760
- "RUNTIME_PROXY_ENABLED=true",
761
- "-e",
762
762
  "CES_CREDENTIAL_URL=http://localhost:8090",
763
763
  "-e",
764
764
  "GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
@@ -107,7 +107,7 @@ function isTransientPollError(err: unknown): boolean {
107
107
  *
108
108
  * Transient errors raised by `poll()` (5xx, network hiccups, rate-limits) are
109
109
  * retried up to `maxTransientErrors` times before the last error propagates,
110
- * matching the pre-rewrite `platformPollExportStatus` loop's behavior so a
110
+ * matching the pre-rewrite migration-export polling loop's behavior so a
111
111
  * single flaky poll doesn't abort a migration that may still be running.
112
112
  */
113
113
  export async function pollJobUntilDone(
@@ -1,7 +1,10 @@
1
+ import type { AssistantEntry } from "./assistant-config.js";
1
2
  import {
3
+ authHeaders,
2
4
  parseUnifiedJobStatus,
3
5
  type UnifiedJobStatus,
4
6
  } from "./platform-client.js";
7
+ import { resolveRuntimeMigrationUrl } from "./runtime-url.js";
5
8
 
6
9
  /**
7
10
  * Thrown when the local runtime returns 409 for an export/import request
@@ -34,6 +37,29 @@ function bearerHeaders(token: string): Record<string, string> {
34
37
  };
35
38
  }
36
39
 
40
+ /**
41
+ * Build the auth + content headers for a runtime migration request.
42
+ *
43
+ * - For `cloud === "vellum"` we go through the platform's wildcard runtime
44
+ * proxy, which authenticates user-session / vak_ tokens via DRF's default
45
+ * authentication classes — `authHeaders()` produces the right combination
46
+ * (`X-Session-Token` + `Vellum-Organization-Id`, or `Authorization: Bearer
47
+ * vak_...`).
48
+ * - For local/docker the runtime endpoint expects a guardian-token bearer.
49
+ */
50
+ async function migrationRequestHeaders(
51
+ entry: Pick<AssistantEntry, "cloud" | "runtimeUrl">,
52
+ token: string,
53
+ ): Promise<Record<string, string>> {
54
+ if (entry.cloud === "vellum") {
55
+ return {
56
+ ...(await authHeaders(token, entry.runtimeUrl)),
57
+ Accept: "application/json",
58
+ };
59
+ }
60
+ return bearerHeaders(token);
61
+ }
62
+
37
63
  interface Raw409Body {
38
64
  detail?: string;
39
65
  // The runtime's current 409 contract nests the payload under `error`:
@@ -69,13 +95,21 @@ async function throwIfInProgress(
69
95
  }
70
96
 
71
97
  /**
72
- * Kick off an async export-to-GCS job on the local runtime.
73
- * POSTs to `{runtimeUrl}/v1/migrations/export-to-gcs` and returns the
74
- * 202-accepted job_id. On 409 (another export in flight) throws
75
- * {@link MigrationInProgressError} with the existing job_id.
98
+ * Kick off an async export-to-GCS job on the assistant's runtime.
99
+ *
100
+ * For local/docker assistants this POSTs to
101
+ * `{runtimeUrl}/v1/migrations/export-to-gcs` with guardian-token bearer
102
+ * auth. For platform-managed (cloud="vellum") assistants the URL is rewritten
103
+ * to the wildcard-runtime-proxy shape
104
+ * `{platformUrl}/v1/assistants/<assistantId>/migrations/export-to-gcs` and
105
+ * authenticated via the platform-token header set the platform's DRF auth
106
+ * accepts (session / vak_).
107
+ *
108
+ * Returns the 202-accepted `job_id`. On 409 (another export in flight)
109
+ * throws {@link MigrationInProgressError} with the existing job_id.
76
110
  */
77
111
  export async function localRuntimeExportToGcs(
78
- runtimeUrl: string,
112
+ entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
79
113
  token: string,
80
114
  params: { uploadUrl: string; description?: string },
81
115
  ): Promise<{ jobId: string }> {
@@ -84,11 +118,14 @@ export async function localRuntimeExportToGcs(
84
118
  body.description = params.description;
85
119
  }
86
120
 
87
- const response = await fetch(`${runtimeUrl}/v1/migrations/export-to-gcs`, {
88
- method: "POST",
89
- headers: bearerHeaders(token),
90
- body: JSON.stringify(body),
91
- });
121
+ const response = await fetch(
122
+ resolveRuntimeMigrationUrl(entry, "export-to-gcs"),
123
+ {
124
+ method: "POST",
125
+ headers: await migrationRequestHeaders(entry, token),
126
+ body: JSON.stringify(body),
127
+ },
128
+ );
92
129
 
93
130
  await throwIfInProgress(response, "export_in_progress");
94
131
 
@@ -110,20 +147,29 @@ export async function localRuntimeExportToGcs(
110
147
  }
111
148
 
112
149
  /**
113
- * Kick off an async import-from-GCS job on the local runtime.
114
- * POSTs to `{runtimeUrl}/v1/migrations/import-from-gcs` with a signed
115
- * download URL. On 409 throws {@link MigrationInProgressError}.
150
+ * Kick off an async import-from-GCS job on the assistant's runtime.
151
+ *
152
+ * For local/docker assistants this POSTs to
153
+ * `{runtimeUrl}/v1/migrations/import-from-gcs` with guardian-token bearer
154
+ * auth. For platform-managed (cloud="vellum") assistants the URL is rewritten
155
+ * to the wildcard-runtime-proxy shape
156
+ * `{platformUrl}/v1/assistants/<assistantId>/migrations/import-from-gcs` and
157
+ * authenticated via the platform token. On 409 throws
158
+ * {@link MigrationInProgressError}.
116
159
  */
117
160
  export async function localRuntimeImportFromGcs(
118
- runtimeUrl: string,
161
+ entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
119
162
  token: string,
120
163
  params: { bundleUrl: string },
121
164
  ): Promise<{ jobId: string }> {
122
- const response = await fetch(`${runtimeUrl}/v1/migrations/import-from-gcs`, {
123
- method: "POST",
124
- headers: bearerHeaders(token),
125
- body: JSON.stringify({ bundle_url: params.bundleUrl }),
126
- });
165
+ const response = await fetch(
166
+ resolveRuntimeMigrationUrl(entry, "import-from-gcs"),
167
+ {
168
+ method: "POST",
169
+ headers: await migrationRequestHeaders(entry, token),
170
+ body: JSON.stringify({ bundle_url: params.bundleUrl }),
171
+ },
172
+ );
127
173
 
128
174
  await throwIfInProgress(response, "import_in_progress");
129
175
 
@@ -145,21 +191,28 @@ export async function localRuntimeImportFromGcs(
145
191
  }
146
192
 
147
193
  /**
148
- * Poll the local runtime's unified job-status endpoint.
149
- * GETs `{runtimeUrl}/v1/migrations/jobs/{jobId}` and parses into
150
- * {@link UnifiedJobStatus}.
194
+ * Poll the runtime's unified job-status endpoint.
195
+ *
196
+ * For local/docker assistants this GETs
197
+ * `{runtimeUrl}/v1/migrations/jobs/{jobId}` directly (guardian-token
198
+ * bearer). For platform-managed assistants it routes through the wildcard
199
+ * runtime proxy at
200
+ * `{platformUrl}/v1/assistants/<assistantId>/migrations/jobs/{jobId}` with
201
+ * platform-token auth — important: the platform's dedicated
202
+ * `/v1/migrations/jobs/{id}/` endpoint queries platform-side ImportJob
203
+ * records and would 404 on runtime-created job IDs.
151
204
  */
152
205
  export async function localRuntimePollJobStatus(
153
- runtimeUrl: string,
206
+ entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
154
207
  token: string,
155
208
  jobId: string,
156
209
  ): Promise<UnifiedJobStatus> {
157
- const response = await fetch(`${runtimeUrl}/v1/migrations/jobs/${jobId}`, {
158
- headers: {
159
- Authorization: `Bearer ${token}`,
160
- Accept: "application/json",
210
+ const response = await fetch(
211
+ resolveRuntimeMigrationUrl(entry, `jobs/${jobId}`),
212
+ {
213
+ headers: await migrationRequestHeaders(entry, token),
161
214
  },
162
- });
215
+ );
163
216
 
164
217
  if (response.status === 404) {
165
218
  throw new Error("Migration job not found");
package/src/lib/local.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  writeFileSync,
9
9
  } from "fs";
10
10
  import { createRequire } from "module";
11
- import { homedir, hostname, networkInterfaces, platform, tmpdir } from "os";
11
+ import { homedir, networkInterfaces, platform, tmpdir } from "os";
12
12
  import { dirname, join } from "path";
13
13
 
14
14
  import {
@@ -392,7 +392,6 @@ async function startDaemonFromSource(
392
392
  : {}),
393
393
  };
394
394
  if (resources) {
395
- env.BASE_DATA_DIR = resources.instanceDir;
396
395
  env.VELLUM_WORKSPACE_DIR = join(
397
396
  resources.instanceDir,
398
397
  ".vellum",
@@ -403,6 +402,11 @@ async function startDaemonFromSource(
403
402
  ".vellum",
404
403
  "protected",
405
404
  );
405
+ env.CREDENTIAL_SECURITY_DIR = join(
406
+ resources.instanceDir,
407
+ ".vellum",
408
+ "protected",
409
+ );
406
410
  env.RUNTIME_HTTP_PORT = String(resources.daemonPort);
407
411
  env.GATEWAY_PORT = String(resources.gatewayPort);
408
412
  env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
@@ -523,7 +527,6 @@ async function startDaemonWatchFromSource(
523
527
  : {}),
524
528
  };
525
529
  if (resources) {
526
- env.BASE_DATA_DIR = resources.instanceDir;
527
530
  env.VELLUM_WORKSPACE_DIR = join(
528
531
  resources.instanceDir,
529
532
  ".vellum",
@@ -534,6 +537,11 @@ async function startDaemonWatchFromSource(
534
537
  ".vellum",
535
538
  "protected",
536
539
  );
540
+ env.CREDENTIAL_SECURITY_DIR = join(
541
+ resources.instanceDir,
542
+ ".vellum",
543
+ "protected",
544
+ );
537
545
  env.RUNTIME_HTTP_PORT = String(resources.daemonPort);
538
546
  env.GATEWAY_PORT = String(resources.gatewayPort);
539
547
  env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
@@ -675,51 +683,18 @@ export async function discoverPublicUrl(
675
683
  return `http://${cloudIp}:${effectivePort}`;
676
684
  }
677
685
 
678
- // Log the local address source only when we actually use it.
679
- if (localResult.source === "hostname") {
680
- console.log(` Discovered macOS local hostname: ${localResult.label}`);
681
- } else if (localResult.source === "lan") {
682
- console.log(` Discovered LAN IP: ${localResult.label}`);
683
- }
684
-
685
686
  return localResult.url;
686
687
  }
687
688
 
688
689
  /**
689
- * Resolve a LAN-reachable URL without any async I/O. Returns the best local
690
- * address or falls back to localhost. Does not emit any logs — the caller
691
- * decides whether to log based on which result is actually used.
690
+ * Returns the localhost URL for the gateway on the given port.
692
691
  */
693
692
  function discoverLocalUrl(effectivePort: number): {
694
693
  url: string;
695
- source: "hostname" | "lan" | "localhost";
696
- label?: string;
694
+ source: "localhost";
697
695
  } {
698
- // On macOS, prefer the .local hostname (Bonjour/mDNS) so other devices on
699
- // the same network can reach the gateway by name.
700
- if (platform() === "darwin") {
701
- const localHostname = getMacLocalHostname();
702
- if (localHostname) {
703
- return {
704
- url: `http://${localHostname}:${effectivePort}`,
705
- source: "hostname",
706
- label: localHostname,
707
- };
708
- }
709
- }
710
-
711
- const lanIp = getLocalLanIPv4();
712
- if (lanIp) {
713
- return {
714
- url: `http://${lanIp}:${effectivePort}`,
715
- source: "lan",
716
- label: lanIp,
717
- };
718
- }
719
-
720
- // Final fallback to localhost when no LAN address could be discovered.
721
696
  return {
722
- url: `http://localhost:${effectivePort}`,
697
+ url: `http://127.0.0.1:${effectivePort}`,
723
698
  source: "localhost",
724
699
  };
725
700
  }
@@ -776,19 +751,6 @@ async function discoverCloudExternalIp(): Promise<string | undefined> {
776
751
  return gcpIp ?? awsIp;
777
752
  }
778
753
 
779
- /**
780
- * Returns the macOS Bonjour/mDNS `.local` hostname (e.g. "Vargass-Mac-Mini.local"),
781
- * or undefined if not running on macOS or the hostname cannot be determined.
782
- */
783
- export function getMacLocalHostname(): string | undefined {
784
- const host = hostname();
785
- if (!host) return undefined;
786
- // macOS hostnames already end with .local when Bonjour is active
787
- if (host.endsWith(".local")) return host;
788
- // Otherwise, append .local — macOS resolves <ComputerName>.local via mDNS
789
- return `${host}.local`;
790
- }
791
-
792
754
  /**
793
755
  * Returns the local IPv4 address most likely to be reachable from other
794
756
  * devices on the same LAN.
@@ -988,8 +950,8 @@ export async function startLocalDaemon(
988
950
  for (const key of [
989
951
  "ANTHROPIC_API_KEY",
990
952
  "APP_VERSION",
991
- "BASE_DATA_DIR",
992
953
  "GATEWAY_SECURITY_DIR",
954
+ "CREDENTIAL_SECURITY_DIR",
993
955
  "VELLUM_ENVIRONMENT",
994
956
  "VELLUM_PLATFORM_URL",
995
957
  "QDRANT_HTTP_PORT",
@@ -1015,7 +977,6 @@ export async function startLocalDaemon(
1015
977
  // When running a named instance, override env so the daemon resolves
1016
978
  // all paths under the instance directory and listens on its own port.
1017
979
  if (resources) {
1018
- daemonEnv.BASE_DATA_DIR = resources.instanceDir;
1019
980
  daemonEnv.VELLUM_WORKSPACE_DIR = join(
1020
981
  resources.instanceDir,
1021
982
  ".vellum",
@@ -1026,6 +987,11 @@ export async function startLocalDaemon(
1026
987
  ".vellum",
1027
988
  "protected",
1028
989
  );
990
+ daemonEnv.CREDENTIAL_SECURITY_DIR = join(
991
+ resources.instanceDir,
992
+ ".vellum",
993
+ "protected",
994
+ );
1029
995
  daemonEnv.RUNTIME_HTTP_PORT = String(resources.daemonPort);
1030
996
  daemonEnv.GATEWAY_PORT = String(resources.gatewayPort);
1031
997
  daemonEnv.QDRANT_HTTP_PORT = String(resources.qdrantPort);
@@ -1165,7 +1131,7 @@ export async function startGateway(
1165
1131
 
1166
1132
  const publicUrl = await discoverPublicUrl(effectiveGatewayPort);
1167
1133
  if (publicUrl) {
1168
- console.log(` Public URL: ${publicUrl}`);
1134
+ console.log(` HTTP URL: ${publicUrl}`);
1169
1135
  }
1170
1136
 
1171
1137
  console.log("🌐 Starting gateway...");
@@ -1179,7 +1145,6 @@ export async function startGateway(
1179
1145
  GATEWAY_PORT: String(effectiveGatewayPort),
1180
1146
  // Pass gateway operational settings via env vars so the CLI does not
1181
1147
  // need direct access to the workspace config file.
1182
- RUNTIME_PROXY_ENABLED: "true",
1183
1148
  RUNTIME_PROXY_REQUIRE_AUTH: "true",
1184
1149
  UNMAPPED_POLICY: "default",
1185
1150
  DEFAULT_ASSISTANT_ID: "self",
@@ -1197,7 +1162,6 @@ export async function startGateway(
1197
1162
  // assistant DB directly for guardian bootstrap.
1198
1163
  ...(resources
1199
1164
  ? {
1200
- BASE_DATA_DIR: resources.instanceDir,
1201
1165
  VELLUM_WORKSPACE_DIR: join(
1202
1166
  resources.instanceDir,
1203
1167
  ".vellum",
@@ -1208,6 +1172,11 @@ export async function startGateway(
1208
1172
  ".vellum",
1209
1173
  "protected",
1210
1174
  ),
1175
+ CREDENTIAL_SECURITY_DIR: join(
1176
+ resources.instanceDir,
1177
+ ".vellum",
1178
+ "protected",
1179
+ ),
1211
1180
  }
1212
1181
  : {}),
1213
1182
  };
@@ -1215,7 +1184,7 @@ export async function startGateway(
1215
1184
  applyIpcSocketDirOverride(gatewayEnv);
1216
1185
 
1217
1186
  if (publicUrl) {
1218
- console.log(` Ingress URL: ${publicUrl}`);
1187
+ console.log(` HTTP URL: ${publicUrl}`);
1219
1188
  }
1220
1189
 
1221
1190
  let gateway;