@forgezero/agent 0.1.62 → 0.1.63

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/dist/bootstrap.js CHANGED
@@ -1209,19 +1209,175 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1209
1209
  return evidence;
1210
1210
  }
1211
1211
 
1212
+ // src/bootstrap-bundle.ts
1213
+ import { createHash, randomBytes } from "crypto";
1214
+ import {
1215
+ chmodSync,
1216
+ createReadStream,
1217
+ existsSync,
1218
+ lstatSync,
1219
+ mkdirSync,
1220
+ readFileSync,
1221
+ renameSync,
1222
+ rmSync,
1223
+ writeFileSync
1224
+ } from "fs";
1225
+ import { dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
1226
+ var BOOTSTRAP_BUNDLE_FORMAT = 1;
1227
+ var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
1228
+ var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
1229
+ var run = async (argv) => {
1230
+ const child = Bun.spawn([...argv], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
1231
+ const [stdout, stderr, exitCode] = await Promise.all([
1232
+ new Response(child.stdout).text(),
1233
+ new Response(child.stderr).text(),
1234
+ child.exited
1235
+ ]);
1236
+ return { exitCode, output: `${stdout}${stderr}`.slice(0, 65536) };
1237
+ };
1238
+ var checked = async (exec, argv, label) => {
1239
+ const result = await exec(argv);
1240
+ if (result.exitCode !== 0)
1241
+ throw new Error(`${label} failed${result.output.trim() ? `: ${result.output.trim()}` : ""}`);
1242
+ return result.output.trim();
1243
+ };
1244
+ async function sha256File(path) {
1245
+ const hash = createHash("sha256");
1246
+ await new Promise((resolveDone, reject) => {
1247
+ const stream = createReadStream(path);
1248
+ stream.on("data", (chunk) => hash.update(chunk));
1249
+ stream.on("error", reject);
1250
+ stream.on("end", resolveDone);
1251
+ });
1252
+ return hash.digest("hex");
1253
+ }
1254
+ var branchName = (value) => {
1255
+ if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
1256
+ throw new Error("bootstrap bundle branch is malformed");
1257
+ }
1258
+ return value;
1259
+ };
1260
+ function parseBootstrapBundleManifest(value) {
1261
+ if (!value || typeof value !== "object" || Array.isArray(value))
1262
+ throw new Error("bootstrap bundle manifest must be an object");
1263
+ const source = value;
1264
+ const allowed = ["format", "kind", "branch", "revision", "sha256", "bytes", "createdAt"];
1265
+ const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
1266
+ if (unknown.length)
1267
+ throw new Error(`bootstrap bundle manifest contains unknown field ${unknown[0]}`);
1268
+ if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
1269
+ throw new Error("bootstrap bundle manifest format is unsupported");
1270
+ }
1271
+ branchName(source.branch);
1272
+ if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
1273
+ throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
1274
+ }
1275
+ if (typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256)) {
1276
+ throw new Error("bootstrap bundle digest is malformed");
1277
+ }
1278
+ if (!Number.isSafeInteger(source.bytes) || Number(source.bytes) < 1 || Number(source.bytes) > MAX_BOOTSTRAP_BUNDLE_BYTES) {
1279
+ throw new Error("bootstrap bundle size is outside the supported boundary");
1280
+ }
1281
+ if (typeof source.createdAt !== "string" || Number.isNaN(Date.parse(source.createdAt))) {
1282
+ throw new Error("bootstrap bundle creation time is malformed");
1283
+ }
1284
+ return source;
1285
+ }
1286
+ function ownerRegularFile(path, maximum, label) {
1287
+ if (!isAbsolute(path) || resolve2(path) !== path || /[\r\n\0]/.test(path)) {
1288
+ throw new Error(`${label} path must be canonical and absolute`);
1289
+ }
1290
+ const value = lstatSync(path);
1291
+ const uid = process.getuid?.() ?? value.uid;
1292
+ if (!value.isFile() || value.isSymbolicLink() || value.uid !== uid || value.nlink !== 1 || (value.mode & 63) !== 0 || value.size < 1 || value.size > maximum) {
1293
+ throw new Error(`${label} must be an owner-only regular file with one link and at most ${maximum} bytes`);
1294
+ }
1295
+ }
1296
+ async function readBootstrapBundle(bundlePath, manifestPath = `${bundlePath}.json`) {
1297
+ ownerRegularFile(bundlePath, MAX_BOOTSTRAP_BUNDLE_BYTES, "bootstrap bundle");
1298
+ ownerRegularFile(manifestPath, 16 * 1024, "bootstrap bundle manifest");
1299
+ let parsed;
1300
+ try {
1301
+ parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
1302
+ } catch {
1303
+ throw new Error("bootstrap bundle manifest is not valid JSON");
1304
+ }
1305
+ const manifest = parseBootstrapBundleManifest(parsed);
1306
+ const size = lstatSync(bundlePath).size;
1307
+ if (size !== manifest.bytes || await sha256File(bundlePath) !== manifest.sha256) {
1308
+ throw new Error("bootstrap bundle bytes do not match their manifest");
1309
+ }
1310
+ return { bundlePath, manifestPath, manifest };
1311
+ }
1312
+ async function buildBootstrapBundle(input, exec = run) {
1313
+ const repositoryRoot = resolve2(input.repositoryRoot);
1314
+ const outputPath = resolve2(input.outputPath);
1315
+ const manifestPath = `${outputPath}.json`;
1316
+ const branch = branchName(input.branch);
1317
+ if (!isAbsolute(input.outputPath) || outputPath !== input.outputPath) {
1318
+ throw new Error("bootstrap bundle output must be a canonical absolute path");
1319
+ }
1320
+ if (existsSync(outputPath) || existsSync(manifestPath)) {
1321
+ throw new Error("bootstrap bundle output already exists");
1322
+ }
1323
+ const root = lstatSync(repositoryRoot);
1324
+ if (!root.isDirectory() || root.isSymbolicLink())
1325
+ throw new Error("bootstrap bundle source must be a real directory");
1326
+ mkdirSync(dirname2(outputPath), { recursive: true, mode: 448 });
1327
+ const parent = lstatSync(dirname2(outputPath));
1328
+ const uid = process.getuid?.() ?? parent.uid;
1329
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
1330
+ throw new Error("bootstrap bundle output directory must be caller-owned and owner-only");
1331
+ }
1332
+ const dirty = await checked(exec, ["git", "-C", repositoryRoot, "status", "--porcelain=v1", "--untracked-files=no"], "Git worktree check");
1333
+ if (dirty)
1334
+ throw new Error("bootstrap bundle source has tracked changes; commit the reviewed API release first");
1335
+ const revision = (await checked(exec, ["git", "-C", repositoryRoot, "rev-parse", "--verify", `refs/heads/${branch}^{commit}`], "Git branch resolution")).toLowerCase();
1336
+ if (!/^[a-f0-9]{40}$/.test(revision))
1337
+ throw new Error("bootstrap bundle branch did not resolve to one exact commit");
1338
+ const temporary = `${outputPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
1339
+ try {
1340
+ await checked(exec, ["git", "-C", repositoryRoot, "bundle", "create", temporary, `refs/heads/${branch}`], "Git bundle creation");
1341
+ chmodSync(temporary, 384);
1342
+ const file = lstatSync(temporary);
1343
+ if (!file.isFile() || file.isSymbolicLink() || file.size < 1 || file.size > MAX_BOOTSTRAP_BUNDLE_BYTES) {
1344
+ throw new Error("created bootstrap bundle is outside the supported boundary");
1345
+ }
1346
+ await checked(exec, ["git", "bundle", "verify", temporary], "Git bundle verification");
1347
+ const manifest = {
1348
+ format: BOOTSTRAP_BUNDLE_FORMAT,
1349
+ kind: BOOTSTRAP_BUNDLE_KIND,
1350
+ branch,
1351
+ revision,
1352
+ sha256: await sha256File(temporary),
1353
+ bytes: file.size,
1354
+ createdAt: new Date().toISOString()
1355
+ };
1356
+ const manifestTemporary = `${manifestPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
1357
+ writeFileSync(manifestTemporary, `${JSON.stringify(manifest, null, 2)}
1358
+ `, { mode: 384, flag: "wx" });
1359
+ renameSync(temporary, outputPath);
1360
+ renameSync(manifestTemporary, manifestPath);
1361
+ return { bundlePath: outputPath, manifestPath, manifest };
1362
+ } catch (cause) {
1363
+ rmSync(temporary, { force: true });
1364
+ throw cause;
1365
+ }
1366
+ }
1367
+
1212
1368
  // src/bootstrap.ts
1213
- import { createHash, createHmac as createHmac2, randomBytes as randomBytes2 } from "crypto";
1369
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
1214
1370
  import {
1215
- chmodSync as chmodSync2,
1216
- existsSync as existsSync4,
1217
- lstatSync as lstatSync2,
1218
- mkdirSync as mkdirSync4,
1219
- readFileSync as readFileSync4,
1220
- renameSync as renameSync4,
1221
- rmSync as rmSync4,
1222
- writeFileSync as writeFileSync4
1371
+ chmodSync as chmodSync3,
1372
+ existsSync as existsSync5,
1373
+ lstatSync as lstatSync3,
1374
+ mkdirSync as mkdirSync5,
1375
+ readFileSync as readFileSync5,
1376
+ renameSync as renameSync5,
1377
+ rmSync as rmSync5,
1378
+ writeFileSync as writeFileSync5
1223
1379
  } from "fs";
1224
- import { dirname as dirname5 } from "path";
1380
+ import { dirname as dirname6 } from "path";
1225
1381
  import { fileURLToPath } from "url";
1226
1382
 
1227
1383
  // src/agent-update-helper.ts
@@ -1240,7 +1396,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1240
1396
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1241
1397
 
1242
1398
  // src/version.ts
1243
- var VERSION = "0.1.62";
1399
+ var VERSION = "0.1.63";
1244
1400
 
1245
1401
  // src/software.ts
1246
1402
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1255,8 +1411,8 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
1255
1411
  `;
1256
1412
 
1257
1413
  // src/service-supervisor.ts
1258
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "fs";
1259
- import { dirname as dirname2, join as join2, resolve as resolve2, sep } from "path";
1414
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1415
+ import { dirname as dirname3, join as join2, resolve as resolve3, sep } from "path";
1260
1416
 
1261
1417
  // src/definition.ts
1262
1418
  var RESERVED_STEP_ENV = new Set([
@@ -1275,17 +1431,17 @@ var RESERVED_STEP_ENV = new Set([
1275
1431
  // src/service-supervisor.ts
1276
1432
  var defaultHost = {
1277
1433
  write(path, content, mode) {
1278
- mkdirSync(dirname2(path), { recursive: true, mode: 493 });
1434
+ mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
1279
1435
  const next = `${path}.next`;
1280
- writeFileSync(next, content, { mode });
1281
- renameSync(next, path);
1436
+ writeFileSync2(next, content, { mode });
1437
+ renameSync2(next, path);
1282
1438
  },
1283
- read: (path) => readFileSync(path, "utf8"),
1284
- exists: existsSync,
1285
- list: (path) => existsSync(path) ? readdirSync(path) : [],
1439
+ read: (path) => readFileSync2(path, "utf8"),
1440
+ exists: existsSync2,
1441
+ list: (path) => existsSync2(path) ? readdirSync(path) : [],
1286
1442
  realpath: realpathSync,
1287
- mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
1288
- remove: (path) => rmSync(path, { force: true }),
1443
+ mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
1444
+ remove: (path) => rmSync2(path, { force: true }),
1289
1445
  async exec(argv) {
1290
1446
  const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: {
1291
1447
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -1315,21 +1471,21 @@ var defaultHost = {
1315
1471
  };
1316
1472
 
1317
1473
  // src/container-supervisor.ts
1318
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1319
- import { dirname as dirname3, resolve as resolve3, sep as sep2 } from "path";
1474
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
1475
+ import { dirname as dirname4, resolve as resolve4, sep as sep2 } from "path";
1320
1476
  var defaultHost2 = {
1321
1477
  realpath: realpathSync2,
1322
- exists: existsSync2,
1323
- read: (path) => readFileSync2(path, "utf8"),
1478
+ exists: existsSync3,
1479
+ read: (path) => readFileSync3(path, "utf8"),
1324
1480
  write(path, content, mode) {
1325
- mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
1481
+ mkdirSync3(dirname4(path), { recursive: true, mode: 493 });
1326
1482
  const next = `${path}.next`;
1327
- writeFileSync2(next, content, { mode });
1328
- renameSync2(next, path);
1483
+ writeFileSync3(next, content, { mode });
1484
+ renameSync3(next, path);
1329
1485
  },
1330
- remove: (path) => rmSync2(path, { force: true }),
1331
- list: (path) => existsSync2(path) ? readdirSync2(path) : [],
1332
- mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
1486
+ remove: (path) => rmSync3(path, { force: true }),
1487
+ list: (path) => existsSync3(path) ? readdirSync2(path) : [],
1488
+ mkdir: (path, mode) => mkdirSync3(path, { recursive: true, mode }),
1333
1489
  async exec(argv) {
1334
1490
  const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
1335
1491
  const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
@@ -1479,7 +1635,11 @@ function agentEgressUnit(options) {
1479
1635
  const user = options.user ?? "forgezero";
1480
1636
  if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
1481
1637
  throw new Error("invalid Agent service user");
1482
- const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1638
+ const bootstrapBundleEnabled = Boolean(options.bootstrapBundlePath && options.bootstrapBundleManifestPath);
1639
+ if (Boolean(options.bootstrapBundlePath) !== Boolean(options.bootstrapBundleManifestPath) || bootstrapBundleEnabled && ![options.bootstrapBundlePath, options.bootstrapBundleManifestPath].every((path) => path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
1640
+ throw new Error("bootstrap bundle and manifest must be supplied together as absolute paths");
1641
+ }
1642
+ const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
1483
1643
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1484
1644
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1485
1645
  if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
@@ -1949,6 +2109,8 @@ function agentUnit(options) {
1949
2109
  options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
1950
2110
  options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
1951
2111
  options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
2112
+ options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
2113
+ options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
1952
2114
  options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
1953
2115
  options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
1954
2116
  deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
@@ -2331,27 +2493,27 @@ function planProvision(options) {
2331
2493
  }
2332
2494
 
2333
2495
  // src/cli/agent-install.ts
2334
- import { randomBytes } from "crypto";
2496
+ import { randomBytes as randomBytes2 } from "crypto";
2335
2497
  import {
2336
- chmodSync,
2498
+ chmodSync as chmodSync2,
2337
2499
  copyFileSync,
2338
- existsSync as existsSync3,
2339
- lstatSync,
2340
- mkdirSync as mkdirSync3,
2341
- readFileSync as readFileSync3,
2500
+ existsSync as existsSync4,
2501
+ lstatSync as lstatSync2,
2502
+ mkdirSync as mkdirSync4,
2503
+ readFileSync as readFileSync4,
2342
2504
  realpathSync as realpathSync3,
2343
- renameSync as renameSync3,
2344
- rmSync as rmSync3,
2505
+ renameSync as renameSync4,
2506
+ rmSync as rmSync4,
2345
2507
  symlinkSync,
2346
- writeFileSync as writeFileSync3
2508
+ writeFileSync as writeFileSync4
2347
2509
  } from "fs";
2348
- import { dirname as dirname4 } from "path";
2349
- async function readCapabilities(run) {
2510
+ import { dirname as dirname5 } from "path";
2511
+ async function readCapabilities(run2) {
2350
2512
  const answers = {};
2351
2513
  const checks = Object.entries(CAPABILITY_CHECKS);
2352
2514
  for (const [id, check] of checks) {
2353
2515
  try {
2354
- const result = await run(check.operation);
2516
+ const result = await run2(check.operation);
2355
2517
  answers[id] = check.satisfied(result.stdout, result.exitCode);
2356
2518
  } catch {
2357
2519
  answers[id] = false;
@@ -2404,52 +2566,52 @@ var runProvisionOperation = async (operation) => {
2404
2566
  }
2405
2567
  if (operation.kind === "install-runtime") {
2406
2568
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
2407
- mkdirSync3(`${release}/dist`, { recursive: true, mode: 493 });
2408
- mkdirSync3(dirname4(operation.binary), { recursive: true, mode: 493 });
2569
+ mkdirSync4(`${release}/dist`, { recursive: true, mode: 493 });
2570
+ mkdirSync4(dirname5(operation.binary), { recursive: true, mode: 493 });
2409
2571
  copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
2410
- chmodSync(`${release}/dist/fz-agent.js`, 493);
2411
- const gitSshSource = `${dirname4(operation.source)}/fz-git-ssh.js`;
2412
- if (!existsSync3(gitSshSource))
2572
+ chmodSync2(`${release}/dist/fz-agent.js`, 493);
2573
+ const gitSshSource = `${dirname5(operation.source)}/fz-git-ssh.js`;
2574
+ if (!existsSync4(gitSshSource))
2413
2575
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
2414
2576
  copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
2415
- chmodSync(`${release}/dist/fz-git-ssh.js`, 493);
2577
+ chmodSync2(`${release}/dist/fz-git-ssh.js`, 493);
2416
2578
  const pending = "/opt/forgezero/agent/current.next";
2417
- rmSync3(pending, { force: true });
2579
+ rmSync4(pending, { force: true });
2418
2580
  symlinkSync(`versions/${operation.version}`, pending);
2419
- renameSync3(pending, "/opt/forgezero/agent/current");
2420
- rmSync3(operation.binary, { force: true });
2581
+ renameSync4(pending, "/opt/forgezero/agent/current");
2582
+ rmSync4(operation.binary, { force: true });
2421
2583
  symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
2422
2584
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
2423
- rmSync3(gitSshBinary, { force: true });
2585
+ rmSync4(gitSshBinary, { force: true });
2424
2586
  symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
2425
2587
  return { stdout: "", exitCode: 0 };
2426
2588
  }
2427
2589
  if (operation.kind === "ensure-seed") {
2428
- if (existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
2590
+ if (existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
2429
2591
  return { stdout: "", exitCode: 0 };
2430
- const seed = randomBytes(32).toString("base64url");
2592
+ const seed = randomBytes2(32).toString("base64url");
2431
2593
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
2432
2594
  if (result.exitCode === 0)
2433
- chmodSync(operation.credential, 256);
2595
+ chmodSync2(operation.credential, 256);
2434
2596
  return result;
2435
2597
  }
2436
2598
  if (operation.kind === "ensure-git-identity") {
2437
2599
  const key = "/run/forgezero-git-deploy-key";
2438
2600
  const publicKey = `${key}.pub`;
2439
2601
  try {
2440
- if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
2441
- rmSync3(key, { force: true });
2442
- rmSync3(publicKey, { force: true });
2602
+ if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
2603
+ rmSync4(key, { force: true });
2604
+ rmSync4(publicKey, { force: true });
2443
2605
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
2444
2606
  if (result.exitCode !== 0)
2445
2607
  return result;
2446
2608
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
2447
2609
  if (result.exitCode !== 0)
2448
2610
  return result;
2449
- chmodSync(operation.credential, 256);
2611
+ chmodSync2(operation.credential, 256);
2450
2612
  }
2451
- if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
2452
- if (!existsSync3(key)) {
2613
+ if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
2614
+ if (!existsSync4(key)) {
2453
2615
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
2454
2616
  if (decrypted.exitCode !== 0)
2455
2617
  return decrypted;
@@ -2457,30 +2619,30 @@ var runProvisionOperation = async (operation) => {
2457
2619
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2458
2620
  if (derived.exitCode !== 0)
2459
2621
  return derived;
2460
- writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
2622
+ writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
2461
2623
  `, { mode: 292 });
2462
2624
  }
2463
2625
  return { stdout: "", exitCode: 0 };
2464
2626
  } finally {
2465
- rmSync3(key, { force: true });
2466
- rmSync3(publicKey, { force: true });
2627
+ rmSync4(key, { force: true });
2628
+ rmSync4(publicKey, { force: true });
2467
2629
  }
2468
2630
  }
2469
2631
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
2470
2632
  const key = "/run/forgezero-bootstrap-ssh-key";
2471
2633
  const generatedPublicKey = `${key}.pub`;
2472
2634
  try {
2473
- if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
2474
- rmSync3(key, { force: true });
2475
- rmSync3(generatedPublicKey, { force: true });
2635
+ if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
2636
+ rmSync4(key, { force: true });
2637
+ rmSync4(generatedPublicKey, { force: true });
2476
2638
  let result;
2477
2639
  if (operation.source) {
2478
- const source = existsSync3(operation.source) ? lstatSync(operation.source) : undefined;
2640
+ const source = existsSync4(operation.source) ? lstatSync2(operation.source) : undefined;
2479
2641
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
2480
2642
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
2481
2643
  }
2482
2644
  copyFileSync(operation.source, key);
2483
- chmodSync(key, 384);
2645
+ chmodSync2(key, 384);
2484
2646
  } else {
2485
2647
  result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
2486
2648
  if (result.exitCode !== 0)
@@ -2493,48 +2655,48 @@ var runProvisionOperation = async (operation) => {
2493
2655
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
2494
2656
  if (result.exitCode !== 0)
2495
2657
  return result;
2496
- chmodSync(operation.credential, 256);
2658
+ chmodSync2(operation.credential, 256);
2497
2659
  }
2498
- if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
2499
- if (!existsSync3(key)) {
2660
+ if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
2661
+ if (!existsSync4(key)) {
2500
2662
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
2501
2663
  if (decrypted.exitCode !== 0)
2502
2664
  return decrypted;
2503
- chmodSync(key, 384);
2665
+ chmodSync2(key, 384);
2504
2666
  }
2505
2667
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2506
2668
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
2507
2669
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
2508
2670
  }
2509
- mkdirSync3(dirname4(operation.publicKey), { recursive: true, mode: 493 });
2510
- writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
2671
+ mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
2672
+ writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
2511
2673
  `, { mode: 292 });
2512
- chmodSync(operation.publicKey, 292);
2674
+ chmodSync2(operation.publicKey, 292);
2513
2675
  }
2514
2676
  if (operation.source)
2515
- rmSync3(operation.source, { force: true });
2677
+ rmSync4(operation.source, { force: true });
2516
2678
  return { stdout: "", exitCode: 0 };
2517
2679
  } finally {
2518
- rmSync3(key, { force: true });
2519
- rmSync3(generatedPublicKey, { force: true });
2680
+ rmSync4(key, { force: true });
2681
+ rmSync4(generatedPublicKey, { force: true });
2520
2682
  }
2521
2683
  }
2522
2684
  if (operation.kind === "ensure-enrolment") {
2523
- if (existsSync3(operation.state) && lstatSync(operation.state).size > 0 || existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
2685
+ if (existsSync4(operation.state) && lstatSync2(operation.state).size > 0 || existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
2524
2686
  return { stdout: "", exitCode: 0 };
2525
- if (!existsSync3(operation.source))
2687
+ if (!existsSync4(operation.source))
2526
2688
  return { stdout: "enrolment source is missing", exitCode: 1 };
2527
2689
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
2528
2690
  if (result.exitCode === 0) {
2529
- chmodSync(operation.credential, 256);
2530
- rmSync3(operation.source, { force: true });
2691
+ chmodSync2(operation.credential, 256);
2692
+ rmSync4(operation.source, { force: true });
2531
2693
  }
2532
2694
  return result;
2533
2695
  }
2534
2696
  if (operation.kind === "wait-socket") {
2535
2697
  for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
2536
2698
  try {
2537
- if (lstatSync(operation.path).isSocket())
2699
+ if (lstatSync2(operation.path).isSocket())
2538
2700
  return { stdout: "", exitCode: 0 };
2539
2701
  } catch {}
2540
2702
  await Bun.sleep(operation.intervalMs);
@@ -2542,7 +2704,7 @@ var runProvisionOperation = async (operation) => {
2542
2704
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
2543
2705
  }
2544
2706
  if (operation.kind === "verify-file")
2545
- return existsSync3(operation.path) && lstatSync(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
2707
+ return existsSync4(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
2546
2708
  if (operation.kind === "verify-egress") {
2547
2709
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
2548
2710
  if (active.exitCode !== 0)
@@ -2564,25 +2726,25 @@ var runProvisionOperation = async (operation) => {
2564
2726
  }
2565
2727
  }
2566
2728
  if (operation.kind === "install-warp") {
2567
- const os = readFileSync3("/etc/os-release", "utf8");
2729
+ const os = readFileSync4("/etc/os-release", "utf8");
2568
2730
  if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
2569
2731
  return { stdout: "unsupported WARP host OS", exitCode: 1 };
2570
2732
  const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
2571
2733
  if (!response.ok)
2572
2734
  return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
2573
- mkdirSync3("/usr/share/keyrings", { recursive: true, mode: 493 });
2574
- mkdirSync3("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
2575
- mkdirSync3("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
2735
+ mkdirSync4("/usr/share/keyrings", { recursive: true, mode: 493 });
2736
+ mkdirSync4("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
2737
+ mkdirSync4("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
2576
2738
  const key = "/run/cloudflare-warp-key.gpg";
2577
- writeFileSync3(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
2739
+ writeFileSync4(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
2578
2740
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
2579
- rmSync3(key, { force: true });
2741
+ rmSync4(key, { force: true });
2580
2742
  if (result.exitCode !== 0)
2581
2743
  return result;
2582
2744
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
2583
2745
  if (!codename)
2584
2746
  return { stdout: "Ubuntu codename missing", exitCode: 1 };
2585
- writeFileSync3("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
2747
+ writeFileSync4("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
2586
2748
  `, { mode: 420 });
2587
2749
  result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
2588
2750
  return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
@@ -2597,7 +2759,7 @@ async function localRunner(operation) {
2597
2759
  if (capability.kind === "version")
2598
2760
  return fixed(capability.argv);
2599
2761
  try {
2600
- const metadata = lstatSync(capability.path);
2762
+ const metadata = lstatSync2(capability.path);
2601
2763
  const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
2602
2764
  return { stdout: present ? `yes
2603
2765
  ` : `no
@@ -2611,10 +2773,10 @@ function planInstall(options) {
2611
2773
  const { capabilities, ...unit } = options;
2612
2774
  return planProvision({ ...unit, mode: modeFor(capabilities) });
2613
2775
  }
2614
- async function applyPlan(plan, run) {
2776
+ async function applyPlan(plan, run2) {
2615
2777
  const transcript = [];
2616
2778
  for (const step2 of plan.steps) {
2617
- const result = await run(step2.operation);
2779
+ const result = await run2(step2.operation);
2618
2780
  transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
2619
2781
  if (result.exitCode !== 0 && !step2.optional) {
2620
2782
  throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
@@ -2708,8 +2870,6 @@ function validatePlatformSharedEnvironment(input) {
2708
2870
  databaseUser: input.databaseUser,
2709
2871
  sharedDirectory: input.sharedDirectory,
2710
2872
  seedSyncEpoch: input.seedSyncEpoch,
2711
- repository: input.repository,
2712
- branch: input.branch,
2713
2873
  deployProfile: input.deployProfile
2714
2874
  }))
2715
2875
  safeAtom(name, value);
@@ -2792,8 +2952,6 @@ function renderPlatformSharedEnvironment(input) {
2792
2952
  FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
2793
2953
  FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
2794
2954
  FZ_PROFILE: value.deployProfile,
2795
- FZ_REPO: value.repository,
2796
- FZ_BRANCH: value.branch,
2797
2955
  FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
2798
2956
  FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
2799
2957
  FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
@@ -3004,7 +3162,7 @@ var EXTRACT = "/run/forgezero-otelcol-release";
3004
3162
  var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
3005
3163
  var CONFIG = "/etc/forgezero/otelcol.yaml";
3006
3164
  var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
3007
- var checked = async (host, argv) => {
3165
+ var checked2 = async (host, argv) => {
3008
3166
  const result = await host.exec(argv);
3009
3167
  const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
3010
3168
  if (result.exitCode !== 0)
@@ -3086,7 +3244,7 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
3086
3244
  });
3087
3245
  const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
3088
3246
  if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
3089
- await checked(host, [
3247
+ await checked2(host, [
3090
3248
  "/usr/bin/curl",
3091
3249
  "--fail",
3092
3250
  "--silent",
@@ -3098,19 +3256,19 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
3098
3256
  ARCHIVE,
3099
3257
  `https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
3100
3258
  ]);
3101
- const digest = (await checked(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
3259
+ const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
3102
3260
  if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
3103
3261
  throw new Error("OTLP collector archive checksum mismatch");
3104
- await checked(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
3105
- await checked(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
3106
- await checked(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
3107
- await checked(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
3262
+ await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
3263
+ await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
3264
+ await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
3265
+ await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
3108
3266
  }
3109
3267
  if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
3110
- await checked(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
3268
+ await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
3111
3269
  }
3112
3270
  if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
3113
- await checked(host, [
3271
+ await checked2(host, [
3114
3272
  "/usr/sbin/useradd",
3115
3273
  "--system",
3116
3274
  "--no-create-home",
@@ -3123,8 +3281,8 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
3123
3281
  }
3124
3282
  host.write(CONFIG, rendered.config, 420);
3125
3283
  host.write(UNIT, rendered.unit, 420);
3126
- await checked(host, ["/usr/bin/systemctl", "daemon-reload"]);
3127
- await checked(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
3284
+ await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
3285
+ await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
3128
3286
  let ready = false;
3129
3287
  for (let attempt = 0;attempt < 100; attempt += 1) {
3130
3288
  const probe = await host.exec([
@@ -3253,7 +3411,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
3253
3411
  var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
3254
3412
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
3255
3413
  var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
3256
- var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
3414
+ var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
3257
3415
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
3258
3416
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
3259
3417
  var CONTROL_SOCKET = "/run/forgezero/control.sock";
@@ -3326,6 +3484,9 @@ function validateBootstrapConfig(value) {
3326
3484
  throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
3327
3485
  }
3328
3486
  if (value.kind === "enrolled-compute") {
3487
+ if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
3488
+ throw new Error("gitDeployKey must be boolean");
3489
+ }
3329
3490
  if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
3330
3491
  throw new Error("tenant realm is malformed");
3331
3492
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
@@ -3351,6 +3512,9 @@ function validateBootstrapConfig(value) {
3351
3512
  throw new Error("unsupported platform software profile");
3352
3513
  if (!["production", "development"].includes(value.environment))
3353
3514
  throw new Error("platform environment must be production or development");
3515
+ if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
3516
+ throw new Error("platform bootstrap requires absolute bundle and manifest paths");
3517
+ }
3354
3518
  let api;
3355
3519
  try {
3356
3520
  api = new URL(value.apiUrl);
@@ -3383,7 +3547,7 @@ function validateBootstrapConfig(value) {
3383
3547
  if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
3384
3548
  throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
3385
3549
  }
3386
- if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.repository !== value.repository || runtime.branch !== value.branch || runtime.databaseCoordinators.join(",") !== write.join(",")) {
3550
+ if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
3387
3551
  throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
3388
3552
  }
3389
3553
  if (runtime.deployProfile !== value.environment)
@@ -3413,6 +3577,7 @@ function planBootstrap(input, initialized = false) {
3413
3577
  ] : [
3414
3578
  ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
3415
3579
  { id: "bun", version: "1.3.14" },
3580
+ { id: "git", version: "ubuntu-26.04" },
3416
3581
  { id: "nginx", version: "ubuntu-26.04" },
3417
3582
  ...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
3418
3583
  ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
@@ -3441,7 +3606,7 @@ function planBootstrap(input, initialized = false) {
3441
3606
  ]
3442
3607
  };
3443
3608
  }
3444
- var checked2 = async (host, argv, label, options) => {
3609
+ var checked3 = async (host, argv, label, options) => {
3445
3610
  const result = await host.exec(argv, options);
3446
3611
  if (result.exitCode !== 0)
3447
3612
  throw new Error(`${label} failed: ${result.output.trim()}`);
@@ -3622,6 +3787,37 @@ async function seal(host, name, destination2, value) {
3622
3787
  if (result.exitCode !== 0)
3623
3788
  throw new Error(`could not seal ${name}: ${result.output.trim()}`);
3624
3789
  }
3790
+ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
3791
+ const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
3792
+ const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
3793
+ if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
3794
+ throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
3795
+ }
3796
+ if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
3797
+ throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
3798
+ }
3799
+ if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
3800
+ throw new Error("attended bootstrap bundle and manifest are required for release generation one");
3801
+ }
3802
+ let parsed;
3803
+ try {
3804
+ parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
3805
+ } catch {
3806
+ throw new Error("bootstrap bundle manifest is not valid JSON");
3807
+ }
3808
+ const manifest = parseBootstrapBundleManifest(parsed);
3809
+ const expectedBranch = config.environment === "production" ? "main" : "dev";
3810
+ if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
3811
+ throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
3812
+ }
3813
+ const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
3814
+ if (digest !== manifest.sha256)
3815
+ throw new Error("bootstrap bundle digest does not match its manifest");
3816
+ if (verifyGit) {
3817
+ await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
3818
+ }
3819
+ return manifest;
3820
+ }
3625
3821
  function bootstrapIdentity(config) {
3626
3822
  if (config.kind === "enrolled-compute")
3627
3823
  return {
@@ -3648,8 +3844,7 @@ function bootstrapIdentity(config) {
3648
3844
  computeReference: config.computeReference,
3649
3845
  nodeHostname: config.nodeHostname,
3650
3846
  apiUrl: config.apiUrl,
3651
- repository: config.repository,
3652
- branch: config.branch,
3847
+ bootstrapBundle: config.bootstrapBundle,
3653
3848
  deployRoot: config.deployRoot ?? "/opt/forgezero",
3654
3849
  telemetryEndpoint: config.telemetryEndpoint,
3655
3850
  database: {
@@ -3679,7 +3874,7 @@ function bootstrapIdentity(config) {
3679
3874
  };
3680
3875
  }
3681
3876
  function bootstrapIdentityDigest(config) {
3682
- return createHash("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
3877
+ return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
3683
3878
  }
3684
3879
  function parseStoredState(raw) {
3685
3880
  let value;
@@ -3729,34 +3924,6 @@ function bindBootstrapIntent(host, config) {
3729
3924
  }
3730
3925
  return identityDigest;
3731
3926
  }
3732
- async function preparePlatformBootstrap(input, host = localBootstrapHost()) {
3733
- const config = validateBootstrapConfig(structuredClone(input));
3734
- if (config.kind !== "platform")
3735
- throw new Error("platform preparation requires a platform bootstrap config");
3736
- if (host.uid() !== 0)
3737
- throw new Error("fz bootstrap platform prepare --apply must run as root");
3738
- if (host.exists(STATE_PATH)) {
3739
- const installed = parseStoredState(host.read(STATE_PATH));
3740
- if (installed.kind !== "platform" || installed.identityDigest !== bootstrapIdentityDigest(config)) {
3741
- throw new Error("platform preparation coordinates do not match the installed host identity");
3742
- }
3743
- }
3744
- const identityDigest = bindBootstrapIntent(host, config);
3745
- await host.installAgent(config);
3746
- if (!host.exists(GIT_PUBLIC_KEY))
3747
- throw new Error("Agent installation did not produce its public Git deploy key");
3748
- const gitPublicKey = host.read(GIT_PUBLIC_KEY).trim();
3749
- if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(gitPublicKey)) {
3750
- throw new Error("Agent public Git deploy key is malformed");
3751
- }
3752
- return {
3753
- kind: "platform",
3754
- prepared: true,
3755
- identityDigest,
3756
- gitPublicKey,
3757
- next: "register this read-only deploy key, then run --apply concurrently on all three genesis Agency members"
3758
- };
3759
- }
3760
3927
  function stateFor(config, cloudflare, previousCloudflareTunnelId) {
3761
3928
  return `${JSON.stringify({
3762
3929
  format: 2,
@@ -3910,6 +4077,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
3910
4077
  let installed;
3911
4078
  if (host.exists(STATE_PATH))
3912
4079
  installed = parseStoredState(host.read(STATE_PATH));
4080
+ const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
3913
4081
  let cloudflare;
3914
4082
  if (config.cloudflareHandoff) {
3915
4083
  if (host.exists(config.cloudflareHandoff.handoffFile)) {
@@ -3967,14 +4135,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
3967
4135
  const platformPrivate = config.kind === "platform" ? (() => {
3968
4136
  if (!secrets)
3969
4137
  throw new Error("platform apply requires attended credentials on stdin");
3970
- const checked3 = validatePlatformBootstrapSecrets(config, secrets);
4138
+ const checked4 = validatePlatformBootstrapSecrets(config, secrets);
3971
4139
  return {
3972
- root: checked3.clusterBootstrapCode,
3973
- email: checked3.emailSecret,
3974
- enrolmentToken: checked3.enrolmentToken,
3975
- backup: checked3.backupS3Secret,
3976
- cloudflareTunnelToken: checked3.cloudflareTunnelToken,
3977
- cloudflareApiToken: checked3.cloudflareApiToken
4140
+ root: checked4.clusterBootstrapCode,
4141
+ email: checked4.emailSecret,
4142
+ enrolmentToken: checked4.enrolmentToken,
4143
+ backup: checked4.backupS3Secret,
4144
+ cloudflareTunnelToken: checked4.cloudflareTunnelToken,
4145
+ cloudflareApiToken: checked4.cloudflareApiToken
3978
4146
  };
3979
4147
  })() : undefined;
3980
4148
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -4019,18 +4187,21 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4019
4187
  } else
4020
4188
  await host.ensureSoftware(plan.software);
4021
4189
  if (config.kind === "platform" && config.firewall.enabled) {
4022
- await checked2(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
4023
- await checked2(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
4024
- await checked2(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
4190
+ await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
4191
+ await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
4192
+ await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
4025
4193
  for (const cidr of config.firewall.privateCidrs) {
4026
4194
  if (config.database.role !== "none")
4027
- await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
4195
+ await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
4028
4196
  for (const port of [config.runtime.bluePort, config.runtime.greenPort])
4029
- await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
4197
+ await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
4030
4198
  }
4031
- await checked2(host, ["ufw", "--force", "enable"], "firewall activation");
4199
+ await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
4032
4200
  await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
4033
4201
  }
4202
+ if (config.kind === "platform" && bootstrapManifest) {
4203
+ await verifyBootstrapBundleOnHost(host, config, true);
4204
+ }
4034
4205
  if (config.kind === "platform") {
4035
4206
  const root = platformPrivate.root;
4036
4207
  if (!host.exists(JWT_CREDENTIAL)) {
@@ -4085,8 +4256,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4085
4256
  keepReleases: runtime.keepReleases,
4086
4257
  drainDeadlineMs: runtime.environment.drainDeadlineMs
4087
4258
  });
4088
- await checked2(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
4089
- await checked2(host, ["id", runtime.serviceUser], "existing API service account");
4259
+ await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
4260
+ await checked3(host, ["id", runtime.serviceUser], "existing API service account");
4090
4261
  });
4091
4262
  host.mkdir(runtime.environment.sharedDirectory, 488);
4092
4263
  host.mkdir(runtime.slotsDirectory, 493);
@@ -4095,7 +4266,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4095
4266
  host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
4096
4267
  `, 420);
4097
4268
  }
4098
- await checked2(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
4269
+ await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
4099
4270
  host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
4100
4271
  host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
4101
4272
  host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
@@ -4104,30 +4275,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4104
4275
  host.write("/etc/forgezero/deploy.env", activation.environment, 420);
4105
4276
  host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
4106
4277
  host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
4107
- await checked2(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
4278
+ await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
4108
4279
  const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
4109
- await checked2(host, telemetry.unitCheck.argv, "OTLP collector supervision");
4110
- const otlpStatus = (await checked2(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
4280
+ await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
4281
+ const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
4111
4282
  if (!/^2\d\d$/.test(otlpStatus))
4112
4283
  throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
4113
- await checked2(host, ["nginx", "-t"], "nginx configuration");
4114
- await checked2(host, ["systemctl", "daemon-reload"], "systemd reload");
4115
- await checked2(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
4284
+ await checked3(host, ["nginx", "-t"], "nginx configuration");
4285
+ await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
4286
+ await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
4116
4287
  if (config.database.role === "master") {
4117
4288
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
4118
4289
  if (!host.exists(invite)) {
4119
- host.write(invite, `plt_${randomBytes2(24).toString("hex")}
4290
+ host.write(invite, `plt_${randomBytes3(24).toString("hex")}
4120
4291
  `, 384);
4121
- await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
4292
+ await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
4122
4293
  }
4123
4294
  }
4124
4295
  if (config.database.role !== "none") {
4125
4296
  host.mkdir("/var/lib/forgezero-cluster", 448);
4126
- await checked2(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
4297
+ await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
4127
4298
  host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
4128
4299
  host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
4129
- await checked2(host, ["systemctl", "daemon-reload"], "database unit reload");
4130
- await checked2(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
4300
+ await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
4301
+ await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
4131
4302
  const evidence = {
4132
4303
  expectedMode: "default",
4133
4304
  role: "COORDINATOR",
@@ -4138,15 +4309,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4138
4309
  host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
4139
4310
  `, 384);
4140
4311
  }
4141
- await checked2(host, [
4142
- "runuser",
4143
- "-u",
4144
- "forgezero-agent",
4145
- "--",
4146
- "/usr/local/bin/fz-agent",
4147
- "deploy",
4148
- ...config.database.role === "master" ? ["--release-executor"] : []
4149
- ], "initial Agent deployment");
4312
+ if (bootstrapManifest) {
4313
+ await checked3(host, [
4314
+ "runuser",
4315
+ "-u",
4316
+ "forgezero-agent",
4317
+ "--",
4318
+ "/usr/local/bin/fz-agent",
4319
+ "deploy",
4320
+ `--revision=${bootstrapManifest.revision}`,
4321
+ ...config.database.role === "master" ? ["--release-executor"] : []
4322
+ ], "initial Agent deployment");
4323
+ host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
4324
+ format: 1,
4325
+ kind: "forgezero-bootstrap-release",
4326
+ revision: bootstrapManifest.revision,
4327
+ sha256: bootstrapManifest.sha256,
4328
+ branch: bootstrapManifest.branch,
4329
+ deployedAt: new Date().toISOString()
4330
+ }, null, 2)}
4331
+ `, 384);
4332
+ host.remove(config.bootstrapBundle.bundleFile);
4333
+ host.remove(config.bootstrapBundle.manifestFile);
4334
+ await host.installAgent(config);
4335
+ }
4150
4336
  }
4151
4337
  if (config.cloudflareHandoff) {
4152
4338
  if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
@@ -4155,8 +4341,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4155
4341
  if (!host.exists(TUNNEL_CREDENTIAL))
4156
4342
  throw new Error("sealed cloudflared connector credential is missing");
4157
4343
  host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
4158
- await checked2(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
4159
- await checked2(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
4344
+ await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
4345
+ await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
4160
4346
  const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
4161
4347
  if (!tunnelId)
4162
4348
  throw new Error("Cloudflare tunnel identity is missing after handoff validation");
@@ -4166,8 +4352,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4166
4352
  if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
4167
4353
  throw new Error("sealed Cloudflare Mesh connector credential is missing");
4168
4354
  host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
4169
- await checked2(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
4170
- await checked2(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
4355
+ await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
4356
+ await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
4171
4357
  }
4172
4358
  host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
4173
4359
  const status = await bootstrapStatus(host);
@@ -4212,8 +4398,7 @@ function strictBootstrapDocument(value) {
4212
4398
  "computeReference",
4213
4399
  "nodeHostname",
4214
4400
  "apiUrl",
4215
- "repository",
4216
- "branch",
4401
+ "bootstrapBundle",
4217
4402
  "deployRoot",
4218
4403
  "telemetryEndpoint",
4219
4404
  "database",
@@ -4226,9 +4411,11 @@ function strictBootstrapDocument(value) {
4226
4411
  "realm",
4227
4412
  "software",
4228
4413
  "deploymentCredentials",
4414
+ "gitDeployKey",
4229
4415
  "bootstrapRunner"
4230
4416
  ], "bootstrap config");
4231
4417
  if (root.kind === "platform") {
4418
+ exactKeys(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
4232
4419
  exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
4233
4420
  if (root.cloudflareHandoff !== undefined)
4234
4421
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
@@ -4270,8 +4457,6 @@ function strictBootstrapDocument(value) {
4270
4457
  "agentOtlpEndpoint",
4271
4458
  "custodianEmail",
4272
4459
  "email",
4273
- "repository",
4274
- "branch",
4275
4460
  "deployProfile",
4276
4461
  "otlpFlushIntervalMs",
4277
4462
  "otlpTraceSampleRatio",
@@ -4309,11 +4494,11 @@ function strictBootstrapDocument(value) {
4309
4494
  return value;
4310
4495
  }
4311
4496
  function readBootstrapConfig(path) {
4312
- const metadata = lstatSync2(path);
4497
+ const metadata = lstatSync3(path);
4313
4498
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
4314
4499
  throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
4315
4500
  }
4316
- return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync4(path, "utf8"))));
4501
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync5(path, "utf8"))));
4317
4502
  }
4318
4503
  function localBootstrapHost() {
4319
4504
  const execute = async (argv, options = {}) => {
@@ -4331,19 +4516,19 @@ function localBootstrapHost() {
4331
4516
  };
4332
4517
  return {
4333
4518
  uid: () => process.getuid?.() ?? -1,
4334
- exists: existsSync4,
4335
- read: (path) => readFileSync4(path, "utf8"),
4519
+ exists: existsSync5,
4520
+ read: (path) => readFileSync5(path, "utf8"),
4336
4521
  write(path, content, mode) {
4337
- mkdirSync4(dirname5(path), { recursive: true, mode: 493 });
4522
+ mkdirSync5(dirname6(path), { recursive: true, mode: 493 });
4338
4523
  const temporary = `${path}.next.${process.pid}`;
4339
- writeFileSync4(temporary, content, { mode });
4340
- chmodSync2(temporary, mode);
4341
- renameSync4(temporary, path);
4524
+ writeFileSync5(temporary, content, { mode });
4525
+ chmodSync3(temporary, mode);
4526
+ renameSync5(temporary, path);
4342
4527
  },
4343
- mkdir: (path, mode) => mkdirSync4(path, { recursive: true, mode }),
4344
- remove: (path) => rmSync4(path, { force: true }),
4528
+ mkdir: (path, mode) => mkdirSync5(path, { recursive: true, mode }),
4529
+ remove: (path) => rmSync5(path, { force: true }),
4345
4530
  inspect(path) {
4346
- const value = lstatSync2(path);
4531
+ const value = lstatSync3(path);
4347
4532
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
4348
4533
  },
4349
4534
  exec: execute,
@@ -4365,7 +4550,12 @@ function localBootstrapHost() {
4365
4550
  async installAgent(config) {
4366
4551
  const capabilities = await readCapabilities(localRunner);
4367
4552
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
4368
- const hasBinding = config.kind === "enrolled-compute" || existsSync4(ENROL_CREDENTIAL) || existsSync4("/var/lib/forgezero/enrolment.json");
4553
+ const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
4554
+ const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
4555
+ path: config.bootstrapBundle.bundleFile,
4556
+ manifestPath: config.bootstrapBundle.manifestFile,
4557
+ manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
4558
+ } : undefined;
4369
4559
  if (config.kind === "platform") {
4370
4560
  const lifecycle = config.database.role === "none" ? {
4371
4561
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -4377,8 +4567,8 @@ function localBootstrapHost() {
4377
4567
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
4378
4568
  databasePorts: [8529]
4379
4569
  };
4380
- mkdirSync4(dirname5(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
4381
- writeFileSync4(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
4570
+ mkdirSync5(dirname6(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
4571
+ writeFileSync5(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
4382
4572
  `, { mode: 256 });
4383
4573
  }
4384
4574
  const plan = planInstall({
@@ -4386,15 +4576,17 @@ function localBootstrapHost() {
4386
4576
  socketPath: DEFAULT_SOCKET2,
4387
4577
  seedPath: "/var/lib/forgezero/node.seed",
4388
4578
  controlSocketPath: "/run/forgezero/control.sock",
4389
- repository: config.repository,
4390
- branch: config.branch,
4579
+ repository: initialBundle?.path ?? (config.kind === "enrolled-compute" ? config.repository : undefined),
4580
+ branch: initialBundle?.manifest.branch ?? (config.kind === "enrolled-compute" ? config.branch : undefined),
4581
+ bootstrapBundlePath: initialBundle?.path,
4582
+ bootstrapBundleManifestPath: initialBundle?.manifestPath,
4391
4583
  profile: config.kind === "platform" ? config.profile : config.profile,
4392
4584
  deployRoot,
4393
4585
  deploymentCredentials: config.deploymentCredentials,
4394
4586
  publicApiUrl: config.apiUrl,
4395
- gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
4396
- gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
4397
- generateGitIdentity: true,
4587
+ gitCredentialPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
4588
+ gitPublicKeyPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
4589
+ generateGitIdentity: config.kind === "enrolled-compute" && config.gitDeployKey === true,
4398
4590
  pullDeployments: hasBinding,
4399
4591
  pullMigrations: config.kind === "platform" && hasBinding,
4400
4592
  pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
@@ -4408,9 +4600,11 @@ function localBootstrapHost() {
4408
4600
  telemetryEndpoint: config.telemetryEndpoint,
4409
4601
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
4410
4602
  sourceBinPath: PACKAGED_AGENT_BIN,
4603
+ ...existsSync5(ENROL_CREDENTIAL) || hasBinding ? {
4604
+ enrolTokenCredentialPath: existsSync5(ENROL_CREDENTIAL) ? ENROL_CREDENTIAL : undefined,
4605
+ enrolStatePath: "/var/lib/forgezero/enrolment.json"
4606
+ } : {},
4411
4607
  ...hasBinding ? {
4412
- enrolTokenCredentialPath: ENROL_CREDENTIAL,
4413
- enrolStatePath: "/var/lib/forgezero/enrolment.json",
4414
4608
  apiUrl: config.apiUrl,
4415
4609
  project: config.kind === "enrolled-compute" ? config.realm : "platform",
4416
4610
  environment: config.kind === "enrolled-compute" ? undefined : config.environment,
@@ -4418,8 +4612,8 @@ function localBootstrapHost() {
4418
4612
  } : {}
4419
4613
  });
4420
4614
  for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
4421
- mkdirSync4(dirname5(unit.path), { recursive: true, mode: 493 });
4422
- writeFileSync4(unit.path, unit.unit, { mode: 420 });
4615
+ mkdirSync5(dirname6(unit.path), { recursive: true, mode: 493 });
4616
+ writeFileSync5(unit.path, unit.unit, { mode: 420 });
4423
4617
  }
4424
4618
  await applyPlan(plan, localRunner);
4425
4619
  return plan;
@@ -4432,7 +4626,6 @@ export {
4432
4626
  validateBootstrapConfig,
4433
4627
  resolveInstalledBootstrapKind,
4434
4628
  readBootstrapConfig,
4435
- preparePlatformBootstrap,
4436
4629
  planBootstrap,
4437
4630
  localBootstrapHost,
4438
4631
  bootstrapStatus,