@vellumai/cli 0.8.2 → 0.8.3
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 +24 -32
- package/package.json +1 -1
- package/src/__tests__/config-utils.test.ts +31 -1
- package/src/__tests__/hatch-provider-secrets.test.ts +284 -0
- package/src/__tests__/setup.test.ts +65 -1
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/commands/hatch.ts +53 -20
- package/src/commands/setup.ts +46 -12
- package/src/commands/teleport.ts +20 -2
- package/src/lib/__tests__/docker.test.ts +106 -0
- package/src/lib/assistant-config.ts +2 -0
- package/src/lib/config-utils.ts +18 -0
- package/src/lib/docker.ts +180 -19
- package/src/lib/hatch-local.ts +42 -3
- package/src/lib/hatch-next-steps.ts +12 -0
- package/src/lib/provider-secrets.ts +151 -0
- package/src/shared/provider-env-vars.ts +0 -3
package/src/commands/hatch.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
saveAssistantEntry,
|
|
9
9
|
setActiveAssistant,
|
|
10
10
|
} from "../lib/assistant-config";
|
|
11
|
-
import { hatchAws } from "../lib/aws";
|
|
12
11
|
import {
|
|
13
12
|
SPECIES_CONFIG,
|
|
14
13
|
VALID_REMOTE_HOSTS,
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
18
17
|
import { buildNestedConfig } from "../lib/config-utils";
|
|
19
18
|
import { hatchDocker } from "../lib/docker";
|
|
20
|
-
import { hatchGcp } from "../lib/gcp";
|
|
21
19
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
22
20
|
import { hatchLocal } from "../lib/hatch-local";
|
|
23
21
|
import {
|
|
@@ -169,6 +167,7 @@ source ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
const DEFAULT_REMOTE: RemoteHost = "local";
|
|
170
|
+
const UNSUPPORTED_REMOTE_HATCH_TARGETS = new Set<RemoteHost>(["aws", "gcp"]);
|
|
172
171
|
|
|
173
172
|
interface HatchArgs {
|
|
174
173
|
species: Species;
|
|
@@ -177,7 +176,9 @@ interface HatchArgs {
|
|
|
177
176
|
name: string | null;
|
|
178
177
|
remote: RemoteHost;
|
|
179
178
|
watch: boolean;
|
|
179
|
+
sourcePath: string | null;
|
|
180
180
|
configValues: Record<string, string>;
|
|
181
|
+
analyze: boolean;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
function parseArgs(): HatchArgs {
|
|
@@ -188,7 +189,9 @@ function parseArgs(): HatchArgs {
|
|
|
188
189
|
let name: string | null = null;
|
|
189
190
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
190
191
|
let watch = false;
|
|
192
|
+
let sourcePath: string | null = null;
|
|
191
193
|
const configValues: Record<string, string> = {};
|
|
194
|
+
let analyze = false;
|
|
192
195
|
|
|
193
196
|
for (let i = 0; i < args.length; i++) {
|
|
194
197
|
const arg = args[i];
|
|
@@ -210,17 +213,33 @@ function parseArgs(): HatchArgs {
|
|
|
210
213
|
console.log(
|
|
211
214
|
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
212
215
|
);
|
|
216
|
+
console.log(
|
|
217
|
+
" --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
|
|
218
|
+
);
|
|
213
219
|
console.log(
|
|
214
220
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
215
221
|
);
|
|
216
222
|
console.log(
|
|
217
223
|
" --config <key=value> Set a workspace config value (repeatable)",
|
|
218
224
|
);
|
|
225
|
+
console.log(
|
|
226
|
+
" --analyze Emit a structured hatch-timing log line on stdout",
|
|
227
|
+
);
|
|
219
228
|
process.exit(0);
|
|
220
229
|
} else if (arg === "-d") {
|
|
221
230
|
detached = true;
|
|
222
231
|
} else if (arg === "--watch") {
|
|
223
232
|
watch = true;
|
|
233
|
+
} else if (arg === "--analyze") {
|
|
234
|
+
analyze = true;
|
|
235
|
+
} else if (arg === "--source") {
|
|
236
|
+
const next = args[i + 1];
|
|
237
|
+
if (!next || next.startsWith("-")) {
|
|
238
|
+
console.error("Error: --source requires a path argument");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
sourcePath = next;
|
|
242
|
+
i++;
|
|
224
243
|
} else if (arg === "--keep-alive") {
|
|
225
244
|
keepAlive = true;
|
|
226
245
|
} else if (arg === "--name") {
|
|
@@ -270,7 +289,7 @@ function parseArgs(): HatchArgs {
|
|
|
270
289
|
species = arg as Species;
|
|
271
290
|
} else {
|
|
272
291
|
console.error(
|
|
273
|
-
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value
|
|
292
|
+
`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>, --analyze`,
|
|
274
293
|
);
|
|
275
294
|
process.exit(1);
|
|
276
295
|
}
|
|
@@ -283,7 +302,9 @@ function parseArgs(): HatchArgs {
|
|
|
283
302
|
name,
|
|
284
303
|
remote,
|
|
285
304
|
watch,
|
|
305
|
+
sourcePath,
|
|
286
306
|
configValues,
|
|
307
|
+
analyze,
|
|
287
308
|
};
|
|
288
309
|
}
|
|
289
310
|
|
|
@@ -508,8 +529,17 @@ export async function hatch(): Promise<void> {
|
|
|
508
529
|
const cliVersion = getCliVersion();
|
|
509
530
|
console.log(`@vellumai/cli v${cliVersion}`);
|
|
510
531
|
|
|
511
|
-
const {
|
|
512
|
-
|
|
532
|
+
const {
|
|
533
|
+
species,
|
|
534
|
+
detached,
|
|
535
|
+
keepAlive,
|
|
536
|
+
name,
|
|
537
|
+
remote,
|
|
538
|
+
watch,
|
|
539
|
+
sourcePath,
|
|
540
|
+
configValues,
|
|
541
|
+
analyze,
|
|
542
|
+
} = parseArgs();
|
|
513
543
|
|
|
514
544
|
if (watch && remote !== "local" && remote !== "docker") {
|
|
515
545
|
console.error(
|
|
@@ -518,30 +548,33 @@ export async function hatch(): Promise<void> {
|
|
|
518
548
|
process.exit(1);
|
|
519
549
|
}
|
|
520
550
|
|
|
521
|
-
if (remote
|
|
522
|
-
|
|
523
|
-
|
|
551
|
+
if (sourcePath !== null && remote !== "docker") {
|
|
552
|
+
console.error(
|
|
553
|
+
"Error: --source is only supported for docker hatch targets.",
|
|
554
|
+
);
|
|
555
|
+
process.exit(1);
|
|
524
556
|
}
|
|
525
557
|
|
|
526
|
-
if (remote
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
detached,
|
|
530
|
-
name,
|
|
531
|
-
buildStartupScript,
|
|
532
|
-
watchHatching,
|
|
533
|
-
configValues,
|
|
558
|
+
if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
|
|
559
|
+
console.error(
|
|
560
|
+
`Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
|
|
534
561
|
);
|
|
535
|
-
|
|
562
|
+
console.error(
|
|
563
|
+
"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.",
|
|
564
|
+
);
|
|
565
|
+
process.exit(1);
|
|
536
566
|
}
|
|
537
567
|
|
|
538
|
-
if (remote === "
|
|
539
|
-
await
|
|
568
|
+
if (remote === "local") {
|
|
569
|
+
await hatchLocal(species, name, watch, keepAlive, configValues);
|
|
540
570
|
return;
|
|
541
571
|
}
|
|
542
572
|
|
|
543
573
|
if (remote === "docker") {
|
|
544
|
-
await hatchDocker(species, detached, name, watch, configValues
|
|
574
|
+
await hatchDocker(species, detached, name, watch, configValues, {
|
|
575
|
+
sourcePath,
|
|
576
|
+
analyze,
|
|
577
|
+
});
|
|
545
578
|
return;
|
|
546
579
|
}
|
|
547
580
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveAssistant } from "../lib/assistant-config.js";
|
|
2
2
|
import {
|
|
3
|
+
leaseGuardianToken,
|
|
3
4
|
loadGuardianToken,
|
|
4
5
|
refreshGuardianToken,
|
|
5
6
|
type GuardianTokenData,
|
|
@@ -41,6 +42,50 @@ function isGuardianAccessTokenUsable(
|
|
|
41
42
|
return Number.isFinite(expiresAt) && expiresAt > Date.now();
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
async function resolveSetupBearerToken(
|
|
46
|
+
entry: NonNullable<ReturnType<typeof resolveAssistant>>,
|
|
47
|
+
gatewayUrl: string,
|
|
48
|
+
): Promise<string | undefined> {
|
|
49
|
+
const guardianToken = loadGuardianToken(entry.assistantId);
|
|
50
|
+
if (isGuardianAccessTokenUsable(guardianToken)) {
|
|
51
|
+
return guardianToken.accessToken;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (guardianToken) {
|
|
55
|
+
const refreshedToken = await refreshGuardianToken(
|
|
56
|
+
gatewayUrl,
|
|
57
|
+
entry.assistantId,
|
|
58
|
+
);
|
|
59
|
+
if (isGuardianAccessTokenUsable(refreshedToken)) {
|
|
60
|
+
return refreshedToken.accessToken;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const canLeaseGuardianToken =
|
|
65
|
+
entry.cloud === "local" || entry.cloud === "docker" || entry.localUrl;
|
|
66
|
+
if (canLeaseGuardianToken) {
|
|
67
|
+
try {
|
|
68
|
+
const bootstrapSecret =
|
|
69
|
+
typeof entry.guardianBootstrapSecret === "string"
|
|
70
|
+
? entry.guardianBootstrapSecret
|
|
71
|
+
: undefined;
|
|
72
|
+
const leasedToken = await leaseGuardianToken(
|
|
73
|
+
gatewayUrl,
|
|
74
|
+
entry.assistantId,
|
|
75
|
+
bootstrapSecret,
|
|
76
|
+
);
|
|
77
|
+
if (isGuardianAccessTokenUsable(leasedToken)) {
|
|
78
|
+
return leasedToken.accessToken;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Fall through to any lockfile bearer token, or let the setup request
|
|
82
|
+
// surface the gateway's auth error below.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return entry.bearerToken;
|
|
87
|
+
}
|
|
88
|
+
|
|
44
89
|
export async function setup(): Promise<void> {
|
|
45
90
|
const args = process.argv.slice(3);
|
|
46
91
|
|
|
@@ -85,18 +130,7 @@ export async function setup(): Promise<void> {
|
|
|
85
130
|
}
|
|
86
131
|
|
|
87
132
|
const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
|
|
88
|
-
|
|
89
|
-
const guardianToken = loadGuardianToken(entry.assistantId);
|
|
90
|
-
if (isGuardianAccessTokenUsable(guardianToken)) {
|
|
91
|
-
bearerToken = guardianToken.accessToken;
|
|
92
|
-
} else {
|
|
93
|
-
const refreshedToken = guardianToken
|
|
94
|
-
? await refreshGuardianToken(gatewayUrl, entry.assistantId)
|
|
95
|
-
: null;
|
|
96
|
-
bearerToken = isGuardianAccessTokenUsable(refreshedToken)
|
|
97
|
-
? refreshedToken.accessToken
|
|
98
|
-
: entry.bearerToken;
|
|
99
|
-
}
|
|
133
|
+
const bearerToken = await resolveSetupBearerToken(entry, gatewayUrl);
|
|
100
134
|
|
|
101
135
|
console.log("Vellum Setup");
|
|
102
136
|
console.log("============\n");
|
package/src/commands/teleport.ts
CHANGED
|
@@ -877,7 +877,16 @@ export async function resolveOrHatchTarget(
|
|
|
877
877
|
// Hatch a new assistant in the target environment
|
|
878
878
|
if (targetEnv === "local") {
|
|
879
879
|
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
880
|
-
await hatchLocal(
|
|
880
|
+
await hatchLocal(
|
|
881
|
+
"vellum",
|
|
882
|
+
targetName ?? null,
|
|
883
|
+
false,
|
|
884
|
+
false,
|
|
885
|
+
{},
|
|
886
|
+
{
|
|
887
|
+
setupProviderCredentials: false,
|
|
888
|
+
},
|
|
889
|
+
);
|
|
881
890
|
const entry = targetName
|
|
882
891
|
? findAssistantByName(targetName)
|
|
883
892
|
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
@@ -892,7 +901,16 @@ export async function resolveOrHatchTarget(
|
|
|
892
901
|
|
|
893
902
|
if (targetEnv === "docker") {
|
|
894
903
|
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
895
|
-
await hatchDocker(
|
|
904
|
+
await hatchDocker(
|
|
905
|
+
"vellum",
|
|
906
|
+
false,
|
|
907
|
+
targetName ?? null,
|
|
908
|
+
false,
|
|
909
|
+
{},
|
|
910
|
+
{
|
|
911
|
+
setupProviderCredentials: false,
|
|
912
|
+
},
|
|
913
|
+
);
|
|
896
914
|
const entry = targetName
|
|
897
915
|
? findAssistantByName(targetName)
|
|
898
916
|
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
AVATAR_DEVICE_ENV_VAR,
|
|
5
5
|
dockerResourceNames,
|
|
6
6
|
resolveAvatarDevicePath,
|
|
7
|
+
resolveDockerHatchMode,
|
|
8
|
+
resolveDockerProviderCredentialSetupAction,
|
|
7
9
|
type ServiceName,
|
|
8
10
|
} from "../docker.js";
|
|
9
11
|
import { buildServiceRunArgs } from "../statefulset.js";
|
|
@@ -103,6 +105,51 @@ describe("buildServiceRunArgs — assistant", () => {
|
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
107
|
|
|
108
|
+
describe("resolveDockerProviderCredentialSetupAction", () => {
|
|
109
|
+
test("defers provider setup in detached mode", () => {
|
|
110
|
+
expect(
|
|
111
|
+
resolveDockerProviderCredentialSetupAction({
|
|
112
|
+
provider: "anthropic",
|
|
113
|
+
detached: true,
|
|
114
|
+
}),
|
|
115
|
+
).toBe("defer");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("reports missing guardian token only when a lease was expected", () => {
|
|
119
|
+
expect(
|
|
120
|
+
resolveDockerProviderCredentialSetupAction({
|
|
121
|
+
provider: "anthropic",
|
|
122
|
+
detached: false,
|
|
123
|
+
}),
|
|
124
|
+
).toBe("missing-token");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("configures provider setup when a guardian token is available", () => {
|
|
128
|
+
expect(
|
|
129
|
+
resolveDockerProviderCredentialSetupAction({
|
|
130
|
+
provider: "anthropic",
|
|
131
|
+
guardianAccessToken: "guardian-token",
|
|
132
|
+
detached: false,
|
|
133
|
+
}),
|
|
134
|
+
).toBe("configure");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("skips provider setup for internal hatches and detached keyless hatches", () => {
|
|
138
|
+
expect(
|
|
139
|
+
resolveDockerProviderCredentialSetupAction({
|
|
140
|
+
provider: undefined,
|
|
141
|
+
detached: false,
|
|
142
|
+
}),
|
|
143
|
+
).toBe("skip");
|
|
144
|
+
expect(
|
|
145
|
+
resolveDockerProviderCredentialSetupAction({
|
|
146
|
+
provider: null,
|
|
147
|
+
detached: true,
|
|
148
|
+
}),
|
|
149
|
+
).toBe("skip");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
106
153
|
describe("buildServiceRunArgs — gateway", () => {
|
|
107
154
|
const savedVelayBaseUrl = process.env.VELAY_BASE_URL;
|
|
108
155
|
|
|
@@ -171,3 +218,62 @@ describe("VELLUM_AVATAR_DEVICE passthrough", () => {
|
|
|
171
218
|
);
|
|
172
219
|
});
|
|
173
220
|
});
|
|
221
|
+
|
|
222
|
+
describe("resolveDockerHatchMode", () => {
|
|
223
|
+
test("defaults to pulling published images when no source flag is set", () => {
|
|
224
|
+
expect(
|
|
225
|
+
resolveDockerHatchMode({
|
|
226
|
+
watch: false,
|
|
227
|
+
buildFromSource: false,
|
|
228
|
+
fullSourceTreeAvailable: true,
|
|
229
|
+
}),
|
|
230
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: false });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("--source <path> builds without enabling the file watcher", () => {
|
|
234
|
+
expect(
|
|
235
|
+
resolveDockerHatchMode({
|
|
236
|
+
watch: false,
|
|
237
|
+
buildFromSource: true,
|
|
238
|
+
fullSourceTreeAvailable: true,
|
|
239
|
+
}),
|
|
240
|
+
).toEqual({ build: true, watcher: false, fellBackToPull: false });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("--watch builds and enables the file watcher", () => {
|
|
244
|
+
expect(
|
|
245
|
+
resolveDockerHatchMode({
|
|
246
|
+
watch: true,
|
|
247
|
+
buildFromSource: false,
|
|
248
|
+
fullSourceTreeAvailable: true,
|
|
249
|
+
}),
|
|
250
|
+
).toEqual({ build: true, watcher: true, fellBackToPull: false });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("--watch + --source <path> still enables the watcher (watch wins)", () => {
|
|
254
|
+
expect(
|
|
255
|
+
resolveDockerHatchMode({
|
|
256
|
+
watch: true,
|
|
257
|
+
buildFromSource: true,
|
|
258
|
+
fullSourceTreeAvailable: true,
|
|
259
|
+
}),
|
|
260
|
+
).toEqual({ build: true, watcher: true, fellBackToPull: false });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("falls back to pull when source flag is set but source tree is missing", () => {
|
|
264
|
+
expect(
|
|
265
|
+
resolveDockerHatchMode({
|
|
266
|
+
watch: false,
|
|
267
|
+
buildFromSource: true,
|
|
268
|
+
fullSourceTreeAvailable: false,
|
|
269
|
+
}),
|
|
270
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: true });
|
|
271
|
+
expect(
|
|
272
|
+
resolveDockerHatchMode({
|
|
273
|
+
watch: true,
|
|
274
|
+
buildFromSource: false,
|
|
275
|
+
fullSourceTreeAvailable: false,
|
|
276
|
+
}),
|
|
277
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: true });
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -89,6 +89,8 @@ export interface AssistantEntry {
|
|
|
89
89
|
resources?: LocalInstanceResources;
|
|
90
90
|
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
91
91
|
watcherPid?: number;
|
|
92
|
+
/** Local bootstrap secret used to lease guardian tokens for Docker assistants after detached hatch. */
|
|
93
|
+
guardianBootstrapSecret?: string;
|
|
92
94
|
/** Docker image metadata for rollback. Only present for docker topology entries. */
|
|
93
95
|
containerInfo?: ContainerInfo;
|
|
94
96
|
/** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
|
package/src/lib/config-utils.ts
CHANGED
|
@@ -32,6 +32,24 @@ export function buildNestedConfig(
|
|
|
32
32
|
return config;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Ensure hatch always provides enough initial LLM config for the assistant to
|
|
37
|
+
* detect a fresh off-platform hatch and seed BYOK profiles.
|
|
38
|
+
*/
|
|
39
|
+
export function buildHatchConfigValues(
|
|
40
|
+
configValues: Record<string, string>,
|
|
41
|
+
provider: string | null | undefined,
|
|
42
|
+
): Record<string, string> {
|
|
43
|
+
if (!provider || configValues["llm.default.provider"]) {
|
|
44
|
+
return configValues;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...configValues,
|
|
49
|
+
"llm.default.provider": provider,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Write arbitrary key-value pairs to a temporary JSON file and return its
|
|
37
55
|
* path. The caller passes this path to the daemon via the
|