@forgezero/agent 0.1.88 → 0.1.90

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 (40) hide show
  1. package/README.md +23 -9
  2. package/dist/agent-heartbeat.d.ts +3 -0
  3. package/dist/agent-heartbeat.js +2 -1
  4. package/dist/bootstrap.d.ts +8 -1
  5. package/dist/bootstrap.js +241 -62
  6. package/dist/capacity-calibration.d.ts +35 -5
  7. package/dist/capacity-calibration.js +66 -22
  8. package/dist/cli/agent-install.d.ts +3 -0
  9. package/dist/community-rehearsal-host.js +7 -2
  10. package/dist/control.d.ts +3 -1
  11. package/dist/definition.js +83 -28
  12. package/dist/deploy-file.js +83 -28
  13. package/dist/deployment.d.ts +5 -0
  14. package/dist/egress-policy.d.ts +4 -0
  15. package/dist/fz-agent.js +450 -158
  16. package/dist/fz.js +429 -138
  17. package/dist/host-maintenance.js +1 -1
  18. package/dist/index.d.ts +7 -0
  19. package/dist/metal-bootstrap.js +1 -1
  20. package/dist/metal-helper-socket.js +7 -2
  21. package/dist/metal-provision.js +7 -2
  22. package/dist/migration-pull.d.ts +1 -0
  23. package/dist/migration-pull.js +4 -0
  24. package/dist/operator-bootstrap.d.ts +12 -0
  25. package/dist/operator-bootstrap.js +385 -72
  26. package/dist/platform-bootstrap-runtime.d.ts +1 -0
  27. package/dist/platform-bootstrap-runtime.js +47 -8
  28. package/dist/platform-fleet-verification.js +273 -95
  29. package/dist/platform-genesis.js +7 -2
  30. package/dist/platform-launch-profile.d.ts +1 -0
  31. package/dist/provision.d.ts +5 -1
  32. package/dist/provision.js +192 -75
  33. package/dist/recovery-host.js +1 -1
  34. package/dist/software-helper.js +144 -48
  35. package/dist/software.js +7 -2
  36. package/dist/ssh-bootstrap.d.ts +1 -0
  37. package/dist/ubuntu.js +7 -2
  38. package/dist/version.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/schema/deploy-v3.json +5 -2
package/dist/bootstrap.js CHANGED
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1420
1420
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1421
1421
 
1422
1422
  // src/version.ts
1423
- var VERSION = "0.1.88";
1423
+ var VERSION = "0.1.90";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1698,6 +1698,7 @@ function agentEgressUnit(options) {
1698
1698
  }
1699
1699
  const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
1700
1700
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1701
+ const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
1701
1702
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1702
1703
  if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
1703
1704
  throw new Error("deployed project runner needs at least one vetted public TCP port");
@@ -1705,8 +1706,10 @@ function agentEgressUnit(options) {
1705
1706
  if (deploymentEnabled)
1706
1707
  systemdAgentEgressDirectives(runnerLoopbackPorts);
1707
1708
  const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
1708
- const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
1709
- const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}`;
1709
+ const portGrantEnabled = deploymentEnabled || agentLoopbackPorts.length > 0;
1710
+ const restrictedUser = deploymentEnabled ? DEPLOYMENT_RUNNER_USER : user;
1711
+ const portGrant = portGrantEnabled ? ` --loopback-user=${restrictedUser}` + (deploymentEnabled ? runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") : "") + agentLoopbackPorts.map((port) => ` --shared-loopback-tcp-port=${port}`).join("") + (deploymentEnabled ? runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "") : "";
1712
+ const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}`;
1710
1713
  return `[Unit]
1711
1714
  Description=ForgeZero Agent host egress policy
1712
1715
  Documentation=https://www.forgezero.net/docs/agent
@@ -1719,7 +1722,7 @@ Type=notify
1719
1722
  NotifyAccess=all
1720
1723
  User=root
1721
1724
  Group=root
1722
- ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
1725
+ ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}
1723
1726
  ${policyProof}
1724
1727
  Restart=on-failure
1725
1728
  RestartSec=2
@@ -2003,7 +2006,7 @@ function agentEnrolmentUnit(options) {
2003
2006
  Requires=forgezero-agent-egress.service
2004
2007
  BindsTo=forgezero-agent-egress.service
2005
2008
  ` : "";
2006
- const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
2009
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
2007
2010
  return `[Unit]
2008
2011
  Description=Bind this machine to its ForgeZero compute
2009
2012
  After=network-online.target
@@ -2040,7 +2043,7 @@ WantedBy=multi-user.target
2040
2043
  function deploymentRunnerUnit(options) {
2041
2044
  const bin = options.binPath ?? "fz-agent";
2042
2045
  const root = options.deployRoot ?? "/opt/forgezero";
2043
- const agentUser = options.user ?? "forgezero-agent";
2046
+ const agentUser = options.user ?? "forgezero";
2044
2047
  const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
2045
2048
  Requires=forgezero-agent-egress.service
2046
2049
  BindsTo=forgezero-agent-egress.service
@@ -2081,7 +2084,7 @@ RestrictRealtime=true
2081
2084
  MemoryDenyWriteExecute=true
2082
2085
  LockPersonality=true
2083
2086
  ${egressDirectives}
2084
- ReadWritePaths=${root}/releases ${root}/runner-home
2087
+ ReadWritePaths=${root}/releases ${root}/runner-home ${root}/slots /etc/nginx/conf.d /var/log/nginx
2085
2088
 
2086
2089
  [Install]
2087
2090
  WantedBy=multi-user.target
@@ -2091,7 +2094,7 @@ function agentUnit(options) {
2091
2094
  if (!validNodeHostname(options.nodeHostname))
2092
2095
  throw new Error("node hostname is invalid");
2093
2096
  if (!options.telemetryEndpoint) {
2094
- throw new Error("compute Agent provisioning requires OTEL_EXPORTER_OTLP_ENDPOINT as an explicit public HTTPS collector coordinate");
2097
+ throw new Error("compute Agent provisioning requires an explicit supervised OTLP collector coordinate");
2095
2098
  }
2096
2099
  let telemetryEndpoint;
2097
2100
  {
@@ -2099,10 +2102,13 @@ function agentUnit(options) {
2099
2102
  try {
2100
2103
  endpoint2 = new URL(options.telemetryEndpoint);
2101
2104
  } catch {
2102
- throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL");
2105
+ throw new Error("compute telemetry endpoint must be an absolute collector URL");
2106
+ }
2107
+ const localCollector = endpoint2.protocol === "http:" && endpoint2.hostname === "127.0.0.1" && endpoint2.port === "4318" && endpoint2.pathname === "/";
2108
+ const publicCollector = endpoint2.protocol === "https:" && !endpoint2.username && !endpoint2.password && !endpoint2.search && !endpoint2.hash && isIP3(endpoint2.hostname) === 0 && endpoint2.hostname.includes(".") && endpoint2.hostname !== "localhost" && !endpoint2.hostname.endsWith(".local");
2109
+ if (!localCollector && !publicCollector || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash) {
2110
+ throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
2103
2111
  }
2104
- if (endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash || isIP3(endpoint2.hostname) !== 0 || !endpoint2.hostname.includes(".") || endpoint2.hostname === "localhost" || endpoint2.hostname.endsWith(".local"))
2105
- throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
2106
2112
  telemetryEndpoint = endpoint2.toString().replace(/\/$/, "");
2107
2113
  }
2108
2114
  const bin = options.binPath ?? "fz-agent";
@@ -2216,7 +2222,7 @@ function agentUnit(options) {
2216
2222
  const supplementaryGroups = [
2217
2223
  AGENT_UPDATE_GROUP,
2218
2224
  deploymentEnabled ? DEPLOYMENT_GROUP : null,
2219
- deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
2225
+ SOFTWARE_HELPER_GROUP,
2220
2226
  lifecycleEnabled ? LIFECYCLE_GROUP : null
2221
2227
  ].filter((value) => value !== null);
2222
2228
  const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
@@ -2225,7 +2231,7 @@ function agentUnit(options) {
2225
2231
  "forgezero-agent-update-helper.service",
2226
2232
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
2227
2233
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
2228
- deploymentEnabled ? "forgezero-software-helper.service" : null,
2234
+ "forgezero-software-helper.service",
2229
2235
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
2230
2236
  warpEnabled ? "warp-svc.service" : null,
2231
2237
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -2234,7 +2240,7 @@ function agentUnit(options) {
2234
2240
  "forgezero-agent-update-helper.service",
2235
2241
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
2236
2242
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
2237
- deploymentEnabled ? "forgezero-software-helper.service" : null,
2243
+ "forgezero-software-helper.service",
2238
2244
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
2239
2245
  warpEnabled ? "warp-svc.service" : null,
2240
2246
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -2249,9 +2255,9 @@ function agentUnit(options) {
2249
2255
  const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
2250
2256
  DeviceAllow=/dev/sev-guest rw` : "";
2251
2257
  const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
2252
- ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
2258
+ ExecStartPre=+/bin/chmod 0660 /dev/sev-guest
2253
2259
  ` : "";
2254
- const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
2260
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
2255
2261
  return `[Unit]
2256
2262
  Description=ForgeZero node agent (${options.mode})
2257
2263
  Documentation=https://www.forgezero.net/docs/agent
@@ -2382,6 +2388,7 @@ function planProvision(options) {
2382
2388
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
2383
2389
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
2384
2390
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
2391
+ const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
2385
2392
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
2386
2393
  const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
2387
2394
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
@@ -2430,8 +2437,9 @@ function planProvision(options) {
2430
2437
  const enabledUnits = [
2431
2438
  "forgezero-agent.socket",
2432
2439
  "forgezero-agent-update-helper.service",
2440
+ "forgezero-software-helper.service",
2433
2441
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2434
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2442
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
2435
2443
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2436
2444
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2437
2445
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
@@ -2439,8 +2447,9 @@ function planProvision(options) {
2439
2447
  ];
2440
2448
  const restartedUnits = [
2441
2449
  "forgezero-agent-update-helper.service",
2450
+ "forgezero-software-helper.service",
2442
2451
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2443
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2452
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
2444
2453
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2445
2454
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2446
2455
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
@@ -2464,9 +2473,9 @@ function planProvision(options) {
2464
2473
  ...options.enforceEgress ? [
2465
2474
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
2466
2475
  ] : [],
2476
+ { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) },
2467
2477
  ...deploymentEnabled ? [
2468
- { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
2469
- { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
2478
+ { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
2470
2479
  ] : [],
2471
2480
  ...enrolmentEnabled ? [
2472
2481
  { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
@@ -2499,24 +2508,29 @@ function planProvision(options) {
2499
2508
  step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
2500
2509
  ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION })] : [],
2501
2510
  ...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
2502
- ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
2503
- { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
2511
+ step("software strategy helper group", { kind: "commands", commands: [
2504
2512
  { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
2513
+ ] }),
2514
+ ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
2515
+ { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] }
2505
2516
  ] })] : [],
2506
2517
  ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
2507
2518
  step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
2508
2519
  step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
2509
2520
  step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
2521
+ step("grant software helper socket access", { kind: "commands", commands: [
2522
+ { argv: ["/usr/sbin/usermod", "-a", "-G", SOFTWARE_HELPER_GROUP, user] }
2523
+ ] }),
2510
2524
  ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
2511
2525
  ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
2512
2526
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
2513
2527
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
2514
2528
  { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
2515
- { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
2529
+ { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, user] }
2516
2530
  ] })] : [],
2517
2531
  step("credential and state directories", { kind: "directories", directories: [
2518
2532
  { path: credentialDir, mode: 448, owner: "root", group: "root" },
2519
- { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
2533
+ { path: "/var/lib/forgezero", mode: 488, owner: "root", group: VAULT_GROUP }
2520
2534
  ] }),
2521
2535
  step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
2522
2536
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
@@ -2567,19 +2581,26 @@ function planProvision(options) {
2567
2581
  { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
2568
2582
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2569
2583
  ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
2584
+ { argv: ["/usr/bin/systemctl", "stop", "forgezero-agent-proxy.service"], acceptedExitCodes: [0, 1, 5] },
2570
2585
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
2571
2586
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2572
2587
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
2573
2588
  ] }),
2574
2589
  ...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
2575
- ...options.enforceEgress ? [step("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
2590
+ ...options.enforceEgress ? [step("prove the Agent egress policy is active", {
2591
+ kind: "verify-egress",
2592
+ deploymentEnabled,
2593
+ runnerPublicTcpPorts,
2594
+ runnerLoopbackPorts,
2595
+ agentLoopbackPorts
2596
+ })] : [],
2576
2597
  step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
2577
2598
  step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
2578
2599
  step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
2579
2600
  step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
2601
+ step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 }),
2580
2602
  ...deploymentEnabled ? [
2581
- step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
2582
- step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
2603
+ step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
2583
2604
  ] : [],
2584
2605
  ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
2585
2606
  ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
@@ -2604,6 +2625,25 @@ import {
2604
2625
  writeFileSync as writeFileSync4
2605
2626
  } from "fs";
2606
2627
  import { dirname as dirname5 } from "path";
2628
+ function normalizeEd25519PublicKey(output) {
2629
+ const line = output.trim();
2630
+ if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}(?: [^\r\n]{1,256})?$/.test(line))
2631
+ return;
2632
+ const [algorithm, encoded] = line.split(" ", 3);
2633
+ if (algorithm !== "ssh-ed25519" || !encoded)
2634
+ return;
2635
+ let blob;
2636
+ try {
2637
+ blob = Buffer.from(encoded, "base64");
2638
+ } catch {
2639
+ return;
2640
+ }
2641
+ if (blob.length !== 51 || blob.readUInt32BE(0) !== 11 || blob.subarray(4, 15).toString("ascii") !== "ssh-ed25519" || blob.readUInt32BE(15) !== 32)
2642
+ return;
2643
+ if (blob.toString("base64") !== encoded)
2644
+ return;
2645
+ return `${algorithm} ${encoded}`;
2646
+ }
2607
2647
  async function readCapabilities(run2) {
2608
2648
  const answers = {};
2609
2649
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -2743,7 +2783,7 @@ var runProvisionOperation = async (operation) => {
2743
2783
  return result;
2744
2784
  }
2745
2785
  result = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2746
- if (result.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(result.stdout)) {
2786
+ if (result.exitCode !== 0 || !normalizeEd25519PublicKey(result.stdout)) {
2747
2787
  return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
2748
2788
  }
2749
2789
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
@@ -2759,11 +2799,12 @@ var runProvisionOperation = async (operation) => {
2759
2799
  chmodSync2(key, 384);
2760
2800
  }
2761
2801
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2762
- if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
2802
+ const publicKey = normalizeEd25519PublicKey(derived.stdout);
2803
+ if (derived.exitCode !== 0 || !publicKey) {
2763
2804
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
2764
2805
  }
2765
2806
  mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
2766
- writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
2807
+ writeFileSync4(operation.publicKey, `${publicKey} forgezero-bootstrap-runner
2767
2808
  `, { mode: 292 });
2768
2809
  chmodSync2(operation.publicKey, 292);
2769
2810
  }
@@ -2806,8 +2847,9 @@ var runProvisionOperation = async (operation) => {
2806
2847
  const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
2807
2848
  const required = [
2808
2849
  "forgezero-agent-egress-v1",
2809
- `public-tcp=${operation.runnerPublicTcpPorts.join(",")}`,
2810
- ...operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
2850
+ ...operation.agentLoopbackPorts.length ? [`shared-loopback=${operation.agentLoopbackPorts.join(",")}`] : [],
2851
+ ...operation.deploymentEnabled && operation.runnerPublicTcpPorts.length ? [`public-tcp=${operation.runnerPublicTcpPorts.join(",")}`] : [],
2852
+ ...operation.deploymentEnabled && operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
2811
2853
  ];
2812
2854
  return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
2813
2855
  }
@@ -2873,7 +2915,9 @@ async function applyPlan(plan, run2) {
2873
2915
  const result = await run2(step2.operation);
2874
2916
  transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
2875
2917
  if (result.exitCode !== 0 && !step2.optional) {
2876
- throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
2918
+ const safeDetail = step2.operation.kind === "ensure-bootstrap-ssh-identity" ? result.stdout.trim().slice(0, 1000) : "";
2919
+ throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}${safeDetail ? `
2920
+ ${safeDetail}` : ""}`);
2877
2921
  }
2878
2922
  }
2879
2923
  return transcript;
@@ -3371,12 +3415,26 @@ map $limit_conn_status $forgezero_retry_after { default ''; REJECTED 1; REJECTED
3371
3415
  server {
3372
3416
  listen 127.0.0.1:${input.publicPort};
3373
3417
  server_name _;
3418
+ server_tokens off;
3419
+ more_clear_headers Server X-Powered-By;
3374
3420
  limit_conn forgezero_admission ${concurrencyLimit};
3375
3421
  limit_conn_status 503;
3376
3422
  add_header Retry-After $forgezero_retry_after always;
3377
- location ^~ /api/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $forgezero_connection; proxy_read_timeout 3600s; }
3378
- location ^~ /v1/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $forgezero_connection; proxy_read_timeout 3600s; }
3379
- location / { return 404; }
3423
+ error_page 400 403 404 405 408 413 414 429 500 502 503 504 = @transport_failure;
3424
+ location @transport_failure { internal; return 444; }
3425
+ location / {
3426
+ proxy_pass http://forgezero;
3427
+ proxy_http_version 1.1;
3428
+ proxy_intercept_errors off;
3429
+ proxy_hide_header Server;
3430
+ proxy_hide_header X-Powered-By;
3431
+ proxy_set_header Host $host;
3432
+ proxy_set_header X-Forwarded-Proto https;
3433
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
3434
+ proxy_set_header Upgrade $http_upgrade;
3435
+ proxy_set_header Connection $forgezero_connection;
3436
+ proxy_read_timeout 3600s;
3437
+ }
3380
3438
  }
3381
3439
  `
3382
3440
  };
@@ -3988,7 +4046,7 @@ Type=oneshot
3988
4046
  User=arangodb
3989
4047
  Group=arangodb
3990
4048
  LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
3991
- ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default")quit(0);last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
4049
+ ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
3992
4050
  RemainAfterExit=yes
3993
4051
  TimeoutStartSec=200
3994
4052
  NoNewPrivileges=true
@@ -4114,11 +4172,11 @@ async function seal(host, name, destination2, value) {
4114
4172
  async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
4115
4173
  const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
4116
4174
  const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
4117
- if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
4118
- throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
4175
+ if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 18) !== 0 || manifestMetadata.size > 16 * 1024)) {
4176
+ throw new Error("bootstrap bundle manifest must be a root-owned non-writable regular file");
4119
4177
  }
4120
- if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
4121
- throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
4178
+ if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 18) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
4179
+ throw new Error("bootstrap bundle must be a root-owned non-writable regular file within 512 MiB");
4122
4180
  }
4123
4181
  if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
4124
4182
  throw new Error("attended bootstrap bundle and manifest are required for release generation one");
@@ -4137,11 +4195,25 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
4137
4195
  if (digest !== manifest.sha256)
4138
4196
  throw new Error("bootstrap bundle digest does not match its manifest");
4139
4197
  if (verifyGit) {
4140
- await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
4198
+ const verificationRepository = "/run/forgezero-bootstrap-bundle-verify";
4199
+ await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "stale bootstrap bundle verifier cleanup");
4200
+ await checked3(host, ["/usr/bin/git", "init", "--bare", "--quiet", verificationRepository], "bootstrap bundle verifier repository");
4201
+ try {
4202
+ await checked3(host, [
4203
+ "/usr/bin/git",
4204
+ "-C",
4205
+ verificationRepository,
4206
+ "bundle",
4207
+ "verify",
4208
+ config.bootstrapBundle.bundleFile
4209
+ ], "bootstrap Git bundle verification");
4210
+ } finally {
4211
+ await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "bootstrap bundle verifier cleanup");
4212
+ }
4141
4213
  }
4142
4214
  return manifest;
4143
4215
  }
4144
- function bootstrapIdentity(config) {
4216
+ function bootstrapIdentity(config, includeDeploymentEvidence = false) {
4145
4217
  if (config.kind === "enrolled-compute")
4146
4218
  return {
4147
4219
  kind: config.kind,
@@ -4160,7 +4232,13 @@ function bootstrapIdentity(config) {
4160
4232
  bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
4161
4233
  };
4162
4234
  const environment = config.runtime.environment;
4163
- const { cloudflare: _cloudflare, realtime: _realtime, ...stableEnvironment } = environment;
4235
+ const { cloudflare: _cloudflare, realtime: _realtime, initialInventory, ...stableEnvironmentRest } = environment;
4236
+ const stableEnvironment = {
4237
+ ...stableEnvironmentRest,
4238
+ ...initialInventory ? {
4239
+ initialInventory: { ...initialInventory, deployment: includeDeploymentEvidence ? initialInventory.deployment : undefined }
4240
+ } : {}
4241
+ };
4164
4242
  return {
4165
4243
  kind: config.kind,
4166
4244
  environment: config.environment,
@@ -4201,6 +4279,26 @@ function bootstrapIdentity(config) {
4201
4279
  function bootstrapIdentityDigest(config) {
4202
4280
  return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
4203
4281
  }
4282
+ var legacyBootstrapIdentityDigest = (config) => createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config, true))).digest("hex");
4283
+ function interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest) {
4284
+ if (!config.runtime.environment.initialInventory)
4285
+ return [];
4286
+ const previous = structuredClone(config);
4287
+ previous.runtime.environment.initialInventory.deployment = {
4288
+ source: "bootstrap-bundle",
4289
+ branch: releaseEvidence.branch,
4290
+ revision: releaseEvidence.revision,
4291
+ bundleSha256: releaseEvidence.sha256
4292
+ };
4293
+ const expectedCurrentEpoch = bootstrapManifest ? `${config.environment}-${bootstrapManifest.revision.slice(0, 16)}` : undefined;
4294
+ if (expectedCurrentEpoch && config.runtime.environment.seedSyncEpoch === expectedCurrentEpoch) {
4295
+ previous.runtime.environment.seedSyncEpoch = `${config.environment}-${releaseEvidence.revision.slice(0, 16)}`;
4296
+ }
4297
+ return [bootstrapIdentityDigest(previous), legacyBootstrapIdentityDigest(previous)];
4298
+ }
4299
+ function storedPlatformCoordinatesMatch(state, config) {
4300
+ return state.kind === "platform" && state.profile === config.profile && state.nodeHostname === config.nodeHostname && state.apiUrl === config.apiUrl && state.environment === config.environment && state.databaseRole === config.database.role && state.databaseAgency === config.database.agency && state.databaseReadPreferred === config.database.readPreferred && state.databaseServerMode === config.database.serverMode && (state.databaseAddress ?? null) === (config.database.address ?? null) && JSON.stringify(state.databaseCoordinators ?? []) === JSON.stringify(config.database.coordinators) && state.collectorUnit === config.runtime.environment.otlpCollectorUnit && state.publicApiPort === config.runtime.environment.publicApiPort && state.healthPath === config.runtime.healthPath;
4301
+ }
4204
4302
  function parseStoredState(raw) {
4205
4303
  let value;
4206
4304
  try {
@@ -4231,11 +4329,21 @@ function parseStoredIntent(raw) {
4231
4329
  }
4232
4330
  return intent;
4233
4331
  }
4234
- function bindBootstrapIntent(host, config) {
4332
+ function bindBootstrapIntent(host, config, previousReleaseDigests = []) {
4235
4333
  const identityDigest = bootstrapIdentityDigest(config);
4236
4334
  if (host.exists(INTENT_PATH)) {
4237
4335
  const intent = parseStoredIntent(host.read(INTENT_PATH));
4238
4336
  if (intent.kind !== config.kind || intent.identityDigest !== identityDigest) {
4337
+ if (previousReleaseDigests.includes(intent.identityDigest) && intent.kind === "platform" && config.kind === "platform") {
4338
+ host.write(INTENT_PATH, `${JSON.stringify({
4339
+ format: 1,
4340
+ kind: config.kind,
4341
+ identityDigest,
4342
+ createdAt: new Date().toISOString()
4343
+ }, null, 2)}
4344
+ `, 384);
4345
+ return identityDigest;
4346
+ }
4239
4347
  throw new Error("bootstrap resume coordinates do not match the interrupted host intent");
4240
4348
  }
4241
4349
  } else if (!host.exists(STATE_PATH)) {
@@ -4310,7 +4418,7 @@ async function bootstrapStatus(host = localBootstrapHost()) {
4310
4418
  if (result.exitCode !== 0)
4311
4419
  problems.push(`${unit} is not active`);
4312
4420
  }
4313
- for (const socket of [DEFAULT_SOCKET2, CONTROL_SOCKET]) {
4421
+ for (const socket of [DEFAULT_SOCKET2]) {
4314
4422
  services[socket] = host.exists(socket);
4315
4423
  if (!services[socket])
4316
4424
  problems.push(`${socket} is missing`);
@@ -4403,7 +4511,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4403
4511
  let installed;
4404
4512
  if (host.exists(STATE_PATH))
4405
4513
  installed = parseStoredState(host.read(STATE_PATH));
4406
- const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
4514
+ let bootstrapManifest;
4515
+ let releaseEvidence;
4516
+ if (config.kind === "platform") {
4517
+ const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
4518
+ if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
4519
+ throw new Error("bootstrap bundle and manifest must be staged together");
4520
+ }
4521
+ const reviewed = staged ? await verifyBootstrapBundleOnHost(host, config) : undefined;
4522
+ if (!host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4523
+ if (!reviewed)
4524
+ throw new Error("attended bootstrap bundle and manifest are required for release generation one");
4525
+ bootstrapManifest = reviewed;
4526
+ } else if (reviewed) {
4527
+ let evidence;
4528
+ try {
4529
+ evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
4530
+ } catch {
4531
+ throw new Error("bootstrap release evidence is malformed");
4532
+ }
4533
+ if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
4534
+ throw new Error("bootstrap release evidence is malformed");
4535
+ releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4536
+ if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
4537
+ bootstrapManifest = reviewed;
4538
+ } else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4539
+ let evidence;
4540
+ try {
4541
+ evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
4542
+ } catch {
4543
+ throw new Error("bootstrap release evidence is malformed");
4544
+ }
4545
+ if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
4546
+ throw new Error("bootstrap release evidence is malformed");
4547
+ releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4548
+ }
4549
+ }
4407
4550
  let cloudflare;
4408
4551
  if (config.cloudflareHandoff) {
4409
4552
  if (host.exists(config.cloudflareHandoff.handoffFile)) {
@@ -4453,10 +4596,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4453
4596
  throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
4454
4597
  }
4455
4598
  }
4599
+ let allowInterruptedReleaseRebind = false;
4456
4600
  if (installed) {
4457
- if (installed.kind !== config.kind || installed.identityDigest !== bootstrapIdentityDigest(config)) {
4601
+ const exactIdentity = installed.kind === config.kind && installed.identityDigest === bootstrapIdentityDigest(config);
4602
+ const boundedReleaseResume = config.kind === "platform" && !host.exists("/var/lib/forgezero/enrolment.json") && host.exists(BOOTSTRAP_RELEASE_EVIDENCE) && storedPlatformCoordinatesMatch(installed, config);
4603
+ if (!exactIdentity && !boundedReleaseResume) {
4458
4604
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
4459
4605
  }
4606
+ allowInterruptedReleaseRebind = !exactIdentity && boundedReleaseResume;
4460
4607
  }
4461
4608
  const platformPrivate = config.kind === "platform" ? (() => {
4462
4609
  if (!secrets)
@@ -4476,10 +4623,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4476
4623
  if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
4477
4624
  privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
4478
4625
  }
4479
- bindBootstrapIntent(host, config);
4626
+ let previousReleaseDigests = [];
4627
+ if (!installed && config.kind === "platform" && releaseEvidence && !host.exists("/var/lib/forgezero/enrolment.json")) {
4628
+ previousReleaseDigests = [...interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest)];
4629
+ }
4630
+ bindBootstrapIntent(host, config, allowInterruptedReleaseRebind && installed?.identityDigest ? [installed.identityDigest] : previousReleaseDigests);
4480
4631
  const plan = planBootstrap(config, host.exists(STATE_PATH));
4481
4632
  await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
4482
- const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
4633
+ let alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
4483
4634
  host.mkdir(CREDS, 448);
4484
4635
  host.mkdir("/var/lib/forgezero", 448);
4485
4636
  if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
@@ -4508,14 +4659,29 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4508
4659
  if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
4509
4660
  await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
4510
4661
  }
4662
+ let installedAgentPlan;
4511
4663
  if (alreadyEnrolled) {
4512
- await host.installAgent(config, "bound");
4664
+ installedAgentPlan = await host.installAgent(config, bootstrapManifest ? "bootstrap" : "bound");
4513
4665
  } else if (config.kind === "platform") {
4514
4666
  if (bootstrapManifest)
4515
- await host.installAgent(config, "bootstrap");
4667
+ installedAgentPlan = await host.installAgent(config, "bootstrap");
4668
+ else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4669
+ await checked3(host, [
4670
+ "curl",
4671
+ "--fail",
4672
+ "--silent",
4673
+ "--show-error",
4674
+ "--max-time",
4675
+ "10",
4676
+ `http://127.0.0.1:${config.runtime.environment.publicApiPort}${config.runtime.healthPath}`
4677
+ ], "persisted release-one API health");
4678
+ await host.installAgent(config, "enrol");
4679
+ installedAgentPlan = await host.installAgent(config, "bound");
4680
+ alreadyEnrolled = true;
4681
+ }
4516
4682
  } else {
4517
4683
  await host.installAgent(config, "enrol");
4518
- await host.installAgent(config, "bound");
4684
+ installedAgentPlan = await host.installAgent(config, "bound");
4519
4685
  }
4520
4686
  if (config.kind === "platform" && config.firewall.enabled) {
4521
4687
  await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
@@ -4595,6 +4761,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4595
4761
  await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
4596
4762
  await checked3(host, ["id", runtime.serviceUser], "existing API service account");
4597
4763
  });
4764
+ await checked3(host, ["usermod", "-a", "-G", DEPLOYMENT_GROUP, runtime.serviceUser], "API deployment group");
4598
4765
  host.mkdir(runtime.environment.sharedDirectory, 488);
4599
4766
  host.mkdir(runtime.slotsDirectory, 493);
4600
4767
  host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
@@ -4613,7 +4780,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4613
4780
  host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
4614
4781
  await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
4615
4782
  const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
4616
- await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
4783
+ await checked3(host, [telemetry.unitCheck.command, ...telemetry.unitCheck.argv], "OTLP collector supervision");
4617
4784
  const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
4618
4785
  if (!/^2\d\d$/.test(otlpStatus))
4619
4786
  throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
@@ -4646,10 +4813,12 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4646
4813
  `, 384);
4647
4814
  }
4648
4815
  if (bootstrapManifest) {
4816
+ if (!installedAgentPlan?.user)
4817
+ throw new Error("initial Agent deployment requires the converged Agent user");
4649
4818
  await checked3(host, [
4650
4819
  "runuser",
4651
4820
  "-u",
4652
- "forgezero-agent",
4821
+ installedAgentPlan.user,
4653
4822
  "--",
4654
4823
  "/usr/local/bin/fz-agent",
4655
4824
  "deploy",
@@ -4667,6 +4836,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4667
4836
  `, 384);
4668
4837
  host.remove(config.bootstrapBundle.bundleFile);
4669
4838
  host.remove(config.bootstrapBundle.manifestFile);
4839
+ if (alreadyEnrolled)
4840
+ installedAgentPlan = await host.installAgent(config, "bound");
4670
4841
  }
4671
4842
  if (!alreadyEnrolled) {
4672
4843
  await checked3(host, [
@@ -4676,7 +4847,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4676
4847
  "--show-error",
4677
4848
  "--max-time",
4678
4849
  "10",
4679
- `http://127.0.0.1:3000${runtime.healthPath}`
4850
+ `http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
4680
4851
  ], "release-one API health");
4681
4852
  await host.installAgent(config, "enrol");
4682
4853
  await host.installAgent(config, "bound");
@@ -4878,8 +5049,8 @@ function readBootstrapConfig(path) {
4878
5049
  function planBootstrapAgentInstall(config, phase, context) {
4879
5050
  const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
4880
5051
  if (phase === "bootstrap") {
4881
- if (config.kind !== "platform" || hasBinding || !initialBundle) {
4882
- throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
5052
+ if (config.kind !== "platform" || !initialBundle) {
5053
+ throw new Error("platform bundle Agent install requires a verified bootstrap bundle");
4883
5054
  }
4884
5055
  } else if (phase === "enrol") {
4885
5056
  if (hasBinding || !hasEnrolCredential) {
@@ -4917,8 +5088,10 @@ function planBootstrapAgentInstall(config, phase, context) {
4917
5088
  bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
4918
5089
  lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
4919
5090
  enforceEgress: true,
5091
+ runnerLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort, config.runtime.bluePort, config.runtime.greenPort] : [],
5092
+ agentLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort] : [],
4920
5093
  nodeHostname: config.nodeHostname,
4921
- telemetryEndpoint: config.telemetryEndpoint,
5094
+ telemetryEndpoint: config.kind === "platform" ? config.runtime.environment.otlpEndpoint : "http://127.0.0.1:4318",
4922
5095
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
4923
5096
  sourceBinPath: PACKAGED_AGENT_BIN,
4924
5097
  ...enrolling ? {
@@ -4926,7 +5099,7 @@ function planBootstrapAgentInstall(config, phase, context) {
4926
5099
  enrolStatePath: "/var/lib/forgezero/enrolment.json"
4927
5100
  } : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
4928
5101
  ...authenticated ? {
4929
- apiUrl: config.apiUrl,
5102
+ apiUrl: config.kind === "platform" ? `http://127.0.0.1:${config.runtime.environment.publicApiPort}` : config.apiUrl,
4930
5103
  project: config.kind === "enrolled-compute" ? config.realm : "platform",
4931
5104
  environment: config.kind === "enrolled-compute" ? undefined : config.environment,
4932
5105
  nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
@@ -4935,6 +5108,7 @@ function planBootstrapAgentInstall(config, phase, context) {
4935
5108
  return planInstall(options);
4936
5109
  }
4937
5110
  function localBootstrapHost() {
5111
+ let softwareClientUser;
4938
5112
  const execute = async (argv, options = {}) => {
4939
5113
  const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
4940
5114
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -4967,10 +5141,12 @@ function localBootstrapHost() {
4967
5141
  exec: execute,
4968
5142
  sleep: (milliseconds) => Bun.sleep(milliseconds),
4969
5143
  async ensureSoftware(requirements) {
5144
+ if (!softwareClientUser)
5145
+ throw new Error("Agent must be converged before software requirements are applied");
4970
5146
  const result = await execute([
4971
5147
  "runuser",
4972
5148
  "-u",
4973
- "forgezero-agent",
5149
+ softwareClientUser,
4974
5150
  "--",
4975
5151
  "/usr/local/bin/fz-agent",
4976
5152
  "software-ensure",
@@ -4984,7 +5160,7 @@ function localBootstrapHost() {
4984
5160
  const capabilities = await readCapabilities(localRunner);
4985
5161
  const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
4986
5162
  const hasEnrolCredential = existsSync5(ENROL_CREDENTIAL);
4987
- const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
5163
+ const initialBundle = config.kind === "platform" && phase === "bootstrap" && existsSync5(config.bootstrapBundle.bundleFile) && existsSync5(config.bootstrapBundle.manifestFile) ? {
4988
5164
  path: config.bootstrapBundle.bundleFile,
4989
5165
  manifestPath: config.bootstrapBundle.manifestFile,
4990
5166
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
@@ -5015,6 +5191,7 @@ function localBootstrapHost() {
5015
5191
  writeFileSync5(unit.path, unit.unit, { mode: 420 });
5016
5192
  }
5017
5193
  await applyPlan(plan, localRunner);
5194
+ softwareClientUser = plan.user;
5018
5195
  return plan;
5019
5196
  }
5020
5197
  };
@@ -5028,6 +5205,8 @@ export {
5028
5205
  planBootstrapAgentInstall,
5029
5206
  planBootstrap,
5030
5207
  localBootstrapHost,
5208
+ legacyBootstrapIdentityDigest,
5209
+ interruptedPlatformReleaseIdentityDigests,
5031
5210
  bootstrapStatus,
5032
5211
  bootstrapIdentityDigest,
5033
5212
  applyBootstrap,