@forgezero/agent 0.1.86 → 0.1.88

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.
@@ -16,6 +16,7 @@ import {
16
16
  } from "node:fs";
17
17
  import { dirname, join, resolve } from "node:path";
18
18
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
19
+ var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
19
20
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
20
21
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
21
22
  var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
@@ -209,25 +210,90 @@ function restoreAgentRelease(staged) {
209
210
  rmSync(next, { force: true });
210
211
  }
211
212
  }
213
+ function selectAgentCandidate(staged) {
214
+ const link = join(dirname(staged.currentLink), "candidate");
215
+ const next = `${link}.${randomUUID()}.next`;
216
+ try {
217
+ symlinkSync(staged.nextTarget, next);
218
+ renameSync(next, link);
219
+ syncPath(dirname(link));
220
+ } finally {
221
+ rmSync(next, { force: true });
222
+ }
223
+ }
224
+ function clearAgentCandidate(root = DEFAULT_AGENT_RELEASE_ROOT) {
225
+ rmSync(join(resolve(root), "candidate"), { force: true });
226
+ }
212
227
 
213
228
  // src/agent-update-helper.ts
214
- import { randomUUID as randomUUID2 } from "node:crypto";
229
+ import { randomUUID as randomUUID3 } from "node:crypto";
215
230
  import {
216
- chmodSync as chmodSync2,
231
+ chmodSync as chmodSync3,
217
232
  closeSync as closeSync2,
218
- existsSync as existsSync2,
233
+ existsSync as existsSync3,
219
234
  fsyncSync as fsyncSync2,
220
235
  mkdirSync as mkdirSync2,
221
236
  openSync as openSync2,
222
237
  readFileSync as readFileSync2,
223
- renameSync as renameSync2,
224
- rmSync as rmSync2,
225
- unlinkSync,
238
+ renameSync as renameSync3,
239
+ rmSync as rmSync3,
240
+ unlinkSync as unlinkSync2,
226
241
  writeFileSync as writeFileSync2
227
242
  } from "node:fs";
228
- import { connect, createServer } from "node:net";
229
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
243
+ import { connect as connect2, createServer as createServer2 } from "node:net";
244
+ import { dirname as dirname3, join as join2, resolve as resolve2 } from "node:path";
230
245
  import { DEFAULT_SOCKET } from "@forgezero/vault";
246
+
247
+ // src/agent-handover.ts
248
+ import { randomUUID as randomUUID2 } from "node:crypto";
249
+ import { chmodSync as chmodSync2, existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2, symlinkSync as symlinkSync2, unlinkSync } from "node:fs";
250
+ import { basename, dirname as dirname2 } from "node:path";
251
+ import { connect, createServer } from "node:net";
252
+ var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
253
+ function switchAgentSocketRoute(route, backend) {
254
+ if (dirname2(route) !== dirname2(backend))
255
+ throw new Error("Agent handover sockets must share one runtime directory");
256
+ const target = basename(backend);
257
+ const next = `${route}.${randomUUID2()}.next`;
258
+ try {
259
+ symlinkSync2(target, next);
260
+ renameSync2(next, route);
261
+ } finally {
262
+ rmSync2(next, { force: true });
263
+ }
264
+ }
265
+ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET, timeoutMs = 5000) {
266
+ return new Promise((resolve2) => {
267
+ const socket = connect(socketPath);
268
+ let buffer = "";
269
+ let settled = false;
270
+ const finish = (value) => {
271
+ if (settled)
272
+ return;
273
+ settled = true;
274
+ clearTimeout(timer);
275
+ socket.destroy();
276
+ resolve2(value);
277
+ };
278
+ const timer = setTimeout(() => finish(false), timeoutMs);
279
+ socket.on("data", (chunk) => {
280
+ buffer += chunk.toString("utf8");
281
+ const newline = buffer.indexOf(`
282
+ `);
283
+ if (newline < 0)
284
+ return;
285
+ try {
286
+ const value = JSON.parse(buffer.slice(0, newline));
287
+ finish(value.version === expected.version && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.includes(value.vault));
288
+ } catch {
289
+ finish(false);
290
+ }
291
+ });
292
+ socket.on("error", () => finish(false));
293
+ });
294
+ }
295
+
296
+ // src/agent-update-helper.ts
231
297
  var AGENT_UPDATE_GROUP = "forgezero-update";
232
298
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
233
299
  var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
@@ -322,13 +388,13 @@ function validateJournal(value, root) {
322
388
  return journal;
323
389
  }
324
390
  function readJournal(path, root) {
325
- if (!existsSync2(path))
391
+ if (!existsSync3(path))
326
392
  return;
327
393
  return validateJournal(JSON.parse(readFileSync2(path, "utf8")), root);
328
394
  }
329
395
  function writeAtomic(path, value, mode) {
330
- mkdirSync2(dirname2(path), { recursive: true, mode: 493 });
331
- const next = `${path}.${randomUUID2()}.next`;
396
+ mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
397
+ const next = `${path}.${randomUUID3()}.next`;
332
398
  let file;
333
399
  try {
334
400
  file = openSync2(next, "wx", mode);
@@ -337,8 +403,8 @@ function writeAtomic(path, value, mode) {
337
403
  fsyncSync2(file);
338
404
  closeSync2(file);
339
405
  file = undefined;
340
- renameSync2(next, path);
341
- const directory = openSync2(dirname2(path), "r");
406
+ renameSync3(next, path);
407
+ const directory = openSync2(dirname3(path), "r");
342
408
  try {
343
409
  fsyncSync2(directory);
344
410
  } finally {
@@ -347,7 +413,7 @@ function writeAtomic(path, value, mode) {
347
413
  } finally {
348
414
  if (file !== undefined)
349
415
  closeSync2(file);
350
- rmSync2(next, { force: true });
416
+ rmSync3(next, { force: true });
351
417
  }
352
418
  }
353
419
  var publicReceipt = (journal) => {
@@ -380,7 +446,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
380
446
  }
381
447
  function readAgentUpdateReceipt(path = AGENT_UPDATE_RECEIPT) {
382
448
  try {
383
- if (!existsSync2(path))
449
+ if (!existsSync3(path))
384
450
  return;
385
451
  return validateReceipt(JSON.parse(readFileSync2(path, "utf8")));
386
452
  } catch {
@@ -420,10 +486,37 @@ var restartAgent = async (target, run) => {
420
486
  throw new Error(`systemd could not restart ${service}`);
421
487
  }
422
488
  };
489
+ var candidateUnit = (target) => target === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
490
+ var startCandidate = async (staged, target, expectedNodeKey, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
491
+ selectAgentCandidate(staged);
492
+ if (!await runOk(run, "/usr/bin/systemctl", ["restart", candidateUnit(target)])) {
493
+ clearAgentCandidate(dirname3(staged.currentLink));
494
+ throw new Error(`systemd could not start ${candidateUnit(target)}`);
495
+ }
496
+ if (!await probe({
497
+ version: staged.version,
498
+ nodeKey: expectedNodeKey,
499
+ bound: true,
500
+ vault: target === "compute" ? ["ready"] : ["unbound"]
501
+ })) {
502
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
503
+ clearAgentCandidate(dirname3(staged.currentLink));
504
+ throw new Error("the candidate Agent did not prove its identity and durable state");
505
+ }
506
+ };
507
+ var stopCandidate = async (staged, target, run) => {
508
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
509
+ clearAgentCandidate(dirname3(staged.currentLink));
510
+ };
511
+ var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
512
+ route: `${publicSocket}.backend`,
513
+ active: `${publicSocket}.backend.active`,
514
+ candidate: `${publicSocket}.backend.candidate`
515
+ });
423
516
  var targetProbe = (target, run) => target === "compute" ? () => probeAgentSocket() : async () => await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
424
517
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
425
518
  return new Promise((resolve3) => {
426
- const socket = connect(socketPath);
519
+ const socket = connect2(socketPath);
427
520
  let settled = false;
428
521
  let buffer = "";
429
522
  const finish = (value) => {
@@ -456,12 +549,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
456
549
  async function activateAgentRelease(staged, options = {}) {
457
550
  const run = options.run ?? runCommand;
458
551
  const target = options.target ?? "compute";
459
- const probe = options.probe ?? targetProbe(target, run);
552
+ const paths = socketPaths(options.publicSocketPath);
553
+ const probe = options.probe ?? (target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target, run));
460
554
  const now = options.now ?? Date.now;
461
555
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
462
556
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
463
- const previous = readJournal(journalPath, dirname2(staged.currentLink));
464
- const attemptId = options.attemptId ?? randomUUID2();
557
+ const previous = readJournal(journalPath, dirname3(staged.currentLink));
558
+ const attemptId = options.attemptId ?? randomUUID3();
465
559
  if (!ATTEMPT_ID.test(attemptId))
466
560
  throw new Error("Agent update attempt ID is invalid");
467
561
  const startedAtTs = now();
@@ -483,11 +577,19 @@ async function activateAgentRelease(staged, options = {}) {
483
577
  writeUpdateState(journalPath, receiptPath, journal);
484
578
  let selectionAttempted = false;
485
579
  try {
580
+ if (!options.candidatePrepared) {
581
+ throw new Error("direct activation requires a separately identity-bound candidate preparation");
582
+ }
583
+ if (target === "compute")
584
+ switchAgentSocketRoute(paths.route, paths.candidate);
486
585
  selectionAttempted = true;
487
586
  selectAgentRelease(staged);
488
587
  await restartAgent(target, run);
489
588
  if (!await probe())
490
- throw new Error("the replacement Agent did not answer its retained Vault socket");
589
+ throw new Error("the replacement Agent did not answer its active Vault backend");
590
+ if (target === "compute")
591
+ switchAgentSocketRoute(paths.route, paths.active);
592
+ await stopCandidate(staged, target, run);
491
593
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
492
594
  writeUpdateState(journalPath, receiptPath, journal);
493
595
  run({
@@ -505,10 +607,13 @@ async function activateAgentRelease(staged, options = {}) {
505
607
  restored = true;
506
608
  await restartAgent(target, run);
507
609
  rollbackHealthy = await probe();
610
+ if (rollbackHealthy && target === "compute")
611
+ switchAgentSocketRoute(paths.route, paths.active);
508
612
  } catch {
509
613
  rollbackHealthy = false;
510
614
  }
511
615
  }
616
+ await stopCandidate(staged, target, run).catch(() => {});
512
617
  const failures = failureCount + 1;
513
618
  const updatedAtTs = now();
514
619
  journal = {
@@ -528,28 +633,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
528
633
  const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
529
634
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
530
635
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
636
+ const run = options.run ?? runCommand;
531
637
  const journal = readJournal(journalPath, root);
532
- if (!journal)
638
+ if (!journal) {
639
+ for (const target of ["compute", "metal"]) {
640
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
641
+ }
642
+ clearAgentCandidate(root);
533
643
  return;
644
+ }
534
645
  if (journal.outcome !== "activating") {
646
+ await stopCandidate(stagedFromJournal(journal, root), journal.target, run).catch(() => {});
535
647
  writeAtomic(receiptPath, publicReceipt(journal), 416);
536
648
  return publicReceipt(journal);
537
649
  }
538
650
  const staged = stagedFromJournal(journal, root);
539
- if (!existsSync2(join2(root, journal.previousTarget))) {
651
+ if (!existsSync3(join2(root, journal.previousTarget))) {
540
652
  throw new Error("Agent update rollback release is missing");
541
653
  }
542
- const run = options.run ?? runCommand;
543
- const probe = options.probe ?? targetProbe(journal.target, run);
654
+ const paths = socketPaths(options.publicSocketPath);
655
+ const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run));
544
656
  restoreAgentRelease(staged);
545
657
  let rollbackHealthy = false;
546
658
  let failureMessage = "activation was interrupted before its health verdict became durable";
547
659
  try {
548
660
  await restartAgent(journal.target, run);
549
661
  rollbackHealthy = await probe();
662
+ if (rollbackHealthy && journal.target === "compute")
663
+ switchAgentSocketRoute(paths.route, paths.active);
550
664
  } catch (cause) {
551
665
  failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
552
666
  }
667
+ await stopCandidate(staged, journal.target, run).catch(() => {});
553
668
  const failures = journal.failureCount + 1;
554
669
  const updatedAtTs = (options.now ?? Date.now)();
555
670
  const recovered = {
@@ -566,15 +681,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
566
681
  }
567
682
  function startAgentUpdateHelper(options = {}) {
568
683
  const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
569
- if (existsSync2(socketPath))
570
- unlinkSync(socketPath);
571
- mkdirSync2(dirname2(socketPath), { recursive: true, mode: 488 });
572
- const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
684
+ if (existsSync3(socketPath))
685
+ unlinkSync2(socketPath);
686
+ mkdirSync2(dirname3(socketPath), { recursive: true, mode: 488 });
573
687
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
574
688
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
575
689
  const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
576
- const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, { target, attemptId, journalPath, receiptPath, now: options.now }));
690
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
691
+ const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, {
692
+ target,
693
+ attemptId,
694
+ journalPath,
695
+ receiptPath,
696
+ now: options.now,
697
+ candidatePrepared: true
698
+ }));
577
699
  let busy = true;
700
+ let pending;
578
701
  let blocked;
579
702
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
580
703
  root: releaseRoot,
@@ -586,7 +709,7 @@ function startAgentUpdateHelper(options = {}) {
586
709
  }).finally(() => {
587
710
  busy = false;
588
711
  });
589
- const server = createServer((socket) => {
712
+ const server = createServer2((socket) => {
590
713
  let buffer = "";
591
714
  socket.on("data", (chunk) => {
592
715
  buffer += chunk.toString("utf8");
@@ -605,16 +728,52 @@ function startAgentUpdateHelper(options = {}) {
605
728
  Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
606
729
  if (blocked)
607
730
  throw new Error(`update journal needs operator recovery: ${blocked}`);
608
- if (busy)
609
- throw new Error("another Agent update or recovery is already active");
610
- if (request.op !== "apply")
611
- throw new Error("unknown update operation");
612
731
  if (request.target !== "compute" && request.target !== "metal") {
613
732
  throw new Error("agent update target is invalid");
614
733
  }
615
- const attemptId = request.attemptId ?? randomUUID2();
734
+ const attemptId = request.attemptId ?? randomUUID3();
616
735
  if (!ATTEMPT_ID.test(attemptId))
617
736
  throw new Error("Agent update attempt ID is invalid");
737
+ if (request.op === "commit" || request.op === "abort") {
738
+ if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
739
+ throw new Error("Agent update handover does not match the prepared candidate");
740
+ }
741
+ const selected = pending;
742
+ pending = undefined;
743
+ if (request.op === "abort") {
744
+ await stopCandidate(selected.staged, selected.target, runCommand);
745
+ busy = false;
746
+ const response3 = {
747
+ ok: true,
748
+ status: "aborted",
749
+ version: selected.staged.version,
750
+ attemptId
751
+ };
752
+ socket.end(`${JSON.stringify(response3)}
753
+ `);
754
+ return;
755
+ }
756
+ const outcome = await activate(selected.staged, selected.target, attemptId);
757
+ busy = false;
758
+ if (outcome?.ok === false)
759
+ throw new Error(outcome.reason ?? "Agent handover failed");
760
+ const response2 = {
761
+ ok: true,
762
+ status: "active",
763
+ version: selected.staged.version,
764
+ attemptId
765
+ };
766
+ socket.end(`${JSON.stringify(response2)}
767
+ `);
768
+ return;
769
+ }
770
+ if (request.op !== "prepare")
771
+ throw new Error("unknown update operation");
772
+ if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
773
+ throw new Error("Agent update expected node identity is invalid");
774
+ }
775
+ if (busy)
776
+ throw new Error("another Agent update or recovery is already active");
618
777
  const prior = readJournal(journalPath, releaseRoot);
619
778
  const now = (options.now ?? Date.now)();
620
779
  if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
@@ -625,17 +784,23 @@ function startAgentUpdateHelper(options = {}) {
625
784
  currentVersion: request.currentVersion,
626
785
  root: releaseRoot
627
786
  });
628
- const response = { ok: true, status: "staged", version: staged.version, attemptId };
787
+ await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
788
+ pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
789
+ ownsBusy = false;
790
+ setTimer(() => {
791
+ if (pending?.attemptId !== attemptId)
792
+ return;
793
+ const expired = pending;
794
+ pending = undefined;
795
+ stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
796
+ busy = false;
797
+ });
798
+ }, 180000);
799
+ const response = { ok: true, status: "prepared", version: staged.version, attemptId };
629
800
  socket.end(`${JSON.stringify(response)}
630
801
  `);
631
- setTimer(() => void activate(staged, request.target, attemptId).catch((cause) => {
632
- blocked = cause instanceof Error ? cause.message : String(cause);
633
- }).finally(() => {
634
- busy = false;
635
- }), 100);
636
- ownsBusy = false;
637
802
  }).catch((cause) => {
638
- if (ownsBusy)
803
+ if (ownsBusy && !pending)
639
804
  busy = false;
640
805
  const response = {
641
806
  ok: false,
@@ -647,12 +812,12 @@ function startAgentUpdateHelper(options = {}) {
647
812
  });
648
813
  socket.on("error", () => socket.destroy());
649
814
  });
650
- server.listen(socketPath, () => chmodSync2(socketPath, 432));
815
+ server.listen(socketPath, () => chmodSync3(socketPath, 432));
651
816
  return server;
652
817
  }
653
818
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
654
819
  return new Promise((resolve3, reject) => {
655
- const socket = connect(socketPath, () => socket.write(`${JSON.stringify(request)}
820
+ const socket = connect2(socketPath, () => socket.write(`${JSON.stringify(request)}
656
821
  `));
657
822
  let buffer = "";
658
823
  socket.setTimeout(timeoutMs, () => {
@@ -680,22 +845,22 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
680
845
  import { createHash as createHash2 } from "node:crypto";
681
846
  import {
682
847
  accessSync,
683
- chmodSync as chmodSync3,
848
+ chmodSync as chmodSync4,
684
849
  copyFileSync,
685
850
  createReadStream,
686
- existsSync as existsSync3,
851
+ existsSync as existsSync4,
687
852
  mkdtempSync,
688
853
  mkdirSync as mkdirSync3,
689
854
  readFileSync as readFileSync3,
690
- renameSync as renameSync3,
691
- rmSync as rmSync3,
855
+ renameSync as renameSync4,
856
+ rmSync as rmSync4,
692
857
  statSync,
693
- symlinkSync as symlinkSync2,
694
- unlinkSync as unlinkSync2,
858
+ symlinkSync as symlinkSync3,
859
+ unlinkSync as unlinkSync3,
695
860
  writeFileSync as writeFileSync3
696
861
  } from "node:fs";
697
862
  import { tmpdir } from "node:os";
698
- import { dirname as dirname3, join as join3 } from "node:path";
863
+ import { dirname as dirname4, join as join3 } from "node:path";
699
864
  var PINNED_BUN_VERSION = "1.3.14";
700
865
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
701
866
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
@@ -868,23 +1033,23 @@ var softwareDownloadCommand = (url, destination, maximumBytes) => {
868
1033
  ];
869
1034
  };
870
1035
  var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
871
- if (existsSync3(destination))
1036
+ if (existsSync4(destination))
872
1037
  throw new Error("download destination already exists");
873
1038
  const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
874
1039
  if (received.exitCode !== 0) {
875
- rmSync3(destination, { force: true });
1040
+ rmSync4(destination, { force: true });
876
1041
  throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
877
1042
  }
878
- chmodSync3(destination, 384);
1043
+ chmodSync4(destination, 384);
879
1044
  if (statSync(destination).size > maximumBytes) {
880
- rmSync3(destination, { force: true });
1045
+ rmSync4(destination, { force: true });
881
1046
  throw new Error("download exceeds reviewed size bound");
882
1047
  }
883
1048
  const digest = createHash2("sha256");
884
1049
  for await (const chunk of createReadStream(destination))
885
1050
  digest.update(chunk);
886
1051
  if (digest.digest("hex") !== sha256) {
887
- rmSync3(destination, { force: true });
1052
+ rmSync4(destination, { force: true });
888
1053
  throw new Error("download checksum mismatch");
889
1054
  }
890
1055
  };
@@ -900,12 +1065,12 @@ var aptInstallMany = async (names) => {
900
1065
  return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
901
1066
  };
902
1067
  var writeOwnedPolicy = (pathValue, content, mode = 420) => {
903
- if (existsSync3(pathValue)) {
1068
+ if (existsSync4(pathValue)) {
904
1069
  if (readFileSync3(pathValue, "utf8") !== content)
905
1070
  throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
906
1071
  return;
907
1072
  }
908
- mkdirSync3(dirname3(pathValue), { recursive: true, mode: 493 });
1073
+ mkdirSync3(dirname4(pathValue), { recursive: true, mode: 493 });
909
1074
  writeFileSync3(pathValue, content, { mode, flag: "wx" });
910
1075
  };
911
1076
  var installContainerdRuntime = async (directory, osVersion) => {
@@ -917,7 +1082,7 @@ var installContainerdRuntime = async (directory, osVersion) => {
917
1082
  const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
918
1083
  if (nerdctlExtract.exitCode !== 0)
919
1084
  return nerdctlExtract;
920
- chmodSync3("/usr/local/bin/nerdctl", 493);
1085
+ chmodSync4("/usr/local/bin/nerdctl", 493);
921
1086
  const buildkit = join3(directory, "buildkit.tar.gz");
922
1087
  await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
923
1088
  const unpacked = join3(directory, "buildkit");
@@ -927,15 +1092,15 @@ var installContainerdRuntime = async (directory, osVersion) => {
927
1092
  return buildkitExtract;
928
1093
  for (const binary of ["buildctl", "buildkitd"]) {
929
1094
  copyFileSync(join3(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
930
- chmodSync3(`/usr/local/bin/${binary}`, 493);
1095
+ chmodSync4(`/usr/local/bin/${binary}`, 493);
931
1096
  }
932
1097
  mkdirSync3("/etc/containerd", { recursive: true, mode: 493 });
933
1098
  const expected = CONTAINERD_CONFIG(false);
934
1099
  const kataExpected = CONTAINERD_CONFIG(true);
935
- if (existsSync3(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
1100
+ if (existsSync4(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
936
1101
  return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
937
1102
  }
938
- if (!existsSync3(CONTAINERD_CONFIG_PATH))
1103
+ if (!existsSync4(CONTAINERD_CONFIG_PATH))
939
1104
  writeFileSync3(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
940
1105
  writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
941
1106
  mkdirSync3("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
@@ -948,10 +1113,10 @@ var installContainerdRuntime = async (directory, osVersion) => {
948
1113
  };
949
1114
  var installKataRuntime = async (directory) => {
950
1115
  for (const pathValue of ["/dev/kvm", "/dev/sev"])
951
- if (!existsSync3(pathValue))
1116
+ if (!existsSync4(pathValue))
952
1117
  return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
953
1118
  for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
954
- if (!existsSync3(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
1119
+ if (!existsSync4(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
955
1120
  return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
956
1121
  }
957
1122
  const dependencies = await aptInstallMany(["zstd"]);
@@ -966,7 +1131,7 @@ var installKataRuntime = async (directory) => {
966
1131
  const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
967
1132
  if (extracted.exitCode !== 0)
968
1133
  return extracted;
969
- if (!existsSync3("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync3(KATA_SNP_CONFIG_PATH)) {
1134
+ if (!existsSync4("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync4(KATA_SNP_CONFIG_PATH)) {
970
1135
  return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
971
1136
  }
972
1137
  const snpConfig = readFileSync3(KATA_SNP_CONFIG_PATH, "utf8");
@@ -974,19 +1139,19 @@ var installKataRuntime = async (directory) => {
974
1139
  return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
975
1140
  }
976
1141
  mkdirSync3("/etc/kata-containers", { recursive: true, mode: 493 });
977
- if (existsSync3(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
1142
+ if (existsSync4(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
978
1143
  return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
979
1144
  }
980
- if (!existsSync3(KATA_ACTIVE_CONFIG_PATH))
1145
+ if (!existsSync4(KATA_ACTIVE_CONFIG_PATH))
981
1146
  copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
982
- chmodSync3(KATA_ACTIVE_CONFIG_PATH, 420);
1147
+ chmodSync4(KATA_ACTIVE_CONFIG_PATH, 420);
983
1148
  try {
984
- unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
1149
+ unlinkSync3("/usr/local/bin/containerd-shim-kata-v2");
985
1150
  } catch {}
986
- symlinkSync2("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
1151
+ symlinkSync3("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
987
1152
  const base = CONTAINERD_CONFIG(false);
988
1153
  const kata = CONTAINERD_CONFIG(true);
989
- if (!existsSync3(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
1154
+ if (!existsSync4(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
990
1155
  return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
991
1156
  }
992
1157
  if (readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
@@ -1006,7 +1171,7 @@ async function executeSoftwareOperation(operation) {
1006
1171
  const binary = await run(["/usr/bin/docker", "--version"]);
1007
1172
  if (!successful(binary, /Docker version/))
1008
1173
  return { ...binary, exitCode: 1 };
1009
- if (!existsSync3(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
1174
+ if (!existsSync4(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
1010
1175
  return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
1011
1176
  const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
1012
1177
  return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
@@ -1019,17 +1184,17 @@ async function executeSoftwareOperation(operation) {
1019
1184
  run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
1020
1185
  run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
1021
1186
  ]);
1022
- const policy = existsSync3(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1187
+ const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1023
1188
  const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
1024
1189
  return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
1025
1190
  }
1026
1191
  if (software === "kata-containers") {
1027
1192
  const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
1028
- const policy = existsSync3(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1029
- const activeConfig = existsSync3(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
1030
- const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync3) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync3(parameter) && /^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()));
1193
+ const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1194
+ const activeConfig = existsSync4(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
1195
+ const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync4) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync4(parameter) && /^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()));
1031
1196
  const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
1032
- return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync3(KATA_SNP_CONFIG_PATH) ? readFileSync3(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
1197
+ return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync4(KATA_SNP_CONFIG_PATH) ? readFileSync3(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
1033
1198
  }
1034
1199
  if (software === "nginx") {
1035
1200
  const binary = await run(["/usr/sbin/nginx", "-v"]);
@@ -1070,10 +1235,10 @@ async function executeSoftwareOperation(operation) {
1070
1235
  return installed;
1071
1236
  if (software === "docker") {
1072
1237
  mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
1073
- if (existsSync3(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
1238
+ if (existsSync4(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
1074
1239
  return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
1075
1240
  }
1076
- if (!existsSync3(DOCKER_DAEMON_PATH))
1241
+ if (!existsSync4(DOCKER_DAEMON_PATH))
1077
1242
  writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
1078
1243
  const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
1079
1244
  return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
@@ -1123,20 +1288,20 @@ async function executeSoftwareOperation(operation) {
1123
1288
  return unzipped;
1124
1289
  mkdirSync3("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
1125
1290
  copyFileSync(join3(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
1126
- chmodSync3("/usr/local/lib/forgezero/runtime/bun.next", 493);
1127
- renameSync3("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
1291
+ chmodSync4("/usr/local/lib/forgezero/runtime/bun.next", 493);
1292
+ renameSync4("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
1128
1293
  try {
1129
- unlinkSync2("/usr/local/bin/bun");
1294
+ unlinkSync3("/usr/local/bin/bun");
1130
1295
  } catch {}
1131
- symlinkSync2("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
1296
+ symlinkSync3("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
1132
1297
  return { exitCode: 0, output: "" };
1133
1298
  }
1134
1299
  if (software === "cloudflared") {
1135
1300
  const binary = join3(directory, "cloudflared");
1136
1301
  await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
1137
- chmodSync3(binary, 493);
1302
+ chmodSync4(binary, 493);
1138
1303
  copyFileSync(binary, "/usr/local/bin/cloudflared");
1139
- chmodSync3("/usr/local/bin/cloudflared", 493);
1304
+ chmodSync4("/usr/local/bin/cloudflared", 493);
1140
1305
  return { exitCode: 0, output: "" };
1141
1306
  }
1142
1307
  const deb = join3(directory, "arangodb.deb");
@@ -1149,7 +1314,7 @@ async function executeSoftwareOperation(operation) {
1149
1314
  await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
1150
1315
  return { exitCode: 0, output: "" };
1151
1316
  } finally {
1152
- rmSync3(directory, { recursive: true, force: true });
1317
+ rmSync4(directory, { recursive: true, force: true });
1153
1318
  }
1154
1319
  }
1155
1320
  function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -1648,9 +1813,9 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
1648
1813
 
1649
1814
  // src/deployment-connectivity.ts
1650
1815
  import { createHash as createHash3 } from "node:crypto";
1651
- import { mkdirSync as mkdirSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
1816
+ import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
1652
1817
  import { createConnection } from "node:net";
1653
- import { dirname as dirname4 } from "node:path";
1818
+ import { dirname as dirname5 } from "node:path";
1654
1819
 
1655
1820
  // src/process-input.ts
1656
1821
  async function writeAndCloseProcessInput(input, value) {
@@ -1671,7 +1836,7 @@ var checked2 = async (host, argv2, label) => {
1671
1836
  return result.output.trim();
1672
1837
  };
1673
1838
  async function sealWithSystemd(name, path2, value) {
1674
- mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
1839
+ mkdirSync4(dirname5(path2), { recursive: true, mode: 448 });
1675
1840
  const next = `${path2}.next`;
1676
1841
  const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
1677
1842
  stdin: "pipe",
@@ -1687,15 +1852,15 @@ async function sealWithSystemd(name, path2, value) {
1687
1852
  ]);
1688
1853
  if (exitCode !== 0)
1689
1854
  throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
1690
- renameSync4(next, path2);
1855
+ renameSync5(next, path2);
1691
1856
  }
1692
1857
  var defaultHost = {
1693
1858
  seal: sealWithSystemd,
1694
1859
  write(path2, content, mode) {
1695
- mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
1860
+ mkdirSync4(dirname5(path2), { recursive: true, mode: 493 });
1696
1861
  const next = `${path2}.next`;
1697
1862
  writeFileSync4(next, content, { mode });
1698
- renameSync4(next, path2);
1863
+ renameSync5(next, path2);
1699
1864
  },
1700
1865
  async exec(argv2) {
1701
1866
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -1927,14 +2092,14 @@ WantedBy=multi-user.target
1927
2092
  }
1928
2093
 
1929
2094
  // src/software-helper.ts
1930
- import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync3 } from "node:fs";
1931
- import { connect as connect2, createServer as createServer2 } from "node:net";
1932
- import { dirname as dirname7 } from "node:path";
2095
+ import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "node:fs";
2096
+ import { connect as connect3, createServer as createServer3 } from "node:net";
2097
+ import { dirname as dirname8 } from "node:path";
1933
2098
 
1934
2099
  // src/service-supervisor.ts
1935
2100
  import { createHash as createHash4 } from "node:crypto";
1936
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
1937
- import { dirname as dirname5, join as join4, resolve as resolve3, sep } from "node:path";
2101
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
2102
+ import { dirname as dirname6, join as join4, resolve as resolve3, sep } from "node:path";
1938
2103
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
1939
2104
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
1940
2105
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
@@ -1944,17 +2109,17 @@ var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
1944
2109
  var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
1945
2110
  var defaultHost2 = {
1946
2111
  write(path2, content, mode) {
1947
- mkdirSync5(dirname5(path2), { recursive: true, mode: 493 });
2112
+ mkdirSync5(dirname6(path2), { recursive: true, mode: 493 });
1948
2113
  const next = `${path2}.next`;
1949
2114
  writeFileSync5(next, content, { mode });
1950
- renameSync5(next, path2);
2115
+ renameSync6(next, path2);
1951
2116
  },
1952
2117
  read: (path2) => readFileSync4(path2, "utf8"),
1953
- exists: existsSync4,
1954
- list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
2118
+ exists: existsSync5,
2119
+ list: (path2) => existsSync5(path2) ? readdirSync(path2) : [],
1955
2120
  realpath: realpathSync,
1956
2121
  mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
1957
- remove: (path2) => rmSync4(path2, { force: true }),
2122
+ remove: (path2) => rmSync5(path2, { force: true }),
1958
2123
  async exec(argv2) {
1959
2124
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
1960
2125
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -2150,20 +2315,20 @@ async function activateSupervisedService(request, host = defaultHost2) {
2150
2315
 
2151
2316
  // src/container-supervisor.ts
2152
2317
  import { createHash as createHash5 } from "node:crypto";
2153
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
2154
- import { dirname as dirname6, resolve as resolve4, sep as sep2 } from "node:path";
2318
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
2319
+ import { dirname as dirname7, resolve as resolve4, sep as sep2 } from "node:path";
2155
2320
  var defaultHost3 = {
2156
2321
  realpath: realpathSync2,
2157
- exists: existsSync5,
2322
+ exists: existsSync6,
2158
2323
  read: (path2) => readFileSync5(path2, "utf8"),
2159
2324
  write(path2, content, mode) {
2160
- mkdirSync6(dirname6(path2), { recursive: true, mode: 493 });
2325
+ mkdirSync6(dirname7(path2), { recursive: true, mode: 493 });
2161
2326
  const next = `${path2}.next`;
2162
2327
  writeFileSync6(next, content, { mode });
2163
- renameSync6(next, path2);
2328
+ renameSync7(next, path2);
2164
2329
  },
2165
- remove: (path2) => rmSync5(path2, { force: true }),
2166
- list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
2330
+ remove: (path2) => rmSync6(path2, { force: true }),
2331
+ list: (path2) => existsSync6(path2) ? readdirSync2(path2) : [],
2167
2332
  mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
2168
2333
  async exec(argv2) {
2169
2334
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -2458,9 +2623,9 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
2458
2623
  var MAX_PENDING_REQUESTS = 128;
2459
2624
  function startSoftwareHelper(options = {}) {
2460
2625
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
2461
- if (existsSync6(socketPath))
2462
- unlinkSync3(socketPath);
2463
- mkdirSync7(dirname7(socketPath), { recursive: true, mode: 488 });
2626
+ if (existsSync7(socketPath))
2627
+ unlinkSync4(socketPath);
2628
+ mkdirSync7(dirname8(socketPath), { recursive: true, mode: 488 });
2464
2629
  const ensure = options.ensure ?? ensureSoftwareRequirements;
2465
2630
  const activate = options.activate ?? activateSupervisedService;
2466
2631
  const buildContainer = options.buildContainer ?? buildContainerImage;
@@ -2468,7 +2633,7 @@ function startSoftwareHelper(options = {}) {
2468
2633
  const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
2469
2634
  let tail = Promise.resolve();
2470
2635
  let pending = 0;
2471
- const server = createServer2((socket) => {
2636
+ const server = createServer3((socket) => {
2472
2637
  let buffer = "";
2473
2638
  socket.on("data", (chunk) => {
2474
2639
  buffer += chunk.toString("utf8");
@@ -2547,12 +2712,12 @@ function startSoftwareHelper(options = {}) {
2547
2712
  });
2548
2713
  socket.on("error", () => socket.destroy());
2549
2714
  });
2550
- server.listen(socketPath, () => chmodSync4(socketPath, 432));
2715
+ server.listen(socketPath, () => chmodSync5(socketPath, 432));
2551
2716
  return server;
2552
2717
  }
2553
2718
  function requestContainer(op, request, socketPath, timeoutMs) {
2554
2719
  return new Promise((resolve5, reject) => {
2555
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
2720
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
2556
2721
  `));
2557
2722
  let buffer = "";
2558
2723
  socket.setTimeout(timeoutMs, () => {
@@ -2590,7 +2755,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
2590
2755
  }
2591
2756
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
2592
2757
  return new Promise((resolve5, reject) => {
2593
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
2758
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
2594
2759
  `));
2595
2760
  let buffer = "";
2596
2761
  socket.setTimeout(timeoutMs, () => {
@@ -2619,7 +2784,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
2619
2784
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
2620
2785
  validateSoftwareRequirements(requirements);
2621
2786
  return new Promise((resolve5, reject) => {
2622
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2787
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2623
2788
  `));
2624
2789
  let buffer = "";
2625
2790
  socket.setTimeout(timeoutMs, () => {
@@ -2647,7 +2812,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2812
  }
2648
2813
 
2649
2814
  // src/version.ts
2650
- var VERSION3 = "0.1.86";
2815
+ var VERSION3 = "0.1.88";
2651
2816
 
2652
2817
  // src/egress-policy.ts
2653
2818
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -2763,6 +2928,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
2763
2928
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
2764
2929
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
2765
2930
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
2931
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
2766
2932
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
2767
2933
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
2768
2934
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -2874,6 +3040,7 @@ Type=simple
2874
3040
  User=root
2875
3041
  Group=${AGENT_UPDATE_GROUP}
2876
3042
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
3043
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
2877
3044
  ExecStart=${bin} update-helper
2878
3045
  Restart=always
2879
3046
  RestartSec=2
@@ -2918,13 +3085,11 @@ WantedBy=sockets.target
2918
3085
  `;
2919
3086
  }
2920
3087
  function agentSocketProxyUnit(options) {
2921
- const backend = agentBackendSocketPath(options.socketPath);
3088
+ const backend = agentRoutingSocketPath(options.socketPath);
2922
3089
  const user = options.user ?? "forgezero";
2923
3090
  return `[Unit]
2924
3091
  Description=ForgeZero application Vault socket proxy
2925
3092
  Documentation=https://www.forgezero.net/docs/agent
2926
- Requires=forgezero-agent.service
2927
- After=forgezero-agent.service
2928
3093
 
2929
3094
  [Service]
2930
3095
  User=${user}
@@ -2945,12 +3110,26 @@ RestrictAddressFamilies=AF_UNIX
2945
3110
  `;
2946
3111
  }
2947
3112
  function agentBackendSocketPath(publicSocketPath) {
3113
+ const socket = systemdPath(publicSocketPath, "agent socket");
3114
+ const backend = `${socket}.backend.active`;
3115
+ if (Buffer.byteLength(backend) > 100)
3116
+ throw new Error("agent socket path is too long for a Unix socket");
3117
+ return backend;
3118
+ }
3119
+ function agentRoutingSocketPath(publicSocketPath) {
2948
3120
  const socket = systemdPath(publicSocketPath, "agent socket");
2949
3121
  const backend = `${socket}.backend`;
2950
3122
  if (Buffer.byteLength(backend) > 100)
2951
3123
  throw new Error("agent socket path is too long for a Unix socket");
2952
3124
  return backend;
2953
3125
  }
3126
+ function agentCandidateSocketPath(publicSocketPath) {
3127
+ const socket = systemdPath(publicSocketPath, "agent socket");
3128
+ const backend = `${socket}.backend.candidate`;
3129
+ if (Buffer.byteLength(backend) > 100)
3130
+ throw new Error("agent socket path is too long for a Unix socket");
3131
+ return backend;
3132
+ }
2954
3133
  var systemdPath = (value, label) => {
2955
3134
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
2956
3135
  throw new Error(`invalid ${label} path`);
@@ -3234,7 +3413,11 @@ function agentUnit(options) {
3234
3413
  }
3235
3414
  const environment = [
3236
3415
  "NODE_ENV=production",
3237
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
3416
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
3417
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
3418
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
3419
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
3420
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
3238
3421
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
3239
3422
  `FZ_SEED_CREDENTIAL=agent-seed`,
3240
3423
  `FZ_AGENT_MODE=${options.mode}`,
@@ -3371,6 +3554,26 @@ ${deploymentWrites}
3371
3554
  WantedBy=multi-user.target
3372
3555
  `;
3373
3556
  }
3557
+ function agentCandidateUnit(options) {
3558
+ return agentUnit({
3559
+ ...options,
3560
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
3561
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
3562
+ handoverCandidate: true,
3563
+ gitCredentialPath: undefined,
3564
+ deploymentCredentials: {},
3565
+ bootstrapSshCredentialPath: undefined,
3566
+ bootstrapSshPublicKeyPath: undefined,
3567
+ pullBootstrap: false,
3568
+ pullDeployments: false,
3569
+ pullMigrations: false,
3570
+ lifecycleProfilePath: undefined,
3571
+ bootstrapTargetTelemetryEndpoint: undefined,
3572
+ repository: undefined,
3573
+ bootstrapBundlePath: undefined,
3574
+ bootstrapBundleManifestPath: undefined
3575
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
3576
+ }
3374
3577
  var renderOperation = (operation) => {
3375
3578
  if (operation.kind === "commands")
3376
3579
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -3506,6 +3709,7 @@ function planProvision(options) {
3506
3709
  auxiliaryUnits: [
3507
3710
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
3508
3711
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
3712
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
3509
3713
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
3510
3714
  ...options.enforceEgress ? [
3511
3715
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -3773,19 +3977,19 @@ function platformCommunityRehearsalDeletionClaims(seedGuests, rehearsalGuest) {
3773
3977
 
3774
3978
  // src/platform-bootstrap-runtime.ts
3775
3979
  import {
3776
- chmodSync as chmodSync5,
3980
+ chmodSync as chmodSync6,
3777
3981
  chownSync,
3778
3982
  copyFileSync as copyFileSync2,
3779
- existsSync as existsSync7,
3983
+ existsSync as existsSync8,
3780
3984
  lstatSync,
3781
3985
  mkdirSync as mkdirSync8,
3782
3986
  readFileSync as readFileSync6,
3783
3987
  readdirSync as readdirSync3,
3784
3988
  realpathSync as realpathSync4,
3785
- renameSync as renameSync7,
3786
- rmSync as rmSync6,
3989
+ renameSync as renameSync8,
3990
+ rmSync as rmSync7,
3787
3991
  statSync as statSync2,
3788
- symlinkSync as symlinkSync3,
3992
+ symlinkSync as symlinkSync4,
3789
3993
  writeFileSync as writeFileSync7
3790
3994
  } from "node:fs";
3791
3995
  import { join as join5 } from "node:path";
@@ -4216,13 +4420,13 @@ var platformExec = async (argv2) => {
4216
4420
  };
4217
4421
  var replaceLink = (path2, target) => {
4218
4422
  const pending = `${path2}.next`;
4219
- rmSync6(pending, { force: true });
4423
+ rmSync7(pending, { force: true });
4220
4424
  if (!target) {
4221
- rmSync6(path2, { force: true });
4425
+ rmSync7(path2, { force: true });
4222
4426
  return;
4223
4427
  }
4224
- symlinkSync3(target, pending);
4225
- renameSync7(pending, path2);
4428
+ symlinkSync4(target, pending);
4429
+ renameSync8(pending, path2);
4226
4430
  };
4227
4431
  var secureRelease = (path2, uid, gid) => {
4228
4432
  const visit = (current2) => {
@@ -4230,7 +4434,7 @@ var secureRelease = (path2, uid, gid) => {
4230
4434
  if (metadata.isSymbolicLink())
4231
4435
  throw new Error("release contains a symbolic link");
4232
4436
  chownSync(current2, uid, gid);
4233
- chmodSync5(current2, metadata.isDirectory() ? 365 : 292);
4437
+ chmodSync6(current2, metadata.isDirectory() ? 365 : 292);
4234
4438
  if (metadata.isDirectory())
4235
4439
  for (const name of readdirSync3(current2))
4236
4440
  visit(join5(current2, name));
@@ -4255,7 +4459,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4255
4459
  const slots = join5(normalized.root, "slots");
4256
4460
  mkdirSync8(slots, { recursive: true, mode: 493 });
4257
4461
  const slotFile = join5(normalized.root, ".forge-slot");
4258
- const previousSlot = existsSync7(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
4462
+ const previousSlot = existsSync8(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
4259
4463
  const target = previousSlot === "blue" ? "green" : "blue";
4260
4464
  const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
4261
4465
  const targetLink = join5(slots, target);
@@ -4295,25 +4499,25 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4295
4499
  }
4296
4500
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
4297
4501
  const backup = `${upstream}.forgezero-backup`;
4298
- if (existsSync7(upstream))
4502
+ if (existsSync8(upstream))
4299
4503
  copyFileSync2(upstream, backup);
4300
4504
  else
4301
- rmSync6(backup, { force: true });
4505
+ rmSync7(backup, { force: true });
4302
4506
  writeFileSync7(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
4303
4507
  `, { mode: 420 });
4304
4508
  const test = await exec(["/usr/sbin/nginx", "-t"]);
4305
4509
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
4306
4510
  if (reload.exitCode !== 0) {
4307
- if (existsSync7(backup))
4308
- renameSync7(backup, upstream);
4511
+ if (existsSync8(backup))
4512
+ renameSync8(backup, upstream);
4309
4513
  else
4310
- rmSync6(upstream, { force: true });
4514
+ rmSync7(upstream, { force: true });
4311
4515
  await exec(["/usr/sbin/nginx", "-t"]);
4312
4516
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
4313
4517
  await stopTarget();
4314
4518
  throw new Error("nginx refused the promoted upstream");
4315
4519
  }
4316
- rmSync6(backup, { force: true });
4520
+ rmSync7(backup, { force: true });
4317
4521
  writeFileSync7(slotFile, `${target}
4318
4522
  `, { mode: 420 });
4319
4523
  if (previousSlot && previousSlot !== target) {
@@ -4323,7 +4527,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4323
4527
  const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs).slice(normalized.keepReleases);
4324
4528
  for (const path2 of old)
4325
4529
  if (path2 !== release)
4326
- rmSync6(path2, { recursive: true, force: true });
4530
+ rmSync7(path2, { recursive: true, force: true });
4327
4531
  return { release, slot: target };
4328
4532
  }
4329
4533
  function validateCollectorUnit(unit2) {
@@ -4369,34 +4573,34 @@ import { lstatSync as lstatSync5 } from "node:fs";
4369
4573
  // src/bootstrap.ts
4370
4574
  import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
4371
4575
  import {
4372
- chmodSync as chmodSync8,
4373
- existsSync as existsSync10,
4576
+ chmodSync as chmodSync9,
4577
+ existsSync as existsSync11,
4374
4578
  lstatSync as lstatSync4,
4375
4579
  mkdirSync as mkdirSync11,
4376
4580
  readFileSync as readFileSync9,
4377
- renameSync as renameSync10,
4378
- rmSync as rmSync9,
4581
+ renameSync as renameSync11,
4582
+ rmSync as rmSync10,
4379
4583
  writeFileSync as writeFileSync10
4380
4584
  } from "node:fs";
4381
- import { dirname as dirname11 } from "node:path";
4585
+ import { dirname as dirname12 } from "node:path";
4382
4586
  import { fileURLToPath } from "node:url";
4383
4587
 
4384
4588
  // src/cli/agent-install.ts
4385
4589
  import { randomBytes } from "node:crypto";
4386
4590
  import {
4387
- chmodSync as chmodSync6,
4591
+ chmodSync as chmodSync7,
4388
4592
  copyFileSync as copyFileSync3,
4389
- existsSync as existsSync8,
4593
+ existsSync as existsSync9,
4390
4594
  lstatSync as lstatSync2,
4391
4595
  mkdirSync as mkdirSync9,
4392
4596
  readFileSync as readFileSync7,
4393
4597
  realpathSync as realpathSync5,
4394
- renameSync as renameSync8,
4395
- rmSync as rmSync7,
4396
- symlinkSync as symlinkSync4,
4598
+ renameSync as renameSync9,
4599
+ rmSync as rmSync8,
4600
+ symlinkSync as symlinkSync5,
4397
4601
  writeFileSync as writeFileSync8
4398
4602
  } from "node:fs";
4399
- import { dirname as dirname8 } from "node:path";
4603
+ import { dirname as dirname9 } from "node:path";
4400
4604
  async function readCapabilities(run2) {
4401
4605
  const answers = {};
4402
4606
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -4454,51 +4658,51 @@ var runProvisionOperation = async (operation) => {
4454
4658
  if (operation.kind === "install-runtime") {
4455
4659
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
4456
4660
  mkdirSync9(`${release}/dist`, { recursive: true, mode: 493 });
4457
- mkdirSync9(dirname8(operation.binary), { recursive: true, mode: 493 });
4661
+ mkdirSync9(dirname9(operation.binary), { recursive: true, mode: 493 });
4458
4662
  copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
4459
- chmodSync6(`${release}/dist/fz-agent.js`, 493);
4460
- const gitSshSource = `${dirname8(operation.source)}/fz-git-ssh.js`;
4461
- if (!existsSync8(gitSshSource))
4663
+ chmodSync7(`${release}/dist/fz-agent.js`, 493);
4664
+ const gitSshSource = `${dirname9(operation.source)}/fz-git-ssh.js`;
4665
+ if (!existsSync9(gitSshSource))
4462
4666
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
4463
4667
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
4464
- chmodSync6(`${release}/dist/fz-git-ssh.js`, 493);
4668
+ chmodSync7(`${release}/dist/fz-git-ssh.js`, 493);
4465
4669
  const pending = "/opt/forgezero/agent/current.next";
4466
- rmSync7(pending, { force: true });
4467
- symlinkSync4(`versions/${operation.version}`, pending);
4468
- renameSync8(pending, "/opt/forgezero/agent/current");
4469
- rmSync7(operation.binary, { force: true });
4470
- symlinkSync4("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
4670
+ rmSync8(pending, { force: true });
4671
+ symlinkSync5(`versions/${operation.version}`, pending);
4672
+ renameSync9(pending, "/opt/forgezero/agent/current");
4673
+ rmSync8(operation.binary, { force: true });
4674
+ symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
4471
4675
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
4472
- rmSync7(gitSshBinary, { force: true });
4473
- symlinkSync4("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
4676
+ rmSync8(gitSshBinary, { force: true });
4677
+ symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
4474
4678
  return { stdout: "", exitCode: 0 };
4475
4679
  }
4476
4680
  if (operation.kind === "ensure-seed") {
4477
- if (existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
4681
+ if (existsSync9(operation.credential) && lstatSync2(operation.credential).size > 0)
4478
4682
  return { stdout: "", exitCode: 0 };
4479
4683
  const seed = randomBytes(32).toString("base64url");
4480
4684
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
4481
4685
  if (result.exitCode === 0)
4482
- chmodSync6(operation.credential, 256);
4686
+ chmodSync7(operation.credential, 256);
4483
4687
  return result;
4484
4688
  }
4485
4689
  if (operation.kind === "ensure-git-identity") {
4486
4690
  const key = "/run/forgezero-git-deploy-key";
4487
4691
  const publicKey2 = `${key}.pub`;
4488
4692
  try {
4489
- if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
4490
- rmSync7(key, { force: true });
4491
- rmSync7(publicKey2, { force: true });
4693
+ if (!existsSync9(operation.credential) || lstatSync2(operation.credential).size < 1) {
4694
+ rmSync8(key, { force: true });
4695
+ rmSync8(publicKey2, { force: true });
4492
4696
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
4493
4697
  if (result.exitCode !== 0)
4494
4698
  return result;
4495
4699
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
4496
4700
  if (result.exitCode !== 0)
4497
4701
  return result;
4498
- chmodSync6(operation.credential, 256);
4702
+ chmodSync7(operation.credential, 256);
4499
4703
  }
4500
- if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
4501
- if (!existsSync8(key)) {
4704
+ if (!existsSync9(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
4705
+ if (!existsSync9(key)) {
4502
4706
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
4503
4707
  if (decrypted.exitCode !== 0)
4504
4708
  return decrypted;
@@ -4511,25 +4715,25 @@ var runProvisionOperation = async (operation) => {
4511
4715
  }
4512
4716
  return { stdout: "", exitCode: 0 };
4513
4717
  } finally {
4514
- rmSync7(key, { force: true });
4515
- rmSync7(publicKey2, { force: true });
4718
+ rmSync8(key, { force: true });
4719
+ rmSync8(publicKey2, { force: true });
4516
4720
  }
4517
4721
  }
4518
4722
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
4519
4723
  const key = "/run/forgezero-bootstrap-ssh-key";
4520
4724
  const generatedPublicKey = `${key}.pub`;
4521
4725
  try {
4522
- if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
4523
- rmSync7(key, { force: true });
4524
- rmSync7(generatedPublicKey, { force: true });
4726
+ if (!existsSync9(operation.credential) || lstatSync2(operation.credential).size < 1) {
4727
+ rmSync8(key, { force: true });
4728
+ rmSync8(generatedPublicKey, { force: true });
4525
4729
  let result;
4526
4730
  if (operation.source) {
4527
- const source = existsSync8(operation.source) ? lstatSync2(operation.source) : undefined;
4731
+ const source = existsSync9(operation.source) ? lstatSync2(operation.source) : undefined;
4528
4732
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
4529
4733
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
4530
4734
  }
4531
4735
  copyFileSync3(operation.source, key);
4532
- chmodSync6(key, 384);
4736
+ chmodSync7(key, 384);
4533
4737
  } else {
4534
4738
  result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
4535
4739
  if (result.exitCode !== 0)
@@ -4542,41 +4746,41 @@ var runProvisionOperation = async (operation) => {
4542
4746
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
4543
4747
  if (result.exitCode !== 0)
4544
4748
  return result;
4545
- chmodSync6(operation.credential, 256);
4749
+ chmodSync7(operation.credential, 256);
4546
4750
  }
4547
- if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
4548
- if (!existsSync8(key)) {
4751
+ if (!existsSync9(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
4752
+ if (!existsSync9(key)) {
4549
4753
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
4550
4754
  if (decrypted.exitCode !== 0)
4551
4755
  return decrypted;
4552
- chmodSync6(key, 384);
4756
+ chmodSync7(key, 384);
4553
4757
  }
4554
4758
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
4555
4759
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
4556
4760
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
4557
4761
  }
4558
- mkdirSync9(dirname8(operation.publicKey), { recursive: true, mode: 493 });
4762
+ mkdirSync9(dirname9(operation.publicKey), { recursive: true, mode: 493 });
4559
4763
  writeFileSync8(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
4560
4764
  `, { mode: 292 });
4561
- chmodSync6(operation.publicKey, 292);
4765
+ chmodSync7(operation.publicKey, 292);
4562
4766
  }
4563
4767
  if (operation.source)
4564
- rmSync7(operation.source, { force: true });
4768
+ rmSync8(operation.source, { force: true });
4565
4769
  return { stdout: "", exitCode: 0 };
4566
4770
  } finally {
4567
- rmSync7(key, { force: true });
4568
- rmSync7(generatedPublicKey, { force: true });
4771
+ rmSync8(key, { force: true });
4772
+ rmSync8(generatedPublicKey, { force: true });
4569
4773
  }
4570
4774
  }
4571
4775
  if (operation.kind === "ensure-enrolment") {
4572
- if (existsSync8(operation.state) && lstatSync2(operation.state).size > 0 || existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
4776
+ if (existsSync9(operation.state) && lstatSync2(operation.state).size > 0 || existsSync9(operation.credential) && lstatSync2(operation.credential).size > 0)
4573
4777
  return { stdout: "", exitCode: 0 };
4574
- if (!existsSync8(operation.source))
4778
+ if (!existsSync9(operation.source))
4575
4779
  return { stdout: "enrolment source is missing", exitCode: 1 };
4576
4780
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
4577
4781
  if (result.exitCode === 0) {
4578
- chmodSync6(operation.credential, 256);
4579
- rmSync7(operation.source, { force: true });
4782
+ chmodSync7(operation.credential, 256);
4783
+ rmSync8(operation.source, { force: true });
4580
4784
  }
4581
4785
  return result;
4582
4786
  }
@@ -4591,7 +4795,7 @@ var runProvisionOperation = async (operation) => {
4591
4795
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
4592
4796
  }
4593
4797
  if (operation.kind === "verify-file")
4594
- return existsSync8(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
4798
+ return existsSync9(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
4595
4799
  if (operation.kind === "verify-egress") {
4596
4800
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
4597
4801
  if (active.exitCode !== 0)
@@ -4625,7 +4829,7 @@ var runProvisionOperation = async (operation) => {
4625
4829
  const key = "/run/cloudflare-warp-key.gpg";
4626
4830
  writeFileSync8(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
4627
4831
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
4628
- rmSync7(key, { force: true });
4832
+ rmSync8(key, { force: true });
4629
4833
  if (result.exitCode !== 0)
4630
4834
  return result;
4631
4835
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
@@ -4674,9 +4878,9 @@ async function applyPlan(plan, run2) {
4674
4878
 
4675
4879
  // src/cloudflare-bootstrap.ts
4676
4880
  import { constants } from "node:fs";
4677
- import { createHmac, randomUUID as randomUUID3 } from "node:crypto";
4881
+ import { createHmac, randomUUID as randomUUID4 } from "node:crypto";
4678
4882
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
4679
- import { dirname as dirname9, join as join6, resolve as resolve5 } from "node:path";
4883
+ import { dirname as dirname10, join as join6, resolve as resolve5 } from "node:path";
4680
4884
  import { isIP as isIP3 } from "node:net";
4681
4885
 
4682
4886
  // src/cloudflare-edge.ts
@@ -4692,17 +4896,17 @@ import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
4692
4896
  // src/bootstrap-bundle.ts
4693
4897
  import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
4694
4898
  import {
4695
- chmodSync as chmodSync7,
4899
+ chmodSync as chmodSync8,
4696
4900
  createReadStream as createReadStream2,
4697
- existsSync as existsSync9,
4901
+ existsSync as existsSync10,
4698
4902
  lstatSync as lstatSync3,
4699
4903
  mkdirSync as mkdirSync10,
4700
4904
  readFileSync as readFileSync8,
4701
- renameSync as renameSync9,
4702
- rmSync as rmSync8,
4905
+ renameSync as renameSync10,
4906
+ rmSync as rmSync9,
4703
4907
  writeFileSync as writeFileSync9
4704
4908
  } from "node:fs";
4705
- import { dirname as dirname10, isAbsolute, resolve as resolve6 } from "node:path";
4909
+ import { dirname as dirname11, isAbsolute, resolve as resolve6 } from "node:path";
4706
4910
  var BOOTSTRAP_BUNDLE_FORMAT = 1;
4707
4911
  var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
4708
4912
  var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
@@ -5007,17 +5211,17 @@ function localBootstrapHost() {
5007
5211
  };
5008
5212
  return {
5009
5213
  uid: () => process.getuid?.() ?? -1,
5010
- exists: existsSync10,
5214
+ exists: existsSync11,
5011
5215
  read: (path2) => readFileSync9(path2, "utf8"),
5012
5216
  write(path2, content, mode) {
5013
- mkdirSync11(dirname11(path2), { recursive: true, mode: 493 });
5217
+ mkdirSync11(dirname12(path2), { recursive: true, mode: 493 });
5014
5218
  const temporary = `${path2}.next.${process.pid}`;
5015
5219
  writeFileSync10(temporary, content, { mode });
5016
- chmodSync8(temporary, mode);
5017
- renameSync10(temporary, path2);
5220
+ chmodSync9(temporary, mode);
5221
+ renameSync11(temporary, path2);
5018
5222
  },
5019
5223
  mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
5020
- remove: (path2) => rmSync9(path2, { force: true }),
5224
+ remove: (path2) => rmSync10(path2, { force: true }),
5021
5225
  inspect(path2) {
5022
5226
  const value = lstatSync4(path2);
5023
5227
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
@@ -5040,9 +5244,9 @@ function localBootstrapHost() {
5040
5244
  },
5041
5245
  async installAgent(config, phase) {
5042
5246
  const capabilities = await readCapabilities(localRunner);
5043
- const hasBinding = existsSync10("/var/lib/forgezero/enrolment.json");
5044
- const hasEnrolCredential = existsSync10(ENROL_CREDENTIAL);
5045
- const initialBundle = config.kind === "platform" && !existsSync10(BOOTSTRAP_RELEASE_EVIDENCE) ? {
5247
+ const hasBinding = existsSync11("/var/lib/forgezero/enrolment.json");
5248
+ const hasEnrolCredential = existsSync11(ENROL_CREDENTIAL);
5249
+ const initialBundle = config.kind === "platform" && !existsSync11(BOOTSTRAP_RELEASE_EVIDENCE) ? {
5046
5250
  path: config.bootstrapBundle.bundleFile,
5047
5251
  manifestPath: config.bootstrapBundle.manifestFile,
5048
5252
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
@@ -5058,7 +5262,7 @@ function localBootstrapHost() {
5058
5262
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
5059
5263
  databasePorts: [8529]
5060
5264
  };
5061
- mkdirSync11(dirname11(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
5265
+ mkdirSync11(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
5062
5266
  writeFileSync10(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
5063
5267
  `, { mode: 256 });
5064
5268
  }
@@ -5069,7 +5273,7 @@ function localBootstrapHost() {
5069
5273
  initialBundle
5070
5274
  });
5071
5275
  for (const unit2 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
5072
- mkdirSync11(dirname11(unit2.path), { recursive: true, mode: 493 });
5276
+ mkdirSync11(dirname12(unit2.path), { recursive: true, mode: 493 });
5073
5277
  writeFileSync10(unit2.path, unit2.unit, { mode: 420 });
5074
5278
  }
5075
5279
  await applyPlan(plan, localRunner);