@forgezero/agent 0.1.85 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz-agent.js CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { randomBytes as randomBytes7 } from "crypto";
6
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, existsSync as existsSync22, mkdirSync as mkdirSync18, chmodSync as chmodSync18, lstatSync as lstatSync9, statfsSync as statfsSync3 } from "fs";
6
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, existsSync as existsSync23, mkdirSync as mkdirSync18, chmodSync as chmodSync19, lstatSync as lstatSync10, statfsSync as statfsSync3 } from "fs";
7
7
  import { cpus as cpus2, totalmem as totalmem2 } from "os";
8
- import { dirname as dirname14, join as join12 } from "path";
8
+ import { dirname as dirname16, join as join12 } from "path";
9
9
 
10
10
  // ../access/dist/security.js
11
11
  var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
@@ -4201,7 +4201,16 @@ var DEFAULT_SOCKET = "/run/forgezero/vault.sock";
4201
4201
 
4202
4202
  // src/socket.ts
4203
4203
  import { createServer } from "net";
4204
- import { chmodSync, existsSync, unlinkSync } from "fs";
4204
+ import {
4205
+ existsSync,
4206
+ chmodSync,
4207
+ lstatSync,
4208
+ readlinkSync,
4209
+ renameSync,
4210
+ symlinkSync,
4211
+ unlinkSync
4212
+ } from "fs";
4213
+ import { basename, dirname } from "path";
4205
4214
 
4206
4215
  // src/cache.ts
4207
4216
  class CacheError extends Error {
@@ -4326,9 +4335,11 @@ function createSecretCache(options) {
4326
4335
  // src/signed-node-http.ts
4327
4336
  class SignedNodeHttpError extends Error {
4328
4337
  status;
4329
- constructor(status, message) {
4338
+ code;
4339
+ constructor(status, message, code) {
4330
4340
  super(message);
4331
4341
  this.status = status;
4342
+ this.code = code;
4332
4343
  this.name = "SignedNodeHttpError";
4333
4344
  }
4334
4345
  }
@@ -4375,7 +4386,7 @@ async function postSignedNode(options, path, body) {
4375
4386
  if (!response.ok) {
4376
4387
  const failure = payload;
4377
4388
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
4378
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
4389
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
4379
4390
  }
4380
4391
  try {
4381
4392
  return await openResponse(recipient.secretKey, signature, payload);
@@ -4413,8 +4424,17 @@ function tenantNodeApiUrl(apiUrl, tenantSlug) {
4413
4424
  return url.toString().replace(/\/$/, "");
4414
4425
  }
4415
4426
  function createNodeVaultCache(options) {
4416
- const post = (operation, body) => postSignedNode(options, `v1/node/vault/${operation}`, body);
4417
- return createSecretCache({
4427
+ let cache;
4428
+ const post = async (operation, body) => {
4429
+ try {
4430
+ return await postSignedNode(options, `v1/node/vault/${operation}`, body);
4431
+ } catch (cause) {
4432
+ if (cause instanceof SignedNodeHttpError && cause.code === "VAULT_API_ONLY")
4433
+ cache?.clear();
4434
+ throw cause;
4435
+ }
4436
+ };
4437
+ cache = createSecretCache({
4418
4438
  ttlMs: options.ttlMs,
4419
4439
  maxStaleMs: options.maxStaleMs,
4420
4440
  list: async () => {
@@ -4443,6 +4463,7 @@ function createNodeVaultCache(options) {
4443
4463
  };
4444
4464
  }
4445
4465
  });
4466
+ return cache;
4446
4467
  }
4447
4468
  function startNodeVaultSync(cache, options = {}) {
4448
4469
  const interval = Math.max(1000, options.intervalMs ?? 30000);
@@ -4483,6 +4504,32 @@ function handleApplicationRequest(options, request) {
4483
4504
  return handleRequest(options, request);
4484
4505
  }
4485
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
+ }
4486
4533
  function handleRequest(options, request) {
4487
4534
  switch (request?.op) {
4488
4535
  case "identity":
@@ -4641,9 +4688,9 @@ function safeOp(line) {
4641
4688
  }
4642
4689
 
4643
4690
  // src/deployment.ts
4644
- 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";
4645
4692
  import { createHash as createHash5, randomUUID } from "crypto";
4646
- 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";
4647
4694
 
4648
4695
  // ../runtime/dist/queue.js
4649
4696
  class QueueStoppedError extends Error {
@@ -4958,15 +5005,15 @@ import {
4958
5005
  mkdtempSync,
4959
5006
  mkdirSync,
4960
5007
  readFileSync,
4961
- renameSync,
5008
+ renameSync as renameSync2,
4962
5009
  rmSync,
4963
5010
  statSync,
4964
- symlinkSync,
5011
+ symlinkSync as symlinkSync2,
4965
5012
  unlinkSync as unlinkSync2,
4966
5013
  writeFileSync
4967
5014
  } from "fs";
4968
5015
  import { tmpdir } from "os";
4969
- import { dirname, join } from "path";
5016
+ import { dirname as dirname2, join } from "path";
4970
5017
  var PINNED_BUN_VERSION = "1.3.14";
4971
5018
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
4972
5019
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
@@ -5176,7 +5223,7 @@ var writeOwnedPolicy = (pathValue, content, mode = 420) => {
5176
5223
  throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
5177
5224
  return;
5178
5225
  }
5179
- mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
5226
+ mkdirSync(dirname2(pathValue), { recursive: true, mode: 493 });
5180
5227
  writeFileSync(pathValue, content, { mode, flag: "wx" });
5181
5228
  };
5182
5229
  var installContainerdRuntime = async (directory, osVersion) => {
@@ -5254,7 +5301,7 @@ var installKataRuntime = async (directory) => {
5254
5301
  try {
5255
5302
  unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
5256
5303
  } catch {}
5257
- 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");
5258
5305
  const base = CONTAINERD_CONFIG(false);
5259
5306
  const kata = CONTAINERD_CONFIG(true);
5260
5307
  if (!existsSync2(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
@@ -5395,11 +5442,11 @@ async function executeSoftwareOperation(operation) {
5395
5442
  mkdirSync("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
5396
5443
  copyFileSync(join(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
5397
5444
  chmodSync2("/usr/local/lib/forgezero/runtime/bun.next", 493);
5398
- 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");
5399
5446
  try {
5400
5447
  unlinkSync2("/usr/local/bin/bun");
5401
5448
  } catch {}
5402
- symlinkSync("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
5449
+ symlinkSync2("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
5403
5450
  return { exitCode: 0, output: "" };
5404
5451
  }
5405
5452
  if (software === "cloudflared") {
@@ -7766,10 +7813,10 @@ import {
7766
7813
  chmodSync as chmodSync3,
7767
7814
  createReadStream as createReadStream2,
7768
7815
  existsSync as existsSync3,
7769
- lstatSync,
7816
+ lstatSync as lstatSync2,
7770
7817
  mkdirSync as mkdirSync2,
7771
7818
  readFileSync as readFileSync2,
7772
- renameSync as renameSync2,
7819
+ renameSync as renameSync3,
7773
7820
  rmSync as rmSync2,
7774
7821
  writeFileSync as writeFileSync2
7775
7822
  } from "fs";
@@ -7865,13 +7912,13 @@ function persistCapacityCalibration(args) {
7865
7912
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be an absolute Agent-owned path");
7866
7913
  }
7867
7914
  if (!existsSync4(args.directory)) {
7868
- const parent = lstatSync2(dirname2(args.directory));
7915
+ const parent = lstatSync3(dirname3(args.directory));
7869
7916
  if (!parent.isDirectory() || parent.isSymbolicLink()) {
7870
7917
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence parent must be a real Agent-owned directory");
7871
7918
  }
7872
7919
  mkdirSync3(args.directory, { mode: 448 });
7873
7920
  }
7874
- const stats = lstatSync2(args.directory);
7921
+ const stats = lstatSync3(args.directory);
7875
7922
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
7876
7923
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be a real Agent-owned directory");
7877
7924
  }
@@ -7894,7 +7941,7 @@ function persistCapacityCalibration(args) {
7894
7941
  }, null, 2)}
7895
7942
  `, { mode: 384, flag: "wx" });
7896
7943
  chmodSync4(next, 384);
7897
- renameSync3(next, path2);
7944
+ renameSync4(next, path2);
7898
7945
  return path2;
7899
7946
  }
7900
7947
  function createDeploymentManager(options) {
@@ -7929,10 +7976,10 @@ function createDeploymentManager(options) {
7929
7976
  }
7930
7977
  if (existsSync4(knownHostsPath) && readFileSync3(knownHostsPath, "utf8") === content)
7931
7978
  return;
7932
- mkdirSync3(dirname2(knownHostsPath), { recursive: true, mode: 448 });
7979
+ mkdirSync3(dirname3(knownHostsPath), { recursive: true, mode: 448 });
7933
7980
  const next = `${knownHostsPath}.${process.pid}.${randomUUID()}.next`;
7934
7981
  writeFileSync3(next, content, { mode: 384, flag: "wx" });
7935
- renameSync3(next, knownHostsPath);
7982
+ renameSync4(next, knownHostsPath);
7936
7983
  chmodSync4(knownHostsPath, 384);
7937
7984
  };
7938
7985
  const gitBaseEnvironment = () => ({
@@ -8058,7 +8105,7 @@ function createDeploymentManager(options) {
8058
8105
  if (bootstrapBundle) {
8059
8106
  let metadata;
8060
8107
  try {
8061
- metadata = lstatSync2(bootstrapBundle.path);
8108
+ metadata = lstatSync3(bootstrapBundle.path);
8062
8109
  } catch {
8063
8110
  throw new DeploymentError("SOURCE_FAILED", "The attended bootstrap bundle is missing.");
8064
8111
  }
@@ -8346,7 +8393,7 @@ function createDeploymentManager(options) {
8346
8393
  const component2 = plan.spec.components[String(resolved.component ?? "")];
8347
8394
  const mounts = component2?.kind === "database" ? [component2.storage] : component2?.storage ?? [];
8348
8395
  for (const mount of mounts) {
8349
- const candidate = existsSync4(mount.path) ? mount.path : dirname2(mount.path);
8396
+ const candidate = existsSync4(mount.path) ? mount.path : dirname3(mount.path);
8350
8397
  const stats = statfsSync(candidate);
8351
8398
  const freePercent = Number(stats.bavail) / Number(stats.blocks) * 100;
8352
8399
  const minimum = mount.class === "ephemeral" ? 0 : mount.minimumFreePercent ?? 0;
@@ -8922,12 +8969,12 @@ import {
8922
8969
  existsSync as existsSync6,
8923
8970
  mkdirSync as mkdirSync4,
8924
8971
  readFileSync as readFileSync4,
8925
- renameSync as renameSync4,
8972
+ renameSync as renameSync5,
8926
8973
  statSync as statSync2,
8927
8974
  unlinkSync as unlinkSync4,
8928
8975
  writeFileSync as writeFileSync4
8929
8976
  } from "fs";
8930
- import { dirname as dirname3 } from "path";
8977
+ import { dirname as dirname4 } from "path";
8931
8978
  function privateNetworkAttachmentFromEnvironment(env = process.env) {
8932
8979
  const values = {
8933
8980
  accountId: env.FZ_CF_ACCOUNT_ID?.trim() ?? "",
@@ -8973,12 +9020,12 @@ function loadGuestBinding(path2, expectedNodeKey) {
8973
9020
  return parsed;
8974
9021
  }
8975
9022
  function persistGuestBinding(path2, binding) {
8976
- mkdirSync4(dirname3(path2), { recursive: true, mode: 448 });
9023
+ mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
8977
9024
  const temporary = `${path2}.next`;
8978
9025
  writeFileSync4(temporary, `${JSON.stringify(binding)}
8979
9026
  `, { mode: 384 });
8980
9027
  chmodSync6(temporary, 384);
8981
- renameSync4(temporary, path2);
9028
+ renameSync5(temporary, path2);
8982
9029
  }
8983
9030
  async function enrolGuestIdentity(options) {
8984
9031
  const token = options.token?.trim() ?? (options.tokenPath ? readFileSync4(options.tokenPath, "utf8").trim() : "");
@@ -9331,7 +9378,7 @@ async function writeAndCloseProcessInput(input, value) {
9331
9378
  }
9332
9379
 
9333
9380
  // src/version.ts
9334
- var VERSION2 = "0.1.85";
9381
+ var VERSION2 = "0.1.87";
9335
9382
 
9336
9383
  // src/ssh-bootstrap.ts
9337
9384
  class SshBootstrapError extends Error {
@@ -10050,7 +10097,7 @@ import {
10050
10097
  unlinkSync as unlinkSync5,
10051
10098
  writeFileSync as writeFileSync6
10052
10099
  } from "fs";
10053
- 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";
10054
10101
  import { isIP as isIP2 } from "net";
10055
10102
 
10056
10103
  // src/compute.ts
@@ -10599,7 +10646,7 @@ async function provisionMetalGuest(profile, claim, exec) {
10599
10646
  }
10600
10647
  const service = `forgezero-guest@${name}.service`;
10601
10648
  const unitPath = join4(profile.unitDir, service);
10602
- mkdirSync5(dirname4(unitPath), { recursive: true });
10649
+ mkdirSync5(dirname5(unitPath), { recursive: true });
10603
10650
  writeFileSync6(unitPath, guestUnit(spec), { mode: 420 });
10604
10651
  await checked2(exec, ["systemctl", "daemon-reload"]);
10605
10652
  if (prior?.phase === "running") {
@@ -11567,7 +11614,7 @@ function requestLifecycleAction(claim, socketPath = DEFAULT_LIFECYCLE_HELPER_SOC
11567
11614
  }
11568
11615
 
11569
11616
  // src/warp-config.ts
11570
- 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";
11571
11618
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
11572
11619
  function renderWarpMdm(options) {
11573
11620
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.organization))
@@ -11593,14 +11640,14 @@ function materializeWarpMdm(options) {
11593
11640
  const next = `${runtimePath}.next`;
11594
11641
  writeFileSync8(next, renderWarpMdm(options), { mode: 384 });
11595
11642
  chmodSync11(next, 384);
11596
- renameSync5(next, runtimePath);
11643
+ renameSync6(next, runtimePath);
11597
11644
  try {
11598
11645
  unlinkSync9(servicePath);
11599
11646
  } catch (cause) {
11600
11647
  if (cause.code !== "ENOENT")
11601
11648
  throw cause;
11602
11649
  }
11603
- symlinkSync2(runtimePath, servicePath);
11650
+ symlinkSync3(runtimePath, servicePath);
11604
11651
  return { runtimePath, servicePath };
11605
11652
  }
11606
11653
 
@@ -11657,14 +11704,15 @@ import {
11657
11704
  mkdirSync as mkdirSync8,
11658
11705
  openSync as openSync2,
11659
11706
  readFileSync as readFileSync9,
11660
- readlinkSync,
11661
- renameSync as renameSync6,
11707
+ readlinkSync as readlinkSync2,
11708
+ renameSync as renameSync7,
11662
11709
  rmSync as rmSync5,
11663
- symlinkSync as symlinkSync3,
11710
+ symlinkSync as symlinkSync4,
11664
11711
  writeFileSync as writeFileSync9
11665
11712
  } from "fs";
11666
- 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";
11667
11714
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
11715
+ var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
11668
11716
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
11669
11717
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
11670
11718
  var VERSION3 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
@@ -11685,7 +11733,7 @@ var syncReleaseDirectory = (directory) => {
11685
11733
  join7(directory, "dist", "fz-git-ssh.js"),
11686
11734
  join7(directory, "dist"),
11687
11735
  directory,
11688
- dirname5(directory)
11736
+ dirname6(directory)
11689
11737
  ])
11690
11738
  syncPath(path2);
11691
11739
  };
@@ -11807,19 +11855,19 @@ async function stageAgentRelease(releaseInput, options) {
11807
11855
  args: ["-xOzf", archive, member]
11808
11856
  }, `agent update extraction of ${member}`);
11809
11857
  const destination = join7(unpacked, relative);
11810
- mkdirSync8(dirname5(destination), { recursive: true, mode: 448 });
11858
+ mkdirSync8(dirname6(destination), { recursive: true, mode: 448 });
11811
11859
  writeFileSync9(destination, extracted.output, { mode: 384, flag: "wx" });
11812
11860
  }
11813
11861
  await validateReleaseDirectory(unpacked, release, run2);
11814
11862
  if (!existsSync12(finalDirectory)) {
11815
- renameSync6(unpacked, finalDirectory);
11863
+ renameSync7(unpacked, finalDirectory);
11816
11864
  syncReleaseDirectory(finalDirectory);
11817
11865
  } else
11818
11866
  await validateReleaseDirectory(finalDirectory, release, run2);
11819
11867
  if (!existsSync12(currentLink)) {
11820
11868
  throw new Error("agent update requires an active immutable release to roll back to");
11821
11869
  }
11822
- const previousTarget = readlinkSync(currentLink);
11870
+ const previousTarget = readlinkSync2(currentLink);
11823
11871
  if (previousTarget !== join7("versions", options.currentVersion)) {
11824
11872
  throw new Error("agent update current release does not match the running version");
11825
11873
  }
@@ -11839,43 +11887,116 @@ async function stageAgentRelease(releaseInput, options) {
11839
11887
  }
11840
11888
  }
11841
11889
  function selectAgentRelease(staged) {
11842
- const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.next`);
11890
+ const next = join7(dirname6(staged.currentLink), `.current.${randomUUID2()}.next`);
11843
11891
  try {
11844
- symlinkSync3(staged.nextTarget, next);
11845
- renameSync6(next, staged.currentLink);
11846
- syncPath(dirname5(staged.currentLink));
11892
+ symlinkSync4(staged.nextTarget, next);
11893
+ renameSync7(next, staged.currentLink);
11894
+ syncPath(dirname6(staged.currentLink));
11847
11895
  } finally {
11848
11896
  rmSync5(next, { force: true });
11849
11897
  }
11850
11898
  }
11851
11899
  function restoreAgentRelease(staged) {
11852
- 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`;
11853
11912
  try {
11854
- symlinkSync3(staged.previousTarget, next);
11855
- renameSync6(next, staged.currentLink);
11856
- syncPath(dirname5(staged.currentLink));
11913
+ symlinkSync4(staged.nextTarget, next);
11914
+ renameSync7(next, link);
11915
+ syncPath(dirname6(link));
11857
11916
  } finally {
11858
11917
  rmSync5(next, { force: true });
11859
11918
  }
11860
11919
  }
11920
+ function clearAgentCandidate(root = DEFAULT_AGENT_RELEASE_ROOT) {
11921
+ rmSync5(join7(resolve2(root), "candidate"), { force: true });
11922
+ }
11861
11923
 
11862
11924
  // src/agent-update-helper.ts
11863
- import { randomUUID as randomUUID3 } from "crypto";
11925
+ import { randomUUID as randomUUID4 } from "crypto";
11864
11926
  import {
11865
- chmodSync as chmodSync13,
11927
+ chmodSync as chmodSync14,
11866
11928
  closeSync as closeSync3,
11867
- existsSync as existsSync13,
11929
+ existsSync as existsSync14,
11868
11930
  fsyncSync as fsyncSync2,
11869
11931
  mkdirSync as mkdirSync9,
11870
11932
  openSync as openSync3,
11871
11933
  readFileSync as readFileSync10,
11872
- renameSync as renameSync7,
11873
- rmSync as rmSync6,
11874
- unlinkSync as unlinkSync10,
11934
+ renameSync as renameSync9,
11935
+ rmSync as rmSync7,
11936
+ unlinkSync as unlinkSync11,
11875
11937
  writeFileSync as writeFileSync10
11876
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";
11877
11946
  import { connect as connect5, createServer as createServer6 } from "net";
11878
- import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
11947
+ var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
11948
+ function switchAgentSocketRoute(route, backend) {
11949
+ if (dirname7(route) !== dirname7(backend))
11950
+ throw new Error("Agent handover sockets must share one runtime directory");
11951
+ const target2 = basename2(backend);
11952
+ const next = `${route}.${randomUUID3()}.next`;
11953
+ try {
11954
+ symlinkSync5(target2, next);
11955
+ renameSync8(next, route);
11956
+ } finally {
11957
+ rmSync6(next, { force: true });
11958
+ }
11959
+ }
11960
+ function startAgentCandidateReady(ready, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET) {
11961
+ if (existsSync13(socketPath))
11962
+ unlinkSync10(socketPath);
11963
+ const server = createServer6((socket) => socket.end(`${JSON.stringify(ready)}
11964
+ `));
11965
+ server.listen(socketPath, () => chmodSync13(socketPath, 432));
11966
+ return server;
11967
+ }
11968
+ function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET, timeoutMs = 5000) {
11969
+ return new Promise((resolve3) => {
11970
+ const socket = connect5(socketPath);
11971
+ let buffer = "";
11972
+ let settled = false;
11973
+ const finish = (value) => {
11974
+ if (settled)
11975
+ return;
11976
+ settled = true;
11977
+ clearTimeout(timer);
11978
+ socket.destroy();
11979
+ resolve3(value);
11980
+ };
11981
+ const timer = setTimeout(() => finish(false), timeoutMs);
11982
+ socket.on("data", (chunk) => {
11983
+ buffer += chunk.toString("utf8");
11984
+ const newline = buffer.indexOf(`
11985
+ `);
11986
+ if (newline < 0)
11987
+ return;
11988
+ try {
11989
+ const value = JSON.parse(buffer.slice(0, newline));
11990
+ finish(value.version === expected.version && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].includes(value.vault ?? ""));
11991
+ } catch {
11992
+ finish(false);
11993
+ }
11994
+ });
11995
+ socket.on("error", () => finish(false));
11996
+ });
11997
+ }
11998
+
11999
+ // src/agent-update-helper.ts
11879
12000
  var AGENT_UPDATE_GROUP = "forgezero-update";
11880
12001
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
11881
12002
  var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
@@ -11970,13 +12091,13 @@ function validateJournal(value, root) {
11970
12091
  return journal;
11971
12092
  }
11972
12093
  function readJournal(path2, root) {
11973
- if (!existsSync13(path2))
12094
+ if (!existsSync14(path2))
11974
12095
  return;
11975
12096
  return validateJournal(JSON.parse(readFileSync10(path2, "utf8")), root);
11976
12097
  }
11977
12098
  function writeAtomic(path2, value, mode) {
11978
- mkdirSync9(dirname6(path2), { recursive: true, mode: 493 });
11979
- const next = `${path2}.${randomUUID3()}.next`;
12099
+ mkdirSync9(dirname8(path2), { recursive: true, mode: 493 });
12100
+ const next = `${path2}.${randomUUID4()}.next`;
11980
12101
  let file;
11981
12102
  try {
11982
12103
  file = openSync3(next, "wx", mode);
@@ -11985,8 +12106,8 @@ function writeAtomic(path2, value, mode) {
11985
12106
  fsyncSync2(file);
11986
12107
  closeSync3(file);
11987
12108
  file = undefined;
11988
- renameSync7(next, path2);
11989
- const directory = openSync3(dirname6(path2), "r");
12109
+ renameSync9(next, path2);
12110
+ const directory = openSync3(dirname8(path2), "r");
11990
12111
  try {
11991
12112
  fsyncSync2(directory);
11992
12113
  } finally {
@@ -11995,7 +12116,7 @@ function writeAtomic(path2, value, mode) {
11995
12116
  } finally {
11996
12117
  if (file !== undefined)
11997
12118
  closeSync3(file);
11998
- rmSync6(next, { force: true });
12119
+ rmSync7(next, { force: true });
11999
12120
  }
12000
12121
  }
12001
12122
  var publicReceipt = (journal) => {
@@ -12028,7 +12149,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
12028
12149
  }
12029
12150
  function readAgentUpdateReceipt(path2 = AGENT_UPDATE_RECEIPT) {
12030
12151
  try {
12031
- if (!existsSync13(path2))
12152
+ if (!existsSync14(path2))
12032
12153
  return;
12033
12154
  return validateReceipt(JSON.parse(readFileSync10(path2, "utf8")));
12034
12155
  } catch {
@@ -12068,10 +12189,32 @@ var restartAgent = async (target2, run2) => {
12068
12189
  throw new Error(`systemd could not restart ${service}`);
12069
12190
  }
12070
12191
  };
12192
+ var candidateUnit = (target2) => target2 === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
12193
+ var startCandidate = async (staged, target2, run2, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
12194
+ selectAgentCandidate(staged);
12195
+ if (!await runOk(run2, "/usr/bin/systemctl", ["restart", candidateUnit(target2)])) {
12196
+ clearAgentCandidate(dirname8(staged.currentLink));
12197
+ throw new Error(`systemd could not start ${candidateUnit(target2)}`);
12198
+ }
12199
+ if (!await probe({ version: staged.version })) {
12200
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12201
+ clearAgentCandidate(dirname8(staged.currentLink));
12202
+ throw new Error("the candidate Agent did not prove its identity and durable state");
12203
+ }
12204
+ };
12205
+ var stopCandidate = async (staged, target2, run2) => {
12206
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12207
+ clearAgentCandidate(dirname8(staged.currentLink));
12208
+ };
12209
+ var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
12210
+ route: `${publicSocket}.backend`,
12211
+ active: `${publicSocket}.backend.active`,
12212
+ candidate: `${publicSocket}.backend.candidate`
12213
+ });
12071
12214
  var targetProbe = (target2, run2) => target2 === "compute" ? () => probeAgentSocket() : async () => await runOk(run2, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run2, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
12072
12215
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12073
12216
  return new Promise((resolve4) => {
12074
- const socket = connect5(socketPath);
12217
+ const socket = connect6(socketPath);
12075
12218
  let settled = false;
12076
12219
  let buffer = "";
12077
12220
  const finish = (value) => {
@@ -12104,12 +12247,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
12104
12247
  async function activateAgentRelease(staged, options = {}) {
12105
12248
  const run2 = options.run ?? runCommand;
12106
12249
  const target2 = options.target ?? "compute";
12107
- const probe = options.probe ?? targetProbe(target2, run2);
12250
+ const paths = socketPaths(options.publicSocketPath);
12251
+ const probe = options.probe ?? (target2 === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target2, run2));
12108
12252
  const now = options.now ?? Date.now;
12109
12253
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12110
12254
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12111
- const previous = readJournal(journalPath, dirname6(staged.currentLink));
12112
- const attemptId = options.attemptId ?? randomUUID3();
12255
+ const previous = readJournal(journalPath, dirname8(staged.currentLink));
12256
+ const attemptId = options.attemptId ?? randomUUID4();
12113
12257
  if (!ATTEMPT_ID.test(attemptId))
12114
12258
  throw new Error("Agent update attempt ID is invalid");
12115
12259
  const startedAtTs = now();
@@ -12131,11 +12275,18 @@ async function activateAgentRelease(staged, options = {}) {
12131
12275
  writeUpdateState(journalPath, receiptPath, journal);
12132
12276
  let selectionAttempted = false;
12133
12277
  try {
12278
+ if (!options.candidatePrepared)
12279
+ await startCandidate(staged, target2, run2);
12280
+ if (target2 === "compute")
12281
+ switchAgentSocketRoute(paths.route, paths.candidate);
12134
12282
  selectionAttempted = true;
12135
12283
  selectAgentRelease(staged);
12136
12284
  await restartAgent(target2, run2);
12137
12285
  if (!await probe())
12138
- throw new Error("the replacement Agent did not answer its retained Vault socket");
12286
+ throw new Error("the replacement Agent did not answer its active Vault backend");
12287
+ if (target2 === "compute")
12288
+ switchAgentSocketRoute(paths.route, paths.active);
12289
+ await stopCandidate(staged, target2, run2);
12139
12290
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
12140
12291
  writeUpdateState(journalPath, receiptPath, journal);
12141
12292
  run2({
@@ -12153,10 +12304,13 @@ async function activateAgentRelease(staged, options = {}) {
12153
12304
  restored = true;
12154
12305
  await restartAgent(target2, run2);
12155
12306
  rollbackHealthy = await probe();
12307
+ if (rollbackHealthy && target2 === "compute")
12308
+ switchAgentSocketRoute(paths.route, paths.active);
12156
12309
  } catch {
12157
12310
  rollbackHealthy = false;
12158
12311
  }
12159
12312
  }
12313
+ await stopCandidate(staged, target2, run2).catch(() => {});
12160
12314
  const failures = failureCount + 1;
12161
12315
  const updatedAtTs = now();
12162
12316
  journal = {
@@ -12176,28 +12330,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12176
12330
  const root = resolve3(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
12177
12331
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12178
12332
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12333
+ const run2 = options.run ?? runCommand;
12179
12334
  const journal = readJournal(journalPath, root);
12180
- if (!journal)
12335
+ if (!journal) {
12336
+ for (const target2 of ["compute", "metal"]) {
12337
+ await run2({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target2)] });
12338
+ }
12339
+ clearAgentCandidate(root);
12181
12340
  return;
12341
+ }
12182
12342
  if (journal.outcome !== "activating") {
12343
+ await stopCandidate(stagedFromJournal(journal, root), journal.target, run2).catch(() => {});
12183
12344
  writeAtomic(receiptPath, publicReceipt(journal), 416);
12184
12345
  return publicReceipt(journal);
12185
12346
  }
12186
12347
  const staged = stagedFromJournal(journal, root);
12187
- if (!existsSync13(join8(root, journal.previousTarget))) {
12348
+ if (!existsSync14(join8(root, journal.previousTarget))) {
12188
12349
  throw new Error("Agent update rollback release is missing");
12189
12350
  }
12190
- const run2 = options.run ?? runCommand;
12191
- const probe = options.probe ?? targetProbe(journal.target, run2);
12351
+ const paths = socketPaths(options.publicSocketPath);
12352
+ const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run2));
12192
12353
  restoreAgentRelease(staged);
12193
12354
  let rollbackHealthy = false;
12194
12355
  let failureMessage = "activation was interrupted before its health verdict became durable";
12195
12356
  try {
12196
12357
  await restartAgent(journal.target, run2);
12197
12358
  rollbackHealthy = await probe();
12359
+ if (rollbackHealthy && journal.target === "compute")
12360
+ switchAgentSocketRoute(paths.route, paths.active);
12198
12361
  } catch (cause) {
12199
12362
  failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
12200
12363
  }
12364
+ await stopCandidate(staged, journal.target, run2).catch(() => {});
12201
12365
  const failures = journal.failureCount + 1;
12202
12366
  const updatedAtTs = (options.now ?? Date.now)();
12203
12367
  const recovered = {
@@ -12214,15 +12378,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
12214
12378
  }
12215
12379
  function startAgentUpdateHelper(options = {}) {
12216
12380
  const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
12217
- if (existsSync13(socketPath))
12218
- unlinkSync10(socketPath);
12219
- mkdirSync9(dirname6(socketPath), { recursive: true, mode: 488 });
12220
- const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
12381
+ if (existsSync14(socketPath))
12382
+ unlinkSync11(socketPath);
12383
+ mkdirSync9(dirname8(socketPath), { recursive: true, mode: 488 });
12221
12384
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
12222
12385
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
12223
12386
  const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
12224
- const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, { target: target2, attemptId, journalPath, receiptPath, now: options.now }));
12387
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
12388
+ const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, {
12389
+ target: target2,
12390
+ attemptId,
12391
+ journalPath,
12392
+ receiptPath,
12393
+ now: options.now,
12394
+ candidatePrepared: true
12395
+ }));
12225
12396
  let busy = true;
12397
+ let pending;
12226
12398
  let blocked;
12227
12399
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
12228
12400
  root: releaseRoot,
@@ -12234,7 +12406,7 @@ function startAgentUpdateHelper(options = {}) {
12234
12406
  }).finally(() => {
12235
12407
  busy = false;
12236
12408
  });
12237
- const server = createServer6((socket) => {
12409
+ const server = createServer7((socket) => {
12238
12410
  let buffer = "";
12239
12411
  socket.on("data", (chunk) => {
12240
12412
  buffer += chunk.toString("utf8");
@@ -12253,16 +12425,49 @@ function startAgentUpdateHelper(options = {}) {
12253
12425
  Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
12254
12426
  if (blocked)
12255
12427
  throw new Error(`update journal needs operator recovery: ${blocked}`);
12256
- if (busy)
12257
- throw new Error("another Agent update or recovery is already active");
12258
- if (request.op !== "apply")
12259
- throw new Error("unknown update operation");
12260
12428
  if (request.target !== "compute" && request.target !== "metal") {
12261
12429
  throw new Error("agent update target is invalid");
12262
12430
  }
12263
- const attemptId = request.attemptId ?? randomUUID3();
12431
+ const attemptId = request.attemptId ?? randomUUID4();
12264
12432
  if (!ATTEMPT_ID.test(attemptId))
12265
12433
  throw new Error("Agent update attempt ID is invalid");
12434
+ if (request.op === "commit" || request.op === "abort") {
12435
+ if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
12436
+ throw new Error("Agent update handover does not match the prepared candidate");
12437
+ }
12438
+ const selected = pending;
12439
+ pending = undefined;
12440
+ if (request.op === "abort") {
12441
+ await stopCandidate(selected.staged, selected.target, runCommand);
12442
+ busy = false;
12443
+ const response3 = {
12444
+ ok: true,
12445
+ status: "aborted",
12446
+ version: selected.staged.version,
12447
+ attemptId
12448
+ };
12449
+ socket.end(`${JSON.stringify(response3)}
12450
+ `);
12451
+ return;
12452
+ }
12453
+ const outcome = await activate(selected.staged, selected.target, attemptId);
12454
+ busy = false;
12455
+ if (outcome?.ok === false)
12456
+ throw new Error(outcome.reason ?? "Agent handover failed");
12457
+ const response2 = {
12458
+ ok: true,
12459
+ status: "active",
12460
+ version: selected.staged.version,
12461
+ attemptId
12462
+ };
12463
+ socket.end(`${JSON.stringify(response2)}
12464
+ `);
12465
+ return;
12466
+ }
12467
+ if (request.op !== "prepare")
12468
+ throw new Error("unknown update operation");
12469
+ if (busy)
12470
+ throw new Error("another Agent update or recovery is already active");
12266
12471
  const prior = readJournal(journalPath, releaseRoot);
12267
12472
  const now = (options.now ?? Date.now)();
12268
12473
  if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
@@ -12273,17 +12478,23 @@ function startAgentUpdateHelper(options = {}) {
12273
12478
  currentVersion: request.currentVersion,
12274
12479
  root: releaseRoot
12275
12480
  });
12276
- const response = { ok: true, status: "staged", version: staged.version, attemptId };
12481
+ await startCandidate(staged, request.target, runCommand);
12482
+ pending = { staged, target: request.target, attemptId };
12483
+ ownsBusy = false;
12484
+ setTimer(() => {
12485
+ if (pending?.attemptId !== attemptId)
12486
+ return;
12487
+ const expired = pending;
12488
+ pending = undefined;
12489
+ stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
12490
+ busy = false;
12491
+ });
12492
+ }, 180000);
12493
+ const response = { ok: true, status: "prepared", version: staged.version, attemptId };
12277
12494
  socket.end(`${JSON.stringify(response)}
12278
12495
  `);
12279
- setTimer(() => void activate(staged, request.target, attemptId).catch((cause) => {
12280
- blocked = cause instanceof Error ? cause.message : String(cause);
12281
- }).finally(() => {
12282
- busy = false;
12283
- }), 100);
12284
- ownsBusy = false;
12285
12496
  }).catch((cause) => {
12286
- if (ownsBusy)
12497
+ if (ownsBusy && !pending)
12287
12498
  busy = false;
12288
12499
  const response = {
12289
12500
  ok: false,
@@ -12295,12 +12506,12 @@ function startAgentUpdateHelper(options = {}) {
12295
12506
  });
12296
12507
  socket.on("error", () => socket.destroy());
12297
12508
  });
12298
- server.listen(socketPath, () => chmodSync13(socketPath, 432));
12509
+ server.listen(socketPath, () => chmodSync14(socketPath, 432));
12299
12510
  return server;
12300
12511
  }
12301
12512
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
12302
12513
  return new Promise((resolve4, reject) => {
12303
- const socket = connect5(socketPath, () => socket.write(`${JSON.stringify(request)}
12514
+ const socket = connect6(socketPath, () => socket.write(`${JSON.stringify(request)}
12304
12515
  `));
12305
12516
  let buffer = "";
12306
12517
  socket.setTimeout(timeoutMs, () => {
@@ -12325,7 +12536,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
12325
12536
  }
12326
12537
 
12327
12538
  // src/agent-heartbeat.ts
12328
- import { existsSync as existsSync14, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12539
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
12329
12540
  import { cpus, freemem, loadavg, totalmem } from "os";
12330
12541
  function readAgentHostMetrics() {
12331
12542
  const filesystem = statfsSync2("/");
@@ -12370,7 +12581,7 @@ function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = rea
12370
12581
  runtimeCapabilities: {
12371
12582
  native: true,
12372
12583
  ociRunc: ubuntuX64,
12373
- kataQemuSnp: ubuntuX64 && osVersion === "26.04" && existsSync14("/dev/kvm") && existsSync14("/dev/sev") && enabled("/sys/module/kvm_amd/parameters/sev") && enabled("/sys/module/kvm_amd/parameters/sev_snp")
12584
+ kataQemuSnp: ubuntuX64 && osVersion === "26.04" && existsSync15("/dev/kvm") && existsSync15("/dev/sev") && enabled("/sys/module/kvm_amd/parameters/sev") && enabled("/sys/module/kvm_amd/parameters/sev_snp")
12374
12585
  },
12375
12586
  capacity: {
12376
12587
  logicalCpu: Math.max(1, metrics.logicalCpu),
@@ -12422,18 +12633,24 @@ async function heartbeatAgentOnce(options) {
12422
12633
  });
12423
12634
  return response;
12424
12635
  }
12425
- let prepared = false;
12636
+ let candidatePrepared = false;
12637
+ let drained = false;
12426
12638
  try {
12427
- await options.prepareUpdate?.(release);
12428
- prepared = true;
12639
+ const update2 = options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate(phase === "prepare" ? { op: "prepare", target: options.updateTarget ?? "compute", release: next, currentVersion: current2, attemptId } : {
12640
+ op: phase,
12641
+ target: options.updateTarget ?? "compute",
12642
+ currentVersion: current2,
12643
+ targetVersion: next.version,
12644
+ attemptId
12645
+ }));
12429
12646
  const apply = async () => {
12430
- const applied = await (options.applyUpdate ?? ((next, current2, attemptId) => requestAgentUpdate({
12431
- op: "apply",
12432
- target: options.updateTarget ?? "compute",
12433
- release: next,
12434
- currentVersion: current2,
12435
- attemptId
12436
- })))(release, observation.version, desired.attemptId);
12647
+ const prepared = await update2("prepare", release, observation.version, desired.attemptId);
12648
+ if (!prepared.ok)
12649
+ throw new AgentUpdateRefusedError(`agent update refused: ${prepared.error.message}`);
12650
+ candidatePrepared = true;
12651
+ await options.prepareUpdate?.(release);
12652
+ drained = true;
12653
+ const applied = await update2("commit", release, observation.version, desired.attemptId);
12437
12654
  if (!applied.ok)
12438
12655
  throw new AgentUpdateRefusedError(`agent update refused: ${applied.error.message}`);
12439
12656
  if (applied.attemptId !== desired.attemptId) {
@@ -12446,9 +12663,18 @@ async function heartbeatAgentOnce(options) {
12446
12663
  } else {
12447
12664
  await apply();
12448
12665
  }
12449
- options.onEvent?.("update-staged", { from: observation.version, to: release.version });
12666
+ options.onEvent?.("update-active", { from: observation.version, to: release.version });
12450
12667
  } catch (cause) {
12451
- if (prepared)
12668
+ if (candidatePrepared && !drained) {
12669
+ await (options.applyUpdate ?? ((phase, next, current2, attemptId) => requestAgentUpdate({
12670
+ op: phase,
12671
+ target: options.updateTarget ?? "compute",
12672
+ currentVersion: current2,
12673
+ targetVersion: next.version,
12674
+ attemptId
12675
+ })))("abort", release, observation.version, desired.attemptId).catch(() => {});
12676
+ }
12677
+ if (drained)
12452
12678
  await options.recoverUpdate?.(cause);
12453
12679
  throw cause;
12454
12680
  }
@@ -12489,14 +12715,14 @@ function startAgentHeartbeat(options) {
12489
12715
  }
12490
12716
 
12491
12717
  // src/software-helper.ts
12492
- import { chmodSync as chmodSync14, existsSync as existsSync17, mkdirSync as mkdirSync13, unlinkSync as unlinkSync11 } from "fs";
12493
- import { connect as connect6, createServer as createServer7 } from "net";
12494
- import { dirname as dirname10 } from "path";
12718
+ import { chmodSync as chmodSync15, existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync12 } from "fs";
12719
+ import { connect as connect7, createServer as createServer8 } from "net";
12720
+ import { dirname as dirname12 } from "path";
12495
12721
 
12496
12722
  // src/service-supervisor.ts
12497
12723
  import { createHash as createHash9 } from "crypto";
12498
- 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";
12499
- import { dirname as dirname7, join as join9, resolve as resolve4, sep as sep2 } from "path";
12724
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync10, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "fs";
12725
+ import { dirname as dirname9, join as join9, resolve as resolve4, sep as sep2 } from "path";
12500
12726
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
12501
12727
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
12502
12728
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
@@ -12506,17 +12732,17 @@ var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
12506
12732
  var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
12507
12733
  var defaultHost = {
12508
12734
  write(path2, content, mode) {
12509
- mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
12735
+ mkdirSync10(dirname9(path2), { recursive: true, mode: 493 });
12510
12736
  const next = `${path2}.next`;
12511
12737
  writeFileSync11(next, content, { mode });
12512
- renameSync8(next, path2);
12738
+ renameSync10(next, path2);
12513
12739
  },
12514
12740
  read: (path2) => readFileSync12(path2, "utf8"),
12515
- exists: existsSync15,
12516
- list: (path2) => existsSync15(path2) ? readdirSync2(path2) : [],
12741
+ exists: existsSync16,
12742
+ list: (path2) => existsSync16(path2) ? readdirSync2(path2) : [],
12517
12743
  realpath: realpathSync2,
12518
12744
  mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
12519
- remove: (path2) => rmSync7(path2, { force: true }),
12745
+ remove: (path2) => rmSync8(path2, { force: true }),
12520
12746
  async exec(argv2) {
12521
12747
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
12522
12748
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -12712,20 +12938,20 @@ async function activateSupervisedService(request, host = defaultHost) {
12712
12938
 
12713
12939
  // src/container-supervisor.ts
12714
12940
  import { createHash as createHash10 } from "crypto";
12715
- 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";
12716
- import { dirname as dirname8, resolve as resolve5, sep as sep3 } from "path";
12941
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync11, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "fs";
12942
+ import { dirname as dirname10, resolve as resolve5, sep as sep3 } from "path";
12717
12943
  var defaultHost2 = {
12718
12944
  realpath: realpathSync3,
12719
- exists: existsSync16,
12945
+ exists: existsSync17,
12720
12946
  read: (path2) => readFileSync13(path2, "utf8"),
12721
12947
  write(path2, content, mode) {
12722
- mkdirSync11(dirname8(path2), { recursive: true, mode: 493 });
12948
+ mkdirSync11(dirname10(path2), { recursive: true, mode: 493 });
12723
12949
  const next = `${path2}.next`;
12724
12950
  writeFileSync12(next, content, { mode });
12725
- renameSync9(next, path2);
12951
+ renameSync11(next, path2);
12726
12952
  },
12727
- remove: (path2) => rmSync8(path2, { force: true }),
12728
- list: (path2) => existsSync16(path2) ? readdirSync3(path2) : [],
12953
+ remove: (path2) => rmSync9(path2, { force: true }),
12954
+ list: (path2) => existsSync17(path2) ? readdirSync3(path2) : [],
12729
12955
  mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
12730
12956
  async exec(argv2) {
12731
12957
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -13014,9 +13240,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
13014
13240
 
13015
13241
  // src/deployment-connectivity.ts
13016
13242
  import { createHash as createHash11 } from "crypto";
13017
- import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
13243
+ import { mkdirSync as mkdirSync12, renameSync as renameSync12, writeFileSync as writeFileSync13 } from "fs";
13018
13244
  import { createConnection as createConnection2 } from "net";
13019
- import { dirname as dirname9 } from "path";
13245
+ import { dirname as dirname11 } from "path";
13020
13246
  var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
13021
13247
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
13022
13248
  var HOSTNAME2 = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -13029,7 +13255,7 @@ var checked7 = async (host, argv2, label) => {
13029
13255
  return result.output.trim();
13030
13256
  };
13031
13257
  async function sealWithSystemd(name, path2, value) {
13032
- mkdirSync12(dirname9(path2), { recursive: true, mode: 448 });
13258
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 448 });
13033
13259
  const next = `${path2}.next`;
13034
13260
  const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
13035
13261
  stdin: "pipe",
@@ -13045,15 +13271,15 @@ async function sealWithSystemd(name, path2, value) {
13045
13271
  ]);
13046
13272
  if (exitCode !== 0)
13047
13273
  throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
13048
- renameSync10(next, path2);
13274
+ renameSync12(next, path2);
13049
13275
  }
13050
13276
  var defaultHost3 = {
13051
13277
  seal: sealWithSystemd,
13052
13278
  write(path2, content, mode) {
13053
- mkdirSync12(dirname9(path2), { recursive: true, mode: 493 });
13279
+ mkdirSync12(dirname11(path2), { recursive: true, mode: 493 });
13054
13280
  const next = `${path2}.next`;
13055
13281
  writeFileSync13(next, content, { mode });
13056
- renameSync10(next, path2);
13282
+ renameSync12(next, path2);
13057
13283
  },
13058
13284
  async exec(argv2) {
13059
13285
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
@@ -13292,9 +13518,9 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
13292
13518
  var MAX_PENDING_REQUESTS = 128;
13293
13519
  function startSoftwareHelper(options = {}) {
13294
13520
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
13295
- if (existsSync17(socketPath))
13296
- unlinkSync11(socketPath);
13297
- mkdirSync13(dirname10(socketPath), { recursive: true, mode: 488 });
13521
+ if (existsSync18(socketPath))
13522
+ unlinkSync12(socketPath);
13523
+ mkdirSync13(dirname12(socketPath), { recursive: true, mode: 488 });
13298
13524
  const ensure = options.ensure ?? ensureSoftwareRequirements;
13299
13525
  const activate = options.activate ?? activateSupervisedService;
13300
13526
  const buildContainer = options.buildContainer ?? buildContainerImage;
@@ -13302,7 +13528,7 @@ function startSoftwareHelper(options = {}) {
13302
13528
  const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
13303
13529
  let tail = Promise.resolve();
13304
13530
  let pending = 0;
13305
- const server = createServer7((socket) => {
13531
+ const server = createServer8((socket) => {
13306
13532
  let buffer = "";
13307
13533
  socket.on("data", (chunk) => {
13308
13534
  buffer += chunk.toString("utf8");
@@ -13381,12 +13607,12 @@ function startSoftwareHelper(options = {}) {
13381
13607
  });
13382
13608
  socket.on("error", () => socket.destroy());
13383
13609
  });
13384
- server.listen(socketPath, () => chmodSync14(socketPath, 432));
13610
+ server.listen(socketPath, () => chmodSync15(socketPath, 432));
13385
13611
  return server;
13386
13612
  }
13387
13613
  function requestContainer(op, request, socketPath, timeoutMs) {
13388
13614
  return new Promise((resolve6, reject) => {
13389
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13615
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
13390
13616
  `));
13391
13617
  let buffer = "";
13392
13618
  socket.setTimeout(timeoutMs, () => {
@@ -13424,7 +13650,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
13424
13650
  }
13425
13651
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
13426
13652
  return new Promise((resolve6, reject) => {
13427
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13653
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
13428
13654
  `));
13429
13655
  let buffer = "";
13430
13656
  socket.setTimeout(timeoutMs, () => {
@@ -13453,7 +13679,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
13453
13679
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
13454
13680
  validateSoftwareRequirements(requirements);
13455
13681
  return new Promise((resolve6, reject) => {
13456
- const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13682
+ const socket = connect7(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
13457
13683
  `));
13458
13684
  let buffer = "";
13459
13685
  socket.setTimeout(timeoutMs, () => {
@@ -14413,19 +14639,19 @@ var METAL_SYSTEMD_CREDENTIALS = {
14413
14639
 
14414
14640
  // src/platform-bootstrap-runtime.ts
14415
14641
  import {
14416
- chmodSync as chmodSync15,
14642
+ chmodSync as chmodSync16,
14417
14643
  chownSync,
14418
14644
  copyFileSync as copyFileSync2,
14419
- existsSync as existsSync18,
14420
- lstatSync as lstatSync3,
14645
+ existsSync as existsSync19,
14646
+ lstatSync as lstatSync4,
14421
14647
  mkdirSync as mkdirSync14,
14422
14648
  readFileSync as readFileSync14,
14423
14649
  readdirSync as readdirSync4,
14424
14650
  realpathSync as realpathSync5,
14425
- renameSync as renameSync11,
14426
- rmSync as rmSync9,
14651
+ renameSync as renameSync13,
14652
+ rmSync as rmSync10,
14427
14653
  statSync as statSync4,
14428
- symlinkSync as symlinkSync4,
14654
+ symlinkSync as symlinkSync6,
14429
14655
  writeFileSync as writeFileSync14
14430
14656
  } from "fs";
14431
14657
  import { join as join10 } from "path";
@@ -14480,21 +14706,21 @@ var platformExec = async (argv2) => {
14480
14706
  };
14481
14707
  var replaceLink = (path2, target2) => {
14482
14708
  const pending = `${path2}.next`;
14483
- rmSync9(pending, { force: true });
14709
+ rmSync10(pending, { force: true });
14484
14710
  if (!target2) {
14485
- rmSync9(path2, { force: true });
14711
+ rmSync10(path2, { force: true });
14486
14712
  return;
14487
14713
  }
14488
- symlinkSync4(target2, pending);
14489
- renameSync11(pending, path2);
14714
+ symlinkSync6(target2, pending);
14715
+ renameSync13(pending, path2);
14490
14716
  };
14491
14717
  var secureRelease = (path2, uid, gid) => {
14492
14718
  const visit = (current2) => {
14493
- const metadata = lstatSync3(current2);
14719
+ const metadata = lstatSync4(current2);
14494
14720
  if (metadata.isSymbolicLink())
14495
14721
  throw new Error("release contains a symbolic link");
14496
14722
  chownSync(current2, uid, gid);
14497
- chmodSync15(current2, metadata.isDirectory() ? 365 : 292);
14723
+ chmodSync16(current2, metadata.isDirectory() ? 365 : 292);
14498
14724
  if (metadata.isDirectory())
14499
14725
  for (const name of readdirSync4(current2))
14500
14726
  visit(join10(current2, name));
@@ -14519,7 +14745,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14519
14745
  const slots = join10(normalized.root, "slots");
14520
14746
  mkdirSync14(slots, { recursive: true, mode: 493 });
14521
14747
  const slotFile = join10(normalized.root, ".forge-slot");
14522
- const previousSlot = existsSync18(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14748
+ const previousSlot = existsSync19(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
14523
14749
  const target2 = previousSlot === "blue" ? "green" : "blue";
14524
14750
  const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
14525
14751
  const targetLink = join10(slots, target2);
@@ -14559,25 +14785,25 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14559
14785
  }
14560
14786
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
14561
14787
  const backup = `${upstream}.forgezero-backup`;
14562
- if (existsSync18(upstream))
14788
+ if (existsSync19(upstream))
14563
14789
  copyFileSync2(upstream, backup);
14564
14790
  else
14565
- rmSync9(backup, { force: true });
14791
+ rmSync10(backup, { force: true });
14566
14792
  writeFileSync14(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
14567
14793
  `, { mode: 420 });
14568
14794
  const test = await exec(["/usr/sbin/nginx", "-t"]);
14569
14795
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
14570
14796
  if (reload.exitCode !== 0) {
14571
- if (existsSync18(backup))
14572
- renameSync11(backup, upstream);
14797
+ if (existsSync19(backup))
14798
+ renameSync13(backup, upstream);
14573
14799
  else
14574
- rmSync9(upstream, { force: true });
14800
+ rmSync10(upstream, { force: true });
14575
14801
  await exec(["/usr/sbin/nginx", "-t"]);
14576
14802
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
14577
14803
  await stopTarget();
14578
14804
  throw new Error("nginx refused the promoted upstream");
14579
14805
  }
14580
- rmSync9(backup, { force: true });
14806
+ rmSync10(backup, { force: true });
14581
14807
  writeFileSync14(slotFile, `${target2}
14582
14808
  `, { mode: 420 });
14583
14809
  if (previousSlot && previousSlot !== target2) {
@@ -14587,12 +14813,12 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
14587
14813
  const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync4(right).mtimeMs - statSync4(left).mtimeMs).slice(normalized.keepReleases);
14588
14814
  for (const path2 of old)
14589
14815
  if (path2 !== release)
14590
- rmSync9(path2, { recursive: true, force: true });
14816
+ rmSync10(path2, { recursive: true, force: true });
14591
14817
  return { release, slot: target2 };
14592
14818
  }
14593
14819
 
14594
14820
  // src/recovery-host.ts
14595
- import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14821
+ import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
14596
14822
  import { hostname } from "os";
14597
14823
  import { join as join11 } from "path";
14598
14824
  var execute2 = async (argv2) => {
@@ -14618,7 +14844,7 @@ var exactState = (path2, expected) => {
14618
14844
  throw new Error(`state mismatch: ${expected}`);
14619
14845
  };
14620
14846
  var nonEmpty = (path2) => {
14621
- if (!existsSync19(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14847
+ if (!existsSync20(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
14622
14848
  throw new Error(`required file missing: ${path2}`);
14623
14849
  };
14624
14850
  var option = (args, name) => {
@@ -14714,20 +14940,20 @@ async function runRecoveryHost(args, run2 = execute2) {
14714
14940
  }
14715
14941
 
14716
14942
  // src/platform-fleet-verification.ts
14717
- import { lstatSync as lstatSync6 } from "fs";
14943
+ import { lstatSync as lstatSync7 } from "fs";
14718
14944
 
14719
14945
  // src/bootstrap.ts
14720
14946
  import {
14721
- chmodSync as chmodSync17,
14722
- existsSync as existsSync21,
14723
- lstatSync as lstatSync5,
14947
+ chmodSync as chmodSync18,
14948
+ existsSync as existsSync22,
14949
+ lstatSync as lstatSync6,
14724
14950
  mkdirSync as mkdirSync16,
14725
14951
  readFileSync as readFileSync17,
14726
- renameSync as renameSync13,
14727
- rmSync as rmSync11,
14952
+ renameSync as renameSync15,
14953
+ rmSync as rmSync12,
14728
14954
  writeFileSync as writeFileSync16
14729
14955
  } from "fs";
14730
- import { dirname as dirname12 } from "path";
14956
+ import { dirname as dirname14 } from "path";
14731
14957
  import { fileURLToPath as fileURLToPath2 } from "url";
14732
14958
 
14733
14959
  // src/provision.ts
@@ -14784,6 +15010,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
14784
15010
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
14785
15011
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
14786
15012
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
15013
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
14787
15014
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
14788
15015
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
14789
15016
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -14895,6 +15122,7 @@ Type=simple
14895
15122
  User=root
14896
15123
  Group=${AGENT_UPDATE_GROUP}
14897
15124
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
15125
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
14898
15126
  ExecStart=${bin} update-helper
14899
15127
  Restart=always
14900
15128
  RestartSec=2
@@ -14939,13 +15167,11 @@ WantedBy=sockets.target
14939
15167
  `;
14940
15168
  }
14941
15169
  function agentSocketProxyUnit(options) {
14942
- const backend = agentBackendSocketPath(options.socketPath);
15170
+ const backend = agentRoutingSocketPath(options.socketPath);
14943
15171
  const user = options.user ?? "forgezero";
14944
15172
  return `[Unit]
14945
15173
  Description=ForgeZero application Vault socket proxy
14946
15174
  Documentation=https://www.forgezero.net/docs/agent
14947
- Requires=forgezero-agent.service
14948
- After=forgezero-agent.service
14949
15175
 
14950
15176
  [Service]
14951
15177
  User=${user}
@@ -14966,12 +15192,26 @@ RestrictAddressFamilies=AF_UNIX
14966
15192
  `;
14967
15193
  }
14968
15194
  function agentBackendSocketPath(publicSocketPath) {
15195
+ const socket = systemdPath(publicSocketPath, "agent socket");
15196
+ const backend = `${socket}.backend.active`;
15197
+ if (Buffer.byteLength(backend) > 100)
15198
+ throw new Error("agent socket path is too long for a Unix socket");
15199
+ return backend;
15200
+ }
15201
+ function agentRoutingSocketPath(publicSocketPath) {
14969
15202
  const socket = systemdPath(publicSocketPath, "agent socket");
14970
15203
  const backend = `${socket}.backend`;
14971
15204
  if (Buffer.byteLength(backend) > 100)
14972
15205
  throw new Error("agent socket path is too long for a Unix socket");
14973
15206
  return backend;
14974
15207
  }
15208
+ function agentCandidateSocketPath(publicSocketPath) {
15209
+ const socket = systemdPath(publicSocketPath, "agent socket");
15210
+ const backend = `${socket}.backend.candidate`;
15211
+ if (Buffer.byteLength(backend) > 100)
15212
+ throw new Error("agent socket path is too long for a Unix socket");
15213
+ return backend;
15214
+ }
14975
15215
  var systemdPath = (value, label) => {
14976
15216
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
14977
15217
  throw new Error(`invalid ${label} path`);
@@ -15255,7 +15495,11 @@ function agentUnit(options) {
15255
15495
  }
15256
15496
  const environment = [
15257
15497
  "NODE_ENV=production",
15258
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
15498
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
15499
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
15500
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
15501
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
15502
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
15259
15503
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
15260
15504
  `FZ_SEED_CREDENTIAL=agent-seed`,
15261
15505
  `FZ_AGENT_MODE=${options.mode}`,
@@ -15392,6 +15636,26 @@ ${deploymentWrites}
15392
15636
  WantedBy=multi-user.target
15393
15637
  `;
15394
15638
  }
15639
+ function agentCandidateUnit(options) {
15640
+ return agentUnit({
15641
+ ...options,
15642
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
15643
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
15644
+ handoverCandidate: true,
15645
+ gitCredentialPath: undefined,
15646
+ deploymentCredentials: {},
15647
+ bootstrapSshCredentialPath: undefined,
15648
+ bootstrapSshPublicKeyPath: undefined,
15649
+ pullBootstrap: false,
15650
+ pullDeployments: false,
15651
+ pullMigrations: false,
15652
+ lifecycleProfilePath: undefined,
15653
+ bootstrapTargetTelemetryEndpoint: undefined,
15654
+ repository: undefined,
15655
+ bootstrapBundlePath: undefined,
15656
+ bootstrapBundleManifestPath: undefined
15657
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
15658
+ }
15395
15659
  var renderOperation = (operation) => {
15396
15660
  if (operation.kind === "commands")
15397
15661
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -15527,6 +15791,7 @@ function planProvision(options) {
15527
15791
  auxiliaryUnits: [
15528
15792
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
15529
15793
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
15794
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
15530
15795
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
15531
15796
  ...options.enforceEgress ? [
15532
15797
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -15658,19 +15923,19 @@ function planProvision(options) {
15658
15923
  // src/cli/agent-install.ts
15659
15924
  import { randomBytes as randomBytes6 } from "crypto";
15660
15925
  import {
15661
- chmodSync as chmodSync16,
15926
+ chmodSync as chmodSync17,
15662
15927
  copyFileSync as copyFileSync3,
15663
- existsSync as existsSync20,
15664
- lstatSync as lstatSync4,
15928
+ existsSync as existsSync21,
15929
+ lstatSync as lstatSync5,
15665
15930
  mkdirSync as mkdirSync15,
15666
15931
  readFileSync as readFileSync16,
15667
15932
  realpathSync as realpathSync6,
15668
- renameSync as renameSync12,
15669
- rmSync as rmSync10,
15670
- symlinkSync as symlinkSync5,
15933
+ renameSync as renameSync14,
15934
+ rmSync as rmSync11,
15935
+ symlinkSync as symlinkSync7,
15671
15936
  writeFileSync as writeFileSync15
15672
15937
  } from "fs";
15673
- import { dirname as dirname11 } from "path";
15938
+ import { dirname as dirname13 } from "path";
15674
15939
  async function readCapabilities(run2) {
15675
15940
  const answers = {};
15676
15941
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -15728,51 +15993,51 @@ var runProvisionOperation = async (operation) => {
15728
15993
  if (operation.kind === "install-runtime") {
15729
15994
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
15730
15995
  mkdirSync15(`${release}/dist`, { recursive: true, mode: 493 });
15731
- mkdirSync15(dirname11(operation.binary), { recursive: true, mode: 493 });
15996
+ mkdirSync15(dirname13(operation.binary), { recursive: true, mode: 493 });
15732
15997
  copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
15733
- chmodSync16(`${release}/dist/fz-agent.js`, 493);
15734
- const gitSshSource = `${dirname11(operation.source)}/fz-git-ssh.js`;
15735
- if (!existsSync20(gitSshSource))
15998
+ chmodSync17(`${release}/dist/fz-agent.js`, 493);
15999
+ const gitSshSource = `${dirname13(operation.source)}/fz-git-ssh.js`;
16000
+ if (!existsSync21(gitSshSource))
15736
16001
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
15737
16002
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
15738
- chmodSync16(`${release}/dist/fz-git-ssh.js`, 493);
16003
+ chmodSync17(`${release}/dist/fz-git-ssh.js`, 493);
15739
16004
  const pending = "/opt/forgezero/agent/current.next";
15740
- rmSync10(pending, { force: true });
15741
- symlinkSync5(`versions/${operation.version}`, pending);
15742
- renameSync12(pending, "/opt/forgezero/agent/current");
15743
- rmSync10(operation.binary, { force: true });
15744
- symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
16005
+ rmSync11(pending, { force: true });
16006
+ symlinkSync7(`versions/${operation.version}`, pending);
16007
+ renameSync14(pending, "/opt/forgezero/agent/current");
16008
+ rmSync11(operation.binary, { force: true });
16009
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
15745
16010
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
15746
- rmSync10(gitSshBinary, { force: true });
15747
- symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
16011
+ rmSync11(gitSshBinary, { force: true });
16012
+ symlinkSync7("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
15748
16013
  return { stdout: "", exitCode: 0 };
15749
16014
  }
15750
16015
  if (operation.kind === "ensure-seed") {
15751
- if (existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16016
+ if (existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15752
16017
  return { stdout: "", exitCode: 0 };
15753
16018
  const seed = randomBytes6(32).toString("base64url");
15754
16019
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
15755
16020
  if (result.exitCode === 0)
15756
- chmodSync16(operation.credential, 256);
16021
+ chmodSync17(operation.credential, 256);
15757
16022
  return result;
15758
16023
  }
15759
16024
  if (operation.kind === "ensure-git-identity") {
15760
16025
  const key = "/run/forgezero-git-deploy-key";
15761
16026
  const publicKey = `${key}.pub`;
15762
16027
  try {
15763
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15764
- rmSync10(key, { force: true });
15765
- rmSync10(publicKey, { force: true });
16028
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16029
+ rmSync11(key, { force: true });
16030
+ rmSync11(publicKey, { force: true });
15766
16031
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
15767
16032
  if (result.exitCode !== 0)
15768
16033
  return result;
15769
16034
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
15770
16035
  if (result.exitCode !== 0)
15771
16036
  return result;
15772
- chmodSync16(operation.credential, 256);
16037
+ chmodSync17(operation.credential, 256);
15773
16038
  }
15774
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15775
- if (!existsSync20(key)) {
16039
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16040
+ if (!existsSync21(key)) {
15776
16041
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
15777
16042
  if (decrypted.exitCode !== 0)
15778
16043
  return decrypted;
@@ -15785,25 +16050,25 @@ var runProvisionOperation = async (operation) => {
15785
16050
  }
15786
16051
  return { stdout: "", exitCode: 0 };
15787
16052
  } finally {
15788
- rmSync10(key, { force: true });
15789
- rmSync10(publicKey, { force: true });
16053
+ rmSync11(key, { force: true });
16054
+ rmSync11(publicKey, { force: true });
15790
16055
  }
15791
16056
  }
15792
16057
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
15793
16058
  const key = "/run/forgezero-bootstrap-ssh-key";
15794
16059
  const generatedPublicKey = `${key}.pub`;
15795
16060
  try {
15796
- if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
15797
- rmSync10(key, { force: true });
15798
- rmSync10(generatedPublicKey, { force: true });
16061
+ if (!existsSync21(operation.credential) || lstatSync5(operation.credential).size < 1) {
16062
+ rmSync11(key, { force: true });
16063
+ rmSync11(generatedPublicKey, { force: true });
15799
16064
  let result;
15800
16065
  if (operation.source) {
15801
- const source = existsSync20(operation.source) ? lstatSync4(operation.source) : undefined;
16066
+ const source = existsSync21(operation.source) ? lstatSync5(operation.source) : undefined;
15802
16067
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
15803
16068
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
15804
16069
  }
15805
16070
  copyFileSync3(operation.source, key);
15806
- chmodSync16(key, 384);
16071
+ chmodSync17(key, 384);
15807
16072
  } else {
15808
16073
  result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
15809
16074
  if (result.exitCode !== 0)
@@ -15816,48 +16081,48 @@ var runProvisionOperation = async (operation) => {
15816
16081
  result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
15817
16082
  if (result.exitCode !== 0)
15818
16083
  return result;
15819
- chmodSync16(operation.credential, 256);
16084
+ chmodSync17(operation.credential, 256);
15820
16085
  }
15821
- if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
15822
- if (!existsSync20(key)) {
16086
+ if (!existsSync21(operation.publicKey) || lstatSync5(operation.publicKey).size < 1) {
16087
+ if (!existsSync21(key)) {
15823
16088
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
15824
16089
  if (decrypted.exitCode !== 0)
15825
16090
  return decrypted;
15826
- chmodSync16(key, 384);
16091
+ chmodSync17(key, 384);
15827
16092
  }
15828
16093
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
15829
16094
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
15830
16095
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
15831
16096
  }
15832
- mkdirSync15(dirname11(operation.publicKey), { recursive: true, mode: 493 });
16097
+ mkdirSync15(dirname13(operation.publicKey), { recursive: true, mode: 493 });
15833
16098
  writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
15834
16099
  `, { mode: 292 });
15835
- chmodSync16(operation.publicKey, 292);
16100
+ chmodSync17(operation.publicKey, 292);
15836
16101
  }
15837
16102
  if (operation.source)
15838
- rmSync10(operation.source, { force: true });
16103
+ rmSync11(operation.source, { force: true });
15839
16104
  return { stdout: "", exitCode: 0 };
15840
16105
  } finally {
15841
- rmSync10(key, { force: true });
15842
- rmSync10(generatedPublicKey, { force: true });
16106
+ rmSync11(key, { force: true });
16107
+ rmSync11(generatedPublicKey, { force: true });
15843
16108
  }
15844
16109
  }
15845
16110
  if (operation.kind === "ensure-enrolment") {
15846
- if (existsSync20(operation.state) && lstatSync4(operation.state).size > 0 || existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
16111
+ if (existsSync21(operation.state) && lstatSync5(operation.state).size > 0 || existsSync21(operation.credential) && lstatSync5(operation.credential).size > 0)
15847
16112
  return { stdout: "", exitCode: 0 };
15848
- if (!existsSync20(operation.source))
16113
+ if (!existsSync21(operation.source))
15849
16114
  return { stdout: "enrolment source is missing", exitCode: 1 };
15850
16115
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
15851
16116
  if (result.exitCode === 0) {
15852
- chmodSync16(operation.credential, 256);
15853
- rmSync10(operation.source, { force: true });
16117
+ chmodSync17(operation.credential, 256);
16118
+ rmSync11(operation.source, { force: true });
15854
16119
  }
15855
16120
  return result;
15856
16121
  }
15857
16122
  if (operation.kind === "wait-socket") {
15858
16123
  for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
15859
16124
  try {
15860
- if (lstatSync4(operation.path).isSocket())
16125
+ if (lstatSync5(operation.path).isSocket())
15861
16126
  return { stdout: "", exitCode: 0 };
15862
16127
  } catch {}
15863
16128
  await Bun.sleep(operation.intervalMs);
@@ -15865,7 +16130,7 @@ var runProvisionOperation = async (operation) => {
15865
16130
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
15866
16131
  }
15867
16132
  if (operation.kind === "verify-file")
15868
- return existsSync20(operation.path) && lstatSync4(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
16133
+ return existsSync21(operation.path) && lstatSync5(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
15869
16134
  if (operation.kind === "verify-egress") {
15870
16135
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
15871
16136
  if (active.exitCode !== 0)
@@ -15899,7 +16164,7 @@ var runProvisionOperation = async (operation) => {
15899
16164
  const key = "/run/cloudflare-warp-key.gpg";
15900
16165
  writeFileSync15(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
15901
16166
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
15902
- rmSync10(key, { force: true });
16167
+ rmSync11(key, { force: true });
15903
16168
  if (result.exitCode !== 0)
15904
16169
  return result;
15905
16170
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
@@ -15920,7 +16185,7 @@ async function localRunner(operation) {
15920
16185
  if (capability.kind === "version")
15921
16186
  return fixed(capability.argv);
15922
16187
  try {
15923
- const metadata = lstatSync4(capability.path);
16188
+ const metadata = lstatSync5(capability.path);
15924
16189
  const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
15925
16190
  return { stdout: present ? `yes
15926
16191
  ` : `no
@@ -16218,19 +16483,19 @@ function localBootstrapHost() {
16218
16483
  };
16219
16484
  return {
16220
16485
  uid: () => process.getuid?.() ?? -1,
16221
- exists: existsSync21,
16486
+ exists: existsSync22,
16222
16487
  read: (path2) => readFileSync17(path2, "utf8"),
16223
16488
  write(path2, content, mode) {
16224
- mkdirSync16(dirname12(path2), { recursive: true, mode: 493 });
16489
+ mkdirSync16(dirname14(path2), { recursive: true, mode: 493 });
16225
16490
  const temporary = `${path2}.next.${process.pid}`;
16226
16491
  writeFileSync16(temporary, content, { mode });
16227
- chmodSync17(temporary, mode);
16228
- renameSync13(temporary, path2);
16492
+ chmodSync18(temporary, mode);
16493
+ renameSync15(temporary, path2);
16229
16494
  },
16230
16495
  mkdir: (path2, mode) => mkdirSync16(path2, { recursive: true, mode }),
16231
- remove: (path2) => rmSync11(path2, { force: true }),
16496
+ remove: (path2) => rmSync12(path2, { force: true }),
16232
16497
  inspect(path2) {
16233
- const value = lstatSync5(path2);
16498
+ const value = lstatSync6(path2);
16234
16499
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
16235
16500
  },
16236
16501
  exec: execute3,
@@ -16251,9 +16516,9 @@ function localBootstrapHost() {
16251
16516
  },
16252
16517
  async installAgent(config, phase) {
16253
16518
  const capabilities = await readCapabilities(localRunner);
16254
- const hasBinding = existsSync21("/var/lib/forgezero/enrolment.json");
16255
- const hasEnrolCredential = existsSync21(ENROL_CREDENTIAL);
16256
- const initialBundle = config.kind === "platform" && !existsSync21(BOOTSTRAP_RELEASE_EVIDENCE) ? {
16519
+ const hasBinding = existsSync22("/var/lib/forgezero/enrolment.json");
16520
+ const hasEnrolCredential = existsSync22(ENROL_CREDENTIAL);
16521
+ const initialBundle = config.kind === "platform" && !existsSync22(BOOTSTRAP_RELEASE_EVIDENCE) ? {
16257
16522
  path: config.bootstrapBundle.bundleFile,
16258
16523
  manifestPath: config.bootstrapBundle.manifestFile,
16259
16524
  manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync17(config.bootstrapBundle.manifestFile, "utf8")))
@@ -16269,7 +16534,7 @@ function localBootstrapHost() {
16269
16534
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
16270
16535
  databasePorts: [8529]
16271
16536
  };
16272
- mkdirSync16(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16537
+ mkdirSync16(dirname14(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
16273
16538
  writeFileSync16(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
16274
16539
  `, { mode: 256 });
16275
16540
  }
@@ -16280,7 +16545,7 @@ function localBootstrapHost() {
16280
16545
  initialBundle
16281
16546
  });
16282
16547
  for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
16283
- mkdirSync16(dirname12(unit3.path), { recursive: true, mode: 493 });
16548
+ mkdirSync16(dirname14(unit3.path), { recursive: true, mode: 493 });
16284
16549
  writeFileSync16(unit3.path, unit3.unit, { mode: 420 });
16285
16550
  }
16286
16551
  await applyPlan(plan, localRunner);
@@ -16295,7 +16560,7 @@ var localRuntime = () => ({
16295
16560
  bootstrapStatus: () => bootstrapStatus(),
16296
16561
  characterDevice(path2) {
16297
16562
  try {
16298
- return lstatSync6(path2).isCharacterDevice();
16563
+ return lstatSync7(path2).isCharacterDevice();
16299
16564
  } catch {
16300
16565
  return false;
16301
16566
  }
@@ -16334,9 +16599,9 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
16334
16599
 
16335
16600
  // src/community-rehearsal-host.ts
16336
16601
  import { createHash as createHash12 } from "crypto";
16337
- import { lstatSync as lstatSync7, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync12, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync17 } from "fs";
16602
+ import { lstatSync as lstatSync8, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync13, symlinkSync as symlinkSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync17 } from "fs";
16338
16603
  import { isIP as isIP5 } from "net";
16339
- import { dirname as dirname13 } from "path";
16604
+ import { dirname as dirname15 } from "path";
16340
16605
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
16341
16606
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
16342
16607
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
@@ -16550,19 +16815,19 @@ async function exec(argv2, stdin) {
16550
16815
  async function applyOperations(operations) {
16551
16816
  for (const operation of operations) {
16552
16817
  if (operation.kind === "remove-tree")
16553
- rmSync12(operation.path, { recursive: true, force: true });
16818
+ rmSync13(operation.path, { recursive: true, force: true });
16554
16819
  else if (operation.kind === "write") {
16555
- mkdirSync17(dirname13(operation.path), { recursive: true });
16820
+ mkdirSync17(dirname15(operation.path), { recursive: true });
16556
16821
  writeFileSync17(operation.path, operation.content, { mode: operation.mode });
16557
16822
  } else if (operation.kind === "unlink") {
16558
16823
  try {
16559
- unlinkSync12(operation.path);
16824
+ unlinkSync13(operation.path);
16560
16825
  } catch (cause) {
16561
16826
  if (cause.code !== "ENOENT")
16562
16827
  throw cause;
16563
16828
  }
16564
16829
  } else if (operation.kind === "symlink")
16565
- symlinkSync6(operation.target, operation.path);
16830
+ symlinkSync8(operation.target, operation.path);
16566
16831
  else {
16567
16832
  const result = await exec(operation.argv, operation.stdin);
16568
16833
  if (!(operation.accepted ?? [0]).includes(result.exitCode))
@@ -16604,7 +16869,7 @@ async function runCommunityRehearsalHost(request) {
16604
16869
  return { ok: true, action: request.action, node: node.name };
16605
16870
  }
16606
16871
  if (request.action === "prepare") {
16607
- const metadata = lstatSync7(request.archivePath);
16872
+ const metadata = lstatSync8(request.archivePath);
16608
16873
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash12("sha256").update(readFileSync18(request.archivePath)).digest("hex") !== request.archiveSha256) {
16609
16874
  throw new Error("community rehearsal archive is not the declared bounded release");
16610
16875
  }
@@ -16654,9 +16919,9 @@ async function runCommunityRehearsalHost(request) {
16654
16919
  }
16655
16920
 
16656
16921
  // src/supervised-app.ts
16657
- import { lstatSync as lstatSync8, readFileSync as readFileSync19 } from "fs";
16922
+ import { lstatSync as lstatSync9, readFileSync as readFileSync19 } from "fs";
16658
16923
  function readConfig(path2) {
16659
- const stat = lstatSync8(path2);
16924
+ const stat = lstatSync9(path2);
16660
16925
  if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 18) !== 0 || stat.size > 128 * 1024) {
16661
16926
  throw new Error("supervised app config is unsafe");
16662
16927
  }
@@ -16839,17 +17104,17 @@ function agentCommandArgs(args, command3) {
16839
17104
  return args.slice(index + 1);
16840
17105
  }
16841
17106
  function loadOrCreateSeed(path2) {
16842
- if (existsSync22(path2)) {
17107
+ if (existsSync23(path2)) {
16843
17108
  const seed2 = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16844
17109
  if (seed2.length < 32) {
16845
17110
  throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
16846
17111
  }
16847
17112
  return seed2;
16848
17113
  }
16849
- mkdirSync18(dirname14(path2), { recursive: true });
17114
+ mkdirSync18(dirname16(path2), { recursive: true });
16850
17115
  const seed = new Uint8Array(randomBytes7(32));
16851
17116
  writeFileSync18(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
16852
- chmodSync18(path2, 384);
17117
+ chmodSync19(path2, 384);
16853
17118
  return seed;
16854
17119
  }
16855
17120
  var DEFAULT_SOCKET_PATH = DEFAULT_SOCKET;
@@ -16878,7 +17143,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
16878
17143
  if (!directory)
16879
17144
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
16880
17145
  const path2 = `${directory}/${name}`;
16881
- if (!existsSync22(path2))
17146
+ if (!existsSync23(path2))
16882
17147
  throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
16883
17148
  const seed = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
16884
17149
  if (seed.length < 32)
@@ -17040,7 +17305,7 @@ if (import.meta.main) {
17040
17305
  if (args.length !== 1)
17041
17306
  throw new Error("project-release-check accepts no coordinates");
17042
17307
  for (const relative of ["src/index.ts", "bun.lock", "src/generated/shared/contract-manifest.json"]) {
17043
- const stat = lstatSync9(join12(process.cwd(), relative));
17308
+ const stat = lstatSync10(join12(process.cwd(), relative));
17044
17309
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1) {
17045
17310
  throw new Error(`project release is missing ${relative}`);
17046
17311
  }
@@ -17379,9 +17644,9 @@ if (import.meta.main) {
17379
17644
  vcpu: cpus2().length,
17380
17645
  memoryGib: Math.max(1, Math.floor(totalmem2() / 1024 ** 3)),
17381
17646
  diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
17382
- kvm: existsSync22("/dev/kvm"),
17383
- snpHost: existsSync22("/dev/sev"),
17384
- helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17647
+ kvm: existsSync23("/dev/kvm"),
17648
+ snpHost: existsSync23("/dev/sev"),
17649
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17385
17650
  }
17386
17651
  };
17387
17652
  const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
@@ -17408,6 +17673,19 @@ if (import.meta.main) {
17408
17673
  const keys2 = deriveKeysFromSeed(seed);
17409
17674
  seed.fill(0);
17410
17675
  const nodeKey2 = process.env.FZ_NODE_KEY ?? keys2.ed25519.publicKey;
17676
+ if (process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true") {
17677
+ const ready = startAgentCandidateReady({
17678
+ version: VERSION2,
17679
+ nodeKey: nodeKey2,
17680
+ bound: true,
17681
+ vault: "unbound"
17682
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17683
+ console.log(`[metal-agent] candidate ${VERSION2} ready for atomic handover`);
17684
+ const stop2 = () => ready.close(() => process.exit(0));
17685
+ process.on("SIGTERM", stop2);
17686
+ process.on("SIGINT", stop2);
17687
+ await new Promise(() => {});
17688
+ }
17411
17689
  const pull = startProvisioningPull({
17412
17690
  apiUrl: process.env.FZ_API,
17413
17691
  nodeKey: nodeKey2,
@@ -17416,9 +17694,9 @@ if (import.meta.main) {
17416
17694
  metalHostname: process.env.FZ_METAL_HOSTNAME,
17417
17695
  run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
17418
17696
  metalPreflight: () => ({
17419
- snpHost: existsSync22("/dev/sev"),
17420
- kvm: existsSync22("/dev/kvm"),
17421
- helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17697
+ snpHost: existsSync23("/dev/sev"),
17698
+ kvm: existsSync23("/dev/kvm"),
17699
+ helper: existsSync23(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
17422
17700
  }),
17423
17701
  onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
17424
17702
  });
@@ -17501,7 +17779,11 @@ if (import.meta.main) {
17501
17779
  const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
17502
17780
  telemetry.event("agent.started");
17503
17781
  telemetry.setDraining(false);
17504
- const attestationSource = existsSync22("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17782
+ const handoverCandidate = process.env.FZ_AGENT_HANDOVER_CANDIDATE === "true";
17783
+ if (!handoverCandidate && process.env.FZ_AGENT_ROUTE_SOCKET && process.env.FZ_SOCKET_PATH) {
17784
+ ensureAgentSocketRoute(process.env.FZ_AGENT_ROUTE_SOCKET, process.env.FZ_SOCKET_PATH);
17785
+ }
17786
+ const attestationSource = existsSync23("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
17505
17787
  const running = runAgent({
17506
17788
  socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
17507
17789
  seedCredential: process.env.FZ_SEED_CREDENTIAL,
@@ -17516,8 +17798,8 @@ if (import.meta.main) {
17516
17798
  const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
17517
17799
  let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
17518
17800
  const enrolmentCredential = process.env.FZ_ENROL_TOKEN_CREDENTIAL;
17519
- const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync22(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
17520
- const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync22(process.env.FZ_ENROL_TOKEN_FILE));
17801
+ const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync23(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
17802
+ const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync23(process.env.FZ_ENROL_TOKEN_FILE));
17521
17803
  if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
17522
17804
  binding = await enrolGuestIdentity({
17523
17805
  apiUrl: process.env.FZ_API,
@@ -17537,6 +17819,7 @@ if (import.meta.main) {
17537
17819
  let nodeApiUrl = binding && process.env.FZ_API ? binding.realm === "platform" ? process.env.FZ_API : tenantNodeApiUrl(process.env.FZ_API, binding.tenantSlug) : process.env.FZ_API;
17538
17820
  let secretCache;
17539
17821
  let vaultSync;
17822
+ let initialVaultState = "unbound";
17540
17823
  if (binding && attestationSource && nodeApiUrl) {
17541
17824
  const result = await telemetry.observe("attestation.refresh", () => attestNodeOnce({
17542
17825
  apiUrl: nodeApiUrl,
@@ -17554,6 +17837,7 @@ if (import.meta.main) {
17554
17837
  projectKey: binding.projectKey
17555
17838
  });
17556
17839
  const initialVault = await telemetry.observe("vault.sync", () => initializeVaultReplica(secretCache), (result) => result.state === "ready" ? "success" : "failed");
17840
+ initialVaultState = initialVault.state;
17557
17841
  running.setVault(secretCache, binding.projectKey);
17558
17842
  vaultSync = startNodeVaultSync(secretCache, {
17559
17843
  telemetry,
@@ -17567,6 +17851,31 @@ if (import.meta.main) {
17567
17851
  console.error("[agent] vault replica unavailable; authenticated refresh remains active and only explicit " + "same-name systemd deployment fallbacks are available");
17568
17852
  }
17569
17853
  }
17854
+ if (handoverCandidate) {
17855
+ const ready = startAgentCandidateReady({
17856
+ version: VERSION2,
17857
+ nodeKey,
17858
+ bound: Boolean(binding),
17859
+ vault: initialVaultState
17860
+ }, process.env.FZ_AGENT_HANDOVER_READY_SOCKET ?? DEFAULT_AGENT_CANDIDATE_READY_SOCKET);
17861
+ console.log(`[agent] candidate ${VERSION2} ready for atomic handover`);
17862
+ let stopping = false;
17863
+ const stop = async () => {
17864
+ if (stopping)
17865
+ return;
17866
+ stopping = true;
17867
+ await vaultSync?.stop();
17868
+ await Promise.all([
17869
+ new Promise((resolve6) => ready.close(() => resolve6())),
17870
+ new Promise((resolve6) => server.close(() => resolve6()))
17871
+ ]);
17872
+ await telemetry.close();
17873
+ process.exit(0);
17874
+ };
17875
+ process.on("SIGTERM", () => void stop());
17876
+ process.on("SIGINT", () => void stop());
17877
+ await new Promise(() => {});
17878
+ }
17570
17879
  const attestationLoop = attestationSource && nodeApiUrl ? startNodeAttestation({
17571
17880
  apiUrl: nodeApiUrl,
17572
17881
  nodeKey,
@@ -17595,7 +17904,7 @@ if (import.meta.main) {
17595
17904
  const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
17596
17905
  const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
17597
17906
  const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
17598
- if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync22(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
17907
+ if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync23(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
17599
17908
  throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
17600
17909
  }
17601
17910
  const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({