@forgezero/agent 0.1.34 → 0.1.36

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,8 +3,8 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { randomBytes as randomBytes5 } from "crypto";
6
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync13, mkdirSync as mkdirSync9, chmodSync as chmodSync12 } from "fs";
7
- import { dirname as dirname7, join as join6 } from "path";
6
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync9, chmodSync as chmodSync13 } from "fs";
7
+ import { dirname as dirname7, join as join7 } from "path";
8
8
 
9
9
  // ../access/dist/security.js
10
10
  var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
@@ -4884,6 +4884,7 @@ function createQueue(options = {}) {
4884
4884
 
4885
4885
  // src/software.ts
4886
4886
  import { readFileSync } from "fs";
4887
+ var PINNED_BUN_VERSION = "1.3.14";
4887
4888
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4888
4889
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
4889
4890
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -4895,7 +4896,8 @@ var SOFTWARE_CATALOG = [
4895
4896
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4896
4897
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4897
4898
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4898
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4899
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4900
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4899
4901
  ];
4900
4902
  var UBUNTU_2604_X64 = [
4901
4903
  {
@@ -4922,6 +4924,11 @@ var UBUNTU_2604_X64 = [
4922
4924
  requirement: { id: "ufw", version: "ubuntu-26.04" },
4923
4925
  check: "command -v ufw >/dev/null",
4924
4926
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
4927
+ },
4928
+ {
4929
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
4930
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
4931
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
4925
4932
  }
4926
4933
  ];
4927
4934
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -4946,7 +4953,7 @@ function validateSoftwareRequirements(value, _options = {}) {
4946
4953
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
4947
4954
  throw new Error("software requirement contains an unknown field");
4948
4955
  }
4949
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4956
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4950
4957
  throw new Error("software requirement coordinate is invalid");
4951
4958
  }
4952
4959
  const requirement = { id: row.id, version: row.version };
@@ -6633,8 +6640,317 @@ function startMigrationPull(options) {
6633
6640
  };
6634
6641
  }
6635
6642
 
6643
+ // src/ssh-bootstrap.ts
6644
+ import { chmodSync as chmodSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
6645
+ import { tmpdir } from "os";
6646
+ import { join as join2 } from "path";
6647
+ import { fileURLToPath } from "url";
6648
+ class SshBootstrapError extends Error {
6649
+ code;
6650
+ constructor(code, message = code) {
6651
+ super(message);
6652
+ this.code = code;
6653
+ }
6654
+ }
6655
+ var defaultExec = async (argv, options = {}) => {
6656
+ const child = Bun.spawn([...argv], {
6657
+ stdin: options.stdin === undefined ? "ignore" : "pipe",
6658
+ stdout: "pipe",
6659
+ stderr: "pipe",
6660
+ env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C" }
6661
+ });
6662
+ if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
6663
+ child.stdin.write(options.stdin);
6664
+ child.stdin.end();
6665
+ }
6666
+ const [stdout, stderr, exitCode] = await Promise.all([
6667
+ new Response(child.stdout).text(),
6668
+ new Response(child.stderr).text(),
6669
+ child.exited
6670
+ ]);
6671
+ return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 8192) };
6672
+ };
6673
+ var checked = async (exec, argv, code, options) => {
6674
+ const result = await exec(argv, options);
6675
+ if (result.exitCode !== 0)
6676
+ throw new SshBootstrapError(code);
6677
+ return result.output.trim();
6678
+ };
6679
+ var validateClaim = (claim) => {
6680
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(claim.computeKey) || !/^[A-Za-z0-9_-]{16,128}$/.test(claim.claimToken) || !/^fze_[A-Za-z0-9_-]{32,128}$/.test(claim.enrolmentToken) || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(claim.realm) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.projectKey) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.environmentKey))
6681
+ throw new SshBootstrapError("CLAIM_INVALID");
6682
+ const { host, port, user, hostKeySha256, nodeHostname } = claim.target;
6683
+ if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
6684
+ throw new SshBootstrapError("CLAIM_INVALID");
6685
+ }
6686
+ };
6687
+ var target = (claim, pinned) => {
6688
+ const address = pinned.addresses[0];
6689
+ return `${claim.target.user}@${address.includes(":") ? `[${address}]` : address}`;
6690
+ };
6691
+ var sshOptions = (claim, pinned, knownHosts, keyPath) => [
6692
+ "-F",
6693
+ "/dev/null",
6694
+ "-o",
6695
+ "BatchMode=yes",
6696
+ "-o",
6697
+ "IdentitiesOnly=yes",
6698
+ "-o",
6699
+ `IdentityFile=${keyPath}`,
6700
+ "-o",
6701
+ "IdentityAgent=none",
6702
+ "-o",
6703
+ `UserKnownHostsFile=${knownHosts}`,
6704
+ "-o",
6705
+ "StrictHostKeyChecking=yes",
6706
+ "-o",
6707
+ `HostKeyAlias=${pinned.hostKeyAlias}`,
6708
+ "-o",
6709
+ "CheckHostIP=no",
6710
+ "-o",
6711
+ "ProxyCommand=none",
6712
+ "-o",
6713
+ "ProxyJump=none",
6714
+ "-o",
6715
+ "ForwardAgent=no",
6716
+ "-o",
6717
+ "ClearAllForwardings=yes",
6718
+ "-o",
6719
+ "ConnectTimeout=10",
6720
+ "-p",
6721
+ String(claim.target.port)
6722
+ ];
6723
+ var scpOptions = (claim, pinned, knownHosts, keyPath) => {
6724
+ const options = sshOptions(claim, pinned, knownHosts, keyPath);
6725
+ const portFlag = options.lastIndexOf("-p");
6726
+ if (portFlag < 0)
6727
+ throw new SshBootstrapError("SSH_OPTIONS_INVALID");
6728
+ options[portFlag] = "-P";
6729
+ return options;
6730
+ };
6731
+ async function pinnedKnownHost(claim, pinned, directory, exec) {
6732
+ const address = pinned.addresses[0];
6733
+ const scanned = await checked(exec, [
6734
+ "ssh-keyscan",
6735
+ "-T",
6736
+ "5",
6737
+ "-p",
6738
+ String(claim.target.port),
6739
+ "-t",
6740
+ "ed25519",
6741
+ address
6742
+ ], "HOST_KEY_SCAN_FAILED");
6743
+ const candidates = scanned.split(`
6744
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
6745
+ let matched;
6746
+ for (const line of candidates) {
6747
+ const fingerprint = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
6748
+ ` });
6749
+ if (fingerprint.split(/\s+/).includes(claim.target.hostKeySha256)) {
6750
+ matched = line;
6751
+ break;
6752
+ }
6753
+ }
6754
+ if (!matched)
6755
+ throw new SshBootstrapError("HOST_KEY_MISMATCH");
6756
+ const key = matched.split(/\s+/).slice(1).join(" ");
6757
+ if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}$/.test(key))
6758
+ throw new SshBootstrapError("HOST_KEY_INVALID");
6759
+ const path = join2(directory, "known_hosts");
6760
+ writeFileSync3(path, `${pinned.hostKeyAlias} ${key}
6761
+ `, { mode: 384, flag: "wx" });
6762
+ return path;
6763
+ }
6764
+ var remoteInstallScript = `set -Eeuo pipefail
6765
+ umask 077
6766
+ work="$1"
6767
+ if [[ "$(/usr/local/bin/bun --version 2>/dev/null || true)" != "${PINNED_BUN_VERSION}" ]]; then
6768
+ tmp="$(mktemp -d)"; trap 'rm -rf -- "$tmp"' EXIT
6769
+ curl -fsSL https://bun.sh/install -o "$tmp/install"
6770
+ echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -
6771
+ BUN_INSTALL="$tmp/bun" BUN_VERSION=${PINNED_BUN_VERSION} bash "$tmp/install" >/dev/null
6772
+ sudo -n install -d -m 0755 /usr/local/lib/forgezero/runtime
6773
+ sudo -n install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next
6774
+ sudo -n mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun
6775
+ sudo -n ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun
6776
+ fi
6777
+ sudo -n install -d -m 0755 /usr/local/lib/forgezero/agent /usr/local/bin /run/forgezero-bootstrap
6778
+ sudo -n install -m 0755 "$work/fz.js" /usr/local/lib/forgezero/agent/fz.js
6779
+ sudo -n install -m 0755 "$work/fz-agent.js" /usr/local/lib/forgezero/agent/fz-agent.js
6780
+ sudo -n ln -sfn /usr/local/lib/forgezero/agent/fz.js /usr/local/bin/fz
6781
+ sudo -n install -m 0600 "$work/config.json" /run/forgezero-bootstrap/config.json
6782
+ sudo -n install -m 0600 "$work/enrol.token" /run/forgezero-bootstrap/enrol.token
6783
+ sudo -n /usr/local/bin/fz bootstrap tenant --bootstrap-config /run/forgezero-bootstrap/config.json --apply
6784
+ sudo -n rm -rf -- /run/forgezero-bootstrap
6785
+ `;
6786
+ async function executeSshBootstrap(claim, options) {
6787
+ validateClaim(claim);
6788
+ if (!options.sshKeyPath.startsWith("/") || /[\r\n]/.test(options.sshKeyPath))
6789
+ throw new SshBootstrapError("SSH_KEY_INVALID");
6790
+ const telemetry = new URL(options.targetTelemetryEndpoint);
6791
+ if (telemetry.protocol !== "https:" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash || telemetry.port && telemetry.port !== "443")
6792
+ throw new SshBootstrapError("TARGET_TELEMETRY_INVALID");
6793
+ const api = new URL(options.platformApiUrl);
6794
+ if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash)
6795
+ throw new SshBootstrapError("TARGET_API_INVALID");
6796
+ const exec = options.exec ?? defaultExec;
6797
+ const directory = mkdtempSync(join2(tmpdir(), "forgezero-ssh-bootstrap-"));
6798
+ chmodSync5(directory, 448);
6799
+ let remoteDirectory = "";
6800
+ let pinned;
6801
+ try {
6802
+ const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
6803
+ const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
6804
+ if (!resolved || resolved.protocol !== "ssh")
6805
+ throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
6806
+ pinned = resolved;
6807
+ const knownHosts = await pinnedKnownHost(claim, pinned, directory, exec);
6808
+ const ssh = sshOptions(claim, pinned, knownHosts, options.sshKeyPath);
6809
+ remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
6810
+ if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
6811
+ throw new SshBootstrapError("REMOTE_PATH_INVALID");
6812
+ const config = {
6813
+ kind: "tenant",
6814
+ apiUrl: api.origin,
6815
+ realm: claim.realm,
6816
+ nodeHostname: claim.target.nodeHostname,
6817
+ telemetryEndpoint: telemetry.origin,
6818
+ enrolTokenFile: "/run/forgezero-bootstrap/enrol.token",
6819
+ profile: "app",
6820
+ software: [{ id: "bun", version: PINNED_BUN_VERSION }]
6821
+ };
6822
+ const files = {
6823
+ "config.json": `${JSON.stringify(config, null, 2)}
6824
+ `,
6825
+ "enrol.token": `${claim.enrolmentToken}
6826
+ `
6827
+ };
6828
+ for (const [name, content] of Object.entries(files))
6829
+ writeFileSync3(join2(directory, name), content, { mode: 384, flag: "wx" });
6830
+ const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
6831
+ const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
6832
+ for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
6833
+ if (!readFileSync4(source).length)
6834
+ throw new SshBootstrapError("PACKAGE_ARTIFACT_MISSING");
6835
+ await checked(exec, [
6836
+ "scp",
6837
+ ...scpOptions(claim, pinned, knownHosts, options.sshKeyPath),
6838
+ source,
6839
+ `${target(claim, pinned)}:${remoteDirectory}/${name}`
6840
+ ], "SCP_FAILED");
6841
+ }
6842
+ for (const name of Object.keys(files))
6843
+ await checked(exec, [
6844
+ "scp",
6845
+ ...scpOptions(claim, pinned, knownHosts, options.sshKeyPath),
6846
+ join2(directory, name),
6847
+ `${target(claim, pinned)}:${remoteDirectory}/${name}`
6848
+ ], "SCP_FAILED", { secret: name === "enrol.token" });
6849
+ await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "bash", "-s", "--", remoteDirectory], "REMOTE_BOOTSTRAP_FAILED", { stdin: remoteInstallScript, secret: true });
6850
+ } finally {
6851
+ if (remoteDirectory && pinned) {
6852
+ try {
6853
+ const knownHosts = join2(directory, "known_hosts");
6854
+ await exec([
6855
+ "ssh",
6856
+ ...sshOptions(claim, pinned, knownHosts, options.sshKeyPath),
6857
+ target(claim, pinned),
6858
+ "--",
6859
+ "sudo",
6860
+ "-n",
6861
+ "rm",
6862
+ "-rf",
6863
+ "--",
6864
+ remoteDirectory
6865
+ ]);
6866
+ } catch {}
6867
+ }
6868
+ rmSync(directory, { recursive: true, force: true });
6869
+ }
6870
+ }
6871
+ var post3 = (options, operation, body) => postSignedNode(options, `v1/node/bootstrap/${operation}`, body);
6872
+ async function pullSshBootstrapOnce(options) {
6873
+ const response = await post3(options, "claim", {});
6874
+ if (!response.claim)
6875
+ return { status: "idle" };
6876
+ const claim = response.claim;
6877
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
6878
+ const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
6879
+ const now = options.now ?? Date.now;
6880
+ let expires = claim.claimExpiresAtTs;
6881
+ let timer;
6882
+ let stopped = false;
6883
+ let renewal = null;
6884
+ const renew = () => {
6885
+ if (stopped)
6886
+ return;
6887
+ timer = setTimer(() => {
6888
+ renewal = post3(options, "renew", {
6889
+ computeKey: claim.computeKey,
6890
+ claimToken: claim.claimToken
6891
+ }).then((value) => {
6892
+ expires = value.claimExpiresAtTs;
6893
+ }).finally(() => {
6894
+ renewal = null;
6895
+ if (!stopped)
6896
+ renew();
6897
+ });
6898
+ }, Math.max(1000, Math.floor(Math.max(0, expires - now()) / 3)));
6899
+ };
6900
+ renew();
6901
+ try {
6902
+ await executeSshBootstrap(claim, options);
6903
+ stopped = true;
6904
+ clearTimer(timer);
6905
+ await renewal;
6906
+ await post3(options, "complete", { computeKey: claim.computeKey, claimToken: claim.claimToken, ok: true });
6907
+ return { status: "completed", computeKey: claim.computeKey, attempt: claim.attempt };
6908
+ } catch (cause) {
6909
+ stopped = true;
6910
+ clearTimer(timer);
6911
+ await renewal;
6912
+ if (cause instanceof SignedNodeHttpError && cause.status < 500)
6913
+ throw cause;
6914
+ const failureCode = cause instanceof SshBootstrapError ? cause.code : "BOOTSTRAP_FAILED";
6915
+ await post3(options, "complete", { computeKey: claim.computeKey, claimToken: claim.claimToken, ok: false, failureCode });
6916
+ return { status: "failed", computeKey: claim.computeKey, attempt: claim.attempt, failureCode };
6917
+ }
6918
+ }
6919
+ function startSshBootstrapPull(options) {
6920
+ const interval = Math.max(1000, options.intervalMs ?? 5000);
6921
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
6922
+ const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
6923
+ let stopped = false;
6924
+ let timer;
6925
+ let active = null;
6926
+ const tick = () => {
6927
+ if (stopped || active)
6928
+ return;
6929
+ active = pullSshBootstrapOnce(options).then((result) => options.onEvent?.(result.status, result.status === "idle" ? undefined : {
6930
+ computeKey: result.computeKey,
6931
+ attempt: result.attempt,
6932
+ ...result.status === "failed" ? { failureCode: result.failureCode } : {}
6933
+ })).catch((cause) => options.onEvent?.("poll-failed", cause)).finally(() => {
6934
+ active = null;
6935
+ if (!stopped)
6936
+ timer = setTimer(tick, interval);
6937
+ });
6938
+ };
6939
+ tick();
6940
+ return {
6941
+ async stop() {
6942
+ stopped = true;
6943
+ clearTimer(timer);
6944
+ await active;
6945
+ },
6946
+ get active() {
6947
+ return !stopped;
6948
+ }
6949
+ };
6950
+ }
6951
+
6636
6952
  // src/metal-helper-socket.ts
6637
- import { chmodSync as chmodSync5, existsSync as existsSync6, unlinkSync as unlinkSync5 } from "fs";
6953
+ import { chmodSync as chmodSync6, existsSync as existsSync6, unlinkSync as unlinkSync5 } from "fs";
6638
6954
  import { connect as connect2, createServer as createServer3 } from "net";
6639
6955
 
6640
6956
  // src/metal-provision.ts
@@ -6642,14 +6958,14 @@ import { createHash as createHash3 } from "crypto";
6642
6958
  import {
6643
6959
  existsSync as existsSync5,
6644
6960
  mkdirSync as mkdirSync3,
6645
- readFileSync as readFileSync4,
6961
+ readFileSync as readFileSync5,
6646
6962
  readdirSync,
6647
- rmSync,
6963
+ rmSync as rmSync2,
6648
6964
  statSync as statSync2,
6649
6965
  unlinkSync as unlinkSync4,
6650
- writeFileSync as writeFileSync3
6966
+ writeFileSync as writeFileSync4
6651
6967
  } from "fs";
6652
- import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2 } from "path";
6968
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3 } from "path";
6653
6969
  import { isIP as isIP2 } from "net";
6654
6970
 
6655
6971
  // src/compute.ts
@@ -6875,7 +7191,7 @@ function validateMetalProfile(profile) {
6875
7191
  var readManifests = (stateDir) => {
6876
7192
  if (!existsSync5(stateDir))
6877
7193
  return [];
6878
- return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync4(join2(stateDir, name), "utf8")));
7194
+ return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync5(join3(stateDir, name), "utf8")));
6879
7195
  };
6880
7196
  function allocateAddress(profile, computeKey, rows) {
6881
7197
  const existing = rows.find((row) => row.computeKey === computeKey);
@@ -7008,7 +7324,7 @@ ethernets:
7008
7324
  `
7009
7325
  };
7010
7326
  }
7011
- var checked = async (exec, argv) => {
7327
+ var checked2 = async (exec, argv) => {
7012
7328
  const result = await exec(argv);
7013
7329
  if (result.exitCode !== 0) {
7014
7330
  throw new MetalProvisionError(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
@@ -7026,7 +7342,7 @@ async function provisionMetalGuest(profile, claim, exec) {
7026
7342
  }
7027
7343
  if (!statSync2(image.path).isFile())
7028
7344
  throw new MetalProvisionError("configured image is not a file");
7029
- const digest = (await checked(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
7345
+ const digest = (await checked2(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
7030
7346
  if (digest !== image.sha256)
7031
7347
  throw new MetalProvisionError("configured image checksum mismatch");
7032
7348
  for (const path of [profile.stateDir, profile.seedDir, profile.unitDir])
@@ -7043,7 +7359,7 @@ async function provisionMetalGuest(profile, claim, exec) {
7043
7359
  const conflictingName = manifests.find((row) => row.name === name && row.computeKey !== claim.computeKey);
7044
7360
  if (conflictingName)
7045
7361
  throw new MetalProvisionError("requested guest name is already allocated");
7046
- const manifestPath = join2(profile.stateDir, `${name}.json`);
7362
+ const manifestPath = join3(profile.stateDir, `${name}.json`);
7047
7363
  const prior = manifests.find((row) => row.computeKey === claim.computeKey);
7048
7364
  if (prior && prior.reference !== claim.spec.reference)
7049
7365
  throw new MetalProvisionError("compute identity conflicts with host inventory");
@@ -7060,25 +7376,25 @@ async function provisionMetalGuest(profile, claim, exec) {
7060
7376
  allowedMemoryNodes: cpuPool.memoryNodes,
7061
7377
  phase: "allocating"
7062
7378
  };
7063
- const save = () => writeFileSync3(manifestPath, `${JSON.stringify(manifest, null, 2)}
7379
+ const save = () => writeFileSync4(manifestPath, `${JSON.stringify(manifest, null, 2)}
7064
7380
  `, { mode: 384 });
7065
7381
  save();
7066
7382
  const lv = `/dev/${profile.volumeGroup}/${name}`;
7067
7383
  const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
7068
7384
  if (!exists)
7069
- await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
7385
+ await checked2(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
7070
7386
  if (manifest.phase === "allocating") {
7071
- await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
7387
+ await checked2(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
7072
7388
  manifest.phase = "image-ready";
7073
7389
  save();
7074
7390
  }
7075
- const seedBase = join2(profile.seedDir, name);
7391
+ const seedBase = join3(profile.seedDir, name);
7076
7392
  const init = cloudInit(profile, claim, manifest);
7077
- writeFileSync3(`${seedBase}-user-data`, init.userData, { mode: 384 });
7078
- writeFileSync3(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
7079
- writeFileSync3(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
7393
+ writeFileSync4(`${seedBase}-user-data`, init.userData, { mode: 384 });
7394
+ writeFileSync4(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
7395
+ writeFileSync4(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
7080
7396
  const seed = `${seedBase}-seed.iso`;
7081
- await checked(exec, [
7397
+ await checked2(exec, [
7082
7398
  "cloud-localds",
7083
7399
  "-N",
7084
7400
  `${seedBase}-network-config`,
@@ -7108,16 +7424,16 @@ async function provisionMetalGuest(profile, claim, exec) {
7108
7424
  throw new MetalProvisionError("confidential compute requested but host SNP profile is absent");
7109
7425
  }
7110
7426
  const service = `forgezero-guest@${name}.service`;
7111
- const unitPath = join2(profile.unitDir, service);
7427
+ const unitPath = join3(profile.unitDir, service);
7112
7428
  mkdirSync3(dirname3(unitPath), { recursive: true });
7113
- writeFileSync3(unitPath, guestUnit(spec), { mode: 420 });
7114
- await checked(exec, ["systemctl", "daemon-reload"]);
7429
+ writeFileSync4(unitPath, guestUnit(spec), { mode: 420 });
7430
+ await checked2(exec, ["systemctl", "daemon-reload"]);
7115
7431
  if (prior?.phase === "running") {
7116
- await checked(exec, ["systemctl", "restart", service]);
7432
+ await checked2(exec, ["systemctl", "restart", service]);
7117
7433
  } else {
7118
- await checked(exec, ["systemctl", "enable", "--now", service]);
7434
+ await checked2(exec, ["systemctl", "enable", "--now", service]);
7119
7435
  }
7120
- await checked(exec, ["systemctl", "is-active", service]);
7436
+ await checked2(exec, ["systemctl", "is-active", service]);
7121
7437
  manifest.phase = "running";
7122
7438
  save();
7123
7439
  return { guestAddress: address };
@@ -7126,11 +7442,11 @@ function legacyPlatformManifest(profile, claim, name) {
7126
7442
  if (claim.bootstrap?.kind !== "platform-genesis" || claim.computeKey !== `platform:${name}` || claim.spec.reference !== `platform:${name}` || claim.spec.guestAddress === undefined)
7127
7443
  return;
7128
7444
  const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
7129
- const legacyPath = join2(legacyDir, `${name}.conf`);
7445
+ const legacyPath = join3(legacyDir, `${name}.conf`);
7130
7446
  if (!existsSync5(legacyPath))
7131
7447
  return;
7132
7448
  const values = new Map;
7133
- for (const line of readFileSync4(legacyPath, "utf8").split(/\r?\n/)) {
7449
+ for (const line of readFileSync5(legacyPath, "utf8").split(/\r?\n/)) {
7134
7450
  if (!line || line.startsWith("#"))
7135
7451
  continue;
7136
7452
  const match = /^([A-Z][A-Z0-9_]*)=([^\s'"`$;|&<>]+)$/.exec(line);
@@ -7160,13 +7476,13 @@ async function removeMetalGuest(profile, claim, exec) {
7160
7476
  if (claim.action !== "delete")
7161
7477
  throw new MetalProvisionError("create claim cannot remove a guest");
7162
7478
  const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
7163
- const manifestPath = join2(profile.stateDir, `${name}.json`);
7479
+ const manifestPath = join3(profile.stateDir, `${name}.json`);
7164
7480
  const service = `forgezero-guest@${name}.service`;
7165
- const unitPath = join2(profile.unitDir, service);
7481
+ const unitPath = join3(profile.unitDir, service);
7166
7482
  const lv = `/dev/${profile.volumeGroup}/${name}`;
7167
- const manifest = existsSync5(manifestPath) ? JSON.parse(readFileSync4(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
7483
+ const manifest = existsSync5(manifestPath) ? JSON.parse(readFileSync5(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
7168
7484
  if (!manifest) {
7169
- const seedBase = join2(profile.seedDir, name);
7485
+ const seedBase = join3(profile.seedDir, name);
7170
7486
  const localArtifacts = [
7171
7487
  unitPath,
7172
7488
  `${seedBase}-seed.iso`,
@@ -7192,33 +7508,33 @@ async function removeMetalGuest(profile, claim, exec) {
7192
7508
  throw new MetalProvisionError("compute identity conflicts with host inventory");
7193
7509
  }
7194
7510
  if (existsSync5(unitPath))
7195
- await checked(exec, ["systemctl", "disable", "--now", service]);
7511
+ await checked2(exec, ["systemctl", "disable", "--now", service]);
7196
7512
  else if (legacyPlatformIdentity)
7197
- await checked(exec, ["systemctl", "disable", "--now", service]);
7513
+ await checked2(exec, ["systemctl", "disable", "--now", service]);
7198
7514
  else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
7199
7515
  throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
7200
7516
  }
7201
7517
  if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
7202
- await checked(exec, ["lvremove", "-fy", lv]);
7518
+ await checked2(exec, ["lvremove", "-fy", lv]);
7203
7519
  for (const path of [
7204
7520
  unitPath,
7205
- join2(profile.seedDir, `${name}-seed.iso`),
7206
- join2(profile.seedDir, `${name}-user-data`),
7207
- join2(profile.seedDir, `${name}-meta-data`),
7208
- join2(profile.seedDir, `${name}-network-config`),
7521
+ join3(profile.seedDir, `${name}-seed.iso`),
7522
+ join3(profile.seedDir, `${name}-user-data`),
7523
+ join3(profile.seedDir, `${name}-meta-data`),
7524
+ join3(profile.seedDir, `${name}-network-config`),
7209
7525
  manifestPath
7210
7526
  ])
7211
7527
  if (existsSync5(path))
7212
7528
  unlinkSync4(path);
7213
7529
  if (legacyPlatformIdentity) {
7214
- const legacyConfig = join2(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
7215
- const legacyDropIn = join2(profile.unitDir, `${service}.d`);
7530
+ const legacyConfig = join3(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
7531
+ const legacyDropIn = join3(profile.unitDir, `${service}.d`);
7216
7532
  if (existsSync5(legacyConfig))
7217
7533
  unlinkSync4(legacyConfig);
7218
7534
  if (existsSync5(legacyDropIn))
7219
- rmSync(legacyDropIn, { recursive: true });
7535
+ rmSync2(legacyDropIn, { recursive: true });
7220
7536
  }
7221
- await checked(exec, ["systemctl", "daemon-reload"]);
7537
+ await checked2(exec, ["systemctl", "daemon-reload"]);
7222
7538
  return {};
7223
7539
  }
7224
7540
 
@@ -7301,7 +7617,7 @@ function startMetalHelper(options) {
7301
7617
  });
7302
7618
  socket.on("error", () => socket.destroy());
7303
7619
  });
7304
- server.listen(socketPath, () => chmodSync5(socketPath, 432));
7620
+ server.listen(socketPath, () => chmodSync6(socketPath, 432));
7305
7621
  return {
7306
7622
  server,
7307
7623
  async stop() {
@@ -7341,7 +7657,7 @@ function requestMetalProvision(claim, socketPath = DEFAULT_METAL_HELPER_SOCKET)
7341
7657
  }
7342
7658
 
7343
7659
  // src/deployment-runner.ts
7344
- import { chmodSync as chmodSync6, existsSync as existsSync7, realpathSync, unlinkSync as unlinkSync6 } from "fs";
7660
+ import { chmodSync as chmodSync7, existsSync as existsSync7, realpathSync, unlinkSync as unlinkSync6 } from "fs";
7345
7661
  import { isAbsolute as isAbsolute4, resolve, sep } from "path";
7346
7662
  import { connect as connect3, createServer as createServer4 } from "net";
7347
7663
  var DEFAULT_DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
@@ -7490,7 +7806,7 @@ function startDeploymentRunner(options) {
7490
7806
  const ready = new Promise((resolveReady, rejectReady) => {
7491
7807
  server.once("error", rejectReady);
7492
7808
  server.listen(socketPath, () => {
7493
- chmodSync6(socketPath, options.socketMode ?? 432);
7809
+ chmodSync7(socketPath, options.socketMode ?? 432);
7494
7810
  server.off("error", rejectReady);
7495
7811
  resolveReady();
7496
7812
  });
@@ -7720,8 +8036,8 @@ function startNodeAttestation(options) {
7720
8036
  }
7721
8037
 
7722
8038
  // src/metal-isolation.ts
7723
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
7724
- import { join as join3 } from "path";
8039
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
8040
+ import { join as join4 } from "path";
7725
8041
  var members = (list) => list.split(",").flatMap((part) => {
7726
8042
  const [first, last = first] = part.split("-").map(Number);
7727
8043
  return Array.from({ length: last - first + 1 }, (_, index) => first + index);
@@ -7758,7 +8074,7 @@ function metalHousekeepingDropIn(profile, kind) {
7758
8074
  AllowedCPUs=${profile.housekeepingCpus}
7759
8075
  ${memoryDirective(profile.housekeepingMemoryNodes)}`;
7760
8076
  }
7761
- var defaultExec = async (argv) => {
8077
+ var defaultExec2 = async (argv) => {
7762
8078
  const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
7763
8079
  const [exitCode, stdout, stderr] = await Promise.all([
7764
8080
  child.exited,
@@ -7767,14 +8083,14 @@ var defaultExec = async (argv) => {
7767
8083
  ]);
7768
8084
  return { exitCode, stdout, stderr };
7769
8085
  };
7770
- var checked2 = async (exec, argv) => {
8086
+ var checked3 = async (exec, argv) => {
7771
8087
  const result = await exec(argv);
7772
8088
  if (result.exitCode !== 0)
7773
8089
  throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
7774
8090
  return result;
7775
8091
  };
7776
8092
  var requireGuestsInSlice = async (exec) => {
7777
- const active = await checked2(exec, [
8093
+ const active = await checked3(exec, [
7778
8094
  "systemctl",
7779
8095
  "list-units",
7780
8096
  "--type=service",
@@ -7788,33 +8104,33 @@ var requireGuestsInSlice = async (exec) => {
7788
8104
  const service = line.trim().split(/\s+/)[0];
7789
8105
  if (!service)
7790
8106
  continue;
7791
- const cgroup = await checked2(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
8107
+ const cgroup = await checked3(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
7792
8108
  if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
7793
8109
  throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
7794
8110
  }
7795
8111
  }
7796
8112
  };
7797
- async function applyMetalIsolation(profile, exec = defaultExec) {
8113
+ async function applyMetalIsolation(profile, exec = defaultExec2) {
7798
8114
  validateMetalProfile(profile);
7799
8115
  await requireGuestsInSlice(exec);
7800
8116
  const unitDir = profile.unitDir;
7801
8117
  mkdirSync4(unitDir, { recursive: true });
7802
- writeFileSync4(join3(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
8118
+ writeFileSync5(join4(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
7803
8119
  for (const unit of ["system.slice", "user.slice"]) {
7804
- const directory = join3(unitDir, `${unit}.d`);
8120
+ const directory = join4(unitDir, `${unit}.d`);
7805
8121
  mkdirSync4(directory, { recursive: true });
7806
- writeFileSync4(join3(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
8122
+ writeFileSync5(join4(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
7807
8123
  }
7808
- const initDirectory = join3(unitDir, "init.scope.d");
8124
+ const initDirectory = join4(unitDir, "init.scope.d");
7809
8125
  mkdirSync4(initDirectory, { recursive: true });
7810
- writeFileSync4(join3(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
7811
- await checked2(exec, ["systemctl", "daemon-reload"]);
8126
+ writeFileSync5(join4(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
8127
+ await checked3(exec, ["systemctl", "daemon-reload"]);
7812
8128
  await requireGuestsInSlice(exec);
7813
8129
  const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
7814
8130
  if (profile.housekeepingMemoryNodes)
7815
8131
  properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
7816
8132
  for (const unit of ["system.slice", "user.slice", "init.scope"]) {
7817
- await checked2(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
8133
+ await checked3(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
7818
8134
  }
7819
8135
  }
7820
8136
 
@@ -7842,7 +8158,7 @@ async function closeServerWithin(server, timeoutMs) {
7842
8158
  }
7843
8159
 
7844
8160
  // src/lifecycle-helper.ts
7845
- import { chmodSync as chmodSync7, existsSync as existsSync9, readFileSync as readFileSync5, unlinkSync as unlinkSync7 } from "fs";
8161
+ import { chmodSync as chmodSync8, existsSync as existsSync9, readFileSync as readFileSync6, unlinkSync as unlinkSync7 } from "fs";
7846
8162
  import { connect as connect4, createConnection, createServer as createServer5, isIP as isIP3 } from "net";
7847
8163
  var DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
7848
8164
  var MAX_REQUEST_BYTES4 = 16 * 1024;
@@ -7886,7 +8202,7 @@ function validateLifecycleProfile(profile) {
7886
8202
  }
7887
8203
  }
7888
8204
  function loadLifecycleProfile(path) {
7889
- const profile = JSON.parse(readFileSync5(path, "utf8"));
8205
+ const profile = JSON.parse(readFileSync6(path, "utf8"));
7890
8206
  validateLifecycleProfile(profile);
7891
8207
  return profile;
7892
8208
  }
@@ -8024,7 +8340,7 @@ function startLifecycleHelper(options) {
8024
8340
  });
8025
8341
  socket.on("error", () => socket.destroy());
8026
8342
  });
8027
- server.listen(socketPath, () => chmodSync7(socketPath, 432));
8343
+ server.listen(socketPath, () => chmodSync8(socketPath, 432));
8028
8344
  return {
8029
8345
  server,
8030
8346
  async stop() {
@@ -8064,7 +8380,7 @@ function requestLifecycleAction(claim, socketPath = DEFAULT_LIFECYCLE_HELPER_SOC
8064
8380
  }
8065
8381
 
8066
8382
  // src/warp-config.ts
8067
- import { chmodSync as chmodSync8, mkdirSync as mkdirSync5, renameSync as renameSync3, symlinkSync, unlinkSync as unlinkSync8, writeFileSync as writeFileSync5 } from "fs";
8383
+ import { chmodSync as chmodSync9, mkdirSync as mkdirSync5, renameSync as renameSync3, symlinkSync, unlinkSync as unlinkSync8, writeFileSync as writeFileSync6 } from "fs";
8068
8384
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
8069
8385
  function renderWarpMdm(options) {
8070
8386
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.organization))
@@ -8088,8 +8404,8 @@ function materializeWarpMdm(options) {
8088
8404
  mkdirSync5(runtimePath.replace(/\/[^/]+$/, ""), { recursive: true, mode: 448 });
8089
8405
  mkdirSync5(servicePath.replace(/\/[^/]+$/, ""), { recursive: true, mode: 448 });
8090
8406
  const next = `${runtimePath}.next`;
8091
- writeFileSync5(next, renderWarpMdm(options), { mode: 384 });
8092
- chmodSync8(next, 384);
8407
+ writeFileSync6(next, renderWarpMdm(options), { mode: 384 });
8408
+ chmodSync9(next, 384);
8093
8409
  renameSync3(next, runtimePath);
8094
8410
  try {
8095
8411
  unlinkSync8(servicePath);
@@ -8104,20 +8420,20 @@ function materializeWarpMdm(options) {
8104
8420
  // src/agent-update.ts
8105
8421
  import { createHash as createHash4, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
8106
8422
  import {
8107
- chmodSync as chmodSync9,
8423
+ chmodSync as chmodSync10,
8108
8424
  closeSync,
8109
8425
  existsSync as existsSync10,
8110
8426
  fsyncSync,
8111
8427
  mkdirSync as mkdirSync6,
8112
8428
  openSync,
8113
- readFileSync as readFileSync6,
8429
+ readFileSync as readFileSync7,
8114
8430
  readlinkSync,
8115
8431
  renameSync as renameSync4,
8116
- rmSync as rmSync2,
8432
+ rmSync as rmSync3,
8117
8433
  symlinkSync as symlinkSync2,
8118
- writeFileSync as writeFileSync6
8434
+ writeFileSync as writeFileSync7
8119
8435
  } from "fs";
8120
- import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
8436
+ import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
8121
8437
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
8122
8438
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
8123
8439
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
@@ -8133,10 +8449,10 @@ var syncPath = (path) => {
8133
8449
  };
8134
8450
  var syncReleaseDirectory = (directory) => {
8135
8451
  for (const path of [
8136
- join4(directory, "package.json"),
8137
- join4(directory, "dist", "fz-agent.js"),
8138
- join4(directory, "dist", "fz.js"),
8139
- join4(directory, "dist"),
8452
+ join5(directory, "package.json"),
8453
+ join5(directory, "dist", "fz-agent.js"),
8454
+ join5(directory, "dist", "fz.js"),
8455
+ join5(directory, "dist"),
8140
8456
  directory,
8141
8457
  dirname4(directory)
8142
8458
  ])
@@ -8189,27 +8505,27 @@ var command = async (input) => {
8189
8505
  ]);
8190
8506
  return { exitCode, output: `${stdout}${stderr}` };
8191
8507
  };
8192
- var checked3 = async (run, input, label) => {
8508
+ var checked4 = async (run, input, label) => {
8193
8509
  const result = await run(input);
8194
8510
  if (result.exitCode !== 0)
8195
8511
  throw new Error(`${label} failed: ${result.output.trim()}`);
8196
8512
  return result;
8197
8513
  };
8198
8514
  async function validateReleaseDirectory(directory, release, run) {
8199
- const manifest = JSON.parse(readFileSync6(join4(directory, "package.json"), "utf8"));
8515
+ const manifest = JSON.parse(readFileSync7(join5(directory, "package.json"), "utf8"));
8200
8516
  if (manifest.name !== release.package || manifest.version !== release.version) {
8201
8517
  throw new Error("agent update manifest does not match the selected release");
8202
8518
  }
8203
- const agent = join4(directory, "dist", "fz-agent.js");
8204
- const cli = join4(directory, "dist", "fz.js");
8519
+ const agent = join5(directory, "dist", "fz-agent.js");
8520
+ const cli = join5(directory, "dist", "fz.js");
8205
8521
  for (const binary of [agent, cli]) {
8206
- if (!readFileSync6(binary, "utf8").startsWith(`#!/usr/bin/env bun
8522
+ if (!readFileSync7(binary, "utf8").startsWith(`#!/usr/bin/env bun
8207
8523
  `)) {
8208
8524
  throw new Error("agent update artifact is not a self-contained Bun executable");
8209
8525
  }
8210
- chmodSync9(binary, 493);
8526
+ chmodSync10(binary, 493);
8211
8527
  }
8212
- const version = (await checked3(run, { command: agent, args: ["--version"] }, "agent update smoke test")).output.trim();
8528
+ const version = (await checked4(run, { command: agent, args: ["--version"] }, "agent update smoke test")).output.trim();
8213
8529
  if (version !== release.version)
8214
8530
  throw new Error(`agent update binary reports ${version}`);
8215
8531
  }
@@ -8219,12 +8535,12 @@ async function stageAgentRelease(releaseInput, options) {
8219
8535
  throw new Error(`agent update ${release.version} is not newer than ${options.currentVersion}`);
8220
8536
  }
8221
8537
  const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
8222
- const versions = join4(root, "versions");
8223
- const finalDirectory = join4(versions, release.version);
8224
- const currentLink = join4(root, "current");
8225
- const stage2 = join4(versions, `.${release.version}.${randomUUID2()}.staging`);
8226
- const archive = join4(stage2, "agent.tgz");
8227
- const unpacked = join4(stage2, "unpacked");
8538
+ const versions = join5(root, "versions");
8539
+ const finalDirectory = join5(versions, release.version);
8540
+ const currentLink = join5(root, "current");
8541
+ const stage2 = join5(versions, `.${release.version}.${randomUUID2()}.staging`);
8542
+ const archive = join5(stage2, "agent.tgz");
8543
+ const unpacked = join5(stage2, "unpacked");
8228
8544
  const run = options.run ?? command;
8229
8545
  mkdirSync6(unpacked, { recursive: true, mode: 448 });
8230
8546
  try {
@@ -8245,8 +8561,8 @@ async function stageAgentRelease(releaseInput, options) {
8245
8561
  const actual = createHash4("sha512").update(bytes).digest();
8246
8562
  if (!timingSafeEqual(actual, expected))
8247
8563
  throw new Error("agent update integrity mismatch");
8248
- writeFileSync6(archive, bytes, { mode: 384, flag: "wx" });
8249
- await checked3(run, {
8564
+ writeFileSync7(archive, bytes, { mode: 384, flag: "wx" });
8565
+ await checked4(run, {
8250
8566
  command: "/usr/bin/tar",
8251
8567
  args: [
8252
8568
  "-xzf",
@@ -8269,10 +8585,10 @@ async function stageAgentRelease(releaseInput, options) {
8269
8585
  throw new Error("agent update requires an active immutable release to roll back to");
8270
8586
  }
8271
8587
  const previousTarget = readlinkSync(currentLink);
8272
- if (previousTarget !== join4("versions", options.currentVersion)) {
8588
+ if (previousTarget !== join5("versions", options.currentVersion)) {
8273
8589
  throw new Error("agent update current release does not match the running version");
8274
8590
  }
8275
- if (!existsSync10(join4(root, previousTarget))) {
8591
+ if (!existsSync10(join5(root, previousTarget))) {
8276
8592
  throw new Error("agent update rollback release is missing");
8277
8593
  }
8278
8594
  return {
@@ -8280,51 +8596,51 @@ async function stageAgentRelease(releaseInput, options) {
8280
8596
  fromVersion: options.currentVersion,
8281
8597
  directory: finalDirectory,
8282
8598
  previousTarget,
8283
- nextTarget: join4("versions", release.version),
8599
+ nextTarget: join5("versions", release.version),
8284
8600
  currentLink
8285
8601
  };
8286
8602
  } finally {
8287
- rmSync2(stage2, { recursive: true, force: true });
8603
+ rmSync3(stage2, { recursive: true, force: true });
8288
8604
  }
8289
8605
  }
8290
8606
  function selectAgentRelease(staged) {
8291
- const next = join4(dirname4(staged.currentLink), `.current.${randomUUID2()}.next`);
8607
+ const next = join5(dirname4(staged.currentLink), `.current.${randomUUID2()}.next`);
8292
8608
  try {
8293
8609
  symlinkSync2(staged.nextTarget, next);
8294
8610
  renameSync4(next, staged.currentLink);
8295
8611
  syncPath(dirname4(staged.currentLink));
8296
8612
  } finally {
8297
- rmSync2(next, { force: true });
8613
+ rmSync3(next, { force: true });
8298
8614
  }
8299
8615
  }
8300
8616
  function restoreAgentRelease(staged) {
8301
- const next = join4(dirname4(staged.currentLink), `.current.${randomUUID2()}.rollback`);
8617
+ const next = join5(dirname4(staged.currentLink), `.current.${randomUUID2()}.rollback`);
8302
8618
  try {
8303
8619
  symlinkSync2(staged.previousTarget, next);
8304
8620
  renameSync4(next, staged.currentLink);
8305
8621
  syncPath(dirname4(staged.currentLink));
8306
8622
  } finally {
8307
- rmSync2(next, { force: true });
8623
+ rmSync3(next, { force: true });
8308
8624
  }
8309
8625
  }
8310
8626
 
8311
8627
  // src/agent-update-helper.ts
8312
8628
  import { randomUUID as randomUUID3 } from "crypto";
8313
8629
  import {
8314
- chmodSync as chmodSync10,
8630
+ chmodSync as chmodSync11,
8315
8631
  closeSync as closeSync2,
8316
8632
  existsSync as existsSync11,
8317
8633
  fsyncSync as fsyncSync2,
8318
8634
  mkdirSync as mkdirSync7,
8319
8635
  openSync as openSync2,
8320
- readFileSync as readFileSync7,
8636
+ readFileSync as readFileSync8,
8321
8637
  renameSync as renameSync5,
8322
- rmSync as rmSync3,
8638
+ rmSync as rmSync4,
8323
8639
  unlinkSync as unlinkSync9,
8324
- writeFileSync as writeFileSync7
8640
+ writeFileSync as writeFileSync8
8325
8641
  } from "fs";
8326
8642
  import { connect as connect5, createServer as createServer6 } from "net";
8327
- import { dirname as dirname5, join as join5, resolve as resolve3 } from "path";
8643
+ import { dirname as dirname5, join as join6, resolve as resolve3 } from "path";
8328
8644
  var AGENT_UPDATE_GROUP = "forgezero-update";
8329
8645
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
8330
8646
  var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
@@ -8408,12 +8724,12 @@ function validateJournal(value, root) {
8408
8724
  throw new Error("Agent update failure count is invalid");
8409
8725
  }
8410
8726
  const releaseRoot = resolve3(root);
8411
- if (journal.currentLink !== join5(releaseRoot, "current"))
8727
+ if (journal.currentLink !== join6(releaseRoot, "current"))
8412
8728
  throw new Error("Agent update current link is invalid");
8413
- if (journal.previousTarget !== join5("versions", receipt.fromVersion)) {
8729
+ if (journal.previousTarget !== join6("versions", receipt.fromVersion)) {
8414
8730
  throw new Error("Agent update rollback target is invalid");
8415
8731
  }
8416
- if (journal.nextTarget !== join5("versions", receipt.targetVersion)) {
8732
+ if (journal.nextTarget !== join6("versions", receipt.targetVersion)) {
8417
8733
  throw new Error("Agent update next target is invalid");
8418
8734
  }
8419
8735
  return journal;
@@ -8421,7 +8737,7 @@ function validateJournal(value, root) {
8421
8737
  function readJournal(path, root) {
8422
8738
  if (!existsSync11(path))
8423
8739
  return;
8424
- return validateJournal(JSON.parse(readFileSync7(path, "utf8")), root);
8740
+ return validateJournal(JSON.parse(readFileSync8(path, "utf8")), root);
8425
8741
  }
8426
8742
  function writeAtomic(path, value, mode) {
8427
8743
  mkdirSync7(dirname5(path), { recursive: true, mode: 493 });
@@ -8429,7 +8745,7 @@ function writeAtomic(path, value, mode) {
8429
8745
  let file;
8430
8746
  try {
8431
8747
  file = openSync2(next, "wx", mode);
8432
- writeFileSync7(file, `${JSON.stringify(value)}
8748
+ writeFileSync8(file, `${JSON.stringify(value)}
8433
8749
  `);
8434
8750
  fsyncSync2(file);
8435
8751
  closeSync2(file);
@@ -8444,7 +8760,7 @@ function writeAtomic(path, value, mode) {
8444
8760
  } finally {
8445
8761
  if (file !== undefined)
8446
8762
  closeSync2(file);
8447
- rmSync3(next, { force: true });
8763
+ rmSync4(next, { force: true });
8448
8764
  }
8449
8765
  }
8450
8766
  var publicReceipt = (journal) => {
@@ -8479,7 +8795,7 @@ function readAgentUpdateReceipt(path = AGENT_UPDATE_RECEIPT) {
8479
8795
  try {
8480
8796
  if (!existsSync11(path))
8481
8797
  return;
8482
- return validateReceipt(JSON.parse(readFileSync7(path, "utf8")));
8798
+ return validateReceipt(JSON.parse(readFileSync8(path, "utf8")));
8483
8799
  } catch {
8484
8800
  return;
8485
8801
  }
@@ -8503,21 +8819,21 @@ var retryAfter = (now, failures) => now + Math.min(UPDATE_RETRY_MAX_MS, UPDATE_R
8503
8819
  var stagedFromJournal = (journal, root) => ({
8504
8820
  version: journal.targetVersion,
8505
8821
  fromVersion: journal.fromVersion,
8506
- directory: join5(resolve3(root), journal.nextTarget),
8822
+ directory: join6(resolve3(root), journal.nextTarget),
8507
8823
  previousTarget: journal.previousTarget,
8508
8824
  nextTarget: journal.nextTarget,
8509
8825
  currentLink: journal.currentLink
8510
8826
  });
8511
- var restartAgent = async (target, run) => {
8512
- const helpers = target === "compute" ? COMPUTE_HELPER_UNITS : ["forgezero-metal-helper.service"];
8827
+ var restartAgent = async (target2, run) => {
8828
+ const helpers = target2 === "compute" ? COMPUTE_HELPER_UNITS : ["forgezero-metal-helper.service"];
8513
8829
  for (const unit of helpers)
8514
8830
  await run({ command: "/usr/bin/systemctl", args: ["try-restart", unit] });
8515
- const service = target === "compute" ? "forgezero-agent.service" : "forgezero-metal-agent.service";
8831
+ const service = target2 === "compute" ? "forgezero-agent.service" : "forgezero-metal-agent.service";
8516
8832
  if (!await runOk(run, "/usr/bin/systemctl", ["restart", service])) {
8517
8833
  throw new Error(`systemd could not restart ${service}`);
8518
8834
  }
8519
8835
  };
8520
- var targetProbe = (target, run) => target === "compute" ? () => probeAgentSocket() : async () => await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
8836
+ var targetProbe = (target2, run) => target2 === "compute" ? () => probeAgentSocket() : async () => await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
8521
8837
  function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
8522
8838
  return new Promise((resolve4) => {
8523
8839
  const socket = connect5(socketPath);
@@ -8552,8 +8868,8 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
8552
8868
  }
8553
8869
  async function activateAgentRelease(staged, options = {}) {
8554
8870
  const run = options.run ?? runCommand;
8555
- const target = options.target ?? "compute";
8556
- const probe = options.probe ?? targetProbe(target, run);
8871
+ const target2 = options.target ?? "compute";
8872
+ const probe = options.probe ?? targetProbe(target2, run);
8557
8873
  const now = options.now ?? Date.now;
8558
8874
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
8559
8875
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
@@ -8566,7 +8882,7 @@ async function activateAgentRelease(staged, options = {}) {
8566
8882
  let journal = {
8567
8883
  schemaVersion: 1,
8568
8884
  attemptId,
8569
- target,
8885
+ target: target2,
8570
8886
  fromVersion: staged.fromVersion,
8571
8887
  targetVersion: staged.version,
8572
8888
  outcome: "activating",
@@ -8582,7 +8898,7 @@ async function activateAgentRelease(staged, options = {}) {
8582
8898
  try {
8583
8899
  selectionAttempted = true;
8584
8900
  selectAgentRelease(staged);
8585
- await restartAgent(target, run);
8901
+ await restartAgent(target2, run);
8586
8902
  if (!await probe())
8587
8903
  throw new Error("the replacement Agent did not answer its retained Vault socket");
8588
8904
  journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
@@ -8600,7 +8916,7 @@ async function activateAgentRelease(staged, options = {}) {
8600
8916
  try {
8601
8917
  restoreAgentRelease(staged);
8602
8918
  restored = true;
8603
- await restartAgent(target, run);
8919
+ await restartAgent(target2, run);
8604
8920
  rollbackHealthy = await probe();
8605
8921
  } catch {
8606
8922
  rollbackHealthy = false;
@@ -8633,7 +8949,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
8633
8949
  return publicReceipt(journal);
8634
8950
  }
8635
8951
  const staged = stagedFromJournal(journal, root);
8636
- if (!existsSync11(join5(root, journal.previousTarget))) {
8952
+ if (!existsSync11(join6(root, journal.previousTarget))) {
8637
8953
  throw new Error("Agent update rollback release is missing");
8638
8954
  }
8639
8955
  const run = options.run ?? runCommand;
@@ -8670,7 +8986,7 @@ function startAgentUpdateHelper(options = {}) {
8670
8986
  const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
8671
8987
  const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
8672
8988
  const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
8673
- const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, { target, attemptId, journalPath, receiptPath, now: options.now }));
8989
+ const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, { target: target2, attemptId, journalPath, receiptPath, now: options.now }));
8674
8990
  let busy = true;
8675
8991
  let blocked;
8676
8992
  (options.recover ?? (() => recoverInterruptedAgentUpdate({
@@ -8744,7 +9060,7 @@ function startAgentUpdateHelper(options = {}) {
8744
9060
  });
8745
9061
  socket.on("error", () => socket.destroy());
8746
9062
  });
8747
- server.listen(socketPath, () => chmodSync10(socketPath, 432));
9063
+ server.listen(socketPath, () => chmodSync11(socketPath, 432));
8748
9064
  return server;
8749
9065
  }
8750
9066
  function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
@@ -8774,10 +9090,10 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
8774
9090
  }
8775
9091
 
8776
9092
  // src/agent-heartbeat.ts
8777
- import { readFileSync as readFileSync8 } from "fs";
9093
+ import { readFileSync as readFileSync9 } from "fs";
8778
9094
 
8779
9095
  // src/version.ts
8780
- var VERSION3 = "0.1.34";
9096
+ var VERSION3 = "0.1.36";
8781
9097
 
8782
9098
  // src/agent-heartbeat.ts
8783
9099
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
@@ -8785,7 +9101,7 @@ var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.st
8785
9101
 
8786
9102
  class AgentUpdateRefusedError extends Error {
8787
9103
  }
8788
- function observeAgentHost(version = VERSION3, mode = "enrolled", osRelease = readFileSync8("/etc/os-release", "utf8"), architecture = process.arch) {
9104
+ function observeAgentHost(version = VERSION3, mode = "enrolled", osRelease = readFileSync9("/etc/os-release", "utf8"), architecture = process.arch) {
8789
9105
  const values = Object.fromEntries(osRelease.split(`
8790
9106
  `).flatMap((line) => {
8791
9107
  const separator = line.indexOf("=");
@@ -8895,7 +9211,7 @@ function startAgentHeartbeat(options) {
8895
9211
  }
8896
9212
 
8897
9213
  // src/software-helper.ts
8898
- import { chmodSync as chmodSync11, existsSync as existsSync12, mkdirSync as mkdirSync8, unlinkSync as unlinkSync10 } from "fs";
9214
+ import { chmodSync as chmodSync12, existsSync as existsSync12, mkdirSync as mkdirSync8, unlinkSync as unlinkSync10 } from "fs";
8899
9215
  import { connect as connect6, createServer as createServer7 } from "net";
8900
9216
  import { dirname as dirname6 } from "path";
8901
9217
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
@@ -8967,7 +9283,7 @@ function startSoftwareHelper(options = {}) {
8967
9283
  });
8968
9284
  socket.on("error", () => socket.destroy());
8969
9285
  });
8970
- server.listen(socketPath, () => chmodSync11(socketPath, 432));
9286
+ server.listen(socketPath, () => chmodSync12(socketPath, 432));
8971
9287
  return server;
8972
9288
  }
8973
9289
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
@@ -9530,7 +9846,7 @@ function createAgentTelemetry(input, options = {}) {
9530
9846
  logs.push(logRecord("telemetry.queue_overflow"));
9531
9847
  }
9532
9848
  };
9533
- const post3 = async (path, body, sentItems) => {
9849
+ const post4 = async (path, body, sentItems) => {
9534
9850
  for (let attempt = 0;attempt < AGENT_OTLP_MAX_ATTEMPTS; attempt += 1) {
9535
9851
  try {
9536
9852
  const response = await send(`${config.endpoint}${path}`, {
@@ -9581,9 +9897,9 @@ function createAgentTelemetry(input, options = {}) {
9581
9897
  addExportDiscards(records.length);
9582
9898
  return;
9583
9899
  }
9584
- const target = kind === "traces" ? spans : logs;
9900
+ const target2 = kind === "traces" ? spans : logs;
9585
9901
  const maximum = kind === "traces" ? AGENT_TELEMETRY_MAX_SPANS : AGENT_TELEMETRY_MAX_LOGS;
9586
- const combined = [...records, ...target];
9902
+ const combined = [...records, ...target2];
9587
9903
  const overflow = Math.max(0, combined.length - maximum);
9588
9904
  const retained = combined.slice(-maximum);
9589
9905
  if (kind === "traces")
@@ -9736,7 +10052,7 @@ function createAgentTelemetry(input, options = {}) {
9736
10052
  exports.push({
9737
10053
  kind: "traces",
9738
10054
  itemCount: currentSpans.length,
9739
- promise: post3("/v1/traces", {
10055
+ promise: post4("/v1/traces", {
9740
10056
  resourceSpans: [{ resource, scopeSpans: [{
9741
10057
  scope: { name: "forgezero.agent" },
9742
10058
  spans: currentSpans
@@ -9747,7 +10063,7 @@ function createAgentTelemetry(input, options = {}) {
9747
10063
  exports.push({
9748
10064
  kind: "metrics",
9749
10065
  itemCount: metricPointCount,
9750
- promise: post3("/v1/metrics", {
10066
+ promise: post4("/v1/metrics", {
9751
10067
  resourceMetrics: [{ resource, scopeMetrics: [{
9752
10068
  scope: { name: "forgezero.agent" },
9753
10069
  metrics: metricInstruments
@@ -9758,7 +10074,7 @@ function createAgentTelemetry(input, options = {}) {
9758
10074
  exports.push({
9759
10075
  kind: "logs",
9760
10076
  itemCount: currentLogs.length,
9761
- promise: post3("/v1/logs", {
10077
+ promise: post4("/v1/logs", {
9762
10078
  resourceLogs: [{ resource, scopeLogs: [{
9763
10079
  scope: { name: "forgezero.agent" },
9764
10080
  logRecords: currentLogs
@@ -9881,7 +10197,7 @@ class AgentTelemetryRuntime {
9881
10197
  // src/index.ts
9882
10198
  function loadOrCreateSeed(path) {
9883
10199
  if (existsSync13(path)) {
9884
- const seed2 = new Uint8Array(Buffer.from(readFileSync9(path, "utf8").trim(), "base64url"));
10200
+ const seed2 = new Uint8Array(Buffer.from(readFileSync10(path, "utf8").trim(), "base64url"));
9885
10201
  if (seed2.length < 32) {
9886
10202
  throw new Error(`agent: the seed at ${path} is too short to derive a key from.`);
9887
10203
  }
@@ -9889,8 +10205,8 @@ function loadOrCreateSeed(path) {
9889
10205
  }
9890
10206
  mkdirSync9(dirname7(path), { recursive: true });
9891
10207
  const seed = new Uint8Array(randomBytes5(32));
9892
- writeFileSync8(path, Buffer.from(seed).toString("base64url"), { mode: 384 });
9893
- chmodSync12(path, 384);
10208
+ writeFileSync9(path, Buffer.from(seed).toString("base64url"), { mode: 384 });
10209
+ chmodSync13(path, 384);
9894
10210
  return seed;
9895
10211
  }
9896
10212
  var DEFAULT_SOCKET_PATH = DEFAULT_SOCKET;
@@ -9921,7 +10237,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
9921
10237
  const path = `${directory}/${name}`;
9922
10238
  if (!existsSync13(path))
9923
10239
  throw new Error(`agent: the systemd credential ${name} is missing at ${path}.`);
9924
- const seed = new Uint8Array(Buffer.from(readFileSync9(path, "utf8").trim(), "base64url"));
10240
+ const seed = new Uint8Array(Buffer.from(readFileSync10(path, "utf8").trim(), "base64url"));
9925
10241
  if (seed.length < 32)
9926
10242
  throw new Error(`agent: the systemd credential ${name} is too short to derive a key from.`);
9927
10243
  return seed;
@@ -9931,7 +10247,7 @@ function loadTextCredential(name, directory = process.env.CREDENTIALS_DIRECTORY)
9931
10247
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the credential.");
9932
10248
  if (!/^[A-Za-z0-9_.-]+$/.test(name))
9933
10249
  throw new Error("agent: invalid systemd credential name.");
9934
- const value = readFileSync9(`${directory}/${name}`, "utf8").trim();
10250
+ const value = readFileSync10(`${directory}/${name}`, "utf8").trim();
9935
10251
  if (!value)
9936
10252
  throw new Error(`agent: systemd credential ${name} is empty.`);
9937
10253
  return value;
@@ -10054,7 +10370,7 @@ if (import.meta.main) {
10054
10370
  keys: keys2,
10055
10371
  label: process.env.FZ_NODE_LABEL,
10056
10372
  edgeHostname: process.env.FZ_NODE_HOSTNAME,
10057
- gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync9(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
10373
+ gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync10(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
10058
10374
  privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
10059
10375
  });
10060
10376
  console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
@@ -10087,7 +10403,7 @@ if (import.meta.main) {
10087
10403
  const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
10088
10404
  if (!profilePath)
10089
10405
  throw new Error("metal-helper requires --profile=/absolute/path.json");
10090
- const profile2 = JSON.parse(readFileSync9(profilePath, "utf8"));
10406
+ const profile2 = JSON.parse(readFileSync10(profilePath, "utf8"));
10091
10407
  const helper = startMetalHelper({
10092
10408
  profile: profile2,
10093
10409
  socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
@@ -10205,7 +10521,7 @@ if (import.meta.main) {
10205
10521
  const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
10206
10522
  if (!profilePath)
10207
10523
  throw new Error("metal-isolation requires --profile=/absolute/path.json");
10208
- const profile2 = JSON.parse(readFileSync9(profilePath, "utf8"));
10524
+ const profile2 = JSON.parse(readFileSync10(profilePath, "utf8"));
10209
10525
  await applyMetalIsolation(profile2);
10210
10526
  console.log("[metal-isolation] host and guest cgroup boundaries active");
10211
10527
  process.exit(0);
@@ -10250,7 +10566,7 @@ if (import.meta.main) {
10250
10566
  if (claimArg !== "-" && !claimArg.startsWith("/")) {
10251
10567
  throw new Error("metal-apply claim path must be absolute");
10252
10568
  }
10253
- const raw = readFileSync9(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
10569
+ const raw = readFileSync10(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
10254
10570
  if (Buffer.byteLength(raw) > 32 * 1024)
10255
10571
  throw new Error("metal-apply claim exceeds 32 KiB");
10256
10572
  const claim = JSON.parse(raw);
@@ -10396,7 +10712,7 @@ if (import.meta.main) {
10396
10712
  keys,
10397
10713
  label: process.env.FZ_NODE_LABEL,
10398
10714
  edgeHostname: process.env.FZ_NODE_HOSTNAME,
10399
- gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync9(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
10715
+ gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync10(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
10400
10716
  privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
10401
10717
  });
10402
10718
  console.log(`[agent] enrolled ${binding.computeReference} in project ${binding.projectKey}/${binding.environmentKey}`);
@@ -10456,6 +10772,23 @@ if (import.meta.main) {
10456
10772
  }) : undefined;
10457
10773
  if (migrationPull)
10458
10774
  console.log("[agent] PQ-authenticated outbound lifecycle claims enabled");
10775
+ const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
10776
+ const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
10777
+ const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
10778
+ if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync13(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
10779
+ throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
10780
+ }
10781
+ const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
10782
+ apiUrl: nodeApiUrl,
10783
+ platformApiUrl: process.env.FZ_API,
10784
+ nodeKey,
10785
+ keys,
10786
+ sshKeyPath: bootstrapKeyPath,
10787
+ targetTelemetryEndpoint: process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT,
10788
+ onEvent: (event) => console.log(`[agent] bootstrap ${event}`)
10789
+ }) : undefined;
10790
+ if (bootstrapPull)
10791
+ console.log("[agent] PQ-authenticated delegated SSH bootstrap claims enabled");
10459
10792
  const systemdDeploymentSecrets = createSystemdDeploymentSecrets(process.env.FZ_DEPLOY_SYSTEMD_SECRETS);
10460
10793
  const deploymentSecrets = secretCache ? {
10461
10794
  async get(name) {
@@ -10493,10 +10826,10 @@ if (import.meta.main) {
10493
10826
  return [name, process.env[name]];
10494
10827
  })),
10495
10828
  gitCredentialPath: process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/git-deploy-key` : undefined,
10496
- knownHostsPath: source.knownHosts ? join6(root, "cache", `known-hosts-${key}`) : undefined,
10829
+ knownHostsPath: source.knownHosts ? join7(root, "cache", `known-hosts-${key}`) : undefined,
10497
10830
  knownHostsContent: source.knownHosts,
10498
10831
  cache: deploymentSecrets,
10499
- capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ?? join6(root, "cache", "capacity"),
10832
+ capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ?? join7(root, "cache", "capacity"),
10500
10833
  ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
10501
10834
  projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
10502
10835
  });
@@ -10545,7 +10878,8 @@ if (import.meta.main) {
10545
10878
  try {
10546
10879
  await Promise.all([
10547
10880
  pull?.stop() ?? Promise.resolve(),
10548
- migrationPull?.stop() ?? Promise.resolve()
10881
+ migrationPull?.stop() ?? Promise.resolve(),
10882
+ bootstrapPull?.stop() ?? Promise.resolve()
10549
10883
  ]);
10550
10884
  const reports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000)))));
10551
10885
  if (reports.some(({ timedOut }) => timedOut)) {
@@ -10583,9 +10917,10 @@ if (import.meta.main) {
10583
10917
  const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
10584
10918
  const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
10585
10919
  const migrationDrain = migrationPull?.stop() ?? Promise.resolve();
10920
+ const bootstrapDrain = bootstrapPull?.stop() ?? Promise.resolve();
10586
10921
  const heartbeatDrain = heartbeat?.stop() ?? Promise.resolve();
10587
10922
  const pullDrained = pull ? await settleWithin(pullDrain, remaining()) : true;
10588
- const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain, migrationDrain, heartbeatDrain]), remaining());
10923
+ const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain, migrationDrain, bootstrapDrain, heartbeatDrain]), remaining());
10589
10924
  const managerReports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(remaining())));
10590
10925
  const socketClosed = await closeServerWithin(server, remaining());
10591
10926
  telemetry.event("agent.stopped");
@@ -10616,7 +10951,10 @@ if (import.meta.main) {
10616
10951
  return;
10617
10952
  updatePrepared = true;
10618
10953
  try {
10619
- await migrationPull?.stop();
10954
+ await Promise.all([
10955
+ migrationPull?.stop() ?? Promise.resolve(),
10956
+ bootstrapPull?.stop() ?? Promise.resolve()
10957
+ ]);
10620
10958
  telemetry.event("agent.update_prepared");
10621
10959
  } catch (cause) {
10622
10960
  updatePrepared = false;
@@ -10648,6 +10986,7 @@ if (import.meta.main) {
10648
10986
  vaultSync?.stop() ?? Promise.resolve(),
10649
10987
  attestationLoop?.stop() ?? Promise.resolve(),
10650
10988
  migrationPull?.stop() ?? Promise.resolve(),
10989
+ bootstrapPull?.stop() ?? Promise.resolve(),
10651
10990
  heartbeat?.stop() ?? Promise.resolve()
10652
10991
  ]), remaining());
10653
10992
  const socketClosed = await closeServerWithin(server, remaining());
@@ -10670,6 +11009,7 @@ export {
10670
11009
  systemdListenFd,
10671
11010
  systemdAgentEgressDirectives,
10672
11011
  superviseAgentEgressPolicy,
11012
+ startSshBootstrapPull,
10673
11013
  startSoftwareHelper,
10674
11014
  startProvisioningPull,
10675
11015
  startNodeVaultSync,
@@ -10699,6 +11039,7 @@ export {
10699
11039
  removeMetalGuest,
10700
11040
  recoverInterruptedAgentUpdate,
10701
11041
  readAgentUpdateReceipt,
11042
+ pullSshBootstrapOnce,
10702
11043
  pullProvisioningOnce,
10703
11044
  pullMigrationOnce,
10704
11045
  pullDeploymentOnce,
@@ -10722,6 +11063,7 @@ export {
10722
11063
  handleRequest,
10723
11064
  handleApplicationRequest,
10724
11065
  guestNameFor,
11066
+ executeSshBootstrap,
10725
11067
  executeLifecycleAction,
10726
11068
  ensureSoftwareRequirements,
10727
11069
  enrolGuestIdentity,