@vellumai/cli 0.8.10 → 0.8.11-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/AGENTS.md +2 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +13 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +2 -2
- package/node_modules/@vellumai/local-mode/src/index.ts +1 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +20 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +3 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +169 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +9 -4
- package/package.json +1 -1
- package/src/__tests__/confirm.test.ts +85 -0
- package/src/__tests__/device-id.test.ts +167 -0
- package/src/__tests__/guardian-token.test.ts +79 -0
- package/src/__tests__/helpers/env.ts +19 -0
- package/src/__tests__/statefulset.test.ts +149 -0
- package/src/__tests__/upgrade-replay-env.test.ts +165 -0
- package/src/__tests__/wake.test.ts +68 -0
- package/src/commands/backup.ts +3 -2
- package/src/commands/client.ts +22 -5
- package/src/commands/confirm.ts +144 -0
- package/src/commands/connect.ts +1 -1
- package/src/commands/devices.ts +4 -3
- package/src/commands/hatch.ts +16 -1
- package/src/commands/pair.ts +3 -2
- package/src/commands/restore.ts +3 -2
- package/src/commands/retire.ts +2 -1
- package/src/commands/roadmap.ts +2 -1
- package/src/commands/rollback.ts +9 -37
- package/src/commands/unpair.ts +1 -1
- package/src/commands/upgrade.ts +13 -44
- package/src/commands/wake.ts +49 -1
- package/src/index.ts +11 -4
- package/src/lib/assistant-client.ts +3 -2
- package/src/lib/backup-ops.ts +5 -4
- package/src/lib/device-id.ts +85 -0
- package/src/lib/docker.ts +19 -3
- package/src/lib/guardian-token.ts +44 -8
- package/src/lib/hatch-local.ts +2 -1
- package/src/lib/health-check.ts +6 -4
- package/src/lib/http-client.ts +3 -1
- package/src/lib/local-runtime-client.ts +5 -4
- package/src/lib/local.ts +1 -0
- package/src/lib/loopback-fetch.ts +28 -0
- package/src/lib/ngrok.ts +2 -1
- package/src/lib/platform-client.ts +28 -21
- package/src/lib/platform-releases.ts +3 -2
- package/src/lib/statefulset.ts +43 -0
- package/src/lib/terminal-client.ts +6 -5
- package/src/lib/upgrade-lifecycle.ts +114 -53
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vellum confirm <assistant> --request-id <id> --decision allow|deny`
|
|
3
|
+
*
|
|
4
|
+
* Resolve a pending tool confirmation on a running assistant via its
|
|
5
|
+
* runtime HTTP API. The assistant raises a `confirmation_request` event
|
|
6
|
+
* (with a `requestId`) when a tool exceeds the auto-approve risk
|
|
7
|
+
* threshold; this command answers it so the turn can proceed. Headless
|
|
8
|
+
* automation (e.g. the evals harness) uses it to approve requests that
|
|
9
|
+
* would otherwise hang waiting for an interactive user.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { extractFlag } from "../lib/arg-utils.js";
|
|
13
|
+
import { AssistantClient } from "../lib/assistant-client.js";
|
|
14
|
+
|
|
15
|
+
function printUsage(): void {
|
|
16
|
+
console.log(`vellum confirm - Resolve a pending tool confirmation
|
|
17
|
+
|
|
18
|
+
USAGE:
|
|
19
|
+
vellum confirm [assistant] --request-id <id> [--decision allow|deny]
|
|
20
|
+
|
|
21
|
+
ARGUMENTS:
|
|
22
|
+
[assistant] Instance name (default: active assistant)
|
|
23
|
+
|
|
24
|
+
OPTIONS:
|
|
25
|
+
--request-id <id> The requestId from the confirmation_request event (required)
|
|
26
|
+
--decision <value> allow or deny (default: allow)
|
|
27
|
+
--json Output raw JSON response
|
|
28
|
+
|
|
29
|
+
EXAMPLES:
|
|
30
|
+
vellum confirm --request-id ede263d9-cc45-4d63-86f8-a656d17b3a3a
|
|
31
|
+
vellum confirm my-assistant --request-id req-1 --decision deny
|
|
32
|
+
vellum confirm --json --request-id req-1
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ParsedConfirmArgs {
|
|
37
|
+
assistantId?: string;
|
|
38
|
+
requestId: string;
|
|
39
|
+
decision: "allow" | "deny";
|
|
40
|
+
jsonOutput: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ParseResult =
|
|
44
|
+
| { ok: true; value: ParsedConfirmArgs }
|
|
45
|
+
| { ok: false; error: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse `vellum confirm` arguments. Pure: does no I/O and never exits, so the
|
|
49
|
+
* positional/flag rules can be unit-tested. Defaults the decision to `allow`,
|
|
50
|
+
* which is the common automation case (approve and continue).
|
|
51
|
+
*/
|
|
52
|
+
export function parseConfirmArgs(rawArgs: string[]): ParseResult {
|
|
53
|
+
const jsonOutput = rawArgs.includes("--json");
|
|
54
|
+
let args = rawArgs.filter((a) => a !== "--json");
|
|
55
|
+
|
|
56
|
+
const requestIdFlagPresent = args.includes("--request-id");
|
|
57
|
+
const [requestId, afterRequestId] = extractFlag(args, "--request-id");
|
|
58
|
+
args = afterRequestId;
|
|
59
|
+
|
|
60
|
+
const decisionFlagPresent = args.includes("--decision");
|
|
61
|
+
const [decisionRaw, afterDecision] = extractFlag(args, "--decision");
|
|
62
|
+
args = afterDecision;
|
|
63
|
+
|
|
64
|
+
// `extractFlag` strips a trailing value-less flag, which would otherwise let
|
|
65
|
+
// the next positional masquerade as the flag's value (or, for --decision,
|
|
66
|
+
// silently fall back to "allow" and approve a tool call the caller never
|
|
67
|
+
// meant to approve). Treat a flag supplied without a value as an error.
|
|
68
|
+
if (requestIdFlagPresent && requestId === undefined) {
|
|
69
|
+
return { ok: false, error: "--request-id requires a value." };
|
|
70
|
+
}
|
|
71
|
+
if (!requestId) {
|
|
72
|
+
return { ok: false, error: "--request-id is required." };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (decisionFlagPresent && decisionRaw === undefined) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: '--decision requires a value ("allow" or "deny").',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const decision = decisionRaw ?? "allow";
|
|
82
|
+
if (decision !== "allow" && decision !== "deny") {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `--decision must be "allow" or "deny" (got "${decision}").`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (args.length >= 2) {
|
|
90
|
+
return { ok: false, error: "unexpected extra arguments." };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
value: { assistantId: args[0], requestId, decision, jsonOutput },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function exitWithUsage(error: string): never {
|
|
100
|
+
console.error(`Error: ${error}`);
|
|
101
|
+
console.error("");
|
|
102
|
+
printUsage();
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function confirm(): Promise<void> {
|
|
107
|
+
const rawArgs = process.argv.slice(3);
|
|
108
|
+
|
|
109
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
110
|
+
printUsage();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parsed = parseConfirmArgs(rawArgs);
|
|
115
|
+
if (!parsed.ok) {
|
|
116
|
+
exitWithUsage(parsed.error);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { assistantId, requestId, decision, jsonOutput } = parsed.value;
|
|
120
|
+
|
|
121
|
+
const client = new AssistantClient({ assistantId });
|
|
122
|
+
|
|
123
|
+
const response = await client.post("/confirm", { requestId, decision });
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const body = await response.text().catch(() => "");
|
|
127
|
+
console.error(
|
|
128
|
+
`Error: HTTP ${response.status}: ${body || response.statusText}`,
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = (await response.json()) as { accepted: boolean };
|
|
134
|
+
|
|
135
|
+
if (jsonOutput) {
|
|
136
|
+
console.log(JSON.stringify(result, null, 2));
|
|
137
|
+
} else {
|
|
138
|
+
console.log(
|
|
139
|
+
result.accepted
|
|
140
|
+
? `Confirmation resolved (${decision})`
|
|
141
|
+
: `Confirmation not accepted`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/commands/connect.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { connectImport } from "./connect/import.js";
|
|
2
2
|
|
|
3
3
|
function printUsage(): void {
|
|
4
|
-
console.log("Usage: vellum connect <subcommand>");
|
|
4
|
+
console.log("Usage: vellum connect [beta] <subcommand>");
|
|
5
5
|
console.log("");
|
|
6
6
|
console.log("Connect to an assistant paired from another machine.");
|
|
7
7
|
console.log("");
|
package/src/commands/devices.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
canPromptForConfirmation,
|
|
29
29
|
confirmAction,
|
|
30
30
|
} from "../lib/confirm-action.js";
|
|
31
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
31
32
|
|
|
32
33
|
interface DeviceRecord {
|
|
33
34
|
hashedDeviceId: string;
|
|
@@ -38,7 +39,7 @@ interface DeviceRecord {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function printUsage(): void {
|
|
41
|
-
console.log(`vellum devices - List and revoke devices paired to a local assistant
|
|
42
|
+
console.log(`vellum devices [beta] - List and revoke devices paired to a local assistant
|
|
42
43
|
|
|
43
44
|
USAGE:
|
|
44
45
|
vellum devices [name]
|
|
@@ -108,7 +109,7 @@ async function listDevices(entry: AssistantEntry, base: string): Promise<void> {
|
|
|
108
109
|
|
|
109
110
|
let response: Response;
|
|
110
111
|
try {
|
|
111
|
-
response = await
|
|
112
|
+
response = await loopbackSafeFetch(`${base}/v1/devices`, {
|
|
112
113
|
method: "GET",
|
|
113
114
|
headers: getClientRegistrationHeaders(CLI_INTERFACE_ID),
|
|
114
115
|
});
|
|
@@ -186,7 +187,7 @@ async function revokeDevice(
|
|
|
186
187
|
|
|
187
188
|
let response: Response;
|
|
188
189
|
try {
|
|
189
|
-
response = await
|
|
190
|
+
response = await loopbackSafeFetch(`${base}/v1/devices/revoke`, {
|
|
190
191
|
method: "POST",
|
|
191
192
|
headers: {
|
|
192
193
|
"Content-Type": "application/json",
|
package/src/commands/hatch.ts
CHANGED
|
@@ -181,6 +181,7 @@ interface HatchArgs {
|
|
|
181
181
|
configValues: Record<string, string>;
|
|
182
182
|
flagEnvVars: Record<string, string>;
|
|
183
183
|
analyze: boolean;
|
|
184
|
+
disablePlatform: boolean;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
function parseArgs(): HatchArgs {
|
|
@@ -188,6 +189,8 @@ function parseArgs(): HatchArgs {
|
|
|
188
189
|
process.argv.slice(3),
|
|
189
190
|
);
|
|
190
191
|
const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
|
|
192
|
+
const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
|
|
193
|
+
let disablePlatform = disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
|
|
191
194
|
let species: Species = DEFAULT_SPECIES;
|
|
192
195
|
let detached = false;
|
|
193
196
|
let keepAlive = false;
|
|
@@ -233,6 +236,9 @@ function parseArgs(): HatchArgs {
|
|
|
233
236
|
console.log(
|
|
234
237
|
" --analyze Emit a structured hatch-timing log line on stdout",
|
|
235
238
|
);
|
|
239
|
+
console.log(
|
|
240
|
+
" --disable-platform Suppress all outbound platform API calls",
|
|
241
|
+
);
|
|
236
242
|
process.exit(0);
|
|
237
243
|
} else if (arg === "-d") {
|
|
238
244
|
detached = true;
|
|
@@ -293,11 +299,13 @@ function parseArgs(): HatchArgs {
|
|
|
293
299
|
const value = next.slice(eqIndex + 1);
|
|
294
300
|
configValues[key] = value;
|
|
295
301
|
i++;
|
|
302
|
+
} else if (arg === "--disable-platform") {
|
|
303
|
+
disablePlatform = true;
|
|
296
304
|
} else if (VALID_SPECIES.includes(arg as Species)) {
|
|
297
305
|
species = arg as Species;
|
|
298
306
|
} else {
|
|
299
307
|
console.error(
|
|
300
|
-
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze`,
|
|
308
|
+
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform`,
|
|
301
309
|
);
|
|
302
310
|
process.exit(1);
|
|
303
311
|
}
|
|
@@ -314,6 +322,7 @@ function parseArgs(): HatchArgs {
|
|
|
314
322
|
configValues,
|
|
315
323
|
flagEnvVars,
|
|
316
324
|
analyze,
|
|
325
|
+
disablePlatform,
|
|
317
326
|
};
|
|
318
327
|
}
|
|
319
328
|
|
|
@@ -549,8 +558,14 @@ export async function hatch(): Promise<void> {
|
|
|
549
558
|
configValues,
|
|
550
559
|
flagEnvVars,
|
|
551
560
|
analyze,
|
|
561
|
+
disablePlatform,
|
|
552
562
|
} = parseArgs();
|
|
553
563
|
|
|
564
|
+
if (disablePlatform) {
|
|
565
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
566
|
+
flagEnvVars.VELLUM_DISABLE_PLATFORM = "true";
|
|
567
|
+
}
|
|
568
|
+
|
|
554
569
|
if (watch && remote !== "local" && remote !== "docker") {
|
|
555
570
|
console.error(
|
|
556
571
|
"Error: --watch is only supported for local and docker hatch targets.",
|
package/src/commands/pair.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "../lib/client-identity.js";
|
|
27
27
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
28
28
|
import { getLocalLanIPv4 } from "../lib/local.js";
|
|
29
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
29
30
|
|
|
30
31
|
function isLoopbackHost(url: string): boolean {
|
|
31
32
|
try {
|
|
@@ -37,7 +38,7 @@ function isLoopbackHost(url: string): boolean {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function printUsage(): void {
|
|
40
|
-
console.log(`vellum pair - Mint a device-scoped token for another machine
|
|
41
|
+
console.log(`vellum pair [beta] - Mint a device-scoped token for another machine
|
|
41
42
|
|
|
42
43
|
USAGE:
|
|
43
44
|
vellum pair [assistant] [options]
|
|
@@ -154,7 +155,7 @@ export async function pair(): Promise<void> {
|
|
|
154
155
|
|
|
155
156
|
let response: Response;
|
|
156
157
|
try {
|
|
157
|
-
response = await
|
|
158
|
+
response = await loopbackSafeFetch(`${mintUrl}/v1/pair`, {
|
|
158
159
|
method: "POST",
|
|
159
160
|
headers: {
|
|
160
161
|
"Content-Type": "application/json",
|
package/src/commands/restore.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
platformPollJobStatus,
|
|
17
17
|
} from "../lib/platform-client.js";
|
|
18
18
|
import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
|
|
19
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
19
20
|
|
|
20
21
|
function printUsage(): void {
|
|
21
22
|
console.log(
|
|
@@ -588,7 +589,7 @@ export async function restore(): Promise<void> {
|
|
|
588
589
|
|
|
589
590
|
let response: Response;
|
|
590
591
|
try {
|
|
591
|
-
response = await
|
|
592
|
+
response = await loopbackSafeFetch(
|
|
592
593
|
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
593
594
|
{
|
|
594
595
|
method: "POST",
|
|
@@ -694,7 +695,7 @@ export async function restore(): Promise<void> {
|
|
|
694
695
|
|
|
695
696
|
let response: Response;
|
|
696
697
|
try {
|
|
697
|
-
response = await
|
|
698
|
+
response = await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
698
699
|
method: "POST",
|
|
699
700
|
headers: {
|
|
700
701
|
Authorization: `Bearer ${accessToken}`,
|
package/src/commands/retire.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
resetLogFile,
|
|
37
37
|
writeToLogFile,
|
|
38
38
|
} from "../lib/xdg-log.js";
|
|
39
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
39
40
|
|
|
40
41
|
export { retireLocal };
|
|
41
42
|
|
|
@@ -96,7 +97,7 @@ async function retireVellum(
|
|
|
96
97
|
|
|
97
98
|
const platformUrl = runtimeUrl || getPlatformUrl();
|
|
98
99
|
const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
|
|
99
|
-
const response = await
|
|
100
|
+
const response = await loopbackSafeFetch(url, {
|
|
100
101
|
method: "DELETE",
|
|
101
102
|
headers: await authHeaders(token, runtimeUrl),
|
|
102
103
|
});
|
package/src/commands/roadmap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readPlatformToken, getWebUrl } from "../lib/platform-client.js";
|
|
2
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
2
3
|
|
|
3
4
|
function printUsage(): void {
|
|
4
5
|
console.log("Usage: vellum roadmap <subcommand>");
|
|
@@ -88,7 +89,7 @@ async function apiFetch(
|
|
|
88
89
|
if (options.token) headers["X-Session-Token"] = options.token;
|
|
89
90
|
if (options.body) headers["Content-Type"] = "application/json";
|
|
90
91
|
|
|
91
|
-
return
|
|
92
|
+
return loopbackSafeFetch(url, {
|
|
92
93
|
method: options.method ?? "GET",
|
|
93
94
|
headers,
|
|
94
95
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
package/src/commands/rollback.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
findAssistantByName,
|
|
5
3
|
getActiveAssistant,
|
|
@@ -27,9 +25,8 @@ import {
|
|
|
27
25
|
buildProgressEvent,
|
|
28
26
|
buildStartingEvent,
|
|
29
27
|
buildUpgradeCommitMessage,
|
|
30
|
-
|
|
28
|
+
captureReplayState,
|
|
31
29
|
commitWorkspaceViaGateway,
|
|
32
|
-
CONTAINER_ENV_EXCLUDE_KEYS,
|
|
33
30
|
fetchCurrentVersion,
|
|
34
31
|
fetchPreviousVersion,
|
|
35
32
|
performDockerRollback,
|
|
@@ -308,39 +305,13 @@ export async function rollback(): Promise<void> {
|
|
|
308
305
|
`🔄 Rolling back Docker assistant '${instanceName}' to ${previousVersion}...\n`,
|
|
309
306
|
);
|
|
310
307
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
|
|
319
|
-
// set on gateway, not assistant) so it persists across container restarts.
|
|
320
|
-
const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
|
|
321
|
-
const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
|
|
322
|
-
|
|
323
|
-
// Extract CES_SERVICE_TOKEN from captured env, or generate fresh one
|
|
324
|
-
const cesServiceToken =
|
|
325
|
-
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
326
|
-
|
|
327
|
-
// Extract or generate the shared JWT signing key.
|
|
328
|
-
const signingKey =
|
|
329
|
-
capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
|
|
330
|
-
|
|
331
|
-
// Build extra env vars, excluding keys managed by buildServiceRunArgs
|
|
332
|
-
const envKeysSetByRunArgs = new Set(CONTAINER_ENV_EXCLUDE_KEYS);
|
|
333
|
-
for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
|
|
334
|
-
if (process.env[envVar]) {
|
|
335
|
-
envKeysSetByRunArgs.add(envVar);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const extraAssistantEnv: Record<string, string> = {};
|
|
339
|
-
for (const [key, value] of Object.entries(capturedEnv)) {
|
|
340
|
-
if (!envKeysSetByRunArgs.has(key)) {
|
|
341
|
-
extraAssistantEnv[key] = value;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
308
|
+
const {
|
|
309
|
+
bootstrapSecret,
|
|
310
|
+
cesServiceToken,
|
|
311
|
+
signingKey,
|
|
312
|
+
extraAssistantEnv,
|
|
313
|
+
extraGatewayEnv,
|
|
314
|
+
} = await captureReplayState(res);
|
|
344
315
|
|
|
345
316
|
// Parse gateway port from entry's runtimeUrl, fall back to default
|
|
346
317
|
let gatewayPort = GATEWAY_INTERNAL_PORT;
|
|
@@ -401,6 +372,7 @@ export async function rollback(): Promise<void> {
|
|
|
401
372
|
bootstrapSecret,
|
|
402
373
|
cesServiceToken,
|
|
403
374
|
extraAssistantEnv,
|
|
375
|
+
extraGatewayEnv,
|
|
404
376
|
gatewayPort,
|
|
405
377
|
imageTags: previousImageRefs,
|
|
406
378
|
instanceName,
|
package/src/commands/unpair.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { deleteGuardianToken } from "../lib/guardian-token";
|
|
25
25
|
|
|
26
26
|
function printUsage(): void {
|
|
27
|
-
console.log(`vellum unpair - Forget a paired assistant imported from another machine
|
|
27
|
+
console.log(`vellum unpair [beta] - Forget a paired assistant imported from another machine
|
|
28
28
|
|
|
29
29
|
USAGE:
|
|
30
30
|
vellum unpair <name> [--yes]
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { spawnSync } from "child_process";
|
|
3
2
|
|
|
4
3
|
import cliPkg from "../../package.json";
|
|
@@ -40,15 +39,15 @@ import {
|
|
|
40
39
|
buildProgressEvent,
|
|
41
40
|
buildStartingEvent,
|
|
42
41
|
buildUpgradeCommitMessage,
|
|
43
|
-
|
|
42
|
+
captureReplayState,
|
|
44
43
|
captureUpgradeFailureLogs,
|
|
45
44
|
commitWorkspaceViaGateway,
|
|
46
|
-
CONTAINER_ENV_EXCLUDE_KEYS,
|
|
47
45
|
rollbackMigrations,
|
|
48
46
|
UPGRADE_PROGRESS,
|
|
49
47
|
waitForReady,
|
|
50
48
|
} from "../lib/upgrade-lifecycle.js";
|
|
51
49
|
import { compareVersions } from "../lib/version-compat.js";
|
|
50
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
52
51
|
|
|
53
52
|
interface UpgradeArgs {
|
|
54
53
|
name: string | null;
|
|
@@ -232,7 +231,7 @@ async function upgradeDocker(
|
|
|
232
231
|
lastWorkspaceMigrationId?: string;
|
|
233
232
|
} = {};
|
|
234
233
|
try {
|
|
235
|
-
const healthResp = await
|
|
234
|
+
const healthResp = await loopbackSafeFetch(
|
|
236
235
|
`${entry.runtimeUrl}/healthz?include=migrations`,
|
|
237
236
|
{
|
|
238
237
|
signal: AbortSignal.timeout(5000),
|
|
@@ -297,16 +296,13 @@ async function upgradeDocker(
|
|
|
297
296
|
}),
|
|
298
297
|
);
|
|
299
298
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// set on gateway, not assistant) so it persists across container restarts.
|
|
308
|
-
const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
|
|
309
|
-
const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
|
|
299
|
+
const {
|
|
300
|
+
bootstrapSecret,
|
|
301
|
+
cesServiceToken,
|
|
302
|
+
signingKey,
|
|
303
|
+
extraAssistantEnv,
|
|
304
|
+
extraGatewayEnv,
|
|
305
|
+
} = await captureReplayState(res);
|
|
310
306
|
|
|
311
307
|
// Notify connected clients that an upgrade is about to begin.
|
|
312
308
|
// This must fire BEFORE any progress broadcasts so the UI sets
|
|
@@ -361,18 +357,6 @@ async function upgradeDocker(
|
|
|
361
357
|
// use default
|
|
362
358
|
}
|
|
363
359
|
|
|
364
|
-
// Extract CES_SERVICE_TOKEN from the captured env so it can be passed via
|
|
365
|
-
// the dedicated cesServiceToken parameter (which propagates it to all three
|
|
366
|
-
// containers). If the old instance predates CES_SERVICE_TOKEN, generate a
|
|
367
|
-
// fresh one so gateway and CES can authenticate.
|
|
368
|
-
const cesServiceToken =
|
|
369
|
-
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
370
|
-
|
|
371
|
-
// Extract or generate the shared JWT signing key. Pre-env-var instances
|
|
372
|
-
// won't have it in capturedEnv, so generate fresh in that case.
|
|
373
|
-
const signingKey =
|
|
374
|
-
capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
|
|
375
|
-
|
|
376
360
|
// Create pre-upgrade backup (best-effort, daemon must be running)
|
|
377
361
|
await broadcastUpgradeEvent(
|
|
378
362
|
entry.runtimeUrl,
|
|
@@ -415,23 +399,6 @@ async function upgradeDocker(
|
|
|
415
399
|
await stopContainers(res);
|
|
416
400
|
console.log("✅ Containers stopped\n");
|
|
417
401
|
|
|
418
|
-
// Build the set of extra env vars to replay on the new assistant container.
|
|
419
|
-
// Captured env vars serve as the base; keys already managed by
|
|
420
|
-
// buildServiceRunArgs are excluded to avoid duplicates.
|
|
421
|
-
const envKeysSetByRunArgs = new Set(CONTAINER_ENV_EXCLUDE_KEYS);
|
|
422
|
-
// Only exclude keys that buildServiceRunArgs will actually set
|
|
423
|
-
for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
|
|
424
|
-
if (process.env[envVar]) {
|
|
425
|
-
envKeysSetByRunArgs.add(envVar);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
const extraAssistantEnv: Record<string, string> = {};
|
|
429
|
-
for (const [key, value] of Object.entries(capturedEnv)) {
|
|
430
|
-
if (!envKeysSetByRunArgs.has(key)) {
|
|
431
|
-
extraAssistantEnv[key] = value;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
402
|
console.log("🚀 Starting upgraded containers...");
|
|
436
403
|
await startContainers(
|
|
437
404
|
{
|
|
@@ -439,6 +406,7 @@ async function upgradeDocker(
|
|
|
439
406
|
bootstrapSecret,
|
|
440
407
|
cesServiceToken,
|
|
441
408
|
extraAssistantEnv,
|
|
409
|
+
extraGatewayEnv,
|
|
442
410
|
gatewayPort,
|
|
443
411
|
imageTags,
|
|
444
412
|
instanceName,
|
|
@@ -544,6 +512,7 @@ async function upgradeDocker(
|
|
|
544
512
|
bootstrapSecret,
|
|
545
513
|
cesServiceToken,
|
|
546
514
|
extraAssistantEnv,
|
|
515
|
+
extraGatewayEnv,
|
|
547
516
|
gatewayPort,
|
|
548
517
|
imageTags: previousImageRefs,
|
|
549
518
|
instanceName,
|
|
@@ -727,7 +696,7 @@ async function upgradePlatform(
|
|
|
727
696
|
body.version = version;
|
|
728
697
|
}
|
|
729
698
|
|
|
730
|
-
const response = await
|
|
699
|
+
const response = await loopbackSafeFetch(url, {
|
|
731
700
|
method: "POST",
|
|
732
701
|
headers,
|
|
733
702
|
body: JSON.stringify(body),
|
package/src/commands/wake.ts
CHANGED
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
saveAssistantEntry,
|
|
8
8
|
} from "../lib/assistant-config.js";
|
|
9
9
|
import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
leaseGuardianToken,
|
|
12
|
+
loadGuardianToken,
|
|
13
|
+
resetGuardianBootstrap,
|
|
14
|
+
seedGuardianTokenFromSiblingEnv,
|
|
15
|
+
} from "../lib/guardian-token.js";
|
|
11
16
|
import { resolveProcessState, stopProcessByPidFile } from "../lib/process";
|
|
12
17
|
import {
|
|
13
18
|
generateLocalSigningKey,
|
|
@@ -37,11 +42,23 @@ export async function wake(): Promise<void> {
|
|
|
37
42
|
console.log(
|
|
38
43
|
" --foreground Run assistant in foreground with logs printed to terminal",
|
|
39
44
|
);
|
|
45
|
+
console.log(
|
|
46
|
+
" --repair-guardian Re-provision the guardian token if missing (resets the\n" +
|
|
47
|
+
" gateway bootstrap and re-leases — REVOKES other device-bound\n" +
|
|
48
|
+
" tokens, so only use deliberately, never from auto-repair)",
|
|
49
|
+
);
|
|
40
50
|
process.exit(0);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
const watch = args.includes("--watch");
|
|
44
54
|
const foreground = args.includes("--foreground");
|
|
55
|
+
// Re-leasing the guardian token calls guardian/init, which revokes every
|
|
56
|
+
// other device-bound token (other tabs, other local clients on this machine).
|
|
57
|
+
// Gate it behind an explicit flag so the automatic connect-repair path
|
|
58
|
+
// (`runWake` spawns `wake <id>` with no flags) can never revoke a live session
|
|
59
|
+
// — it only ever restarts + sibling-seeds. A genuine spent-bootstrap brick is
|
|
60
|
+
// recovered deliberately via `vellum wake <id> --repair-guardian`.
|
|
61
|
+
const repairGuardian = args.includes("--repair-guardian");
|
|
45
62
|
const nameArg = args.find((a) => !a.startsWith("-"));
|
|
46
63
|
const entry = resolveTargetAssistant(nameArg);
|
|
47
64
|
|
|
@@ -221,6 +238,37 @@ export async function wake(): Promise<void> {
|
|
|
221
238
|
console.log(" Seeded guardian token from sibling environment.");
|
|
222
239
|
}
|
|
223
240
|
|
|
241
|
+
// Last-resort recovery (explicit `--repair-guardian` only): if no guardian
|
|
242
|
+
// token exists for this env even after sibling seeding, re-provision one. The
|
|
243
|
+
// single-use bootstrap secret may already be spent — a prior connect can
|
|
244
|
+
// lease a token that's then lost, or the gateway marks the secret consumed
|
|
245
|
+
// before the client persists it — which otherwise bricks connect into a
|
|
246
|
+
// 401 → auth-rate-limit → 429 cascade with no path back short of retire+hatch.
|
|
247
|
+
// Reset the gateway's bootstrap lock+consumed state (loopback-only, authorized
|
|
248
|
+
// by the lockfile secret — mirrors the macOS client's forceReBootstrap), then
|
|
249
|
+
// re-lease. Gated behind the flag because the re-lease revokes other
|
|
250
|
+
// device-bound tokens; it must never run from the automatic repair path.
|
|
251
|
+
if (repairGuardian && !loadGuardianToken(entry.assistantId)) {
|
|
252
|
+
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
253
|
+
const maxAttempts = 3;
|
|
254
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
255
|
+
try {
|
|
256
|
+
await resetGuardianBootstrap(loopbackUrl, bootstrapSecret);
|
|
257
|
+
await leaseGuardianToken(loopbackUrl, entry.assistantId, bootstrapSecret);
|
|
258
|
+
console.log(" Re-provisioned guardian token.");
|
|
259
|
+
break;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (attempt < maxAttempts) {
|
|
262
|
+
await new Promise((r) => setTimeout(r, 2000 * 2 ** (attempt - 1)));
|
|
263
|
+
} else {
|
|
264
|
+
console.warn(
|
|
265
|
+
` Guardian token re-provision failed after ${maxAttempts} attempts: ${err}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
224
272
|
// Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
|
|
225
273
|
const workspaceDir = join(resources.instanceDir, ".vellum", "workspace");
|
|
226
274
|
const ngrokChild = await maybeStartNgrokTunnel(
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import cliPkg from "../package.json";
|
|
|
4
4
|
import { backup } from "./commands/backup";
|
|
5
5
|
import { clean } from "./commands/clean";
|
|
6
6
|
import { client } from "./commands/client";
|
|
7
|
+
import { confirm } from "./commands/confirm";
|
|
7
8
|
import { connect } from "./commands/connect";
|
|
8
9
|
import { devices } from "./commands/devices";
|
|
9
10
|
import { env } from "./commands/env";
|
|
@@ -40,6 +41,7 @@ const commands = {
|
|
|
40
41
|
backup,
|
|
41
42
|
clean,
|
|
42
43
|
client,
|
|
44
|
+
confirm,
|
|
43
45
|
connect,
|
|
44
46
|
devices,
|
|
45
47
|
env,
|
|
@@ -81,8 +83,13 @@ function printHelp(): void {
|
|
|
81
83
|
console.log(" backup Export a backup of a running assistant");
|
|
82
84
|
console.log(" clean Kill orphaned vellum processes");
|
|
83
85
|
console.log(" client Connect to a hatched assistant");
|
|
84
|
-
console.log("
|
|
85
|
-
console.log(
|
|
86
|
+
console.log(" confirm Resolve a pending tool confirmation on an assistant");
|
|
87
|
+
console.log(
|
|
88
|
+
" connect Import an assistant paired from another machine [beta]",
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
" devices List or revoke devices paired to a local assistant [beta]",
|
|
92
|
+
);
|
|
86
93
|
console.log(" env Manage the default CLI environment");
|
|
87
94
|
console.log(" events Stream events from a running assistant");
|
|
88
95
|
console.log(" exec Execute a command inside an assistant's container");
|
|
@@ -94,7 +101,7 @@ function printHelp(): void {
|
|
|
94
101
|
console.log(" logout Log out of the Vellum platform");
|
|
95
102
|
console.log(" message Send a message to a running assistant");
|
|
96
103
|
console.log(
|
|
97
|
-
" pair Mint a device-scoped token to connect another machine",
|
|
104
|
+
" pair Mint a device-scoped token to connect another machine [beta]",
|
|
98
105
|
);
|
|
99
106
|
console.log(
|
|
100
107
|
" ps List assistants (or processes for a specific assistant)",
|
|
@@ -113,7 +120,7 @@ function printHelp(): void {
|
|
|
113
120
|
console.log(" terminal Open a terminal into a managed assistant container");
|
|
114
121
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
115
122
|
console.log(
|
|
116
|
-
" unpair Forget a paired assistant imported from another machine",
|
|
123
|
+
" unpair Forget a paired assistant imported from another machine [beta]",
|
|
117
124
|
);
|
|
118
125
|
console.log(" upgrade Upgrade an assistant to a newer version");
|
|
119
126
|
console.log(" use Set the active assistant for commands");
|