@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.
@@ -1198,7 +1198,12 @@ async function executeSoftwareOperation(operation) {
1198
1198
  }
1199
1199
  if (software === "nginx") {
1200
1200
  const binary = await run(["/usr/sbin/nginx", "-v"]);
1201
- return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
1201
+ if (binary.exitCode !== 0)
1202
+ return binary;
1203
+ if (!existsSync4("/usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so")) {
1204
+ return { exitCode: 1, output: "Nginx response-header suppression module is unavailable" };
1205
+ }
1206
+ return run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]);
1202
1207
  }
1203
1208
  if (software === "arangodb") {
1204
1209
  const binary = await run(["/usr/sbin/arangod", "--version"]);
@@ -1230,7 +1235,7 @@ async function executeSoftwareOperation(operation) {
1230
1235
  }
1231
1236
  if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client" || software === "git") {
1232
1237
  const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
1233
- const installed = await aptInstall(packageName);
1238
+ const installed = software === "nginx" ? await aptInstallMany(["nginx", "libnginx-mod-http-headers-more-filter"]) : await aptInstall(packageName);
1234
1239
  if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
1235
1240
  return installed;
1236
1241
  if (software === "docker") {
@@ -1382,11 +1387,22 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
1382
1387
  }
1383
1388
 
1384
1389
  // src/capacity-calibration.ts
1385
- var percentile95 = (values) => {
1390
+ import { cpus, freemem, totalmem } from "node:os";
1391
+ var percentile = (values, quantile) => {
1386
1392
  if (values.length === 0)
1387
1393
  return Number.POSITIVE_INFINITY;
1388
1394
  const sorted = values.toSorted((left, right) => left - right);
1389
- return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];
1395
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * quantile) - 1)];
1396
+ };
1397
+ var hostSample = () => {
1398
+ let cpuIdle = 0;
1399
+ let cpuTotal = 0;
1400
+ for (const cpu of cpus()) {
1401
+ cpuIdle += cpu.times.idle;
1402
+ cpuTotal += Object.values(cpu.times).reduce((sum, value) => sum + value, 0);
1403
+ }
1404
+ const memoryTotal = totalmem();
1405
+ return { cpuIdle, cpuTotal, memoryUsed: memoryTotal - freemem(), memoryTotal };
1390
1406
  };
1391
1407
  function localCalibrationEndpoint(value) {
1392
1408
  const endpoint = new URL(value);
@@ -1401,9 +1417,12 @@ function validateCapacityCalibrationOptions(options) {
1401
1417
  const requestsPerWorker = options.requestsPerWorker ?? 8;
1402
1418
  const maxP95Ms = options.maxP95Ms ?? 250;
1403
1419
  const maxErrorRate = options.maxErrorRate ?? 0.01;
1404
- const headroomRatio = options.headroomRatio ?? 0.8;
1420
+ const safetyRatio = options.safetyRatio ?? 0.8;
1405
1421
  const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
1406
- if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(headroomRatio) || headroomRatio < 0.25 || headroomRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000) {
1422
+ const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
1423
+ const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
1424
+ const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
1425
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
1407
1426
  throw new Error("Capacity calibration bounds are invalid.");
1408
1427
  }
1409
1428
  return {
@@ -1412,18 +1431,24 @@ function validateCapacityCalibrationOptions(options) {
1412
1431
  requestsPerWorker,
1413
1432
  maxP95Ms,
1414
1433
  maxErrorRate,
1415
- headroomRatio,
1416
- requestTimeoutMs
1434
+ safetyRatio,
1435
+ requestTimeoutMs,
1436
+ minimumStageDurationMs,
1437
+ maxCpuUtilizationPercent,
1438
+ maxMemoryUtilizationPercent
1417
1439
  };
1418
1440
  }
1419
- async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, fetcher) {
1441
+ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
1420
1442
  const latencies = [];
1421
1443
  let succeeded = 0;
1422
1444
  let failed = 0;
1423
1445
  let overloaded = 0;
1446
+ const systemBefore = sampler();
1424
1447
  const started = performance.now();
1425
1448
  await Promise.all(Array.from({ length: concurrency }, async () => {
1426
- for (let request = 0;request < requestsPerWorker; request += 1) {
1449
+ let request = 0;
1450
+ while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
1451
+ request += 1;
1427
1452
  const requestStarted = performance.now();
1428
1453
  try {
1429
1454
  const response = await fetcher(endpoint, {
@@ -1447,50 +1472,74 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, fetche
1447
1472
  }
1448
1473
  }
1449
1474
  }));
1450
- const elapsedSeconds = Math.max((performance.now() - started) / 1000, 0.001);
1475
+ const durationMs = Math.max(performance.now() - started, 1);
1476
+ const elapsedSeconds = durationMs / 1000;
1477
+ const systemAfter = sampler();
1478
+ const cpuTotal = Math.max(0, systemAfter.cpuTotal - systemBefore.cpuTotal);
1479
+ const cpuIdle = Math.max(0, systemAfter.cpuIdle - systemBefore.cpuIdle);
1480
+ const cpuUtilizationPercent = cpuTotal === 0 ? 0 : (cpuTotal - cpuIdle) / cpuTotal * 100;
1481
+ const memoryUtilizationPercent = Math.max(systemBefore.memoryTotal > 0 ? systemBefore.memoryUsed / systemBefore.memoryTotal * 100 : 0, systemAfter.memoryTotal > 0 ? systemAfter.memoryUsed / systemAfter.memoryTotal * 100 : 0);
1482
+ const requests = succeeded + failed;
1451
1483
  return {
1452
1484
  concurrency,
1453
- requests: concurrency * requestsPerWorker,
1485
+ requests,
1454
1486
  succeeded,
1455
1487
  failed,
1456
1488
  overloaded,
1457
- throughputPerSecond: Number(((succeeded + failed) / elapsedSeconds).toFixed(2)),
1458
- p95Ms: Number(percentile95(latencies).toFixed(2))
1489
+ successfulRequestsPerSecond: Number((succeeded / elapsedSeconds).toFixed(2)),
1490
+ attemptedRequestsPerSecond: Number((requests / elapsedSeconds).toFixed(2)),
1491
+ durationMs: Number(durationMs.toFixed(2)),
1492
+ errorRate: Number((failed / Math.max(requests, 1)).toFixed(6)),
1493
+ p95Ms: Number(percentile(latencies, 0.95).toFixed(2)),
1494
+ p99Ms: Number(percentile(latencies, 0.99).toFixed(2)),
1495
+ cpuUtilizationPercent: Number(cpuUtilizationPercent.toFixed(2)),
1496
+ memoryUtilizationPercent: Number(memoryUtilizationPercent.toFixed(2))
1459
1497
  };
1460
1498
  }
1461
- async function calibrateHttpConcurrency(options, fetcher = fetch) {
1499
+ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = hostSample) {
1462
1500
  const {
1463
1501
  endpoint,
1464
1502
  maxConcurrency,
1465
1503
  requestsPerWorker,
1466
1504
  maxP95Ms,
1467
1505
  maxErrorRate,
1468
- headroomRatio,
1469
- requestTimeoutMs: timeoutMs
1506
+ safetyRatio,
1507
+ requestTimeoutMs: timeoutMs,
1508
+ minimumStageDurationMs,
1509
+ maxCpuUtilizationPercent,
1510
+ maxMemoryUtilizationPercent
1470
1511
  } = validateCapacityCalibrationOptions(options);
1471
1512
  const stages = [];
1472
- let lastSafe = 1;
1513
+ let lastSafe;
1473
1514
  let stopReason = "maximum-tested";
1474
1515
  for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
1475
- const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, fetcher);
1516
+ const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
1476
1517
  stages.push(measured);
1477
- const errorRate = measured.failed / measured.requests;
1478
1518
  const previous = stages.at(-2);
1479
- const throughputRegressed = Boolean(previous && concurrency > 1 && measured.throughputPerSecond < previous.throughputPerSecond * 0.9);
1480
- if (measured.overloaded > 0 || errorRate > maxErrorRate)
1519
+ const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
1520
+ if (measured.overloaded > 0 || measured.errorRate > maxErrorRate)
1481
1521
  stopReason = "errors";
1482
1522
  else if (measured.p95Ms > maxP95Ms)
1483
1523
  stopReason = "latency";
1524
+ else if (measured.cpuUtilizationPercent > maxCpuUtilizationPercent)
1525
+ stopReason = "cpu";
1526
+ else if (measured.memoryUtilizationPercent > maxMemoryUtilizationPercent)
1527
+ stopReason = "memory";
1484
1528
  else if (throughputRegressed)
1485
1529
  stopReason = "throughput-regression";
1486
1530
  else
1487
- lastSafe = concurrency;
1531
+ lastSafe = measured;
1488
1532
  if (stopReason !== "maximum-tested" || concurrency === maxConcurrency)
1489
1533
  break;
1490
1534
  }
1535
+ if (!lastSafe)
1536
+ throw new Error("Capacity calibration found no safe request stage.");
1491
1537
  return {
1492
1538
  endpoint: endpoint.toString(),
1493
- recommendedConcurrency: Math.max(1, Math.floor(lastSafe * headroomRatio)),
1539
+ measuredSustainableRequestsPerSecond: lastSafe.successfulRequestsPerSecond,
1540
+ allowedRequestsPerSecond: Math.max(1, Math.floor(lastSafe.successfulRequestsPerSecond * safetyRatio)),
1541
+ safetyRatio,
1542
+ recommendedConcurrency: Math.max(1, Math.floor(lastSafe.concurrency * safetyRatio)),
1494
1543
  stopReason,
1495
1544
  stages
1496
1545
  };
@@ -1567,8 +1616,11 @@ function capacityCalibration(value, where) {
1567
1616
  "requestsPerWorker",
1568
1617
  "maxP95Ms",
1569
1618
  "maxErrorRate",
1570
- "headroomRatio",
1571
- "requestTimeoutMs"
1619
+ "safetyRatio",
1620
+ "requestTimeoutMs",
1621
+ "minimumStageDurationMs",
1622
+ "maxCpuUtilizationPercent",
1623
+ "maxMemoryUtilizationPercent"
1572
1624
  ], where);
1573
1625
  const endpoint = text(calibration.endpoint, `${where}.endpoint`);
1574
1626
  try {
@@ -1592,8 +1644,11 @@ function capacityCalibration(value, where) {
1592
1644
  "requestsPerWorker",
1593
1645
  "maxP95Ms",
1594
1646
  "maxErrorRate",
1595
- "headroomRatio",
1596
- "requestTimeoutMs"
1647
+ "safetyRatio",
1648
+ "requestTimeoutMs",
1649
+ "minimumStageDurationMs",
1650
+ "maxCpuUtilizationPercent",
1651
+ "maxMemoryUtilizationPercent"
1597
1652
  ].flatMap((name) => {
1598
1653
  const found = optionalNumber(name);
1599
1654
  return found === undefined ? [] : [[name, found]];
@@ -2784,35 +2839,76 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
2784
2839
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
2785
2840
  validateSoftwareRequirements(requirements);
2786
2841
  return new Promise((resolve5, reject) => {
2787
- const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2788
- `));
2789
- let buffer = "";
2790
- socket.setTimeout(timeoutMs, () => {
2791
- socket.destroy();
2792
- reject(new Error("software helper did not answer before its deadline"));
2793
- });
2794
- socket.on("data", (chunk) => {
2795
- buffer += chunk.toString("utf8");
2796
- const newline = buffer.indexOf(`
2797
- `);
2798
- if (newline < 0)
2842
+ const deadline = Date.now() + timeoutMs;
2843
+ let settled = false;
2844
+ const finish = (cause, results) => {
2845
+ if (settled)
2799
2846
  return;
2800
- socket.end();
2847
+ settled = true;
2848
+ if (cause)
2849
+ reject(cause);
2850
+ else
2851
+ resolve5(results);
2852
+ };
2853
+ const attempt = () => {
2854
+ if (settled)
2855
+ return;
2856
+ const remaining = deadline - Date.now();
2857
+ if (remaining <= 0)
2858
+ return finish(new Error("software helper did not answer before its deadline"));
2859
+ if (!existsSync7(socketPath)) {
2860
+ setTimeout(attempt, Math.min(100, Math.max(1, remaining)));
2861
+ return;
2862
+ }
2863
+ let buffer = "";
2864
+ let socket;
2801
2865
  try {
2802
- const response = JSON.parse(buffer.slice(0, newline));
2803
- if (!response.ok || !response.results)
2804
- throw new Error(response.error?.message ?? "software helper refused the request");
2805
- resolve5(response.results);
2866
+ socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
2867
+ `));
2806
2868
  } catch (cause) {
2807
- reject(cause);
2869
+ const error = cause;
2870
+ if ((error.code === "ENOENT" || error.code === "ECONNREFUSED") && Date.now() < deadline) {
2871
+ setTimeout(attempt, Math.min(100, Math.max(1, deadline - Date.now())));
2872
+ return;
2873
+ }
2874
+ finish(error);
2875
+ return;
2808
2876
  }
2809
- });
2810
- socket.on("error", reject);
2877
+ socket.setTimeout(remaining, () => {
2878
+ socket.destroy();
2879
+ finish(new Error("software helper did not answer before its deadline"));
2880
+ });
2881
+ socket.on("data", (chunk) => {
2882
+ buffer += chunk.toString("utf8");
2883
+ const newline = buffer.indexOf(`
2884
+ `);
2885
+ if (newline < 0)
2886
+ return;
2887
+ socket.end();
2888
+ try {
2889
+ const response = JSON.parse(buffer.slice(0, newline));
2890
+ if (!response.ok || !response.results)
2891
+ throw new Error(response.error?.message ?? "software helper refused the request");
2892
+ finish(undefined, response.results);
2893
+ } catch (cause) {
2894
+ finish(cause);
2895
+ }
2896
+ });
2897
+ socket.on("error", (cause) => {
2898
+ socket.destroy();
2899
+ if ((cause.code === "ENOENT" || cause.code === "ECONNREFUSED") && Date.now() < deadline) {
2900
+ setTimeout(attempt, Math.min(100, Math.max(1, deadline - Date.now())));
2901
+ return;
2902
+ }
2903
+ finish(cause);
2904
+ });
2905
+ };
2906
+ attempt();
2811
2907
  });
2812
2908
  }
2813
2909
 
2814
2910
  // src/version.ts
2815
- var VERSION3 = "0.1.88";
2911
+ var VERSION3 = "0.1.89";
2816
2912
 
2817
2913
  // src/egress-policy.ts
2818
2914
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -2948,6 +3044,7 @@ function agentEgressUnit(options) {
2948
3044
  }
2949
3045
  const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
2950
3046
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
3047
+ const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
2951
3048
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
2952
3049
  if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
2953
3050
  throw new Error("deployed project runner needs at least one vetted public TCP port");
@@ -2955,8 +3052,10 @@ function agentEgressUnit(options) {
2955
3052
  if (deploymentEnabled)
2956
3053
  systemdAgentEgressDirectives(runnerLoopbackPorts);
2957
3054
  const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
2958
- const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
2959
- const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}`;
3055
+ const portGrantEnabled = deploymentEnabled || agentLoopbackPorts.length > 0;
3056
+ const restrictedUser = deploymentEnabled ? DEPLOYMENT_RUNNER_USER : user;
3057
+ 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("") : "") : "";
3058
+ const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}`;
2960
3059
  return `[Unit]
2961
3060
  Description=ForgeZero Agent host egress policy
2962
3061
  Documentation=https://www.forgezero.net/docs/agent
@@ -2969,7 +3068,7 @@ Type=notify
2969
3068
  NotifyAccess=all
2970
3069
  User=root
2971
3070
  Group=root
2972
- ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
3071
+ ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}
2973
3072
  ${policyProof}
2974
3073
  Restart=on-failure
2975
3074
  RestartSec=2
@@ -3253,7 +3352,7 @@ function agentEnrolmentUnit(options) {
3253
3352
  Requires=forgezero-agent-egress.service
3254
3353
  BindsTo=forgezero-agent-egress.service
3255
3354
  ` : "";
3256
- const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
3355
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
3257
3356
  return `[Unit]
3258
3357
  Description=Bind this machine to its ForgeZero compute
3259
3358
  After=network-online.target
@@ -3290,7 +3389,7 @@ WantedBy=multi-user.target
3290
3389
  function deploymentRunnerUnit(options) {
3291
3390
  const bin = options.binPath ?? "fz-agent";
3292
3391
  const root = options.deployRoot ?? "/opt/forgezero";
3293
- const agentUser = options.user ?? "forgezero-agent";
3392
+ const agentUser = options.user ?? "forgezero";
3294
3393
  const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
3295
3394
  Requires=forgezero-agent-egress.service
3296
3395
  BindsTo=forgezero-agent-egress.service
@@ -3331,7 +3430,7 @@ RestrictRealtime=true
3331
3430
  MemoryDenyWriteExecute=true
3332
3431
  LockPersonality=true
3333
3432
  ${egressDirectives}
3334
- ReadWritePaths=${root}/releases ${root}/runner-home
3433
+ ReadWritePaths=${root}/releases ${root}/runner-home ${root}/slots /etc/nginx/conf.d /var/log/nginx
3335
3434
 
3336
3435
  [Install]
3337
3436
  WantedBy=multi-user.target
@@ -3466,7 +3565,7 @@ function agentUnit(options) {
3466
3565
  const supplementaryGroups = [
3467
3566
  AGENT_UPDATE_GROUP,
3468
3567
  deploymentEnabled ? DEPLOYMENT_GROUP : null,
3469
- deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
3568
+ SOFTWARE_HELPER_GROUP,
3470
3569
  lifecycleEnabled ? LIFECYCLE_GROUP : null
3471
3570
  ].filter((value) => value !== null);
3472
3571
  const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
@@ -3475,7 +3574,7 @@ function agentUnit(options) {
3475
3574
  "forgezero-agent-update-helper.service",
3476
3575
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
3477
3576
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
3478
- deploymentEnabled ? "forgezero-software-helper.service" : null,
3577
+ "forgezero-software-helper.service",
3479
3578
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
3480
3579
  warpEnabled ? "warp-svc.service" : null,
3481
3580
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -3484,7 +3583,7 @@ function agentUnit(options) {
3484
3583
  "forgezero-agent-update-helper.service",
3485
3584
  options.enforceEgress ? "forgezero-agent-egress.service" : null,
3486
3585
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
3487
- deploymentEnabled ? "forgezero-software-helper.service" : null,
3586
+ "forgezero-software-helper.service",
3488
3587
  lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
3489
3588
  warpEnabled ? "warp-svc.service" : null,
3490
3589
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
@@ -3499,9 +3598,9 @@ function agentUnit(options) {
3499
3598
  const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
3500
3599
  DeviceAllow=/dev/sev-guest rw` : "";
3501
3600
  const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
3502
- ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
3601
+ ExecStartPre=+/bin/chmod 0660 /dev/sev-guest
3503
3602
  ` : "";
3504
- const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
3603
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
3505
3604
  return `[Unit]
3506
3605
  Description=ForgeZero node agent (${options.mode})
3507
3606
  Documentation=https://www.forgezero.net/docs/agent
@@ -3632,6 +3731,7 @@ function planProvision(options) {
3632
3731
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
3633
3732
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
3634
3733
  const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
3734
+ const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
3635
3735
  const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
3636
3736
  const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
3637
3737
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
@@ -3680,8 +3780,9 @@ function planProvision(options) {
3680
3780
  const enabledUnits = [
3681
3781
  "forgezero-agent.socket",
3682
3782
  "forgezero-agent-update-helper.service",
3783
+ "forgezero-software-helper.service",
3683
3784
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
3684
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
3785
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
3685
3786
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
3686
3787
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
3687
3788
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
@@ -3689,8 +3790,9 @@ function planProvision(options) {
3689
3790
  ];
3690
3791
  const restartedUnits = [
3691
3792
  "forgezero-agent-update-helper.service",
3793
+ "forgezero-software-helper.service",
3692
3794
  ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
3693
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
3795
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
3694
3796
  ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
3695
3797
  ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
3696
3798
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
@@ -3714,9 +3816,9 @@ function planProvision(options) {
3714
3816
  ...options.enforceEgress ? [
3715
3817
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
3716
3818
  ] : [],
3819
+ { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) },
3717
3820
  ...deploymentEnabled ? [
3718
- { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
3719
- { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
3821
+ { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
3720
3822
  ] : [],
3721
3823
  ...enrolmentEnabled ? [
3722
3824
  { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
@@ -3749,24 +3851,29 @@ function planProvision(options) {
3749
3851
  step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
3750
3852
  ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION3 })] : [],
3751
3853
  ...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
3752
- ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
3753
- { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
3854
+ step("software strategy helper group", { kind: "commands", commands: [
3754
3855
  { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
3856
+ ] }),
3857
+ ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
3858
+ { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] }
3755
3859
  ] })] : [],
3756
3860
  ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
3757
3861
  step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
3758
3862
  step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
3759
3863
  step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
3864
+ step("grant software helper socket access", { kind: "commands", commands: [
3865
+ { argv: ["/usr/sbin/usermod", "-a", "-G", SOFTWARE_HELPER_GROUP, user] }
3866
+ ] }),
3760
3867
  ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
3761
3868
  ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
3762
3869
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
3763
3870
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
3764
3871
  { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
3765
- { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
3872
+ { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, user] }
3766
3873
  ] })] : [],
3767
3874
  step("credential and state directories", { kind: "directories", directories: [
3768
3875
  { path: credentialDir, mode: 448, owner: "root", group: "root" },
3769
- { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
3876
+ { path: "/var/lib/forgezero", mode: 488, owner: "root", group: VAULT_GROUP }
3770
3877
  ] }),
3771
3878
  step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
3772
3879
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
@@ -3817,19 +3924,26 @@ function planProvision(options) {
3817
3924
  { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
3818
3925
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
3819
3926
  ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
3927
+ { argv: ["/usr/bin/systemctl", "stop", "forgezero-agent-proxy.service"], acceptedExitCodes: [0, 1, 5] },
3820
3928
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
3821
3929
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
3822
3930
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
3823
3931
  ] }),
3824
3932
  ...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
3825
- ...options.enforceEgress ? [step("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
3933
+ ...options.enforceEgress ? [step("prove the Agent egress policy is active", {
3934
+ kind: "verify-egress",
3935
+ deploymentEnabled,
3936
+ runnerPublicTcpPorts,
3937
+ runnerLoopbackPorts,
3938
+ agentLoopbackPorts
3939
+ })] : [],
3826
3940
  step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
3827
3941
  step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
3828
3942
  step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
3829
3943
  step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
3944
+ step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 }),
3830
3945
  ...deploymentEnabled ? [
3831
- step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
3832
- step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
3946
+ step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
3833
3947
  ] : [],
3834
3948
  ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
3835
3949
  ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
@@ -4350,12 +4464,26 @@ map $limit_conn_status $forgezero_retry_after { default ''; REJECTED 1; REJECTED
4350
4464
  server {
4351
4465
  listen 127.0.0.1:${input.publicPort};
4352
4466
  server_name _;
4467
+ server_tokens off;
4468
+ more_clear_headers Server X-Powered-By;
4353
4469
  limit_conn forgezero_admission ${concurrencyLimit};
4354
4470
  limit_conn_status 503;
4355
4471
  add_header Retry-After $forgezero_retry_after always;
4356
- 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; }
4357
- 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; }
4358
- location / { return 404; }
4472
+ error_page 400 403 404 405 408 413 414 429 500 502 503 504 = @transport_failure;
4473
+ location @transport_failure { internal; return 444; }
4474
+ location / {
4475
+ proxy_pass http://forgezero;
4476
+ proxy_http_version 1.1;
4477
+ proxy_intercept_errors off;
4478
+ proxy_hide_header Server;
4479
+ proxy_hide_header X-Powered-By;
4480
+ proxy_set_header Host $host;
4481
+ proxy_set_header X-Forwarded-Proto https;
4482
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4483
+ proxy_set_header Upgrade $http_upgrade;
4484
+ proxy_set_header Connection $forgezero_connection;
4485
+ proxy_read_timeout 3600s;
4486
+ }
4359
4487
  }
4360
4488
  `
4361
4489
  };
@@ -4428,11 +4556,29 @@ var replaceLink = (path2, target) => {
4428
4556
  symlinkSync4(target, pending);
4429
4557
  renameSync8(pending, path2);
4430
4558
  };
4559
+ var assertReleaseLinksContained = (path2) => {
4560
+ const root = realpathSync4(path2);
4561
+ const visit = (current2) => {
4562
+ const metadata = lstatSync(current2);
4563
+ if (metadata.isSymbolicLink()) {
4564
+ const target = realpathSync4(current2);
4565
+ if (target !== root && !target.startsWith(`${root}/`)) {
4566
+ throw new Error("release contains a symbolic link outside its immutable root");
4567
+ }
4568
+ return;
4569
+ }
4570
+ if (metadata.isDirectory())
4571
+ for (const name of readdirSync3(current2))
4572
+ visit(join5(current2, name));
4573
+ };
4574
+ visit(root);
4575
+ };
4431
4576
  var secureRelease = (path2, uid, gid) => {
4577
+ assertReleaseLinksContained(path2);
4432
4578
  const visit = (current2) => {
4433
4579
  const metadata = lstatSync(current2);
4434
4580
  if (metadata.isSymbolicLink())
4435
- throw new Error("release contains a symbolic link");
4581
+ return;
4436
4582
  chownSync(current2, uid, gid);
4437
4583
  chmodSync6(current2, metadata.isDirectory() ? 365 : 292);
4438
4584
  if (metadata.isDirectory())
@@ -4458,7 +4604,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4458
4604
  const sleep = options.sleep ?? Bun.sleep;
4459
4605
  const slots = join5(normalized.root, "slots");
4460
4606
  mkdirSync8(slots, { recursive: true, mode: 493 });
4461
- const slotFile = join5(normalized.root, ".forge-slot");
4607
+ const slotFile = join5(slots, ".active");
4462
4608
  const previousSlot = existsSync8(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
4463
4609
  const target = previousSlot === "blue" ? "green" : "blue";
4464
4610
  const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
@@ -4508,6 +4654,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4508
4654
  const test = await exec(["/usr/sbin/nginx", "-t"]);
4509
4655
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
4510
4656
  if (reload.exitCode !== 0) {
4657
+ const refusal = (reload.output || test.output).trim().slice(0, 2000);
4511
4658
  if (existsSync8(backup))
4512
4659
  renameSync8(backup, upstream);
4513
4660
  else
@@ -4515,7 +4662,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4515
4662
  await exec(["/usr/sbin/nginx", "-t"]);
4516
4663
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
4517
4664
  await stopTarget();
4518
- throw new Error("nginx refused the promoted upstream");
4665
+ throw new Error(`nginx refused the promoted upstream${refusal ? `: ${refusal}` : ""}`);
4519
4666
  }
4520
4667
  rmSync7(backup, { force: true });
4521
4668
  writeFileSync7(slotFile, `${target}
@@ -4601,6 +4748,25 @@ import {
4601
4748
  writeFileSync as writeFileSync8
4602
4749
  } from "node:fs";
4603
4750
  import { dirname as dirname9 } from "node:path";
4751
+ function normalizeEd25519PublicKey(output) {
4752
+ const line = output.trim();
4753
+ if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}(?: [^\r\n]{1,256})?$/.test(line))
4754
+ return;
4755
+ const [algorithm, encoded] = line.split(" ", 3);
4756
+ if (algorithm !== "ssh-ed25519" || !encoded)
4757
+ return;
4758
+ let blob;
4759
+ try {
4760
+ blob = Buffer.from(encoded, "base64");
4761
+ } catch {
4762
+ return;
4763
+ }
4764
+ if (blob.length !== 51 || blob.readUInt32BE(0) !== 11 || blob.subarray(4, 15).toString("ascii") !== "ssh-ed25519" || blob.readUInt32BE(15) !== 32)
4765
+ return;
4766
+ if (blob.toString("base64") !== encoded)
4767
+ return;
4768
+ return `${algorithm} ${encoded}`;
4769
+ }
4604
4770
  async function readCapabilities(run2) {
4605
4771
  const answers = {};
4606
4772
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -4740,7 +4906,7 @@ var runProvisionOperation = async (operation) => {
4740
4906
  return result;
4741
4907
  }
4742
4908
  result = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
4743
- if (result.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(result.stdout)) {
4909
+ if (result.exitCode !== 0 || !normalizeEd25519PublicKey(result.stdout)) {
4744
4910
  return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
4745
4911
  }
4746
4912
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
@@ -4756,11 +4922,12 @@ var runProvisionOperation = async (operation) => {
4756
4922
  chmodSync7(key, 384);
4757
4923
  }
4758
4924
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
4759
- if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
4925
+ const publicKey2 = normalizeEd25519PublicKey(derived.stdout);
4926
+ if (derived.exitCode !== 0 || !publicKey2) {
4760
4927
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
4761
4928
  }
4762
4929
  mkdirSync9(dirname9(operation.publicKey), { recursive: true, mode: 493 });
4763
- writeFileSync8(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
4930
+ writeFileSync8(operation.publicKey, `${publicKey2} forgezero-bootstrap-runner
4764
4931
  `, { mode: 292 });
4765
4932
  chmodSync7(operation.publicKey, 292);
4766
4933
  }
@@ -4803,8 +4970,9 @@ var runProvisionOperation = async (operation) => {
4803
4970
  const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
4804
4971
  const required = [
4805
4972
  "forgezero-agent-egress-v1",
4806
- `public-tcp=${operation.runnerPublicTcpPorts.join(",")}`,
4807
- ...operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
4973
+ ...operation.agentLoopbackPorts.length ? [`shared-loopback=${operation.agentLoopbackPorts.join(",")}`] : [],
4974
+ ...operation.deploymentEnabled && operation.runnerPublicTcpPorts.length ? [`public-tcp=${operation.runnerPublicTcpPorts.join(",")}`] : [],
4975
+ ...operation.deploymentEnabled && operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
4808
4976
  ];
4809
4977
  return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
4810
4978
  }
@@ -4870,7 +5038,9 @@ async function applyPlan(plan, run2) {
4870
5038
  const result = await run2(step2.operation);
4871
5039
  transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
4872
5040
  if (result.exitCode !== 0 && !step2.optional) {
4873
- throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
5041
+ const safeDetail = step2.operation.kind === "ensure-bootstrap-ssh-identity" ? result.stdout.trim().slice(0, 1000) : "";
5042
+ throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}${safeDetail ? `
5043
+ ${safeDetail}` : ""}`);
4874
5044
  }
4875
5045
  }
4876
5046
  return transcript;
@@ -4960,7 +5130,6 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
4960
5130
  var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
4961
5131
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
4962
5132
  var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
4963
- var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
4964
5133
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
4965
5134
  var CONTROL_SOCKET = "/run/forgezero/control.sock";
4966
5135
  var CLOUDFLARED_METRICS_ADDRESS = "127.0.0.1:20241";
@@ -5179,6 +5348,8 @@ function planBootstrapAgentInstall(config, phase, context) {
5179
5348
  bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
5180
5349
  lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
5181
5350
  enforceEgress: true,
5351
+ runnerLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort, config.runtime.bluePort, config.runtime.greenPort] : [],
5352
+ agentLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort] : [],
5182
5353
  nodeHostname: config.nodeHostname,
5183
5354
  telemetryEndpoint: config.telemetryEndpoint,
5184
5355
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
@@ -5188,7 +5359,7 @@ function planBootstrapAgentInstall(config, phase, context) {
5188
5359
  enrolStatePath: "/var/lib/forgezero/enrolment.json"
5189
5360
  } : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
5190
5361
  ...authenticated ? {
5191
- apiUrl: config.apiUrl,
5362
+ apiUrl: config.kind === "platform" ? `http://127.0.0.1:${config.runtime.environment.publicApiPort}` : config.apiUrl,
5192
5363
  project: config.kind === "enrolled-compute" ? config.realm : "platform",
5193
5364
  environment: config.kind === "enrolled-compute" ? undefined : config.environment,
5194
5365
  nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
@@ -5197,6 +5368,7 @@ function planBootstrapAgentInstall(config, phase, context) {
5197
5368
  return planInstall(options);
5198
5369
  }
5199
5370
  function localBootstrapHost() {
5371
+ let softwareClientUser;
5200
5372
  const execute = async (argv2, options = {}) => {
5201
5373
  const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
5202
5374
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -5229,10 +5401,12 @@ function localBootstrapHost() {
5229
5401
  exec: execute,
5230
5402
  sleep: (milliseconds) => Bun.sleep(milliseconds),
5231
5403
  async ensureSoftware(requirements) {
5404
+ if (!softwareClientUser)
5405
+ throw new Error("Agent must be converged before software requirements are applied");
5232
5406
  const result = await execute([
5233
5407
  "runuser",
5234
5408
  "-u",
5235
- "forgezero-agent",
5409
+ softwareClientUser,
5236
5410
  "--",
5237
5411
  "/usr/local/bin/fz-agent",
5238
5412
  "software-ensure",
@@ -5246,7 +5420,7 @@ function localBootstrapHost() {
5246
5420
  const capabilities = await readCapabilities(localRunner);
5247
5421
  const hasBinding = existsSync11("/var/lib/forgezero/enrolment.json");
5248
5422
  const hasEnrolCredential = existsSync11(ENROL_CREDENTIAL);
5249
- const initialBundle = config.kind === "platform" && !existsSync11(BOOTSTRAP_RELEASE_EVIDENCE) ? {
5423
+ const initialBundle = config.kind === "platform" && phase === "bootstrap" && existsSync11(config.bootstrapBundle.bundleFile) && existsSync11(config.bootstrapBundle.manifestFile) ? {
5250
5424
  path: config.bootstrapBundle.bundleFile,
5251
5425
  manifestPath: config.bootstrapBundle.manifestFile,
5252
5426
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
@@ -5277,6 +5451,7 @@ function localBootstrapHost() {
5277
5451
  writeFileSync10(unit2.path, unit2.unit, { mode: 420 });
5278
5452
  }
5279
5453
  await applyPlan(plan, localRunner);
5454
+ softwareClientUser = plan.user;
5280
5455
  return plan;
5281
5456
  }
5282
5457
  };