@forgezero/agent 0.1.86 → 0.1.88

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.88";
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 && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.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,37 @@ 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, expectedNodeKey, 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({
12200
+ version: staged.version,
12201
+ nodeKey: expectedNodeKey,
12202
+ bound: true,
12203
+ vault: target2 === "compute" ? ["ready"] : ["unbound"]
12204
+ })) {
12205
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12206
+ clearAgentCandidate(dirname8(staged.currentLink));
12207
+ throw new Error("the candidate Agent did not prove its identity and durable state");
12208
+ }
12209
+ };
12210
+ var stopCandidate = async (staged, target2, run2) => {
12211
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12212
+ clearAgentCandidate(dirname8(staged.currentLink));
12213
+ };
12214
+ var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
12215
+ route: `${publicSocket}.backend`,
12216
+ active: `${publicSocket}.backend.active`,
12217
+ candidate: `${publicSocket}.backend.candidate`
12218
+ });
12083
12219
  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
12220
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12085
12221
  return new Promise((resolve4) => {
12086
- const socket = connect5(socketPath);
12222
+ const socket = connect6(socketPath);
12087
12223
  let settled = false;
12088
12224
  let buffer = "";
12089
12225
  const finish = (value) => {
@@ -12116,12 +12252,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12116
12252
  async function activateAgentRelease(staged, options = {}) {
12117
12253
  const run2 = options.run ?? runCommand;
12118
12254
  const target2 = options.target ?? "compute";
12119
- const probe = options.probe ?? targetProbe(target2, run2);
12255
+ const paths = socketPaths(options.publicSocketPath);
12256
+ const probe = options.probe ?? (target2 === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target2, run2));
12120
12257
  const now = options.now ?? Date.now;
12121
12258
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12122
12259
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12123
- const previous = readJournal(journalPath, dirname6(staged.currentLink));
12124
- const attemptId = options.attemptId ?? randomUUID3();
12260
+ const previous = readJournal(journalPath, dirname8(staged.currentLink));
12261
+ const attemptId = options.attemptId ?? randomUUID4();
12125
12262
  if (!ATTEMPT_ID.test(attemptId))
12126
12263
  throw new Error("Agent update attempt ID is invalid");
12127
12264
  const startedAtTs = now();
@@ -12143,11 +12280,19 @@ async function activateAgentRelease(staged, options = {}) {
12143
12280
  writeUpdateState(journalPath, receiptPath, journal);
12144
12281
  let selectionAttempted = false;
12145
12282
  try {
12283
+ if (!options.candidatePrepared) {
12284
+ throw new Error("direct activation requires a separately identity-bound candidate preparation");
12285
+ }
12286
+ if (target2 === "compute")
12287
+ switchAgentSocketRoute(paths.route, paths.candidate);
12146
12288
  selectionAttempted = true;
12147
12289
  selectAgentRelease(staged);
12148
12290
  await restartAgent(target2, run2);
12149
12291
  if (!await probe())
12150
- throw new Error("the replacement Agent did not answer its retained Vault socket");
12292
+ throw new Error("the replacement Agent did not answer its active Vault backend");
12293
+ if (target2 === "compute")
12294
+ switchAgentSocketRoute(paths.route, paths.active);
12295
+ await stopCandidate(staged, target2, run2);
12151
12296
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
12152
12297
  writeUpdateState(journalPath, receiptPath, journal);
12153
12298
  run2({
@@ -12165,10 +12310,13 @@ async function activateAgentRelease(staged, options = {}) {
12165
12310
  restored = true;
12166
12311
  await restartAgent(target2, run2);
12167
12312
  rollbackHealthy = await probe();
12313
+ if (rollbackHealthy && target2 === "compute")
12314
+ switchAgentSocketRoute(paths.route, paths.active);
12168
12315
  } catch {
12169
12316
  rollbackHealthy = false;
12170
12317
  }
12171
12318
  }
12319
+ await stopCandidate(staged, target2, run2).catch(() => {});
12172
12320
  const failures = failureCount + 1;
12173
12321
  const updatedAtTs = now();
12174
12322
  journal = {
@@ -12188,28 +12336,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12188
12336
  const root = resolve3(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
12189
12337
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12190
12338
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12339
+ const run2 = options.run ?? runCommand;
12191
12340
  const journal = readJournal(journalPath, root);
12192
- if (!journal)
12341
+ if (!journal) {
12342
+ for (const target2 of ["compute", "metal"]) {
12343
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12344
+ }
12345
+ clearAgentCandidate(root);
12193
12346
  return;
12347
+ }
12194
12348
  if (journal.outcome !== "activating") {
12349
+ await stopCandidate(stagedFromJournal(journal, root), journal.target, run2).catch(() => {});
12195
12350
  writeAtomic(receiptPath, publicReceipt(journal), 416);
12196
12351
  return publicReceipt(journal);
12197
12352
  }
12198
12353
  const staged = stagedFromJournal(journal, root);
12199
- if (!existsSync13(join8(root, journal.previousTarget))) {
12354
+ if (!existsSync14(join8(root, journal.previousTarget))) {
12200
12355
  throw new Error("Agent update rollback release is missing");
12201
12356
  }
12202
- const run2 = options.run ?? runCommand;
12203
- const probe = options.probe ?? targetProbe(journal.target, run2);
12357
+ const paths = socketPaths(options.publicSocketPath);
12358
+ const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run2));
12204
12359
  restoreAgentRelease(staged);
12205
12360
  let rollbackHealthy = false;
12206
12361
  let failureMessage = "activation was interrupted before its health verdict became durable";
12207
12362
  try {
12208
12363
  await restartAgent(journal.target, run2);
12209
12364
  rollbackHealthy = await probe();
12365
+ if (rollbackHealthy && journal.target === "compute")
12366
+ switchAgentSocketRoute(paths.route, paths.active);
12210
12367
  } catch (cause) {
12211
12368
  failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
12212
12369
  }
12370
+ await stopCandidate(staged, journal.target, run2).catch(() => {});
12213
12371
  const failures = journal.failureCount + 1;
12214
12372
  const updatedAtTs = (options.now ?? Date.now)();
12215
12373
  const recovered = {
@@ -12226,15 +12384,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12226
12384
  }
12227
12385
  function startAgentUpdateHelper(options = {}) {
12228
12386
  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));
12387
+ if (existsSync14(socketPath))
12388
+ unlinkSync11(socketPath);
12389
+ mkdirSync9(dirname8(socketPath), { recursive: true, mode: 488 });
12233
12390
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12234
12391
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12235
12392
  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 }));
12393
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
12394
+ const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, {
12395
+ target: target2,
12396
+ attemptId,
12397
+ journalPath,
12398
+ receiptPath,
12399
+ now: options.now,
12400
+ candidatePrepared: true
12401
+ }));
12237
12402
  let busy = true;
12403
+ let pending;
12238
12404
  let blocked;
12239
12405
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
12240
12406
  root: releaseRoot,
@@ -12246,7 +12412,7 @@ function startAgentUpdateHelper(options = {}) {
12246
12412
  }).finally(() => {
12247
12413
  busy = false;
12248
12414
  });
12249
- const server = createServer6((socket) => {
12415
+ const server = createServer7((socket) => {
12250
12416
  let buffer = "";
12251
12417
  socket.on("data", (chunk) => {
12252
12418
  buffer += chunk.toString("utf8");
@@ -12265,16 +12431,52 @@ function startAgentUpdateHelper(options = {}) {
12265
12431
  Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
12266
12432
  if (blocked)
12267
12433
  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
12434
  if (request.target !== "compute" && request.target !== "metal") {
12273
12435
  throw new Error("agent update target is invalid");
12274
12436
  }
12275
- const attemptId = request.attemptId ?? randomUUID3();
12437
+ const attemptId = request.attemptId ?? randomUUID4();
12276
12438
  if (!ATTEMPT_ID.test(attemptId))
12277
12439
  throw new Error("Agent update attempt ID is invalid");
12440
+ if (request.op === "commit" || request.op === "abort") {
12441
+ if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
12442
+ throw new Error("Agent update handover does not match the prepared candidate");
12443
+ }
12444
+ const selected = pending;
12445
+ pending = undefined;
12446
+ if (request.op === "abort") {
12447
+ await stopCandidate(selected.staged, selected.target, runCommand);
12448
+ busy = false;
12449
+ const response3 = {
12450
+ ok: true,
12451
+ status: "aborted",
12452
+ version: selected.staged.version,
12453
+ attemptId
12454
+ };
12455
+ socket.end(`${JSON.stringify(response3)}
12456
+ `);
12457
+ return;
12458
+ }
12459
+ const outcome = await activate(selected.staged, selected.target, attemptId);
12460
+ busy = false;
12461
+ if (outcome?.ok === false)
12462
+ throw new Error(outcome.reason ?? "Agent handover failed");
12463
+ const response2 = {
12464
+ ok: true,
12465
+ status: "active",
12466
+ version: selected.staged.version,
12467
+ attemptId
12468
+ };
12469
+ socket.end(`${JSON.stringify(response2)}
12470
+ `);
12471
+ return;
12472
+ }
12473
+ if (request.op !== "prepare")
12474
+ throw new Error("unknown update operation");
12475
+ if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
12476
+ throw new Error("Agent update expected node identity is invalid");
12477
+ }
12478
+ if (busy)
12479
+ throw new Error("another Agent update or recovery is already active");
12278
12480
  const prior = readJournal(journalPath, releaseRoot);
12279
12481
  const now = (options.now ?? Date.now)();
12280
12482
  if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
@@ -12285,17 +12487,23 @@ function startAgentUpdateHelper(options = {}) {
12285
12487
  currentVersion: request.currentVersion,
12286
12488
  root: releaseRoot
12287
12489
  });
12288
- const response = { ok: true, status: "staged", version: staged.version, attemptId };
12490
+ await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
12491
+ pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
12492
+ ownsBusy = false;
12493
+ setTimer(() => {
12494
+ if (pending?.attemptId !== attemptId)
12495
+ return;
12496
+ const expired = pending;
12497
+ pending = undefined;
12498
+ stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
12499
+ busy = false;
12500
+ });
12501
+ }, 180000);
12502
+ const response = { ok: true, status: "prepared", version: staged.version, attemptId };
12289
12503
  socket.end(`${JSON.stringify(response)}
12290
12504
  `);
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
12505
  }).catch((cause) => {
12298
- if (ownsBusy)
12506
+ if (ownsBusy && !pending)
12299
12507
  busy = false;
12300
12508
  const response = {
12301
12509
  ok: false,
@@ -12307,12 +12515,12 @@ function startAgentUpdateHelper(options = {}) {
12307
12515
  });
12308
12516
  socket.on("error", () => socket.destroy());
12309
12517
  });
12310
- server.listen(socketPath, () => chmodSync13(socketPath, 432));
12518
+ server.listen(socketPath, () => chmodSync14(socketPath, 432));
12311
12519
  return server;
12312
12520
  }
12313
12521
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
12314
12522
  return new Promise((resolve4, reject) => {
12315
- const socket = connect5(socketPath, () => socket.write(`${JSON.stringify(request)}
12523
+ const socket = connect6(socketPath, () => socket.write(`${JSON.stringify(request)}
12316
12524
  `));
12317
12525
  let buffer = "";
12318
12526
  socket.setTimeout(timeoutMs, () => {
@@ -12337,7 +12545,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
12337
12545
  }
12338
12546
 
12339
12547
  // src/agent-heartbeat.ts
12340
- import { existsSync as existsSync14, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12548
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12341
12549
  import { cpus, freemem, loadavg, totalmem } from "os";
12342
12550
  function readAgentHostMetrics() {
12343
12551
  const filesystem = statfsSync2("/");
@@ -12382,7 +12590,7 @@ function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = rea
12382
12590
  runtimeCapabilities: {
12383
12591
  native: true,
12384
12592
  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")
12593
+ 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
12594
  },
12387
12595
  capacity: {
12388
12596
  logicalCpu: Math.max(1, metrics.logicalCpu),
@@ -12434,18 +12642,31 @@ async function heartbeatAgentOnce(options) {
12434
12642
  });
12435
12643
  return response;
12436
12644
  }
12437
- let prepared = false;
12645
+ let candidatePrepared = false;
12646
+ let drained = false;
12438
12647
  try {
12439
- await options.prepareUpdate?.(release);
12440
- prepared = true;
12648
+ const update2 = options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate(phase === "prepare" ? {
12649
+ op: "prepare",
12650
+ target: options.updateTarget ?? "compute",
12651
+ release: next,
12652
+ currentVersion: current2,
12653
+ expectedNodeKey: options.nodeKey,
12654
+ attemptId
12655
+ } : {
12656
+ op: phase,
12657
+ target: options.updateTarget ?? "compute",
12658
+ currentVersion: current2,
12659
+ targetVersion: next.version,
12660
+ attemptId
12661
+ }));
12441
12662
  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);
12663
+ const prepared = await update2("prepare", release, observation.version, desired.attemptId);
12664
+ if (!prepared.ok)
12665
+ throw new AgentUpdateRefusedError(`agent update refused: ${prepared.error.message}`);
12666
+ candidatePrepared = true;
12667
+ await options.prepareUpdate?.(release);
12668
+ drained = true;
12669
+ const applied = await update2("commit", release, observation.version, desired.attemptId);
12449
12670
  if (!applied.ok)
12450
12671
  throw new AgentUpdateRefusedError(`agent update refused: ${applied.error.message}`);
12451
12672
  if (applied.attemptId !== desired.attemptId) {
@@ -12458,9 +12679,18 @@ async function heartbeatAgentOnce(options) {
12458
12679
  } else {
12459
12680
  await apply();
12460
12681
  }
12461
- options.onEvent?.("update-staged", { from: observation.version, to: release.version });
12682
+ options.onEvent?.("update-active", { from: observation.version, to: release.version });
12462
12683
  } catch (cause) {
12463
- if (prepared)
12684
+ if (candidatePrepared && !drained) {
12685
+ await (options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate({
12686
+ op: phase,
12687
+ target: options.updateTarget ?? "compute",
12688
+ currentVersion: current2,
12689
+ targetVersion: next.version,
12690
+ attemptId
12691
+ })))("abort", release, observation.version, desired.attemptId).catch(() => {});
12692
+ }
12693
+ if (drained)
12464
12694
  await options.recoverUpdate?.(cause);
12465
12695
  throw cause;
12466
12696
  }
@@ -12501,14 +12731,14 @@ function startAgentHeartbeat(options) {
12501
12731
  }
12502
12732
 
12503
12733
  // 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";
12734
+ import { chmodSync as chmodSync15, existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync12 } from "fs";
12735
+ import { connect as connect7, createServer as createServer8 } from "net";
12736
+ import { dirname as dirname12 } from "path";
12507
12737
 
12508
12738
  // src/service-supervisor.ts
12509
12739
  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";
12740
+ 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";
12741
+ import { dirname as dirname9, join as join9, resolve as resolve4, sep as sep2 } from "path";
12512
12742
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
12513
12743
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
12514
12744
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
@@ -12518,17 +12748,17 @@ var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
12518
12748
  var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
12519
12749
  var defaultHost = {
12520
12750
  write(path2, content, mode) {
12521
- mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
12751
+ mkdirSync10(dirname9(path2), { recursive: true, mode: 493 });
12522
12752
  const next = `${path2}.next`;
12523
12753
  writeFileSync11(next, content, { mode });
12524
- renameSync8(next, path2);
12754
+ renameSync10(next, path2);
12525
12755
  },
12526
12756
  read: (path2) => readFileSync12(path2, "utf8"),
12527
- exists: existsSync15,
12528
- list: (path2) => existsSync15(path2) ? readdirSync2(path2) : [],
12757
+ exists: existsSync16,
12758
+ list: (path2) => existsSync16(path2) ? readdirSync2(path2) : [],
12529
12759
  realpath: realpathSync2,
12530
12760
  mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
12531
- remove: (path2) => rmSync7(path2, { force: true }),
12761
+ remove: (path2) => rmSync8(path2, { force: true }),
12532
12762
  async exec(argv2) {
12533
12763
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
12534
12764
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -12724,20 +12954,20 @@ async function activateSupervisedService(request, host = defaultHost) {
12724
12954
 
12725
12955
  // src/container-supervisor.ts
12726
12956
  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";
12957
+ 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";
12958
+ import { dirname as dirname10, resolve as resolve5, sep as sep3 } from "path";
12729
12959
  var defaultHost2 = {
12730
12960
  realpath: realpathSync3,
12731
- exists: existsSync16,
12961
+ exists: existsSync17,
12732
12962
  read: (path2) => readFileSync13(path2, "utf8"),
12733
12963
  write(path2, content, mode) {
12734
- mkdirSync11(dirname8(path2), { recursive: true, mode: 493 });
12964
+ mkdirSync11(dirname10(path2), { recursive: true, mode: 493 });
12735
12965
  const next = `${path2}.next`;
12736
12966
  writeFileSync12(next, content, { mode });
12737
- renameSync9(next, path2);
12967
+ renameSync11(next, path2);
12738
12968
  },
12739
- remove: (path2) => rmSync8(path2, { force: true }),
12740
- list: (path2) => existsSync16(path2) ? readdirSync3(path2) : [],
12969
+ remove: (path2) => rmSync9(path2, { force: true }),
12970
+ list: (path2) => existsSync17(path2) ? readdirSync3(path2) : [],
12741
12971
  mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
12742
12972
  async exec(argv2) {
12743
12973
  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 +13256,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
13026
13256
 
13027
13257
  // src/deployment-connectivity.ts
13028
13258
  import { createHash as createHash11 } from "crypto";
13029
- import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
13259
+ import { mkdirSync as mkdirSync12, renameSync as renameSync12, writeFileSync as writeFileSync13 } from "fs";
13030
13260
  import { createConnection as createConnection2 } from "net";
13031
- import { dirname as dirname9 } from "path";
13261
+ import { dirname as dirname11 } from "path";
13032
13262
  var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
13033
13263
  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
13264
  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 +13271,7 @@ var checked7 = async (host, argv2, label) => {
13041
13271
  return result.output.trim();
13042
13272
  };
13043
13273
  async function sealWithSystemd(name, path2, value) {
13044
- mkdirSync12(dirname9(path2), { recursive: true, mode: 448 });
13274
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 448 });
13045
13275
  const next = `${path2}.next`;
13046
13276
  const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
13047
13277
  stdin: "pipe",
@@ -13057,15 +13287,15 @@ async function sealWithSystemd(name, path2, value) {
13057
13287
  ]);
13058
13288
  if (exitCode !== 0)
13059
13289
  throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
13060
- renameSync10(next, path2);
13290
+ renameSync12(next, path2);
13061
13291
  }
13062
13292
  var defaultHost3 = {
13063
13293
  seal: sealWithSystemd,
13064
13294
  write(path2, content, mode) {
13065
- mkdirSync12(dirname9(path2), { recursive: true, mode: 493 });
13295
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 493 });
13066
13296
  const next = `${path2}.next`;
13067
13297
  writeFileSync13(next, content, { mode });
13068
- renameSync10(next, path2);
13298
+ renameSync12(next, path2);
13069
13299
  },
13070
13300
  async exec(argv2) {
13071
13301
  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 +13534,9 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
13304
13534
  var MAX_PENDING_REQUESTS = 128;
13305
13535
  function startSoftwareHelper(options = {}) {
13306
13536
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
13307
- if (existsSync17(socketPath))
13308
- unlinkSync11(socketPath);
13309
- mkdirSync13(dirname10(socketPath), { recursive: true, mode: 488 });
13537
+ if (existsSync18(socketPath))
13538
+ unlinkSync12(socketPath);
13539
+ mkdirSync13(dirname12(socketPath), { recursive: true, mode: 488 });
13310
13540
  const ensure = options.ensure ?? ensureSoftwareRequirements;
13311
13541
  const activate = options.activate ?? activateSupervisedService;
13312
13542
  const buildContainer = options.buildContainer ?? buildContainerImage;
@@ -13314,7 +13544,7 @@ function startSoftwareHelper(options = {}) {
13314
13544
  const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
13315
13545
  let tail = Promise.resolve();
13316
13546
  let pending = 0;
13317
- const server = createServer7((socket) => {
13547
+ const server = createServer8((socket) => {
13318
13548
  let buffer = "";
13319
13549
  socket.on("data", (chunk) => {
13320
13550
  buffer += chunk.toString("utf8");
@@ -13393,12 +13623,12 @@ function startSoftwareHelper(options = {}) {
13393
13623
  });
13394
13624
  socket.on("error", () => socket.destroy());
13395
13625
  });
13396
- server.listen(socketPath, () => chmodSync14(socketPath, 432));
13626
+ server.listen(socketPath, () => chmodSync15(socketPath, 432));
13397
13627
  return server;
13398
13628
  }
13399
13629
  function requestContainer(op, request, socketPath, timeoutMs) {
13400
13630
  return new Promise((resolve6, reject) => {
13401
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13631
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13402
13632
  `));
13403
13633
  let buffer = "";
13404
13634
  socket.setTimeout(timeoutMs, () => {
@@ -13436,7 +13666,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
13436
13666
  }
13437
13667
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
13438
13668
  return new Promise((resolve6, reject) => {
13439
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13669
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13440
13670
  `));
13441
13671
  let buffer = "";
13442
13672
  socket.setTimeout(timeoutMs, () => {
@@ -13465,7 +13695,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
13465
13695
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
13466
13696
  validateSoftwareRequirements(requirements);
13467
13697
  return new Promise((resolve6, reject) => {
13468
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13698
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13469
13699
  `));
13470
13700
  let buffer = "";
13471
13701
  socket.setTimeout(timeoutMs, () => {
@@ -14425,19 +14655,19 @@ var METAL_SYSTEMD_CREDENTIALS = {
14425
14655
 
14426
14656
  // src/platform-bootstrap-runtime.ts
14427
14657
  import {
14428
- chmodSync as chmodSync15,
14658
+ chmodSync as chmodSync16,
14429
14659
  chownSync,
14430
14660
  copyFileSync as copyFileSync2,
14431
- existsSync as existsSync18,
14432
- lstatSync as lstatSync3,
14661
+ existsSync as existsSync19,
14662
+ lstatSync as lstatSync4,
14433
14663
  mkdirSync as mkdirSync14,
14434
14664
  readFileSync as readFileSync14,
14435
14665
  readdirSync as readdirSync4,
14436
14666
  realpathSync as realpathSync5,
14437
- renameSync as renameSync11,
14438
- rmSync as rmSync9,
14667
+ renameSync as renameSync13,
14668
+ rmSync as rmSync10,
14439
14669
  statSync as statSync4,
14440
- symlinkSync as symlinkSync4,
14670
+ symlinkSync as symlinkSync6,
14441
14671
  writeFileSync as writeFileSync14
14442
14672
  } from "fs";
14443
14673
  import { join as join10 } from "path";
@@ -14492,21 +14722,21 @@ var platformExec = async (argv2) => {
14492
14722
  };
14493
14723
  var replaceLink = (path2, target2) => {
14494
14724
  const pending = `${path2}.next`;
14495
- rmSync9(pending, { force: true });
14725
+ rmSync10(pending, { force: true });
14496
14726
  if (!target2) {
14497
- rmSync9(path2, { force: true });
14727
+ rmSync10(path2, { force: true });
14498
14728
  return;
14499
14729
  }
14500
- symlinkSync4(target2, pending);
14501
- renameSync11(pending, path2);
14730
+ symlinkSync6(target2, pending);
14731
+ renameSync13(pending, path2);
14502
14732
  };
14503
14733
  var secureRelease = (path2, uid, gid) => {
14504
14734
  const visit = (current2) => {
14505
- const metadata = lstatSync3(current2);
14735
+ const metadata = lstatSync4(current2);
14506
14736
  if (metadata.isSymbolicLink())
14507
14737
  throw new Error("release contains a symbolic link");
14508
14738
  chownSync(current2, uid, gid);
14509
- chmodSync15(current2, metadata.isDirectory() ? 365 : 292);
14739
+ chmodSync16(current2, metadata.isDirectory() ? 365 : 292);
14510
14740
  if (metadata.isDirectory())
14511
14741
  for (const name of readdirSync4(current2))
14512
14742
  visit(join10(current2, name));
@@ -14531,7 +14761,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14531
14761
  const slots = join10(normalized.root, "slots");
14532
14762
  mkdirSync14(slots, { recursive: true, mode: 493 });
14533
14763
  const slotFile = join10(normalized.root, ".forge-slot");
14534
- const previousSlot = existsSync18(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14764
+ const previousSlot = existsSync19(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14535
14765
  const target2 = previousSlot === "blue" ? "green" : "blue";
14536
14766
  const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
14537
14767
  const targetLink = join10(slots, target2);
@@ -14571,25 +14801,25 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14571
14801
  }
14572
14802
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
14573
14803
  const backup = `${upstream}.forgezero-backup`;
14574
- if (existsSync18(upstream))
14804
+ if (existsSync19(upstream))
14575
14805
  copyFileSync2(upstream, backup);
14576
14806
  else
14577
- rmSync9(backup, { force: true });
14807
+ rmSync10(backup, { force: true });
14578
14808
  writeFileSync14(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
14579
14809
  `, { mode: 420 });
14580
14810
  const test = await exec(["/usr/sbin/nginx", "-t"]);
14581
14811
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
14582
14812
  if (reload.exitCode !== 0) {
14583
- if (existsSync18(backup))
14584
- renameSync11(backup, upstream);
14813
+ if (existsSync19(backup))
14814
+ renameSync13(backup, upstream);
14585
14815
  else
14586
- rmSync9(upstream, { force: true });
14816
+ rmSync10(upstream, { force: true });
14587
14817
  await exec(["/usr/sbin/nginx", "-t"]);
14588
14818
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
14589
14819
  await stopTarget();
14590
14820
  throw new Error("nginx refused the promoted upstream");
14591
14821
  }
14592
- rmSync9(backup, { force: true });
14822
+ rmSync10(backup, { force: true });
14593
14823
  writeFileSync14(slotFile, `${target2}
14594
14824
  `, { mode: 420 });
14595
14825
  if (previousSlot && previousSlot !== target2) {
@@ -14599,12 +14829,12 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14599
14829
  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
14830
  for (const path2 of old)
14601
14831
  if (path2 !== release)
14602
- rmSync9(path2, { recursive: true, force: true });
14832
+ rmSync10(path2, { recursive: true, force: true });
14603
14833
  return { release, slot: target2 };
14604
14834
  }
14605
14835
 
14606
14836
  // src/recovery-host.ts
14607
- import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14837
+ import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14608
14838
  import { hostname } from "os";
14609
14839
  import { join as join11 } from "path";
14610
14840
  var execute2 = async (argv2) => {
@@ -14630,7 +14860,7 @@ var exactState = (path2, expected) => {
14630
14860
  throw new Error(`state mismatch: ${expected}`);
14631
14861
  };
14632
14862
  var nonEmpty = (path2) => {
14633
- if (!existsSync19(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14863
+ if (!existsSync20(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14634
14864
  throw new Error(`required file missing: ${path2}`);
14635
14865
  };
14636
14866
  var option = (args, name) => {
@@ -14726,20 +14956,20 @@ async function runRecoveryHost(args, run2 = execute2) {
14726
14956
  }
14727
14957
 
14728
14958
  // src/platform-fleet-verification.ts
14729
- import { lstatSync as lstatSync6 } from "fs";
14959
+ import { lstatSync as lstatSync7 } from "fs";
14730
14960
 
14731
14961
  // src/bootstrap.ts
14732
14962
  import {
14733
- chmodSync as chmodSync17,
14734
- existsSync as existsSync21,
14735
- lstatSync as lstatSync5,
14963
+ chmodSync as chmodSync18,
14964
+ existsSync as existsSync22,
14965
+ lstatSync as lstatSync6,
14736
14966
  mkdirSync as mkdirSync16,
14737
14967
  readFileSync as readFileSync17,
14738
- renameSync as renameSync13,
14739
- rmSync as rmSync11,
14968
+ renameSync as renameSync15,
14969
+ rmSync as rmSync12,
14740
14970
  writeFileSync as writeFileSync16
14741
14971
  } from "fs";
14742
- import { dirname as dirname12 } from "path";
14972
+ import { dirname as dirname14 } from "path";
14743
14973
  import { fileURLToPath as fileURLToPath2 } from "url";
14744
14974
 
14745
14975
  // src/provision.ts
@@ -14796,6 +15026,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
14796
15026
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
14797
15027
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
14798
15028
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
15029
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
14799
15030
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
14800
15031
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
14801
15032
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -14907,6 +15138,7 @@ Type=simple
14907
15138
  User=root
14908
15139
  Group=${AGENT_UPDATE_GROUP}
14909
15140
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
15141
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
14910
15142
  ExecStart=${bin} update-helper
14911
15143
  Restart=always
14912
15144
  RestartSec=2
@@ -14951,13 +15183,11 @@ WantedBy=sockets.target
14951
15183
  `;
14952
15184
  }
14953
15185
  function agentSocketProxyUnit(options) {
14954
- const backend = agentBackendSocketPath(options.socketPath);
15186
+ const backend = agentRoutingSocketPath(options.socketPath);
14955
15187
  const user = options.user ?? "forgezero";
14956
15188
  return `[Unit]
14957
15189
  Description=ForgeZero application Vault socket proxy
14958
15190
  Documentation=https://www.forgezero.net/docs/agent
14959
- Requires=forgezero-agent.service
14960
- After=forgezero-agent.service
14961
15191
 
14962
15192
  [Service]
14963
15193
  User=${user}
@@ -14978,12 +15208,26 @@ RestrictAddressFamilies=AF_UNIX
14978
15208
  `;
14979
15209
  }
14980
15210
  function agentBackendSocketPath(publicSocketPath) {
15211
+ const socket = systemdPath(publicSocketPath, "agent socket");
15212
+ const backend = `${socket}.backend.active`;
15213
+ if (Buffer.byteLength(backend) > 100)
15214
+ throw new Error("agent socket path is too long for a Unix socket");
15215
+ return backend;
15216
+ }
15217
+ function agentRoutingSocketPath(publicSocketPath) {
14981
15218
  const socket = systemdPath(publicSocketPath, "agent socket");
14982
15219
  const backend = `${socket}.backend`;
14983
15220
  if (Buffer.byteLength(backend) > 100)
14984
15221
  throw new Error("agent socket path is too long for a Unix socket");
14985
15222
  return backend;
14986
15223
  }
15224
+ function agentCandidateSocketPath(publicSocketPath) {
15225
+ const socket = systemdPath(publicSocketPath, "agent socket");
15226
+ const backend = `${socket}.backend.candidate`;
15227
+ if (Buffer.byteLength(backend) > 100)
15228
+ throw new Error("agent socket path is too long for a Unix socket");
15229
+ return backend;
15230
+ }
14987
15231
  var systemdPath = (value, label) => {
14988
15232
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
14989
15233
  throw new Error(`invalid ${label} path`);
@@ -15267,7 +15511,11 @@ function agentUnit(options) {
15267
15511
  }
15268
15512
  const environment = [
15269
15513
  "NODE_ENV=production",
15270
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
15514
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
15515
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
15516
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
15517
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
15518
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
15271
15519
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
15272
15520
  `FZ_SEED_CREDENTIAL=agent-seed`,
15273
15521
  `FZ_AGENT_MODE=${options.mode}`,
@@ -15404,6 +15652,26 @@ ${deploymentWrites}
15404
15652
  WantedBy=multi-user.target
15405
15653
  `;
15406
15654
  }
15655
+ function agentCandidateUnit(options) {
15656
+ return agentUnit({
15657
+ ...options,
15658
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
15659
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
15660
+ handoverCandidate: true,
15661
+ gitCredentialPath: undefined,
15662
+ deploymentCredentials: {},
15663
+ bootstrapSshCredentialPath: undefined,
15664
+ bootstrapSshPublicKeyPath: undefined,
15665
+ pullBootstrap: false,
15666
+ pullDeployments: false,
15667
+ pullMigrations: false,
15668
+ lifecycleProfilePath: undefined,
15669
+ bootstrapTargetTelemetryEndpoint: undefined,
15670
+ repository: undefined,
15671
+ bootstrapBundlePath: undefined,
15672
+ bootstrapBundleManifestPath: undefined
15673
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
15674
+ }
15407
15675
  var renderOperation = (operation) => {
15408
15676
  if (operation.kind === "commands")
15409
15677
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -15539,6 +15807,7 @@ function planProvision(options) {
15539
15807
  auxiliaryUnits: [
15540
15808
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
15541
15809
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
15810
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
15542
15811
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
15543
15812
  ...options.enforceEgress ? [
15544
15813
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -15670,19 +15939,19 @@ function planProvision(options) {
15670
15939
  // src/cli/agent-install.ts
15671
15940
  import { randomBytes as randomBytes6 } from "crypto";
15672
15941
  import {
15673
- chmodSync as chmodSync16,
15942
+ chmodSync as chmodSync17,
15674
15943
  copyFileSync as copyFileSync3,
15675
- existsSync as existsSync20,
15676
- lstatSync as lstatSync4,
15944
+ existsSync as existsSync21,
15945
+ lstatSync as lstatSync5,
15677
15946
  mkdirSync as mkdirSync15,
15678
15947
  readFileSync as readFileSync16,
15679
15948
  realpathSync as realpathSync6,
15680
- renameSync as renameSync12,
15681
- rmSync as rmSync10,
15682
- symlinkSync as symlinkSync5,
15949
+ renameSync as renameSync14,
15950
+ rmSync as rmSync11,
15951
+ symlinkSync as symlinkSync7,
15683
15952
  writeFileSync as writeFileSync15
15684
15953
  } from "fs";
15685
- import { dirname as dirname11 } from "path";
15954
+ import { dirname as dirname13 } from "path";
15686
15955
  async function readCapabilities(run2) {
15687
15956
  const answers = {};
15688
15957
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -15740,51 +16009,51 @@ var runProvisionOperation = async (operation) => {
15740
16009
  if (operation.kind === "install-runtime") {
15741
16010
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
15742
16011
  mkdirSync15(`${release}/dist`, { recursive: true, mode: 493 });
15743
- mkdirSync15(dirname11(operation.binary), { recursive: true, mode: 493 });
16012
+ mkdirSync15(dirname13(operation.binary), { recursive: true, mode: 493 });
15744
16013
  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))
16014
+ chmodSync17(`${release}/dist/fz-agent.js`, 493);
16015
+ const gitSshSource = `${dirname13(operation.source)}/fz-git-ssh.js`;
16016
+ if (!existsSync21(gitSshSource))
15748
16017
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
15749
16018
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
15750
- chmodSync16(`${release}/dist/fz-git-ssh.js`, 493);
16019
+ chmodSync17(`${release}/dist/fz-git-ssh.js`, 493);
15751
16020
  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);
16021
+ rmSync11(pending, { force: true });
16022
+ symlinkSync7(`versions/${operation.version}`, pending);
16023
+ renameSync14(pending, "/opt/forgezero/agent/current");
16024
+ rmSync11(operation.binary, { force: true });
16025
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
15757
16026
  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);
16027
+ rmSync11(gitSshBinary, { force: true });
16028
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
15760
16029
  return { stdout: "", exitCode: 0 };
15761
16030
  }
15762
16031
  if (operation.kind === "ensure-seed") {
15763
- if (existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16032
+ if (existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15764
16033
  return { stdout: "", exitCode: 0 };
15765
16034
  const seed = randomBytes6(32).toString("base64url");
15766
16035
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
15767
16036
  if (result.exitCode === 0)
15768
- chmodSync16(operation.credential, 256);
16037
+ chmodSync17(operation.credential, 256);
15769
16038
  return result;
15770
16039
  }
15771
16040
  if (operation.kind === "ensure-git-identity") {
15772
16041
  const key = "/run/forgezero-git-deploy-key";
15773
16042
  const publicKey = `${key}.pub`;
15774
16043
  try {
15775
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15776
- rmSync10(key, { force: true });
15777
- rmSync10(publicKey, { force: true });
16044
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16045
+ rmSync11(key, { force: true });
16046
+ rmSync11(publicKey, { force: true });
15778
16047
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
15779
16048
  if (result.exitCode !== 0)
15780
16049
  return result;
15781
16050
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
15782
16051
  if (result.exitCode !== 0)
15783
16052
  return result;
15784
- chmodSync16(operation.credential, 256);
16053
+ chmodSync17(operation.credential, 256);
15785
16054
  }
15786
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15787
- if (!existsSync20(key)) {
16055
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16056
+ if (!existsSync21(key)) {
15788
16057
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
15789
16058
  if (decrypted.exitCode !== 0)
15790
16059
  return decrypted;
@@ -15797,25 +16066,25 @@ var runProvisionOperation = async (operation) => {
15797
16066
  }
15798
16067
  return { stdout: "", exitCode: 0 };
15799
16068
  } finally {
15800
- rmSync10(key, { force: true });
15801
- rmSync10(publicKey, { force: true });
16069
+ rmSync11(key, { force: true });
16070
+ rmSync11(publicKey, { force: true });
15802
16071
  }
15803
16072
  }
15804
16073
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
15805
16074
  const key = "/run/forgezero-bootstrap-ssh-key";
15806
16075
  const generatedPublicKey = `${key}.pub`;
15807
16076
  try {
15808
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15809
- rmSync10(key, { force: true });
15810
- rmSync10(generatedPublicKey, { force: true });
16077
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16078
+ rmSync11(key, { force: true });
16079
+ rmSync11(generatedPublicKey, { force: true });
15811
16080
  let result;
15812
16081
  if (operation.source) {
15813
- const source = existsSync20(operation.source) ? lstatSync4(operation.source) : undefined;
16082
+ const source = existsSync21(operation.source) ? lstatSync5(operation.source) : undefined;
15814
16083
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
15815
16084
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
15816
16085
  }
15817
16086
  copyFileSync3(operation.source, key);
15818
- chmodSync16(key, 384);
16087
+ chmodSync17(key, 384);
15819
16088
  } else {
15820
16089
  result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
15821
16090
  if (result.exitCode !== 0)
@@ -15828,48 +16097,48 @@ var runProvisionOperation = async (operation) => {
15828
16097
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
15829
16098
  if (result.exitCode !== 0)
15830
16099
  return result;
15831
- chmodSync16(operation.credential, 256);
16100
+ chmodSync17(operation.credential, 256);
15832
16101
  }
15833
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15834
- if (!existsSync20(key)) {
16102
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16103
+ if (!existsSync21(key)) {
15835
16104
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
15836
16105
  if (decrypted.exitCode !== 0)
15837
16106
  return decrypted;
15838
- chmodSync16(key, 384);
16107
+ chmodSync17(key, 384);
15839
16108
  }
15840
16109
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
15841
16110
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
15842
16111
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
15843
16112
  }
15844
- mkdirSync15(dirname11(operation.publicKey), { recursive: true, mode: 493 });
16113
+ mkdirSync15(dirname13(operation.publicKey), { recursive: true, mode: 493 });
15845
16114
  writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
15846
16115
  `, { mode: 292 });
15847
- chmodSync16(operation.publicKey, 292);
16116
+ chmodSync17(operation.publicKey, 292);
15848
16117
  }
15849
16118
  if (operation.source)
15850
- rmSync10(operation.source, { force: true });
16119
+ rmSync11(operation.source, { force: true });
15851
16120
  return { stdout: "", exitCode: 0 };
15852
16121
  } finally {
15853
- rmSync10(key, { force: true });
15854
- rmSync10(generatedPublicKey, { force: true });
16122
+ rmSync11(key, { force: true });
16123
+ rmSync11(generatedPublicKey, { force: true });
15855
16124
  }
15856
16125
  }
15857
16126
  if (operation.kind === "ensure-enrolment") {
15858
- if (existsSync20(operation.state) && lstatSync4(operation.state).size > 0 || existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16127
+ if (existsSync21(operation.state) && lstatSync5(operation.state).size > 0 || existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15859
16128
  return { stdout: "", exitCode: 0 };
15860
- if (!existsSync20(operation.source))
16129
+ if (!existsSync21(operation.source))
15861
16130
  return { stdout: "enrolment source is missing", exitCode: 1 };
15862
16131
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
15863
16132
  if (result.exitCode === 0) {
15864
- chmodSync16(operation.credential, 256);
15865
- rmSync10(operation.source, { force: true });
16133
+ chmodSync17(operation.credential, 256);
16134
+ rmSync11(operation.source, { force: true });
15866
16135
  }
15867
16136
  return result;
15868
16137
  }
15869
16138
  if (operation.kind === "wait-socket") {
15870
16139
  for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
15871
16140
  try {
15872
- if (lstatSync4(operation.path).isSocket())
16141
+ if (lstatSync5(operation.path).isSocket())
15873
16142
  return { stdout: "", exitCode: 0 };
15874
16143
  } catch {}
15875
16144
  await Bun.sleep(operation.intervalMs);
@@ -15877,7 +16146,7 @@ var runProvisionOperation = async (operation) => {
15877
16146
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
15878
16147
  }
15879
16148
  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 };
16149
+ return existsSync21(operation.path) && lstatSync5(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
15881
16150
  if (operation.kind === "verify-egress") {
15882
16151
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
15883
16152
  if (active.exitCode !== 0)
@@ -15911,7 +16180,7 @@ var runProvisionOperation = async (operation) => {
15911
16180
  const key = "/run/cloudflare-warp-key.gpg";
15912
16181
  writeFileSync15(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
15913
16182
  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 });
16183
+ rmSync11(key, { force: true });
15915
16184
  if (result.exitCode !== 0)
15916
16185
  return result;
15917
16186
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
@@ -15932,7 +16201,7 @@ async function localRunner(operation) {
15932
16201
  if (capability.kind === "version")
15933
16202
  return fixed(capability.argv);
15934
16203
  try {
15935
- const metadata = lstatSync4(capability.path);
16204
+ const metadata = lstatSync5(capability.path);
15936
16205
  const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
15937
16206
  return { stdout: present ? `yes
15938
16207
  ` : `no
@@ -16230,19 +16499,19 @@ function localBootstrapHost() {
16230
16499
  };
16231
16500
  return {
16232
16501
  uid: () => process.getuid?.() ?? -1,
16233
- exists: existsSync21,
16502
+ exists: existsSync22,
16234
16503
  read: (path2) => readFileSync17(path2, "utf8"),
16235
16504
  write(path2, content, mode) {
16236
- mkdirSync16(dirname12(path2), { recursive: true, mode: 493 });
16505
+ mkdirSync16(dirname14(path2), { recursive: true, mode: 493 });
16237
16506
  const temporary = `${path2}.next.${process.pid}`;
16238
16507
  writeFileSync16(temporary, content, { mode });
16239
- chmodSync17(temporary, mode);
16240
- renameSync13(temporary, path2);
16508
+ chmodSync18(temporary, mode);
16509
+ renameSync15(temporary, path2);
16241
16510
  },
16242
16511
  mkdir: (path2, mode) => mkdirSync16(path2, { recursive: true, mode }),
16243
- remove: (path2) => rmSync11(path2, { force: true }),
16512
+ remove: (path2) => rmSync12(path2, { force: true }),
16244
16513
  inspect(path2) {
16245
- const value = lstatSync5(path2);
16514
+ const value = lstatSync6(path2);
16246
16515
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
16247
16516
  },
16248
16517
  exec: execute3,
@@ -16263,9 +16532,9 @@ function localBootstrapHost() {
16263
16532
  },
16264
16533
  async installAgent(config, phase) {
16265
16534
  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) ? {
16535
+ const hasBinding = existsSync22("/var/lib/forgezero/enrolment.json");
16536
+ const hasEnrolCredential = existsSync22(ENROL_CREDENTIAL);
16537
+ const initialBundle = config.kind === "platform" && !existsSync22(BOOTSTRAP_RELEASE_EVIDENCE) ? {
16269
16538
  path: config.bootstrapBundle.bundleFile,
16270
16539
  manifestPath: config.bootstrapBundle.manifestFile,
16271
16540
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync17(config.bootstrapBundle.manifestFile, "utf8")))
@@ -16281,7 +16550,7 @@ function localBootstrapHost() {
16281
16550
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
16282
16551
  databasePorts: [8529]
16283
16552
  };
16284
- mkdirSync16(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16553
+ mkdirSync16(dirname14(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16285
16554
  writeFileSync16(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
16286
16555
  `, { mode: 256 });
16287
16556
  }
@@ -16292,7 +16561,7 @@ function localBootstrapHost() {
16292
16561
  initialBundle
16293
16562
  });
16294
16563
  for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
16295
- mkdirSync16(dirname12(unit3.path), { recursive: true, mode: 493 });
16564
+ mkdirSync16(dirname14(unit3.path), { recursive: true, mode: 493 });
16296
16565
  writeFileSync16(unit3.path, unit3.unit, { mode: 420 });
16297
16566
  }
16298
16567
  await applyPlan(plan, localRunner);
@@ -16307,7 +16576,7 @@ var localRuntime = () => ({
16307
16576
  bootstrapStatus: () => bootstrapStatus(),
16308
16577
  characterDevice(path2) {
16309
16578
  try {
16310
- return lstatSync6(path2).isCharacterDevice();
16579
+ return lstatSync7(path2).isCharacterDevice();
16311
16580
  } catch {
16312
16581
  return false;
16313
16582
  }
@@ -16346,9 +16615,9 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
16346
16615
 
16347
16616
  // src/community-rehearsal-host.ts
16348
16617
  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";
16618
+ 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
16619
  import { isIP as isIP5 } from "net";
16351
- import { dirname as dirname13 } from "path";
16620
+ import { dirname as dirname15 } from "path";
16352
16621
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
16353
16622
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
16354
16623
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
@@ -16562,19 +16831,19 @@ async function exec(argv2, stdin) {
16562
16831
  async function applyOperations(operations) {
16563
16832
  for (const operation of operations) {
16564
16833
  if (operation.kind === "remove-tree")
16565
- rmSync12(operation.path, { recursive: true, force: true });
16834
+ rmSync13(operation.path, { recursive: true, force: true });
16566
16835
  else if (operation.kind === "write") {
16567
- mkdirSync17(dirname13(operation.path), { recursive: true });
16836
+ mkdirSync17(dirname15(operation.path), { recursive: true });
16568
16837
  writeFileSync17(operation.path, operation.content, { mode: operation.mode });
16569
16838
  } else if (operation.kind === "unlink") {
16570
16839
  try {
16571
- unlinkSync12(operation.path);
16840
+ unlinkSync13(operation.path);
16572
16841
  } catch (cause) {
16573
16842
  if (cause.code !== "ENOENT")
16574
16843
  throw cause;
16575
16844
  }
16576
16845
  } else if (operation.kind === "symlink")
16577
- symlinkSync6(operation.target, operation.path);
16846
+ symlinkSync8(operation.target, operation.path);
16578
16847
  else {
16579
16848
  const result = await exec(operation.argv, operation.stdin);
16580
16849
  if (!(operation.accepted ?? [0]).includes(result.exitCode))
@@ -16616,7 +16885,7 @@ async function runCommunityRehearsalHost(request) {
16616
16885
  return { ok: true, action: request.action, node: node.name };
16617
16886
  }
16618
16887
  if (request.action === "prepare") {
16619
- const metadata = lstatSync7(request.archivePath);
16888
+ const metadata = lstatSync8(request.archivePath);
16620
16889
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash12("sha256").update(readFileSync18(request.archivePath)).digest("hex") !== request.archiveSha256) {
16621
16890
  throw new Error("community rehearsal archive is not the declared bounded release");
16622
16891
  }
@@ -16666,9 +16935,9 @@ async function runCommunityRehearsalHost(request) {
16666
16935
  }
16667
16936
 
16668
16937
  // src/supervised-app.ts
16669
- import { lstatSync as lstatSync8, readFileSync as readFileSync19 } from "fs";
16938
+ import { lstatSync as lstatSync9, readFileSync as readFileSync19 } from "fs";
16670
16939
  function readConfig(path2) {
16671
- const stat = lstatSync8(path2);
16940
+ const stat = lstatSync9(path2);
16672
16941
  if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 18) !== 0 || stat.size > 128 * 1024) {
16673
16942
  throw new Error("supervised app config is unsafe");
16674
16943
  }
@@ -16851,17 +17120,17 @@ function agentCommandArgs(args, command3) {
16851
17120
  return args.slice(index + 1);
16852
17121
  }
16853
17122
  function loadOrCreateSeed(path2) {
16854
- if (existsSync22(path2)) {
17123
+ if (existsSync23(path2)) {
16855
17124
  const seed2 = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16856
17125
  if (seed2.length < 32) {
16857
17126
  throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
16858
17127
  }
16859
17128
  return seed2;
16860
17129
  }
16861
- mkdirSync18(dirname14(path2), { recursive: true });
17130
+ mkdirSync18(dirname16(path2), { recursive: true });
16862
17131
  const seed = new Uint8Array(randomBytes7(32));
16863
17132
  writeFileSync18(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
16864
- chmodSync18(path2, 384);
17133
+ chmodSync19(path2, 384);
16865
17134
  return seed;
16866
17135
  }
16867
17136
  var DEFAULT_SOCKET_PATH = DEFAULT_SOCKET;
@@ -16890,7 +17159,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
16890
17159
  if (!directory)
16891
17160
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
16892
17161
  const path2 = `${directory}/${name}`;
16893
- if (!existsSync22(path2))
17162
+ if (!existsSync23(path2))
16894
17163
  throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
16895
17164
  const seed = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16896
17165
  if (seed.length < 32)
@@ -17052,7 +17321,7 @@ if (import.meta.main) {
17052
17321
  if (args.length !== 1)
17053
17322
  throw new Error("project-release-check accepts no coordinates");
17054
17323
  for (const relative of ["src/index.ts", "bun.lock", "src/generated/shared/contract-manifest.json"]) {
17055
- const stat = lstatSync9(join12(process.cwd(), relative));
17324
+ const stat = lstatSync10(join12(process.cwd(), relative));
17056
17325
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1) {
17057
17326
  throw new Error(`project release is missing ${relative}`);
17058
17327
  }
@@ -17391,9 +17660,9 @@ if (import.meta.main) {
17391
17660
  vcpu: cpus2().length,
17392
17661
  memoryGib: Math.max(1, Math.floor(totalmem2() / 1024 ** 3)),
17393
17662
  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)
17663
+ kvm: existsSync23("/dev/kvm"),
17664
+ snpHost: existsSync23("/dev/sev"),
17665
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17397
17666
  }
17398
17667
  };
17399
17668
  const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
@@ -17420,6 +17689,19 @@ if (import.meta.main) {
17420
17689
  const keys2 = deriveKeysFromSeed(seed);
17421
17690
  seed.fill(0);
17422
17691
  const nodeKey2 = process.env.FZ_NODE_KEY ?? keys2.ed25519.publicKey;
17692
+ if (process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true") {
17693
+ const ready = startAgentCandidateReady({
17694
+ version: VERSION2,
17695
+ nodeKey: nodeKey2,
17696
+ bound: true,
17697
+ vault: "unbound"
17698
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17699
+ console.log(`[metal-agent] candidate ${VERSION2} ready for atomic handover`);
17700
+ const stop2 = () => ready.close(() => process.exit(0));
17701
+ process.on("SIGTERM", stop2);
17702
+ process.on("SIGINT", stop2);
17703
+ await new Promise(() => {});
17704
+ }
17423
17705
  const pull = startProvisioningPull({
17424
17706
  apiUrl: process.env.FZ_API,
17425
17707
  nodeKey: nodeKey2,
@@ -17428,9 +17710,9 @@ if (import.meta.main) {
17428
17710
  metalHostname: process.env.FZ_METAL_HOSTNAME,
17429
17711
  run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
17430
17712
  metalPreflight: () => ({
17431
- snpHost: existsSync22("/dev/sev"),
17432
- kvm: existsSync22("/dev/kvm"),
17433
- helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17713
+ snpHost: existsSync23("/dev/sev"),
17714
+ kvm: existsSync23("/dev/kvm"),
17715
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17434
17716
  }),
17435
17717
  onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
17436
17718
  });
@@ -17513,7 +17795,11 @@ if (import.meta.main) {
17513
17795
  const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
17514
17796
  telemetry.event("agent.started");
17515
17797
  telemetry.setDraining(false);
17516
- const attestationSource = existsSync22("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17798
+ const handoverCandidate = process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true";
17799
+ if (!handoverCandidate && process.env.FZ_AGENT_ROUTE_SOCKET && process.env.FZ_SOCKET_PATH) {
17800
+ ensureAgentSocketRoute(process.env.FZ_AGENT_ROUTE_SOCKET, process.env.FZ_SOCKET_PATH);
17801
+ }
17802
+ const attestationSource = existsSync23("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17517
17803
  const running = runAgent({
17518
17804
  socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
17519
17805
  seedCredential: process.env.FZ_SEED_CREDENTIAL,
@@ -17528,8 +17814,8 @@ if (import.meta.main) {
17528
17814
  const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
17529
17815
  let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
17530
17816
  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));
17817
+ const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync23(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
17818
+ const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync23(process.env.FZ_ENROL_TOKEN_FILE));
17533
17819
  if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
17534
17820
  binding = await enrolGuestIdentity({
17535
17821
  apiUrl: process.env.FZ_API,
@@ -17549,6 +17835,7 @@ if (import.meta.main) {
17549
17835
  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
17836
  let secretCache;
17551
17837
  let vaultSync;
17838
+ let initialVaultState = "unbound";
17552
17839
  if (binding && attestationSource && nodeApiUrl) {
17553
17840
  const result = await telemetry.observe("attestation.refresh", () => attestNodeOnce({
17554
17841
  apiUrl: nodeApiUrl,
@@ -17566,6 +17853,7 @@ if (import.meta.main) {
17566
17853
  projectKey: binding.projectKey
17567
17854
  });
17568
17855
  const initialVault = await telemetry.observe("vault.sync", () => initializeVaultReplica(secretCache), (result) => result.state === "ready" ? "success" : "failed");
17856
+ initialVaultState = initialVault.state;
17569
17857
  running.setVault(secretCache, binding.projectKey);
17570
17858
  vaultSync = startNodeVaultSync(secretCache, {
17571
17859
  telemetry,
@@ -17579,6 +17867,31 @@ if (import.meta.main) {
17579
17867
  console.error("[agent] vault replica unavailable; authenticated refresh remains active and only explicit " + "same-name systemd deployment fallbacks are available");
17580
17868
  }
17581
17869
  }
17870
+ if (handoverCandidate) {
17871
+ const ready = startAgentCandidateReady({
17872
+ version: VERSION2,
17873
+ nodeKey,
17874
+ bound: Boolean(binding),
17875
+ vault: initialVaultState
17876
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17877
+ console.log(`[agent] candidate ${VERSION2} ready for atomic handover`);
17878
+ let stopping = false;
17879
+ const stop = async () => {
17880
+ if (stopping)
17881
+ return;
17882
+ stopping = true;
17883
+ await vaultSync?.stop();
17884
+ await Promise.all([
17885
+ new Promise((resolve6) => ready.close(() => resolve6())),
17886
+ new Promise((resolve6) => server.close(() => resolve6()))
17887
+ ]);
17888
+ await telemetry.close();
17889
+ process.exit(0);
17890
+ };
17891
+ process.on("SIGTERM", () => void stop());
17892
+ process.on("SIGINT", () => void stop());
17893
+ await new Promise(() => {});
17894
+ }
17582
17895
  const attestationLoop = attestationSource && nodeApiUrl ? startNodeAttestation({
17583
17896
  apiUrl: nodeApiUrl,
17584
17897
  nodeKey,
@@ -17607,7 +17920,7 @@ if (import.meta.main) {
17607
17920
  const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
17608
17921
  const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
17609
17922
  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)) {
17923
+ if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync23(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
17611
17924
  throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
17612
17925
  }
17613
17926
  const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({