@forgezero/agent 0.1.40 → 0.1.42

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.
Files changed (64) hide show
  1. package/README.md +573 -424
  2. package/dist/agent-heartbeat.js +6 -3
  3. package/dist/agent-update-helper.js +5 -2
  4. package/dist/agent-update.js +5 -2
  5. package/dist/bootstrap.d.ts +17 -8
  6. package/dist/bootstrap.js +1504 -512
  7. package/dist/cli/agent-install.d.ts +6 -5
  8. package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
  9. package/dist/cli/maintenance.d.ts +23 -0
  10. package/dist/cli/run.d.ts +3 -1
  11. package/dist/cli/session-store.d.ts +5 -0
  12. package/dist/cloudflare-bootstrap.d.ts +73 -35
  13. package/dist/cloudflare-bootstrap.js +587 -90
  14. package/dist/cloudflare-edge.d.ts +64 -12
  15. package/dist/cloudflare-edge.js +103 -8
  16. package/dist/community-rehearsal-host.d.ts +51 -0
  17. package/dist/community-rehearsal-host.js +272 -0
  18. package/dist/credential-schema.d.ts +54 -0
  19. package/dist/credential-schema.js +47 -0
  20. package/dist/definition.d.ts +31 -5
  21. package/dist/definition.js +271 -44
  22. package/dist/deploy-file.js +294 -68
  23. package/dist/deployment-runner.js +18 -5
  24. package/dist/deployment.d.ts +13 -1
  25. package/dist/fz-agent.js +9162 -7564
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +5679 -5077
  28. package/dist/git-ssh.d.ts +5 -0
  29. package/dist/guest-enrolment.d.ts +2 -0
  30. package/dist/guest-enrolment.js +1 -0
  31. package/dist/host-maintenance.d.ts +39 -0
  32. package/dist/host-maintenance.js +135 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/mesh-connector.d.ts +16 -0
  35. package/dist/mesh-connector.js +46 -0
  36. package/dist/metal-bootstrap.js +145 -7
  37. package/dist/metal-helper-socket.js +61 -31
  38. package/dist/metal-provision.d.ts +2 -2
  39. package/dist/metal-provision.js +62 -32
  40. package/dist/operator-bootstrap.d.ts +90 -0
  41. package/dist/operator-bootstrap.js +5704 -0
  42. package/dist/otel-collector.d.ts +18 -0
  43. package/dist/pipeline.d.ts +3 -2
  44. package/dist/pipeline.js +1 -1
  45. package/dist/platform-bootstrap-runtime.d.ts +39 -21
  46. package/dist/platform-bootstrap-runtime.js +182 -59
  47. package/dist/platform-fleet-verification.d.ts +19 -0
  48. package/dist/platform-fleet-verification.js +3873 -0
  49. package/dist/platform-genesis-config.d.ts +7 -0
  50. package/dist/platform-genesis.d.ts +17 -0
  51. package/dist/provision.d.ts +76 -3
  52. package/dist/provision.js +1061 -229
  53. package/dist/recovery-host.d.ts +7 -0
  54. package/dist/recovery-host.js +124 -0
  55. package/dist/service-supervisor.d.ts +42 -0
  56. package/dist/software-helper.d.ts +4 -0
  57. package/dist/software-helper.js +865 -63
  58. package/dist/software.d.ts +14 -3
  59. package/dist/software.js +163 -37
  60. package/dist/ssh-bootstrap.d.ts +97 -0
  61. package/dist/supervised-app.d.ts +2 -0
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +175 -164
  64. package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
package/dist/provision.js CHANGED
@@ -33,6 +33,7 @@ var syncReleaseDirectory = (directory) => {
33
33
  join(directory, "package.json"),
34
34
  join(directory, "dist", "fz-agent.js"),
35
35
  join(directory, "dist", "fz.js"),
36
+ join(directory, "dist", "fz-git-ssh.js"),
36
37
  join(directory, "dist"),
37
38
  directory,
38
39
  dirname(directory)
@@ -101,7 +102,8 @@ async function validateReleaseDirectory(directory, release, run) {
101
102
  }
102
103
  const agent = join(directory, "dist", "fz-agent.js");
103
104
  const cli = join(directory, "dist", "fz.js");
104
- for (const binary of [agent, cli]) {
105
+ const gitSsh = join(directory, "dist", "fz-git-ssh.js");
106
+ for (const binary of [agent, cli, gitSsh]) {
105
107
  if (!readFileSync(binary, "utf8").startsWith(`#!/usr/bin/env bun
106
108
  `)) {
107
109
  throw new Error("agent update artifact is not a self-contained Bun executable");
@@ -148,7 +150,8 @@ async function stageAgentRelease(releaseInput, options) {
148
150
  for (const [member, relative] of [
149
151
  ["package/package.json", "package.json"],
150
152
  ["package/dist/fz-agent.js", "dist/fz-agent.js"],
151
- ["package/dist/fz.js", "dist/fz.js"]
153
+ ["package/dist/fz.js", "dist/fz.js"],
154
+ ["package/dist/fz-git-ssh.js", "dist/fz-git-ssh.js"]
152
155
  ]) {
153
156
  const extracted = await checked(run, {
154
157
  command: "/usr/bin/tar",
@@ -674,9 +677,24 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
674
677
  }
675
678
 
676
679
  // src/software.ts
677
- import { readFileSync as readFileSync3 } from "node:fs";
680
+ import { createHash as createHash2 } from "node:crypto";
681
+ import {
682
+ accessSync,
683
+ chmodSync as chmodSync3,
684
+ copyFileSync,
685
+ mkdtempSync,
686
+ mkdirSync as mkdirSync3,
687
+ readFileSync as readFileSync3,
688
+ renameSync as renameSync3,
689
+ rmSync as rmSync3,
690
+ symlinkSync as symlinkSync2,
691
+ unlinkSync as unlinkSync2,
692
+ writeFileSync as writeFileSync3
693
+ } from "node:fs";
694
+ import { tmpdir } from "node:os";
695
+ import { join as join3 } from "node:path";
678
696
  var PINNED_BUN_VERSION = "1.3.14";
679
- var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
697
+ var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
680
698
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
681
699
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
682
700
  var OS_CATALOG = [
@@ -687,41 +705,151 @@ var SOFTWARE_CATALOG = [
687
705
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
688
706
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
689
707
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
708
+ { id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
690
709
  { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
691
710
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
692
711
  ];
693
712
  var UBUNTU_2604_X64 = [
694
- {
695
- requirement: { id: "bun", version: "1.3.14" },
696
- check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
697
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
698
- },
699
- {
700
- requirement: { id: "nginx", version: "ubuntu-26.04" },
701
- check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
702
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
703
- },
704
- {
705
- requirement: { id: "arangodb", version: "3.11.14" },
706
- check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14' && ` + `! systemctl is-active --quiet arangodb3.service && ` + `! systemctl is-enabled --quiet arangodb3.service`,
707
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install; ` + `systemctl disable --now arangodb3.service`
708
- },
709
- {
710
- requirement: { id: "cloudflared", version: "2026.7.3" },
711
- check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
712
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
713
- },
714
- {
715
- requirement: { id: "ufw", version: "ubuntu-26.04" },
716
- check: "command -v ufw >/dev/null",
717
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
718
- },
719
- {
720
- requirement: { id: "openssh-client", version: "ubuntu-26.04" },
721
- check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
722
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
723
- }
713
+ { requirement: { id: "bun", version: "1.3.14" } },
714
+ { requirement: { id: "nginx", version: "ubuntu-26.04" } },
715
+ { requirement: { id: "arangodb", version: "3.11.14" } },
716
+ { requirement: { id: "cloudflared", version: "2026.7.3" } },
717
+ { requirement: { id: "cloudflare-warp", version: "2026.6.822.0-min" } },
718
+ { requirement: { id: "ufw", version: "ubuntu-26.04" } },
719
+ { requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
724
720
  ];
721
+ var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
722
+ var run = async (argv, env = {}) => {
723
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: path, LANG: "C", LC_ALL: "C", ...env } });
724
+ const [stdout, stderr, exitCode] = await Promise.all([
725
+ new Response(child.stdout).text(),
726
+ new Response(child.stderr).text(),
727
+ child.exited
728
+ ]);
729
+ return { exitCode, output: `${stdout}${stderr}` };
730
+ };
731
+ var download = async (url, destination, sha256) => {
732
+ const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
733
+ if (!response.ok)
734
+ throw new Error(`download failed with HTTP ${response.status}`);
735
+ const bytes = new Uint8Array(await response.arrayBuffer());
736
+ if (createHash2("sha256").update(bytes).digest("hex") !== sha256)
737
+ throw new Error("download checksum mismatch");
738
+ writeFileSync3(destination, bytes, { mode: 384, flag: "wx" });
739
+ };
740
+ var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
741
+ var aptInstall = async (name) => {
742
+ const environment = { DEBIAN_FRONTEND: "noninteractive" };
743
+ const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
744
+ return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
745
+ };
746
+ async function executeSoftwareOperation(operation) {
747
+ const { software, version } = operation;
748
+ if (!UBUNTU_2604_X64.some(({ requirement }) => requirement.id === software && requirement.version === version)) {
749
+ return { exitCode: 2, output: "unsupported software operation" };
750
+ }
751
+ if (operation.kind === "check") {
752
+ if (software === "bun")
753
+ return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
754
+ if (software === "nginx") {
755
+ const binary = await run(["/usr/sbin/nginx", "-v"]);
756
+ return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
757
+ }
758
+ if (software === "arangodb") {
759
+ const binary = await run(["/usr/bin/arangod", "--version"]);
760
+ if (!successful(binary, /3\.11\.14/))
761
+ return { ...binary, exitCode: 1 };
762
+ const [active, enabled] = await Promise.all([
763
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "arangodb3.service"]),
764
+ run(["/usr/bin/systemctl", "is-enabled", "--quiet", "arangodb3.service"])
765
+ ]);
766
+ return active.exitCode !== 0 && enabled.exitCode !== 0 ? { exitCode: 0, output: binary.output } : { exitCode: 1, output: "vendor standalone unit remains active or enabled" };
767
+ }
768
+ if (software === "cloudflared")
769
+ return run(["/usr/local/bin/cloudflared", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /2026\.7\.3/) ? 0 : 1 }));
770
+ if (software === "cloudflare-warp")
771
+ return run(["/usr/bin/warp-cli", "--version"]).then((result) => {
772
+ const match = result.output.match(/(\d{4})\.(\d+)\.(\d+)\.(\d+)/);
773
+ const observed = match?.slice(1).map(Number);
774
+ const minimum = [2026, 6, 822, 0];
775
+ const supported = observed && observed.some((part, index) => part > minimum[index] && observed.slice(0, index).every((prior, priorIndex) => prior === minimum[priorIndex])) || observed?.every((part, index) => part === minimum[index]);
776
+ return { ...result, exitCode: result.exitCode === 0 && supported ? 0 : 1 };
777
+ });
778
+ const binaries = software === "ufw" ? ["/usr/sbin/ufw"] : ["/usr/bin/ssh", "/usr/bin/scp", "/usr/bin/ssh-keyscan", "/usr/bin/ssh-keygen"];
779
+ try {
780
+ binaries.forEach((binary) => accessSync(binary));
781
+ return { exitCode: 0, output: "" };
782
+ } catch (cause) {
783
+ return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
784
+ }
785
+ }
786
+ if (software === "nginx" || software === "ufw" || software === "openssh-client") {
787
+ const installed = await aptInstall(software === "openssh-client" ? "openssh-client" : software);
788
+ if (installed.exitCode !== 0 || software !== "nginx")
789
+ return installed;
790
+ return run(["/usr/bin/systemctl", "enable", "--now", "nginx.service"]);
791
+ }
792
+ const directory = mkdtempSync(join3(tmpdir(), "forgezero-software-"));
793
+ try {
794
+ if (software === "cloudflare-warp") {
795
+ const key = join3(directory, "cloudflare-warp-key.gpg");
796
+ await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
797
+ mkdirSync3("/usr/share/keyrings", { recursive: true, mode: 493 });
798
+ const dearmored = await run([
799
+ "/usr/bin/gpg",
800
+ "--batch",
801
+ "--yes",
802
+ "--dearmor",
803
+ "-o",
804
+ "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg",
805
+ key
806
+ ]);
807
+ if (dearmored.exitCode !== 0)
808
+ return dearmored;
809
+ mkdirSync3("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
810
+ writeFileSync3("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ resolute main
811
+ `, { mode: 420 });
812
+ return aptInstall("cloudflare-warp");
813
+ }
814
+ if (software === "bun") {
815
+ const archive = join3(directory, "bun.zip");
816
+ await download("https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip", archive, BUN_RELEASE_SHA256);
817
+ const unpacked = join3(directory, "unpacked");
818
+ mkdirSync3(unpacked, { mode: 448 });
819
+ const unzipped = await run(["/usr/bin/unzip", "-q", archive, "-d", unpacked]);
820
+ if (unzipped.exitCode !== 0)
821
+ return unzipped;
822
+ mkdirSync3("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
823
+ copyFileSync(join3(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
824
+ chmodSync3("/usr/local/lib/forgezero/runtime/bun.next", 493);
825
+ renameSync3("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
826
+ try {
827
+ unlinkSync2("/usr/local/bin/bun");
828
+ } catch {}
829
+ symlinkSync2("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
830
+ return { exitCode: 0, output: "" };
831
+ }
832
+ if (software === "cloudflared") {
833
+ const binary = join3(directory, "cloudflared");
834
+ await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
835
+ chmodSync3(binary, 493);
836
+ copyFileSync(binary, "/usr/local/bin/cloudflared");
837
+ chmodSync3("/usr/local/bin/cloudflared", 493);
838
+ return { exitCode: 0, output: "" };
839
+ }
840
+ const deb = join3(directory, "arangodb.deb");
841
+ await download("https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb", deb, ARANGO_SHA256);
842
+ let installed = await run(["/usr/bin/dpkg", "-i", deb], { DEBIAN_FRONTEND: "noninteractive" });
843
+ if (installed.exitCode !== 0)
844
+ installed = await run(["/usr/bin/apt-get", "-y", "-f", "install"], { DEBIAN_FRONTEND: "noninteractive" });
845
+ if (installed.exitCode !== 0)
846
+ return installed;
847
+ await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
848
+ return { exitCode: 0, output: "" };
849
+ } finally {
850
+ rmSync3(directory, { recursive: true, force: true });
851
+ }
852
+ }
725
853
  function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
726
854
  const values = Object.fromEntries(osRelease.split(`
727
855
  `).flatMap((line) => {
@@ -744,7 +872,7 @@ function validateSoftwareRequirements(value, _options = {}) {
744
872
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
745
873
  throw new Error("software requirement contains an unknown field");
746
874
  }
747
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
875
+ if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
748
876
  throw new Error("software requirement coordinate is invalid");
749
877
  }
750
878
  const requirement = { id: row.id, version: row.version };
@@ -770,15 +898,15 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
770
898
  const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
771
899
  if (!strategy)
772
900
  throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
773
- const before = await options.exec(strategy.check);
901
+ const before = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
774
902
  if (before.exitCode === 0) {
775
903
  results.push({ ...requirement, changed: false });
776
904
  continue;
777
905
  }
778
- const installed = await options.exec(strategy.install);
906
+ const installed = await options.exec({ kind: "install", software: requirement.id, version: requirement.version });
779
907
  if (installed.exitCode !== 0)
780
908
  throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
781
- const after = await options.exec(strategy.check);
909
+ const after = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
782
910
  if (after.exitCode !== 0)
783
911
  throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
784
912
  results.push({ ...requirement, changed: true });
@@ -786,34 +914,671 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
786
914
  return results;
787
915
  }
788
916
 
917
+ // src/capacity-calibration.ts
918
+ var percentile95 = (values) => {
919
+ if (values.length === 0)
920
+ return Number.POSITIVE_INFINITY;
921
+ const sorted = values.toSorted((left, right) => left - right);
922
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];
923
+ };
924
+ function localCalibrationEndpoint(value) {
925
+ const endpoint = new URL(value);
926
+ if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
927
+ throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
928
+ }
929
+ return endpoint;
930
+ }
931
+ function validateCapacityCalibrationOptions(options) {
932
+ const endpoint = localCalibrationEndpoint(options.endpoint);
933
+ const maxConcurrency = options.maxConcurrency ?? 256;
934
+ const requestsPerWorker = options.requestsPerWorker ?? 8;
935
+ const maxP95Ms = options.maxP95Ms ?? 250;
936
+ const maxErrorRate = options.maxErrorRate ?? 0.01;
937
+ const headroomRatio = options.headroomRatio ?? 0.8;
938
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
939
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(headroomRatio) || headroomRatio < 0.25 || headroomRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000) {
940
+ throw new Error("Capacity calibration bounds are invalid.");
941
+ }
942
+ return {
943
+ endpoint,
944
+ maxConcurrency,
945
+ requestsPerWorker,
946
+ maxP95Ms,
947
+ maxErrorRate,
948
+ headroomRatio,
949
+ requestTimeoutMs
950
+ };
951
+ }
952
+ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, fetcher) {
953
+ const latencies = [];
954
+ let succeeded = 0;
955
+ let failed = 0;
956
+ let overloaded = 0;
957
+ const started = performance.now();
958
+ await Promise.all(Array.from({ length: concurrency }, async () => {
959
+ for (let request = 0;request < requestsPerWorker; request += 1) {
960
+ const requestStarted = performance.now();
961
+ try {
962
+ const response = await fetcher(endpoint, {
963
+ method: "GET",
964
+ headers: { accept: "application/json", "user-agent": "forgezero-capacity-calibration/1" },
965
+ signal: AbortSignal.timeout(timeoutMs),
966
+ redirect: "error"
967
+ });
968
+ await response.body?.cancel();
969
+ if (response.ok)
970
+ succeeded += 1;
971
+ else {
972
+ failed += 1;
973
+ if (response.status === 503)
974
+ overloaded += 1;
975
+ }
976
+ } catch {
977
+ failed += 1;
978
+ } finally {
979
+ latencies.push(performance.now() - requestStarted);
980
+ }
981
+ }
982
+ }));
983
+ const elapsedSeconds = Math.max((performance.now() - started) / 1000, 0.001);
984
+ return {
985
+ concurrency,
986
+ requests: concurrency * requestsPerWorker,
987
+ succeeded,
988
+ failed,
989
+ overloaded,
990
+ throughputPerSecond: Number(((succeeded + failed) / elapsedSeconds).toFixed(2)),
991
+ p95Ms: Number(percentile95(latencies).toFixed(2))
992
+ };
993
+ }
994
+ async function calibrateHttpConcurrency(options, fetcher = fetch) {
995
+ const {
996
+ endpoint,
997
+ maxConcurrency,
998
+ requestsPerWorker,
999
+ maxP95Ms,
1000
+ maxErrorRate,
1001
+ headroomRatio,
1002
+ requestTimeoutMs: timeoutMs
1003
+ } = validateCapacityCalibrationOptions(options);
1004
+ const stages = [];
1005
+ let lastSafe = 1;
1006
+ let stopReason = "maximum-tested";
1007
+ for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
1008
+ const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, fetcher);
1009
+ stages.push(measured);
1010
+ const errorRate = measured.failed / measured.requests;
1011
+ const previous = stages.at(-2);
1012
+ const throughputRegressed = Boolean(previous && concurrency > 1 && measured.throughputPerSecond < previous.throughputPerSecond * 0.9);
1013
+ if (measured.overloaded > 0 || errorRate > maxErrorRate)
1014
+ stopReason = "errors";
1015
+ else if (measured.p95Ms > maxP95Ms)
1016
+ stopReason = "latency";
1017
+ else if (throughputRegressed)
1018
+ stopReason = "throughput-regression";
1019
+ else
1020
+ lastSafe = concurrency;
1021
+ if (stopReason !== "maximum-tested" || concurrency === maxConcurrency)
1022
+ break;
1023
+ }
1024
+ return {
1025
+ endpoint: endpoint.toString(),
1026
+ recommendedConcurrency: Math.max(1, Math.floor(lastSafe * headroomRatio)),
1027
+ stopReason,
1028
+ stages
1029
+ };
1030
+ }
1031
+
1032
+ // src/definition.ts
1033
+ var PIPELINE_VERSION = 3;
1034
+ var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v3.json";
1035
+
1036
+ class DefinitionError extends Error {
1037
+ constructor(message) {
1038
+ super(message);
1039
+ this.name = "DefinitionError";
1040
+ }
1041
+ }
1042
+ var record = (value, where) => {
1043
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1044
+ throw new DefinitionError(`${where} must be an object.`);
1045
+ }
1046
+ return value;
1047
+ };
1048
+ var text = (value, where) => {
1049
+ if (typeof value !== "string" || value.trim() === "") {
1050
+ throw new DefinitionError(`${where} must be a non-empty string.`);
1051
+ }
1052
+ return value;
1053
+ };
1054
+ var argv = (value, where) => {
1055
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
1056
+ throw new DefinitionError(`${where} must contain from 1 to 256 arguments.`);
1057
+ }
1058
+ let bytes = 0;
1059
+ const parsed = value.map((argument, index) => {
1060
+ if (typeof argument !== "string" || argument.length === 0 || argument.length > 16384 || argument.includes("\x00")) {
1061
+ throw new DefinitionError(`${where}[${index}] must be a non-empty bounded string without NUL.`);
1062
+ }
1063
+ if (argument.includes("${") && !/^\$\{FZ_[A-Z0-9_]+\}$/.test(argument)) {
1064
+ throw new DefinitionError(`${where}[${index}] contains unsupported interpolation; only one exact FZ coordinate is allowed.`);
1065
+ }
1066
+ bytes += Buffer.byteLength(argument);
1067
+ if (bytes > 64 * 1024)
1068
+ throw new DefinitionError(`${where} is larger than 64 KiB.`);
1069
+ return argument;
1070
+ });
1071
+ const executable = parsed[0].split("/").at(-1).toLowerCase();
1072
+ if (new Set(["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"]).has(executable)) {
1073
+ throw new DefinitionError(`${where}[0] may not invoke a shell or command dispatcher.`);
1074
+ }
1075
+ return parsed;
1076
+ };
1077
+ var exactKeys = (value, allowed, where) => {
1078
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
1079
+ if (unknown.length > 0)
1080
+ throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
1081
+ };
1082
+ var NAME = /^[a-z][a-z0-9-]{0,62}$/;
1083
+ var RESERVED_STEP_ENV = new Set([
1084
+ "PATH",
1085
+ "HOME",
1086
+ "SHELL",
1087
+ "PWD",
1088
+ "BUN_INSTALL",
1089
+ "NODE_OPTIONS",
1090
+ "LD_PRELOAD",
1091
+ "LD_LIBRARY_PATH",
1092
+ "GIT_SSH",
1093
+ "GIT_SSH_COMMAND"
1094
+ ]);
1095
+ function capacityCalibration(value, where) {
1096
+ const calibration = record(value, where);
1097
+ exactKeys(calibration, [
1098
+ "endpoint",
1099
+ "maxConcurrency",
1100
+ "requestsPerWorker",
1101
+ "maxP95Ms",
1102
+ "maxErrorRate",
1103
+ "headroomRatio",
1104
+ "requestTimeoutMs"
1105
+ ], where);
1106
+ const endpoint = text(calibration.endpoint, `${where}.endpoint`);
1107
+ try {
1108
+ localCalibrationEndpoint(endpoint);
1109
+ } catch (cause) {
1110
+ throw new DefinitionError(cause instanceof Error ? cause.message : `${where}.endpoint is invalid.`);
1111
+ }
1112
+ const optionalNumber = (name) => {
1113
+ const raw = calibration[name];
1114
+ if (raw === undefined)
1115
+ return;
1116
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
1117
+ throw new DefinitionError(`${where}.${name} must be a finite number.`);
1118
+ }
1119
+ return raw;
1120
+ };
1121
+ const parsed = {
1122
+ endpoint,
1123
+ ...Object.fromEntries([
1124
+ "maxConcurrency",
1125
+ "requestsPerWorker",
1126
+ "maxP95Ms",
1127
+ "maxErrorRate",
1128
+ "headroomRatio",
1129
+ "requestTimeoutMs"
1130
+ ].flatMap((name) => {
1131
+ const found = optionalNumber(name);
1132
+ return found === undefined ? [] : [[name, found]];
1133
+ }))
1134
+ };
1135
+ try {
1136
+ validateCapacityCalibrationOptions(parsed);
1137
+ } catch (cause) {
1138
+ throw new DefinitionError(cause instanceof Error ? cause.message : `${where} bounds are invalid.`);
1139
+ }
1140
+ return parsed;
1141
+ }
1142
+ function validateDeploymentService(value, where = "service") {
1143
+ const service = record(value, where);
1144
+ exactKeys(service, [
1145
+ "strategy",
1146
+ "publicPort",
1147
+ "applicationPorts",
1148
+ "command",
1149
+ "healthPath",
1150
+ "maxConnections",
1151
+ "websocket",
1152
+ "drainMs"
1153
+ ], where);
1154
+ if (service.strategy !== "direct" && service.strategy !== "blue-green") {
1155
+ throw new DefinitionError(`${where}.strategy must be direct or blue-green.`);
1156
+ }
1157
+ const publicPort = Number(service.publicPort);
1158
+ if (!Number.isSafeInteger(publicPort) || publicPort < 1024 || publicPort > 65535) {
1159
+ throw new DefinitionError(`${where}.publicPort must be an unprivileged TCP port.`);
1160
+ }
1161
+ const allocation = record(service.applicationPorts, `${where}.applicationPorts`);
1162
+ let applicationPorts;
1163
+ const required = service.strategy === "blue-green" ? 2 : 1;
1164
+ if (allocation.mode === "fixed") {
1165
+ exactKeys(allocation, ["mode", "ports"], `${where}.applicationPorts`);
1166
+ if (!Array.isArray(allocation.ports) || allocation.ports.length !== required || allocation.ports.some((port) => !Number.isSafeInteger(port) || Number(port) < 1024 || Number(port) > 65535) || new Set(allocation.ports).size !== allocation.ports.length || allocation.ports.includes(publicPort)) {
1167
+ throw new DefinitionError(`${where}.applicationPorts needs ${required} distinct unprivileged port(s), disjoint from publicPort.`);
1168
+ }
1169
+ applicationPorts = { mode: "fixed", ports: allocation.ports };
1170
+ } else if (allocation.mode === "dynamic") {
1171
+ exactKeys(allocation, ["mode", "from", "to"], `${where}.applicationPorts`);
1172
+ const from = Number(allocation.from);
1173
+ const to = Number(allocation.to);
1174
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 1024 || to > 65535 || to - from + 1 < required || publicPort >= from && publicPort <= to) {
1175
+ throw new DefinitionError(`${where}.applicationPorts dynamic range is invalid or includes publicPort.`);
1176
+ }
1177
+ applicationPorts = { mode: "dynamic", from, to };
1178
+ } else
1179
+ throw new DefinitionError(`${where}.applicationPorts.mode must be fixed or dynamic.`);
1180
+ const command2 = argv(service.command, `${where}.command`);
1181
+ for (const argument of command2) {
1182
+ const coordinate = argument.match(/^\$\{(FZ_[A-Z0-9_]+)\}$/)?.[1];
1183
+ if (coordinate && !["FZ_APP_PORT", "FZ_RELEASE"].includes(coordinate)) {
1184
+ throw new DefinitionError(`${where}.command uses unsupported coordinate ${coordinate}.`);
1185
+ }
1186
+ }
1187
+ if (typeof service.healthPath !== "string" || !/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/.test(service.healthPath) || service.healthPath.includes("..") || service.healthPath.includes("//")) {
1188
+ throw new DefinitionError(`${where}.healthPath must be one bounded absolute path.`);
1189
+ }
1190
+ if (service.websocket !== undefined && typeof service.websocket !== "boolean") {
1191
+ throw new DefinitionError(`${where}.websocket must be a boolean.`);
1192
+ }
1193
+ const maxConnections = service.maxConnections === undefined ? undefined : Number(service.maxConnections);
1194
+ if (maxConnections !== undefined && (!Number.isSafeInteger(maxConnections) || maxConnections < 1 || maxConnections > 1e6)) {
1195
+ throw new DefinitionError(`${where}.maxConnections must be from 1 to 1000000.`);
1196
+ }
1197
+ const drainMs = service.drainMs === undefined ? 30000 : Number(service.drainMs);
1198
+ if (!Number.isSafeInteger(drainMs) || drainMs < 0 || drainMs > 300000) {
1199
+ throw new DefinitionError(`${where}.drainMs must be from 0 to 300000.`);
1200
+ }
1201
+ return {
1202
+ strategy: service.strategy,
1203
+ publicPort,
1204
+ applicationPorts,
1205
+ command: command2,
1206
+ healthPath: service.healthPath,
1207
+ ...maxConnections === undefined ? {} : { maxConnections },
1208
+ websocket: service.websocket === true,
1209
+ drainMs
1210
+ };
1211
+ }
1212
+ function parseDeployDefinition(value, options = {}) {
1213
+ const root = record(value, "pipeline");
1214
+ exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
1215
+ if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
1216
+ throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
1217
+ }
1218
+ if (root.version !== PIPELINE_VERSION) {
1219
+ throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
1220
+ }
1221
+ if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
1222
+ throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
1223
+ }
1224
+ const rawProfiles = record(root.profiles, "pipeline.profiles");
1225
+ const profileEntries = Object.entries(rawProfiles);
1226
+ if (profileEntries.length === 0 || profileEntries.length > 32) {
1227
+ throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
1228
+ }
1229
+ if (!Array.isArray(root.steps) || root.steps.length === 0 || root.steps.length > 256) {
1230
+ throw new DefinitionError("pipeline.steps must contain from 1 to 256 steps.");
1231
+ }
1232
+ const profiles = {};
1233
+ for (const [name2, raw] of profileEntries) {
1234
+ if (!NAME.test(name2))
1235
+ throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
1236
+ const profile = record(raw, `profiles.${name2}`);
1237
+ exactKeys(profile, ["software", "service", "capacityCalibration"], `profiles.${name2}`);
1238
+ if (!Array.isArray(profile.software)) {
1239
+ throw new DefinitionError(`profiles.${name2}.software must be an array.`);
1240
+ }
1241
+ const software = validateSoftwareRequirements(profile.software, options);
1242
+ const service = profile.service === undefined ? undefined : validateDeploymentService(profile.service, `profiles.${name2}.service`);
1243
+ if (service && !software.some(({ id }) => id === "nginx")) {
1244
+ throw new DefinitionError(`profiles.${name2}.service requires the reviewed nginx software strategy.`);
1245
+ }
1246
+ profiles[name2] = {
1247
+ software,
1248
+ ...profile.service === undefined ? {} : {
1249
+ service
1250
+ },
1251
+ ...profile.capacityCalibration === undefined ? {} : {
1252
+ capacityCalibration: capacityCalibration(profile.capacityCalibration, `profiles.${name2}.capacityCalibration`)
1253
+ }
1254
+ };
1255
+ }
1256
+ const phases = new Set(["build", "release", "migrate", "health"]);
1257
+ const steps = root.steps.map((raw, index) => {
1258
+ const step = record(raw, `steps[${index}]`);
1259
+ exactKeys(step, ["name", "exec", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
1260
+ const phase = text(step.phase, `steps[${index}].phase`);
1261
+ if (!phases.has(phase))
1262
+ throw new DefinitionError(`steps[${index}].phase is not supported.`);
1263
+ if (step.scope !== "target" && step.scope !== "release") {
1264
+ throw new DefinitionError(`steps[${index}].scope must be target or release.`);
1265
+ }
1266
+ if (step.always !== undefined && typeof step.always !== "boolean") {
1267
+ throw new DefinitionError(`steps[${index}].always must be a boolean.`);
1268
+ }
1269
+ let selectedProfiles;
1270
+ if (step.profiles !== undefined) {
1271
+ if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
1272
+ throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
1273
+ }
1274
+ selectedProfiles = [...step.profiles];
1275
+ if (new Set(selectedProfiles).size !== selectedProfiles.length) {
1276
+ throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
1277
+ }
1278
+ }
1279
+ if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
1280
+ throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
1281
+ }
1282
+ if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
1283
+ throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
1284
+ }
1285
+ if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
1286
+ throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
1287
+ }
1288
+ const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
1289
+ if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)) {
1290
+ throw new DefinitionError(`steps[${index}].timeoutMs must be an integer from 1 to 86400000.`);
1291
+ }
1292
+ let when;
1293
+ if (step.when !== undefined) {
1294
+ const conditions = record(step.when, `steps[${index}].when`);
1295
+ when = {};
1296
+ for (const [name2, expected] of Object.entries(conditions)) {
1297
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
1298
+ throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
1299
+ }
1300
+ when[name2] = expected;
1301
+ }
1302
+ if (Object.keys(when).length === 0)
1303
+ throw new DefinitionError(`steps[${index}].when must not be empty.`);
1304
+ }
1305
+ return {
1306
+ name: text(step.name, `steps[${index}].name`),
1307
+ exec: argv(step.exec, `steps[${index}].exec`),
1308
+ phase,
1309
+ scope: step.scope,
1310
+ profiles: selectedProfiles,
1311
+ secrets: step.secrets,
1312
+ always: step.always === true,
1313
+ timeoutMs,
1314
+ when
1315
+ };
1316
+ });
1317
+ if (new Set(steps.map((step) => step.name)).size !== steps.length) {
1318
+ throw new DefinitionError("pipeline.steps must have unique names.");
1319
+ }
1320
+ for (const [profile, selected] of Object.entries(profiles)) {
1321
+ if (selected.capacityCalibration && !steps.some((step) => step.phase === "health" && step.scope === "target" && (!step.profiles || step.profiles.includes(profile)))) {
1322
+ throw new DefinitionError(`profiles.${profile}.capacityCalibration requires a target-scoped health step.`);
1323
+ }
1324
+ }
1325
+ const name = text(root.name, "pipeline.name");
1326
+ if (name.length > 120)
1327
+ throw new DefinitionError("pipeline.name must be at most 120 characters.");
1328
+ return {
1329
+ version: PIPELINE_VERSION,
1330
+ name,
1331
+ requireAttestation: root.requireAttestation === true,
1332
+ profiles,
1333
+ steps
1334
+ };
1335
+ }
1336
+ function phasePipeline(definition, phase, profile, executeRelease = false) {
1337
+ if (!Object.hasOwn(definition.profiles, profile)) {
1338
+ throw new DefinitionError(`pipeline profile does not exist: ${profile}.`);
1339
+ }
1340
+ return {
1341
+ name: `${definition.name}:${phase}`,
1342
+ requireAttestation: definition.requireAttestation,
1343
+ steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(profile)) && (step.scope === "target" || executeRelease))
1344
+ };
1345
+ }
1346
+
789
1347
  // src/software-helper.ts
790
- import { chmodSync as chmodSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
1348
+ import { chmodSync as chmodSync4, existsSync as existsSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "node:fs";
791
1349
  import { connect as connect2, createServer as createServer2 } from "node:net";
792
- import { dirname as dirname3 } from "node:path";
1350
+ import { dirname as dirname4 } from "node:path";
1351
+
1352
+ // src/service-supervisor.ts
1353
+ import { createHash as createHash3 } from "node:crypto";
1354
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
1355
+ import { dirname as dirname3, join as join4, resolve as resolve3, sep } from "node:path";
1356
+ var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
1357
+ var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
1358
+ var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
1359
+ var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
1360
+ var idFor = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
1361
+ var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
1362
+ var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
1363
+ var defaultHost = {
1364
+ write(path2, content, mode) {
1365
+ mkdirSync4(dirname3(path2), { recursive: true, mode: 493 });
1366
+ const next = `${path2}.next`;
1367
+ writeFileSync4(next, content, { mode });
1368
+ renameSync4(next, path2);
1369
+ },
1370
+ read: (path2) => readFileSync4(path2, "utf8"),
1371
+ exists: existsSync3,
1372
+ list: (path2) => existsSync3(path2) ? readdirSync(path2) : [],
1373
+ realpath: realpathSync,
1374
+ mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
1375
+ remove: (path2) => rmSync4(path2, { force: true }),
1376
+ async exec(argv2) {
1377
+ const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
1378
+ PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
1379
+ LANG: "C",
1380
+ LC_ALL: "C"
1381
+ } });
1382
+ const [stdout, stderr, exitCode] = await Promise.all([
1383
+ new Response(child.stdout).text(),
1384
+ new Response(child.stderr).text(),
1385
+ child.exited
1386
+ ]);
1387
+ return { exitCode, output: `${stdout}${stderr}` };
1388
+ },
1389
+ async health(port, path2) {
1390
+ try {
1391
+ const response = await fetch(`http://127.0.0.1:${port}${path2}`, {
1392
+ signal: AbortSignal.timeout(2000),
1393
+ redirect: "manual"
1394
+ });
1395
+ return response.status >= 200 && response.status < 300;
1396
+ } catch {
1397
+ return false;
1398
+ }
1399
+ },
1400
+ sleep: (ms) => Bun.sleep(ms),
1401
+ now: Date.now
1402
+ };
1403
+ function readState(host, path2) {
1404
+ if (!host.exists(path2))
1405
+ return null;
1406
+ try {
1407
+ const value = JSON.parse(host.read(path2));
1408
+ return value?.format === 1 && Array.isArray(value.applicationPorts) ? value : null;
1409
+ } catch {
1410
+ return null;
1411
+ }
1412
+ }
1413
+ function allStates(host) {
1414
+ return host.list(SERVICE_STATE_DIRECTORY).filter((name) => /^[a-f0-9]{24}\.json$/.test(name)).flatMap((name) => {
1415
+ const state = readState(host, join4(SERVICE_STATE_DIRECTORY, name));
1416
+ return state ? [state] : [];
1417
+ });
1418
+ }
1419
+ function allocatedPorts(request, id, previous, states) {
1420
+ const required = request.service.strategy === "blue-green" ? 2 : 1;
1421
+ const occupied = new Set(states.filter((state) => state.id !== id).flatMap((state) => [state.publicPort, ...state.applicationPorts]));
1422
+ if (occupied.has(request.service.publicPort))
1423
+ throw new Error("SERVICE_PUBLIC_PORT_CONFLICT");
1424
+ const allocation = request.service.applicationPorts;
1425
+ if (allocation.mode === "fixed") {
1426
+ if (allocation.ports.some((port) => occupied.has(port)))
1427
+ throw new Error("SERVICE_APPLICATION_PORT_CONFLICT");
1428
+ return [...allocation.ports];
1429
+ }
1430
+ if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
1431
+ return [...previous.applicationPorts];
1432
+ const width = allocation.to - allocation.from + 1;
1433
+ const start = Number.parseInt(createHash3("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
1434
+ for (let offset = 0;offset < width; offset += 1) {
1435
+ const first = allocation.from + (start + offset) % width;
1436
+ const candidate = Array.from({ length: required }, (_, index) => first + index);
1437
+ if (candidate.at(-1) <= allocation.to && candidate.every((port) => !occupied.has(port) && port !== request.service.publicPort)) {
1438
+ return candidate;
1439
+ }
1440
+ }
1441
+ throw new Error("SERVICE_DYNAMIC_PORTS_EXHAUSTED");
1442
+ }
1443
+ var appConfig = (request, port) => `${JSON.stringify({
1444
+ format: 1,
1445
+ root: request.root,
1446
+ release: request.release,
1447
+ home: `${request.root}/app-home`,
1448
+ argv: request.service.command.map((argument) => argument === "${FZ_APP_PORT}" ? String(port) : argument === "${FZ_RELEASE}" ? request.release : argument),
1449
+ environment: { FZ_APP_PORT: String(port), FZ_RELEASE: request.release }
1450
+ }, null, 2)}
1451
+ `;
1452
+ var unit = (id, slot) => `[Unit]
1453
+ Description=ForgeZero supervised tenant application ${id} slot ${slot}
1454
+ After=network-online.target
1455
+ Wants=network-online.target
1456
+
1457
+ [Service]
1458
+ Type=simple
1459
+ User=forgezero-app
1460
+ Group=forgezero-vault
1461
+ ExecStart=/usr/local/lib/forgezero/agent/fz-agent supervised-app --config=${SERVICE_CONFIG_DIRECTORY}/${id}-slot${slot}.json
1462
+ Restart=always
1463
+ RestartSec=2
1464
+ NoNewPrivileges=true
1465
+ PrivateTmp=true
1466
+ ProtectSystem=strict
1467
+ ProtectHome=true
1468
+ LimitCORE=0
1469
+
1470
+ [Install]
1471
+ WantedBy=multi-user.target
1472
+ `;
1473
+ var nginx = (request, id, port) => `# ForgeZero ${id}
1474
+ ${request.service.maxConnections ? `limit_conn_zone $server_name zone=fz_${id}:64k;
1475
+ ` : ""}server {
1476
+ listen 127.0.0.1:${request.service.publicPort};
1477
+ server_name _;
1478
+ ${request.service.maxConnections ? ` limit_conn fz_${id} ${request.service.maxConnections};
1479
+ limit_conn_status 503;
1480
+ ` : ""} location / {
1481
+ proxy_pass http://127.0.0.1:${port};
1482
+ proxy_http_version 1.1;
1483
+ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
1484
+ proxy_set_header Connection "upgrade";
1485
+ ` : ""} proxy_set_header Host $host;
1486
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1487
+ proxy_set_header X-Forwarded-Proto $scheme;
1488
+ }
1489
+ }
1490
+ `;
1491
+ async function checked2(host, argv2, code) {
1492
+ const result = await host.exec(argv2);
1493
+ if (result.exitCode !== 0)
1494
+ throw new Error(`${code}: ${result.output.slice(0, 512)}`);
1495
+ }
1496
+ async function activateSupervisedService(request, host = defaultHost) {
1497
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
1498
+ throw new Error("SERVICE_IDENTITY_INVALID");
1499
+ }
1500
+ const root = host.realpath(resolve3(request.root));
1501
+ const release = host.realpath(resolve3(request.release));
1502
+ if (!within(root, release))
1503
+ throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
1504
+ request = { ...request, root, release, service: validateDeploymentService(request.service) };
1505
+ const id = idFor(request.key);
1506
+ host.mkdir(SERVICE_STATE_DIRECTORY, 448);
1507
+ host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
1508
+ const previous = readState(host, statePath(id));
1509
+ const ports = allocatedPorts(request, id, previous, allStates(host));
1510
+ const nextSlot = request.service.strategy === "blue-green" ? previous?.activeSlot === 0 ? 1 : 0 : 0;
1511
+ const port = ports[nextSlot] ?? ports[0];
1512
+ const configPath = `${SERVICE_CONFIG_DIRECTORY}/${id}-slot${nextSlot}.json`;
1513
+ const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
1514
+ host.write(configPath, appConfig({ ...request, release }, port), 292);
1515
+ host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
1516
+ await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
1517
+ if (request.service.strategy === "direct" && previous) {
1518
+ await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
1519
+ }
1520
+ await checked2(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
1521
+ let healthy = false;
1522
+ for (let attempt = 0;attempt < 30; attempt += 1) {
1523
+ const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
1524
+ if (active.exitCode === 0 && await host.health(port, request.service.healthPath)) {
1525
+ healthy = true;
1526
+ break;
1527
+ }
1528
+ await host.sleep(1000);
1529
+ }
1530
+ if (!healthy) {
1531
+ await host.exec(["/usr/bin/systemctl", "stop", unitName]);
1532
+ throw new Error("SERVICE_HEALTH_FAILED");
1533
+ }
1534
+ const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
1535
+ const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
1536
+ try {
1537
+ host.write(nginxPath, nginx(request, id, port), 420);
1538
+ await checked2(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
1539
+ await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
1540
+ } catch (cause) {
1541
+ if (oldNginx === null)
1542
+ host.remove(nginxPath);
1543
+ else
1544
+ host.write(nginxPath, oldNginx, 420);
1545
+ await host.exec(["/usr/bin/systemctl", "stop", unitName]);
1546
+ throw cause;
1547
+ }
1548
+ const state = {
1549
+ format: 1,
1550
+ id,
1551
+ key: request.key,
1552
+ revision: request.revision,
1553
+ strategy: request.service.strategy,
1554
+ publicPort: request.service.publicPort,
1555
+ applicationPorts: ports,
1556
+ activeSlot: nextSlot,
1557
+ healthPath: request.service.healthPath,
1558
+ updatedAtTs: host.now()
1559
+ };
1560
+ host.write(statePath(id), `${JSON.stringify(state, null, 2)}
1561
+ `, 384);
1562
+ if (request.service.strategy === "blue-green" && previous && previous.activeSlot !== nextSlot) {
1563
+ await host.sleep(request.service.drainMs ?? 30000);
1564
+ await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
1565
+ }
1566
+ return state;
1567
+ }
1568
+
1569
+ // src/software-helper.ts
793
1570
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
794
1571
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
795
1572
  var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
796
- var MAX_REQUEST_BYTES2 = 8 * 1024;
1573
+ var MAX_REQUEST_BYTES2 = 128 * 1024;
797
1574
  var MAX_PENDING_REQUESTS = 128;
798
- var execute = async (command2) => {
799
- const child = Bun.spawn(["/bin/bash", "-Eeuo", "pipefail", "-c", command2], {
800
- stdout: "pipe",
801
- stderr: "pipe",
802
- env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
803
- });
804
- const [stdout, stderr, exitCode] = await Promise.all([
805
- new Response(child.stdout).text(),
806
- new Response(child.stderr).text(),
807
- child.exited
808
- ]);
809
- return { exitCode, output: `${stdout}${stderr}` };
810
- };
811
1575
  function startSoftwareHelper(options = {}) {
812
1576
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
813
- if (existsSync3(socketPath))
814
- unlinkSync2(socketPath);
815
- mkdirSync3(dirname3(socketPath), { recursive: true, mode: 488 });
1577
+ if (existsSync4(socketPath))
1578
+ unlinkSync3(socketPath);
1579
+ mkdirSync5(dirname4(socketPath), { recursive: true, mode: 488 });
816
1580
  const ensure = options.ensure ?? ensureSoftwareRequirements;
1581
+ const activate = options.activate ?? activateSupervisedService;
817
1582
  let tail = Promise.resolve();
818
1583
  let pending = 0;
819
1584
  const server = createServer2((socket) => {
@@ -838,12 +1603,23 @@ function startSoftwareHelper(options = {}) {
838
1603
  }
839
1604
  pending += 1;
840
1605
  const work = tail.then(() => Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
841
- if (request.op !== "ensure")
842
- throw new Error("unknown software helper operation");
843
- const requirements = validateSoftwareRequirements(request.requirements);
844
- const results = await ensure(requirements, { exec: execute });
845
- socket.end(`${JSON.stringify({ ok: true, results })}
1606
+ if (request.op === "ensure") {
1607
+ const requirements = validateSoftwareRequirements(request.requirements);
1608
+ const results = await ensure(requirements, { exec: executeSoftwareOperation });
1609
+ socket.end(`${JSON.stringify({ ok: true, results })}
1610
+ `);
1611
+ return;
1612
+ }
1613
+ if (request.op === "activate-service" && request.request) {
1614
+ if (!options.allowedRoot || request.request.root !== options.allowedRoot) {
1615
+ throw new Error("service deployment root is not owned by this helper");
1616
+ }
1617
+ const state = await activate(request.request);
1618
+ socket.end(`${JSON.stringify({ ok: true, state })}
846
1619
  `);
1620
+ return;
1621
+ }
1622
+ throw new Error("unknown software helper operation");
847
1623
  }).catch((cause) => socket.end(`${JSON.stringify({
848
1624
  ok: false,
849
1625
  error: { code: "SOFTWARE_REFUSED", message: cause instanceof Error ? cause.message : String(cause) }
@@ -859,12 +1635,40 @@ function startSoftwareHelper(options = {}) {
859
1635
  });
860
1636
  socket.on("error", () => socket.destroy());
861
1637
  });
862
- server.listen(socketPath, () => chmodSync3(socketPath, 432));
1638
+ server.listen(socketPath, () => chmodSync4(socketPath, 432));
863
1639
  return server;
864
1640
  }
1641
+ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
1642
+ return new Promise((resolve4, reject) => {
1643
+ const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
1644
+ `));
1645
+ let buffer = "";
1646
+ socket.setTimeout(timeoutMs, () => {
1647
+ socket.destroy();
1648
+ reject(new Error("service helper timed out"));
1649
+ });
1650
+ socket.on("data", (chunk) => {
1651
+ buffer += chunk.toString("utf8");
1652
+ const newline = buffer.indexOf(`
1653
+ `);
1654
+ if (newline < 0)
1655
+ return;
1656
+ socket.end();
1657
+ try {
1658
+ const response = JSON.parse(buffer.slice(0, newline));
1659
+ if (!response.ok || !response.state)
1660
+ throw new Error(response.error?.message ?? "service helper refused activation");
1661
+ resolve4(response.state);
1662
+ } catch (cause) {
1663
+ reject(cause);
1664
+ }
1665
+ });
1666
+ socket.on("error", reject);
1667
+ });
1668
+ }
865
1669
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
866
1670
  validateSoftwareRequirements(requirements);
867
- return new Promise((resolve3, reject) => {
1671
+ return new Promise((resolve4, reject) => {
868
1672
  const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
869
1673
  `));
870
1674
  let buffer = "";
@@ -883,7 +1687,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
883
1687
  const response = JSON.parse(buffer.slice(0, newline));
884
1688
  if (!response.ok || !response.results)
885
1689
  throw new Error(response.error?.message ?? "software helper refused the request");
886
- resolve3(response.results);
1690
+ resolve4(response.results);
887
1691
  } catch (cause) {
888
1692
  reject(cause);
889
1693
  }
@@ -893,11 +1697,10 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
893
1697
  }
894
1698
 
895
1699
  // src/version.ts
896
- var VERSION3 = "0.1.40";
1700
+ var VERSION3 = "0.1.42";
897
1701
 
898
1702
  // src/egress-policy.ts
899
- import { realpathSync } from "node:fs";
900
- var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
1703
+ import { realpathSync as realpathSync2 } from "node:fs";
901
1704
  var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
902
1705
  var BLOCKED_IPV4 = [
903
1706
  "0.0.0.0/8",
@@ -976,22 +1779,26 @@ function atLeast(version, floor) {
976
1779
  }
977
1780
  var CAPABILITY_CHECKS = {
978
1781
  snpGuest: {
979
- command: "test -e /dev/sev-guest && echo yes || echo no",
1782
+ command: "fz host check-device /dev/sev-guest",
1783
+ operation: { kind: "path-exists", path: "/dev/sev-guest", nodeType: "file" },
980
1784
  satisfied: (stdout) => stdout.trim() === "yes",
981
1785
  remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
982
1786
  },
983
1787
  systemd: {
984
- command: "test -d /run/systemd/system && echo yes || echo no",
1788
+ command: "fz host check-directory /run/systemd/system",
1789
+ operation: { kind: "path-exists", path: "/run/systemd/system", nodeType: "directory" },
985
1790
  satisfied: (stdout) => stdout.trim() === "yes",
986
1791
  remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
987
1792
  },
988
1793
  bun: {
989
- command: "bun --version 2>/dev/null || echo missing",
1794
+ command: "fz host check-version bun",
1795
+ operation: { kind: "version", argv: ["/usr/local/bin/bun", "--version"] },
990
1796
  satisfied: (stdout) => atLeast(stdout, "1.1.0"),
991
- remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
1797
+ remedy: "Install the pinned Bun release with `fz bootstrap`."
992
1798
  },
993
1799
  python: {
994
- command: "python3 --version 2>/dev/null || echo missing",
1800
+ command: "fz host check-version python3",
1801
+ operation: { kind: "version", argv: ["/usr/bin/python3", "--version"] },
995
1802
  satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
996
1803
  remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
997
1804
  }
@@ -999,6 +1806,7 @@ var CAPABILITY_CHECKS = {
999
1806
  var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
1000
1807
  var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, so the agent can prove what it is running and the platform can refuse it if the measurement is wrong." : "No SEV-SNP guest device. The agent authenticates with its enrolment token and hybrid Ed25519 + ML-DSA signature — weaker than attestation, stronger than an API key in the application.";
1001
1808
  var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
1809
+ var APPLICATION_RUNTIME_USER = "forgezero-app";
1002
1810
  var DEPLOYMENT_GROUP = "forgezero-deploy";
1003
1811
  var VAULT_GROUP = "forgezero-vault";
1004
1812
  var LIFECYCLE_GROUP = "forgezero-lifecycle";
@@ -1028,11 +1836,7 @@ function agentEgressUnit(options) {
1028
1836
  systemdAgentEgressDirectives(runnerLoopbackPorts);
1029
1837
  const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
1030
1838
  const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
1031
- const policyProofs = deploymentEnabled ? [
1032
- ...runnerLoopbackPorts.length > 0 ? [`loopback=.*:${runnerLoopbackPorts.join(",")}`] : [],
1033
- `public-tcp=${runnerPublicTcpPorts.join(",")}`
1034
- ].map((pattern) => `ExecStartPost=/bin/sh -c '/usr/sbin/nft --numeric list table inet ${AGENT_EGRESS_TABLE} | /usr/bin/grep -q "${pattern}"'`).join(`
1035
- `) : "";
1839
+ const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}`;
1036
1840
  return `[Unit]
1037
1841
  Description=ForgeZero Agent host egress policy
1038
1842
  Documentation=https://www.forgezero.net/docs/agent
@@ -1046,7 +1850,7 @@ NotifyAccess=all
1046
1850
  User=root
1047
1851
  Group=root
1048
1852
  ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
1049
- ${policyProofs}
1853
+ ${policyProof}
1050
1854
  Restart=on-failure
1051
1855
  RestartSec=2
1052
1856
  LimitCORE=0
@@ -1070,6 +1874,7 @@ WantedBy=multi-user.target
1070
1874
  }
1071
1875
  function softwareHelperUnit(options) {
1072
1876
  const bin = options.binPath ?? "fz-agent";
1877
+ const root = options.deployRoot ?? "/opt/forgezero";
1073
1878
  return `[Unit]
1074
1879
  Description=ForgeZero declarative software strategy helper
1075
1880
  Documentation=https://www.forgezero.net/docs/agent
@@ -1081,6 +1886,7 @@ Type=simple
1081
1886
  User=root
1082
1887
  Group=${SOFTWARE_HELPER_GROUP}
1083
1888
  Environment=FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}
1889
+ Environment=FZ_DEPLOY_ROOT=${root}
1084
1890
  ExecStart=${bin} software-helper
1085
1891
  Restart=always
1086
1892
  RestartSec=2
@@ -1196,10 +2002,6 @@ var systemdPath = (value, label) => {
1196
2002
  throw new Error(`invalid ${label} path`);
1197
2003
  return value;
1198
2004
  };
1199
- var awaitSocketCommand = (path) => {
1200
- const socket = systemdPath(path, "readiness socket");
1201
- return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
1202
- };
1203
2005
  var validNodeHostname = (value) => !value || value.length <= 253 && value === value.toLowerCase() && value.split(".").length >= 3 && value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
1204
2006
  function warpConfigUnit(options) {
1205
2007
  if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
@@ -1300,6 +2102,8 @@ function agentEnrolmentUnit(options) {
1300
2102
  const hostname = options.nodeHostname ? `Environment=FZ_NODE_HOSTNAME=${options.nodeHostname}
1301
2103
  ` : "";
1302
2104
  const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
2105
+ ` : "";
2106
+ const bootstrapSshPublicKey = options.bootstrapSshPublicKeyPath ? `Environment=FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE=${options.bootstrapSshPublicKeyPath}
1303
2107
  ` : "";
1304
2108
  const networkAttachment = [
1305
2109
  options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
@@ -1334,7 +2138,7 @@ Environment=FZ_SEED_CREDENTIAL=agent-seed
1334
2138
  Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
1335
2139
  Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
1336
2140
  Environment=FZ_API=${options.apiUrl}
1337
- ${label}${hostname}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
2141
+ ${label}${hostname}${gitPublicKey}${bootstrapSshPublicKey}${networkAttachment}ExecStart=${bin} enrol
1338
2142
  # A '+' fixed command runs as root solely to remove the host-bound one-time
1339
2143
  # ciphertext. Tenant code and the agent never receive a privilege boundary.
1340
2144
  ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
@@ -1428,9 +2232,14 @@ function agentUnit(options) {
1428
2232
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1429
2233
  throw new Error("migration pull and lifecycle profile must be supplied together");
1430
2234
  }
1431
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1432
- if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1433
- throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
2235
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
2236
+ if ([
2237
+ options.pullBootstrap,
2238
+ options.bootstrapSshCredentialPath,
2239
+ options.bootstrapSshPublicKeyPath,
2240
+ options.bootstrapTargetTelemetryEndpoint
2241
+ ].some(Boolean) && !bootstrapEnabled) {
2242
+ throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
1434
2243
  }
1435
2244
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1436
2245
  const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
@@ -1465,8 +2274,8 @@ function agentUnit(options) {
1465
2274
  throw new Error(`invalid deployment environment entry: ${name}`);
1466
2275
  }
1467
2276
  }
1468
- for (const [name, path] of Object.entries(deploymentCredentials)) {
1469
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path.startsWith("/") || /[\r\n:]/.test(path)) {
2277
+ for (const [name, path2] of Object.entries(deploymentCredentials)) {
2278
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path2.startsWith("/") || /[\r\n:]/.test(path2)) {
1470
2279
  throw new Error(`invalid deployment credential entry: ${name}`);
1471
2280
  }
1472
2281
  }
@@ -1501,6 +2310,7 @@ function agentUnit(options) {
1501
2310
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1502
2311
  options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
1503
2312
  options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
2313
+ options.pullBootstrap && options.bootstrapSshPublicKeyPath ? `FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE=${options.bootstrapSshPublicKeyPath}` : null,
1504
2314
  options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
1505
2315
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1506
2316
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
@@ -1512,7 +2322,7 @@ function agentUnit(options) {
1512
2322
  ` : "";
1513
2323
  const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
1514
2324
  ` : "";
1515
- const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
2325
+ const projectCredentials = Object.entries(deploymentCredentials).map(([name, path2]) => `LoadCredentialEncrypted=${name}:${path2}`).join(`
1516
2326
  `);
1517
2327
  const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache ${deployRoot}/capacity` : "";
1518
2328
  const supplementaryGroups = [
@@ -1606,6 +2416,55 @@ ${deploymentWrites}
1606
2416
  WantedBy=multi-user.target
1607
2417
  `;
1608
2418
  }
2419
+ var renderOperation = (operation) => {
2420
+ if (operation.kind === "commands")
2421
+ return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
2422
+ `);
2423
+ if (operation.kind === "directories")
2424
+ return operation.directories.map((directory) => [
2425
+ "/usr/bin/install",
2426
+ "-d",
2427
+ ...directory.owner ? ["-o", directory.owner] : [],
2428
+ ...directory.group ? ["-g", directory.group] : [],
2429
+ "-m",
2430
+ directory.mode.toString(8).padStart(4, "0"),
2431
+ directory.path
2432
+ ].join(" ")).join(`
2433
+ `);
2434
+ if (operation.kind === "install-runtime")
2435
+ return `/usr/bin/install -m 0755 ${operation.source} /opt/forgezero/agent/versions/${operation.version}/dist/fz-agent.js`;
2436
+ if (operation.kind === "ensure-seed")
2437
+ return `/usr/bin/systemd-creds encrypt --name=agent-seed - ${operation.credential}`;
2438
+ if (operation.kind === "ensure-git-identity")
2439
+ return `/usr/bin/ssh-keygen -t ed25519
2440
+ /usr/bin/systemd-creds encrypt --name=git-deploy-key <private> ${operation.credential}
2441
+ fz host write-public-key ${operation.publicKey}`;
2442
+ if (operation.kind === "ensure-bootstrap-ssh-identity")
2443
+ return `/usr/bin/ssh-keygen -t ed25519
2444
+ /usr/bin/systemd-creds encrypt --name=bootstrap-ssh-key <private> ${operation.credential}
2445
+ fz host write-public-key ${operation.publicKey}`;
2446
+ if (operation.kind === "ensure-enrolment")
2447
+ return `/usr/bin/systemd-creds encrypt --name=enrol-token ${operation.source} ${operation.credential}
2448
+ /usr/bin/rm -f ${operation.source}`;
2449
+ if (operation.kind === "wait-socket")
2450
+ return `fz host wait-socket ${operation.path}`;
2451
+ if (operation.kind === "verify-file")
2452
+ return `fz host verify-file ${operation.path}`;
2453
+ if (operation.kind === "verify-egress")
2454
+ return `/usr/sbin/nft --numeric list table inet forgezero_agent_egress
2455
+ fz-agent egress-policy-check`;
2456
+ if (operation.kind === "verify-resolved-stub")
2457
+ return "fz host verify-resolved-stub /run/systemd/resolve/stub-resolv.conf";
2458
+ if (operation.kind === "install-warp")
2459
+ return "/usr/bin/apt-get install -y cloudflare-warp";
2460
+ return "/usr/bin/warp-cli --accept-tos status";
2461
+ };
2462
+ var step = (label, operation, optional = false) => ({
2463
+ label,
2464
+ operation,
2465
+ optional: optional || undefined,
2466
+ command: renderOperation(operation)
2467
+ });
1609
2468
  var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
1610
2469
  function planProvision(options) {
1611
2470
  const mode = options.mode;
@@ -1620,9 +2479,15 @@ function planProvision(options) {
1620
2479
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1621
2480
  throw new Error("migration pull and lifecycle profile must be supplied together");
1622
2481
  }
1623
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1624
- if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1625
- throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
2482
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
2483
+ if ([
2484
+ options.pullBootstrap,
2485
+ options.bootstrapSshCredentialPath,
2486
+ options.bootstrapSshPublicKeyPath,
2487
+ options.bootstrapSshSourcePath,
2488
+ options.bootstrapTargetTelemetryEndpoint
2489
+ ].some(Boolean) && !bootstrapEnabled) {
2490
+ throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
1626
2491
  }
1627
2492
  const warpValues = [
1628
2493
  options.warpOrganization,
@@ -1650,8 +2515,29 @@ function planProvision(options) {
1650
2515
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
1651
2516
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1652
2517
  const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
2518
+ const bootstrapSshPublicKeyPath = bootstrapEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
2519
+ const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
2520
+ const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
1653
2521
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
1654
2522
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
2523
+ const enabledUnits = [
2524
+ "forgezero-agent.socket",
2525
+ "forgezero-agent-update-helper.service",
2526
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2527
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2528
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2529
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2530
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
2531
+ "forgezero-agent.service"
2532
+ ];
2533
+ const restartedUnits = [
2534
+ "forgezero-agent-update-helper.service",
2535
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2536
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2537
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2538
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2539
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
2540
+ ];
1655
2541
  return {
1656
2542
  mode,
1657
2543
  reason: reasonFor(mode),
@@ -1696,146 +2582,91 @@ function planProvision(options) {
1696
2582
  socketPath: options.socketPath,
1697
2583
  user,
1698
2584
  steps: [
1699
- ...options.enforceEgress ? [{
1700
- label: "Ubuntu Agent egress prerequisites",
1701
- command: `. /etc/os-release; test "$ID" = ubuntu; ` + `DEBIAN_FRONTEND=noninteractive apt-get update -qq; ` + `DEBIAN_FRONTEND=noninteractive apt-get install -y nftables; ` + `systemctl enable --now systemd-resolved.service; ` + `test "$(readlink -f /etc/resolv.conf)" = /run/systemd/resolve/stub-resolv.conf; ` + `test -s /run/systemd/resolve/stub-resolv.conf`
1702
- }] : [],
1703
- {
1704
- label: "vault socket access group",
1705
- command: `groupadd --system ${VAULT_GROUP} || true`
1706
- },
1707
- {
1708
- label: "Agent update helper access group",
1709
- command: `groupadd --system ${AGENT_UPDATE_GROUP} || true`
1710
- },
1711
- ...sourceBinPath && binPath ? [{
1712
- label: "root-owned agent runtime",
1713
- command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION3}/dist; ` + `install -o root -g root -m 0755 ${sourceBinPath} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION3}/dist/fz-agent.js; ` + `ln -sfn versions/${VERSION3} ${DEFAULT_AGENT_RELEASE_ROOT}/current.next; ` + `mv -Tf ${DEFAULT_AGENT_RELEASE_ROOT}/current.next ${DEFAULT_AGENT_RELEASE_ROOT}/current; ` + `rm -f ${binPath}; ln -s ${DEFAULT_AGENT_RELEASE_ROOT}/current/dist/fz-agent.js ${binPath}`
1714
- }] : [],
1715
- ...warpEnabled ? [{
1716
- label: "Cloudflare One client for Ubuntu 26.04",
1717
- command: `. /etc/os-release; test "$ID" = ubuntu && test "$VERSION_ID" = 26.04; ` + `install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d /etc/systemd/system/warp-svc.service.d; ` + `curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg -o /run/cloudflare-warp-key.gpg; ` + `gpg --batch --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg /run/cloudflare-warp-key.gpg; ` + `rm -f /run/cloudflare-warp-key.gpg; ` + `printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\\n' "$VERSION_CODENAME" > /etc/apt/sources.list.d/cloudflare-client.list; ` + `apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y cloudflare-warp`
1718
- }] : [],
1719
- ...deploymentEnabled ? [{
1720
- label: "deployment isolation group",
1721
- command: `groupadd --system ${DEPLOYMENT_GROUP} || true; groupadd --system ${SOFTWARE_HELPER_GROUP} || true`
1722
- }] : [],
1723
- ...lifecycleEnabled ? [{
1724
- label: "lifecycle helper access group",
1725
- command: `groupadd --system ${LIFECYCLE_GROUP} || true`
1726
- }] : [],
1727
- {
1728
- label: "service account",
1729
- command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
1730
- },
1731
- {
1732
- label: "bind service account to vault group",
1733
- command: `usermod -g ${VAULT_GROUP} ${user}`
1734
- },
1735
- {
1736
- label: "grant verified Agent update access",
1737
- command: `usermod -a -G ${AGENT_UPDATE_GROUP} ${user}`
1738
- },
1739
- ...lifecycleEnabled ? [{
1740
- label: "grant lifecycle helper socket access",
1741
- command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
1742
- }] : [],
1743
- ...deploymentEnabled ? [{
1744
- label: "credential-free deployment account",
1745
- command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP} ${user}`
1746
- }] : [],
1747
- {
1748
- label: "credential directory",
1749
- command: `install -d -o root -g root -m 0700 ${credentialDir}`
1750
- },
1751
- {
1752
- label: "Agent state directory",
1753
- command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
1754
- },
1755
- {
1756
- label: "encrypted node identity",
1757
- command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
1758
- },
2585
+ ...options.enforceEgress ? [step("Ubuntu Agent egress prerequisites", { kind: "commands", commands: [
2586
+ { argv: ["/usr/bin/apt-get", "update", "-qq"] },
2587
+ { argv: ["/usr/bin/apt-get", "install", "-y", "nftables"] },
2588
+ { argv: ["/usr/bin/systemctl", "enable", "--now", "systemd-resolved.service"] }
2589
+ ] }), step("prove systemd-resolved stub ownership", { kind: "verify-resolved-stub" })] : [],
2590
+ step("vault socket access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", VAULT_GROUP], acceptedExitCodes: [0, 9] }] }),
2591
+ step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
2592
+ ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION3 })] : [],
2593
+ ...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
2594
+ ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
2595
+ { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
2596
+ { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
2597
+ ] })] : [],
2598
+ ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
2599
+ step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
2600
+ step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
2601
+ step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
2602
+ ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
2603
+ ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
2604
+ { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
2605
+ { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
2606
+ { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
2607
+ { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
2608
+ ] })] : [],
2609
+ step("credential and state directories", { kind: "directories", directories: [
2610
+ { path: credentialDir, mode: 448, owner: "root", group: "root" },
2611
+ { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
2612
+ ] }),
2613
+ step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
1759
2614
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
1760
- {
1761
- label: "Git deploy identity directory",
1762
- command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
1763
- },
1764
- {
1765
- label: "unique encrypted Git deploy identity",
1766
- command: `test -s ${gitCredentialPath} || { ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/forgezero-git-deploy-key; ` + `systemd-creds encrypt --name=git-deploy-key /run/forgezero-git-deploy-key ${gitCredentialPath}; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `chmod 0400 ${gitCredentialPath}; }; ` + `test -s ${gitPublicKeyPath} || { ` + `systemd-creds decrypt --name=git-deploy-key ${gitCredentialPath} /run/forgezero-git-deploy-key; ` + `ssh-keygen -y -f /run/forgezero-git-deploy-key | ` + `sed 's/$/ forgezero-compute/' > /run/forgezero-git-deploy-key.pub; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; }; ` + `test -s ${gitCredentialPath} && test -s ${gitPublicKeyPath}`
1767
- }
2615
+ step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
2616
+ step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
2617
+ ] : [],
2618
+ ...bootstrapEnabled ? [
2619
+ step("bootstrap SSH public identity directory", { kind: "directories", directories: [
2620
+ { path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
2621
+ ] }),
2622
+ step("unique encrypted bootstrap SSH identity", {
2623
+ kind: "ensure-bootstrap-ssh-identity",
2624
+ credential: bootstrapSshCredentialPath,
2625
+ publicKey: bootstrapSshPublicKeyPath,
2626
+ ...bootstrapSshSourcePath ? { source: bootstrapSshSourcePath } : {}
2627
+ })
1768
2628
  ] : [],
1769
2629
  ...enrolmentEnabled ? [
1770
- {
1771
- label: "enrolment state directory",
1772
- command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
1773
- },
1774
- {
1775
- label: "encrypted one-time enrolment capability",
1776
- command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
1777
- }
2630
+ step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
2631
+ step("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
1778
2632
  ] : [],
1779
- ...deploymentEnabled ? [{
1780
- label: "deployment directories",
1781
- command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/capacity && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
1782
- }] : [],
1783
- { label: "reload units", command: "systemctl daemon-reload" },
1784
- ...deploymentEnabled ? [{
1785
- label: "remove unsupported deployment socket activation",
1786
- command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
1787
- }] : [],
1788
- {
1789
- label: "enable and converge services",
1790
- command: `systemctl enable ${[
1791
- "forgezero-agent.socket",
1792
- "forgezero-agent-update-helper.service",
1793
- ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
1794
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
1795
- ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
1796
- ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
1797
- ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
1798
- "forgezero-agent.service"
1799
- ].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
1800
- "forgezero-agent-update-helper.service",
1801
- ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
1802
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
1803
- ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
1804
- ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
1805
- ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
1806
- ].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
1807
- },
1808
- ...enrolmentEnabled ? [{
1809
- label: "prove the compute binding is durable",
1810
- command: `test -s ${enrolStatePath}`
1811
- }] : [],
1812
- ...options.enforceEgress ? [{
1813
- label: "prove the Agent egress policy is active",
1814
- command: "systemctl is-active forgezero-agent-egress.service && " + "nft --numeric list table inet forgezero_agent_egress | grep -q forgezero-agent-egress-v1" + (deploymentEnabled ? ` && nft --numeric list table inet forgezero_agent_egress | grep -q 'public-tcp=${runnerPublicTcpPorts.join(",")}'` + (runnerLoopbackPorts.length > 0 ? ` && nft --numeric list table inet forgezero_agent_egress | grep -Eq 'loopback=[0-9]+:${runnerLoopbackPorts.join(",")}( |")'` : "") : "")
1815
- }] : [],
1816
- { label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
1817
- { label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
1818
- { label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
1819
- { label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
1820
- ...deploymentEnabled ? [{
1821
- label: "prove the deployment runner socket exists",
1822
- command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
1823
- }, {
1824
- label: "prove the software strategy helper socket exists",
1825
- command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
1826
- }] : [],
1827
- ...lifecycleEnabled ? [{
1828
- label: "prove the lifecycle helper socket exists",
1829
- command: awaitSocketCommand(lifecycleHelperSocketPath)
1830
- }] : [],
1831
- ...warpEnabled ? [{
1832
- label: "prove Cloudflare WARP is connected",
1833
- command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
1834
- }] : [],
1835
- ...options.repository ? [{
1836
- label: "prove the deployment control socket exists",
1837
- command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
1838
- }] : []
2633
+ ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
2634
+ { path: deployRoot, mode: 493, owner: "root", group: "root" },
2635
+ { path: `${deployRoot}/releases`, mode: 2040, owner: "root", group: DEPLOYMENT_GROUP },
2636
+ { path: `${deployRoot}/cache`, mode: 488, owner: user, group: user },
2637
+ { path: `${deployRoot}/capacity`, mode: 448, owner: user, group: user },
2638
+ { path: `${deployRoot}/agent-home`, mode: 448, owner: user, group: user },
2639
+ { path: `${deployRoot}/runner-home`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
2640
+ { path: `${deployRoot}/runner-home/cache`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
2641
+ { path: `${deployRoot}/app-home`, mode: 448, owner: APPLICATION_RUNTIME_USER, group: VAULT_GROUP }
2642
+ ] })] : [],
2643
+ step("reload units", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "daemon-reload"] }] }),
2644
+ ...deploymentEnabled ? [step("remove unsupported deployment socket activation", { kind: "commands", commands: [
2645
+ { argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-deploy-runner.socket"], acceptedExitCodes: [0, 1, 5] },
2646
+ { argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
2647
+ { argv: ["/usr/bin/systemctl", "daemon-reload"] }
2648
+ ] })] : [],
2649
+ step("enable and converge services", { kind: "commands", commands: [
2650
+ { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
2651
+ { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2652
+ ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
2653
+ { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
2654
+ { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2655
+ { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
2656
+ ] }),
2657
+ ...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
2658
+ ...options.enforceEgress ? [step("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
2659
+ step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
2660
+ step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
2661
+ step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
2662
+ step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
2663
+ ...deploymentEnabled ? [
2664
+ step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
2665
+ step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
2666
+ ] : [],
2667
+ ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
2668
+ ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
2669
+ ...options.repository ? [step("prove the deployment control socket exists", { kind: "wait-socket", path: options.controlSocketPath ?? "/run/forgezero/control.sock", attempts: 100, intervalMs: 100 })] : []
1839
2670
  ]
1840
2671
  };
1841
2672
  }
@@ -1870,6 +2701,7 @@ export {
1870
2701
  DEPLOYMENT_GROUP,
1871
2702
  DEFAULT_RUNNER_PUBLIC_TCP_PORTS,
1872
2703
  CAPABILITY_CHECKS,
2704
+ APPLICATION_RUNTIME_USER,
1873
2705
  AGENT_SOCKET_UNIT_PATH,
1874
2706
  AGENT_SOCKET_PROXY_UNIT_PATH,
1875
2707
  AGENT_EGRESS_UNIT_PATH