@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.
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 && 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) }
@@ -3647,8 +3851,11 @@ export {
3647
3851
  agentUnit,
3648
3852
  agentSocketUnit,
3649
3853
  agentSocketProxyUnit,
3854
+ agentRoutingSocketPath,
3650
3855
  agentEnrolmentUnit,
3651
3856
  agentEgressUnit,
3857
+ agentCandidateUnit,
3858
+ agentCandidateSocketPath,
3652
3859
  agentBackendSocketPath,
3653
3860
  WARP_SERVICE_DROP_IN_PATH,
3654
3861
  WARP_CONFIG_UNIT_PATH,
@@ -3667,5 +3874,6 @@ export {
3667
3874
  APPLICATION_RUNTIME_USER,
3668
3875
  AGENT_SOCKET_UNIT_PATH,
3669
3876
  AGENT_SOCKET_PROXY_UNIT_PATH,
3670
- AGENT_EGRESS_UNIT_PATH
3877
+ AGENT_EGRESS_UNIT_PATH,
3878
+ AGENT_CANDIDATE_UNIT_PATH
3671
3879
  };