@vellumai/cli 0.4.54 → 0.4.56

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.
@@ -1,4 +1,3 @@
1
- import { createHash, randomBytes, randomUUID } from "crypto";
2
1
  import {
3
2
  appendFileSync,
4
3
  existsSync,
@@ -6,16 +5,14 @@ import {
6
5
  mkdirSync,
7
6
  readFileSync,
8
7
  readlinkSync,
8
+ rmSync,
9
9
  symlinkSync,
10
10
  unlinkSync,
11
11
  writeFileSync,
12
12
  } from "fs";
13
- import { homedir, hostname, userInfo } from "os";
13
+ import { homedir } from "os";
14
14
  import { join } from "path";
15
15
 
16
- import QRCode from "qrcode";
17
- import qrcode from "qrcode-terminal";
18
-
19
16
  // Direct import — bun embeds this at compile time so it works in compiled binaries.
20
17
  import cliPkg from "../../package.json";
21
18
 
@@ -40,7 +37,6 @@ import {
40
37
  } from "../lib/constants";
41
38
  import type { RemoteHost, Species } from "../lib/constants";
42
39
  import { hatchDocker } from "../lib/docker";
43
- import { mintLocalBearerToken } from "../lib/jwt";
44
40
  import { hatchGcp } from "../lib/gcp";
45
41
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
46
42
  import {
@@ -49,9 +45,10 @@ import {
49
45
  stopLocalProcesses,
50
46
  } from "../lib/local";
51
47
  import { maybeStartNgrokTunnel } from "../lib/ngrok";
48
+ import { httpHealthCheck } from "../lib/http-client";
52
49
  import { detectOrphanedProcesses } from "../lib/orphan-detection";
53
50
  import { isProcessAlive, stopProcess } from "../lib/process";
54
- import { generateRandomSuffix } from "../lib/random-name";
51
+ import { generateInstanceName } from "../lib/random-name";
55
52
  import { validateAssistantName } from "../lib/retire-archive";
56
53
  import { archiveLogFile, resetLogFile } from "../lib/xdg-log";
57
54
 
@@ -97,14 +94,12 @@ chown -R "$SSH_USER:$SSH_USER" "$SSH_USER_HOME" 2>/dev/null || true
97
94
 
98
95
  export async function buildStartupScript(
99
96
  species: Species,
100
- bearerToken: string,
101
97
  sshUser: string,
102
98
  anthropicApiKey: string,
103
99
  instanceName: string,
104
100
  cloud: RemoteHost,
105
101
  ): Promise<string> {
106
- const platformUrl =
107
- process.env.VELLUM_PLATFORM_URL ?? "https://assistant.vellum.ai";
102
+ const platformUrl = process.env.VELLUM_PLATFORM_URL ?? "https://vellum.ai";
108
103
  const logPath =
109
104
  cloud === "custom"
110
105
  ? "/tmp/vellum-startup.log"
@@ -117,7 +112,6 @@ export async function buildStartupScript(
117
112
 
118
113
  if (species === "openclaw") {
119
114
  return await buildOpenclawStartupScript(
120
- bearerToken,
121
115
  sshUser,
122
116
  anthropicApiKey,
123
117
  timestampRedirect,
@@ -567,121 +561,6 @@ function installCLISymlink(): void {
567
561
  );
568
562
  }
569
563
 
570
- async function waitForDaemonReady(
571
- runtimeUrl: string,
572
- bearerToken: string | undefined,
573
- timeoutMs = 15000,
574
- ): Promise<boolean> {
575
- const start = Date.now();
576
- const pollInterval = 1000;
577
- while (Date.now() - start < timeoutMs) {
578
- try {
579
- const res = await fetch(`${runtimeUrl}/v1/health`, {
580
- method: "GET",
581
- headers: bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {},
582
- signal: AbortSignal.timeout(2000),
583
- });
584
- if (res.ok) return true;
585
- } catch {
586
- // Daemon not ready yet
587
- }
588
- await new Promise((r) => setTimeout(r, pollInterval));
589
- }
590
- return false;
591
- }
592
-
593
- async function displayPairingQRCode(
594
- runtimeUrl: string,
595
- bearerToken: string | undefined,
596
- /** External gateway URL for the QR payload. When omitted, runtimeUrl is used. */
597
- externalGatewayUrl?: string,
598
- ): Promise<void> {
599
- try {
600
- const pairingRequestId = randomUUID();
601
- const pairingSecret = randomBytes(32).toString("hex");
602
-
603
- // The daemon's HTTP server may not be fully ready even though the gateway
604
- // health check passed (the gateway is up, but the upstream daemon HTTP
605
- // endpoint it proxies to may still be initializing). Poll the daemon's
606
- // health endpoint through the gateway to ensure it's reachable.
607
- const daemonReady = await waitForDaemonReady(runtimeUrl, bearerToken);
608
- if (!daemonReady) {
609
- console.warn(
610
- "⚠ Assistant health check did not pass within 15s. Run `vellum pair` to try again.\n",
611
- );
612
- return;
613
- }
614
-
615
- const registerRes = await fetch(`${runtimeUrl}/pairing/register`, {
616
- method: "POST",
617
- headers: {
618
- "Content-Type": "application/json",
619
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
620
- },
621
- body: JSON.stringify({
622
- pairingRequestId,
623
- pairingSecret,
624
- gatewayUrl: externalGatewayUrl ?? runtimeUrl,
625
- }),
626
- });
627
-
628
- if (!registerRes.ok) {
629
- const body = await registerRes.text().catch(() => "");
630
- console.warn(
631
- `⚠ Could not register pairing request: ${registerRes.status} ${registerRes.statusText}${body ? ` — ${body}` : ""}. Run \`vellum pair\` to try again.\n`,
632
- );
633
- return;
634
- }
635
-
636
- const hostId = createHash("sha256")
637
- .update(hostname() + userInfo().username)
638
- .digest("hex");
639
- const payload = JSON.stringify({
640
- type: "vellum-daemon",
641
- v: 4,
642
- id: hostId,
643
- g: externalGatewayUrl ?? runtimeUrl,
644
- pairingRequestId,
645
- pairingSecret,
646
- });
647
-
648
- const qrString = await new Promise<string>((resolve) => {
649
- qrcode.generate(payload, { small: true }, (code: string) => {
650
- resolve(code);
651
- });
652
- });
653
-
654
- // Save QR code as PNG to a well-known location so it can be retrieved
655
- // (e.g. via SCP) for pairing through the Desktop app.
656
- const qrDir = join(homedir(), ".vellum", "pairing-qr");
657
- mkdirSync(qrDir, { recursive: true });
658
- const qrPngPath = join(qrDir, "initial.png");
659
- try {
660
- const pngBuffer = await QRCode.toBuffer(payload, {
661
- type: "png",
662
- width: 512,
663
- });
664
- writeFileSync(qrPngPath, pngBuffer);
665
- console.log(`QR code PNG saved to ${qrPngPath}\n`);
666
- } catch (pngErr) {
667
- const pngReason =
668
- pngErr instanceof Error ? pngErr.message : String(pngErr);
669
- console.warn(`\u26A0 Could not save QR code PNG: ${pngReason}\n`);
670
- }
671
-
672
- console.log("Scan this QR code with the Vellum iOS app to pair:\n");
673
- console.log(qrString);
674
- console.log("This pairing request expires in 5 minutes.");
675
- console.log("Run `vellum pair` to generate a new one.\n");
676
- } catch (err) {
677
- // Non-fatal — pairing is optional
678
- const reason = err instanceof Error ? err.message : String(err);
679
- console.warn(
680
- `⚠ Could not generate pairing QR code: ${reason}. Run \`vellum pair\` to try again.\n`,
681
- );
682
- }
683
- }
684
-
685
564
  async function hatchLocal(
686
565
  species: Species,
687
566
  name: string | null,
@@ -697,13 +576,15 @@ async function hatchLocal(
697
576
  process.exit(1);
698
577
  }
699
578
 
700
- const instanceName =
701
- name ??
702
- process.env.VELLUM_ASSISTANT_NAME ??
703
- `${species}-${generateRandomSuffix()}`;
579
+ const instanceName = generateInstanceName(
580
+ species,
581
+ name ?? process.env.VELLUM_ASSISTANT_NAME,
582
+ );
704
583
 
705
584
  // Clean up stale local state: if daemon/gateway processes are running but
706
- // the lock file has no entries, stop them before starting fresh.
585
+ // the lock file has no entries AND the daemon is not healthy, stop them
586
+ // before starting fresh. A healthy daemon should be reused, not killed —
587
+ // it may have been started intentionally via `vellum wake`.
707
588
  const vellumDir = join(homedir(), ".vellum");
708
589
  const existingAssistants = loadAllAssistants();
709
590
  const localAssistants = existingAssistants.filter((a) => a.cloud === "local");
@@ -711,10 +592,16 @@ async function hatchLocal(
711
592
  const daemonPid = isProcessAlive(join(vellumDir, "vellum.pid"));
712
593
  const gatewayPid = isProcessAlive(join(vellumDir, "gateway.pid"));
713
594
  if (daemonPid.alive || gatewayPid.alive) {
714
- console.log(
715
- "🧹 Cleaning up stale local processes (no lock file entry)...\n",
716
- );
717
- await stopLocalProcesses();
595
+ // Check if the daemon is actually healthy before killing it.
596
+ // Default port 7821 is used when there's no lockfile entry.
597
+ const defaultPort = parseInt(process.env.RUNTIME_HTTP_PORT || "7821", 10);
598
+ const healthy = await httpHealthCheck(defaultPort);
599
+ if (!healthy) {
600
+ console.log(
601
+ "🧹 Cleaning up stale local processes (no lock file entry)...\n",
602
+ );
603
+ await stopLocalProcesses();
604
+ }
718
605
  }
719
606
  }
720
607
 
@@ -722,17 +609,32 @@ async function hatchLocal(
722
609
  // are not tracked by any PID file or lock file entry and kill them before
723
610
  // starting new ones. This prevents resource leaks when the desktop app
724
611
  // crashes or is force-quit without a clean shutdown.
612
+ //
613
+ // Skip orphan cleanup if the daemon is already healthy on the expected port
614
+ // — those processes are intentional (e.g. started via `vellum wake`) and
615
+ // startLocalDaemon() will reuse them.
725
616
  if (IS_DESKTOP) {
726
- const orphans = await detectOrphanedProcesses();
727
- if (orphans.length > 0) {
728
- desktopLog(
729
- `🧹 Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"} — cleaning up...`,
730
- );
731
- for (const orphan of orphans) {
732
- await stopProcess(
733
- parseInt(orphan.pid, 10),
734
- `${orphan.name} (PID ${orphan.pid})`,
617
+ const existingResources = findAssistantByName(instanceName);
618
+ const expectedPort =
619
+ existingResources?.cloud === "local" && existingResources.resources
620
+ ? existingResources.resources.daemonPort
621
+ : undefined;
622
+ const daemonAlreadyHealthy = expectedPort
623
+ ? await httpHealthCheck(expectedPort)
624
+ : false;
625
+
626
+ if (!daemonAlreadyHealthy) {
627
+ const orphans = await detectOrphanedProcesses();
628
+ if (orphans.length > 0) {
629
+ desktopLog(
630
+ `🧹 Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"} — cleaning up...`,
735
631
  );
632
+ for (const orphan of orphans) {
633
+ await stopProcess(
634
+ parseInt(orphan.pid, 10),
635
+ `${orphan.name} (PID ${orphan.pid})`,
636
+ );
637
+ }
736
638
  }
737
639
  }
738
640
  }
@@ -747,6 +649,38 @@ async function hatchLocal(
747
649
  resources = await allocateLocalResources(instanceName);
748
650
  }
749
651
 
652
+ // Clean up stale workspace data: if the workspace directory already exists for
653
+ // this instance but no local lockfile entry owns it, a previous retire failed
654
+ // to archive it (or a managed-only retire left local data behind). Remove the
655
+ // workspace subtree so the new assistant starts fresh — but preserve the rest
656
+ // of .vellum (e.g. protected/, credentials) which may be shared.
657
+ if (
658
+ !existingEntry ||
659
+ (existingEntry.cloud != null && existingEntry.cloud !== "local")
660
+ ) {
661
+ const instanceWorkspaceDir = join(
662
+ resources.instanceDir,
663
+ ".vellum",
664
+ "workspace",
665
+ );
666
+ if (existsSync(instanceWorkspaceDir)) {
667
+ const ownedByOther = loadAllAssistants().some((a) => {
668
+ if ((a.cloud != null && a.cloud !== "local") || !a.resources)
669
+ return false;
670
+ return (
671
+ join(a.resources.instanceDir, ".vellum", "workspace") ===
672
+ instanceWorkspaceDir
673
+ );
674
+ });
675
+ if (!ownedByOther) {
676
+ console.log(
677
+ `🧹 Removing stale workspace at ${instanceWorkspaceDir} (not owned by any assistant)...\n`,
678
+ );
679
+ rmSync(instanceWorkspaceDir, { recursive: true, force: true });
680
+ }
681
+ }
682
+ }
683
+
750
684
  const logsDir = join(
751
685
  resources.instanceDir,
752
686
  ".vellum",
@@ -795,15 +729,10 @@ async function hatchLocal(
795
729
  delete process.env.BASE_DATA_DIR;
796
730
  }
797
731
 
798
- // Mint a JWT from the signing key so the CLI can authenticate with the
799
- // daemon/gateway (which requires auth by default).
800
- const bearerToken = mintLocalBearerToken(resources.instanceDir);
801
-
802
732
  const localEntry: AssistantEntry = {
803
733
  assistantId: instanceName,
804
734
  runtimeUrl,
805
735
  localUrl: `http://127.0.0.1:${resources.gatewayPort}`,
806
- bearerToken,
807
736
  cloud: "local",
808
737
  species,
809
738
  hatchedAt: new Date().toISOString(),
@@ -825,12 +754,6 @@ async function hatchLocal(
825
754
  console.log(` Name: ${instanceName}`);
826
755
  console.log(` Runtime: ${runtimeUrl}`);
827
756
  console.log("");
828
-
829
- // Use loopback for HTTP calls (health check + pairing register) since
830
- // mDNS hostnames may not resolve on the local machine, but keep the
831
- // external runtimeUrl in the QR payload so iOS devices can reach it.
832
- const localGatewayUrl = `http://127.0.0.1:${resources.gatewayPort}`;
833
- await displayPairingQRCode(localGatewayUrl, bearerToken, runtimeUrl);
834
757
  }
835
758
 
836
759
  if (keepAlive) {
@@ -7,7 +7,7 @@ import { PNG } from "pngjs";
7
7
  import { saveAssistantEntry } from "../lib/assistant-config";
8
8
  import type { AssistantEntry } from "../lib/assistant-config";
9
9
  import type { Species } from "../lib/constants";
10
- import { generateRandomSuffix } from "../lib/random-name";
10
+ import { generateInstanceName } from "../lib/random-name";
11
11
 
12
12
  interface QRPairingPayload {
13
13
  type: string;
@@ -119,7 +119,7 @@ export async function pair(): Promise<void> {
119
119
  throw new Error("QR code does not contain valid Vellum pairing data.");
120
120
  }
121
121
 
122
- const instanceName = `${species}-${generateRandomSuffix()}`;
122
+ const instanceName = generateInstanceName(species);
123
123
  const runtimeUrl = payload.g;
124
124
  const deviceId = getDeviceId();
125
125
  const deviceName = hostname();
@@ -9,7 +9,11 @@ import {
9
9
  removeAssistantEntry,
10
10
  } from "../lib/assistant-config";
11
11
  import type { AssistantEntry } from "../lib/assistant-config";
12
- import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
12
+ import {
13
+ fetchOrganizationId,
14
+ getPlatformUrl,
15
+ readPlatformToken,
16
+ } from "../lib/platform-client";
13
17
  import { retireInstance as retireAwsInstance } from "../lib/aws";
14
18
  import { retireDocker } from "../lib/docker";
15
19
  import { retireInstance as retireGcpInstance } from "../lib/gcp";
@@ -84,6 +88,21 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
84
88
  const gatewayPidFile = join(vellumDir, "gateway.pid");
85
89
  await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
86
90
 
91
+ // Stop Qdrant — the daemon's graceful shutdown tries to stop it via
92
+ // qdrantManager.stop(), but if the daemon was SIGKILL'd (after 2s timeout)
93
+ // Qdrant may still be running as an orphan. Check both the current PID file
94
+ // location and the legacy location.
95
+ const qdrantPidFile = join(
96
+ vellumDir,
97
+ "workspace",
98
+ "data",
99
+ "qdrant",
100
+ "qdrant.pid",
101
+ );
102
+ const qdrantLegacyPidFile = join(vellumDir, "qdrant.pid");
103
+ await stopProcessByPidFile(qdrantPidFile, "qdrant", undefined, 5000);
104
+ await stopProcessByPidFile(qdrantLegacyPidFile, "qdrant", undefined, 5000);
105
+
87
106
  // If the PID file didn't track a running daemon, scan for orphaned
88
107
  // daemon processes that may have been started without writing a PID.
89
108
  if (!daemonStopped) {
@@ -116,12 +135,12 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
116
135
  try {
117
136
  renameSync(dirToArchive, stagingDir);
118
137
  } catch (err) {
119
- console.warn(
120
- `⚠️ Failed to move ${dirToArchive}: ${err instanceof Error ? err.message : err}`,
138
+ // Re-throw so the caller (and the desktop app) knows the archive failed.
139
+ // If the rename fails, old workspace data stays in place and a subsequent
140
+ // hatch would inherit stale SOUL.md, IDENTITY.md, and memories.
141
+ throw new Error(
142
+ `Failed to archive ${dirToArchive}: ${err instanceof Error ? err.message : err}`,
121
143
  );
122
- console.warn("Skipping archive.");
123
- console.log("\u2705 Local instance retired.");
124
- return;
125
144
  }
126
145
 
127
146
  writeFileSync(metadataPath, JSON.stringify(entry, null, 2) + "\n");
@@ -189,10 +208,15 @@ async function retireVellum(assistantId: string): Promise<void> {
189
208
  process.exit(1);
190
209
  }
191
210
 
211
+ const orgId = await fetchOrganizationId(token);
212
+
192
213
  const url = `${getPlatformUrl()}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
193
214
  const response = await fetch(url, {
194
215
  method: "DELETE",
195
- headers: { "X-Session-Token": token },
216
+ headers: {
217
+ "X-Session-Token": token,
218
+ "Vellum-Organization-Id": orgId,
219
+ },
196
220
  });
197
221
 
198
222
  if (!response.ok) {
@@ -4,7 +4,8 @@ import { join } from "path";
4
4
  import { resolveTargetAssistant } from "../lib/assistant-config.js";
5
5
  import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
6
6
  import {
7
- isWatchModeAvailable,
7
+ isAssistantWatchModeAvailable,
8
+ isGatewayWatchModeAvailable,
8
9
  startLocalDaemon,
9
10
  startGateway,
10
11
  } from "../lib/local";
@@ -24,12 +25,16 @@ export async function wake(): Promise<void> {
24
25
  console.log("");
25
26
  console.log("Options:");
26
27
  console.log(
27
- " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
28
+ " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
29
+ );
30
+ console.log(
31
+ " --foreground Run assistant in foreground with logs printed to terminal",
28
32
  );
29
33
  process.exit(0);
30
34
  }
31
35
 
32
36
  const watch = args.includes("--watch");
37
+ const foreground = args.includes("--foreground");
33
38
  const nameArg = args.find((a) => !a.startsWith("-"));
34
39
  const entry = resolveTargetAssistant(nameArg);
35
40
 
@@ -64,7 +69,7 @@ export async function wake(): Promise<void> {
64
69
  // Watch mode requires bun --watch with .ts sources; packaged desktop
65
70
  // builds only have a compiled binary. Stopping the daemon without a
66
71
  // viable watch-mode path would leave the user with no running assistant.
67
- if (!isWatchModeAvailable()) {
72
+ if (!isAssistantWatchModeAvailable()) {
68
73
  console.log(
69
74
  `Assistant running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
70
75
  );
@@ -85,7 +90,7 @@ export async function wake(): Promise<void> {
85
90
  }
86
91
 
87
92
  if (!daemonRunning) {
88
- await startLocalDaemon(watch, resources);
93
+ await startLocalDaemon(watch, resources, { foreground });
89
94
  }
90
95
 
91
96
  // Start gateway
@@ -95,8 +100,8 @@ export async function wake(): Promise<void> {
95
100
  const { alive, pid } = isProcessAlive(gatewayPidFile);
96
101
  if (alive) {
97
102
  if (watch) {
98
- // Same guard as the daemon: only restart if watch mode is viable.
99
- if (!isWatchModeAvailable()) {
103
+ // Guard gateway restart separately: check gateway source availability.
104
+ if (!isGatewayWatchModeAvailable()) {
100
105
  console.log(
101
106
  `Gateway running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
102
107
  );
@@ -131,4 +136,18 @@ export async function wake(): Promise<void> {
131
136
  }
132
137
 
133
138
  console.log("Wake complete.");
139
+
140
+ if (foreground) {
141
+ console.log("Running in foreground (Ctrl+C to stop)...\n");
142
+ // Block forever — the daemon is running with inherited stdio so its
143
+ // output streams to this terminal. When the user hits Ctrl+C, SIGINT
144
+ // propagates to the daemon child and both exit.
145
+ await new Promise<void>((resolve) => {
146
+ process.on("SIGINT", () => {
147
+ console.log("\nShutting down...");
148
+ resolve();
149
+ });
150
+ process.on("SIGTERM", () => resolve());
151
+ });
152
+ }
134
153
  }
@@ -1943,7 +1943,7 @@ function ChatApp({
1943
1943
  .update(hostname() + userInfo().username)
1944
1944
  .digest("hex");
1945
1945
  const payload = JSON.stringify({
1946
- type: "vellum-daemon",
1946
+ type: "vellum-assistant",
1947
1947
  v: 4,
1948
1948
  id: hostId,
1949
1949
  g: gatewayUrl,
@@ -3,6 +3,7 @@ import { homedir } from "os";
3
3
  import { join } from "path";
4
4
 
5
5
  import {
6
+ DAEMON_INTERNAL_ASSISTANT_ID,
6
7
  DEFAULT_DAEMON_PORT,
7
8
  DEFAULT_GATEWAY_PORT,
8
9
  DEFAULT_QDRANT_PORT,
@@ -49,6 +50,8 @@ export interface AssistantEntry {
49
50
  sshUser?: string;
50
51
  zone?: string;
51
52
  hatchedAt?: string;
53
+ /** Name of the shared volume backing BASE_DATA_DIR for containerised instances. */
54
+ volume?: string;
52
55
  /** Per-instance resource config. Present for local entries in multi-instance setups. */
53
56
  resources?: LocalInstanceResources;
54
57
  [key: string]: unknown;
@@ -152,7 +155,9 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
152
155
  "share",
153
156
  "vellum",
154
157
  "assistants",
155
- typeof raw.assistantId === "string" ? raw.assistantId : "default",
158
+ typeof raw.assistantId === "string"
159
+ ? raw.assistantId
160
+ : DAEMON_INTERNAL_ASSISTANT_ID,
156
161
  );
157
162
  raw.resources = {
158
163
  instanceDir,
@@ -172,7 +177,9 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
172
177
  "share",
173
178
  "vellum",
174
179
  "assistants",
175
- typeof raw.assistantId === "string" ? raw.assistantId : "default",
180
+ typeof raw.assistantId === "string"
181
+ ? raw.assistantId
182
+ : DAEMON_INTERNAL_ASSISTANT_ID,
176
183
  );
177
184
  mutated = true;
178
185
  }
package/src/lib/aws.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "crypto";
2
1
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
3
2
  import { homedir, tmpdir, userInfo } from "os";
4
3
  import { join } from "path";
@@ -9,7 +8,8 @@ import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
9
8
  import type { AssistantEntry } from "./assistant-config";
10
9
  import { GATEWAY_PORT } from "./constants";
11
10
  import type { Species } from "./constants";
12
- import { generateRandomSuffix } from "./random-name";
11
+ import { leaseGuardianToken } from "./guardian-token";
12
+ import { generateInstanceName } from "./random-name";
13
13
  import { exec, execOutput } from "./step-runner";
14
14
 
15
15
  const KEY_PAIR_NAME = "vellum-assistant";
@@ -370,28 +370,6 @@ async function pollAwsInstance(
370
370
  }
371
371
  }
372
372
 
373
- async function fetchRemoteBearerToken(
374
- ip: string,
375
- keyPath: string,
376
- ): Promise<string | null> {
377
- try {
378
- const remoteCmd =
379
- 'cat ~/.vellum.lock.json 2>/dev/null || cat ~/.vellum.lockfile.json 2>/dev/null || echo "{}"';
380
- const output = await awsSshExec(ip, keyPath, remoteCmd);
381
- const data = JSON.parse(output.trim());
382
- const assistants = data.assistants;
383
- if (Array.isArray(assistants) && assistants.length > 0) {
384
- const token = assistants[0].bearerToken;
385
- if (typeof token === "string" && token) {
386
- return token;
387
- }
388
- }
389
- return null;
390
- } catch {
391
- return null;
392
- }
393
- }
394
-
395
373
  export async function hatchAws(
396
374
  species: Species,
397
375
  detached: boolean,
@@ -405,12 +383,7 @@ export async function hatchAws(
405
383
  (await getActiveRegion().catch(() => AWS_DEFAULT_REGION));
406
384
  let instanceName: string;
407
385
 
408
- if (name) {
409
- instanceName = name;
410
- } else {
411
- const suffix = generateRandomSuffix();
412
- instanceName = `${species}-${suffix}`;
413
- }
386
+ instanceName = generateInstanceName(species, name);
414
387
 
415
388
  console.log(`\u{1F95A} Creating new assistant: ${instanceName}`);
416
389
  console.log(` Species: ${species}`);
@@ -431,13 +404,11 @@ export async function hatchAws(
431
404
  console.log(
432
405
  `\u26a0\ufe0f Instance name ${instanceName} already exists, generating a new name...`,
433
406
  );
434
- const suffix = generateRandomSuffix();
435
- instanceName = `${species}-${suffix}`;
407
+ instanceName = generateInstanceName(species);
436
408
  }
437
409
  }
438
410
 
439
411
  const sshUser = userInfo().username;
440
- const bearerToken = randomBytes(32).toString("hex");
441
412
  const hatchedBy = process.env.VELLUM_HATCHED_BY;
442
413
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
443
414
  if (!anthropicApiKey) {
@@ -465,7 +436,6 @@ export async function hatchAws(
465
436
 
466
437
  const startupScript = await buildStartupScript(
467
438
  species,
468
- bearerToken,
469
439
  sshUser,
470
440
  anthropicApiKey,
471
441
  instanceName,
@@ -558,10 +528,14 @@ export async function hatchAws(
558
528
  process.exit(1);
559
529
  }
560
530
 
561
- const remoteBearerToken = await fetchRemoteBearerToken(ip, keyPath);
562
- if (remoteBearerToken) {
563
- awsEntry.bearerToken = remoteBearerToken;
531
+ try {
532
+ const tokenData = await leaseGuardianToken(runtimeUrl, instanceName);
533
+ awsEntry.bearerToken = tokenData.accessToken;
564
534
  saveAssistantEntry(awsEntry);
535
+ } catch (err) {
536
+ console.warn(
537
+ `\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
538
+ );
565
539
  }
566
540
  } else {
567
541
  console.log(
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Canonical internal assistant ID used as the default/fallback across the CLI
3
+ * and daemon. Mirrors `DAEMON_INTERNAL_ASSISTANT_ID` from
4
+ * `assistant/src/runtime/assistant-scope.ts`.
5
+ */
6
+ export const DAEMON_INTERNAL_ASSISTANT_ID = "self" as const;
7
+
1
8
  export const FIREWALL_TAG = "vellum-assistant";
2
9
  export const GATEWAY_PORT = process.env.GATEWAY_PORT
3
10
  ? Number(process.env.GATEWAY_PORT)