@forgezero/agent 0.1.88 → 0.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.89";
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
@@ -2216,7 +2219,7 @@ function agentUnit(options) {
2216
2219
  const supplementaryGroups = [
2217
2220
  AGENT_UPDATE_GROUP,
2218
2221
  deploymentEnabled ? DEPLOYMENT_GROUP : null,
2219
- deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
2222
+ SOFTWARE_HELPER_GROUP,
2220
2223
  lifecycleEnabled ? LIFECYCLE_GROUP : null
2221
2224
  ].filter((value) => value !== null);
2222
2225
  const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
@@ -2225,7 +2228,7 @@ function agentUnit(options) {
2225
2228
  "forgezero-agent-update-helper.service",
2226
2229
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
2227
2230
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
2228
- deploymentEnabled ? "forgezero-software-helper.service" : null,
2231
+ "forgezero-software-helper.service",
2229
2232
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
2230
2233
  warpEnabled ? "warp-svc.service" : null,
2231
2234
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -2234,7 +2237,7 @@ function agentUnit(options) {
2234
2237
  "forgezero-agent-update-helper.service",
2235
2238
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
2236
2239
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
2237
- deploymentEnabled ? "forgezero-software-helper.service" : null,
2240
+ "forgezero-software-helper.service",
2238
2241
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
2239
2242
  warpEnabled ? "warp-svc.service" : null,
2240
2243
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -2249,9 +2252,9 @@ function agentUnit(options) {
2249
2252
  const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
2250
2253
  DeviceAllow=/dev/sev-guest rw` : "";
2251
2254
  const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
2252
- ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
2255
+ ExecStartPre=+/bin/chmod 0660 /dev/sev-guest
2253
2256
  ` : "";
2254
- const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
2257
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
2255
2258
  return `[Unit]
2256
2259
  Description=ForgeZero node agent (${options.mode})
2257
2260
  Documentation=https://www.forgezero.net/docs/agent
@@ -2382,6 +2385,7 @@ function planProvision(options) {
2382
2385
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
2383
2386
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
2384
2387
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
2388
+ const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
2385
2389
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
2386
2390
  const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
2387
2391
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
@@ -2430,8 +2434,9 @@ function planProvision(options) {
2430
2434
  const enabledUnits = [
2431
2435
  "forgezero-agent.socket",
2432
2436
  "forgezero-agent-update-helper.service",
2437
+ "forgezero-software-helper.service",
2433
2438
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2434
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2439
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
2435
2440
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2436
2441
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2437
2442
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
@@ -2439,8 +2444,9 @@ function planProvision(options) {
2439
2444
  ];
2440
2445
  const restartedUnits = [
2441
2446
  "forgezero-agent-update-helper.service",
2447
+ "forgezero-software-helper.service",
2442
2448
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2443
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2449
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
2444
2450
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2445
2451
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2446
2452
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
@@ -2464,9 +2470,9 @@ function planProvision(options) {
2464
2470
  ...options.enforceEgress ? [
2465
2471
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
2466
2472
  ] : [],
2473
+ { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) },
2467
2474
  ...deploymentEnabled ? [
2468
- { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
2469
- { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
2475
+ { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
2470
2476
  ] : [],
2471
2477
  ...enrolmentEnabled ? [
2472
2478
  { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
@@ -2499,24 +2505,29 @@ function planProvision(options) {
2499
2505
  step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
2500
2506
  ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION })] : [],
2501
2507
  ...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] },
2508
+ step("software strategy helper group", { kind: "commands", commands: [
2504
2509
  { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
2510
+ ] }),
2511
+ ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
2512
+ { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] }
2505
2513
  ] })] : [],
2506
2514
  ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
2507
2515
  step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
2508
2516
  step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
2509
2517
  step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
2518
+ step("grant software helper socket access", { kind: "commands", commands: [
2519
+ { argv: ["/usr/sbin/usermod", "-a", "-G", SOFTWARE_HELPER_GROUP, user] }
2520
+ ] }),
2510
2521
  ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
2511
2522
  ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
2512
2523
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
2513
2524
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
2514
2525
  { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
2515
- { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
2526
+ { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, user] }
2516
2527
  ] })] : [],
2517
2528
  step("credential and state directories", { kind: "directories", directories: [
2518
2529
  { path: credentialDir, mode: 448, owner: "root", group: "root" },
2519
- { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
2530
+ { path: "/var/lib/forgezero", mode: 488, owner: "root", group: VAULT_GROUP }
2520
2531
  ] }),
2521
2532
  step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
2522
2533
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
@@ -2567,19 +2578,26 @@ function planProvision(options) {
2567
2578
  { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
2568
2579
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2569
2580
  ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
2581
+ { argv: ["/usr/bin/systemctl", "stop", "forgezero-agent-proxy.service"], acceptedExitCodes: [0, 1, 5] },
2570
2582
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
2571
2583
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2572
2584
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
2573
2585
  ] }),
2574
2586
  ...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 })] : [],
2587
+ ...options.enforceEgress ? [step("prove the Agent egress policy is active", {
2588
+ kind: "verify-egress",
2589
+ deploymentEnabled,
2590
+ runnerPublicTcpPorts,
2591
+ runnerLoopbackPorts,
2592
+ agentLoopbackPorts
2593
+ })] : [],
2576
2594
  step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
2577
2595
  step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
2578
2596
  step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
2579
2597
  step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
2598
+ step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 }),
2580
2599
  ...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 })
2600
+ step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
2583
2601
  ] : [],
2584
2602
  ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
2585
2603
  ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
@@ -2604,6 +2622,25 @@ import {
2604
2622
  writeFileSync as writeFileSync4
2605
2623
  } from "fs";
2606
2624
  import { dirname as dirname5 } from "path";
2625
+ function normalizeEd25519PublicKey(output) {
2626
+ const line = output.trim();
2627
+ if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}(?: [^\r\n]{1,256})?$/.test(line))
2628
+ return;
2629
+ const [algorithm, encoded] = line.split(" ", 3);
2630
+ if (algorithm !== "ssh-ed25519" || !encoded)
2631
+ return;
2632
+ let blob;
2633
+ try {
2634
+ blob = Buffer.from(encoded, "base64");
2635
+ } catch {
2636
+ return;
2637
+ }
2638
+ if (blob.length !== 51 || blob.readUInt32BE(0) !== 11 || blob.subarray(4, 15).toString("ascii") !== "ssh-ed25519" || blob.readUInt32BE(15) !== 32)
2639
+ return;
2640
+ if (blob.toString("base64") !== encoded)
2641
+ return;
2642
+ return `${algorithm} ${encoded}`;
2643
+ }
2607
2644
  async function readCapabilities(run2) {
2608
2645
  const answers = {};
2609
2646
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -2743,7 +2780,7 @@ var runProvisionOperation = async (operation) => {
2743
2780
  return result;
2744
2781
  }
2745
2782
  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)) {
2783
+ if (result.exitCode !== 0 || !normalizeEd25519PublicKey(result.stdout)) {
2747
2784
  return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
2748
2785
  }
2749
2786
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
@@ -2759,11 +2796,12 @@ var runProvisionOperation = async (operation) => {
2759
2796
  chmodSync2(key, 384);
2760
2797
  }
2761
2798
  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)) {
2799
+ const publicKey = normalizeEd25519PublicKey(derived.stdout);
2800
+ if (derived.exitCode !== 0 || !publicKey) {
2763
2801
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
2764
2802
  }
2765
2803
  mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
2766
- writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
2804
+ writeFileSync4(operation.publicKey, `${publicKey} forgezero-bootstrap-runner
2767
2805
  `, { mode: 292 });
2768
2806
  chmodSync2(operation.publicKey, 292);
2769
2807
  }
@@ -2806,8 +2844,9 @@ var runProvisionOperation = async (operation) => {
2806
2844
  const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
2807
2845
  const required = [
2808
2846
  "forgezero-agent-egress-v1",
2809
- `public-tcp=${operation.runnerPublicTcpPorts.join(",")}`,
2810
- ...operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
2847
+ ...operation.agentLoopbackPorts.length ? [`shared-loopback=${operation.agentLoopbackPorts.join(",")}`] : [],
2848
+ ...operation.deploymentEnabled && operation.runnerPublicTcpPorts.length ? [`public-tcp=${operation.runnerPublicTcpPorts.join(",")}`] : [],
2849
+ ...operation.deploymentEnabled && operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
2811
2850
  ];
2812
2851
  return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
2813
2852
  }
@@ -2873,7 +2912,9 @@ async function applyPlan(plan, run2) {
2873
2912
  const result = await run2(step2.operation);
2874
2913
  transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
2875
2914
  if (result.exitCode !== 0 && !step2.optional) {
2876
- throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
2915
+ const safeDetail = step2.operation.kind === "ensure-bootstrap-ssh-identity" ? result.stdout.trim().slice(0, 1000) : "";
2916
+ throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}${safeDetail ? `
2917
+ ${safeDetail}` : ""}`);
2877
2918
  }
2878
2919
  }
2879
2920
  return transcript;
@@ -3371,12 +3412,26 @@ map $limit_conn_status $forgezero_retry_after { default ''; REJECTED 1; REJECTED
3371
3412
  server {
3372
3413
  listen 127.0.0.1:${input.publicPort};
3373
3414
  server_name _;
3415
+ server_tokens off;
3416
+ more_clear_headers Server X-Powered-By;
3374
3417
  limit_conn forgezero_admission ${concurrencyLimit};
3375
3418
  limit_conn_status 503;
3376
3419
  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; }
3420
+ error_page 400 403 404 405 408 413 414 429 500 502 503 504 = @transport_failure;
3421
+ location @transport_failure { internal; return 444; }
3422
+ location / {
3423
+ proxy_pass http://forgezero;
3424
+ proxy_http_version 1.1;
3425
+ proxy_intercept_errors off;
3426
+ proxy_hide_header Server;
3427
+ proxy_hide_header X-Powered-By;
3428
+ proxy_set_header Host $host;
3429
+ proxy_set_header X-Forwarded-Proto https;
3430
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
3431
+ proxy_set_header Upgrade $http_upgrade;
3432
+ proxy_set_header Connection $forgezero_connection;
3433
+ proxy_read_timeout 3600s;
3434
+ }
3380
3435
  }
3381
3436
  `
3382
3437
  };
@@ -3988,7 +4043,7 @@ Type=oneshot
3988
4043
  User=arangodb
3989
4044
  Group=arangodb
3990
4045
  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));'
4046
+ 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
4047
  RemainAfterExit=yes
3993
4048
  TimeoutStartSec=200
3994
4049
  NoNewPrivileges=true
@@ -4114,11 +4169,11 @@ async function seal(host, name, destination2, value) {
4114
4169
  async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
4115
4170
  const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
4116
4171
  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");
4172
+ if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 18) !== 0 || manifestMetadata.size > 16 * 1024)) {
4173
+ throw new Error("bootstrap bundle manifest must be a root-owned non-writable regular file");
4119
4174
  }
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");
4175
+ if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 18) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
4176
+ throw new Error("bootstrap bundle must be a root-owned non-writable regular file within 512 MiB");
4122
4177
  }
4123
4178
  if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
4124
4179
  throw new Error("attended bootstrap bundle and manifest are required for release generation one");
@@ -4137,11 +4192,25 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
4137
4192
  if (digest !== manifest.sha256)
4138
4193
  throw new Error("bootstrap bundle digest does not match its manifest");
4139
4194
  if (verifyGit) {
4140
- await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
4195
+ const verificationRepository = "/run/forgezero-bootstrap-bundle-verify";
4196
+ await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "stale bootstrap bundle verifier cleanup");
4197
+ await checked3(host, ["/usr/bin/git", "init", "--bare", "--quiet", verificationRepository], "bootstrap bundle verifier repository");
4198
+ try {
4199
+ await checked3(host, [
4200
+ "/usr/bin/git",
4201
+ "-C",
4202
+ verificationRepository,
4203
+ "bundle",
4204
+ "verify",
4205
+ config.bootstrapBundle.bundleFile
4206
+ ], "bootstrap Git bundle verification");
4207
+ } finally {
4208
+ await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "bootstrap bundle verifier cleanup");
4209
+ }
4141
4210
  }
4142
4211
  return manifest;
4143
4212
  }
4144
- function bootstrapIdentity(config) {
4213
+ function bootstrapIdentity(config, includeDeploymentEvidence = false) {
4145
4214
  if (config.kind === "enrolled-compute")
4146
4215
  return {
4147
4216
  kind: config.kind,
@@ -4160,7 +4229,13 @@ function bootstrapIdentity(config) {
4160
4229
  bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
4161
4230
  };
4162
4231
  const environment = config.runtime.environment;
4163
- const { cloudflare: _cloudflare, realtime: _realtime, ...stableEnvironment } = environment;
4232
+ const { cloudflare: _cloudflare, realtime: _realtime, initialInventory, ...stableEnvironmentRest } = environment;
4233
+ const stableEnvironment = {
4234
+ ...stableEnvironmentRest,
4235
+ ...initialInventory ? {
4236
+ initialInventory: { ...initialInventory, deployment: includeDeploymentEvidence ? initialInventory.deployment : undefined }
4237
+ } : {}
4238
+ };
4164
4239
  return {
4165
4240
  kind: config.kind,
4166
4241
  environment: config.environment,
@@ -4201,6 +4276,26 @@ function bootstrapIdentity(config) {
4201
4276
  function bootstrapIdentityDigest(config) {
4202
4277
  return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
4203
4278
  }
4279
+ var legacyBootstrapIdentityDigest = (config) => createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config, true))).digest("hex");
4280
+ function interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest) {
4281
+ if (!config.runtime.environment.initialInventory)
4282
+ return [];
4283
+ const previous = structuredClone(config);
4284
+ previous.runtime.environment.initialInventory.deployment = {
4285
+ source: "bootstrap-bundle",
4286
+ branch: releaseEvidence.branch,
4287
+ revision: releaseEvidence.revision,
4288
+ bundleSha256: releaseEvidence.sha256
4289
+ };
4290
+ const expectedCurrentEpoch = bootstrapManifest ? `${config.environment}-${bootstrapManifest.revision.slice(0, 16)}` : undefined;
4291
+ if (expectedCurrentEpoch && config.runtime.environment.seedSyncEpoch === expectedCurrentEpoch) {
4292
+ previous.runtime.environment.seedSyncEpoch = `${config.environment}-${releaseEvidence.revision.slice(0, 16)}`;
4293
+ }
4294
+ return [bootstrapIdentityDigest(previous), legacyBootstrapIdentityDigest(previous)];
4295
+ }
4296
+ function storedPlatformCoordinatesMatch(state, config) {
4297
+ 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;
4298
+ }
4204
4299
  function parseStoredState(raw) {
4205
4300
  let value;
4206
4301
  try {
@@ -4231,11 +4326,21 @@ function parseStoredIntent(raw) {
4231
4326
  }
4232
4327
  return intent;
4233
4328
  }
4234
- function bindBootstrapIntent(host, config) {
4329
+ function bindBootstrapIntent(host, config, previousReleaseDigests = []) {
4235
4330
  const identityDigest = bootstrapIdentityDigest(config);
4236
4331
  if (host.exists(INTENT_PATH)) {
4237
4332
  const intent = parseStoredIntent(host.read(INTENT_PATH));
4238
4333
  if (intent.kind !== config.kind || intent.identityDigest !== identityDigest) {
4334
+ if (previousReleaseDigests.includes(intent.identityDigest) && intent.kind === "platform" && config.kind === "platform") {
4335
+ host.write(INTENT_PATH, `${JSON.stringify({
4336
+ format: 1,
4337
+ kind: config.kind,
4338
+ identityDigest,
4339
+ createdAt: new Date().toISOString()
4340
+ }, null, 2)}
4341
+ `, 384);
4342
+ return identityDigest;
4343
+ }
4239
4344
  throw new Error("bootstrap resume coordinates do not match the interrupted host intent");
4240
4345
  }
4241
4346
  } else if (!host.exists(STATE_PATH)) {
@@ -4403,7 +4508,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4403
4508
  let installed;
4404
4509
  if (host.exists(STATE_PATH))
4405
4510
  installed = parseStoredState(host.read(STATE_PATH));
4406
- const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
4511
+ let bootstrapManifest;
4512
+ let releaseEvidence;
4513
+ if (config.kind === "platform") {
4514
+ const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
4515
+ if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
4516
+ throw new Error("bootstrap bundle and manifest must be staged together");
4517
+ }
4518
+ const reviewed = staged ? await verifyBootstrapBundleOnHost(host, config) : undefined;
4519
+ if (!host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4520
+ if (!reviewed)
4521
+ throw new Error("attended bootstrap bundle and manifest are required for release generation one");
4522
+ bootstrapManifest = reviewed;
4523
+ } else if (reviewed) {
4524
+ let evidence;
4525
+ try {
4526
+ evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
4527
+ } catch {
4528
+ throw new Error("bootstrap release evidence is malformed");
4529
+ }
4530
+ 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")
4531
+ throw new Error("bootstrap release evidence is malformed");
4532
+ releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4533
+ if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
4534
+ bootstrapManifest = reviewed;
4535
+ } else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4536
+ let evidence;
4537
+ try {
4538
+ evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
4539
+ } catch {
4540
+ throw new Error("bootstrap release evidence is malformed");
4541
+ }
4542
+ 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")
4543
+ throw new Error("bootstrap release evidence is malformed");
4544
+ releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4545
+ }
4546
+ }
4407
4547
  let cloudflare;
4408
4548
  if (config.cloudflareHandoff) {
4409
4549
  if (host.exists(config.cloudflareHandoff.handoffFile)) {
@@ -4453,10 +4593,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4453
4593
  throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
4454
4594
  }
4455
4595
  }
4596
+ let allowInterruptedReleaseRebind = false;
4456
4597
  if (installed) {
4457
- if (installed.kind !== config.kind || installed.identityDigest !== bootstrapIdentityDigest(config)) {
4598
+ const exactIdentity = installed.kind === config.kind && installed.identityDigest === bootstrapIdentityDigest(config);
4599
+ const boundedReleaseResume = config.kind === "platform" && !host.exists("/var/lib/forgezero/enrolment.json") && host.exists(BOOTSTRAP_RELEASE_EVIDENCE) && storedPlatformCoordinatesMatch(installed, config);
4600
+ if (!exactIdentity && !boundedReleaseResume) {
4458
4601
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
4459
4602
  }
4603
+ allowInterruptedReleaseRebind = !exactIdentity && boundedReleaseResume;
4460
4604
  }
4461
4605
  const platformPrivate = config.kind === "platform" ? (() => {
4462
4606
  if (!secrets)
@@ -4476,10 +4620,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4476
4620
  if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
4477
4621
  privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
4478
4622
  }
4479
- bindBootstrapIntent(host, config);
4623
+ let previousReleaseDigests = [];
4624
+ if (!installed && config.kind === "platform" && releaseEvidence && !host.exists("/var/lib/forgezero/enrolment.json")) {
4625
+ previousReleaseDigests = [...interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest)];
4626
+ }
4627
+ bindBootstrapIntent(host, config, allowInterruptedReleaseRebind && installed?.identityDigest ? [installed.identityDigest] : previousReleaseDigests);
4480
4628
  const plan = planBootstrap(config, host.exists(STATE_PATH));
4481
4629
  await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
4482
- const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
4630
+ let alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
4483
4631
  host.mkdir(CREDS, 448);
4484
4632
  host.mkdir("/var/lib/forgezero", 448);
4485
4633
  if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
@@ -4508,14 +4656,29 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4508
4656
  if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
4509
4657
  await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
4510
4658
  }
4659
+ let installedAgentPlan;
4511
4660
  if (alreadyEnrolled) {
4512
- await host.installAgent(config, "bound");
4661
+ installedAgentPlan = await host.installAgent(config, "bound");
4513
4662
  } else if (config.kind === "platform") {
4514
4663
  if (bootstrapManifest)
4515
- await host.installAgent(config, "bootstrap");
4664
+ installedAgentPlan = await host.installAgent(config, "bootstrap");
4665
+ else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4666
+ await checked3(host, [
4667
+ "curl",
4668
+ "--fail",
4669
+ "--silent",
4670
+ "--show-error",
4671
+ "--max-time",
4672
+ "10",
4673
+ `http://127.0.0.1:${config.runtime.environment.publicApiPort}${config.runtime.healthPath}`
4674
+ ], "persisted release-one API health");
4675
+ await host.installAgent(config, "enrol");
4676
+ installedAgentPlan = await host.installAgent(config, "bound");
4677
+ alreadyEnrolled = true;
4678
+ }
4516
4679
  } else {
4517
4680
  await host.installAgent(config, "enrol");
4518
- await host.installAgent(config, "bound");
4681
+ installedAgentPlan = await host.installAgent(config, "bound");
4519
4682
  }
4520
4683
  if (config.kind === "platform" && config.firewall.enabled) {
4521
4684
  await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
@@ -4595,6 +4758,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4595
4758
  await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
4596
4759
  await checked3(host, ["id", runtime.serviceUser], "existing API service account");
4597
4760
  });
4761
+ await checked3(host, ["usermod", "-a", "-G", DEPLOYMENT_GROUP, runtime.serviceUser], "API deployment group");
4598
4762
  host.mkdir(runtime.environment.sharedDirectory, 488);
4599
4763
  host.mkdir(runtime.slotsDirectory, 493);
4600
4764
  host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
@@ -4613,7 +4777,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4613
4777
  host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
4614
4778
  await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
4615
4779
  const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
4616
- await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
4780
+ await checked3(host, [telemetry.unitCheck.command, ...telemetry.unitCheck.argv], "OTLP collector supervision");
4617
4781
  const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
4618
4782
  if (!/^2\d\d$/.test(otlpStatus))
4619
4783
  throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
@@ -4646,10 +4810,12 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4646
4810
  `, 384);
4647
4811
  }
4648
4812
  if (bootstrapManifest) {
4813
+ if (!installedAgentPlan?.user)
4814
+ throw new Error("initial Agent deployment requires the converged Agent user");
4649
4815
  await checked3(host, [
4650
4816
  "runuser",
4651
4817
  "-u",
4652
- "forgezero-agent",
4818
+ installedAgentPlan.user,
4653
4819
  "--",
4654
4820
  "/usr/local/bin/fz-agent",
4655
4821
  "deploy",
@@ -4676,7 +4842,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4676
4842
  "--show-error",
4677
4843
  "--max-time",
4678
4844
  "10",
4679
- `http://127.0.0.1:3000${runtime.healthPath}`
4845
+ `http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
4680
4846
  ], "release-one API health");
4681
4847
  await host.installAgent(config, "enrol");
4682
4848
  await host.installAgent(config, "bound");
@@ -4917,6 +5083,8 @@ function planBootstrapAgentInstall(config, phase, context) {
4917
5083
  bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
4918
5084
  lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
4919
5085
  enforceEgress: true,
5086
+ runnerLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort, config.runtime.bluePort, config.runtime.greenPort] : [],
5087
+ agentLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort] : [],
4920
5088
  nodeHostname: config.nodeHostname,
4921
5089
  telemetryEndpoint: config.telemetryEndpoint,
4922
5090
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
@@ -4926,7 +5094,7 @@ function planBootstrapAgentInstall(config, phase, context) {
4926
5094
  enrolStatePath: "/var/lib/forgezero/enrolment.json"
4927
5095
  } : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
4928
5096
  ...authenticated ? {
4929
- apiUrl: config.apiUrl,
5097
+ apiUrl: config.kind === "platform" ? `http://127.0.0.1:${config.runtime.environment.publicApiPort}` : config.apiUrl,
4930
5098
  project: config.kind === "enrolled-compute" ? config.realm : "platform",
4931
5099
  environment: config.kind === "enrolled-compute" ? undefined : config.environment,
4932
5100
  nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
@@ -4935,6 +5103,7 @@ function planBootstrapAgentInstall(config, phase, context) {
4935
5103
  return planInstall(options);
4936
5104
  }
4937
5105
  function localBootstrapHost() {
5106
+ let softwareClientUser;
4938
5107
  const execute = async (argv, options = {}) => {
4939
5108
  const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
4940
5109
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -4967,10 +5136,12 @@ function localBootstrapHost() {
4967
5136
  exec: execute,
4968
5137
  sleep: (milliseconds) => Bun.sleep(milliseconds),
4969
5138
  async ensureSoftware(requirements) {
5139
+ if (!softwareClientUser)
5140
+ throw new Error("Agent must be converged before software requirements are applied");
4970
5141
  const result = await execute([
4971
5142
  "runuser",
4972
5143
  "-u",
4973
- "forgezero-agent",
5144
+ softwareClientUser,
4974
5145
  "--",
4975
5146
  "/usr/local/bin/fz-agent",
4976
5147
  "software-ensure",
@@ -4984,7 +5155,7 @@ function localBootstrapHost() {
4984
5155
  const capabilities = await readCapabilities(localRunner);
4985
5156
  const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
4986
5157
  const hasEnrolCredential = existsSync5(ENROL_CREDENTIAL);
4987
- const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
5158
+ const initialBundle = config.kind === "platform" && phase === "bootstrap" && existsSync5(config.bootstrapBundle.bundleFile) && existsSync5(config.bootstrapBundle.manifestFile) ? {
4988
5159
  path: config.bootstrapBundle.bundleFile,
4989
5160
  manifestPath: config.bootstrapBundle.manifestFile,
4990
5161
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
@@ -5015,6 +5186,7 @@ function localBootstrapHost() {
5015
5186
  writeFileSync5(unit.path, unit.unit, { mode: 420 });
5016
5187
  }
5017
5188
  await applyPlan(plan, localRunner);
5189
+ softwareClientUser = plan.user;
5018
5190
  return plan;
5019
5191
  }
5020
5192
  };
@@ -5028,6 +5200,8 @@ export {
5028
5200
  planBootstrapAgentInstall,
5029
5201
  planBootstrap,
5030
5202
  localBootstrapHost,
5203
+ legacyBootstrapIdentityDigest,
5204
+ interruptedPlatformReleaseIdentityDigests,
5031
5205
  bootstrapStatus,
5032
5206
  bootstrapIdentityDigest,
5033
5207
  applyBootstrap,