@forgezero/agent 0.1.85 → 0.1.87

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/provision.js CHANGED
@@ -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 && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].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,32 @@ 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, 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({ version: staged.version })) {
497
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
498
+ clearAgentCandidate(dirname3(staged.currentLink));
499
+ throw new Error("the candidate Agent did not prove its identity and durable state");
500
+ }
501
+ };
502
+ var stopCandidate = async (staged, target, run) => {
503
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
504
+ clearAgentCandidate(dirname3(staged.currentLink));
505
+ };
506
+ var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
507
+ route: `${publicSocket}.backend`,
508
+ active: `${publicSocket}.backend.active`,
509
+ candidate: `${publicSocket}.backend.candidate`
510
+ });
423
511
  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
512
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
425
513
  return new Promise((resolve3) => {
426
- const socket = connect(socketPath);
514
+ const socket = connect2(socketPath);
427
515
  let settled = false;
428
516
  let buffer = "";
429
517
  const finish = (value) => {
@@ -456,12 +544,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
456
544
  async function activateAgentRelease(staged, options = {}) {
457
545
  const run = options.run ?? runCommand;
458
546
  const target = options.target ?? "compute";
459
- const probe = options.probe ?? targetProbe(target, run);
547
+ const paths = socketPaths(options.publicSocketPath);
548
+ const probe = options.probe ?? (target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target, run));
460
549
  const now = options.now ?? Date.now;
461
550
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
462
551
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
463
- const previous = readJournal(journalPath, dirname2(staged.currentLink));
464
- const attemptId = options.attemptId ?? randomUUID2();
552
+ const previous = readJournal(journalPath, dirname3(staged.currentLink));
553
+ const attemptId = options.attemptId ?? randomUUID3();
465
554
  if (!ATTEMPT_ID.test(attemptId))
466
555
  throw new Error("Agent update attempt ID is invalid");
467
556
  const startedAtTs = now();
@@ -483,11 +572,18 @@ async function activateAgentRelease(staged, options = {}) {
483
572
  writeUpdateState(journalPath, receiptPath, journal);
484
573
  let selectionAttempted = false;
485
574
  try {
575
+ if (!options.candidatePrepared)
576
+ await startCandidate(staged, target, run);
577
+ if (target === "compute")
578
+ switchAgentSocketRoute(paths.route, paths.candidate);
486
579
  selectionAttempted = true;
487
580
  selectAgentRelease(staged);
488
581
  await restartAgent(target, run);
489
582
  if (!await probe())
490
- throw new Error("the replacement Agent did not answer its retained Vault socket");
583
+ throw new Error("the replacement Agent did not answer its active Vault backend");
584
+ if (target === "compute")
585
+ switchAgentSocketRoute(paths.route, paths.active);
586
+ await stopCandidate(staged, target, run);
491
587
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
492
588
  writeUpdateState(journalPath, receiptPath, journal);
493
589
  run({
@@ -505,10 +601,13 @@ async function activateAgentRelease(staged, options = {}) {
505
601
  restored = true;
506
602
  await restartAgent(target, run);
507
603
  rollbackHealthy = await probe();
604
+ if (rollbackHealthy && target === "compute")
605
+ switchAgentSocketRoute(paths.route, paths.active);
508
606
  } catch {
509
607
  rollbackHealthy = false;
510
608
  }
511
609
  }
610
+ await stopCandidate(staged, target, run).catch(() => {});
512
611
  const failures = failureCount + 1;
513
612
  const updatedAtTs = now();
514
613
  journal = {
@@ -528,28 +627,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
528
627
  const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
529
628
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
530
629
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
630
+ const run = options.run ?? runCommand;
531
631
  const journal = readJournal(journalPath, root);
532
- if (!journal)
632
+ if (!journal) {
633
+ for (const target of ["compute", "metal"]) {
634
+ await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
635
+ }
636
+ clearAgentCandidate(root);
533
637
  return;
638
+ }
534
639
  if (journal.outcome !== "activating") {
640
+ await stopCandidate(stagedFromJournal(journal, root), journal.target, run).catch(() => {});
535
641
  writeAtomic(receiptPath, publicReceipt(journal), 416);
536
642
  return publicReceipt(journal);
537
643
  }
538
644
  const staged = stagedFromJournal(journal, root);
539
- if (!existsSync2(join2(root, journal.previousTarget))) {
645
+ if (!existsSync3(join2(root, journal.previousTarget))) {
540
646
  throw new Error("Agent update rollback release is missing");
541
647
  }
542
- const run = options.run ?? runCommand;
543
- const probe = options.probe ?? targetProbe(journal.target, run);
648
+ const paths = socketPaths(options.publicSocketPath);
649
+ const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run));
544
650
  restoreAgentRelease(staged);
545
651
  let rollbackHealthy = false;
546
652
  let failureMessage = "activation was interrupted before its health verdict became durable";
547
653
  try {
548
654
  await restartAgent(journal.target, run);
549
655
  rollbackHealthy = await probe();
656
+ if (rollbackHealthy && journal.target === "compute")
657
+ switchAgentSocketRoute(paths.route, paths.active);
550
658
  } catch (cause) {
551
659
  failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
552
660
  }
661
+ await stopCandidate(staged, journal.target, run).catch(() => {});
553
662
  const failures = journal.failureCount + 1;
554
663
  const updatedAtTs = (options.now ?? Date.now)();
555
664
  const recovered = {
@@ -566,15 +675,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
566
675
  }
567
676
  function startAgentUpdateHelper(options = {}) {
568
677
  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));
678
+ if (existsSync3(socketPath))
679
+ unlinkSync2(socketPath);
680
+ mkdirSync2(dirname3(socketPath), { recursive: true, mode: 488 });
573
681
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
574
682
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
575
683
  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 }));
684
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
685
+ const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, {
686
+ target,
687
+ attemptId,
688
+ journalPath,
689
+ receiptPath,
690
+ now: options.now,
691
+ candidatePrepared: true
692
+ }));
577
693
  let busy = true;
694
+ let pending;
578
695
  let blocked;
579
696
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
580
697
  root: releaseRoot,
@@ -586,7 +703,7 @@ function startAgentUpdateHelper(options = {}) {
586
703
  }).finally(() => {
587
704
  busy = false;
588
705
  });
589
- const server = createServer((socket) => {
706
+ const server = createServer2((socket) => {
590
707
  let buffer = "";
591
708
  socket.on("data", (chunk) => {
592
709
  buffer += chunk.toString("utf8");
@@ -605,16 +722,49 @@ function startAgentUpdateHelper(options = {}) {
605
722
  Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
606
723
  if (blocked)
607
724
  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
725
  if (request.target !== "compute" && request.target !== "metal") {
613
726
  throw new Error("agent update target is invalid");
614
727
  }
615
- const attemptId = request.attemptId ?? randomUUID2();
728
+ const attemptId = request.attemptId ?? randomUUID3();
616
729
  if (!ATTEMPT_ID.test(attemptId))
617
730
  throw new Error("Agent update attempt ID is invalid");
731
+ if (request.op === "commit" || request.op === "abort") {
732
+ if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
733
+ throw new Error("Agent update handover does not match the prepared candidate");
734
+ }
735
+ const selected = pending;
736
+ pending = undefined;
737
+ if (request.op === "abort") {
738
+ await stopCandidate(selected.staged, selected.target, runCommand);
739
+ busy = false;
740
+ const response3 = {
741
+ ok: true,
742
+ status: "aborted",
743
+ version: selected.staged.version,
744
+ attemptId
745
+ };
746
+ socket.end(`${JSON.stringify(response3)}
747
+ `);
748
+ return;
749
+ }
750
+ const outcome = await activate(selected.staged, selected.target, attemptId);
751
+ busy = false;
752
+ if (outcome?.ok === false)
753
+ throw new Error(outcome.reason ?? "Agent handover failed");
754
+ const response2 = {
755
+ ok: true,
756
+ status: "active",
757
+ version: selected.staged.version,
758
+ attemptId
759
+ };
760
+ socket.end(`${JSON.stringify(response2)}
761
+ `);
762
+ return;
763
+ }
764
+ if (request.op !== "prepare")
765
+ throw new Error("unknown update operation");
766
+ if (busy)
767
+ throw new Error("another Agent update or recovery is already active");
618
768
  const prior = readJournal(journalPath, releaseRoot);
619
769
  const now = (options.now ?? Date.now)();
620
770
  if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
@@ -625,17 +775,23 @@ function startAgentUpdateHelper(options = {}) {
625
775
  currentVersion: request.currentVersion,
626
776
  root: releaseRoot
627
777
  });
628
- const response = { ok: true, status: "staged", version: staged.version, attemptId };
778
+ await startCandidate(staged, request.target, runCommand);
779
+ pending = { staged, target: request.target, attemptId };
780
+ ownsBusy = false;
781
+ setTimer(() => {
782
+ if (pending?.attemptId !== attemptId)
783
+ return;
784
+ const expired = pending;
785
+ pending = undefined;
786
+ stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
787
+ busy = false;
788
+ });
789
+ }, 180000);
790
+ const response = { ok: true, status: "prepared", version: staged.version, attemptId };
629
791
  socket.end(`${JSON.stringify(response)}
630
792
  `);
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
793
  }).catch((cause) => {
638
- if (ownsBusy)
794
+ if (ownsBusy && !pending)
639
795
  busy = false;
640
796
  const response = {
641
797
  ok: false,
@@ -647,12 +803,12 @@ function startAgentUpdateHelper(options = {}) {
647
803
  });
648
804
  socket.on("error", () => socket.destroy());
649
805
  });
650
- server.listen(socketPath, () => chmodSync2(socketPath, 432));
806
+ server.listen(socketPath, () => chmodSync3(socketPath, 432));
651
807
  return server;
652
808
  }
653
809
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
654
810
  return new Promise((resolve3, reject) => {
655
- const socket = connect(socketPath, () => socket.write(`${JSON.stringify(request)}
811
+ const socket = connect2(socketPath, () => socket.write(`${JSON.stringify(request)}
656
812
  `));
657
813
  let buffer = "";
658
814
  socket.setTimeout(timeoutMs, () => {
@@ -680,22 +836,22 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
680
836
  import { createHash as createHash2 } from "node:crypto";
681
837
  import {
682
838
  accessSync,
683
- chmodSync as chmodSync3,
839
+ chmodSync as chmodSync4,
684
840
  copyFileSync,
685
841
  createReadStream,
686
- existsSync as existsSync3,
842
+ existsSync as existsSync4,
687
843
  mkdtempSync,
688
844
  mkdirSync as mkdirSync3,
689
845
  readFileSync as readFileSync3,
690
- renameSync as renameSync3,
691
- rmSync as rmSync3,
846
+ renameSync as renameSync4,
847
+ rmSync as rmSync4,
692
848
  statSync,
693
- symlinkSync as symlinkSync2,
694
- unlinkSync as unlinkSync2,
849
+ symlinkSync as symlinkSync3,
850
+ unlinkSync as unlinkSync3,
695
851
  writeFileSync as writeFileSync3
696
852
  } from "node:fs";
697
853
  import { tmpdir } from "node:os";
698
- import { dirname as dirname3, join as join3 } from "node:path";
854
+ import { dirname as dirname4, join as join3 } from "node:path";
699
855
  var PINNED_BUN_VERSION = "1.3.14";
700
856
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
701
857
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
@@ -868,23 +1024,23 @@ var softwareDownloadCommand = (url, destination, maximumBytes) => {
868
1024
  ];
869
1025
  };
870
1026
  var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
871
- if (existsSync3(destination))
1027
+ if (existsSync4(destination))
872
1028
  throw new Error("download destination already exists");
873
1029
  const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
874
1030
  if (received.exitCode !== 0) {
875
- rmSync3(destination, { force: true });
1031
+ rmSync4(destination, { force: true });
876
1032
  throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
877
1033
  }
878
- chmodSync3(destination, 384);
1034
+ chmodSync4(destination, 384);
879
1035
  if (statSync(destination).size > maximumBytes) {
880
- rmSync3(destination, { force: true });
1036
+ rmSync4(destination, { force: true });
881
1037
  throw new Error("download exceeds reviewed size bound");
882
1038
  }
883
1039
  const digest = createHash2("sha256");
884
1040
  for await (const chunk of createReadStream(destination))
885
1041
  digest.update(chunk);
886
1042
  if (digest.digest("hex") !== sha256) {
887
- rmSync3(destination, { force: true });
1043
+ rmSync4(destination, { force: true });
888
1044
  throw new Error("download checksum mismatch");
889
1045
  }
890
1046
  };
@@ -900,12 +1056,12 @@ var aptInstallMany = async (names) => {
900
1056
  return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
901
1057
  };
902
1058
  var writeOwnedPolicy = (pathValue, content, mode = 420) => {
903
- if (existsSync3(pathValue)) {
1059
+ if (existsSync4(pathValue)) {
904
1060
  if (readFileSync3(pathValue, "utf8") !== content)
905
1061
  throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
906
1062
  return;
907
1063
  }
908
- mkdirSync3(dirname3(pathValue), { recursive: true, mode: 493 });
1064
+ mkdirSync3(dirname4(pathValue), { recursive: true, mode: 493 });
909
1065
  writeFileSync3(pathValue, content, { mode, flag: "wx" });
910
1066
  };
911
1067
  var installContainerdRuntime = async (directory, osVersion) => {
@@ -917,7 +1073,7 @@ var installContainerdRuntime = async (directory, osVersion) => {
917
1073
  const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
918
1074
  if (nerdctlExtract.exitCode !== 0)
919
1075
  return nerdctlExtract;
920
- chmodSync3("/usr/local/bin/nerdctl", 493);
1076
+ chmodSync4("/usr/local/bin/nerdctl", 493);
921
1077
  const buildkit = join3(directory, "buildkit.tar.gz");
922
1078
  await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
923
1079
  const unpacked = join3(directory, "buildkit");
@@ -927,15 +1083,15 @@ var installContainerdRuntime = async (directory, osVersion) => {
927
1083
  return buildkitExtract;
928
1084
  for (const binary of ["buildctl", "buildkitd"]) {
929
1085
  copyFileSync(join3(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
930
- chmodSync3(`/usr/local/bin/${binary}`, 493);
1086
+ chmodSync4(`/usr/local/bin/${binary}`, 493);
931
1087
  }
932
1088
  mkdirSync3("/etc/containerd", { recursive: true, mode: 493 });
933
1089
  const expected = CONTAINERD_CONFIG(false);
934
1090
  const kataExpected = CONTAINERD_CONFIG(true);
935
- if (existsSync3(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
1091
+ if (existsSync4(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
936
1092
  return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
937
1093
  }
938
- if (!existsSync3(CONTAINERD_CONFIG_PATH))
1094
+ if (!existsSync4(CONTAINERD_CONFIG_PATH))
939
1095
  writeFileSync3(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
940
1096
  writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
941
1097
  mkdirSync3("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
@@ -948,10 +1104,10 @@ var installContainerdRuntime = async (directory, osVersion) => {
948
1104
  };
949
1105
  var installKataRuntime = async (directory) => {
950
1106
  for (const pathValue of ["/dev/kvm", "/dev/sev"])
951
- if (!existsSync3(pathValue))
1107
+ if (!existsSync4(pathValue))
952
1108
  return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
953
1109
  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()))
1110
+ if (!existsSync4(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
955
1111
  return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
956
1112
  }
957
1113
  const dependencies = await aptInstallMany(["zstd"]);
@@ -966,7 +1122,7 @@ var installKataRuntime = async (directory) => {
966
1122
  const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
967
1123
  if (extracted.exitCode !== 0)
968
1124
  return extracted;
969
- if (!existsSync3("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync3(KATA_SNP_CONFIG_PATH)) {
1125
+ if (!existsSync4("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync4(KATA_SNP_CONFIG_PATH)) {
970
1126
  return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
971
1127
  }
972
1128
  const snpConfig = readFileSync3(KATA_SNP_CONFIG_PATH, "utf8");
@@ -974,19 +1130,19 @@ var installKataRuntime = async (directory) => {
974
1130
  return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
975
1131
  }
976
1132
  mkdirSync3("/etc/kata-containers", { recursive: true, mode: 493 });
977
- if (existsSync3(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
1133
+ if (existsSync4(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
978
1134
  return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
979
1135
  }
980
- if (!existsSync3(KATA_ACTIVE_CONFIG_PATH))
1136
+ if (!existsSync4(KATA_ACTIVE_CONFIG_PATH))
981
1137
  copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
982
- chmodSync3(KATA_ACTIVE_CONFIG_PATH, 420);
1138
+ chmodSync4(KATA_ACTIVE_CONFIG_PATH, 420);
983
1139
  try {
984
- unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
1140
+ unlinkSync3("/usr/local/bin/containerd-shim-kata-v2");
985
1141
  } catch {}
986
- symlinkSync2("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
1142
+ symlinkSync3("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
987
1143
  const base = CONTAINERD_CONFIG(false);
988
1144
  const kata = CONTAINERD_CONFIG(true);
989
- if (!existsSync3(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
1145
+ if (!existsSync4(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
990
1146
  return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
991
1147
  }
992
1148
  if (readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
@@ -1006,7 +1162,7 @@ async function executeSoftwareOperation(operation) {
1006
1162
  const binary = await run(["/usr/bin/docker", "--version"]);
1007
1163
  if (!successful(binary, /Docker version/))
1008
1164
  return { ...binary, exitCode: 1 };
1009
- if (!existsSync3(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
1165
+ if (!existsSync4(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
1010
1166
  return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
1011
1167
  const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
1012
1168
  return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
@@ -1019,17 +1175,17 @@ async function executeSoftwareOperation(operation) {
1019
1175
  run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
1020
1176
  run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
1021
1177
  ]);
1022
- const policy = existsSync3(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1178
+ const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1023
1179
  const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
1024
1180
  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
1181
  }
1026
1182
  if (software === "kata-containers") {
1027
1183
  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()));
1184
+ const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
1185
+ const activeConfig = existsSync4(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
1186
+ 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
1187
  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" };
1188
+ 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
1189
  }
1034
1190
  if (software === "nginx") {
1035
1191
  const binary = await run(["/usr/sbin/nginx", "-v"]);
@@ -1070,10 +1226,10 @@ async function executeSoftwareOperation(operation) {
1070
1226
  return installed;
1071
1227
  if (software === "docker") {
1072
1228
  mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
1073
- if (existsSync3(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
1229
+ if (existsSync4(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
1074
1230
  return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
1075
1231
  }
1076
- if (!existsSync3(DOCKER_DAEMON_PATH))
1232
+ if (!existsSync4(DOCKER_DAEMON_PATH))
1077
1233
  writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
1078
1234
  const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
1079
1235
  return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
@@ -1123,20 +1279,20 @@ async function executeSoftwareOperation(operation) {
1123
1279
  return unzipped;
1124
1280
  mkdirSync3("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
1125
1281
  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");
1282
+ chmodSync4("/usr/local/lib/forgezero/runtime/bun.next", 493);
1283
+ renameSync4("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
1128
1284
  try {
1129
- unlinkSync2("/usr/local/bin/bun");
1285
+ unlinkSync3("/usr/local/bin/bun");
1130
1286
  } catch {}
1131
- symlinkSync2("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
1287
+ symlinkSync3("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
1132
1288
  return { exitCode: 0, output: "" };
1133
1289
  }
1134
1290
  if (software === "cloudflared") {
1135
1291
  const binary = join3(directory, "cloudflared");
1136
1292
  await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
1137
- chmodSync3(binary, 493);
1293
+ chmodSync4(binary, 493);
1138
1294
  copyFileSync(binary, "/usr/local/bin/cloudflared");
1139
- chmodSync3("/usr/local/bin/cloudflared", 493);
1295
+ chmodSync4("/usr/local/bin/cloudflared", 493);
1140
1296
  return { exitCode: 0, output: "" };
1141
1297
  }
1142
1298
  const deb = join3(directory, "arangodb.deb");
@@ -1149,7 +1305,7 @@ async function executeSoftwareOperation(operation) {
1149
1305
  await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
1150
1306
  return { exitCode: 0, output: "" };
1151
1307
  } finally {
1152
- rmSync3(directory, { recursive: true, force: true });
1308
+ rmSync4(directory, { recursive: true, force: true });
1153
1309
  }
1154
1310
  }
1155
1311
  function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -1648,9 +1804,9 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
1648
1804
 
1649
1805
  // src/deployment-connectivity.ts
1650
1806
  import { createHash as createHash3 } from "node:crypto";
1651
- import { mkdirSync as mkdirSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
1807
+ import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
1652
1808
  import { createConnection } from "node:net";
1653
- import { dirname as dirname4 } from "node:path";
1809
+ import { dirname as dirname5 } from "node:path";
1654
1810
 
1655
1811
  // src/process-input.ts
1656
1812
  async function writeAndCloseProcessInput(input, value) {
@@ -1671,7 +1827,7 @@ var checked2 = async (host, argv2, label) => {
1671
1827
  return result.output.trim();
1672
1828
  };
1673
1829
  async function sealWithSystemd(name, path2, value) {
1674
- mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
1830
+ mkdirSync4(dirname5(path2), { recursive: true, mode: 448 });
1675
1831
  const next = `${path2}.next`;
1676
1832
  const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
1677
1833
  stdin: "pipe",
@@ -1687,15 +1843,15 @@ async function sealWithSystemd(name, path2, value) {
1687
1843
  ]);
1688
1844
  if (exitCode !== 0)
1689
1845
  throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
1690
- renameSync4(next, path2);
1846
+ renameSync5(next, path2);
1691
1847
  }
1692
1848
  var defaultHost = {
1693
1849
  seal: sealWithSystemd,
1694
1850
  write(path2, content, mode) {
1695
- mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
1851
+ mkdirSync4(dirname5(path2), { recursive: true, mode: 493 });
1696
1852
  const next = `${path2}.next`;
1697
1853
  writeFileSync4(next, content, { mode });
1698
- renameSync4(next, path2);
1854
+ renameSync5(next, path2);
1699
1855
  },
1700
1856
  async exec(argv2) {
1701
1857
  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 +2083,14 @@ WantedBy=multi-user.target
1927
2083
  }
1928
2084
 
1929
2085
  // 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";
2086
+ import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "node:fs";
2087
+ import { connect as connect3, createServer as createServer3 } from "node:net";
2088
+ import { dirname as dirname8 } from "node:path";
1933
2089
 
1934
2090
  // src/service-supervisor.ts
1935
2091
  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";
2092
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
2093
+ import { dirname as dirname6, join as join4, resolve as resolve3, sep } from "node:path";
1938
2094
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
1939
2095
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
1940
2096
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
@@ -1944,17 +2100,17 @@ var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
1944
2100
  var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
1945
2101
  var defaultHost2 = {
1946
2102
  write(path2, content, mode) {
1947
- mkdirSync5(dirname5(path2), { recursive: true, mode: 493 });
2103
+ mkdirSync5(dirname6(path2), { recursive: true, mode: 493 });
1948
2104
  const next = `${path2}.next`;
1949
2105
  writeFileSync5(next, content, { mode });
1950
- renameSync5(next, path2);
2106
+ renameSync6(next, path2);
1951
2107
  },
1952
2108
  read: (path2) => readFileSync4(path2, "utf8"),
1953
- exists: existsSync4,
1954
- list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
2109
+ exists: existsSync5,
2110
+ list: (path2) => existsSync5(path2) ? readdirSync(path2) : [],
1955
2111
  realpath: realpathSync,
1956
2112
  mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
1957
- remove: (path2) => rmSync4(path2, { force: true }),
2113
+ remove: (path2) => rmSync5(path2, { force: true }),
1958
2114
  async exec(argv2) {
1959
2115
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
1960
2116
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -2150,20 +2306,20 @@ async function activateSupervisedService(request, host = defaultHost2) {
2150
2306
 
2151
2307
  // src/container-supervisor.ts
2152
2308
  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";
2309
+ 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";
2310
+ import { dirname as dirname7, resolve as resolve4, sep as sep2 } from "node:path";
2155
2311
  var defaultHost3 = {
2156
2312
  realpath: realpathSync2,
2157
- exists: existsSync5,
2313
+ exists: existsSync6,
2158
2314
  read: (path2) => readFileSync5(path2, "utf8"),
2159
2315
  write(path2, content, mode) {
2160
- mkdirSync6(dirname6(path2), { recursive: true, mode: 493 });
2316
+ mkdirSync6(dirname7(path2), { recursive: true, mode: 493 });
2161
2317
  const next = `${path2}.next`;
2162
2318
  writeFileSync6(next, content, { mode });
2163
- renameSync6(next, path2);
2319
+ renameSync7(next, path2);
2164
2320
  },
2165
- remove: (path2) => rmSync5(path2, { force: true }),
2166
- list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
2321
+ remove: (path2) => rmSync6(path2, { force: true }),
2322
+ list: (path2) => existsSync6(path2) ? readdirSync2(path2) : [],
2167
2323
  mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
2168
2324
  async exec(argv2) {
2169
2325
  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 +2614,9 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
2458
2614
  var MAX_PENDING_REQUESTS = 128;
2459
2615
  function startSoftwareHelper(options = {}) {
2460
2616
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
2461
- if (existsSync6(socketPath))
2462
- unlinkSync3(socketPath);
2463
- mkdirSync7(dirname7(socketPath), { recursive: true, mode: 488 });
2617
+ if (existsSync7(socketPath))
2618
+ unlinkSync4(socketPath);
2619
+ mkdirSync7(dirname8(socketPath), { recursive: true, mode: 488 });
2464
2620
  const ensure = options.ensure ?? ensureSoftwareRequirements;
2465
2621
  const activate = options.activate ?? activateSupervisedService;
2466
2622
  const buildContainer = options.buildContainer ?? buildContainerImage;
@@ -2468,7 +2624,7 @@ function startSoftwareHelper(options = {}) {
2468
2624
  const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
2469
2625
  let tail = Promise.resolve();
2470
2626
  let pending = 0;
2471
- const server = createServer2((socket) => {
2627
+ const server = createServer3((socket) => {
2472
2628
  let buffer = "";
2473
2629
  socket.on("data", (chunk) => {
2474
2630
  buffer += chunk.toString("utf8");
@@ -2547,12 +2703,12 @@ function startSoftwareHelper(options = {}) {
2547
2703
  });
2548
2704
  socket.on("error", () => socket.destroy());
2549
2705
  });
2550
- server.listen(socketPath, () => chmodSync4(socketPath, 432));
2706
+ server.listen(socketPath, () => chmodSync5(socketPath, 432));
2551
2707
  return server;
2552
2708
  }
2553
2709
  function requestContainer(op, request, socketPath, timeoutMs) {
2554
2710
  return new Promise((resolve5, reject) => {
2555
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
2711
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
2556
2712
  `));
2557
2713
  let buffer = "";
2558
2714
  socket.setTimeout(timeoutMs, () => {
@@ -2590,7 +2746,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
2590
2746
  }
2591
2747
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
2592
2748
  return new Promise((resolve5, reject) => {
2593
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
2749
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
2594
2750
  `));
2595
2751
  let buffer = "";
2596
2752
  socket.setTimeout(timeoutMs, () => {
@@ -2619,7 +2775,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
2619
2775
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
2620
2776
  validateSoftwareRequirements(requirements);
2621
2777
  return new Promise((resolve5, reject) => {
2622
- const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2778
+ const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2623
2779
  `));
2624
2780
  let buffer = "";
2625
2781
  socket.setTimeout(timeoutMs, () => {
@@ -2647,7 +2803,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2803
  }
2648
2804
 
2649
2805
  // src/version.ts
2650
- var VERSION3 = "0.1.85";
2806
+ var VERSION3 = "0.1.87";
2651
2807
 
2652
2808
  // src/egress-policy.ts
2653
2809
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -2763,6 +2919,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
2763
2919
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
2764
2920
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
2765
2921
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
2922
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
2766
2923
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
2767
2924
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
2768
2925
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -2874,6 +3031,7 @@ Type=simple
2874
3031
  User=root
2875
3032
  Group=${AGENT_UPDATE_GROUP}
2876
3033
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
3034
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
2877
3035
  ExecStart=${bin} update-helper
2878
3036
  Restart=always
2879
3037
  RestartSec=2
@@ -2918,13 +3076,11 @@ WantedBy=sockets.target
2918
3076
  `;
2919
3077
  }
2920
3078
  function agentSocketProxyUnit(options) {
2921
- const backend = agentBackendSocketPath(options.socketPath);
3079
+ const backend = agentRoutingSocketPath(options.socketPath);
2922
3080
  const user = options.user ?? "forgezero";
2923
3081
  return `[Unit]
2924
3082
  Description=ForgeZero application Vault socket proxy
2925
3083
  Documentation=https://www.forgezero.net/docs/agent
2926
- Requires=forgezero-agent.service
2927
- After=forgezero-agent.service
2928
3084
 
2929
3085
  [Service]
2930
3086
  User=${user}
@@ -2945,12 +3101,26 @@ RestrictAddressFamilies=AF_UNIX
2945
3101
  `;
2946
3102
  }
2947
3103
  function agentBackendSocketPath(publicSocketPath) {
3104
+ const socket = systemdPath(publicSocketPath, "agent socket");
3105
+ const backend = `${socket}.backend.active`;
3106
+ if (Buffer.byteLength(backend) > 100)
3107
+ throw new Error("agent socket path is too long for a Unix socket");
3108
+ return backend;
3109
+ }
3110
+ function agentRoutingSocketPath(publicSocketPath) {
2948
3111
  const socket = systemdPath(publicSocketPath, "agent socket");
2949
3112
  const backend = `${socket}.backend`;
2950
3113
  if (Buffer.byteLength(backend) > 100)
2951
3114
  throw new Error("agent socket path is too long for a Unix socket");
2952
3115
  return backend;
2953
3116
  }
3117
+ function agentCandidateSocketPath(publicSocketPath) {
3118
+ const socket = systemdPath(publicSocketPath, "agent socket");
3119
+ const backend = `${socket}.backend.candidate`;
3120
+ if (Buffer.byteLength(backend) > 100)
3121
+ throw new Error("agent socket path is too long for a Unix socket");
3122
+ return backend;
3123
+ }
2954
3124
  var systemdPath = (value, label) => {
2955
3125
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
2956
3126
  throw new Error(`invalid ${label} path`);
@@ -3234,7 +3404,11 @@ function agentUnit(options) {
3234
3404
  }
3235
3405
  const environment = [
3236
3406
  "NODE_ENV=production",
3237
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
3407
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
3408
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
3409
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
3410
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
3411
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
3238
3412
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
3239
3413
  `FZ_SEED_CREDENTIAL=agent-seed`,
3240
3414
  `FZ_AGENT_MODE=${options.mode}`,
@@ -3371,6 +3545,26 @@ ${deploymentWrites}
3371
3545
  WantedBy=multi-user.target
3372
3546
  `;
3373
3547
  }
3548
+ function agentCandidateUnit(options) {
3549
+ return agentUnit({
3550
+ ...options,
3551
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
3552
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
3553
+ handoverCandidate: true,
3554
+ gitCredentialPath: undefined,
3555
+ deploymentCredentials: {},
3556
+ bootstrapSshCredentialPath: undefined,
3557
+ bootstrapSshPublicKeyPath: undefined,
3558
+ pullBootstrap: false,
3559
+ pullDeployments: false,
3560
+ pullMigrations: false,
3561
+ lifecycleProfilePath: undefined,
3562
+ bootstrapTargetTelemetryEndpoint: undefined,
3563
+ repository: undefined,
3564
+ bootstrapBundlePath: undefined,
3565
+ bootstrapBundleManifestPath: undefined
3566
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
3567
+ }
3374
3568
  var renderOperation = (operation) => {
3375
3569
  if (operation.kind === "commands")
3376
3570
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -3506,6 +3700,7 @@ function planProvision(options) {
3506
3700
  auxiliaryUnits: [
3507
3701
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
3508
3702
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
3703
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
3509
3704
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
3510
3705
  ...options.enforceEgress ? [
3511
3706
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -3647,8 +3842,11 @@ export {
3647
3842
  agentUnit,
3648
3843
  agentSocketUnit,
3649
3844
  agentSocketProxyUnit,
3845
+ agentRoutingSocketPath,
3650
3846
  agentEnrolmentUnit,
3651
3847
  agentEgressUnit,
3848
+ agentCandidateUnit,
3849
+ agentCandidateSocketPath,
3652
3850
  agentBackendSocketPath,
3653
3851
  WARP_SERVICE_DROP_IN_PATH,
3654
3852
  WARP_CONFIG_UNIT_PATH,
@@ -3667,5 +3865,6 @@ export {
3667
3865
  APPLICATION_RUNTIME_USER,
3668
3866
  AGENT_SOCKET_UNIT_PATH,
3669
3867
  AGENT_SOCKET_PROXY_UNIT_PATH,
3670
- AGENT_EGRESS_UNIT_PATH
3868
+ AGENT_EGRESS_UNIT_PATH,
3869
+ AGENT_CANDIDATE_UNIT_PATH
3671
3870
  };