@forgezero/agent 0.1.53 → 0.1.56

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.
@@ -682,6 +682,7 @@ import {
682
682
  accessSync,
683
683
  chmodSync as chmodSync3,
684
684
  copyFileSync,
685
+ existsSync as existsSync3,
685
686
  mkdtempSync,
686
687
  mkdirSync as mkdirSync3,
687
688
  readFileSync as readFileSync3,
@@ -702,6 +703,7 @@ var OS_CATALOG = [
702
703
  ];
703
704
  var SOFTWARE_CATALOG = [
704
705
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
706
+ { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
705
707
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
706
708
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
707
709
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -711,6 +713,7 @@ var SOFTWARE_CATALOG = [
711
713
  ];
712
714
  var UBUNTU_2604_X64 = [
713
715
  { requirement: { id: "bun", version: "1.3.14" } },
716
+ { requirement: { id: "docker", version: "ubuntu-26.04" } },
714
717
  { requirement: { id: "nginx", version: "ubuntu-26.04" } },
715
718
  { requirement: { id: "arangodb", version: "3.11.14" } },
716
719
  { requirement: { id: "cloudflared", version: "2026.7.3" } },
@@ -719,6 +722,15 @@ var UBUNTU_2604_X64 = [
719
722
  { requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
720
723
  ];
721
724
  var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
725
+ var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
726
+ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
727
+ "data-root": "/var/lib/docker",
728
+ "storage-driver": "overlay2",
729
+ "live-restore": true,
730
+ "log-driver": "local",
731
+ "log-opts": { "max-size": "10m", "max-file": "3" }
732
+ }, null, 2)}
733
+ `;
722
734
  var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
723
735
  var runSoftwareCommand = async (argv, env = {}) => {
724
736
  let child;
@@ -761,6 +773,15 @@ async function executeSoftwareOperation(operation) {
761
773
  if (operation.kind === "check") {
762
774
  if (software === "bun")
763
775
  return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
776
+ if (software === "docker") {
777
+ const binary = await run(["/usr/bin/docker", "--version"]);
778
+ if (!successful(binary, /Docker version/))
779
+ return { ...binary, exitCode: 1 };
780
+ if (!existsSync3(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
781
+ return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
782
+ const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
783
+ return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
784
+ }
764
785
  if (software === "nginx") {
765
786
  const binary = await run(["/usr/sbin/nginx", "-v"]);
766
787
  return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
@@ -793,11 +814,22 @@ async function executeSoftwareOperation(operation) {
793
814
  return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
794
815
  }
795
816
  }
796
- if (software === "nginx" || software === "ufw" || software === "openssh-client") {
797
- const installed = await aptInstall(software === "openssh-client" ? "openssh-client" : software);
798
- if (installed.exitCode !== 0 || software !== "nginx")
817
+ if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
818
+ const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
819
+ const installed = await aptInstall(packageName);
820
+ if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
799
821
  return installed;
800
- return run(["/usr/bin/systemctl", "enable", "--now", "nginx.service"]);
822
+ if (software === "docker") {
823
+ mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
824
+ if (existsSync3(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
825
+ return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
826
+ }
827
+ if (!existsSync3(DOCKER_DAEMON_PATH))
828
+ writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
829
+ const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
830
+ return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
831
+ }
832
+ return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
801
833
  }
802
834
  const directory = mkdtempSync(join3(tmpdir(), "forgezero-software-"));
803
835
  try {
@@ -882,7 +914,7 @@ function validateSoftwareRequirements(value, _options = {}) {
882
914
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
883
915
  throw new Error("software requirement contains an unknown field");
884
916
  }
885
- 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)) {
917
+ if (!["bun", "docker", "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)) {
886
918
  throw new Error("software requirement coordinate is invalid");
887
919
  }
888
920
  const requirement = { id: row.id, version: row.version };
@@ -1355,13 +1387,13 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
1355
1387
  }
1356
1388
 
1357
1389
  // src/software-helper.ts
1358
- import { chmodSync as chmodSync4, existsSync as existsSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "node:fs";
1390
+ import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3 } from "node:fs";
1359
1391
  import { connect as connect2, createServer as createServer2 } from "node:net";
1360
- import { dirname as dirname4 } from "node:path";
1392
+ import { dirname as dirname5 } from "node:path";
1361
1393
 
1362
1394
  // src/service-supervisor.ts
1363
1395
  import { createHash as createHash3 } from "node:crypto";
1364
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
1396
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
1365
1397
  import { dirname as dirname3, join as join4, resolve as resolve3, sep } from "node:path";
1366
1398
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
1367
1399
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
@@ -1378,8 +1410,8 @@ var defaultHost = {
1378
1410
  renameSync4(next, path2);
1379
1411
  },
1380
1412
  read: (path2) => readFileSync4(path2, "utf8"),
1381
- exists: existsSync3,
1382
- list: (path2) => existsSync3(path2) ? readdirSync(path2) : [],
1413
+ exists: existsSync4,
1414
+ list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
1383
1415
  realpath: realpathSync,
1384
1416
  mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
1385
1417
  remove: (path2) => rmSync4(path2, { force: true }),
@@ -1576,6 +1608,297 @@ async function activateSupervisedService(request, host = defaultHost) {
1576
1608
  return state;
1577
1609
  }
1578
1610
 
1611
+ // src/container-supervisor.ts
1612
+ import { createHash as createHash4 } from "node:crypto";
1613
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
1614
+ import { dirname as dirname4, resolve as resolve4, sep as sep2 } from "node:path";
1615
+ var defaultHost2 = {
1616
+ realpath: realpathSync2,
1617
+ exists: existsSync5,
1618
+ read: (path2) => readFileSync5(path2, "utf8"),
1619
+ write(path2, content, mode) {
1620
+ mkdirSync5(dirname4(path2), { recursive: true, mode: 493 });
1621
+ const next = `${path2}.next`;
1622
+ writeFileSync5(next, content, { mode });
1623
+ renameSync5(next, path2);
1624
+ },
1625
+ remove: (path2) => rmSync5(path2, { force: true }),
1626
+ list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
1627
+ mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
1628
+ async exec(argv2) {
1629
+ const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
1630
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
1631
+ return { exitCode, output: `${stdout}${stderr}` };
1632
+ },
1633
+ async health(port, path2, timeoutMs, method, expectedStatus) {
1634
+ try {
1635
+ const response = await fetch(`http://127.0.0.1:${port}${path2}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
1636
+ return expectedStatus.includes(response.status);
1637
+ } catch {
1638
+ return false;
1639
+ }
1640
+ },
1641
+ sleep: (ms) => Bun.sleep(ms),
1642
+ now: Date.now
1643
+ };
1644
+ var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
1645
+ var idFor2 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
1646
+ var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
1647
+ var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
1648
+ function validateRequest(request, host) {
1649
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
1650
+ throw new Error("CONTAINER_IDENTITY_INVALID");
1651
+ const root = host.realpath(resolve4(request.root));
1652
+ const release = host.realpath(resolve4(request.release));
1653
+ if (!within2(root, release))
1654
+ throw new Error("CONTAINER_RELEASE_OUTSIDE_ROOT");
1655
+ const application = request.application;
1656
+ if (application.kind !== "application" || application.runtime.kind !== "container")
1657
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
1658
+ if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
1659
+ throw new Error("CONTAINER_LIMITS_REQUIRED");
1660
+ if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
1661
+ throw new Error("CONTAINER_SECURITY_INVALID");
1662
+ if (!application.network?.container || !["bridge", "host"].includes(application.network.container.mode))
1663
+ throw new Error("CONTAINER_NETWORK_INVALID");
1664
+ if (!application.network.ingress?.stablePort)
1665
+ throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
1666
+ return { ...request, root, release };
1667
+ }
1668
+ async function checked3(host, argv2, code) {
1669
+ const result = await host.exec(argv2);
1670
+ if (result.exitCode !== 0)
1671
+ throw new Error(`${code}: ${result.output.slice(0, 512)}`);
1672
+ return result.output.trim();
1673
+ }
1674
+ async function buildContainerImage(requestValue, host = defaultHost2) {
1675
+ const request = validateRequest(requestValue, host);
1676
+ const runtime = request.application.runtime;
1677
+ if (runtime.kind !== "container")
1678
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
1679
+ const source = runtime.image.source;
1680
+ if (source.kind === "registry") {
1681
+ if (!source.reference.includes("@sha256:"))
1682
+ throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
1683
+ await checked3(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
1684
+ const digest2 = await checked3(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
1685
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
1686
+ throw new Error("CONTAINER_DIGEST_INVALID");
1687
+ return { image: source.reference, digest: digest2 };
1688
+ }
1689
+ const context = host.realpath(resolve4(request.release, source.context));
1690
+ const dockerfile = host.realpath(resolve4(request.release, source.dockerfile));
1691
+ if (!within2(request.release, context) || !within2(context, dockerfile))
1692
+ throw new Error("CONTAINER_BUILD_PATH_INVALID");
1693
+ const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
1694
+ await checked3(host, [
1695
+ "/usr/bin/docker",
1696
+ "build",
1697
+ "--pull",
1698
+ ...request.noCache ? ["--no-cache"] : [],
1699
+ "--file",
1700
+ dockerfile,
1701
+ "--tag",
1702
+ tag,
1703
+ "--label",
1704
+ `net.forgezero.revision=${request.revision}`,
1705
+ "--label",
1706
+ `net.forgezero.component=${request.component}`,
1707
+ context
1708
+ ], "CONTAINER_BUILD_FAILED");
1709
+ const digest = await checked3(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
1710
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
1711
+ throw new Error("CONTAINER_DIGEST_INVALID");
1712
+ return { image: tag, digest };
1713
+ }
1714
+ function readState2(host, id) {
1715
+ if (!host.exists(statePath2(id)))
1716
+ return null;
1717
+ try {
1718
+ const value = JSON.parse(host.read(statePath2(id)));
1719
+ return value.format === 1 ? value : null;
1720
+ } catch {
1721
+ return null;
1722
+ }
1723
+ }
1724
+ function usedPorts(host, exceptId) {
1725
+ const ports = new Set;
1726
+ for (const name of host.list(SERVICE_STATE_DIRECTORY).filter((entry) => /^[a-f0-9]{24}\.json$/.test(entry))) {
1727
+ try {
1728
+ const state = JSON.parse(host.read(`${SERVICE_STATE_DIRECTORY}/${name}`));
1729
+ if (state.id !== exceptId) {
1730
+ ports.add(state.publicPort);
1731
+ state.applicationPorts.forEach((port) => ports.add(port));
1732
+ }
1733
+ } catch {}
1734
+ }
1735
+ return ports;
1736
+ }
1737
+ function allocatePorts(host, id, count, stablePort, previous) {
1738
+ const used = usedPorts(host, id);
1739
+ if (used.has(stablePort))
1740
+ throw new Error("CONTAINER_PUBLIC_PORT_CONFLICT");
1741
+ if (previous?.applicationPorts.length === count && previous.applicationPorts.every((port) => !used.has(port)))
1742
+ return [...previous.applicationPorts];
1743
+ const from = 20000;
1744
+ const to = 49999;
1745
+ const width = to - from + 1;
1746
+ const start = Number.parseInt(id.slice(0, 8), 16) % width;
1747
+ for (let offset = 0;offset < width; offset += 1) {
1748
+ const first = from + (start + offset) % width;
1749
+ const values = Array.from({ length: count }, (_, index) => first + index);
1750
+ if (values.at(-1) <= to && values.every((port) => port !== stablePort && !used.has(port)))
1751
+ return values;
1752
+ }
1753
+ throw new Error("CONTAINER_PORTS_EXHAUSTED");
1754
+ }
1755
+ function nginx2(request, id, port) {
1756
+ const service = request.application.service;
1757
+ const stablePort = request.application.network.ingress.stablePort;
1758
+ return `# ForgeZero container ${id}
1759
+ ${service.maximumConnections ? `limit_conn_zone $server_name zone=fz_${id}:64k;
1760
+ ` : ""}server {
1761
+ listen 127.0.0.1:${stablePort};
1762
+ server_name _;
1763
+ ${service.maximumConnections ? ` limit_conn fz_${id} ${service.maximumConnections};
1764
+ limit_conn_status 503;
1765
+ ` : ""} location / {
1766
+ proxy_pass http://127.0.0.1:${port};
1767
+ proxy_http_version 1.1;
1768
+ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
1769
+ proxy_set_header Connection "upgrade";
1770
+ ` : ""} proxy_set_header Host $host;
1771
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1772
+ proxy_set_header X-Forwarded-Proto $scheme;
1773
+ }
1774
+ }
1775
+ `;
1776
+ }
1777
+ function runArgv(request, id, slot, port, networkName) {
1778
+ const { application } = request;
1779
+ const resources = application.resources;
1780
+ const runtime = application.runtime;
1781
+ if (runtime.kind !== "container")
1782
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
1783
+ const memory = resources.memory;
1784
+ const network = application.network.container;
1785
+ const argv2 = [
1786
+ "/usr/bin/docker",
1787
+ "run",
1788
+ "--detach",
1789
+ "--name",
1790
+ nameFor(id, slot),
1791
+ "--restart",
1792
+ "unless-stopped",
1793
+ "--label",
1794
+ `net.forgezero.key=${request.key}`,
1795
+ "--label",
1796
+ `net.forgezero.revision=${request.revision}`,
1797
+ "--cpus",
1798
+ String(resources.cpu.limit),
1799
+ "--cpu-shares",
1800
+ String(resources.cpu.weight ?? 100),
1801
+ "--memory",
1802
+ `${memory.limitMiB}m`,
1803
+ ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
1804
+ ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
1805
+ "--pids-limit",
1806
+ String(resources.pids.limit),
1807
+ "--security-opt",
1808
+ "no-new-privileges=true",
1809
+ "--cap-drop",
1810
+ "ALL",
1811
+ ...runtime.security.root === "read-only" ? ["--read-only"] : [],
1812
+ ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
1813
+ ];
1814
+ for (const mount of application.storage ?? []) {
1815
+ if (mount.class === "ephemeral")
1816
+ argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
1817
+ else
1818
+ argv2.push("--volume", `forgezero-${id}-${mount.name}:${mount.path}`);
1819
+ }
1820
+ argv2.push(request.image, ...runtime.entrypoint ?? []);
1821
+ return argv2;
1822
+ }
1823
+ async function activateContainer(requestValue, host = defaultHost2) {
1824
+ const request = validateRequest(requestValue, host);
1825
+ if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
1826
+ throw new Error("CONTAINER_IMAGE_INVALID");
1827
+ const id = idFor2(request.key);
1828
+ host.mkdir(SERVICE_STATE_DIRECTORY, 448);
1829
+ host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
1830
+ const previous = readState2(host, id);
1831
+ const slots = request.application.rollout.strategy === "blue-green" ? 2 : 1;
1832
+ if (!["direct", "blue-green"].includes(request.application.rollout.strategy))
1833
+ throw new Error("CONTAINER_ROLLOUT_REQUIRES_CONTROL_PLANE");
1834
+ const containerNetwork = request.application.network.container;
1835
+ let networkName = "host";
1836
+ if (containerNetwork.mode === "bridge") {
1837
+ networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
1838
+ if (networkName !== "bridge") {
1839
+ const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
1840
+ if (inspected.exitCode !== 0) {
1841
+ await checked3(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
1842
+ } else if (inspected.output.trim() !== id)
1843
+ throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
1844
+ }
1845
+ }
1846
+ const ports = allocatePorts(host, id, slots, request.application.network.ingress.stablePort, previous);
1847
+ const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
1848
+ const port = ports[nextSlot];
1849
+ const name = nameFor(id, nextSlot);
1850
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1851
+ await checked3(host, runArgv(request, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
1852
+ let healthy = false;
1853
+ const health = request.application.service.health;
1854
+ for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
1855
+ if (await host.health(port, health.path, health.timeoutMs, health.method, health.expectedStatus)) {
1856
+ healthy = true;
1857
+ break;
1858
+ }
1859
+ await host.sleep(health.intervalMs ?? 1000);
1860
+ }
1861
+ if (!healthy) {
1862
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1863
+ throw new Error("CONTAINER_HEALTH_FAILED");
1864
+ }
1865
+ const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
1866
+ const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
1867
+ host.write(nginxPath, nginx2(request, id, port), 420);
1868
+ try {
1869
+ await checked3(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
1870
+ await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
1871
+ } catch (cause) {
1872
+ if (previousNginx === null)
1873
+ host.remove(nginxPath);
1874
+ else
1875
+ host.write(nginxPath, previousNginx, 420);
1876
+ await host.exec(["/usr/sbin/nginx", "-t"]);
1877
+ await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
1878
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1879
+ throw cause;
1880
+ }
1881
+ const state = {
1882
+ format: 1,
1883
+ id,
1884
+ key: request.key,
1885
+ revision: request.revision,
1886
+ strategy: request.application.rollout.strategy,
1887
+ publicPort: request.application.network.ingress.stablePort,
1888
+ applicationPorts: ports,
1889
+ activeSlot: nextSlot,
1890
+ healthPath: health.path,
1891
+ updatedAtTs: host.now()
1892
+ };
1893
+ host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
1894
+ `, 384);
1895
+ if (previous && previous.activeSlot !== nextSlot) {
1896
+ await host.sleep(request.application.rollout.drainMs ?? 30000);
1897
+ await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id, previous.activeSlot)]);
1898
+ }
1899
+ return state;
1900
+ }
1901
+
1579
1902
  // src/software-helper.ts
1580
1903
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
1581
1904
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
@@ -1584,11 +1907,13 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
1584
1907
  var MAX_PENDING_REQUESTS = 128;
1585
1908
  function startSoftwareHelper(options = {}) {
1586
1909
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
1587
- if (existsSync4(socketPath))
1910
+ if (existsSync6(socketPath))
1588
1911
  unlinkSync3(socketPath);
1589
- mkdirSync5(dirname4(socketPath), { recursive: true, mode: 488 });
1912
+ mkdirSync6(dirname5(socketPath), { recursive: true, mode: 488 });
1590
1913
  const ensure = options.ensure ?? ensureSoftwareRequirements;
1591
1914
  const activate = options.activate ?? activateSupervisedService;
1915
+ const buildContainer = options.buildContainer ?? buildContainerImage;
1916
+ const activateTypedContainer = options.activateContainer ?? activateContainer;
1592
1917
  let tail = Promise.resolve();
1593
1918
  let pending = 0;
1594
1919
  const server = createServer2((socket) => {
@@ -1621,10 +1946,29 @@ function startSoftwareHelper(options = {}) {
1621
1946
  return;
1622
1947
  }
1623
1948
  if (request.op === "activate-service" && request.request) {
1624
- if (!options.allowedRoot || request.request.root !== options.allowedRoot) {
1949
+ const serviceRequest = request.request;
1950
+ if (!options.allowedRoot || serviceRequest.root !== options.allowedRoot) {
1625
1951
  throw new Error("service deployment root is not owned by this helper");
1626
1952
  }
1627
- const state = await activate(request.request);
1953
+ const state = await activate(serviceRequest);
1954
+ socket.end(`${JSON.stringify({ ok: true, state })}
1955
+ `);
1956
+ return;
1957
+ }
1958
+ if (request.op === "build-container" && request.request) {
1959
+ const containerRequest = request.request;
1960
+ if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
1961
+ throw new Error("container deployment root is not owned by this helper");
1962
+ const result = await buildContainer(containerRequest);
1963
+ socket.end(`${JSON.stringify({ ok: true, result })}
1964
+ `);
1965
+ return;
1966
+ }
1967
+ if (request.op === "activate-container" && request.request) {
1968
+ const containerRequest = request.request;
1969
+ if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
1970
+ throw new Error("container deployment root is not owned by this helper");
1971
+ const state = await activateTypedContainer(containerRequest);
1628
1972
  socket.end(`${JSON.stringify({ ok: true, state })}
1629
1973
  `);
1630
1974
  return;
@@ -1648,8 +1992,43 @@ function startSoftwareHelper(options = {}) {
1648
1992
  server.listen(socketPath, () => chmodSync4(socketPath, 432));
1649
1993
  return server;
1650
1994
  }
1995
+ function requestContainer(op, request, socketPath, timeoutMs) {
1996
+ return new Promise((resolve5, reject) => {
1997
+ const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
1998
+ `));
1999
+ let buffer = "";
2000
+ socket.setTimeout(timeoutMs, () => {
2001
+ socket.destroy();
2002
+ reject(new Error("container helper timed out"));
2003
+ });
2004
+ socket.on("data", (chunk) => {
2005
+ buffer += chunk.toString("utf8");
2006
+ const newline = buffer.indexOf(`
2007
+ `);
2008
+ if (newline < 0)
2009
+ return;
2010
+ socket.end();
2011
+ try {
2012
+ const response = JSON.parse(buffer.slice(0, newline));
2013
+ const result = response.result ?? response.state;
2014
+ if (!response.ok || !result)
2015
+ throw new Error(response.error?.message ?? "container helper refused request");
2016
+ resolve5(result);
2017
+ } catch (cause) {
2018
+ reject(cause);
2019
+ }
2020
+ });
2021
+ socket.on("error", reject);
2022
+ });
2023
+ }
2024
+ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 30 * 60000) {
2025
+ return requestContainer("build-container", request, socketPath, timeoutMs);
2026
+ }
2027
+ function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
2028
+ return requestContainer("activate-container", request, socketPath, timeoutMs);
2029
+ }
1651
2030
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
1652
- return new Promise((resolve4, reject) => {
2031
+ return new Promise((resolve5, reject) => {
1653
2032
  const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
1654
2033
  `));
1655
2034
  let buffer = "";
@@ -1668,7 +2047,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
1668
2047
  const response = JSON.parse(buffer.slice(0, newline));
1669
2048
  if (!response.ok || !response.state)
1670
2049
  throw new Error(response.error?.message ?? "service helper refused activation");
1671
- resolve4(response.state);
2050
+ resolve5(response.state);
1672
2051
  } catch (cause) {
1673
2052
  reject(cause);
1674
2053
  }
@@ -1678,7 +2057,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
1678
2057
  }
1679
2058
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
1680
2059
  validateSoftwareRequirements(requirements);
1681
- return new Promise((resolve4, reject) => {
2060
+ return new Promise((resolve5, reject) => {
1682
2061
  const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
1683
2062
  `));
1684
2063
  let buffer = "";
@@ -1697,7 +2076,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
1697
2076
  const response = JSON.parse(buffer.slice(0, newline));
1698
2077
  if (!response.ok || !response.results)
1699
2078
  throw new Error(response.error?.message ?? "software helper refused the request");
1700
- resolve4(response.results);
2079
+ resolve5(response.results);
1701
2080
  } catch (cause) {
1702
2081
  reject(cause);
1703
2082
  }
@@ -1707,10 +2086,10 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
1707
2086
  }
1708
2087
 
1709
2088
  // src/version.ts
1710
- var VERSION3 = "0.1.53";
2089
+ var VERSION3 = "0.1.56";
1711
2090
 
1712
2091
  // src/egress-policy.ts
1713
- import { realpathSync as realpathSync2 } from "node:fs";
2092
+ import { realpathSync as realpathSync3 } from "node:fs";
1714
2093
  var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
1715
2094
  var BLOCKED_IPV4 = [
1716
2095
  "0.0.0.0/8",
@@ -2686,17 +3065,17 @@ import {
2686
3065
  chmodSync as chmodSync5,
2687
3066
  chownSync,
2688
3067
  copyFileSync as copyFileSync2,
2689
- existsSync as existsSync5,
3068
+ existsSync as existsSync7,
2690
3069
  lstatSync,
2691
- mkdirSync as mkdirSync6,
2692
- readFileSync as readFileSync5,
2693
- readdirSync as readdirSync2,
2694
- realpathSync as realpathSync3,
2695
- renameSync as renameSync5,
2696
- rmSync as rmSync5,
3070
+ mkdirSync as mkdirSync7,
3071
+ readFileSync as readFileSync6,
3072
+ readdirSync as readdirSync3,
3073
+ realpathSync as realpathSync4,
3074
+ renameSync as renameSync6,
3075
+ rmSync as rmSync6,
2697
3076
  statSync,
2698
3077
  symlinkSync as symlinkSync3,
2699
- writeFileSync as writeFileSync5
3078
+ writeFileSync as writeFileSync6
2700
3079
  } from "node:fs";
2701
3080
  import { join as join5 } from "node:path";
2702
3081
  var safeAtom = (name, value) => {
@@ -3058,13 +3437,13 @@ var platformExec = async (argv2) => {
3058
3437
  };
3059
3438
  var replaceLink = (path2, target) => {
3060
3439
  const pending = `${path2}.next`;
3061
- rmSync5(pending, { force: true });
3440
+ rmSync6(pending, { force: true });
3062
3441
  if (!target) {
3063
- rmSync5(path2, { force: true });
3442
+ rmSync6(path2, { force: true });
3064
3443
  return;
3065
3444
  }
3066
3445
  symlinkSync3(target, pending);
3067
- renameSync5(pending, path2);
3446
+ renameSync6(pending, path2);
3068
3447
  };
3069
3448
  var secureRelease = (path2, uid, gid) => {
3070
3449
  const visit = (current) => {
@@ -3074,7 +3453,7 @@ var secureRelease = (path2, uid, gid) => {
3074
3453
  chownSync(current, uid, gid);
3075
3454
  chmodSync5(current, metadata.isDirectory() ? 365 : 292);
3076
3455
  if (metadata.isDirectory())
3077
- for (const name of readdirSync2(current))
3456
+ for (const name of readdirSync3(current))
3078
3457
  visit(join5(current, name));
3079
3458
  };
3080
3459
  visit(path2);
@@ -3082,11 +3461,11 @@ var secureRelease = (path2, uid, gid) => {
3082
3461
  async function activatePlatformRelease(config, requestedRelease, options = {}) {
3083
3462
  const rendered = renderPlatformActivationFiles(config);
3084
3463
  const normalized = JSON.parse(rendered.helper);
3085
- const releases = realpathSync3(join5(normalized.root, "releases"));
3086
- const release = realpathSync3(requestedRelease);
3464
+ const releases = realpathSync4(join5(normalized.root, "releases"));
3465
+ const release = realpathSync4(requestedRelease);
3087
3466
  if (!release.startsWith(`${releases}/`) || release === releases)
3088
3467
  throw new Error("release is outside configured releases directory");
3089
- for (const required of [".fz/deploy.json", "src/index.ts", "bun.lock"]) {
3468
+ for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
3090
3469
  const metadata = statSync(join5(release, required));
3091
3470
  if (!metadata.isFile() || metadata.size < 1)
3092
3471
  throw new Error(`release is incomplete: ${required}`);
@@ -3095,15 +3474,15 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
3095
3474
  const request = options.fetch ?? fetch;
3096
3475
  const sleep = options.sleep ?? Bun.sleep;
3097
3476
  const slots = join5(normalized.root, "slots");
3098
- mkdirSync6(slots, { recursive: true, mode: 493 });
3477
+ mkdirSync7(slots, { recursive: true, mode: 493 });
3099
3478
  const slotFile = join5(normalized.root, ".forge-slot");
3100
- const previousSlot = existsSync5(slotFile) ? readFileSync5(slotFile, "utf8").trim() : undefined;
3479
+ const previousSlot = existsSync7(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
3101
3480
  const target = previousSlot === "blue" ? "green" : "blue";
3102
3481
  const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
3103
3482
  const targetLink = join5(slots, target);
3104
3483
  let previousTarget;
3105
3484
  try {
3106
- previousTarget = realpathSync3(targetLink);
3485
+ previousTarget = realpathSync4(targetLink);
3107
3486
  } catch {}
3108
3487
  const user = await exec(["/usr/bin/id", "-u", normalized.serviceUser]);
3109
3488
  const group = await exec(["/usr/bin/id", "-g", normalized.serviceUser]);
@@ -3137,35 +3516,35 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
3137
3516
  }
3138
3517
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
3139
3518
  const backup = `${upstream}.forgezero-backup`;
3140
- if (existsSync5(upstream))
3519
+ if (existsSync7(upstream))
3141
3520
  copyFileSync2(upstream, backup);
3142
3521
  else
3143
- rmSync5(backup, { force: true });
3144
- writeFileSync5(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
3522
+ rmSync6(backup, { force: true });
3523
+ writeFileSync6(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
3145
3524
  `, { mode: 420 });
3146
3525
  const test = await exec(["/usr/sbin/nginx", "-t"]);
3147
3526
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
3148
3527
  if (reload.exitCode !== 0) {
3149
- if (existsSync5(backup))
3150
- renameSync5(backup, upstream);
3528
+ if (existsSync7(backup))
3529
+ renameSync6(backup, upstream);
3151
3530
  else
3152
- rmSync5(upstream, { force: true });
3531
+ rmSync6(upstream, { force: true });
3153
3532
  await exec(["/usr/sbin/nginx", "-t"]);
3154
3533
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
3155
3534
  await stopTarget();
3156
3535
  throw new Error("nginx refused the promoted upstream");
3157
3536
  }
3158
- rmSync5(backup, { force: true });
3159
- writeFileSync5(slotFile, `${target}
3537
+ rmSync6(backup, { force: true });
3538
+ writeFileSync6(slotFile, `${target}
3160
3539
  `, { mode: 420 });
3161
3540
  if (previousSlot && previousSlot !== target) {
3162
3541
  await sleep(normalized.drainDeadlineMs);
3163
3542
  await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
3164
3543
  }
3165
- const old = readdirSync2(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs).slice(normalized.keepReleases);
3544
+ const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs).slice(normalized.keepReleases);
3166
3545
  for (const path2 of old)
3167
3546
  if (path2 !== release)
3168
- rmSync5(path2, { recursive: true, force: true });
3547
+ rmSync6(path2, { recursive: true, force: true });
3169
3548
  return { release, slot: target };
3170
3549
  }
3171
3550
  function validateCollectorUnit(unit2) {
@@ -3209,18 +3588,18 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
3209
3588
  import { lstatSync as lstatSync4 } from "node:fs";
3210
3589
 
3211
3590
  // src/bootstrap.ts
3212
- import { createHash as createHash4, createHmac, randomBytes as randomBytes3 } from "node:crypto";
3591
+ import { createHash as createHash5, createHmac, randomBytes as randomBytes3 } from "node:crypto";
3213
3592
  import {
3214
3593
  chmodSync as chmodSync7,
3215
- existsSync as existsSync7,
3594
+ existsSync as existsSync9,
3216
3595
  lstatSync as lstatSync3,
3217
- mkdirSync as mkdirSync8,
3218
- readFileSync as readFileSync7,
3219
- renameSync as renameSync7,
3220
- rmSync as rmSync7,
3221
- writeFileSync as writeFileSync7
3596
+ mkdirSync as mkdirSync9,
3597
+ readFileSync as readFileSync8,
3598
+ renameSync as renameSync8,
3599
+ rmSync as rmSync8,
3600
+ writeFileSync as writeFileSync8
3222
3601
  } from "node:fs";
3223
- import { dirname as dirname7 } from "node:path";
3602
+ import { dirname as dirname8 } from "node:path";
3224
3603
  import { fileURLToPath } from "node:url";
3225
3604
 
3226
3605
  // src/cli/agent-install.ts
@@ -3228,17 +3607,17 @@ import { randomBytes } from "node:crypto";
3228
3607
  import {
3229
3608
  chmodSync as chmodSync6,
3230
3609
  copyFileSync as copyFileSync3,
3231
- existsSync as existsSync6,
3610
+ existsSync as existsSync8,
3232
3611
  lstatSync as lstatSync2,
3233
- mkdirSync as mkdirSync7,
3234
- readFileSync as readFileSync6,
3235
- realpathSync as realpathSync4,
3236
- renameSync as renameSync6,
3237
- rmSync as rmSync6,
3612
+ mkdirSync as mkdirSync8,
3613
+ readFileSync as readFileSync7,
3614
+ realpathSync as realpathSync5,
3615
+ renameSync as renameSync7,
3616
+ rmSync as rmSync7,
3238
3617
  symlinkSync as symlinkSync4,
3239
- writeFileSync as writeFileSync6
3618
+ writeFileSync as writeFileSync7
3240
3619
  } from "node:fs";
3241
- import { dirname as dirname5 } from "node:path";
3620
+ import { dirname as dirname6 } from "node:path";
3242
3621
  async function readCapabilities(run2) {
3243
3622
  const answers = {};
3244
3623
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -3297,28 +3676,28 @@ var runProvisionOperation = async (operation) => {
3297
3676
  }
3298
3677
  if (operation.kind === "install-runtime") {
3299
3678
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
3300
- mkdirSync7(`${release}/dist`, { recursive: true, mode: 493 });
3301
- mkdirSync7(dirname5(operation.binary), { recursive: true, mode: 493 });
3679
+ mkdirSync8(`${release}/dist`, { recursive: true, mode: 493 });
3680
+ mkdirSync8(dirname6(operation.binary), { recursive: true, mode: 493 });
3302
3681
  copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
3303
3682
  chmodSync6(`${release}/dist/fz-agent.js`, 493);
3304
- const gitSshSource = `${dirname5(operation.source)}/fz-git-ssh.js`;
3305
- if (!existsSync6(gitSshSource))
3683
+ const gitSshSource = `${dirname6(operation.source)}/fz-git-ssh.js`;
3684
+ if (!existsSync8(gitSshSource))
3306
3685
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
3307
3686
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
3308
3687
  chmodSync6(`${release}/dist/fz-git-ssh.js`, 493);
3309
3688
  const pending = "/opt/forgezero/agent/current.next";
3310
- rmSync6(pending, { force: true });
3689
+ rmSync7(pending, { force: true });
3311
3690
  symlinkSync4(`versions/${operation.version}`, pending);
3312
- renameSync6(pending, "/opt/forgezero/agent/current");
3313
- rmSync6(operation.binary, { force: true });
3691
+ renameSync7(pending, "/opt/forgezero/agent/current");
3692
+ rmSync7(operation.binary, { force: true });
3314
3693
  symlinkSync4("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
3315
3694
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
3316
- rmSync6(gitSshBinary, { force: true });
3695
+ rmSync7(gitSshBinary, { force: true });
3317
3696
  symlinkSync4("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
3318
3697
  return { stdout: "", exitCode: 0 };
3319
3698
  }
3320
3699
  if (operation.kind === "ensure-seed") {
3321
- if (existsSync6(operation.credential) && lstatSync2(operation.credential).size > 0)
3700
+ if (existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
3322
3701
  return { stdout: "", exitCode: 0 };
3323
3702
  const seed = randomBytes(32).toString("base64url");
3324
3703
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
@@ -3330,9 +3709,9 @@ var runProvisionOperation = async (operation) => {
3330
3709
  const key = "/run/forgezero-git-deploy-key";
3331
3710
  const publicKey = `${key}.pub`;
3332
3711
  try {
3333
- if (!existsSync6(operation.credential) || lstatSync2(operation.credential).size < 1) {
3334
- rmSync6(key, { force: true });
3335
- rmSync6(publicKey, { force: true });
3712
+ if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
3713
+ rmSync7(key, { force: true });
3714
+ rmSync7(publicKey, { force: true });
3336
3715
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
3337
3716
  if (result.exitCode !== 0)
3338
3717
  return result;
@@ -3341,8 +3720,8 @@ var runProvisionOperation = async (operation) => {
3341
3720
  return result;
3342
3721
  chmodSync6(operation.credential, 256);
3343
3722
  }
3344
- if (!existsSync6(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
3345
- if (!existsSync6(key)) {
3723
+ if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
3724
+ if (!existsSync8(key)) {
3346
3725
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
3347
3726
  if (decrypted.exitCode !== 0)
3348
3727
  return decrypted;
@@ -3350,25 +3729,25 @@ var runProvisionOperation = async (operation) => {
3350
3729
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
3351
3730
  if (derived.exitCode !== 0)
3352
3731
  return derived;
3353
- writeFileSync6(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
3732
+ writeFileSync7(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
3354
3733
  `, { mode: 292 });
3355
3734
  }
3356
3735
  return { stdout: "", exitCode: 0 };
3357
3736
  } finally {
3358
- rmSync6(key, { force: true });
3359
- rmSync6(publicKey, { force: true });
3737
+ rmSync7(key, { force: true });
3738
+ rmSync7(publicKey, { force: true });
3360
3739
  }
3361
3740
  }
3362
3741
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
3363
3742
  const key = "/run/forgezero-bootstrap-ssh-key";
3364
3743
  const generatedPublicKey = `${key}.pub`;
3365
3744
  try {
3366
- if (!existsSync6(operation.credential) || lstatSync2(operation.credential).size < 1) {
3367
- rmSync6(key, { force: true });
3368
- rmSync6(generatedPublicKey, { force: true });
3745
+ if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
3746
+ rmSync7(key, { force: true });
3747
+ rmSync7(generatedPublicKey, { force: true });
3369
3748
  let result;
3370
3749
  if (operation.source) {
3371
- const source = existsSync6(operation.source) ? lstatSync2(operation.source) : undefined;
3750
+ const source = existsSync8(operation.source) ? lstatSync2(operation.source) : undefined;
3372
3751
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
3373
3752
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
3374
3753
  }
@@ -3388,8 +3767,8 @@ var runProvisionOperation = async (operation) => {
3388
3767
  return result;
3389
3768
  chmodSync6(operation.credential, 256);
3390
3769
  }
3391
- if (!existsSync6(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
3392
- if (!existsSync6(key)) {
3770
+ if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
3771
+ if (!existsSync8(key)) {
3393
3772
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
3394
3773
  if (decrypted.exitCode !== 0)
3395
3774
  return decrypted;
@@ -3399,28 +3778,28 @@ var runProvisionOperation = async (operation) => {
3399
3778
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
3400
3779
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
3401
3780
  }
3402
- mkdirSync7(dirname5(operation.publicKey), { recursive: true, mode: 493 });
3403
- writeFileSync6(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
3781
+ mkdirSync8(dirname6(operation.publicKey), { recursive: true, mode: 493 });
3782
+ writeFileSync7(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
3404
3783
  `, { mode: 292 });
3405
3784
  chmodSync6(operation.publicKey, 292);
3406
3785
  }
3407
3786
  if (operation.source)
3408
- rmSync6(operation.source, { force: true });
3787
+ rmSync7(operation.source, { force: true });
3409
3788
  return { stdout: "", exitCode: 0 };
3410
3789
  } finally {
3411
- rmSync6(key, { force: true });
3412
- rmSync6(generatedPublicKey, { force: true });
3790
+ rmSync7(key, { force: true });
3791
+ rmSync7(generatedPublicKey, { force: true });
3413
3792
  }
3414
3793
  }
3415
3794
  if (operation.kind === "ensure-enrolment") {
3416
- if (existsSync6(operation.state) && lstatSync2(operation.state).size > 0 || existsSync6(operation.credential) && lstatSync2(operation.credential).size > 0)
3795
+ if (existsSync8(operation.state) && lstatSync2(operation.state).size > 0 || existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
3417
3796
  return { stdout: "", exitCode: 0 };
3418
- if (!existsSync6(operation.source))
3797
+ if (!existsSync8(operation.source))
3419
3798
  return { stdout: "enrolment source is missing", exitCode: 1 };
3420
3799
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
3421
3800
  if (result.exitCode === 0) {
3422
3801
  chmodSync6(operation.credential, 256);
3423
- rmSync6(operation.source, { force: true });
3802
+ rmSync7(operation.source, { force: true });
3424
3803
  }
3425
3804
  return result;
3426
3805
  }
@@ -3435,7 +3814,7 @@ var runProvisionOperation = async (operation) => {
3435
3814
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
3436
3815
  }
3437
3816
  if (operation.kind === "verify-file")
3438
- return existsSync6(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
3817
+ return existsSync8(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
3439
3818
  if (operation.kind === "verify-egress") {
3440
3819
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
3441
3820
  if (active.exitCode !== 0)
@@ -3451,31 +3830,31 @@ var runProvisionOperation = async (operation) => {
3451
3830
  if (operation.kind === "verify-resolved-stub") {
3452
3831
  try {
3453
3832
  const expected = "/run/systemd/resolve/stub-resolv.conf";
3454
- return realpathSync4("/etc/resolv.conf") === expected && realpathSync4(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
3833
+ return realpathSync5("/etc/resolv.conf") === expected && realpathSync5(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
3455
3834
  } catch {
3456
3835
  return { stdout: "resolver stub missing", exitCode: 1 };
3457
3836
  }
3458
3837
  }
3459
3838
  if (operation.kind === "install-warp") {
3460
- const os = readFileSync6("/etc/os-release", "utf8");
3839
+ const os = readFileSync7("/etc/os-release", "utf8");
3461
3840
  if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
3462
3841
  return { stdout: "unsupported WARP host OS", exitCode: 1 };
3463
3842
  const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
3464
3843
  if (!response.ok)
3465
3844
  return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
3466
- mkdirSync7("/usr/share/keyrings", { recursive: true, mode: 493 });
3467
- mkdirSync7("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
3468
- mkdirSync7("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
3845
+ mkdirSync8("/usr/share/keyrings", { recursive: true, mode: 493 });
3846
+ mkdirSync8("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
3847
+ mkdirSync8("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
3469
3848
  const key = "/run/cloudflare-warp-key.gpg";
3470
- writeFileSync6(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
3849
+ writeFileSync7(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
3471
3850
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
3472
- rmSync6(key, { force: true });
3851
+ rmSync7(key, { force: true });
3473
3852
  if (result.exitCode !== 0)
3474
3853
  return result;
3475
3854
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
3476
3855
  if (!codename)
3477
3856
  return { stdout: "Ubuntu codename missing", exitCode: 1 };
3478
- writeFileSync6("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
3857
+ writeFileSync7("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
3479
3858
  `, { mode: 420 });
3480
3859
  result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
3481
3860
  return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
@@ -3520,7 +3899,7 @@ async function applyPlan(plan, run2) {
3520
3899
  import { constants } from "node:fs";
3521
3900
  import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
3522
3901
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
3523
- import { dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
3902
+ import { dirname as dirname7, join as join6, resolve as resolve5 } from "node:path";
3524
3903
  import { isIP as isIP3 } from "node:net";
3525
3904
 
3526
3905
  // src/cloudflare-edge.ts
@@ -3692,9 +4071,9 @@ async function bootstrapStatus(host = localBootstrapHost()) {
3692
4071
  if (health.exitCode !== 0)
3693
4072
  problems.push("platform API health check failed");
3694
4073
  }
3695
- const nginx2 = await host.exec(["nginx", "-t"]);
3696
- services["nginx-config"] = nginx2.exitCode === 0;
3697
- if (nginx2.exitCode !== 0)
4074
+ const nginx3 = await host.exec(["nginx", "-t"]);
4075
+ services["nginx-config"] = nginx3.exitCode === 0;
4076
+ if (nginx3.exitCode !== 0)
3698
4077
  problems.push("nginx configuration is invalid");
3699
4078
  if (state.databaseRole !== "none") {
3700
4079
  const unitPath = "/etc/systemd/system/forgezero-db.service";
@@ -3739,17 +4118,17 @@ function localBootstrapHost() {
3739
4118
  };
3740
4119
  return {
3741
4120
  uid: () => process.getuid?.() ?? -1,
3742
- exists: existsSync7,
3743
- read: (path2) => readFileSync7(path2, "utf8"),
4121
+ exists: existsSync9,
4122
+ read: (path2) => readFileSync8(path2, "utf8"),
3744
4123
  write(path2, content, mode) {
3745
- mkdirSync8(dirname7(path2), { recursive: true, mode: 493 });
4124
+ mkdirSync9(dirname8(path2), { recursive: true, mode: 493 });
3746
4125
  const temporary = `${path2}.next.${process.pid}`;
3747
- writeFileSync7(temporary, content, { mode });
4126
+ writeFileSync8(temporary, content, { mode });
3748
4127
  chmodSync7(temporary, mode);
3749
- renameSync7(temporary, path2);
4128
+ renameSync8(temporary, path2);
3750
4129
  },
3751
- mkdir: (path2, mode) => mkdirSync8(path2, { recursive: true, mode }),
3752
- remove: (path2) => rmSync7(path2, { force: true }),
4130
+ mkdir: (path2, mode) => mkdirSync9(path2, { recursive: true, mode }),
4131
+ remove: (path2) => rmSync8(path2, { force: true }),
3753
4132
  inspect(path2) {
3754
4133
  const value = lstatSync3(path2);
3755
4134
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
@@ -3773,7 +4152,7 @@ function localBootstrapHost() {
3773
4152
  async installAgent(config, enrolTokenSourcePath) {
3774
4153
  const capabilities = await readCapabilities(localRunner);
3775
4154
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
3776
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync7("/var/lib/forgezero/enrolment.json");
4155
+ const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync9("/var/lib/forgezero/enrolment.json");
3777
4156
  if (config.kind === "platform") {
3778
4157
  const lifecycle = config.database.role === "none" ? {
3779
4158
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -3785,8 +4164,8 @@ function localBootstrapHost() {
3785
4164
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
3786
4165
  databasePorts: [8529]
3787
4166
  };
3788
- mkdirSync8(dirname7(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
3789
- writeFileSync7(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
4167
+ mkdirSync9(dirname8(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
4168
+ writeFileSync8(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
3790
4169
  `, { mode: 256 });
3791
4170
  }
3792
4171
  const plan = planInstall({
@@ -3827,8 +4206,8 @@ function localBootstrapHost() {
3827
4206
  } : {}
3828
4207
  });
3829
4208
  for (const unit2 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
3830
- mkdirSync8(dirname7(unit2.path), { recursive: true, mode: 493 });
3831
- writeFileSync7(unit2.path, unit2.unit, { mode: 420 });
4209
+ mkdirSync9(dirname8(unit2.path), { recursive: true, mode: 493 });
4210
+ writeFileSync8(unit2.path, unit2.unit, { mode: 420 });
3832
4211
  }
3833
4212
  await applyPlan(plan, localRunner);
3834
4213
  return plan;