@forgezero/agent 0.1.86 → 0.1.87

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/fz-agent.js CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { randomBytes as randomBytes7 } from "crypto";
6
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, existsSync as existsSync22, mkdirSync as mkdirSync18, chmodSync as chmodSync18, lstatSync as lstatSync9, statfsSync as statfsSync3 } from "fs";
6
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, existsSync as existsSync23, mkdirSync as mkdirSync18, chmodSync as chmodSync19, lstatSync as lstatSync10, statfsSync as statfsSync3 } from "fs";
7
7
  import { cpus as cpus2, totalmem as totalmem2 } from "os";
8
- import { dirname as dirname14, join as join12 } from "path";
8
+ import { dirname as dirname16, join as join12 } from "path";
9
9
 
10
10
  // ../access/dist/security.js
11
11
  var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
@@ -4201,7 +4201,16 @@ var DEFAULT_SOCKET = "/run/forgezero/vault.sock";
4201
4201
 
4202
4202
  // src/socket.ts
4203
4203
  import { createServer } from "net";
4204
- import { chmodSync, existsSync, unlinkSync } from "fs";
4204
+ import {
4205
+ existsSync,
4206
+ chmodSync,
4207
+ lstatSync,
4208
+ readlinkSync,
4209
+ renameSync,
4210
+ symlinkSync,
4211
+ unlinkSync
4212
+ } from "fs";
4213
+ import { basename, dirname } from "path";
4205
4214
 
4206
4215
  // src/cache.ts
4207
4216
  class CacheError extends Error {
@@ -4495,6 +4504,32 @@ function handleApplicationRequest(options, request) {
4495
4504
  return handleRequest(options, request);
4496
4505
  }
4497
4506
  var MAX_LINE_BYTES = 64 * 1024;
4507
+ function ensureAgentSocketRoute(routePath, activePath) {
4508
+ const target = basename(activePath);
4509
+ if (dirname(routePath) !== dirname(activePath) || target.includes("/") || target === "." || target === "..") {
4510
+ throw new Error("agent socket route and active backend must share one runtime directory");
4511
+ }
4512
+ try {
4513
+ if (lstatSync(routePath).isSymbolicLink()) {
4514
+ const current = readlinkSync(routePath);
4515
+ if (current === target || existsSync(`${dirname(routePath)}/${current}`))
4516
+ return;
4517
+ } else
4518
+ throw new Error("agent socket route must be a symbolic link");
4519
+ } catch (cause) {
4520
+ if (cause.code !== "ENOENT")
4521
+ throw cause;
4522
+ }
4523
+ const next = `${routePath}.${process.pid}.next`;
4524
+ try {
4525
+ symlinkSync(target, next);
4526
+ renameSync(next, routePath);
4527
+ } finally {
4528
+ try {
4529
+ unlinkSync(next);
4530
+ } catch {}
4531
+ }
4532
+ }
4498
4533
  function handleRequest(options, request) {
4499
4534
  switch (request?.op) {
4500
4535
  case "identity":
@@ -4653,9 +4688,9 @@ function safeOp(line) {
4653
4688
  }
4654
4689
 
4655
4690
  // src/deployment.ts
4656
- import { chmodSync as chmodSync4, existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, statfsSync, writeFileSync as writeFileSync3 } from "fs";
4691
+ import { chmodSync as chmodSync4, existsSync as existsSync4, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync4, statfsSync, writeFileSync as writeFileSync3 } from "fs";
4657
4692
  import { createHash as createHash5, randomUUID } from "crypto";
4658
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2 } from "path";
4693
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join2 } from "path";
4659
4694
 
4660
4695
  // ../runtime/dist/queue.js
4661
4696
  class QueueStoppedError extends Error {
@@ -4970,15 +5005,15 @@ import {
4970
5005
  mkdtempSync,
4971
5006
  mkdirSync,
4972
5007
  readFileSync,
4973
- renameSync,
5008
+ renameSync as renameSync2,
4974
5009
  rmSync,
4975
5010
  statSync,
4976
- symlinkSync,
5011
+ symlinkSync as symlinkSync2,
4977
5012
  unlinkSync as unlinkSync2,
4978
5013
  writeFileSync
4979
5014
  } from "fs";
4980
5015
  import { tmpdir } from "os";
4981
- import { dirname, join } from "path";
5016
+ import { dirname as dirname2, join } from "path";
4982
5017
  var PINNED_BUN_VERSION = "1.3.14";
4983
5018
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
4984
5019
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
@@ -5188,7 +5223,7 @@ var writeOwnedPolicy = (pathValue, content, mode = 420) => {
5188
5223
  throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
5189
5224
  return;
5190
5225
  }
5191
- mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
5226
+ mkdirSync(dirname2(pathValue), { recursive: true, mode: 493 });
5192
5227
  writeFileSync(pathValue, content, { mode, flag: "wx" });
5193
5228
  };
5194
5229
  var installContainerdRuntime = async (directory, osVersion) => {
@@ -5266,7 +5301,7 @@ var installKataRuntime = async (directory) => {
5266
5301
  try {
5267
5302
  unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
5268
5303
  } catch {}
5269
- symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
5304
+ symlinkSync2("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
5270
5305
  const base = CONTAINERD_CONFIG(false);
5271
5306
  const kata = CONTAINERD_CONFIG(true);
5272
5307
  if (!existsSync2(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
@@ -5407,11 +5442,11 @@ async function executeSoftwareOperation(operation) {
5407
5442
  mkdirSync("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
5408
5443
  copyFileSync(join(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
5409
5444
  chmodSync2("/usr/local/lib/forgezero/runtime/bun.next", 493);
5410
- renameSync("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
5445
+ renameSync2("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
5411
5446
  try {
5412
5447
  unlinkSync2("/usr/local/bin/bun");
5413
5448
  } catch {}
5414
- symlinkSync("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
5449
+ symlinkSync2("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
5415
5450
  return { exitCode: 0, output: "" };
5416
5451
  }
5417
5452
  if (software === "cloudflared") {
@@ -7778,10 +7813,10 @@ import {
7778
7813
  chmodSync as chmodSync3,
7779
7814
  createReadStream as createReadStream2,
7780
7815
  existsSync as existsSync3,
7781
- lstatSync,
7816
+ lstatSync as lstatSync2,
7782
7817
  mkdirSync as mkdirSync2,
7783
7818
  readFileSync as readFileSync2,
7784
- renameSync as renameSync2,
7819
+ renameSync as renameSync3,
7785
7820
  rmSync as rmSync2,
7786
7821
  writeFileSync as writeFileSync2
7787
7822
  } from "fs";
@@ -7877,13 +7912,13 @@ function persistCapacityCalibration(args) {
7877
7912
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be an absolute Agent-owned path");
7878
7913
  }
7879
7914
  if (!existsSync4(args.directory)) {
7880
- const parent = lstatSync2(dirname2(args.directory));
7915
+ const parent = lstatSync3(dirname3(args.directory));
7881
7916
  if (!parent.isDirectory() || parent.isSymbolicLink()) {
7882
7917
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence parent must be a real Agent-owned directory");
7883
7918
  }
7884
7919
  mkdirSync3(args.directory, { mode: 448 });
7885
7920
  }
7886
- const stats = lstatSync2(args.directory);
7921
+ const stats = lstatSync3(args.directory);
7887
7922
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
7888
7923
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be a real Agent-owned directory");
7889
7924
  }
@@ -7906,7 +7941,7 @@ function persistCapacityCalibration(args) {
7906
7941
  }, null, 2)}
7907
7942
  `, { mode: 384, flag: "wx" });
7908
7943
  chmodSync4(next, 384);
7909
- renameSync3(next, path2);
7944
+ renameSync4(next, path2);
7910
7945
  return path2;
7911
7946
  }
7912
7947
  function createDeploymentManager(options) {
@@ -7941,10 +7976,10 @@ function createDeploymentManager(options) {
7941
7976
  }
7942
7977
  if (existsSync4(knownHostsPath) && readFileSync3(knownHostsPath, "utf8") === content)
7943
7978
  return;
7944
- mkdirSync3(dirname2(knownHostsPath), { recursive: true, mode: 448 });
7979
+ mkdirSync3(dirname3(knownHostsPath), { recursive: true, mode: 448 });
7945
7980
  const next = `${knownHostsPath}.${process.pid}.${randomUUID()}.next`;
7946
7981
  writeFileSync3(next, content, { mode: 384, flag: "wx" });
7947
- renameSync3(next, knownHostsPath);
7982
+ renameSync4(next, knownHostsPath);
7948
7983
  chmodSync4(knownHostsPath, 384);
7949
7984
  };
7950
7985
  const gitBaseEnvironment = () => ({
@@ -8070,7 +8105,7 @@ function createDeploymentManager(options) {
8070
8105
  if (bootstrapBundle) {
8071
8106
  let metadata;
8072
8107
  try {
8073
- metadata = lstatSync2(bootstrapBundle.path);
8108
+ metadata = lstatSync3(bootstrapBundle.path);
8074
8109
  } catch {
8075
8110
  throw new DeploymentError("SOURCE_FAILED", "The attended bootstrap bundle is missing.");
8076
8111
  }
@@ -8358,7 +8393,7 @@ function createDeploymentManager(options) {
8358
8393
  const component2 = plan.spec.components[String(resolved.component ?? "")];
8359
8394
  const mounts = component2?.kind === "database" ? [component2.storage] : component2?.storage ?? [];
8360
8395
  for (const mount of mounts) {
8361
- const candidate = existsSync4(mount.path) ? mount.path : dirname2(mount.path);
8396
+ const candidate = existsSync4(mount.path) ? mount.path : dirname3(mount.path);
8362
8397
  const stats = statfsSync(candidate);
8363
8398
  const freePercent = Number(stats.bavail) / Number(stats.blocks) * 100;
8364
8399
  const minimum = mount.class === "ephemeral" ? 0 : mount.minimumFreePercent ?? 0;
@@ -8934,12 +8969,12 @@ import {
8934
8969
  existsSync as existsSync6,
8935
8970
  mkdirSync as mkdirSync4,
8936
8971
  readFileSync as readFileSync4,
8937
- renameSync as renameSync4,
8972
+ renameSync as renameSync5,
8938
8973
  statSync as statSync2,
8939
8974
  unlinkSync as unlinkSync4,
8940
8975
  writeFileSync as writeFileSync4
8941
8976
  } from "fs";
8942
- import { dirname as dirname3 } from "path";
8977
+ import { dirname as dirname4 } from "path";
8943
8978
  function privateNetworkAttachmentFromEnvironment(env = process.env) {
8944
8979
  const values = {
8945
8980
  accountId: env.FZ_CF_ACCOUNT_ID?.trim() ?? "",
@@ -8985,12 +9020,12 @@ function loadGuestBinding(path2, expectedNodeKey) {
8985
9020
  return parsed;
8986
9021
  }
8987
9022
  function persistGuestBinding(path2, binding) {
8988
- mkdirSync4(dirname3(path2), { recursive: true, mode: 448 });
9023
+ mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
8989
9024
  const temporary = `${path2}.next`;
8990
9025
  writeFileSync4(temporary, `${JSON.stringify(binding)}
8991
9026
  `, { mode: 384 });
8992
9027
  chmodSync6(temporary, 384);
8993
- renameSync4(temporary, path2);
9028
+ renameSync5(temporary, path2);
8994
9029
  }
8995
9030
  async function enrolGuestIdentity(options) {
8996
9031
  const token = options.token?.trim() ?? (options.tokenPath ? readFileSync4(options.tokenPath, "utf8").trim() : "");
@@ -9343,7 +9378,7 @@ async function writeAndCloseProcessInput(input, value) {
9343
9378
  }
9344
9379
 
9345
9380
  // src/version.ts
9346
- var VERSION2 = "0.1.86";
9381
+ var VERSION2 = "0.1.87";
9347
9382
 
9348
9383
  // src/ssh-bootstrap.ts
9349
9384
  class SshBootstrapError extends Error {
@@ -10062,7 +10097,7 @@ import {
10062
10097
  unlinkSync as unlinkSync5,
10063
10098
  writeFileSync as writeFileSync6
10064
10099
  } from "fs";
10065
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4 } from "path";
10100
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join4 } from "path";
10066
10101
  import { isIP as isIP2 } from "net";
10067
10102
 
10068
10103
  // src/compute.ts
@@ -10611,7 +10646,7 @@ async function provisionMetalGuest(profile, claim, exec) {
10611
10646
  }
10612
10647
  const service = `forgezero-guest@${name}.service`;
10613
10648
  const unitPath = join4(profile.unitDir, service);
10614
- mkdirSync5(dirname4(unitPath), { recursive: true });
10649
+ mkdirSync5(dirname5(unitPath), { recursive: true });
10615
10650
  writeFileSync6(unitPath, guestUnit(spec), { mode: 420 });
10616
10651
  await checked2(exec, ["systemctl", "daemon-reload"]);
10617
10652
  if (prior?.phase === "running") {
@@ -11579,7 +11614,7 @@ function requestLifecycleAction(claim, socketPath = DEFAULT_LIFECYCLE_HELPER_SOC
11579
11614
  }
11580
11615
 
11581
11616
  // src/warp-config.ts
11582
- import { chmodSync as chmodSync11, mkdirSync as mkdirSync7, renameSync as renameSync5, symlinkSync as symlinkSync2, unlinkSync as unlinkSync9, writeFileSync as writeFileSync8 } from "fs";
11617
+ import { chmodSync as chmodSync11, mkdirSync as mkdirSync7, renameSync as renameSync6, symlinkSync as symlinkSync3, unlinkSync as unlinkSync9, writeFileSync as writeFileSync8 } from "fs";
11583
11618
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
11584
11619
  function renderWarpMdm(options) {
11585
11620
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.organization))
@@ -11605,14 +11640,14 @@ function materializeWarpMdm(options) {
11605
11640
  const next = `${runtimePath}.next`;
11606
11641
  writeFileSync8(next, renderWarpMdm(options), { mode: 384 });
11607
11642
  chmodSync11(next, 384);
11608
- renameSync5(next, runtimePath);
11643
+ renameSync6(next, runtimePath);
11609
11644
  try {
11610
11645
  unlinkSync9(servicePath);
11611
11646
  } catch (cause) {
11612
11647
  if (cause.code !== "ENOENT")
11613
11648
  throw cause;
11614
11649
  }
11615
- symlinkSync2(runtimePath, servicePath);
11650
+ symlinkSync3(runtimePath, servicePath);
11616
11651
  return { runtimePath, servicePath };
11617
11652
  }
11618
11653
 
@@ -11669,14 +11704,15 @@ import {
11669
11704
  mkdirSync as mkdirSync8,
11670
11705
  openSync as openSync2,
11671
11706
  readFileSync as readFileSync9,
11672
- readlinkSync,
11673
- renameSync as renameSync6,
11707
+ readlinkSync as readlinkSync2,
11708
+ renameSync as renameSync7,
11674
11709
  rmSync as rmSync5,
11675
- symlinkSync as symlinkSync3,
11710
+ symlinkSync as symlinkSync4,
11676
11711
  writeFileSync as writeFileSync9
11677
11712
  } from "fs";
11678
- import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
11713
+ import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
11679
11714
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
11715
+ var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
11680
11716
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
11681
11717
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
11682
11718
  var VERSION3 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
@@ -11697,7 +11733,7 @@ var syncReleaseDirectory = (directory) => {
11697
11733
  join7(directory, "dist", "fz-git-ssh.js"),
11698
11734
  join7(directory, "dist"),
11699
11735
  directory,
11700
- dirname5(directory)
11736
+ dirname6(directory)
11701
11737
  ])
11702
11738
  syncPath(path2);
11703
11739
  };
@@ -11819,19 +11855,19 @@ async function stageAgentRelease(releaseInput, options) {
11819
11855
  args: ["-xOzf", archive, member]
11820
11856
  }, `agent update extraction of ${member}`);
11821
11857
  const destination = join7(unpacked, relative);
11822
- mkdirSync8(dirname5(destination), { recursive: true, mode: 448 });
11858
+ mkdirSync8(dirname6(destination), { recursive: true, mode: 448 });
11823
11859
  writeFileSync9(destination, extracted.output, { mode: 384, flag: "wx" });
11824
11860
  }
11825
11861
  await validateReleaseDirectory(unpacked, release, run2);
11826
11862
  if (!existsSync12(finalDirectory)) {
11827
- renameSync6(unpacked, finalDirectory);
11863
+ renameSync7(unpacked, finalDirectory);
11828
11864
  syncReleaseDirectory(finalDirectory);
11829
11865
  } else
11830
11866
  await validateReleaseDirectory(finalDirectory, release, run2);
11831
11867
  if (!existsSync12(currentLink)) {
11832
11868
  throw new Error("agent update requires an active immutable release to roll back to");
11833
11869
  }
11834
- const previousTarget = readlinkSync(currentLink);
11870
+ const previousTarget = readlinkSync2(currentLink);
11835
11871
  if (previousTarget !== join7("versions", options.currentVersion)) {
11836
11872
  throw new Error("agent update current release does not match the running version");
11837
11873
  }
@@ -11851,43 +11887,116 @@ async function stageAgentRelease(releaseInput, options) {
11851
11887
  }
11852
11888
  }
11853
11889
  function selectAgentRelease(staged) {
11854
- const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.next`);
11890
+ const next = join7(dirname6(staged.currentLink), `.current.${randomUUID2()}.next`);
11855
11891
  try {
11856
- symlinkSync3(staged.nextTarget, next);
11857
- renameSync6(next, staged.currentLink);
11858
- syncPath(dirname5(staged.currentLink));
11892
+ symlinkSync4(staged.nextTarget, next);
11893
+ renameSync7(next, staged.currentLink);
11894
+ syncPath(dirname6(staged.currentLink));
11859
11895
  } finally {
11860
11896
  rmSync5(next, { force: true });
11861
11897
  }
11862
11898
  }
11863
11899
  function restoreAgentRelease(staged) {
11864
- const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.rollback`);
11900
+ const next = join7(dirname6(staged.currentLink), `.current.${randomUUID2()}.rollback`);
11901
+ try {
11902
+ symlinkSync4(staged.previousTarget, next);
11903
+ renameSync7(next, staged.currentLink);
11904
+ syncPath(dirname6(staged.currentLink));
11905
+ } finally {
11906
+ rmSync5(next, { force: true });
11907
+ }
11908
+ }
11909
+ function selectAgentCandidate(staged) {
11910
+ const link = join7(dirname6(staged.currentLink), "candidate");
11911
+ const next = `${link}.${randomUUID2()}.next`;
11865
11912
  try {
11866
- symlinkSync3(staged.previousTarget, next);
11867
- renameSync6(next, staged.currentLink);
11868
- syncPath(dirname5(staged.currentLink));
11913
+ symlinkSync4(staged.nextTarget, next);
11914
+ renameSync7(next, link);
11915
+ syncPath(dirname6(link));
11869
11916
  } finally {
11870
11917
  rmSync5(next, { force: true });
11871
11918
  }
11872
11919
  }
11920
+ function clearAgentCandidate(root = DEFAULT_AGENT_RELEASE_ROOT) {
11921
+ rmSync5(join7(resolve2(root), "candidate"), { force: true });
11922
+ }
11873
11923
 
11874
11924
  // src/agent-update-helper.ts
11875
- import { randomUUID as randomUUID3 } from "crypto";
11925
+ import { randomUUID as randomUUID4 } from "crypto";
11876
11926
  import {
11877
- chmodSync as chmodSync13,
11927
+ chmodSync as chmodSync14,
11878
11928
  closeSync as closeSync3,
11879
- existsSync as existsSync13,
11929
+ existsSync as existsSync14,
11880
11930
  fsyncSync as fsyncSync2,
11881
11931
  mkdirSync as mkdirSync9,
11882
11932
  openSync as openSync3,
11883
11933
  readFileSync as readFileSync10,
11884
- renameSync as renameSync7,
11885
- rmSync as rmSync6,
11886
- unlinkSync as unlinkSync10,
11934
+ renameSync as renameSync9,
11935
+ rmSync as rmSync7,
11936
+ unlinkSync as unlinkSync11,
11887
11937
  writeFileSync as writeFileSync10
11888
11938
  } from "fs";
11939
+ import { connect as connect6, createServer as createServer7 } from "net";
11940
+ import { dirname as dirname8, join as join8, resolve as resolve3 } from "path";
11941
+
11942
+ // src/agent-handover.ts
11943
+ import { randomUUID as randomUUID3 } from "crypto";
11944
+ import { chmodSync as chmodSync13, existsSync as existsSync13, renameSync as renameSync8, rmSync as rmSync6, symlinkSync as symlinkSync5, unlinkSync as unlinkSync10 } from "fs";
11945
+ import { basename as basename2, dirname as dirname7 } from "path";
11889
11946
  import { connect as connect5, createServer as createServer6 } from "net";
11890
- import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
11947
+ var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
11948
+ function switchAgentSocketRoute(route, backend) {
11949
+ if (dirname7(route) !== dirname7(backend))
11950
+ throw new Error("Agent handover sockets must share one runtime directory");
11951
+ const target2 = basename2(backend);
11952
+ const next = `${route}.${randomUUID3()}.next`;
11953
+ try {
11954
+ symlinkSync5(target2, next);
11955
+ renameSync8(next, route);
11956
+ } finally {
11957
+ rmSync6(next, { force: true });
11958
+ }
11959
+ }
11960
+ function startAgentCandidateReady(ready, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET) {
11961
+ if (existsSync13(socketPath))
11962
+ unlinkSync10(socketPath);
11963
+ const server = createServer6((socket) => socket.end(`${JSON.stringify(ready)}
11964
+ `));
11965
+ server.listen(socketPath, () => chmodSync13(socketPath, 432));
11966
+ return server;
11967
+ }
11968
+ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET, timeoutMs = 5000) {
11969
+ return new Promise((resolve3) => {
11970
+ const socket = connect5(socketPath);
11971
+ let buffer = "";
11972
+ let settled = false;
11973
+ const finish = (value) => {
11974
+ if (settled)
11975
+ return;
11976
+ settled = true;
11977
+ clearTimeout(timer);
11978
+ socket.destroy();
11979
+ resolve3(value);
11980
+ };
11981
+ const timer = setTimeout(() => finish(false), timeoutMs);
11982
+ socket.on("data", (chunk) => {
11983
+ buffer += chunk.toString("utf8");
11984
+ const newline = buffer.indexOf(`
11985
+ `);
11986
+ if (newline < 0)
11987
+ return;
11988
+ try {
11989
+ const value = JSON.parse(buffer.slice(0, newline));
11990
+ finish(value.version === expected.version && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].includes(value.vault ?? ""));
11991
+ } catch {
11992
+ finish(false);
11993
+ }
11994
+ });
11995
+ socket.on("error", () => finish(false));
11996
+ });
11997
+ }
11998
+
11999
+ // src/agent-update-helper.ts
11891
12000
  var AGENT_UPDATE_GROUP = "forgezero-update";
11892
12001
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
11893
12002
  var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
@@ -11982,13 +12091,13 @@ function validateJournal(value, root) {
11982
12091
  return journal;
11983
12092
  }
11984
12093
  function readJournal(path2, root) {
11985
- if (!existsSync13(path2))
12094
+ if (!existsSync14(path2))
11986
12095
  return;
11987
12096
  return validateJournal(JSON.parse(readFileSync10(path2, "utf8")), root);
11988
12097
  }
11989
12098
  function writeAtomic(path2, value, mode) {
11990
- mkdirSync9(dirname6(path2), { recursive: true, mode: 493 });
11991
- const next = `${path2}.${randomUUID3()}.next`;
12099
+ mkdirSync9(dirname8(path2), { recursive: true, mode: 493 });
12100
+ const next = `${path2}.${randomUUID4()}.next`;
11992
12101
  let file;
11993
12102
  try {
11994
12103
  file = openSync3(next, "wx", mode);
@@ -11997,8 +12106,8 @@ function writeAtomic(path2, value, mode) {
11997
12106
  fsyncSync2(file);
11998
12107
  closeSync3(file);
11999
12108
  file = undefined;
12000
- renameSync7(next, path2);
12001
- const directory = openSync3(dirname6(path2), "r");
12109
+ renameSync9(next, path2);
12110
+ const directory = openSync3(dirname8(path2), "r");
12002
12111
  try {
12003
12112
  fsyncSync2(directory);
12004
12113
  } finally {
@@ -12007,7 +12116,7 @@ function writeAtomic(path2, value, mode) {
12007
12116
  } finally {
12008
12117
  if (file !== undefined)
12009
12118
  closeSync3(file);
12010
- rmSync6(next, { force: true });
12119
+ rmSync7(next, { force: true });
12011
12120
  }
12012
12121
  }
12013
12122
  var publicReceipt = (journal) => {
@@ -12040,7 +12149,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
12040
12149
  }
12041
12150
  function readAgentUpdateReceipt(path2 = AGENT_UPDATE_RECEIPT) {
12042
12151
  try {
12043
- if (!existsSync13(path2))
12152
+ if (!existsSync14(path2))
12044
12153
  return;
12045
12154
  return validateReceipt(JSON.parse(readFileSync10(path2, "utf8")));
12046
12155
  } catch {
@@ -12080,10 +12189,32 @@ var restartAgent = async (target2, run2) => {
12080
12189
  throw new Error(`systemd could not restart ${service}`);
12081
12190
  }
12082
12191
  };
12192
+ var candidateUnit = (target2) => target2 === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
12193
+ var startCandidate = async (staged, target2, run2, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
12194
+ selectAgentCandidate(staged);
12195
+ if (!await runOk(run2, "/usr/bin/systemctl", ["restart", candidateUnit(target2)])) {
12196
+ clearAgentCandidate(dirname8(staged.currentLink));
12197
+ throw new Error(`systemd could not start ${candidateUnit(target2)}`);
12198
+ }
12199
+ if (!await probe({ version: staged.version })) {
12200
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12201
+ clearAgentCandidate(dirname8(staged.currentLink));
12202
+ throw new Error("the candidate Agent did not prove its identity and durable state");
12203
+ }
12204
+ };
12205
+ var stopCandidate = async (staged, target2, run2) => {
12206
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12207
+ clearAgentCandidate(dirname8(staged.currentLink));
12208
+ };
12209
+ var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
12210
+ route: `${publicSocket}.backend`,
12211
+ active: `${publicSocket}.backend.active`,
12212
+ candidate: `${publicSocket}.backend.candidate`
12213
+ });
12083
12214
  var targetProbe = (target2, run2) => target2 === "compute" ? () => probeAgentSocket() : async () => await runOk(run2, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run2, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
12084
12215
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12085
12216
  return new Promise((resolve4) => {
12086
- const socket = connect5(socketPath);
12217
+ const socket = connect6(socketPath);
12087
12218
  let settled = false;
12088
12219
  let buffer = "";
12089
12220
  const finish = (value) => {
@@ -12116,12 +12247,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12116
12247
  async function activateAgentRelease(staged, options = {}) {
12117
12248
  const run2 = options.run ?? runCommand;
12118
12249
  const target2 = options.target ?? "compute";
12119
- const probe = options.probe ?? targetProbe(target2, run2);
12250
+ const paths = socketPaths(options.publicSocketPath);
12251
+ const probe = options.probe ?? (target2 === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target2, run2));
12120
12252
  const now = options.now ?? Date.now;
12121
12253
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12122
12254
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12123
- const previous = readJournal(journalPath, dirname6(staged.currentLink));
12124
- const attemptId = options.attemptId ?? randomUUID3();
12255
+ const previous = readJournal(journalPath, dirname8(staged.currentLink));
12256
+ const attemptId = options.attemptId ?? randomUUID4();
12125
12257
  if (!ATTEMPT_ID.test(attemptId))
12126
12258
  throw new Error("Agent update attempt ID is invalid");
12127
12259
  const startedAtTs = now();
@@ -12143,11 +12275,18 @@ async function activateAgentRelease(staged, options = {}) {
12143
12275
  writeUpdateState(journalPath, receiptPath, journal);
12144
12276
  let selectionAttempted = false;
12145
12277
  try {
12278
+ if (!options.candidatePrepared)
12279
+ await startCandidate(staged, target2, run2);
12280
+ if (target2 === "compute")
12281
+ switchAgentSocketRoute(paths.route, paths.candidate);
12146
12282
  selectionAttempted = true;
12147
12283
  selectAgentRelease(staged);
12148
12284
  await restartAgent(target2, run2);
12149
12285
  if (!await probe())
12150
- throw new Error("the replacement Agent did not answer its retained Vault socket");
12286
+ throw new Error("the replacement Agent did not answer its active Vault backend");
12287
+ if (target2 === "compute")
12288
+ switchAgentSocketRoute(paths.route, paths.active);
12289
+ await stopCandidate(staged, target2, run2);
12151
12290
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
12152
12291
  writeUpdateState(journalPath, receiptPath, journal);
12153
12292
  run2({
@@ -12165,10 +12304,13 @@ async function activateAgentRelease(staged, options = {}) {
12165
12304
  restored = true;
12166
12305
  await restartAgent(target2, run2);
12167
12306
  rollbackHealthy = await probe();
12307
+ if (rollbackHealthy && target2 === "compute")
12308
+ switchAgentSocketRoute(paths.route, paths.active);
12168
12309
  } catch {
12169
12310
  rollbackHealthy = false;
12170
12311
  }
12171
12312
  }
12313
+ await stopCandidate(staged, target2, run2).catch(() => {});
12172
12314
  const failures = failureCount + 1;
12173
12315
  const updatedAtTs = now();
12174
12316
  journal = {
@@ -12188,28 +12330,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12188
12330
  const root = resolve3(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
12189
12331
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12190
12332
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12333
+ const run2 = options.run ?? runCommand;
12191
12334
  const journal = readJournal(journalPath, root);
12192
- if (!journal)
12335
+ if (!journal) {
12336
+ for (const target2 of ["compute", "metal"]) {
12337
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12338
+ }
12339
+ clearAgentCandidate(root);
12193
12340
  return;
12341
+ }
12194
12342
  if (journal.outcome !== "activating") {
12343
+ await stopCandidate(stagedFromJournal(journal, root), journal.target, run2).catch(() => {});
12195
12344
  writeAtomic(receiptPath, publicReceipt(journal), 416);
12196
12345
  return publicReceipt(journal);
12197
12346
  }
12198
12347
  const staged = stagedFromJournal(journal, root);
12199
- if (!existsSync13(join8(root, journal.previousTarget))) {
12348
+ if (!existsSync14(join8(root, journal.previousTarget))) {
12200
12349
  throw new Error("Agent update rollback release is missing");
12201
12350
  }
12202
- const run2 = options.run ?? runCommand;
12203
- const probe = options.probe ?? targetProbe(journal.target, run2);
12351
+ const paths = socketPaths(options.publicSocketPath);
12352
+ const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run2));
12204
12353
  restoreAgentRelease(staged);
12205
12354
  let rollbackHealthy = false;
12206
12355
  let failureMessage = "activation was interrupted before its health verdict became durable";
12207
12356
  try {
12208
12357
  await restartAgent(journal.target, run2);
12209
12358
  rollbackHealthy = await probe();
12359
+ if (rollbackHealthy && journal.target === "compute")
12360
+ switchAgentSocketRoute(paths.route, paths.active);
12210
12361
  } catch (cause) {
12211
12362
  failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
12212
12363
  }
12364
+ await stopCandidate(staged, journal.target, run2).catch(() => {});
12213
12365
  const failures = journal.failureCount + 1;
12214
12366
  const updatedAtTs = (options.now ?? Date.now)();
12215
12367
  const recovered = {
@@ -12226,15 +12378,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12226
12378
  }
12227
12379
  function startAgentUpdateHelper(options = {}) {
12228
12380
  const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
12229
- if (existsSync13(socketPath))
12230
- unlinkSync10(socketPath);
12231
- mkdirSync9(dirname6(socketPath), { recursive: true, mode: 488 });
12232
- const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
12381
+ if (existsSync14(socketPath))
12382
+ unlinkSync11(socketPath);
12383
+ mkdirSync9(dirname8(socketPath), { recursive: true, mode: 488 });
12233
12384
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12234
12385
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12235
12386
  const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
12236
- const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, { target: target2, attemptId, journalPath, receiptPath, now: options.now }));
12387
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
12388
+ const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, {
12389
+ target: target2,
12390
+ attemptId,
12391
+ journalPath,
12392
+ receiptPath,
12393
+ now: options.now,
12394
+ candidatePrepared: true
12395
+ }));
12237
12396
  let busy = true;
12397
+ let pending;
12238
12398
  let blocked;
12239
12399
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
12240
12400
  root: releaseRoot,
@@ -12246,7 +12406,7 @@ function startAgentUpdateHelper(options = {}) {
12246
12406
  }).finally(() => {
12247
12407
  busy = false;
12248
12408
  });
12249
- const server = createServer6((socket) => {
12409
+ const server = createServer7((socket) => {
12250
12410
  let buffer = "";
12251
12411
  socket.on("data", (chunk) => {
12252
12412
  buffer += chunk.toString("utf8");
@@ -12265,16 +12425,49 @@ function startAgentUpdateHelper(options = {}) {
12265
12425
  Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
12266
12426
  if (blocked)
12267
12427
  throw new Error(`update journal needs operator recovery: ${blocked}`);
12268
- if (busy)
12269
- throw new Error("another Agent update or recovery is already active");
12270
- if (request.op !== "apply")
12271
- throw new Error("unknown update operation");
12272
12428
  if (request.target !== "compute" && request.target !== "metal") {
12273
12429
  throw new Error("agent update target is invalid");
12274
12430
  }
12275
- const attemptId = request.attemptId ?? randomUUID3();
12431
+ const attemptId = request.attemptId ?? randomUUID4();
12276
12432
  if (!ATTEMPT_ID.test(attemptId))
12277
12433
  throw new Error("Agent update attempt ID is invalid");
12434
+ if (request.op === "commit" || request.op === "abort") {
12435
+ if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
12436
+ throw new Error("Agent update handover does not match the prepared candidate");
12437
+ }
12438
+ const selected = pending;
12439
+ pending = undefined;
12440
+ if (request.op === "abort") {
12441
+ await stopCandidate(selected.staged, selected.target, runCommand);
12442
+ busy = false;
12443
+ const response3 = {
12444
+ ok: true,
12445
+ status: "aborted",
12446
+ version: selected.staged.version,
12447
+ attemptId
12448
+ };
12449
+ socket.end(`${JSON.stringify(response3)}
12450
+ `);
12451
+ return;
12452
+ }
12453
+ const outcome = await activate(selected.staged, selected.target, attemptId);
12454
+ busy = false;
12455
+ if (outcome?.ok === false)
12456
+ throw new Error(outcome.reason ?? "Agent handover failed");
12457
+ const response2 = {
12458
+ ok: true,
12459
+ status: "active",
12460
+ version: selected.staged.version,
12461
+ attemptId
12462
+ };
12463
+ socket.end(`${JSON.stringify(response2)}
12464
+ `);
12465
+ return;
12466
+ }
12467
+ if (request.op !== "prepare")
12468
+ throw new Error("unknown update operation");
12469
+ if (busy)
12470
+ throw new Error("another Agent update or recovery is already active");
12278
12471
  const prior = readJournal(journalPath, releaseRoot);
12279
12472
  const now = (options.now ?? Date.now)();
12280
12473
  if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
@@ -12285,17 +12478,23 @@ function startAgentUpdateHelper(options = {}) {
12285
12478
  currentVersion: request.currentVersion,
12286
12479
  root: releaseRoot
12287
12480
  });
12288
- const response = { ok: true, status: "staged", version: staged.version, attemptId };
12481
+ await startCandidate(staged, request.target, runCommand);
12482
+ pending = { staged, target: request.target, attemptId };
12483
+ ownsBusy = false;
12484
+ setTimer(() => {
12485
+ if (pending?.attemptId !== attemptId)
12486
+ return;
12487
+ const expired = pending;
12488
+ pending = undefined;
12489
+ stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
12490
+ busy = false;
12491
+ });
12492
+ }, 180000);
12493
+ const response = { ok: true, status: "prepared", version: staged.version, attemptId };
12289
12494
  socket.end(`${JSON.stringify(response)}
12290
12495
  `);
12291
- setTimer(() => void activate(staged, request.target, attemptId).catch((cause) => {
12292
- blocked = cause instanceof Error ? cause.message : String(cause);
12293
- }).finally(() => {
12294
- busy = false;
12295
- }), 100);
12296
- ownsBusy = false;
12297
12496
  }).catch((cause) => {
12298
- if (ownsBusy)
12497
+ if (ownsBusy && !pending)
12299
12498
  busy = false;
12300
12499
  const response = {
12301
12500
  ok: false,
@@ -12307,12 +12506,12 @@ function startAgentUpdateHelper(options = {}) {
12307
12506
  });
12308
12507
  socket.on("error", () => socket.destroy());
12309
12508
  });
12310
- server.listen(socketPath, () => chmodSync13(socketPath, 432));
12509
+ server.listen(socketPath, () => chmodSync14(socketPath, 432));
12311
12510
  return server;
12312
12511
  }
12313
12512
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
12314
12513
  return new Promise((resolve4, reject) => {
12315
- const socket = connect5(socketPath, () => socket.write(`${JSON.stringify(request)}
12514
+ const socket = connect6(socketPath, () => socket.write(`${JSON.stringify(request)}
12316
12515
  `));
12317
12516
  let buffer = "";
12318
12517
  socket.setTimeout(timeoutMs, () => {
@@ -12337,7 +12536,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
12337
12536
  }
12338
12537
 
12339
12538
  // src/agent-heartbeat.ts
12340
- import { existsSync as existsSync14, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12539
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12341
12540
  import { cpus, freemem, loadavg, totalmem } from "os";
12342
12541
  function readAgentHostMetrics() {
12343
12542
  const filesystem = statfsSync2("/");
@@ -12382,7 +12581,7 @@ function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = rea
12382
12581
  runtimeCapabilities: {
12383
12582
  native: true,
12384
12583
  ociRunc: ubuntuX64,
12385
- kataQemuSnp: ubuntuX64 && osVersion === "26.04" && existsSync14("/dev/kvm") && existsSync14("/dev/sev") && enabled("/sys/module/kvm_amd/parameters/sev") && enabled("/sys/module/kvm_amd/parameters/sev_snp")
12584
+ kataQemuSnp: ubuntuX64 && osVersion === "26.04" && existsSync15("/dev/kvm") && existsSync15("/dev/sev") && enabled("/sys/module/kvm_amd/parameters/sev") && enabled("/sys/module/kvm_amd/parameters/sev_snp")
12386
12585
  },
12387
12586
  capacity: {
12388
12587
  logicalCpu: Math.max(1, metrics.logicalCpu),
@@ -12434,18 +12633,24 @@ async function heartbeatAgentOnce(options) {
12434
12633
  });
12435
12634
  return response;
12436
12635
  }
12437
- let prepared = false;
12636
+ let candidatePrepared = false;
12637
+ let drained = false;
12438
12638
  try {
12439
- await options.prepareUpdate?.(release);
12440
- prepared = true;
12639
+ const update2 = options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate(phase === "prepare" ? { op: "prepare", target: options.updateTarget ?? "compute", release: next, currentVersion: current2, attemptId } : {
12640
+ op: phase,
12641
+ target: options.updateTarget ?? "compute",
12642
+ currentVersion: current2,
12643
+ targetVersion: next.version,
12644
+ attemptId
12645
+ }));
12441
12646
  const apply = async () => {
12442
- const applied = await (options.applyUpdate ?? ((next, current2, attemptId) => requestAgentUpdate({
12443
- op: "apply",
12444
- target: options.updateTarget ?? "compute",
12445
- release: next,
12446
- currentVersion: current2,
12447
- attemptId
12448
- })))(release, observation.version, desired.attemptId);
12647
+ const prepared = await update2("prepare", release, observation.version, desired.attemptId);
12648
+ if (!prepared.ok)
12649
+ throw new AgentUpdateRefusedError(`agent update refused: ${prepared.error.message}`);
12650
+ candidatePrepared = true;
12651
+ await options.prepareUpdate?.(release);
12652
+ drained = true;
12653
+ const applied = await update2("commit", release, observation.version, desired.attemptId);
12449
12654
  if (!applied.ok)
12450
12655
  throw new AgentUpdateRefusedError(`agent update refused: ${applied.error.message}`);
12451
12656
  if (applied.attemptId !== desired.attemptId) {
@@ -12458,9 +12663,18 @@ async function heartbeatAgentOnce(options) {
12458
12663
  } else {
12459
12664
  await apply();
12460
12665
  }
12461
- options.onEvent?.("update-staged", { from: observation.version, to: release.version });
12666
+ options.onEvent?.("update-active", { from: observation.version, to: release.version });
12462
12667
  } catch (cause) {
12463
- if (prepared)
12668
+ if (candidatePrepared && !drained) {
12669
+ await (options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate({
12670
+ op: phase,
12671
+ target: options.updateTarget ?? "compute",
12672
+ currentVersion: current2,
12673
+ targetVersion: next.version,
12674
+ attemptId
12675
+ })))("abort", release, observation.version, desired.attemptId).catch(() => {});
12676
+ }
12677
+ if (drained)
12464
12678
  await options.recoverUpdate?.(cause);
12465
12679
  throw cause;
12466
12680
  }
@@ -12501,14 +12715,14 @@ function startAgentHeartbeat(options) {
12501
12715
  }
12502
12716
 
12503
12717
  // src/software-helper.ts
12504
- import { chmodSync as chmodSync14, existsSync as existsSync17, mkdirSync as mkdirSync13, unlinkSync as unlinkSync11 } from "fs";
12505
- import { connect as connect6, createServer as createServer7 } from "net";
12506
- import { dirname as dirname10 } from "path";
12718
+ import { chmodSync as chmodSync15, existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync12 } from "fs";
12719
+ import { connect as connect7, createServer as createServer8 } from "net";
12720
+ import { dirname as dirname12 } from "path";
12507
12721
 
12508
12722
  // src/service-supervisor.ts
12509
12723
  import { createHash as createHash9 } from "crypto";
12510
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync8, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
12511
- import { dirname as dirname7, join as join9, resolve as resolve4, sep as sep2 } from "path";
12724
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync10, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "fs";
12725
+ import { dirname as dirname9, join as join9, resolve as resolve4, sep as sep2 } from "path";
12512
12726
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
12513
12727
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
12514
12728
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
@@ -12518,17 +12732,17 @@ var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
12518
12732
  var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
12519
12733
  var defaultHost = {
12520
12734
  write(path2, content, mode) {
12521
- mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
12735
+ mkdirSync10(dirname9(path2), { recursive: true, mode: 493 });
12522
12736
  const next = `${path2}.next`;
12523
12737
  writeFileSync11(next, content, { mode });
12524
- renameSync8(next, path2);
12738
+ renameSync10(next, path2);
12525
12739
  },
12526
12740
  read: (path2) => readFileSync12(path2, "utf8"),
12527
- exists: existsSync15,
12528
- list: (path2) => existsSync15(path2) ? readdirSync2(path2) : [],
12741
+ exists: existsSync16,
12742
+ list: (path2) => existsSync16(path2) ? readdirSync2(path2) : [],
12529
12743
  realpath: realpathSync2,
12530
12744
  mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
12531
- remove: (path2) => rmSync7(path2, { force: true }),
12745
+ remove: (path2) => rmSync8(path2, { force: true }),
12532
12746
  async exec(argv2) {
12533
12747
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
12534
12748
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -12724,20 +12938,20 @@ async function activateSupervisedService(request, host = defaultHost) {
12724
12938
 
12725
12939
  // src/container-supervisor.ts
12726
12940
  import { createHash as createHash10 } from "crypto";
12727
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync9, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "fs";
12728
- import { dirname as dirname8, resolve as resolve5, sep as sep3 } from "path";
12941
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync11, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "fs";
12942
+ import { dirname as dirname10, resolve as resolve5, sep as sep3 } from "path";
12729
12943
  var defaultHost2 = {
12730
12944
  realpath: realpathSync3,
12731
- exists: existsSync16,
12945
+ exists: existsSync17,
12732
12946
  read: (path2) => readFileSync13(path2, "utf8"),
12733
12947
  write(path2, content, mode) {
12734
- mkdirSync11(dirname8(path2), { recursive: true, mode: 493 });
12948
+ mkdirSync11(dirname10(path2), { recursive: true, mode: 493 });
12735
12949
  const next = `${path2}.next`;
12736
12950
  writeFileSync12(next, content, { mode });
12737
- renameSync9(next, path2);
12951
+ renameSync11(next, path2);
12738
12952
  },
12739
- remove: (path2) => rmSync8(path2, { force: true }),
12740
- list: (path2) => existsSync16(path2) ? readdirSync3(path2) : [],
12953
+ remove: (path2) => rmSync9(path2, { force: true }),
12954
+ list: (path2) => existsSync17(path2) ? readdirSync3(path2) : [],
12741
12955
  mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
12742
12956
  async exec(argv2) {
12743
12957
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -13026,9 +13240,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
13026
13240
 
13027
13241
  // src/deployment-connectivity.ts
13028
13242
  import { createHash as createHash11 } from "crypto";
13029
- import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
13243
+ import { mkdirSync as mkdirSync12, renameSync as renameSync12, writeFileSync as writeFileSync13 } from "fs";
13030
13244
  import { createConnection as createConnection2 } from "net";
13031
- import { dirname as dirname9 } from "path";
13245
+ import { dirname as dirname11 } from "path";
13032
13246
  var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
13033
13247
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
13034
13248
  var HOSTNAME2 = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -13041,7 +13255,7 @@ var checked7 = async (host, argv2, label) => {
13041
13255
  return result.output.trim();
13042
13256
  };
13043
13257
  async function sealWithSystemd(name, path2, value) {
13044
- mkdirSync12(dirname9(path2), { recursive: true, mode: 448 });
13258
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 448 });
13045
13259
  const next = `${path2}.next`;
13046
13260
  const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
13047
13261
  stdin: "pipe",
@@ -13057,15 +13271,15 @@ async function sealWithSystemd(name, path2, value) {
13057
13271
  ]);
13058
13272
  if (exitCode !== 0)
13059
13273
  throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
13060
- renameSync10(next, path2);
13274
+ renameSync12(next, path2);
13061
13275
  }
13062
13276
  var defaultHost3 = {
13063
13277
  seal: sealWithSystemd,
13064
13278
  write(path2, content, mode) {
13065
- mkdirSync12(dirname9(path2), { recursive: true, mode: 493 });
13279
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 493 });
13066
13280
  const next = `${path2}.next`;
13067
13281
  writeFileSync13(next, content, { mode });
13068
- renameSync10(next, path2);
13282
+ renameSync12(next, path2);
13069
13283
  },
13070
13284
  async exec(argv2) {
13071
13285
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -13304,9 +13518,9 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
13304
13518
  var MAX_PENDING_REQUESTS = 128;
13305
13519
  function startSoftwareHelper(options = {}) {
13306
13520
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
13307
- if (existsSync17(socketPath))
13308
- unlinkSync11(socketPath);
13309
- mkdirSync13(dirname10(socketPath), { recursive: true, mode: 488 });
13521
+ if (existsSync18(socketPath))
13522
+ unlinkSync12(socketPath);
13523
+ mkdirSync13(dirname12(socketPath), { recursive: true, mode: 488 });
13310
13524
  const ensure = options.ensure ?? ensureSoftwareRequirements;
13311
13525
  const activate = options.activate ?? activateSupervisedService;
13312
13526
  const buildContainer = options.buildContainer ?? buildContainerImage;
@@ -13314,7 +13528,7 @@ function startSoftwareHelper(options = {}) {
13314
13528
  const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
13315
13529
  let tail = Promise.resolve();
13316
13530
  let pending = 0;
13317
- const server = createServer7((socket) => {
13531
+ const server = createServer8((socket) => {
13318
13532
  let buffer = "";
13319
13533
  socket.on("data", (chunk) => {
13320
13534
  buffer += chunk.toString("utf8");
@@ -13393,12 +13607,12 @@ function startSoftwareHelper(options = {}) {
13393
13607
  });
13394
13608
  socket.on("error", () => socket.destroy());
13395
13609
  });
13396
- server.listen(socketPath, () => chmodSync14(socketPath, 432));
13610
+ server.listen(socketPath, () => chmodSync15(socketPath, 432));
13397
13611
  return server;
13398
13612
  }
13399
13613
  function requestContainer(op, request, socketPath, timeoutMs) {
13400
13614
  return new Promise((resolve6, reject) => {
13401
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13615
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13402
13616
  `));
13403
13617
  let buffer = "";
13404
13618
  socket.setTimeout(timeoutMs, () => {
@@ -13436,7 +13650,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
13436
13650
  }
13437
13651
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
13438
13652
  return new Promise((resolve6, reject) => {
13439
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13653
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13440
13654
  `));
13441
13655
  let buffer = "";
13442
13656
  socket.setTimeout(timeoutMs, () => {
@@ -13465,7 +13679,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
13465
13679
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
13466
13680
  validateSoftwareRequirements(requirements);
13467
13681
  return new Promise((resolve6, reject) => {
13468
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13682
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13469
13683
  `));
13470
13684
  let buffer = "";
13471
13685
  socket.setTimeout(timeoutMs, () => {
@@ -14425,19 +14639,19 @@ var METAL_SYSTEMD_CREDENTIALS = {
14425
14639
 
14426
14640
  // src/platform-bootstrap-runtime.ts
14427
14641
  import {
14428
- chmodSync as chmodSync15,
14642
+ chmodSync as chmodSync16,
14429
14643
  chownSync,
14430
14644
  copyFileSync as copyFileSync2,
14431
- existsSync as existsSync18,
14432
- lstatSync as lstatSync3,
14645
+ existsSync as existsSync19,
14646
+ lstatSync as lstatSync4,
14433
14647
  mkdirSync as mkdirSync14,
14434
14648
  readFileSync as readFileSync14,
14435
14649
  readdirSync as readdirSync4,
14436
14650
  realpathSync as realpathSync5,
14437
- renameSync as renameSync11,
14438
- rmSync as rmSync9,
14651
+ renameSync as renameSync13,
14652
+ rmSync as rmSync10,
14439
14653
  statSync as statSync4,
14440
- symlinkSync as symlinkSync4,
14654
+ symlinkSync as symlinkSync6,
14441
14655
  writeFileSync as writeFileSync14
14442
14656
  } from "fs";
14443
14657
  import { join as join10 } from "path";
@@ -14492,21 +14706,21 @@ var platformExec = async (argv2) => {
14492
14706
  };
14493
14707
  var replaceLink = (path2, target2) => {
14494
14708
  const pending = `${path2}.next`;
14495
- rmSync9(pending, { force: true });
14709
+ rmSync10(pending, { force: true });
14496
14710
  if (!target2) {
14497
- rmSync9(path2, { force: true });
14711
+ rmSync10(path2, { force: true });
14498
14712
  return;
14499
14713
  }
14500
- symlinkSync4(target2, pending);
14501
- renameSync11(pending, path2);
14714
+ symlinkSync6(target2, pending);
14715
+ renameSync13(pending, path2);
14502
14716
  };
14503
14717
  var secureRelease = (path2, uid, gid) => {
14504
14718
  const visit = (current2) => {
14505
- const metadata = lstatSync3(current2);
14719
+ const metadata = lstatSync4(current2);
14506
14720
  if (metadata.isSymbolicLink())
14507
14721
  throw new Error("release contains a symbolic link");
14508
14722
  chownSync(current2, uid, gid);
14509
- chmodSync15(current2, metadata.isDirectory() ? 365 : 292);
14723
+ chmodSync16(current2, metadata.isDirectory() ? 365 : 292);
14510
14724
  if (metadata.isDirectory())
14511
14725
  for (const name of readdirSync4(current2))
14512
14726
  visit(join10(current2, name));
@@ -14531,7 +14745,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14531
14745
  const slots = join10(normalized.root, "slots");
14532
14746
  mkdirSync14(slots, { recursive: true, mode: 493 });
14533
14747
  const slotFile = join10(normalized.root, ".forge-slot");
14534
- const previousSlot = existsSync18(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14748
+ const previousSlot = existsSync19(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14535
14749
  const target2 = previousSlot === "blue" ? "green" : "blue";
14536
14750
  const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
14537
14751
  const targetLink = join10(slots, target2);
@@ -14571,25 +14785,25 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14571
14785
  }
14572
14786
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
14573
14787
  const backup = `${upstream}.forgezero-backup`;
14574
- if (existsSync18(upstream))
14788
+ if (existsSync19(upstream))
14575
14789
  copyFileSync2(upstream, backup);
14576
14790
  else
14577
- rmSync9(backup, { force: true });
14791
+ rmSync10(backup, { force: true });
14578
14792
  writeFileSync14(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
14579
14793
  `, { mode: 420 });
14580
14794
  const test = await exec(["/usr/sbin/nginx", "-t"]);
14581
14795
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
14582
14796
  if (reload.exitCode !== 0) {
14583
- if (existsSync18(backup))
14584
- renameSync11(backup, upstream);
14797
+ if (existsSync19(backup))
14798
+ renameSync13(backup, upstream);
14585
14799
  else
14586
- rmSync9(upstream, { force: true });
14800
+ rmSync10(upstream, { force: true });
14587
14801
  await exec(["/usr/sbin/nginx", "-t"]);
14588
14802
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
14589
14803
  await stopTarget();
14590
14804
  throw new Error("nginx refused the promoted upstream");
14591
14805
  }
14592
- rmSync9(backup, { force: true });
14806
+ rmSync10(backup, { force: true });
14593
14807
  writeFileSync14(slotFile, `${target2}
14594
14808
  `, { mode: 420 });
14595
14809
  if (previousSlot && previousSlot !== target2) {
@@ -14599,12 +14813,12 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14599
14813
  const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync4(right).mtimeMs - statSync4(left).mtimeMs).slice(normalized.keepReleases);
14600
14814
  for (const path2 of old)
14601
14815
  if (path2 !== release)
14602
- rmSync9(path2, { recursive: true, force: true });
14816
+ rmSync10(path2, { recursive: true, force: true });
14603
14817
  return { release, slot: target2 };
14604
14818
  }
14605
14819
 
14606
14820
  // src/recovery-host.ts
14607
- import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14821
+ import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14608
14822
  import { hostname } from "os";
14609
14823
  import { join as join11 } from "path";
14610
14824
  var execute2 = async (argv2) => {
@@ -14630,7 +14844,7 @@ var exactState = (path2, expected) => {
14630
14844
  throw new Error(`state mismatch: ${expected}`);
14631
14845
  };
14632
14846
  var nonEmpty = (path2) => {
14633
- if (!existsSync19(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14847
+ if (!existsSync20(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14634
14848
  throw new Error(`required file missing: ${path2}`);
14635
14849
  };
14636
14850
  var option = (args, name) => {
@@ -14726,20 +14940,20 @@ async function runRecoveryHost(args, run2 = execute2) {
14726
14940
  }
14727
14941
 
14728
14942
  // src/platform-fleet-verification.ts
14729
- import { lstatSync as lstatSync6 } from "fs";
14943
+ import { lstatSync as lstatSync7 } from "fs";
14730
14944
 
14731
14945
  // src/bootstrap.ts
14732
14946
  import {
14733
- chmodSync as chmodSync17,
14734
- existsSync as existsSync21,
14735
- lstatSync as lstatSync5,
14947
+ chmodSync as chmodSync18,
14948
+ existsSync as existsSync22,
14949
+ lstatSync as lstatSync6,
14736
14950
  mkdirSync as mkdirSync16,
14737
14951
  readFileSync as readFileSync17,
14738
- renameSync as renameSync13,
14739
- rmSync as rmSync11,
14952
+ renameSync as renameSync15,
14953
+ rmSync as rmSync12,
14740
14954
  writeFileSync as writeFileSync16
14741
14955
  } from "fs";
14742
- import { dirname as dirname12 } from "path";
14956
+ import { dirname as dirname14 } from "path";
14743
14957
  import { fileURLToPath as fileURLToPath2 } from "url";
14744
14958
 
14745
14959
  // src/provision.ts
@@ -14796,6 +15010,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
14796
15010
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
14797
15011
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
14798
15012
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
15013
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
14799
15014
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
14800
15015
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
14801
15016
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -14907,6 +15122,7 @@ Type=simple
14907
15122
  User=root
14908
15123
  Group=${AGENT_UPDATE_GROUP}
14909
15124
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
15125
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
14910
15126
  ExecStart=${bin} update-helper
14911
15127
  Restart=always
14912
15128
  RestartSec=2
@@ -14951,13 +15167,11 @@ WantedBy=sockets.target
14951
15167
  `;
14952
15168
  }
14953
15169
  function agentSocketProxyUnit(options) {
14954
- const backend = agentBackendSocketPath(options.socketPath);
15170
+ const backend = agentRoutingSocketPath(options.socketPath);
14955
15171
  const user = options.user ?? "forgezero";
14956
15172
  return `[Unit]
14957
15173
  Description=ForgeZero application Vault socket proxy
14958
15174
  Documentation=https://www.forgezero.net/docs/agent
14959
- Requires=forgezero-agent.service
14960
- After=forgezero-agent.service
14961
15175
 
14962
15176
  [Service]
14963
15177
  User=${user}
@@ -14978,12 +15192,26 @@ RestrictAddressFamilies=AF_UNIX
14978
15192
  `;
14979
15193
  }
14980
15194
  function agentBackendSocketPath(publicSocketPath) {
15195
+ const socket = systemdPath(publicSocketPath, "agent socket");
15196
+ const backend = `${socket}.backend.active`;
15197
+ if (Buffer.byteLength(backend) > 100)
15198
+ throw new Error("agent socket path is too long for a Unix socket");
15199
+ return backend;
15200
+ }
15201
+ function agentRoutingSocketPath(publicSocketPath) {
14981
15202
  const socket = systemdPath(publicSocketPath, "agent socket");
14982
15203
  const backend = `${socket}.backend`;
14983
15204
  if (Buffer.byteLength(backend) > 100)
14984
15205
  throw new Error("agent socket path is too long for a Unix socket");
14985
15206
  return backend;
14986
15207
  }
15208
+ function agentCandidateSocketPath(publicSocketPath) {
15209
+ const socket = systemdPath(publicSocketPath, "agent socket");
15210
+ const backend = `${socket}.backend.candidate`;
15211
+ if (Buffer.byteLength(backend) > 100)
15212
+ throw new Error("agent socket path is too long for a Unix socket");
15213
+ return backend;
15214
+ }
14987
15215
  var systemdPath = (value, label) => {
14988
15216
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
14989
15217
  throw new Error(`invalid ${label} path`);
@@ -15267,7 +15495,11 @@ function agentUnit(options) {
15267
15495
  }
15268
15496
  const environment = [
15269
15497
  "NODE_ENV=production",
15270
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
15498
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
15499
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
15500
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
15501
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
15502
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
15271
15503
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
15272
15504
  `FZ_SEED_CREDENTIAL=agent-seed`,
15273
15505
  `FZ_AGENT_MODE=${options.mode}`,
@@ -15404,6 +15636,26 @@ ${deploymentWrites}
15404
15636
  WantedBy=multi-user.target
15405
15637
  `;
15406
15638
  }
15639
+ function agentCandidateUnit(options) {
15640
+ return agentUnit({
15641
+ ...options,
15642
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
15643
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
15644
+ handoverCandidate: true,
15645
+ gitCredentialPath: undefined,
15646
+ deploymentCredentials: {},
15647
+ bootstrapSshCredentialPath: undefined,
15648
+ bootstrapSshPublicKeyPath: undefined,
15649
+ pullBootstrap: false,
15650
+ pullDeployments: false,
15651
+ pullMigrations: false,
15652
+ lifecycleProfilePath: undefined,
15653
+ bootstrapTargetTelemetryEndpoint: undefined,
15654
+ repository: undefined,
15655
+ bootstrapBundlePath: undefined,
15656
+ bootstrapBundleManifestPath: undefined
15657
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
15658
+ }
15407
15659
  var renderOperation = (operation) => {
15408
15660
  if (operation.kind === "commands")
15409
15661
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -15539,6 +15791,7 @@ function planProvision(options) {
15539
15791
  auxiliaryUnits: [
15540
15792
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
15541
15793
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
15794
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
15542
15795
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
15543
15796
  ...options.enforceEgress ? [
15544
15797
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -15670,19 +15923,19 @@ function planProvision(options) {
15670
15923
  // src/cli/agent-install.ts
15671
15924
  import { randomBytes as randomBytes6 } from "crypto";
15672
15925
  import {
15673
- chmodSync as chmodSync16,
15926
+ chmodSync as chmodSync17,
15674
15927
  copyFileSync as copyFileSync3,
15675
- existsSync as existsSync20,
15676
- lstatSync as lstatSync4,
15928
+ existsSync as existsSync21,
15929
+ lstatSync as lstatSync5,
15677
15930
  mkdirSync as mkdirSync15,
15678
15931
  readFileSync as readFileSync16,
15679
15932
  realpathSync as realpathSync6,
15680
- renameSync as renameSync12,
15681
- rmSync as rmSync10,
15682
- symlinkSync as symlinkSync5,
15933
+ renameSync as renameSync14,
15934
+ rmSync as rmSync11,
15935
+ symlinkSync as symlinkSync7,
15683
15936
  writeFileSync as writeFileSync15
15684
15937
  } from "fs";
15685
- import { dirname as dirname11 } from "path";
15938
+ import { dirname as dirname13 } from "path";
15686
15939
  async function readCapabilities(run2) {
15687
15940
  const answers = {};
15688
15941
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -15740,51 +15993,51 @@ var runProvisionOperation = async (operation) => {
15740
15993
  if (operation.kind === "install-runtime") {
15741
15994
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
15742
15995
  mkdirSync15(`${release}/dist`, { recursive: true, mode: 493 });
15743
- mkdirSync15(dirname11(operation.binary), { recursive: true, mode: 493 });
15996
+ mkdirSync15(dirname13(operation.binary), { recursive: true, mode: 493 });
15744
15997
  copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
15745
- chmodSync16(`${release}/dist/fz-agent.js`, 493);
15746
- const gitSshSource = `${dirname11(operation.source)}/fz-git-ssh.js`;
15747
- if (!existsSync20(gitSshSource))
15998
+ chmodSync17(`${release}/dist/fz-agent.js`, 493);
15999
+ const gitSshSource = `${dirname13(operation.source)}/fz-git-ssh.js`;
16000
+ if (!existsSync21(gitSshSource))
15748
16001
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
15749
16002
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
15750
- chmodSync16(`${release}/dist/fz-git-ssh.js`, 493);
16003
+ chmodSync17(`${release}/dist/fz-git-ssh.js`, 493);
15751
16004
  const pending = "/opt/forgezero/agent/current.next";
15752
- rmSync10(pending, { force: true });
15753
- symlinkSync5(`versions/${operation.version}`, pending);
15754
- renameSync12(pending, "/opt/forgezero/agent/current");
15755
- rmSync10(operation.binary, { force: true });
15756
- symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
16005
+ rmSync11(pending, { force: true });
16006
+ symlinkSync7(`versions/${operation.version}`, pending);
16007
+ renameSync14(pending, "/opt/forgezero/agent/current");
16008
+ rmSync11(operation.binary, { force: true });
16009
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
15757
16010
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
15758
- rmSync10(gitSshBinary, { force: true });
15759
- symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
16011
+ rmSync11(gitSshBinary, { force: true });
16012
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
15760
16013
  return { stdout: "", exitCode: 0 };
15761
16014
  }
15762
16015
  if (operation.kind === "ensure-seed") {
15763
- if (existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16016
+ if (existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15764
16017
  return { stdout: "", exitCode: 0 };
15765
16018
  const seed = randomBytes6(32).toString("base64url");
15766
16019
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
15767
16020
  if (result.exitCode === 0)
15768
- chmodSync16(operation.credential, 256);
16021
+ chmodSync17(operation.credential, 256);
15769
16022
  return result;
15770
16023
  }
15771
16024
  if (operation.kind === "ensure-git-identity") {
15772
16025
  const key = "/run/forgezero-git-deploy-key";
15773
16026
  const publicKey = `${key}.pub`;
15774
16027
  try {
15775
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15776
- rmSync10(key, { force: true });
15777
- rmSync10(publicKey, { force: true });
16028
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16029
+ rmSync11(key, { force: true });
16030
+ rmSync11(publicKey, { force: true });
15778
16031
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
15779
16032
  if (result.exitCode !== 0)
15780
16033
  return result;
15781
16034
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
15782
16035
  if (result.exitCode !== 0)
15783
16036
  return result;
15784
- chmodSync16(operation.credential, 256);
16037
+ chmodSync17(operation.credential, 256);
15785
16038
  }
15786
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15787
- if (!existsSync20(key)) {
16039
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16040
+ if (!existsSync21(key)) {
15788
16041
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
15789
16042
  if (decrypted.exitCode !== 0)
15790
16043
  return decrypted;
@@ -15797,25 +16050,25 @@ var runProvisionOperation = async (operation) => {
15797
16050
  }
15798
16051
  return { stdout: "", exitCode: 0 };
15799
16052
  } finally {
15800
- rmSync10(key, { force: true });
15801
- rmSync10(publicKey, { force: true });
16053
+ rmSync11(key, { force: true });
16054
+ rmSync11(publicKey, { force: true });
15802
16055
  }
15803
16056
  }
15804
16057
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
15805
16058
  const key = "/run/forgezero-bootstrap-ssh-key";
15806
16059
  const generatedPublicKey = `${key}.pub`;
15807
16060
  try {
15808
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15809
- rmSync10(key, { force: true });
15810
- rmSync10(generatedPublicKey, { force: true });
16061
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16062
+ rmSync11(key, { force: true });
16063
+ rmSync11(generatedPublicKey, { force: true });
15811
16064
  let result;
15812
16065
  if (operation.source) {
15813
- const source = existsSync20(operation.source) ? lstatSync4(operation.source) : undefined;
16066
+ const source = existsSync21(operation.source) ? lstatSync5(operation.source) : undefined;
15814
16067
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
15815
16068
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
15816
16069
  }
15817
16070
  copyFileSync3(operation.source, key);
15818
- chmodSync16(key, 384);
16071
+ chmodSync17(key, 384);
15819
16072
  } else {
15820
16073
  result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
15821
16074
  if (result.exitCode !== 0)
@@ -15828,48 +16081,48 @@ var runProvisionOperation = async (operation) => {
15828
16081
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
15829
16082
  if (result.exitCode !== 0)
15830
16083
  return result;
15831
- chmodSync16(operation.credential, 256);
16084
+ chmodSync17(operation.credential, 256);
15832
16085
  }
15833
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15834
- if (!existsSync20(key)) {
16086
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16087
+ if (!existsSync21(key)) {
15835
16088
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
15836
16089
  if (decrypted.exitCode !== 0)
15837
16090
  return decrypted;
15838
- chmodSync16(key, 384);
16091
+ chmodSync17(key, 384);
15839
16092
  }
15840
16093
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
15841
16094
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
15842
16095
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
15843
16096
  }
15844
- mkdirSync15(dirname11(operation.publicKey), { recursive: true, mode: 493 });
16097
+ mkdirSync15(dirname13(operation.publicKey), { recursive: true, mode: 493 });
15845
16098
  writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
15846
16099
  `, { mode: 292 });
15847
- chmodSync16(operation.publicKey, 292);
16100
+ chmodSync17(operation.publicKey, 292);
15848
16101
  }
15849
16102
  if (operation.source)
15850
- rmSync10(operation.source, { force: true });
16103
+ rmSync11(operation.source, { force: true });
15851
16104
  return { stdout: "", exitCode: 0 };
15852
16105
  } finally {
15853
- rmSync10(key, { force: true });
15854
- rmSync10(generatedPublicKey, { force: true });
16106
+ rmSync11(key, { force: true });
16107
+ rmSync11(generatedPublicKey, { force: true });
15855
16108
  }
15856
16109
  }
15857
16110
  if (operation.kind === "ensure-enrolment") {
15858
- if (existsSync20(operation.state) && lstatSync4(operation.state).size > 0 || existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16111
+ if (existsSync21(operation.state) && lstatSync5(operation.state).size > 0 || existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15859
16112
  return { stdout: "", exitCode: 0 };
15860
- if (!existsSync20(operation.source))
16113
+ if (!existsSync21(operation.source))
15861
16114
  return { stdout: "enrolment source is missing", exitCode: 1 };
15862
16115
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
15863
16116
  if (result.exitCode === 0) {
15864
- chmodSync16(operation.credential, 256);
15865
- rmSync10(operation.source, { force: true });
16117
+ chmodSync17(operation.credential, 256);
16118
+ rmSync11(operation.source, { force: true });
15866
16119
  }
15867
16120
  return result;
15868
16121
  }
15869
16122
  if (operation.kind === "wait-socket") {
15870
16123
  for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
15871
16124
  try {
15872
- if (lstatSync4(operation.path).isSocket())
16125
+ if (lstatSync5(operation.path).isSocket())
15873
16126
  return { stdout: "", exitCode: 0 };
15874
16127
  } catch {}
15875
16128
  await Bun.sleep(operation.intervalMs);
@@ -15877,7 +16130,7 @@ var runProvisionOperation = async (operation) => {
15877
16130
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
15878
16131
  }
15879
16132
  if (operation.kind === "verify-file")
15880
- return existsSync20(operation.path) && lstatSync4(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
16133
+ return existsSync21(operation.path) && lstatSync5(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
15881
16134
  if (operation.kind === "verify-egress") {
15882
16135
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
15883
16136
  if (active.exitCode !== 0)
@@ -15911,7 +16164,7 @@ var runProvisionOperation = async (operation) => {
15911
16164
  const key = "/run/cloudflare-warp-key.gpg";
15912
16165
  writeFileSync15(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
15913
16166
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
15914
- rmSync10(key, { force: true });
16167
+ rmSync11(key, { force: true });
15915
16168
  if (result.exitCode !== 0)
15916
16169
  return result;
15917
16170
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
@@ -15932,7 +16185,7 @@ async function localRunner(operation) {
15932
16185
  if (capability.kind === "version")
15933
16186
  return fixed(capability.argv);
15934
16187
  try {
15935
- const metadata = lstatSync4(capability.path);
16188
+ const metadata = lstatSync5(capability.path);
15936
16189
  const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
15937
16190
  return { stdout: present ? `yes
15938
16191
  ` : `no
@@ -16230,19 +16483,19 @@ function localBootstrapHost() {
16230
16483
  };
16231
16484
  return {
16232
16485
  uid: () => process.getuid?.() ?? -1,
16233
- exists: existsSync21,
16486
+ exists: existsSync22,
16234
16487
  read: (path2) => readFileSync17(path2, "utf8"),
16235
16488
  write(path2, content, mode) {
16236
- mkdirSync16(dirname12(path2), { recursive: true, mode: 493 });
16489
+ mkdirSync16(dirname14(path2), { recursive: true, mode: 493 });
16237
16490
  const temporary = `${path2}.next.${process.pid}`;
16238
16491
  writeFileSync16(temporary, content, { mode });
16239
- chmodSync17(temporary, mode);
16240
- renameSync13(temporary, path2);
16492
+ chmodSync18(temporary, mode);
16493
+ renameSync15(temporary, path2);
16241
16494
  },
16242
16495
  mkdir: (path2, mode) => mkdirSync16(path2, { recursive: true, mode }),
16243
- remove: (path2) => rmSync11(path2, { force: true }),
16496
+ remove: (path2) => rmSync12(path2, { force: true }),
16244
16497
  inspect(path2) {
16245
- const value = lstatSync5(path2);
16498
+ const value = lstatSync6(path2);
16246
16499
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
16247
16500
  },
16248
16501
  exec: execute3,
@@ -16263,9 +16516,9 @@ function localBootstrapHost() {
16263
16516
  },
16264
16517
  async installAgent(config, phase) {
16265
16518
  const capabilities = await readCapabilities(localRunner);
16266
- const hasBinding = existsSync21("/var/lib/forgezero/enrolment.json");
16267
- const hasEnrolCredential = existsSync21(ENROL_CREDENTIAL);
16268
- const initialBundle = config.kind === "platform" && !existsSync21(BOOTSTRAP_RELEASE_EVIDENCE) ? {
16519
+ const hasBinding = existsSync22("/var/lib/forgezero/enrolment.json");
16520
+ const hasEnrolCredential = existsSync22(ENROL_CREDENTIAL);
16521
+ const initialBundle = config.kind === "platform" && !existsSync22(BOOTSTRAP_RELEASE_EVIDENCE) ? {
16269
16522
  path: config.bootstrapBundle.bundleFile,
16270
16523
  manifestPath: config.bootstrapBundle.manifestFile,
16271
16524
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync17(config.bootstrapBundle.manifestFile, "utf8")))
@@ -16281,7 +16534,7 @@ function localBootstrapHost() {
16281
16534
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
16282
16535
  databasePorts: [8529]
16283
16536
  };
16284
- mkdirSync16(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16537
+ mkdirSync16(dirname14(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16285
16538
  writeFileSync16(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
16286
16539
  `, { mode: 256 });
16287
16540
  }
@@ -16292,7 +16545,7 @@ function localBootstrapHost() {
16292
16545
  initialBundle
16293
16546
  });
16294
16547
  for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
16295
- mkdirSync16(dirname12(unit3.path), { recursive: true, mode: 493 });
16548
+ mkdirSync16(dirname14(unit3.path), { recursive: true, mode: 493 });
16296
16549
  writeFileSync16(unit3.path, unit3.unit, { mode: 420 });
16297
16550
  }
16298
16551
  await applyPlan(plan, localRunner);
@@ -16307,7 +16560,7 @@ var localRuntime = () => ({
16307
16560
  bootstrapStatus: () => bootstrapStatus(),
16308
16561
  characterDevice(path2) {
16309
16562
  try {
16310
- return lstatSync6(path2).isCharacterDevice();
16563
+ return lstatSync7(path2).isCharacterDevice();
16311
16564
  } catch {
16312
16565
  return false;
16313
16566
  }
@@ -16346,9 +16599,9 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
16346
16599
 
16347
16600
  // src/community-rehearsal-host.ts
16348
16601
  import { createHash as createHash12 } from "crypto";
16349
- import { lstatSync as lstatSync7, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync12, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync17 } from "fs";
16602
+ import { lstatSync as lstatSync8, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync13, symlinkSync as symlinkSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync17 } from "fs";
16350
16603
  import { isIP as isIP5 } from "net";
16351
- import { dirname as dirname13 } from "path";
16604
+ import { dirname as dirname15 } from "path";
16352
16605
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
16353
16606
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
16354
16607
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
@@ -16562,19 +16815,19 @@ async function exec(argv2, stdin) {
16562
16815
  async function applyOperations(operations) {
16563
16816
  for (const operation of operations) {
16564
16817
  if (operation.kind === "remove-tree")
16565
- rmSync12(operation.path, { recursive: true, force: true });
16818
+ rmSync13(operation.path, { recursive: true, force: true });
16566
16819
  else if (operation.kind === "write") {
16567
- mkdirSync17(dirname13(operation.path), { recursive: true });
16820
+ mkdirSync17(dirname15(operation.path), { recursive: true });
16568
16821
  writeFileSync17(operation.path, operation.content, { mode: operation.mode });
16569
16822
  } else if (operation.kind === "unlink") {
16570
16823
  try {
16571
- unlinkSync12(operation.path);
16824
+ unlinkSync13(operation.path);
16572
16825
  } catch (cause) {
16573
16826
  if (cause.code !== "ENOENT")
16574
16827
  throw cause;
16575
16828
  }
16576
16829
  } else if (operation.kind === "symlink")
16577
- symlinkSync6(operation.target, operation.path);
16830
+ symlinkSync8(operation.target, operation.path);
16578
16831
  else {
16579
16832
  const result = await exec(operation.argv, operation.stdin);
16580
16833
  if (!(operation.accepted ?? [0]).includes(result.exitCode))
@@ -16616,7 +16869,7 @@ async function runCommunityRehearsalHost(request) {
16616
16869
  return { ok: true, action: request.action, node: node.name };
16617
16870
  }
16618
16871
  if (request.action === "prepare") {
16619
- const metadata = lstatSync7(request.archivePath);
16872
+ const metadata = lstatSync8(request.archivePath);
16620
16873
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash12("sha256").update(readFileSync18(request.archivePath)).digest("hex") !== request.archiveSha256) {
16621
16874
  throw new Error("community rehearsal archive is not the declared bounded release");
16622
16875
  }
@@ -16666,9 +16919,9 @@ async function runCommunityRehearsalHost(request) {
16666
16919
  }
16667
16920
 
16668
16921
  // src/supervised-app.ts
16669
- import { lstatSync as lstatSync8, readFileSync as readFileSync19 } from "fs";
16922
+ import { lstatSync as lstatSync9, readFileSync as readFileSync19 } from "fs";
16670
16923
  function readConfig(path2) {
16671
- const stat = lstatSync8(path2);
16924
+ const stat = lstatSync9(path2);
16672
16925
  if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 18) !== 0 || stat.size > 128 * 1024) {
16673
16926
  throw new Error("supervised app config is unsafe");
16674
16927
  }
@@ -16851,17 +17104,17 @@ function agentCommandArgs(args, command3) {
16851
17104
  return args.slice(index + 1);
16852
17105
  }
16853
17106
  function loadOrCreateSeed(path2) {
16854
- if (existsSync22(path2)) {
17107
+ if (existsSync23(path2)) {
16855
17108
  const seed2 = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16856
17109
  if (seed2.length < 32) {
16857
17110
  throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
16858
17111
  }
16859
17112
  return seed2;
16860
17113
  }
16861
- mkdirSync18(dirname14(path2), { recursive: true });
17114
+ mkdirSync18(dirname16(path2), { recursive: true });
16862
17115
  const seed = new Uint8Array(randomBytes7(32));
16863
17116
  writeFileSync18(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
16864
- chmodSync18(path2, 384);
17117
+ chmodSync19(path2, 384);
16865
17118
  return seed;
16866
17119
  }
16867
17120
  var DEFAULT_SOCKET_PATH = DEFAULT_SOCKET;
@@ -16890,7 +17143,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
16890
17143
  if (!directory)
16891
17144
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
16892
17145
  const path2 = `${directory}/${name}`;
16893
- if (!existsSync22(path2))
17146
+ if (!existsSync23(path2))
16894
17147
  throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
16895
17148
  const seed = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16896
17149
  if (seed.length < 32)
@@ -17052,7 +17305,7 @@ if (import.meta.main) {
17052
17305
  if (args.length !== 1)
17053
17306
  throw new Error("project-release-check accepts no coordinates");
17054
17307
  for (const relative of ["src/index.ts", "bun.lock", "src/generated/shared/contract-manifest.json"]) {
17055
- const stat = lstatSync9(join12(process.cwd(), relative));
17308
+ const stat = lstatSync10(join12(process.cwd(), relative));
17056
17309
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1) {
17057
17310
  throw new Error(`project release is missing ${relative}`);
17058
17311
  }
@@ -17391,9 +17644,9 @@ if (import.meta.main) {
17391
17644
  vcpu: cpus2().length,
17392
17645
  memoryGib: Math.max(1, Math.floor(totalmem2() / 1024 ** 3)),
17393
17646
  diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
17394
- kvm: existsSync22("/dev/kvm"),
17395
- snpHost: existsSync22("/dev/sev"),
17396
- helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17647
+ kvm: existsSync23("/dev/kvm"),
17648
+ snpHost: existsSync23("/dev/sev"),
17649
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17397
17650
  }
17398
17651
  };
17399
17652
  const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
@@ -17420,6 +17673,19 @@ if (import.meta.main) {
17420
17673
  const keys2 = deriveKeysFromSeed(seed);
17421
17674
  seed.fill(0);
17422
17675
  const nodeKey2 = process.env.FZ_NODE_KEY ?? keys2.ed25519.publicKey;
17676
+ if (process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true") {
17677
+ const ready = startAgentCandidateReady({
17678
+ version: VERSION2,
17679
+ nodeKey: nodeKey2,
17680
+ bound: true,
17681
+ vault: "unbound"
17682
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17683
+ console.log(`[metal-agent] candidate ${VERSION2} ready for atomic handover`);
17684
+ const stop2 = () => ready.close(() => process.exit(0));
17685
+ process.on("SIGTERM", stop2);
17686
+ process.on("SIGINT", stop2);
17687
+ await new Promise(() => {});
17688
+ }
17423
17689
  const pull = startProvisioningPull({
17424
17690
  apiUrl: process.env.FZ_API,
17425
17691
  nodeKey: nodeKey2,
@@ -17428,9 +17694,9 @@ if (import.meta.main) {
17428
17694
  metalHostname: process.env.FZ_METAL_HOSTNAME,
17429
17695
  run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
17430
17696
  metalPreflight: () => ({
17431
- snpHost: existsSync22("/dev/sev"),
17432
- kvm: existsSync22("/dev/kvm"),
17433
- helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17697
+ snpHost: existsSync23("/dev/sev"),
17698
+ kvm: existsSync23("/dev/kvm"),
17699
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17434
17700
  }),
17435
17701
  onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
17436
17702
  });
@@ -17513,7 +17779,11 @@ if (import.meta.main) {
17513
17779
  const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
17514
17780
  telemetry.event("agent.started");
17515
17781
  telemetry.setDraining(false);
17516
- const attestationSource = existsSync22("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17782
+ const handoverCandidate = process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true";
17783
+ if (!handoverCandidate && process.env.FZ_AGENT_ROUTE_SOCKET && process.env.FZ_SOCKET_PATH) {
17784
+ ensureAgentSocketRoute(process.env.FZ_AGENT_ROUTE_SOCKET, process.env.FZ_SOCKET_PATH);
17785
+ }
17786
+ const attestationSource = existsSync23("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17517
17787
  const running = runAgent({
17518
17788
  socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
17519
17789
  seedCredential: process.env.FZ_SEED_CREDENTIAL,
@@ -17528,8 +17798,8 @@ if (import.meta.main) {
17528
17798
  const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
17529
17799
  let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
17530
17800
  const enrolmentCredential = process.env.FZ_ENROL_TOKEN_CREDENTIAL;
17531
- const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync22(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
17532
- const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync22(process.env.FZ_ENROL_TOKEN_FILE));
17801
+ const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync23(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
17802
+ const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync23(process.env.FZ_ENROL_TOKEN_FILE));
17533
17803
  if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
17534
17804
  binding = await enrolGuestIdentity({
17535
17805
  apiUrl: process.env.FZ_API,
@@ -17549,6 +17819,7 @@ if (import.meta.main) {
17549
17819
  let nodeApiUrl = binding && process.env.FZ_API ? binding.realm === "platform" ? process.env.FZ_API : tenantNodeApiUrl(process.env.FZ_API, binding.tenantSlug) : process.env.FZ_API;
17550
17820
  let secretCache;
17551
17821
  let vaultSync;
17822
+ let initialVaultState = "unbound";
17552
17823
  if (binding && attestationSource && nodeApiUrl) {
17553
17824
  const result = await telemetry.observe("attestation.refresh", () => attestNodeOnce({
17554
17825
  apiUrl: nodeApiUrl,
@@ -17566,6 +17837,7 @@ if (import.meta.main) {
17566
17837
  projectKey: binding.projectKey
17567
17838
  });
17568
17839
  const initialVault = await telemetry.observe("vault.sync", () => initializeVaultReplica(secretCache), (result) => result.state === "ready" ? "success" : "failed");
17840
+ initialVaultState = initialVault.state;
17569
17841
  running.setVault(secretCache, binding.projectKey);
17570
17842
  vaultSync = startNodeVaultSync(secretCache, {
17571
17843
  telemetry,
@@ -17579,6 +17851,31 @@ if (import.meta.main) {
17579
17851
  console.error("[agent] vault replica unavailable; authenticated refresh remains active and only explicit " + "same-name systemd deployment fallbacks are available");
17580
17852
  }
17581
17853
  }
17854
+ if (handoverCandidate) {
17855
+ const ready = startAgentCandidateReady({
17856
+ version: VERSION2,
17857
+ nodeKey,
17858
+ bound: Boolean(binding),
17859
+ vault: initialVaultState
17860
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17861
+ console.log(`[agent] candidate ${VERSION2} ready for atomic handover`);
17862
+ let stopping = false;
17863
+ const stop = async () => {
17864
+ if (stopping)
17865
+ return;
17866
+ stopping = true;
17867
+ await vaultSync?.stop();
17868
+ await Promise.all([
17869
+ new Promise((resolve6) => ready.close(() => resolve6())),
17870
+ new Promise((resolve6) => server.close(() => resolve6()))
17871
+ ]);
17872
+ await telemetry.close();
17873
+ process.exit(0);
17874
+ };
17875
+ process.on("SIGTERM", () => void stop());
17876
+ process.on("SIGINT", () => void stop());
17877
+ await new Promise(() => {});
17878
+ }
17582
17879
  const attestationLoop = attestationSource && nodeApiUrl ? startNodeAttestation({
17583
17880
  apiUrl: nodeApiUrl,
17584
17881
  nodeKey,
@@ -17607,7 +17904,7 @@ if (import.meta.main) {
17607
17904
  const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
17608
17905
  const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
17609
17906
  const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
17610
- if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync22(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
17907
+ if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync23(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
17611
17908
  throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
17612
17909
  }
17613
17910
  const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({