@vellumai/cli 0.5.6 → 0.5.8
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/knip.json +3 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +152 -13
- package/src/commands/hatch.ts +120 -65
- package/src/commands/restore.ts +359 -16
- package/src/commands/retire.ts +5 -5
- package/src/commands/rollback.ts +436 -142
- package/src/commands/upgrade.ts +575 -205
- package/src/index.ts +4 -4
- package/src/lib/assistant-config.ts +33 -6
- package/src/lib/aws.ts +15 -8
- package/src/lib/backup-ops.ts +213 -0
- package/src/lib/cli-error.ts +93 -0
- package/src/lib/config-utils.ts +59 -0
- package/src/lib/docker.ts +99 -50
- package/src/lib/doctor-client.ts +11 -1
- package/src/lib/gcp.ts +19 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +30 -9
- package/src/lib/platform-client.ts +205 -3
- package/src/lib/platform-releases.ts +112 -0
- package/src/lib/upgrade-lifecycle.ts +844 -0
package/src/lib/docker.ts
CHANGED
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
setActiveAssistant,
|
|
13
13
|
} from "./assistant-config";
|
|
14
14
|
import type { AssistantEntry } from "./assistant-config";
|
|
15
|
+
import { writeInitialConfig } from "./config-utils";
|
|
15
16
|
import { DEFAULT_GATEWAY_PORT, PROVIDER_ENV_VAR_NAMES } from "./constants";
|
|
16
17
|
import type { Species } from "./constants";
|
|
17
|
-
import { leaseGuardianToken
|
|
18
|
+
import { leaseGuardianToken } from "./guardian-token";
|
|
18
19
|
import { isVellumProcess, stopProcess } from "./process";
|
|
19
20
|
import { generateInstanceName } from "./random-name";
|
|
21
|
+
import { resolveImageRefs } from "./platform-releases.js";
|
|
20
22
|
import { exec, execOutput } from "./step-runner";
|
|
21
23
|
import {
|
|
22
24
|
closeLogFile,
|
|
@@ -269,11 +271,30 @@ async function ensureDockerInstalled(): Promise<void> {
|
|
|
269
271
|
console.log("🚀 Docker daemon not running. Starting Colima...");
|
|
270
272
|
try {
|
|
271
273
|
await exec("colima", ["start"]);
|
|
272
|
-
} catch
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
} catch {
|
|
275
|
+
// Colima may fail if a previous VM instance is in a corrupt state.
|
|
276
|
+
// Attempt to delete the stale instance and retry once.
|
|
277
|
+
console.log(
|
|
278
|
+
"⚠️ Colima start failed — attempting to reset stale VM state...",
|
|
276
279
|
);
|
|
280
|
+
try {
|
|
281
|
+
await exec("colima", ["stop", "--force"]).catch(() => {});
|
|
282
|
+
await exec("colima", ["delete", "--force"]);
|
|
283
|
+
} catch {
|
|
284
|
+
// If delete also fails, fall through to the retry which will
|
|
285
|
+
// produce a clear error message.
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
console.log("🔄 Retrying colima start...");
|
|
290
|
+
await exec("colima", ["start"]);
|
|
291
|
+
} catch (retryErr) {
|
|
292
|
+
const message =
|
|
293
|
+
retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Failed to start Colima after resetting stale VM state. Please run 'colima start' manually.\n${message}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
277
298
|
}
|
|
278
299
|
}
|
|
279
300
|
}
|
|
@@ -464,16 +485,19 @@ async function buildAllImages(
|
|
|
464
485
|
* can be restarted independently.
|
|
465
486
|
*/
|
|
466
487
|
export function serviceDockerRunArgs(opts: {
|
|
488
|
+
signingKey?: string;
|
|
467
489
|
bootstrapSecret?: string;
|
|
468
490
|
cesServiceToken?: string;
|
|
469
491
|
extraAssistantEnv?: Record<string, string>;
|
|
470
492
|
gatewayPort: number;
|
|
471
493
|
imageTags: Record<ServiceName, string>;
|
|
494
|
+
defaultWorkspaceConfigPath?: string;
|
|
472
495
|
instanceName: string;
|
|
473
496
|
res: ReturnType<typeof dockerResourceNames>;
|
|
474
497
|
}): Record<ServiceName, () => string[]> {
|
|
475
498
|
const {
|
|
476
499
|
cesServiceToken,
|
|
500
|
+
defaultWorkspaceConfigPath,
|
|
477
501
|
extraAssistantEnv,
|
|
478
502
|
gatewayPort,
|
|
479
503
|
imageTags,
|
|
@@ -496,17 +520,31 @@ export function serviceDockerRunArgs(opts: {
|
|
|
496
520
|
"-e",
|
|
497
521
|
`VELLUM_ASSISTANT_NAME=${instanceName}`,
|
|
498
522
|
"-e",
|
|
523
|
+
"VELLUM_CLOUD=docker",
|
|
524
|
+
"-e",
|
|
499
525
|
"RUNTIME_HTTP_HOST=0.0.0.0",
|
|
500
526
|
"-e",
|
|
501
|
-
"
|
|
527
|
+
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
502
528
|
"-e",
|
|
503
529
|
`CES_CREDENTIAL_URL=http://${res.cesContainer}:8090`,
|
|
504
530
|
"-e",
|
|
505
531
|
`GATEWAY_INTERNAL_URL=http://${res.gatewayContainer}:${GATEWAY_INTERNAL_PORT}`,
|
|
506
532
|
];
|
|
533
|
+
if (defaultWorkspaceConfigPath) {
|
|
534
|
+
const containerPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
535
|
+
args.push(
|
|
536
|
+
"-v",
|
|
537
|
+
`${defaultWorkspaceConfigPath}:${containerPath}:ro`,
|
|
538
|
+
"-e",
|
|
539
|
+
`VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH=${containerPath}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
507
542
|
if (cesServiceToken) {
|
|
508
543
|
args.push("-e", `CES_SERVICE_TOKEN=${cesServiceToken}`);
|
|
509
544
|
}
|
|
545
|
+
if (opts.signingKey) {
|
|
546
|
+
args.push("-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`);
|
|
547
|
+
}
|
|
510
548
|
for (const envVar of [
|
|
511
549
|
...Object.values(PROVIDER_ENV_VAR_NAMES),
|
|
512
550
|
"VELLUM_PLATFORM_URL",
|
|
@@ -537,7 +575,7 @@ export function serviceDockerRunArgs(opts: {
|
|
|
537
575
|
"-v",
|
|
538
576
|
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
539
577
|
"-e",
|
|
540
|
-
"
|
|
578
|
+
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
541
579
|
"-e",
|
|
542
580
|
"GATEWAY_SECURITY_DIR=/gateway-security",
|
|
543
581
|
"-e",
|
|
@@ -553,6 +591,9 @@ export function serviceDockerRunArgs(opts: {
|
|
|
553
591
|
...(cesServiceToken
|
|
554
592
|
? ["-e", `CES_SERVICE_TOKEN=${cesServiceToken}`]
|
|
555
593
|
: []),
|
|
594
|
+
...(opts.signingKey
|
|
595
|
+
? ["-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`]
|
|
596
|
+
: []),
|
|
556
597
|
...(opts.bootstrapSecret
|
|
557
598
|
? ["-e", `GUARDIAN_BOOTSTRAP_SECRET=${opts.bootstrapSecret}`]
|
|
558
599
|
: []),
|
|
@@ -574,7 +615,7 @@ export function serviceDockerRunArgs(opts: {
|
|
|
574
615
|
"-e",
|
|
575
616
|
"CES_MODE=managed",
|
|
576
617
|
"-e",
|
|
577
|
-
"
|
|
618
|
+
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
578
619
|
"-e",
|
|
579
620
|
"CES_BOOTSTRAP_SOCKET_DIR=/run/ces-bootstrap",
|
|
580
621
|
"-e",
|
|
@@ -739,11 +780,13 @@ export const SERVICE_START_ORDER: ServiceName[] = [
|
|
|
739
780
|
/** Start all three containers in dependency order. */
|
|
740
781
|
export async function startContainers(
|
|
741
782
|
opts: {
|
|
783
|
+
signingKey?: string;
|
|
742
784
|
bootstrapSecret?: string;
|
|
743
785
|
cesServiceToken?: string;
|
|
744
786
|
extraAssistantEnv?: Record<string, string>;
|
|
745
787
|
gatewayPort: number;
|
|
746
788
|
imageTags: Record<ServiceName, string>;
|
|
789
|
+
defaultWorkspaceConfigPath?: string;
|
|
747
790
|
instanceName: string;
|
|
748
791
|
res: ReturnType<typeof dockerResourceNames>;
|
|
749
792
|
},
|
|
@@ -765,27 +808,6 @@ export async function stopContainers(
|
|
|
765
808
|
await removeContainer(res.assistantContainer);
|
|
766
809
|
}
|
|
767
810
|
|
|
768
|
-
/**
|
|
769
|
-
* Remove the signing-key-bootstrap lockfile from the gateway security volume.
|
|
770
|
-
* This allows the daemon to re-fetch the signing key from the gateway on the
|
|
771
|
-
* next startup — necessary during upgrades where the gateway may generate a
|
|
772
|
-
* new key.
|
|
773
|
-
*/
|
|
774
|
-
export async function clearSigningKeyBootstrapLock(
|
|
775
|
-
res: ReturnType<typeof dockerResourceNames>,
|
|
776
|
-
): Promise<void> {
|
|
777
|
-
await exec("docker", [
|
|
778
|
-
"run",
|
|
779
|
-
"--rm",
|
|
780
|
-
"-v",
|
|
781
|
-
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
782
|
-
"busybox",
|
|
783
|
-
"rm",
|
|
784
|
-
"-f",
|
|
785
|
-
"/gateway-security/signing-key-bootstrap.lock",
|
|
786
|
-
]);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
811
|
/** Stop containers without removing them (preserves state for `docker start`). */
|
|
790
812
|
export async function sleepContainers(
|
|
791
813
|
res: ReturnType<typeof dockerResourceNames>,
|
|
@@ -1028,6 +1050,7 @@ export async function hatchDocker(
|
|
|
1028
1050
|
detached: boolean,
|
|
1029
1051
|
name: string | null,
|
|
1030
1052
|
watch: boolean = false,
|
|
1053
|
+
configValues: Record<string, string> = {},
|
|
1031
1054
|
): Promise<void> {
|
|
1032
1055
|
resetLogFile("hatch.log");
|
|
1033
1056
|
|
|
@@ -1074,14 +1097,16 @@ export async function hatchDocker(
|
|
|
1074
1097
|
} else {
|
|
1075
1098
|
const version = cliPkg.version;
|
|
1076
1099
|
const versionTag = version ? `v${version}` : "latest";
|
|
1077
|
-
|
|
1078
|
-
|
|
1100
|
+
log("🔍 Resolving image references...");
|
|
1101
|
+
const resolved = await resolveImageRefs(versionTag, log);
|
|
1102
|
+
imageTags.assistant = resolved.imageTags.assistant;
|
|
1103
|
+
imageTags.gateway = resolved.imageTags.gateway;
|
|
1079
1104
|
imageTags["credential-executor"] =
|
|
1080
|
-
|
|
1105
|
+
resolved.imageTags["credential-executor"];
|
|
1081
1106
|
|
|
1082
1107
|
log(`🥚 Hatching Docker assistant: ${instanceName}`);
|
|
1083
1108
|
log(` Species: ${species}`);
|
|
1084
|
-
log(` Images:`);
|
|
1109
|
+
log(` Images (${resolved.source}):`);
|
|
1085
1110
|
log(` assistant: ${imageTags.assistant}`);
|
|
1086
1111
|
log(` gateway: ${imageTags.gateway}`);
|
|
1087
1112
|
log(` credential-executor: ${imageTags["credential-executor"]}`);
|
|
@@ -1115,24 +1140,35 @@ export async function hatchDocker(
|
|
|
1115
1140
|
"/workspace",
|
|
1116
1141
|
]);
|
|
1117
1142
|
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
"run",
|
|
1122
|
-
"--rm",
|
|
1123
|
-
"-v",
|
|
1124
|
-
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
1125
|
-
"busybox",
|
|
1126
|
-
"rm",
|
|
1127
|
-
"-f",
|
|
1128
|
-
"/gateway-security/signing-key-bootstrap.lock",
|
|
1129
|
-
]);
|
|
1143
|
+
// Write --config key=value pairs to a temp file that gets bind-mounted
|
|
1144
|
+
// into the assistant container and read via VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH.
|
|
1145
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
|
|
1130
1146
|
|
|
1131
1147
|
const cesServiceToken = randomBytes(32).toString("hex");
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1148
|
+
const signingKey = randomBytes(32).toString("hex");
|
|
1149
|
+
|
|
1150
|
+
// When launched by a remote hatch startup script, the env var
|
|
1151
|
+
// GUARDIAN_BOOTSTRAP_SECRET is already set with the laptop's secret.
|
|
1152
|
+
// Generate a new secret for the local docker hatch caller and append
|
|
1153
|
+
// it so the gateway receives a comma-separated list of all expected
|
|
1154
|
+
// bootstrap secrets.
|
|
1155
|
+
const ownSecret = randomBytes(32).toString("hex");
|
|
1156
|
+
const preExisting = process.env.GUARDIAN_BOOTSTRAP_SECRET;
|
|
1157
|
+
const bootstrapSecret = preExisting
|
|
1158
|
+
? `${preExisting},${ownSecret}`
|
|
1159
|
+
: ownSecret;
|
|
1160
|
+
|
|
1134
1161
|
await startContainers(
|
|
1135
|
-
{
|
|
1162
|
+
{
|
|
1163
|
+
signingKey,
|
|
1164
|
+
bootstrapSecret,
|
|
1165
|
+
cesServiceToken,
|
|
1166
|
+
gatewayPort,
|
|
1167
|
+
imageTags,
|
|
1168
|
+
defaultWorkspaceConfigPath,
|
|
1169
|
+
instanceName,
|
|
1170
|
+
res,
|
|
1171
|
+
},
|
|
1136
1172
|
log,
|
|
1137
1173
|
);
|
|
1138
1174
|
|
|
@@ -1161,6 +1197,7 @@ export async function hatchDocker(
|
|
|
1161
1197
|
setActiveAssistant(instanceName);
|
|
1162
1198
|
|
|
1163
1199
|
const { ready } = await waitForGatewayAndLease({
|
|
1200
|
+
bootstrapSecret: ownSecret,
|
|
1164
1201
|
containerName: res.assistantContainer,
|
|
1165
1202
|
detached: watch ? false : detached,
|
|
1166
1203
|
instanceName,
|
|
@@ -1218,13 +1255,21 @@ export async function hatchDocker(
|
|
|
1218
1255
|
* lease a guardian token.
|
|
1219
1256
|
*/
|
|
1220
1257
|
async function waitForGatewayAndLease(opts: {
|
|
1258
|
+
bootstrapSecret: string;
|
|
1221
1259
|
containerName: string;
|
|
1222
1260
|
detached: boolean;
|
|
1223
1261
|
instanceName: string;
|
|
1224
1262
|
logFd: number | "ignore";
|
|
1225
1263
|
runtimeUrl: string;
|
|
1226
1264
|
}): Promise<{ ready: boolean }> {
|
|
1227
|
-
const {
|
|
1265
|
+
const {
|
|
1266
|
+
bootstrapSecret,
|
|
1267
|
+
containerName,
|
|
1268
|
+
detached,
|
|
1269
|
+
instanceName,
|
|
1270
|
+
logFd,
|
|
1271
|
+
runtimeUrl,
|
|
1272
|
+
} = opts;
|
|
1228
1273
|
|
|
1229
1274
|
const log = (msg: string): void => {
|
|
1230
1275
|
console.log(msg);
|
|
@@ -1298,7 +1343,11 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1298
1343
|
|
|
1299
1344
|
while (Date.now() < leaseDeadline) {
|
|
1300
1345
|
try {
|
|
1301
|
-
const tokenData = await leaseGuardianToken(
|
|
1346
|
+
const tokenData = await leaseGuardianToken(
|
|
1347
|
+
runtimeUrl,
|
|
1348
|
+
instanceName,
|
|
1349
|
+
bootstrapSecret,
|
|
1350
|
+
);
|
|
1302
1351
|
const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
|
|
1303
1352
|
log(
|
|
1304
1353
|
`Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
|
package/src/lib/doctor-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DOCTOR_URL =
|
|
1
|
+
const DOCTOR_URL = process.env.DOCTOR_SERVICE_URL?.trim() || "";
|
|
2
2
|
|
|
3
3
|
export type ProgressPhase =
|
|
4
4
|
| "invoking_prompt"
|
|
@@ -107,6 +107,16 @@ async function callDoctorDaemon(
|
|
|
107
107
|
chatContext?: ChatLogEntry[],
|
|
108
108
|
onLog?: DoctorLogCallback,
|
|
109
109
|
): Promise<DoctorResult> {
|
|
110
|
+
if (!DOCTOR_URL) {
|
|
111
|
+
onLog?.("Doctor service not configured (DOCTOR_SERVICE_URL is not set)");
|
|
112
|
+
return {
|
|
113
|
+
assistantId,
|
|
114
|
+
diagnostics: null,
|
|
115
|
+
recommendation: null,
|
|
116
|
+
error: "Doctor service not configured",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
const MAX_RETRIES = 2;
|
|
111
121
|
let lastError: unknown;
|
|
112
122
|
|
package/src/lib/gcp.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./constants";
|
|
12
12
|
import type { Species } from "./constants";
|
|
13
13
|
import { leaseGuardianToken } from "./guardian-token";
|
|
14
|
+
import { getPlatformUrl } from "./platform-client";
|
|
14
15
|
import { generateInstanceName } from "./random-name";
|
|
15
16
|
import { exec, execOutput } from "./step-runner";
|
|
16
17
|
|
|
@@ -455,13 +456,15 @@ export async function hatchGcp(
|
|
|
455
456
|
providerApiKeys: Record<string, string>,
|
|
456
457
|
instanceName: string,
|
|
457
458
|
cloud: "gcp",
|
|
458
|
-
|
|
459
|
+
configValues?: Record<string, string>,
|
|
460
|
+
) => Promise<{ script: string; laptopBootstrapSecret: string }>,
|
|
459
461
|
watchHatching: (
|
|
460
462
|
pollFn: () => Promise<PollResult>,
|
|
461
463
|
instanceName: string,
|
|
462
464
|
startTime: number,
|
|
463
465
|
species: Species,
|
|
464
466
|
) => Promise<WatchHatchingResult>,
|
|
467
|
+
configValues: Record<string, string> = {},
|
|
465
468
|
): Promise<void> {
|
|
466
469
|
const startTime = Date.now();
|
|
467
470
|
const account = process.env.GCP_ACCOUNT_EMAIL;
|
|
@@ -519,13 +522,15 @@ export async function hatchGcp(
|
|
|
519
522
|
);
|
|
520
523
|
process.exit(1);
|
|
521
524
|
}
|
|
522
|
-
const startupScript
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
525
|
+
const { script: startupScript, laptopBootstrapSecret } =
|
|
526
|
+
await buildStartupScript(
|
|
527
|
+
species,
|
|
528
|
+
sshUser,
|
|
529
|
+
providerApiKeys,
|
|
530
|
+
instanceName,
|
|
531
|
+
"gcp",
|
|
532
|
+
configValues,
|
|
533
|
+
);
|
|
529
534
|
const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
|
|
530
535
|
writeFileSync(startupScriptPath, startupScript);
|
|
531
536
|
|
|
@@ -640,7 +645,7 @@ export async function hatchGcp(
|
|
|
640
645
|
species === "vellum" &&
|
|
641
646
|
(await checkCurlFailure(instanceName, project, zone, account))
|
|
642
647
|
) {
|
|
643
|
-
const installScriptUrl = `${
|
|
648
|
+
const installScriptUrl = `${getPlatformUrl()}/install.sh`;
|
|
644
649
|
console.log(
|
|
645
650
|
`\ud83d\udd04 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`,
|
|
646
651
|
);
|
|
@@ -658,7 +663,11 @@ export async function hatchGcp(
|
|
|
658
663
|
}
|
|
659
664
|
|
|
660
665
|
try {
|
|
661
|
-
await leaseGuardianToken(
|
|
666
|
+
await leaseGuardianToken(
|
|
667
|
+
runtimeUrl,
|
|
668
|
+
instanceName,
|
|
669
|
+
laptopBootstrapSecret,
|
|
670
|
+
);
|
|
662
671
|
} catch (err) {
|
|
663
672
|
console.warn(
|
|
664
673
|
`\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
|
|
@@ -42,46 +42,6 @@ function getPersistedDeviceIdPath(): string {
|
|
|
42
42
|
return join(getXdgConfigHome(), "vellum", "device-id");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function getBootstrapSecretPath(assistantId: string): string {
|
|
46
|
-
return join(
|
|
47
|
-
getXdgConfigHome(),
|
|
48
|
-
"vellum",
|
|
49
|
-
"assistants",
|
|
50
|
-
assistantId,
|
|
51
|
-
"bootstrap-secret",
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Load a previously saved bootstrap secret for the given assistant.
|
|
57
|
-
* Returns null if the file does not exist or is unreadable.
|
|
58
|
-
*/
|
|
59
|
-
export function loadBootstrapSecret(assistantId: string): string | null {
|
|
60
|
-
try {
|
|
61
|
-
const raw = readFileSync(getBootstrapSecretPath(assistantId), "utf-8").trim();
|
|
62
|
-
return raw.length > 0 ? raw : null;
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Persist a bootstrap secret for the given assistant so that the desktop
|
|
70
|
-
* client and upgrade/rollback paths can retrieve it later.
|
|
71
|
-
*/
|
|
72
|
-
export function saveBootstrapSecret(
|
|
73
|
-
assistantId: string,
|
|
74
|
-
secret: string,
|
|
75
|
-
): void {
|
|
76
|
-
const path = getBootstrapSecretPath(assistantId);
|
|
77
|
-
const dir = dirname(path);
|
|
78
|
-
if (!existsSync(dir)) {
|
|
79
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
80
|
-
}
|
|
81
|
-
writeFileSync(path, secret + "\n", { mode: 0o600 });
|
|
82
|
-
chmodSync(path, 0o600);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
45
|
function hashWithSalt(input: string): string {
|
|
86
46
|
return createHash("sha256")
|
|
87
47
|
.update(input + DEVICE_ID_SALT)
|
|
@@ -206,10 +166,12 @@ export function saveGuardianToken(
|
|
|
206
166
|
export async function leaseGuardianToken(
|
|
207
167
|
gatewayUrl: string,
|
|
208
168
|
assistantId: string,
|
|
169
|
+
bootstrapSecret?: string,
|
|
209
170
|
): Promise<GuardianTokenData> {
|
|
210
171
|
const deviceId = computeDeviceId();
|
|
211
|
-
const headers: Record<string, string> = {
|
|
212
|
-
|
|
172
|
+
const headers: Record<string, string> = {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
};
|
|
213
175
|
if (bootstrapSecret) {
|
|
214
176
|
headers["x-bootstrap-secret"] = bootstrapSecret;
|
|
215
177
|
}
|
package/src/lib/local.ts
CHANGED
|
@@ -192,10 +192,15 @@ function resolveDaemonMainPath(assistantIndex: string): string {
|
|
|
192
192
|
return join(dirname(assistantIndex), "daemon", "main.ts");
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
type DaemonStartOptions = {
|
|
196
|
+
foreground?: boolean;
|
|
197
|
+
defaultWorkspaceConfigPath?: string;
|
|
198
|
+
};
|
|
199
|
+
|
|
195
200
|
async function startDaemonFromSource(
|
|
196
201
|
assistantIndex: string,
|
|
197
202
|
resources: LocalInstanceResources,
|
|
198
|
-
options?:
|
|
203
|
+
options?: DaemonStartOptions,
|
|
199
204
|
): Promise<void> {
|
|
200
205
|
const foreground = options?.foreground ?? false;
|
|
201
206
|
const daemonMainPath = resolveDaemonMainPath(assistantIndex);
|
|
@@ -260,6 +265,7 @@ async function startDaemonFromSource(
|
|
|
260
265
|
const env: Record<string, string | undefined> = {
|
|
261
266
|
...process.env,
|
|
262
267
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
268
|
+
VELLUM_CLOUD: "local",
|
|
263
269
|
};
|
|
264
270
|
if (resources) {
|
|
265
271
|
env.BASE_DATA_DIR = resources.instanceDir;
|
|
@@ -268,6 +274,10 @@ async function startDaemonFromSource(
|
|
|
268
274
|
env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
|
|
269
275
|
delete env.QDRANT_URL;
|
|
270
276
|
}
|
|
277
|
+
if (options?.defaultWorkspaceConfigPath) {
|
|
278
|
+
env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
279
|
+
options.defaultWorkspaceConfigPath;
|
|
280
|
+
}
|
|
271
281
|
|
|
272
282
|
// Write a sentinel PID file before spawning so concurrent hatch() calls
|
|
273
283
|
// detect the in-progress spawn and wait instead of racing.
|
|
@@ -306,6 +316,7 @@ async function startDaemonFromSource(
|
|
|
306
316
|
async function startDaemonWatchFromSource(
|
|
307
317
|
assistantIndex: string,
|
|
308
318
|
resources: LocalInstanceResources,
|
|
319
|
+
options?: DaemonStartOptions,
|
|
309
320
|
): Promise<void> {
|
|
310
321
|
const mainPath = resolveDaemonMainPath(assistantIndex);
|
|
311
322
|
if (!existsSync(mainPath)) {
|
|
@@ -381,6 +392,10 @@ async function startDaemonWatchFromSource(
|
|
|
381
392
|
env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
|
|
382
393
|
delete env.QDRANT_URL;
|
|
383
394
|
}
|
|
395
|
+
if (options?.defaultWorkspaceConfigPath) {
|
|
396
|
+
env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
397
|
+
options.defaultWorkspaceConfigPath;
|
|
398
|
+
}
|
|
384
399
|
|
|
385
400
|
// Write a sentinel PID file before spawning so concurrent hatch() calls
|
|
386
401
|
// detect the in-progress spawn and wait instead of racing.
|
|
@@ -819,7 +834,7 @@ export function isGatewayWatchModeAvailable(): boolean {
|
|
|
819
834
|
export async function startLocalDaemon(
|
|
820
835
|
watch: boolean = false,
|
|
821
836
|
resources: LocalInstanceResources,
|
|
822
|
-
options?:
|
|
837
|
+
options?: DaemonStartOptions,
|
|
823
838
|
): Promise<void> {
|
|
824
839
|
const foreground = options?.foreground ?? false;
|
|
825
840
|
// Check for a compiled daemon binary adjacent to the CLI executable.
|
|
@@ -905,7 +920,7 @@ export async function startLocalDaemon(
|
|
|
905
920
|
|
|
906
921
|
// Build a minimal environment for the daemon. When launched from the
|
|
907
922
|
// macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
|
|
908
|
-
// __CFBundleIdentifier,
|
|
923
|
+
// __CFBundleIdentifier, etc.) that can cause
|
|
909
924
|
// the daemon to take 50+ seconds to start instead of ~1s.
|
|
910
925
|
const home = homedir();
|
|
911
926
|
const bunBinDir = join(home, ".bun", "bin");
|
|
@@ -924,20 +939,26 @@ export async function startLocalDaemon(
|
|
|
924
939
|
"ANTHROPIC_API_KEY",
|
|
925
940
|
"APP_VERSION",
|
|
926
941
|
"BASE_DATA_DIR",
|
|
927
|
-
"
|
|
942
|
+
"VELLUM_PLATFORM_URL",
|
|
928
943
|
"QDRANT_HTTP_PORT",
|
|
929
944
|
"QDRANT_URL",
|
|
930
945
|
"RUNTIME_HTTP_PORT",
|
|
931
|
-
"
|
|
946
|
+
"SENTRY_DSN_ASSISTANT",
|
|
932
947
|
"TMPDIR",
|
|
933
948
|
"USER",
|
|
934
949
|
"LANG",
|
|
935
950
|
"VELLUM_DEBUG",
|
|
951
|
+
"VELLUM_DEV",
|
|
952
|
+
"VELLUM_DESKTOP_APP",
|
|
936
953
|
]) {
|
|
937
954
|
if (process.env[key]) {
|
|
938
955
|
daemonEnv[key] = process.env[key]!;
|
|
939
956
|
}
|
|
940
957
|
}
|
|
958
|
+
if (options?.defaultWorkspaceConfigPath) {
|
|
959
|
+
daemonEnv.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
960
|
+
options.defaultWorkspaceConfigPath;
|
|
961
|
+
}
|
|
941
962
|
// When running a named instance, override env so the daemon resolves
|
|
942
963
|
// all paths under the instance directory and listens on its own port.
|
|
943
964
|
if (resources) {
|
|
@@ -1005,9 +1026,9 @@ export async function startLocalDaemon(
|
|
|
1005
1026
|
// Kill the bundled daemon to avoid two processes competing for the same port
|
|
1006
1027
|
await stopProcessByPidFile(pidFile, "bundled daemon");
|
|
1007
1028
|
if (watch) {
|
|
1008
|
-
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
1029
|
+
await startDaemonWatchFromSource(assistantIndex, resources, options);
|
|
1009
1030
|
} else {
|
|
1010
|
-
await startDaemonFromSource(assistantIndex, resources);
|
|
1031
|
+
await startDaemonFromSource(assistantIndex, resources, options);
|
|
1011
1032
|
}
|
|
1012
1033
|
daemonReady = await waitForDaemonReady(resources.daemonPort, 60000);
|
|
1013
1034
|
}
|
|
@@ -1031,7 +1052,7 @@ export async function startLocalDaemon(
|
|
|
1031
1052
|
);
|
|
1032
1053
|
}
|
|
1033
1054
|
if (watch) {
|
|
1034
|
-
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
1055
|
+
await startDaemonWatchFromSource(assistantIndex, resources, options);
|
|
1035
1056
|
|
|
1036
1057
|
const daemonReady = await waitForDaemonReady(resources.daemonPort, 60000);
|
|
1037
1058
|
if (daemonReady) {
|
|
@@ -1042,7 +1063,7 @@ export async function startLocalDaemon(
|
|
|
1042
1063
|
);
|
|
1043
1064
|
}
|
|
1044
1065
|
} else {
|
|
1045
|
-
await startDaemonFromSource(assistantIndex, resources,
|
|
1066
|
+
await startDaemonFromSource(assistantIndex, resources, options);
|
|
1046
1067
|
|
|
1047
1068
|
const daemonReady = await waitForDaemonReady(resources.daemonPort, 60000);
|
|
1048
1069
|
if (daemonReady) {
|