@vellumai/cli 0.11.8 → 0.11.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/knip.json +0 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token-refresh.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/helpers/child-process-mock.ts +55 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/upgrade.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/devices.test.ts +5 -8
- package/node_modules/@vellumai/local-mode/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +165 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +63 -6
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-api-key-resolution.test.ts +102 -0
- package/src/__tests__/constants.test.ts +6 -2
- package/src/commands/hatch.ts +1 -367
- package/src/commands/login.ts +16 -29
- package/src/commands/teleport.ts +14 -26
- package/src/lib/assistant-api-key-resolution.ts +89 -0
- package/src/lib/aws.ts +0 -593
- package/src/lib/constants.ts +0 -2
- package/src/lib/gcp.ts +3 -671
- package/src/lib/platform-client.ts +58 -5
- package/src/__tests__/ssh-user-guard.test.ts +0 -28
- package/src/adapters/openclaw-http-server.ts +0 -209
- package/src/adapters/openclaw.ts +0 -131
- package/src/lib/openclaw-runtime-server.ts +0 -28
package/src/commands/hatch.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
|
-
|
|
3
1
|
// Direct import — bun embeds this at compile time so it works in compiled binaries.
|
|
4
2
|
import cliPkg from "../../package.json";
|
|
5
3
|
|
|
6
|
-
import { buildOpenclawStartupScript } from "../adapters/openclaw";
|
|
7
4
|
import {
|
|
8
5
|
saveAssistantEntry,
|
|
9
6
|
setActiveAssistant,
|
|
@@ -14,10 +11,8 @@ import {
|
|
|
14
11
|
VALID_SPECIES,
|
|
15
12
|
} from "../lib/constants";
|
|
16
13
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
17
|
-
import { buildNestedConfig } from "../lib/config-utils";
|
|
18
14
|
import { hatchDocker } from "../lib/docker";
|
|
19
15
|
import { parseFeatureFlagArgs, readAmbientFlagEnvVars } from "../lib/flag-args";
|
|
20
|
-
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
21
16
|
import { hatchLocal } from "../lib/hatch-local";
|
|
22
17
|
import {
|
|
23
18
|
getPlatformUrl,
|
|
@@ -26,149 +21,9 @@ import {
|
|
|
26
21
|
} from "../lib/platform-client";
|
|
27
22
|
import { validateAssistantName } from "../lib/retire-archive";
|
|
28
23
|
|
|
29
|
-
export type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
30
|
-
|
|
31
|
-
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
32
|
-
|
|
33
|
-
const HATCH_TIMEOUT_MS: Record<Species, number> = {
|
|
34
|
-
vellum: 5 * 60 * 1000,
|
|
35
|
-
openclaw: 10 * 60 * 1000,
|
|
36
|
-
};
|
|
37
24
|
const DEFAULT_SPECIES: Species = "vellum";
|
|
38
25
|
|
|
39
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
40
|
-
|
|
41
|
-
const IS_DESKTOP = !!process.env.VELLUM_DESKTOP_APP;
|
|
42
|
-
|
|
43
|
-
function desktopLog(msg: string): void {
|
|
44
|
-
process.stdout.write(msg + "\n");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildTimestampRedirect(logPath: string): string {
|
|
48
|
-
return `exec > >(while IFS= read -r line; do printf '[%s] %s\\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$line"; done > ${logPath}) 2>&1`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function buildUserSetup(sshUser: string): string {
|
|
52
|
-
return `
|
|
53
|
-
SSH_USER="${sshUser}"
|
|
54
|
-
if ! id "$SSH_USER" &>/dev/null; then
|
|
55
|
-
useradd -m -s /bin/bash "$SSH_USER"
|
|
56
|
-
fi
|
|
57
|
-
SSH_USER_HOME=$(eval echo "~$SSH_USER")
|
|
58
|
-
mkdir -p "$SSH_USER_HOME"
|
|
59
|
-
export HOME="$SSH_USER_HOME"
|
|
60
|
-
`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildOwnershipFixup(): string {
|
|
64
|
-
return `
|
|
65
|
-
chown -R "$SSH_USER:$SSH_USER" "$SSH_USER_HOME" 2>/dev/null || true
|
|
66
|
-
`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function buildStartupScript(
|
|
70
|
-
species: Species,
|
|
71
|
-
sshUser: string,
|
|
72
|
-
providerApiKeys: Record<string, string>,
|
|
73
|
-
instanceName: string,
|
|
74
|
-
cloud: RemoteHost,
|
|
75
|
-
configValues: Record<string, string> = {},
|
|
76
|
-
): Promise<{ script: string; laptopBootstrapSecret: string }> {
|
|
77
|
-
const platformUrl = getPlatformUrl();
|
|
78
|
-
const logPath =
|
|
79
|
-
cloud === "custom"
|
|
80
|
-
? "/tmp/vellum-startup.log"
|
|
81
|
-
: "/var/log/startup-script.log";
|
|
82
|
-
const errorPath =
|
|
83
|
-
cloud === "custom" ? "/tmp/vellum-startup-error" : "/var/log/startup-error";
|
|
84
|
-
const timestampRedirect = buildTimestampRedirect(logPath);
|
|
85
|
-
const userSetup = buildUserSetup(sshUser);
|
|
86
|
-
const ownershipFixup = buildOwnershipFixup();
|
|
87
|
-
|
|
88
|
-
if (species === "openclaw") {
|
|
89
|
-
const script = await buildOpenclawStartupScript(
|
|
90
|
-
sshUser,
|
|
91
|
-
providerApiKeys,
|
|
92
|
-
timestampRedirect,
|
|
93
|
-
userSetup,
|
|
94
|
-
ownershipFixup,
|
|
95
|
-
);
|
|
96
|
-
return { script, laptopBootstrapSecret: "" };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Generate a bootstrap secret for the laptop that initiated this remote
|
|
100
|
-
// hatch. The startup script exports it as GUARDIAN_BOOTSTRAP_SECRET so
|
|
101
|
-
// that when `vellum hatch --remote docker` runs on the VM, the docker
|
|
102
|
-
// hatch detects the pre-set env var and appends its own secret.
|
|
103
|
-
const laptopBootstrapSecret = randomBytes(32).toString("hex");
|
|
104
|
-
|
|
105
|
-
// Build bash lines that set each provider API key as a shell variable
|
|
106
|
-
// and corresponding dotenv lines for the env file.
|
|
107
|
-
// Include the laptop bootstrap secret so that when the remote runs
|
|
108
|
-
// `vellum hatch --remote docker`, the docker hatch detects the pre-set
|
|
109
|
-
// env var and appends its own secret for multi-secret guardian init.
|
|
110
|
-
const allEnvEntries: Record<string, string> = {
|
|
111
|
-
...providerApiKeys,
|
|
112
|
-
GUARDIAN_BOOTSTRAP_SECRET: laptopBootstrapSecret,
|
|
113
|
-
};
|
|
114
|
-
const envSetLines = Object.entries(allEnvEntries)
|
|
115
|
-
.map(([envVar, value]) => `${envVar}=${value}`)
|
|
116
|
-
.join("\n");
|
|
117
|
-
const dotenvLines = Object.keys(providerApiKeys)
|
|
118
|
-
.map((envVar) => `${envVar}=\$${envVar}`)
|
|
119
|
-
.join("\n");
|
|
120
|
-
|
|
121
|
-
// Write --config key=value pairs to a temp JSON file on the remote host
|
|
122
|
-
// and export the env var so the daemon reads it on first boot.
|
|
123
|
-
let configWriteBlock = "";
|
|
124
|
-
if (Object.keys(configValues).length > 0) {
|
|
125
|
-
const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
|
|
126
|
-
configWriteBlock = `
|
|
127
|
-
echo "Writing default workspace config..."
|
|
128
|
-
VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
|
|
129
|
-
cat > "\$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH" << 'VELLUM_CONFIG_EOF'
|
|
130
|
-
${configJson}
|
|
131
|
-
VELLUM_CONFIG_EOF
|
|
132
|
-
export VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH
|
|
133
|
-
echo "Default workspace config written to \$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH"
|
|
134
|
-
`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
laptopBootstrapSecret,
|
|
139
|
-
script: `#!/bin/bash
|
|
140
|
-
set -e
|
|
141
|
-
|
|
142
|
-
${timestampRedirect}
|
|
143
|
-
|
|
144
|
-
trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > ${errorPath}; echo "Last 20 log lines:" >> ${errorPath}; tail -20 ${logPath} >> ${errorPath} 2>/dev/null || true; fi' EXIT
|
|
145
|
-
${userSetup}
|
|
146
|
-
${envSetLines}
|
|
147
|
-
VELLUM_ASSISTANT_NAME=${instanceName}
|
|
148
|
-
mkdir -p "\$HOME/.config/vellum"
|
|
149
|
-
cat > "\$HOME/.config/vellum/.env" << DOTENV_EOF
|
|
150
|
-
${dotenvLines}
|
|
151
|
-
RUNTIME_HTTP_PORT=7821
|
|
152
|
-
DOTENV_EOF
|
|
153
|
-
|
|
154
|
-
${ownershipFixup}
|
|
155
|
-
${configWriteBlock}
|
|
156
|
-
export GUARDIAN_BOOTSTRAP_SECRET
|
|
157
|
-
export VELLUM_SSH_USER="\$SSH_USER"
|
|
158
|
-
export VELLUM_ASSISTANT_NAME="\$VELLUM_ASSISTANT_NAME"
|
|
159
|
-
export VELLUM_CLOUD="${cloud}"
|
|
160
|
-
echo "Downloading install script from ${platformUrl}/install.sh..."
|
|
161
|
-
curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
162
|
-
echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
|
|
163
|
-
chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
164
|
-
echo "Running install script..."
|
|
165
|
-
source ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
166
|
-
`,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
26
|
const DEFAULT_REMOTE: RemoteHost = "local";
|
|
171
|
-
const UNSUPPORTED_REMOTE_HATCH_TARGETS = new Set<RemoteHost>(["aws", "gcp"]);
|
|
172
27
|
|
|
173
28
|
interface HatchArgs {
|
|
174
29
|
species: Species;
|
|
@@ -226,7 +81,7 @@ function parseArgs(): HatchArgs {
|
|
|
226
81
|
console.log(" -d Run in detached mode");
|
|
227
82
|
console.log(" --name <name> Custom instance name");
|
|
228
83
|
console.log(
|
|
229
|
-
" --remote <host> Remote host (local,
|
|
84
|
+
" --remote <host> Remote host (local, docker, custom, vellum)",
|
|
230
85
|
);
|
|
231
86
|
console.log(
|
|
232
87
|
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
@@ -379,217 +234,6 @@ function parseArgs(): HatchArgs {
|
|
|
379
234
|
};
|
|
380
235
|
}
|
|
381
236
|
|
|
382
|
-
function formatElapsed(ms: number): string {
|
|
383
|
-
const secs = Math.floor(ms / 1000);
|
|
384
|
-
const m = Math.floor(secs / 60);
|
|
385
|
-
const s = secs % 60;
|
|
386
|
-
return m > 0 ? `${m}m ${s.toString().padStart(2, "0")}s` : `${s}s`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function pickMessage(messages: string[], elapsedMs: number): string {
|
|
390
|
-
const idx = Math.floor(elapsedMs / 15000) % messages.length;
|
|
391
|
-
return messages[idx];
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function getPhaseIcon(
|
|
395
|
-
hasLogs: boolean,
|
|
396
|
-
elapsedMs: number,
|
|
397
|
-
species: Species,
|
|
398
|
-
): string {
|
|
399
|
-
if (!hasLogs) {
|
|
400
|
-
return elapsedMs < 30000 ? "🥚" : "🪺";
|
|
401
|
-
}
|
|
402
|
-
return elapsedMs < 120000 ? "🐣" : SPECIES_CONFIG[species].hatchedEmoji;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export async function watchHatching(
|
|
406
|
-
pollFn: () => Promise<PollResult>,
|
|
407
|
-
instanceName: string,
|
|
408
|
-
startTime: number,
|
|
409
|
-
species: Species,
|
|
410
|
-
): Promise<WatchHatchingResult> {
|
|
411
|
-
if (IS_DESKTOP) {
|
|
412
|
-
return watchHatchingDesktop(pollFn, instanceName, startTime, species);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
let spinnerIdx = 0;
|
|
416
|
-
let lastLogLine: string | null = null;
|
|
417
|
-
let linesDrawn = 0;
|
|
418
|
-
let finished = false;
|
|
419
|
-
let failed = false;
|
|
420
|
-
let lastErrorContent = "";
|
|
421
|
-
let pollInFlight = false;
|
|
422
|
-
let nextPollAt = Date.now() + 15000;
|
|
423
|
-
|
|
424
|
-
function draw(): void {
|
|
425
|
-
if (linesDrawn > 0) {
|
|
426
|
-
process.stdout.write(`\x1b[${linesDrawn}A`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const elapsed = Date.now() - startTime;
|
|
430
|
-
|
|
431
|
-
const hasLogs = lastLogLine !== null;
|
|
432
|
-
const icon = finished
|
|
433
|
-
? failed
|
|
434
|
-
? "💀"
|
|
435
|
-
: SPECIES_CONFIG[species].hatchedEmoji
|
|
436
|
-
: getPhaseIcon(hasLogs, elapsed, species);
|
|
437
|
-
const spinner = finished
|
|
438
|
-
? failed
|
|
439
|
-
? "✘"
|
|
440
|
-
: "✔"
|
|
441
|
-
: SPINNER_FRAMES[spinnerIdx % SPINNER_FRAMES.length];
|
|
442
|
-
const config = SPECIES_CONFIG[species];
|
|
443
|
-
const message = finished
|
|
444
|
-
? failed
|
|
445
|
-
? "❌ Startup script failed"
|
|
446
|
-
: "✨ Your assistant has hatched!"
|
|
447
|
-
: hasLogs
|
|
448
|
-
? lastLogLine!.length > 68
|
|
449
|
-
? lastLogLine!.substring(0, 65) + "..."
|
|
450
|
-
: lastLogLine!
|
|
451
|
-
: pickMessage(config.waitingMessages, elapsed);
|
|
452
|
-
spinnerIdx++;
|
|
453
|
-
|
|
454
|
-
const lines = [
|
|
455
|
-
"",
|
|
456
|
-
` ${icon} ${spinner} ${message} ⏱ ${formatElapsed(elapsed)}`,
|
|
457
|
-
"",
|
|
458
|
-
];
|
|
459
|
-
|
|
460
|
-
for (const line of lines) {
|
|
461
|
-
process.stdout.write(`\x1b[K${line}\n`);
|
|
462
|
-
}
|
|
463
|
-
linesDrawn = lines.length;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async function poll(): Promise<void> {
|
|
467
|
-
if (pollInFlight || finished) return;
|
|
468
|
-
pollInFlight = true;
|
|
469
|
-
try {
|
|
470
|
-
const result = await pollFn();
|
|
471
|
-
if (result.lastLine) {
|
|
472
|
-
lastLogLine = result.lastLine;
|
|
473
|
-
}
|
|
474
|
-
if (result.errorContent) {
|
|
475
|
-
lastErrorContent = result.errorContent;
|
|
476
|
-
}
|
|
477
|
-
if (result.done) {
|
|
478
|
-
finished = true;
|
|
479
|
-
failed = result.failed;
|
|
480
|
-
}
|
|
481
|
-
} finally {
|
|
482
|
-
pollInFlight = false;
|
|
483
|
-
nextPollAt = Date.now() + 5000;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return new Promise<WatchHatchingResult>((resolve) => {
|
|
488
|
-
const interval = setInterval(() => {
|
|
489
|
-
if (finished) {
|
|
490
|
-
draw();
|
|
491
|
-
clearInterval(interval);
|
|
492
|
-
resolve({ success: !failed, errorContent: lastErrorContent });
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const elapsed = Date.now() - startTime;
|
|
497
|
-
if (elapsed >= HATCH_TIMEOUT_MS[species]) {
|
|
498
|
-
clearInterval(interval);
|
|
499
|
-
console.log("");
|
|
500
|
-
console.log(
|
|
501
|
-
` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
|
|
502
|
-
);
|
|
503
|
-
console.log(` Monitor with: vel logs ${instanceName}`);
|
|
504
|
-
console.log("");
|
|
505
|
-
resolve({ success: false, errorContent: lastErrorContent });
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (Date.now() >= nextPollAt) {
|
|
510
|
-
poll();
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
draw();
|
|
514
|
-
}, 80);
|
|
515
|
-
|
|
516
|
-
process.on("SIGINT", () => {
|
|
517
|
-
clearInterval(interval);
|
|
518
|
-
console.log("");
|
|
519
|
-
console.log(` ⚠️ Detaching. Instance is still running.`);
|
|
520
|
-
console.log(` Monitor with: vel logs ${instanceName}`);
|
|
521
|
-
console.log("");
|
|
522
|
-
process.exit(0);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function watchHatchingDesktop(
|
|
528
|
-
pollFn: () => Promise<PollResult>,
|
|
529
|
-
instanceName: string,
|
|
530
|
-
startTime: number,
|
|
531
|
-
species: Species,
|
|
532
|
-
): Promise<WatchHatchingResult> {
|
|
533
|
-
return new Promise<WatchHatchingResult>((resolve) => {
|
|
534
|
-
let prevLogLine: string | null = null;
|
|
535
|
-
let lastErrorContent = "";
|
|
536
|
-
let pollInFlight = false;
|
|
537
|
-
let nextPollAt = Date.now() + 15000;
|
|
538
|
-
|
|
539
|
-
desktopLog("Waiting for instance to start...");
|
|
540
|
-
|
|
541
|
-
const interval = setInterval(async () => {
|
|
542
|
-
const elapsed = Date.now() - startTime;
|
|
543
|
-
|
|
544
|
-
if (elapsed >= HATCH_TIMEOUT_MS[species]) {
|
|
545
|
-
clearInterval(interval);
|
|
546
|
-
desktopLog(
|
|
547
|
-
`Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
|
|
548
|
-
);
|
|
549
|
-
desktopLog(`Monitor with: vel logs ${instanceName}`);
|
|
550
|
-
resolve({ success: false, errorContent: lastErrorContent });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (Date.now() < nextPollAt || pollInFlight) return;
|
|
555
|
-
|
|
556
|
-
pollInFlight = true;
|
|
557
|
-
try {
|
|
558
|
-
const result = await pollFn();
|
|
559
|
-
|
|
560
|
-
if (result.lastLine && result.lastLine !== prevLogLine) {
|
|
561
|
-
prevLogLine = result.lastLine;
|
|
562
|
-
desktopLog(result.lastLine);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (result.errorContent) {
|
|
566
|
-
lastErrorContent = result.errorContent;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (result.done) {
|
|
570
|
-
clearInterval(interval);
|
|
571
|
-
if (result.failed) {
|
|
572
|
-
desktopLog("Startup script failed");
|
|
573
|
-
} else {
|
|
574
|
-
desktopLog("Your assistant has hatched!");
|
|
575
|
-
}
|
|
576
|
-
resolve({ success: !result.failed, errorContent: lastErrorContent });
|
|
577
|
-
}
|
|
578
|
-
} finally {
|
|
579
|
-
pollInFlight = false;
|
|
580
|
-
nextPollAt = Date.now() + 5000;
|
|
581
|
-
}
|
|
582
|
-
}, 5000);
|
|
583
|
-
|
|
584
|
-
process.on("SIGINT", () => {
|
|
585
|
-
clearInterval(interval);
|
|
586
|
-
desktopLog("Detaching. Instance is still running.");
|
|
587
|
-
desktopLog(`Monitor with: vel logs ${instanceName}`);
|
|
588
|
-
process.exit(0);
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
237
|
export { hatchLocal };
|
|
594
238
|
|
|
595
239
|
function getCliVersion(): string {
|
|
@@ -656,16 +300,6 @@ export async function hatch(): Promise<void> {
|
|
|
656
300
|
process.exit(1);
|
|
657
301
|
}
|
|
658
302
|
|
|
659
|
-
if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
|
|
660
|
-
console.error(
|
|
661
|
-
`Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
|
|
662
|
-
);
|
|
663
|
-
console.error(
|
|
664
|
-
"No cloud resources were created. To self-host on AWS/GCP, SSH into the VM and run `vellum hatch` or `vellum hatch --remote docker` there.",
|
|
665
|
-
);
|
|
666
|
-
process.exit(1);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
303
|
if (remote === "local") {
|
|
670
304
|
await hatchLocal(
|
|
671
305
|
species,
|
package/src/commands/login.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
saveAssistantEntry,
|
|
11
11
|
setActiveAssistant,
|
|
12
12
|
} from "../lib/assistant-config";
|
|
13
|
+
import { resolveAssistantApiKeyForInjection } from "../lib/assistant-api-key-resolution";
|
|
13
14
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
14
15
|
import { openBrowser } from "../lib/open-browser";
|
|
15
16
|
import {
|
|
@@ -20,9 +21,7 @@ import {
|
|
|
20
21
|
fetchPlatformAssistants,
|
|
21
22
|
getPlatformUrl,
|
|
22
23
|
injectCredentialsIntoAssistant,
|
|
23
|
-
readGatewayCredential,
|
|
24
24
|
readPlatformToken,
|
|
25
|
-
reprovisionAssistantApiKey,
|
|
26
25
|
savePlatformToken,
|
|
27
26
|
} from "../lib/platform-client";
|
|
28
27
|
import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
@@ -405,33 +404,21 @@ export async function login(): Promise<void> {
|
|
|
405
404
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
406
405
|
);
|
|
407
406
|
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
assistantApiKey = cached.value;
|
|
424
|
-
} else if (!cached.unreachable) {
|
|
425
|
-
console.log("No API key available locally — reprovisioning...");
|
|
426
|
-
const reprovision = await reprovisionAssistantApiKey(
|
|
427
|
-
token,
|
|
428
|
-
orgId,
|
|
429
|
-
clientInstallationId,
|
|
430
|
-
entry.assistantId,
|
|
431
|
-
"cli",
|
|
432
|
-
);
|
|
433
|
-
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
434
|
-
}
|
|
407
|
+
// Which key to hand the assistant, and why. Shared with teleport so
|
|
408
|
+
// the two injection paths cannot drift.
|
|
409
|
+
const resolved = await resolveAssistantApiKeyForInjection({
|
|
410
|
+
registrationApiKey: registration.assistant_api_key,
|
|
411
|
+
runtimeUrl: entry.runtimeUrl,
|
|
412
|
+
bearerToken: entry.bearerToken,
|
|
413
|
+
token,
|
|
414
|
+
organizationId: orgId,
|
|
415
|
+
clientInstallationId,
|
|
416
|
+
runtimeAssistantId: entry.assistantId,
|
|
417
|
+
clientPlatform: "cli",
|
|
418
|
+
});
|
|
419
|
+
const assistantApiKey = resolved.apiKey;
|
|
420
|
+
if (resolved.source === "reprovisioned") {
|
|
421
|
+
console.log("Provisioning a replacement API key...");
|
|
435
422
|
}
|
|
436
423
|
|
|
437
424
|
// Inject credentials into the running assistant via the gateway,
|
package/src/commands/teleport.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAssistantApiKeyForInjection } from "../lib/assistant-api-key-resolution";
|
|
1
2
|
import {
|
|
2
3
|
findAssistantByName,
|
|
3
4
|
loadAllAssistants,
|
|
@@ -23,8 +24,6 @@ import {
|
|
|
23
24
|
platformRequestSignedUrl,
|
|
24
25
|
VersionMismatchError,
|
|
25
26
|
ensureSelfHostedLocalRegistration,
|
|
26
|
-
readGatewayCredential,
|
|
27
|
-
reprovisionAssistantApiKey,
|
|
28
27
|
injectCredentialsIntoAssistant,
|
|
29
28
|
fetchCurrentUser,
|
|
30
29
|
fetchOrganizationId,
|
|
@@ -1154,30 +1153,19 @@ async function tryInjectPlatformCredentials(
|
|
|
1154
1153
|
platformOrganizationId: orgId,
|
|
1155
1154
|
});
|
|
1156
1155
|
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
} else if (!cached.unreachable) {
|
|
1171
|
-
const reprovision = await reprovisionAssistantApiKey(
|
|
1172
|
-
token,
|
|
1173
|
-
orgId,
|
|
1174
|
-
clientInstallationId,
|
|
1175
|
-
entry.assistantId,
|
|
1176
|
-
"cli",
|
|
1177
|
-
);
|
|
1178
|
-
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1156
|
+
// Which key to hand the assistant, and why. Shared with login so the two
|
|
1157
|
+
// injection paths cannot drift.
|
|
1158
|
+
const resolvedKey = await resolveAssistantApiKeyForInjection({
|
|
1159
|
+
registrationApiKey: registration.assistant_api_key,
|
|
1160
|
+
runtimeUrl: entry.runtimeUrl,
|
|
1161
|
+
bearerToken: entry.bearerToken,
|
|
1162
|
+
token,
|
|
1163
|
+
organizationId: orgId,
|
|
1164
|
+
clientInstallationId,
|
|
1165
|
+
runtimeAssistantId: entry.assistantId,
|
|
1166
|
+
clientPlatform: "cli",
|
|
1167
|
+
});
|
|
1168
|
+
const assistantApiKey = resolvedKey.apiKey;
|
|
1181
1169
|
|
|
1182
1170
|
const allInjected = await injectCredentialsIntoAssistant({
|
|
1183
1171
|
gatewayUrl: entry.runtimeUrl,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which assistant API key to inject into a running assistant.
|
|
3
|
+
*
|
|
4
|
+
* The one place that decides, for every flow that hands a local assistant its
|
|
5
|
+
* platform credentials (`login`, `teleport`). Keeping the decision here means a
|
|
6
|
+
* rule about which stored key is usable holds in all of them at once.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
readGatewayCredential,
|
|
10
|
+
reprovisionAssistantApiKey,
|
|
11
|
+
verifyGatewayManagedCredential,
|
|
12
|
+
} from "./platform-client.js";
|
|
13
|
+
|
|
14
|
+
/** Where the key came from, so callers can report what they did. */
|
|
15
|
+
export type AssistantApiKeySource =
|
|
16
|
+
| "registration"
|
|
17
|
+
| "stored"
|
|
18
|
+
| "reprovisioned"
|
|
19
|
+
| "unavailable";
|
|
20
|
+
|
|
21
|
+
export interface ResolveAssistantApiKeyArgs {
|
|
22
|
+
/** Key the registration returned, set only when it had none before. */
|
|
23
|
+
registrationApiKey: string | null;
|
|
24
|
+
runtimeUrl: string;
|
|
25
|
+
bearerToken?: string;
|
|
26
|
+
token: string;
|
|
27
|
+
organizationId: string;
|
|
28
|
+
clientInstallationId: string;
|
|
29
|
+
runtimeAssistantId: string;
|
|
30
|
+
clientPlatform: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedAssistantApiKey {
|
|
34
|
+
apiKey: string | null;
|
|
35
|
+
source: AssistantApiKeySource;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Three steps, in order: the key the registration just issued, the key the
|
|
40
|
+
* assistant already holds, then a rotation.
|
|
41
|
+
*
|
|
42
|
+
* A stored key the platform has rejected is skipped, because reinjecting it
|
|
43
|
+
* repairs nothing while looking like a repair. The verdict is asked for
|
|
44
|
+
* before the key is read, so the key returned is one the verdict covers: a
|
|
45
|
+
* repair that lands between the two requests is then read, not overwritten
|
|
46
|
+
* by the value it replaced. Rotation is still withheld when the gateway is
|
|
47
|
+
* merely unreachable: rotating without being able to store the replacement
|
|
48
|
+
* would start the platform's grace clock on a key the assistant still needs,
|
|
49
|
+
* with nothing to hand it in exchange.
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveAssistantApiKeyForInjection(
|
|
52
|
+
args: ResolveAssistantApiKeyArgs,
|
|
53
|
+
): Promise<ResolvedAssistantApiKey> {
|
|
54
|
+
if (args.registrationApiKey) {
|
|
55
|
+
return { apiKey: args.registrationApiKey, source: "registration" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const status = await verifyGatewayManagedCredential(
|
|
59
|
+
args.runtimeUrl,
|
|
60
|
+
args.bearerToken,
|
|
61
|
+
);
|
|
62
|
+
// A settled rejection came from a gateway that answered, so the
|
|
63
|
+
// replacement can be stored; the rejected key is not worth reading.
|
|
64
|
+
if (status !== "rejected") {
|
|
65
|
+
const cached = await readGatewayCredential(
|
|
66
|
+
args.runtimeUrl,
|
|
67
|
+
"vellum:assistant_api_key",
|
|
68
|
+
args.bearerToken,
|
|
69
|
+
);
|
|
70
|
+
if (cached.value) {
|
|
71
|
+
return { apiKey: cached.value, source: "stored" };
|
|
72
|
+
}
|
|
73
|
+
if (cached.unreachable) {
|
|
74
|
+
return { apiKey: null, source: "unavailable" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reprovision = await reprovisionAssistantApiKey(
|
|
79
|
+
args.token,
|
|
80
|
+
args.organizationId,
|
|
81
|
+
args.clientInstallationId,
|
|
82
|
+
args.runtimeAssistantId,
|
|
83
|
+
args.clientPlatform,
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
apiKey: reprovision.provisioning.assistant_api_key,
|
|
87
|
+
source: "reprovisioned",
|
|
88
|
+
};
|
|
89
|
+
}
|