@forgezero/agent 0.1.159 → 0.1.161

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.
@@ -3081,7 +3081,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3081
3081
  }
3082
3082
 
3083
3083
  // src/version.ts
3084
- var VERSION3 = "0.1.159";
3084
+ var VERSION3 = "0.1.161";
3085
3085
 
3086
3086
  // src/egress-policy.ts
3087
3087
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -4712,12 +4712,23 @@ function renderPlatformActivationFiles(input) {
4712
4712
  throw new Error("Activation root must be absolute and single-line.");
4713
4713
  if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
4714
4714
  throw new Error("Invalid activation service user.");
4715
+ boundedInteger("publicPort", input.publicPort, 1024, 65535);
4715
4716
  boundedInteger("bluePort", input.bluePort, 1024, 65535);
4716
4717
  boundedInteger("greenPort", input.greenPort, 1024, 65535);
4717
- if (input.bluePort === input.greenPort)
4718
- throw new Error("Activation slot ports must differ.");
4719
- if (!/^[A-Za-z0-9.-]{1,253}$/.test(input.seedMeshAddress) || input.seedMeshAddress.startsWith(".") || input.seedMeshAddress.endsWith(".")) {
4720
- throw new Error("Activation seed mesh address is malformed.");
4718
+ if (new Set([input.publicPort, input.bluePort, input.greenPort]).size !== 3) {
4719
+ throw new Error("Activation public and slot ports must differ.");
4720
+ }
4721
+ const runtimeHandoff = input.runtimeHandoff;
4722
+ if (!/^[A-Za-z0-9.-]{1,253}$/.test(runtimeHandoff.address) || runtimeHandoff.address.startsWith(".") || runtimeHandoff.address.endsWith(".")) {
4723
+ throw new Error("Runtime handoff address is malformed.");
4724
+ }
4725
+ for (const [name, path2] of Object.entries({
4726
+ residentPath: runtimeHandoff.residentPath,
4727
+ readyPath: runtimeHandoff.readyPath
4728
+ })) {
4729
+ if (!/^\/[A-Za-z0-9/_.-]{1,192}$/.test(path2) || path2.includes("..")) {
4730
+ throw new Error(`Runtime handoff ${name} is malformed.`);
4731
+ }
4721
4732
  }
4722
4733
  boundedInteger("keepReleases", input.keepReleases, 2, 100);
4723
4734
  const drainDeadlineMs = boundedInteger("drainDeadlineMs", input.drainDeadlineMs ?? 35000, 1000, 300000);
@@ -4727,16 +4738,17 @@ function renderPlatformActivationFiles(input) {
4727
4738
  const environment = [
4728
4739
  `FZ_DIR=${input.root}`,
4729
4740
  `FZ_USER=${input.serviceUser}`,
4741
+ `FZ_PUBLIC_PORT=${input.publicPort}`,
4730
4742
  `FZ_BLUE_PORT=${input.bluePort}`,
4731
4743
  `FZ_GREEN_PORT=${input.greenPort}`,
4732
- `FZ_SEED_MESH_ADDRESS=${input.seedMeshAddress}`,
4744
+ `FZ_RUNTIME_HANDOFF_ADDRESS=${runtimeHandoff.address}`,
4733
4745
  `FZ_HEALTH_PATH=${input.healthPath}`,
4734
4746
  `FZ_KEEP_RELEASES=${input.keepReleases}`,
4735
4747
  `FZ_DRAIN_DEADLINE_MS=${drainDeadlineMs}`
4736
4748
  ].join(`
4737
4749
  `) + `
4738
4750
  `;
4739
- const helper = `${JSON.stringify({ ...input, drainDeadlineMs }, null, 2)}
4751
+ const helper = `${JSON.stringify({ ...input, runtimeHandoff, drainDeadlineMs }, null, 2)}
4740
4752
  `;
4741
4753
  return {
4742
4754
  environment,
@@ -4814,27 +4826,66 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4814
4826
  mkdirSync8(slots, { recursive: true, mode: 493 });
4815
4827
  const slotFile = join5(slots, ".active");
4816
4828
  let previousSlot = existsSync8(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
4817
- if (previousSlot !== "blue" && previousSlot !== "green") {
4829
+ if (previousSlot !== "blue" && previousSlot !== "green")
4818
4830
  previousSlot = undefined;
4819
- for (const candidate of ["blue", "green"]) {
4820
- const active = await exec(["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${candidate}.service`]);
4821
- if (active.exitCode === 0) {
4822
- previousSlot = candidate;
4823
- break;
4824
- }
4831
+ const slotPort = (slot) => slot === "blue" ? normalized.bluePort : normalized.greenPort;
4832
+ const healthy = async (port2) => {
4833
+ try {
4834
+ const response = await request(`http://127.0.0.1:${port2}${normalized.healthPath}`, {
4835
+ signal: AbortSignal.timeout(2000)
4836
+ });
4837
+ return response.ok;
4838
+ } catch {
4839
+ return false;
4840
+ }
4841
+ };
4842
+ const runningHealthySlots = [];
4843
+ for (const candidate of ["blue", "green"]) {
4844
+ const active = await exec(["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${candidate}.service`]);
4845
+ if (active.exitCode === 0 && await healthy(slotPort(candidate)))
4846
+ runningHealthySlots.push(candidate);
4847
+ }
4848
+ if (previousSlot && !runningHealthySlots.includes(previousSlot)) {
4849
+ throw new Error("active slot evidence disagrees with the running healthy API slot");
4850
+ }
4851
+ if (!previousSlot) {
4852
+ if (runningHealthySlots.length !== 1) {
4853
+ throw new Error("exactly one running healthy API slot is required before promotion");
4825
4854
  }
4855
+ previousSlot = runningHealthySlots[0];
4826
4856
  }
4827
4857
  const target = previousSlot === "blue" ? "green" : "blue";
4828
- const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
4829
- const meshProbe = async (slotPort, path2) => {
4858
+ const previousPort = slotPort(previousSlot);
4859
+ const port = slotPort(target);
4860
+ const handoffProbe = async (slotPort2, path2) => {
4830
4861
  try {
4831
- const response = await request(`http://${normalized.seedMeshAddress}:${slotPort}/_internal/forgezero/seed-mesh/${path2}`, { signal: AbortSignal.timeout(2000) });
4862
+ const response = await request(`http://${normalized.runtimeHandoff.address}:${slotPort2}${path2}`, { signal: AbortSignal.timeout(2000) });
4832
4863
  return response.ok;
4833
4864
  } catch {
4834
4865
  return false;
4835
4866
  }
4836
4867
  };
4837
- const previousHeldSeed = previousSlot ? await meshProbe(previousSlot === "blue" ? normalized.bluePort : normalized.greenPort, "resident") : false;
4868
+ const upstream = options.upstreamPath ?? "/etc/nginx/conf.d/forgezero-upstream.conf";
4869
+ const renderUpstream = (upstreamPort) => `upstream forgezero { server 127.0.0.1:${upstreamPort}; }
4870
+ `;
4871
+ const reloadNginx = async (upstreamPort) => {
4872
+ const pending = `${upstream}.next`;
4873
+ writeFileSync7(pending, renderUpstream(upstreamPort), { mode: 420, flush: true });
4874
+ renameSync8(pending, upstream);
4875
+ const test = await exec(["/usr/sbin/nginx", "-t"]);
4876
+ const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
4877
+ if (reload.exitCode !== 0) {
4878
+ const refusal = (reload.output || test.output).trim().slice(0, 2000);
4879
+ throw new Error(`nginx refused the active-slot upstream${refusal ? `: ${refusal}` : ""}`);
4880
+ }
4881
+ };
4882
+ if (!existsSync8(upstream) || readFileSync6(upstream, "utf8") !== renderUpstream(previousPort)) {
4883
+ await reloadNginx(previousPort);
4884
+ }
4885
+ if (!await healthy(normalized.publicPort)) {
4886
+ throw new Error("stable public listener does not reach the active API slot");
4887
+ }
4888
+ const previousHeldSeed = previousSlot ? await handoffProbe(previousSlot === "blue" ? normalized.bluePort : normalized.greenPort, normalized.runtimeHandoff.residentPath) : false;
4838
4889
  if (previousSlot && !previousHeldSeed) {
4839
4890
  throw new Error("active slot has no resident Vault seed; unlock custody before promotion");
4840
4891
  }
@@ -4855,13 +4906,10 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4855
4906
  replaceLink(targetLink, previousTarget);
4856
4907
  throw new Error("target slot failed to start");
4857
4908
  }
4858
- let healthy = false;
4909
+ let targetHealthy = false;
4859
4910
  for (let attempt = 0;attempt < 30; attempt += 1) {
4860
- try {
4861
- const response = await request(`http://127.0.0.1:${port}${normalized.healthPath}`, { signal: AbortSignal.timeout(2000) });
4862
- healthy = response.ok;
4863
- } catch {}
4864
- if (healthy)
4911
+ targetHealthy = await healthy(port);
4912
+ if (targetHealthy)
4865
4913
  break;
4866
4914
  await sleep(1000);
4867
4915
  }
@@ -4871,14 +4919,18 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4871
4919
  if (previousSlot)
4872
4920
  await exec(["/usr/bin/systemctl", "start", `forgezero@${previousSlot}.service`]);
4873
4921
  };
4874
- if (!healthy) {
4922
+ if (!targetHealthy) {
4875
4923
  await stopTarget();
4876
4924
  throw new Error("target slot failed its health deadline");
4877
4925
  }
4878
4926
  if (previousHeldSeed) {
4879
4927
  let seedReady = false;
4880
4928
  for (let attempt = 0;attempt < 30; attempt += 1) {
4881
- seedReady = await meshProbe(port, "ready");
4929
+ const [resident, converged] = await Promise.all([
4930
+ handoffProbe(port, normalized.runtimeHandoff.residentPath),
4931
+ handoffProbe(port, normalized.runtimeHandoff.readyPath)
4932
+ ]);
4933
+ seedReady = resident && converged;
4882
4934
  if (seedReady)
4883
4935
  break;
4884
4936
  await sleep(1000);
@@ -4888,18 +4940,14 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4888
4940
  throw new Error("target slot failed RAM-seed handover deadline");
4889
4941
  }
4890
4942
  }
4891
- const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
4892
4943
  const backup = `${upstream}.forgezero-backup`;
4893
4944
  if (existsSync8(upstream))
4894
4945
  copyFileSync2(upstream, backup);
4895
4946
  else
4896
4947
  rmSync7(backup, { force: true });
4897
- writeFileSync7(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
4898
- `, { mode: 420 });
4899
- const test = await exec(["/usr/sbin/nginx", "-t"]);
4900
- const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
4901
- if (reload.exitCode !== 0) {
4902
- const refusal = (reload.output || test.output).trim().slice(0, 2000);
4948
+ try {
4949
+ await reloadNginx(port);
4950
+ } catch (cause) {
4903
4951
  if (existsSync8(backup))
4904
4952
  renameSync8(backup, upstream);
4905
4953
  else
@@ -4907,14 +4955,29 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
4907
4955
  await exec(["/usr/sbin/nginx", "-t"]);
4908
4956
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
4909
4957
  await stopTarget();
4910
- throw new Error(`nginx refused the promoted upstream${refusal ? `: ${refusal}` : ""}`);
4958
+ throw cause;
4959
+ }
4960
+ if (!await healthy(normalized.publicPort)) {
4961
+ if (existsSync8(backup))
4962
+ renameSync8(backup, upstream);
4963
+ else
4964
+ rmSync7(upstream, { force: true });
4965
+ await exec(["/usr/sbin/nginx", "-t"]);
4966
+ await exec(["/usr/sbin/nginx", "-s", "reload"]);
4967
+ await stopTarget();
4968
+ throw new Error("stable public listener failed after the slot switch");
4911
4969
  }
4912
4970
  rmSync7(backup, { force: true });
4913
4971
  writeFileSync7(slotFile, `${target}
4914
4972
  `, { mode: 420 });
4915
4973
  if (previousSlot && previousSlot !== target) {
4916
- await sleep(normalized.drainDeadlineMs);
4917
- await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
4974
+ const previousService = `forgezero@${previousSlot}.service`;
4975
+ const stopped = await exec(["/usr/bin/systemctl", "stop", previousService]);
4976
+ const inactive = await exec(["/usr/bin/systemctl", "is-active", "--quiet", previousService]);
4977
+ const status = await exec(["/usr/bin/systemctl", "show", "--property=ExecMainStatus", "--value", previousService]);
4978
+ if (stopped.exitCode !== 0 || inactive.exitCode === 0 || status.exitCode !== 0 || status.output.trim() !== "0") {
4979
+ throw new Error("old API slot failed its clean request and job drain; target remains active");
4980
+ }
4918
4981
  }
4919
4982
  const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs).slice(normalized.keepReleases);
4920
4983
  for (const path2 of old)
@@ -5506,6 +5569,50 @@ async function bootstrapStatus(host = localBootstrapHost()) {
5506
5569
  services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
5507
5570
  if (!services["forgezero@active.service"])
5508
5571
  problems.push("neither API slot is active");
5572
+ const activeSlots = [
5573
+ ...blue.exitCode === 0 ? ["blue"] : [],
5574
+ ...green.exitCode === 0 ? ["green"] : []
5575
+ ];
5576
+ const activeSlotFile = state.slotsDirectory ? `${state.slotsDirectory}/.active` : "";
5577
+ const recordedSlot = activeSlotFile && host.exists(activeSlotFile) ? host.read(activeSlotFile).trim() : "";
5578
+ const coordinatesValid = Number.isSafeInteger(state.bluePort) && Number.isSafeInteger(state.greenPort) && Boolean(state.slotsDirectory) && Boolean(state.seedMeshAddress) && Boolean(state.healthPath) && Boolean(state.runtimeHandoffResidentPath) && Boolean(state.runtimeHandoffReadyPath);
5579
+ services["platform-active-slot-consistent"] = coordinatesValid && activeSlots.length === 1 && (recordedSlot === "blue" || recordedSlot === "green") && activeSlots[0] === recordedSlot;
5580
+ if (!services["platform-active-slot-consistent"]) {
5581
+ problems.push("active slot marker, service state, or runtime coordinates disagree");
5582
+ }
5583
+ if (services["platform-active-slot-consistent"]) {
5584
+ const slotPort = recordedSlot === "blue" ? state.bluePort : state.greenPort;
5585
+ const directHealth = await host.exec([
5586
+ "curl",
5587
+ "--fail",
5588
+ "--silent",
5589
+ "--show-error",
5590
+ "--max-time",
5591
+ "5",
5592
+ `http://127.0.0.1:${slotPort}${state.healthPath}`
5593
+ ]);
5594
+ services["platform-active-slot-health"] = directHealth.exitCode === 0;
5595
+ if (directHealth.exitCode !== 0)
5596
+ problems.push("active API slot health check failed");
5597
+ const expectedUpstream = `upstream forgezero { server 127.0.0.1:${slotPort}; }
5598
+ `;
5599
+ const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
5600
+ services["platform-nginx-active-slot"] = host.exists(upstream) && host.read(upstream) === expectedUpstream;
5601
+ if (!services["platform-nginx-active-slot"])
5602
+ problems.push("nginx upstream does not match the active API slot");
5603
+ const resident = await host.exec([
5604
+ "curl",
5605
+ "--fail",
5606
+ "--silent",
5607
+ "--show-error",
5608
+ "--max-time",
5609
+ "5",
5610
+ `http://${state.seedMeshAddress}:${slotPort}${state.runtimeHandoffResidentPath}`
5611
+ ]);
5612
+ services["platform-vault-resident"] = resident.exitCode === 0;
5613
+ if (resident.exitCode !== 0)
5614
+ problems.push("active API slot has no resident Vault seed");
5615
+ }
5509
5616
  if (!Number.isSafeInteger(state.publicApiPort) || !state.healthPath) {
5510
5617
  problems.push("platform health coordinates are missing from bootstrap state");
5511
5618
  } else {
package/dist/provision.js CHANGED
@@ -3081,7 +3081,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3081
3081
  }
3082
3082
 
3083
3083
  // src/version.ts
3084
- var VERSION3 = "0.1.159";
3084
+ var VERSION3 = "0.1.161";
3085
3085
 
3086
3086
  // src/egress-policy.ts
3087
3087
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.159";
2
+ export declare const VERSION = "0.1.161";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",