@vellumai/cli 0.8.2 → 0.8.4
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 +18 -6
- package/README.md +24 -32
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +108 -0
- package/src/__tests__/assistant-target-args.test.ts +30 -0
- package/src/__tests__/config-utils.test.ts +31 -1
- package/src/__tests__/hatch-provider-secrets.test.ts +284 -0
- package/src/__tests__/host-image-loader.test.ts +206 -0
- package/src/__tests__/ps-platform-status.test.ts +100 -22
- package/src/__tests__/setup.test.ts +65 -1
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/__tests__/use.test.ts +144 -0
- package/src/commands/client.ts +27 -24
- package/src/commands/hatch.ts +53 -20
- package/src/commands/ps.ts +107 -105
- package/src/commands/setup.ts +46 -12
- package/src/commands/teleport.ts +20 -2
- package/src/commands/use.ts +24 -10
- package/src/components/DefaultMainScreen.tsx +27 -115
- package/src/lib/__tests__/docker.test.ts +106 -0
- package/src/lib/assistant-config.ts +86 -5
- package/src/lib/assistant-target-args.ts +21 -0
- package/src/lib/config-utils.ts +18 -0
- package/src/lib/docker.ts +225 -27
- package/src/lib/hatch-local.ts +42 -3
- package/src/lib/hatch-next-steps.ts +12 -0
- package/src/lib/host-image-loader.ts +138 -0
- package/src/lib/platform-releases.ts +12 -5
- package/src/lib/provider-secrets.ts +151 -0
- package/src/shared/provider-env-vars.ts +0 -3
package/src/lib/docker.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, watch as fsWatch } from "fs";
|
|
3
3
|
import { arch, platform } from "os";
|
|
4
|
-
import { dirname, join } from "path";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
5
|
|
|
6
6
|
// Direct import — bun embeds this at compile time so it works in compiled binaries.
|
|
7
7
|
import cliPkg from "../../package.json";
|
|
@@ -12,15 +12,29 @@ import {
|
|
|
12
12
|
setActiveAssistant,
|
|
13
13
|
} from "./assistant-config";
|
|
14
14
|
import type { AssistantEntry } from "./assistant-config";
|
|
15
|
-
import { writeInitialConfig } from "./config-utils";
|
|
15
|
+
import { buildHatchConfigValues, writeInitialConfig } from "./config-utils";
|
|
16
16
|
import { buildServiceRunArgs } from "./statefulset.js";
|
|
17
17
|
import type { Species } from "./constants";
|
|
18
18
|
import { getDefaultPorts } from "./environments/paths.js";
|
|
19
19
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
20
20
|
import { leaseGuardianToken } from "./guardian-token";
|
|
21
|
+
import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
21
22
|
import { isVellumProcess, stopProcess } from "./process";
|
|
22
23
|
import { generateInstanceName } from "./random-name";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
HOST_IMAGE_LOADER_URL,
|
|
26
|
+
isLocalBuildRef,
|
|
27
|
+
loadImageViaHost,
|
|
28
|
+
} from "./host-image-loader.js";
|
|
29
|
+
import {
|
|
30
|
+
fetchLatestStableVersion,
|
|
31
|
+
resolveImageRefs,
|
|
32
|
+
} from "./platform-releases.js";
|
|
33
|
+
import {
|
|
34
|
+
configureHatchProviderApiKey,
|
|
35
|
+
formatProviderName,
|
|
36
|
+
resolveHatchProvider,
|
|
37
|
+
} from "./provider-secrets.js";
|
|
24
38
|
import { exec, execOutput } from "./step-runner";
|
|
25
39
|
import {
|
|
26
40
|
closeLogFile,
|
|
@@ -40,10 +54,13 @@ export const DOCKERHUB_IMAGES: Record<ServiceName, string> = {
|
|
|
40
54
|
};
|
|
41
55
|
|
|
42
56
|
/** Internal ports exposed by each service's Dockerfile. Re-exported from environments/paths.ts. */
|
|
43
|
-
export {
|
|
57
|
+
export {
|
|
58
|
+
ASSISTANT_INTERNAL_PORT,
|
|
59
|
+
GATEWAY_INTERNAL_PORT,
|
|
60
|
+
} from "./environments/paths.js";
|
|
44
61
|
|
|
45
62
|
/** Max time to wait for the assistant container to emit the readiness sentinel. */
|
|
46
|
-
export const DOCKER_READY_TIMEOUT_MS =
|
|
63
|
+
export const DOCKER_READY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
47
64
|
|
|
48
65
|
/** Default virtual-camera device path. Overridable via `VELLUM_AVATAR_DEVICE`. */
|
|
49
66
|
const DEFAULT_AVATAR_DEVICE_PATH = "/dev/video10";
|
|
@@ -247,6 +264,28 @@ function ensureLocalBinOnPath(): void {
|
|
|
247
264
|
}
|
|
248
265
|
}
|
|
249
266
|
|
|
267
|
+
export interface HatchDockerOptions {
|
|
268
|
+
setupProviderCredentials?: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type DockerProviderCredentialSetupAction =
|
|
272
|
+
| "configure"
|
|
273
|
+
| "defer"
|
|
274
|
+
| "missing-token"
|
|
275
|
+
| "skip";
|
|
276
|
+
|
|
277
|
+
export function resolveDockerProviderCredentialSetupAction(options: {
|
|
278
|
+
provider: string | null | undefined;
|
|
279
|
+
guardianAccessToken?: string;
|
|
280
|
+
detached: boolean;
|
|
281
|
+
}): DockerProviderCredentialSetupAction {
|
|
282
|
+
if (options.provider === undefined) return "skip";
|
|
283
|
+
if (options.provider === null) return options.detached ? "skip" : "configure";
|
|
284
|
+
if (options.detached) return "defer";
|
|
285
|
+
if (!options.guardianAccessToken) return "missing-token";
|
|
286
|
+
return "configure";
|
|
287
|
+
}
|
|
288
|
+
|
|
250
289
|
/**
|
|
251
290
|
* Checks whether the `docker` CLI and daemon are available on the system.
|
|
252
291
|
* Installs Colima and Docker via direct binary download if missing (no sudo
|
|
@@ -461,6 +500,37 @@ function hasFullSourceTree(root: string): boolean {
|
|
|
461
500
|
return existsSync(join(root, "assistant", "package.json"));
|
|
462
501
|
}
|
|
463
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Decide which image-source path `hatchDocker` should take given the user
|
|
505
|
+
* flags and a probe result for the source tree.
|
|
506
|
+
*
|
|
507
|
+
* - `watch` always wants source-build *and* file-watcher (when the source
|
|
508
|
+
* tree is available).
|
|
509
|
+
* - `buildFromSource` wants source-build but no watcher — used by evals so
|
|
510
|
+
* each run picks up fresh CLI changes from the repo.
|
|
511
|
+
* - Without either flag we pull the published images.
|
|
512
|
+
* - If either flag was set but the source tree is missing (e.g. the CLI is
|
|
513
|
+
* running from a packaged .app bundle), fall back to pulling and surface
|
|
514
|
+
* the reason so the caller can log a warning.
|
|
515
|
+
*
|
|
516
|
+
* Returning a plain record keeps this trivially unit-testable — see
|
|
517
|
+
* `__tests__/docker.test.ts`.
|
|
518
|
+
*/
|
|
519
|
+
export function resolveDockerHatchMode(opts: {
|
|
520
|
+
watch: boolean;
|
|
521
|
+
buildFromSource: boolean;
|
|
522
|
+
fullSourceTreeAvailable: boolean;
|
|
523
|
+
}): { build: boolean; watcher: boolean; fellBackToPull: boolean } {
|
|
524
|
+
const requested = opts.watch || opts.buildFromSource;
|
|
525
|
+
if (!requested) {
|
|
526
|
+
return { build: false, watcher: false, fellBackToPull: false };
|
|
527
|
+
}
|
|
528
|
+
if (!opts.fullSourceTreeAvailable) {
|
|
529
|
+
return { build: false, watcher: false, fellBackToPull: true };
|
|
530
|
+
}
|
|
531
|
+
return { build: true, watcher: opts.watch, fellBackToPull: false };
|
|
532
|
+
}
|
|
533
|
+
|
|
464
534
|
/**
|
|
465
535
|
* Locate the repository root by walking up from `cli/src/lib/` until we
|
|
466
536
|
* find a directory containing the expected Dockerfiles.
|
|
@@ -581,7 +651,10 @@ export async function startContainers(
|
|
|
581
651
|
},
|
|
582
652
|
log: (msg: string) => void,
|
|
583
653
|
): Promise<void> {
|
|
584
|
-
const runArgs = buildServiceRunArgs({
|
|
654
|
+
const runArgs = buildServiceRunArgs({
|
|
655
|
+
...opts,
|
|
656
|
+
avatarDevicePath: resolveAvatarDevicePath(),
|
|
657
|
+
});
|
|
585
658
|
for (const service of SERVICE_START_ORDER) {
|
|
586
659
|
log(`🚀 Starting ${service} container...`);
|
|
587
660
|
await exec("docker", runArgs[service]());
|
|
@@ -870,14 +943,32 @@ function startFileWatcher(opts: {
|
|
|
870
943
|
};
|
|
871
944
|
}
|
|
872
945
|
|
|
946
|
+
export interface HatchDockerOptions {
|
|
947
|
+
/**
|
|
948
|
+
* Path to a local source tree to build images from before hatching. When
|
|
949
|
+
* provided, this path is used directly as the repo root and no file
|
|
950
|
+
* watcher is started — useful for callers (e.g. evals) that want each
|
|
951
|
+
* run to pick up local CLI changes without keeping a long-lived watcher
|
|
952
|
+
* process around. `--watch` independently auto-detects the repo root and
|
|
953
|
+
* also enables hot-reload.
|
|
954
|
+
*/
|
|
955
|
+
sourcePath?: string | null;
|
|
956
|
+
analyze?: boolean;
|
|
957
|
+
}
|
|
958
|
+
|
|
873
959
|
export async function hatchDocker(
|
|
874
960
|
species: Species,
|
|
875
961
|
detached: boolean,
|
|
876
962
|
name: string | null,
|
|
877
963
|
watch: boolean = false,
|
|
878
964
|
configValues: Record<string, string> = {},
|
|
965
|
+
options: HatchDockerOptions = {},
|
|
879
966
|
): Promise<void> {
|
|
880
967
|
resetLogFile("hatch.log");
|
|
968
|
+
const provider =
|
|
969
|
+
options.setupProviderCredentials === false
|
|
970
|
+
? undefined
|
|
971
|
+
: resolveHatchProvider(configValues);
|
|
881
972
|
|
|
882
973
|
let logFd = openLogFile("hatch.log");
|
|
883
974
|
const log = (msg: string): void => {
|
|
@@ -898,25 +989,42 @@ export async function hatchDocker(
|
|
|
898
989
|
gateway: "",
|
|
899
990
|
};
|
|
900
991
|
|
|
992
|
+
const sourcePath =
|
|
993
|
+
typeof options.sourcePath === "string" && options.sourcePath.length > 0
|
|
994
|
+
? options.sourcePath
|
|
995
|
+
: null;
|
|
996
|
+
const buildFromSource = sourcePath !== null;
|
|
901
997
|
let repoRoot: string | undefined;
|
|
998
|
+
let fullSourceTreeAvailable = false;
|
|
902
999
|
|
|
903
|
-
if (watch) {
|
|
904
|
-
|
|
1000
|
+
if (watch || buildFromSource) {
|
|
1001
|
+
// When --source <path> is supplied, trust the caller and use it
|
|
1002
|
+
// directly. Otherwise (the --watch case) walk up from known locations
|
|
1003
|
+
// to find the repo root.
|
|
1004
|
+
repoRoot = sourcePath ? resolve(sourcePath) : findRepoRoot();
|
|
905
1005
|
|
|
906
1006
|
// When running from a packaged .app bundle, the Dockerfiles are
|
|
907
1007
|
// present (so findRepoRoot succeeds) but the full source tree is
|
|
908
1008
|
// not — we can't build images locally. Fall back to pulling
|
|
909
1009
|
// pre-built images instead.
|
|
910
|
-
|
|
1010
|
+
fullSourceTreeAvailable = hasFullSourceTree(repoRoot);
|
|
1011
|
+
if (!fullSourceTreeAvailable) {
|
|
911
1012
|
log(
|
|
912
1013
|
"⚠️ Dockerfiles found but no source tree — falling back to image pull",
|
|
913
1014
|
);
|
|
914
|
-
watch = false;
|
|
915
1015
|
repoRoot = undefined;
|
|
916
1016
|
}
|
|
917
1017
|
}
|
|
918
1018
|
|
|
919
|
-
|
|
1019
|
+
const mode = resolveDockerHatchMode({
|
|
1020
|
+
watch,
|
|
1021
|
+
buildFromSource,
|
|
1022
|
+
fullSourceTreeAvailable,
|
|
1023
|
+
});
|
|
1024
|
+
// Honour the resolved mode for the rest of the flow.
|
|
1025
|
+
watch = mode.watcher;
|
|
1026
|
+
|
|
1027
|
+
if (mode.build && repoRoot) {
|
|
920
1028
|
emitProgress(2, 6, "Building images...");
|
|
921
1029
|
const localTag = `local-${instanceName}`;
|
|
922
1030
|
imageTags.assistant = `vellum-assistant:${localTag}`;
|
|
@@ -926,7 +1034,9 @@ export async function hatchDocker(
|
|
|
926
1034
|
|
|
927
1035
|
log(`🥚 Hatching Docker assistant: ${instanceName}`);
|
|
928
1036
|
log(` Species: ${species}`);
|
|
929
|
-
log(
|
|
1037
|
+
log(
|
|
1038
|
+
` Mode: ${mode.watcher ? "development (watch)" : "build-from-source"}`,
|
|
1039
|
+
);
|
|
930
1040
|
log(` Repo: ${repoRoot}`);
|
|
931
1041
|
log(` Images (local build):`);
|
|
932
1042
|
log(` assistant: ${imageTags.assistant}`);
|
|
@@ -938,7 +1048,7 @@ export async function hatchDocker(
|
|
|
938
1048
|
log("✅ Docker images built");
|
|
939
1049
|
}
|
|
940
1050
|
|
|
941
|
-
if (!
|
|
1051
|
+
if (!mode.build || !repoRoot) {
|
|
942
1052
|
emitProgress(2, 6, "Pulling images...");
|
|
943
1053
|
|
|
944
1054
|
// Allow explicit image overrides via environment variables.
|
|
@@ -957,8 +1067,23 @@ export async function hatchDocker(
|
|
|
957
1067
|
imageSource = "env override";
|
|
958
1068
|
log("Using image overrides from environment variables");
|
|
959
1069
|
} else {
|
|
960
|
-
|
|
961
|
-
|
|
1070
|
+
// Resolve image refs from a remote source that may have dev/local
|
|
1071
|
+
// builds. If resolution is unavailable, fall back to the CLI's own
|
|
1072
|
+
// version so a default tag can still be resolved.
|
|
1073
|
+
log("🔍 Fetching latest stable release...");
|
|
1074
|
+
const latestVersion = await fetchLatestStableVersion();
|
|
1075
|
+
let versionTag: string;
|
|
1076
|
+
if (latestVersion) {
|
|
1077
|
+
versionTag = latestVersion.startsWith("v")
|
|
1078
|
+
? latestVersion
|
|
1079
|
+
: `v${latestVersion}`;
|
|
1080
|
+
} else {
|
|
1081
|
+
const fallback = cliPkg.version;
|
|
1082
|
+
versionTag = fallback ? `v${fallback}` : "latest";
|
|
1083
|
+
log(
|
|
1084
|
+
`⚠️ Platform releases unavailable; falling back to CLI version ${versionTag}`,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
962
1087
|
log("🔍 Resolving image references...");
|
|
963
1088
|
const resolved = await resolveImageRefs(versionTag, log);
|
|
964
1089
|
imageTags.assistant = resolved.imageTags.assistant;
|
|
@@ -976,11 +1101,25 @@ export async function hatchDocker(
|
|
|
976
1101
|
log(` credential-executor: ${imageTags["credential-executor"]}`);
|
|
977
1102
|
log("");
|
|
978
1103
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1104
|
+
// Per-ref branching: local-build refs need the image-loader; external
|
|
1105
|
+
// registry refs get a normal `docker pull`. The two transports compose
|
|
1106
|
+
// cleanly — a release can mix different sources for different images.
|
|
1107
|
+
log("📦 Acquiring Docker images...");
|
|
1108
|
+
for (const service of [
|
|
1109
|
+
"assistant",
|
|
1110
|
+
"gateway",
|
|
1111
|
+
"credential-executor",
|
|
1112
|
+
] as const) {
|
|
1113
|
+
const ref = imageTags[service];
|
|
1114
|
+
if (isLocalBuildRef(ref)) {
|
|
1115
|
+
log(` ↪ loading ${ref} via host image-loader`);
|
|
1116
|
+
await loadImageViaHost(HOST_IMAGE_LOADER_URL, ref, log);
|
|
1117
|
+
} else {
|
|
1118
|
+
log(` ↪ pulling ${ref}`);
|
|
1119
|
+
await exec("docker", ["pull", ref]);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
log("✅ Docker images acquired");
|
|
984
1123
|
}
|
|
985
1124
|
|
|
986
1125
|
const res = dockerResourceNames(instanceName);
|
|
@@ -1013,7 +1152,8 @@ export async function hatchDocker(
|
|
|
1013
1152
|
|
|
1014
1153
|
// Write --config key=value pairs to a temp file that gets bind-mounted
|
|
1015
1154
|
// into the assistant container and read via VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH.
|
|
1016
|
-
const
|
|
1155
|
+
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
1156
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
1017
1157
|
|
|
1018
1158
|
const cesServiceToken = randomBytes(32).toString("hex");
|
|
1019
1159
|
const signingKey = randomBytes(32).toString("hex");
|
|
@@ -1043,6 +1183,7 @@ export async function hatchDocker(
|
|
|
1043
1183
|
},
|
|
1044
1184
|
log,
|
|
1045
1185
|
);
|
|
1186
|
+
const containersUpAt = Date.now();
|
|
1046
1187
|
|
|
1047
1188
|
const imageDigests = await captureImageRefs(res);
|
|
1048
1189
|
|
|
@@ -1053,6 +1194,7 @@ export async function hatchDocker(
|
|
|
1053
1194
|
cloud: "docker",
|
|
1054
1195
|
species,
|
|
1055
1196
|
hatchedAt: new Date().toISOString(),
|
|
1197
|
+
guardianBootstrapSecret: ownSecret,
|
|
1056
1198
|
containerInfo: {
|
|
1057
1199
|
assistantImage: imageTags.assistant,
|
|
1058
1200
|
gatewayImage: imageTags.gateway,
|
|
@@ -1068,19 +1210,59 @@ export async function hatchDocker(
|
|
|
1068
1210
|
setActiveAssistant(instanceName);
|
|
1069
1211
|
|
|
1070
1212
|
emitProgress(6, 6, "Waiting for services...");
|
|
1071
|
-
const
|
|
1213
|
+
const waitDetached = watch ? false : detached;
|
|
1214
|
+
const { ready, guardianAccessToken } = await waitForGatewayAndLease({
|
|
1072
1215
|
bootstrapSecret: ownSecret,
|
|
1073
1216
|
containerName: res.assistantContainer,
|
|
1074
|
-
detached:
|
|
1217
|
+
detached: waitDetached,
|
|
1075
1218
|
instanceName,
|
|
1076
1219
|
logFd,
|
|
1077
1220
|
runtimeUrl,
|
|
1221
|
+
containersUpAt,
|
|
1222
|
+
analyze: options.analyze ?? false,
|
|
1078
1223
|
});
|
|
1079
1224
|
|
|
1080
1225
|
if (!ready && !(watch && repoRoot)) {
|
|
1081
1226
|
throw new Error("Timed out waiting for assistant to become ready");
|
|
1082
1227
|
}
|
|
1083
1228
|
|
|
1229
|
+
if (ready) {
|
|
1230
|
+
const providerSetupAction = resolveDockerProviderCredentialSetupAction({
|
|
1231
|
+
provider,
|
|
1232
|
+
guardianAccessToken,
|
|
1233
|
+
detached: waitDetached,
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
if (providerSetupAction === "defer" && provider !== null) {
|
|
1237
|
+
log(
|
|
1238
|
+
`Provider credential setup deferred in detached mode.\n` +
|
|
1239
|
+
`Run \`vellum setup --provider ${provider}\` after the assistant is ready.`,
|
|
1240
|
+
);
|
|
1241
|
+
} else if (providerSetupAction === "missing-token" && provider !== null) {
|
|
1242
|
+
log(
|
|
1243
|
+
`⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
|
|
1244
|
+
` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
|
|
1245
|
+
);
|
|
1246
|
+
} else if (
|
|
1247
|
+
providerSetupAction === "configure" &&
|
|
1248
|
+
provider !== undefined
|
|
1249
|
+
) {
|
|
1250
|
+
log(
|
|
1251
|
+
provider === null
|
|
1252
|
+
? "Checking provider credentials..."
|
|
1253
|
+
: `Checking ${formatProviderName(provider)} credentials...`,
|
|
1254
|
+
);
|
|
1255
|
+
await configureHatchProviderApiKey({
|
|
1256
|
+
gatewayUrl: runtimeUrl,
|
|
1257
|
+
provider,
|
|
1258
|
+
bearerToken: guardianAccessToken,
|
|
1259
|
+
env: process.env,
|
|
1260
|
+
log,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
logHatchNextSteps(log, instanceName);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1084
1266
|
if (watch && repoRoot) {
|
|
1085
1267
|
saveAssistantEntry({ ...dockerEntry, watcherPid: process.pid });
|
|
1086
1268
|
|
|
@@ -1136,7 +1318,9 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1136
1318
|
instanceName: string;
|
|
1137
1319
|
logFd: number | "ignore";
|
|
1138
1320
|
runtimeUrl: string;
|
|
1139
|
-
|
|
1321
|
+
containersUpAt: number;
|
|
1322
|
+
analyze: boolean;
|
|
1323
|
+
}): Promise<{ ready: boolean; guardianAccessToken?: string }> {
|
|
1140
1324
|
const {
|
|
1141
1325
|
bootstrapSecret,
|
|
1142
1326
|
containerName,
|
|
@@ -1144,6 +1328,8 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1144
1328
|
instanceName,
|
|
1145
1329
|
logFd,
|
|
1146
1330
|
runtimeUrl,
|
|
1331
|
+
containersUpAt,
|
|
1332
|
+
analyze,
|
|
1147
1333
|
} = opts;
|
|
1148
1334
|
|
|
1149
1335
|
const log = (msg: string): void => {
|
|
@@ -1168,7 +1354,7 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1168
1354
|
log("Waiting for assistant to become ready...");
|
|
1169
1355
|
|
|
1170
1356
|
const readyUrl = `${runtimeUrl}/readyz`;
|
|
1171
|
-
const start =
|
|
1357
|
+
const start = containersUpAt;
|
|
1172
1358
|
let ready = false;
|
|
1173
1359
|
|
|
1174
1360
|
while (Date.now() - start < DOCKER_READY_TIMEOUT_MS) {
|
|
@@ -1204,8 +1390,18 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1204
1390
|
return { ready: false };
|
|
1205
1391
|
}
|
|
1206
1392
|
|
|
1207
|
-
const
|
|
1393
|
+
const readyAt = Date.now();
|
|
1394
|
+
const containersUpToReadyMs = readyAt - start;
|
|
1395
|
+
const elapsedSec = (containersUpToReadyMs / 1000).toFixed(1);
|
|
1208
1396
|
log(`Assistant ready after ${elapsedSec}s`);
|
|
1397
|
+
if (analyze) {
|
|
1398
|
+
console.info(
|
|
1399
|
+
`[vellum-hatch-timing] ${JSON.stringify({
|
|
1400
|
+
containers_up_to_ready_ms: containersUpToReadyMs,
|
|
1401
|
+
instance: instanceName,
|
|
1402
|
+
})}`,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1209
1405
|
|
|
1210
1406
|
// Lease guardian token. The /readyz check confirms both gateway and
|
|
1211
1407
|
// assistant are reachable. Retry with backoff in case there is a brief
|
|
@@ -1215,6 +1411,7 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1215
1411
|
const leaseDeadline = start + DOCKER_READY_TIMEOUT_MS;
|
|
1216
1412
|
let leaseSuccess = false;
|
|
1217
1413
|
let lastLeaseError: string | undefined;
|
|
1414
|
+
let guardianAccessToken: string | undefined;
|
|
1218
1415
|
|
|
1219
1416
|
while (Date.now() < leaseDeadline) {
|
|
1220
1417
|
try {
|
|
@@ -1227,6 +1424,7 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1227
1424
|
log(
|
|
1228
1425
|
`Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
|
|
1229
1426
|
);
|
|
1427
|
+
guardianAccessToken = tokenData.accessToken;
|
|
1230
1428
|
leaseSuccess = true;
|
|
1231
1429
|
break;
|
|
1232
1430
|
} catch (err) {
|
|
@@ -1257,5 +1455,5 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1257
1455
|
log(` Name: ${instanceName}`);
|
|
1258
1456
|
log(` Runtime: ${runtimeUrl}`);
|
|
1259
1457
|
log("");
|
|
1260
|
-
return { ready: true };
|
|
1458
|
+
return { ready: true, guardianAccessToken };
|
|
1261
1459
|
}
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "./assistant-config.js";
|
|
23
23
|
import type { AssistantEntry } from "./assistant-config.js";
|
|
24
24
|
import type { Species } from "./constants.js";
|
|
25
|
-
import { writeInitialConfig } from "./config-utils.js";
|
|
25
|
+
import { buildHatchConfigValues, writeInitialConfig } from "./config-utils.js";
|
|
26
26
|
import {
|
|
27
27
|
generateLocalSigningKey,
|
|
28
28
|
startLocalDaemon,
|
|
@@ -34,6 +34,12 @@ import { generateInstanceName } from "./random-name.js";
|
|
|
34
34
|
import { leaseGuardianToken } from "./guardian-token.js";
|
|
35
35
|
import { archiveLogFile, resetLogFile } from "./xdg-log.js";
|
|
36
36
|
import { emitProgress } from "./desktop-progress.js";
|
|
37
|
+
import {
|
|
38
|
+
configureHatchProviderApiKey,
|
|
39
|
+
formatProviderName,
|
|
40
|
+
resolveHatchProvider,
|
|
41
|
+
} from "./provider-secrets.js";
|
|
42
|
+
import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
45
|
* Attempts to place a symlink at the given path pointing to cliBinary.
|
|
@@ -125,13 +131,22 @@ function installCLISymlink(): void {
|
|
|
125
131
|
);
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
export interface HatchLocalOptions {
|
|
135
|
+
setupProviderCredentials?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
export async function hatchLocal(
|
|
129
139
|
species: Species,
|
|
130
140
|
name: string | null,
|
|
131
141
|
watch: boolean = false,
|
|
132
142
|
keepAlive: boolean = false,
|
|
133
143
|
configValues: Record<string, string> = {},
|
|
144
|
+
options: HatchLocalOptions = {},
|
|
134
145
|
): Promise<void> {
|
|
146
|
+
const provider =
|
|
147
|
+
options.setupProviderCredentials === false
|
|
148
|
+
? undefined
|
|
149
|
+
: resolveHatchProvider(configValues);
|
|
135
150
|
const instanceName = generateInstanceName(
|
|
136
151
|
species,
|
|
137
152
|
name ?? process.env.VELLUM_ASSISTANT_NAME,
|
|
@@ -168,7 +183,8 @@ export async function hatchLocal(
|
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
emitProgress(2, 6, "Writing configuration...");
|
|
171
|
-
const
|
|
186
|
+
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
187
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
172
188
|
|
|
173
189
|
emitProgress(3, 6, "Starting assistant...");
|
|
174
190
|
const signingKey = generateLocalSigningKey();
|
|
@@ -198,9 +214,11 @@ export async function hatchLocal(
|
|
|
198
214
|
emitProgress(5, 6, "Securing connection...");
|
|
199
215
|
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
200
216
|
const maxLeaseAttempts = 3;
|
|
217
|
+
let guardianAccessToken: string | undefined;
|
|
201
218
|
for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
|
|
202
219
|
try {
|
|
203
|
-
await leaseGuardianToken(loopbackUrl, instanceName);
|
|
220
|
+
const tokenData = await leaseGuardianToken(loopbackUrl, instanceName);
|
|
221
|
+
guardianAccessToken = tokenData.accessToken;
|
|
204
222
|
break;
|
|
205
223
|
} catch (err) {
|
|
206
224
|
if (attempt < maxLeaseAttempts) {
|
|
@@ -238,6 +256,26 @@ export async function hatchLocal(
|
|
|
238
256
|
installCLISymlink();
|
|
239
257
|
}
|
|
240
258
|
|
|
259
|
+
if (provider !== undefined && provider !== null && !guardianAccessToken) {
|
|
260
|
+
console.error(
|
|
261
|
+
`⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
|
|
262
|
+
` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
|
|
263
|
+
);
|
|
264
|
+
} else if (provider !== undefined) {
|
|
265
|
+
console.log("");
|
|
266
|
+
console.log(
|
|
267
|
+
provider === null
|
|
268
|
+
? "Checking provider credentials..."
|
|
269
|
+
: `Checking ${formatProviderName(provider)} credentials...`,
|
|
270
|
+
);
|
|
271
|
+
await configureHatchProviderApiKey({
|
|
272
|
+
gatewayUrl: loopbackUrl,
|
|
273
|
+
provider,
|
|
274
|
+
bearerToken: guardianAccessToken,
|
|
275
|
+
env: process.env,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
241
279
|
console.log("");
|
|
242
280
|
console.log(`✅ Local assistant hatched!`);
|
|
243
281
|
console.log("");
|
|
@@ -245,6 +283,7 @@ export async function hatchLocal(
|
|
|
245
283
|
console.log(` Name: ${instanceName}`);
|
|
246
284
|
console.log(` Runtime: ${runtimeUrl}`);
|
|
247
285
|
console.log("");
|
|
286
|
+
logHatchNextSteps(console.log, instanceName);
|
|
248
287
|
|
|
249
288
|
if (keepAlive) {
|
|
250
289
|
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function logHatchNextSteps(
|
|
2
|
+
log: (message: string) => void,
|
|
3
|
+
instanceName: string,
|
|
4
|
+
): void {
|
|
5
|
+
log("Next steps:");
|
|
6
|
+
log(" vellum client");
|
|
7
|
+
log(' vellum message "hello"');
|
|
8
|
+
log(" vellum events");
|
|
9
|
+
log(" vellum ps");
|
|
10
|
+
log(` vellum use ${instanceName}`);
|
|
11
|
+
log("");
|
|
12
|
+
}
|