@forgezero/agent 0.1.65 → 0.1.67

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
@@ -4970,7 +4970,30 @@ var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cb
4970
4970
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
4971
4971
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
4972
4972
  var OS_CATALOG = [
4973
- { id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
4973
+ {
4974
+ id: "ubuntu",
4975
+ version: "26.04",
4976
+ architecture: "x64",
4977
+ status: "active",
4978
+ imageKey: "ubuntu-resolute-20260731",
4979
+ imageLabel: "Ubuntu 26.04 LTS Resolute",
4980
+ imageVersion: "2026-07-31",
4981
+ imageUrl: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
4982
+ imageSha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05",
4983
+ confidentialModes: ["sev-snp"]
4984
+ },
4985
+ {
4986
+ id: "ubuntu",
4987
+ version: "24.04",
4988
+ architecture: "x64",
4989
+ status: "active",
4990
+ imageKey: "ubuntu-noble-20260705",
4991
+ imageLabel: "Ubuntu 24.04 LTS Noble",
4992
+ imageVersion: "2026-07-05",
4993
+ imageUrl: "https://cloud-images.ubuntu.com/releases/noble/release-20260705/ubuntu-24.04-server-cloudimg-amd64.img",
4994
+ imageSha256: "ffe6203da54deeb6db5d2a98a83f9ec8e55f149d3f7ba622e1abe5fa966ee3d6",
4995
+ confidentialModes: []
4996
+ }
4974
4997
  ];
4975
4998
  var SOFTWARE_CATALOG = [
4976
4999
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -4981,7 +5004,12 @@ var SOFTWARE_CATALOG = [
4981
5004
  { id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4982
5005
  { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4983
5006
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4984
- { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
5007
+ { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
5008
+ { id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
5009
+ { id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
5010
+ { id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
5011
+ { id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
5012
+ { id: "git", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4985
5013
  ];
4986
5014
  var UBUNTU_2604_X64 = [
4987
5015
  { requirement: { id: "bun", version: "1.3.14" } },
@@ -4994,6 +5022,15 @@ var UBUNTU_2604_X64 = [
4994
5022
  { requirement: { id: "openssh-client", version: "ubuntu-26.04" } },
4995
5023
  { requirement: { id: "git", version: "ubuntu-26.04" } }
4996
5024
  ];
5025
+ var UBUNTU_2404_X64 = [
5026
+ { requirement: { id: "bun", version: "1.3.14" } },
5027
+ { requirement: { id: "docker", version: "ubuntu-24.04" } },
5028
+ { requirement: { id: "nginx", version: "ubuntu-24.04" } },
5029
+ { requirement: { id: "ufw", version: "ubuntu-24.04" } },
5030
+ { requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
5031
+ { requirement: { id: "git", version: "ubuntu-24.04" } }
5032
+ ];
5033
+ var strategiesFor = (observation) => observation.os.id === "ubuntu" && observation.architecture === "x64" ? observation.os.versionId === "26.04" ? UBUNTU_2604_X64 : observation.os.versionId === "24.04" ? UBUNTU_2404_X64 : undefined : undefined;
4997
5034
  var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
4998
5035
  var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
4999
5036
  var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
@@ -5040,7 +5077,7 @@ var aptInstall = async (name) => {
5040
5077
  };
5041
5078
  async function executeSoftwareOperation(operation) {
5042
5079
  const { software, version } = operation;
5043
- if (!UBUNTU_2604_X64.some(({ requirement }) => requirement.id === software && requirement.version === version)) {
5080
+ if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
5044
5081
  return { exitCode: 2, output: "unsupported software operation" };
5045
5082
  }
5046
5083
  if (operation.kind === "check") {
@@ -5210,7 +5247,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
5210
5247
  }
5211
5248
  const results = [];
5212
5249
  for (const requirement of requirements) {
5213
- const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
5250
+ const strategy = strategiesFor(observation)?.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
5214
5251
  if (!strategy)
5215
5252
  throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
5216
5253
  const before = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
@@ -6015,13 +6052,13 @@ function validateBuiltInProvider(requirement2, where) {
6015
6052
  return;
6016
6053
  const expected = {
6017
6054
  "forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
6018
- "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
6019
- "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
6055
+ "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
6056
+ "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
6020
6057
  "forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
6021
6058
  "forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
6022
6059
  "forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
6023
- "forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
6024
- "forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
6060
+ "forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
6061
+ "forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "mode"] },
6025
6062
  "forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
6026
6063
  };
6027
6064
  const contract = expected[requirement2.provider];
@@ -7148,7 +7185,7 @@ async function sha256File(path2) {
7148
7185
  });
7149
7186
  return hash.digest("hex");
7150
7187
  }
7151
- var branchName = (value) => {
7188
+ var bootstrapBundleBranch = (value) => {
7152
7189
  if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
7153
7190
  throw new Error("bootstrap bundle branch is malformed");
7154
7191
  }
@@ -7165,7 +7202,7 @@ function parseBootstrapBundleManifest(value) {
7165
7202
  if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
7166
7203
  throw new Error("bootstrap bundle manifest format is unsupported");
7167
7204
  }
7168
- branchName(source.branch);
7205
+ bootstrapBundleBranch(source.branch);
7169
7206
  if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
7170
7207
  throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
7171
7208
  }
@@ -8550,23 +8587,8 @@ import { tmpdir as tmpdir2 } from "os";
8550
8587
  import { join as join3 } from "path";
8551
8588
  import { fileURLToPath } from "url";
8552
8589
 
8553
- // src/ubuntu.ts
8554
- var SUPPORTED_GUEST_IMAGE = Object.freeze({
8555
- key: "ubuntu-resolute-20260731",
8556
- family: "ubuntu-26.04",
8557
- version: "2026-07-31",
8558
- label: "Ubuntu 26.04 LTS Resolute",
8559
- url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
8560
- sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
8561
- });
8562
- function assertSupportedGuestImage(imageKey) {
8563
- if (imageKey !== SUPPORTED_GUEST_IMAGE.key) {
8564
- throw new Error(`unsupported guest image ${imageKey}; ForgeZero currently supports only ${SUPPORTED_GUEST_IMAGE.key}`);
8565
- }
8566
- }
8567
-
8568
8590
  // src/version.ts
8569
- var VERSION2 = "0.1.65";
8591
+ var VERSION2 = "0.1.67";
8570
8592
 
8571
8593
  // src/ssh-bootstrap.ts
8572
8594
  class SshBootstrapError extends Error {
@@ -8603,10 +8625,12 @@ var checked = async (exec, argv2, code, options) => {
8603
8625
  var validateClaim = (claim) => {
8604
8626
  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))
8605
8627
  throw new SshBootstrapError("CLAIM_INVALID");
8606
- const { host, port, user, hostKeySha256, nodeHostname } = claim.target;
8628
+ const { host, port, user, hostKeySha256, nodeHostname, operatingSystem } = claim.target;
8607
8629
  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(".")) {
8608
8630
  throw new SshBootstrapError("CLAIM_INVALID");
8609
8631
  }
8632
+ if (operatingSystem?.id !== "ubuntu" || !["24.04", "26.04"].includes(operatingSystem.version) || operatingSystem.architecture !== "x64")
8633
+ throw new SshBootstrapError("CLAIM_INVALID");
8610
8634
  if (!claim.target.credential || claim.target.credential.source !== "runner" && claim.target.credential.source !== "vault" || claim.target.credential.source === "vault" && (typeof claim.target.credential.privateKey !== "string" || claim.target.credential.privateKey.length > 32768 || !claim.target.credential.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") || !claim.target.credential.privateKey.trimEnd().endsWith("-----END OPENSSH PRIVATE KEY-----"))) {
8611
8635
  throw new SshBootstrapError("CLAIM_INVALID");
8612
8636
  }
@@ -8736,6 +8760,7 @@ async function executeSshBootstrap(claim, options) {
8736
8760
  throw new SshBootstrapError("REMOTE_PATH_INVALID");
8737
8761
  const config = {
8738
8762
  kind: "enrolled-compute",
8763
+ operatingSystem: claim.target.operatingSystem,
8739
8764
  apiUrl: api.origin,
8740
8765
  realm: claim.realm,
8741
8766
  nodeHostname: claim.target.nodeHostname,
@@ -9071,10 +9096,10 @@ async function executeMetalAdmission(claim, options) {
9071
9096
  unitDir: "/etc/systemd/system",
9072
9097
  apiUrl: api.origin,
9073
9098
  agentTelemetryEndpoint: telemetry.origin,
9074
- images: { [SUPPORTED_GUEST_IMAGE.key]: {
9075
- path: `/var/lib/forgezero/images/${SUPPORTED_GUEST_IMAGE.key}.img`,
9076
- sha256: SUPPORTED_GUEST_IMAGE.sha256
9077
- } },
9099
+ images: Object.fromEntries(OS_CATALOG.filter(({ status }) => status === "active").map((image) => [
9100
+ image.imageKey,
9101
+ { path: `/var/lib/forgezero/images/${image.imageKey}.img`, sha256: image.imageSha256 }
9102
+ ])),
9078
9103
  bunVersion: PINNED_BUN_VERSION,
9079
9104
  bunReleaseSha256: BUN_RELEASE_SHA256,
9080
9105
  agentVersion: VERSION2
@@ -9384,6 +9409,36 @@ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
9384
9409
  ];
9385
9410
  }
9386
9411
 
9412
+ // src/ubuntu.ts
9413
+ var current = OS_CATALOG[0];
9414
+ var SUPPORTED_GUEST_IMAGE = Object.freeze({
9415
+ key: current.imageKey,
9416
+ family: `${current.id}-${current.version}`,
9417
+ version: current.imageVersion,
9418
+ label: current.imageLabel,
9419
+ url: current.imageUrl,
9420
+ sha256: current.imageSha256
9421
+ });
9422
+ var SUPPORTED_GUEST_IMAGES = Object.freeze(OS_CATALOG.filter(({ status }) => status === "active").map(({ id: id2, imageKey, imageLabel, imageVersion, version, confidentialModes }) => ({
9423
+ key: imageKey,
9424
+ label: imageLabel,
9425
+ family: `${id2}-${version}`,
9426
+ version: imageVersion,
9427
+ confidential: confidentialModes.some((mode) => mode === "sev-snp")
9428
+ })));
9429
+ function supportedGuestImage(imageKey) {
9430
+ return SUPPORTED_GUEST_IMAGES.find(({ key }) => key === imageKey);
9431
+ }
9432
+ function assertSupportedGuestImage(imageKey, confidential = false) {
9433
+ const selected = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
9434
+ if (!selected) {
9435
+ throw new Error(`unsupported guest image ${imageKey}; supported images: ${SUPPORTED_GUEST_IMAGES.map(({ key }) => key).join(", ")}`);
9436
+ }
9437
+ if (confidential && !selected.confidentialModes.some((mode) => mode === "sev-snp")) {
9438
+ throw new Error(`guest image ${imageKey} is not approved for SEV-SNP`);
9439
+ }
9440
+ }
9441
+
9387
9442
  // src/metal-provision.ts
9388
9443
  var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
9389
9444
  var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
@@ -9443,9 +9498,16 @@ function validateMetalProfile(profile) {
9443
9498
  if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP2(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
9444
9499
  throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
9445
9500
  const imageKeys = Object.keys(profile.images);
9446
- if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
9447
- throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
9501
+ if (imageKeys.length === 0)
9502
+ throw new MetalProvisionError("metal profile requires at least one pinned guest image");
9503
+ for (const imageKey of imageKeys) {
9504
+ const catalog = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
9505
+ if (!catalog || profile.images[imageKey]?.sha256 !== catalog.imageSha256) {
9506
+ throw new MetalProvisionError(`metal profile image ${imageKey} does not match the pinned OS catalog`);
9507
+ }
9448
9508
  }
9509
+ if (profile.confidential && !imageKeys.some((key) => OS_CATALOG.find((entry) => entry.imageKey === key)?.confidentialModes.some((mode) => mode === "sev-snp")))
9510
+ throw new MetalProvisionError("confidential metal profile requires a pinned SEV-SNP guest image");
9449
9511
  if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
9450
9512
  throw new MetalProvisionError("at least one exclusive CPU pool is required");
9451
9513
  }
@@ -9664,7 +9726,7 @@ var checked2 = async (exec, argv2) => {
9664
9726
  };
9665
9727
  async function provisionMetalGuest(profile, claim, exec) {
9666
9728
  validateMetalProfile(profile);
9667
- assertSupportedGuestImage(claim.spec.imageKey);
9729
+ assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
9668
9730
  if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
9669
9731
  throw new MetalProvisionError("invalid compute claim");
9670
9732
  const image = profile.images[claim.spec.imageKey];
@@ -11544,11 +11606,11 @@ async function heartbeatAgentOnce(options) {
11544
11606
  await options.prepareUpdate?.(release);
11545
11607
  prepared = true;
11546
11608
  const apply = async () => {
11547
- const applied = await (options.applyUpdate ?? ((next, current, attemptId) => requestAgentUpdate({
11609
+ const applied = await (options.applyUpdate ?? ((next, current2, attemptId) => requestAgentUpdate({
11548
11610
  op: "apply",
11549
11611
  target: options.updateTarget ?? "compute",
11550
11612
  release: next,
11551
- currentVersion: current,
11613
+ currentVersion: current2,
11552
11614
  attemptId
11553
11615
  })))(release, observation.version, desired.attemptId);
11554
11616
  if (!applied.ok)
@@ -12551,8 +12613,8 @@ function applyAgentEgressPolicy(options) {
12551
12613
  } : undefined;
12552
12614
  if (loopback && !uids.includes(loopback.uid))
12553
12615
  uids.push(loopback.uid);
12554
- const current = listPolicy(command2);
12555
- const applied = command2(["/usr/sbin/nft", "--file", "-"], renderAgentEgressNft(uids, current.exitCode === 0, loopback));
12616
+ const current2 = listPolicy(command2);
12617
+ const applied = command2(["/usr/sbin/nft", "--file", "-"], renderAgentEgressNft(uids, current2.exitCode === 0, loopback));
12556
12618
  if (applied.exitCode !== 0) {
12557
12619
  throw new Error(`Could not apply Agent egress policy: ${applied.stderr.trim() || "nft refused it"}`);
12558
12620
  }
@@ -13313,15 +13375,15 @@ var replaceLink = (path2, target2) => {
13313
13375
  renameSync10(pending, path2);
13314
13376
  };
13315
13377
  var secureRelease = (path2, uid, gid) => {
13316
- const visit = (current) => {
13317
- const metadata = lstatSync3(current);
13378
+ const visit = (current2) => {
13379
+ const metadata = lstatSync3(current2);
13318
13380
  if (metadata.isSymbolicLink())
13319
13381
  throw new Error("release contains a symbolic link");
13320
- chownSync(current, uid, gid);
13321
- chmodSync15(current, metadata.isDirectory() ? 365 : 292);
13382
+ chownSync(current2, uid, gid);
13383
+ chmodSync15(current2, metadata.isDirectory() ? 365 : 292);
13322
13384
  if (metadata.isDirectory())
13323
- for (const name of readdirSync4(current))
13324
- visit(join10(current, name));
13385
+ for (const name of readdirSync4(current2))
13386
+ visit(join10(current2, name));
13325
13387
  };
13326
13388
  visit(path2);
13327
13389
  };
@@ -15162,19 +15224,15 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
15162
15224
  // src/community-rehearsal-host.ts
15163
15225
  import { createHash as createHash11 } from "crypto";
15164
15226
  import { lstatSync as lstatSync7, mkdirSync as mkdirSync16, readFileSync as readFileSync18, rmSync as rmSync12, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync16 } from "fs";
15227
+ import { isIP as isIP5 } from "net";
15165
15228
  import { dirname as dirname11 } from "path";
15166
15229
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
15167
15230
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
15168
15231
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
15169
15232
  var COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
15170
15233
  var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
15171
- var NODES = new Map([
15172
- ["dev-fz-n1", { address: "10.42.0.21", agency: true }],
15173
- ["dev-fz-n2", { address: "10.42.0.22", agency: true }],
15174
- ["dev-fz-n3", { address: "10.42.0.23", agency: true }],
15175
- ["dev-fz-n4", { address: "10.42.0.24", agency: false }]
15176
- ]);
15177
15234
  var RELEASE = /^[a-f0-9]{64}$/;
15235
+ var SAFE_NAME2 = /^[a-z][a-z0-9-]{0,62}$/;
15178
15236
  async function ensureCommunityRehearsalArango(execute3 = executeSoftwareOperation) {
15179
15237
  const requirement2 = { software: "arangodb", version: COMMUNITY_REHEARSAL_VERSION };
15180
15238
  if ((await execute3({ kind: "check", ...requirement2 })).exitCode === 0)
@@ -15189,50 +15247,70 @@ var exactObject = (value) => {
15189
15247
  throw new Error("community rehearsal request must be an object");
15190
15248
  return value;
15191
15249
  };
15192
- var exactNode = (node) => {
15193
- if (typeof node !== "string")
15250
+ var nodeName = (node) => {
15251
+ if (typeof node !== "string" || !SAFE_NAME2.test(node))
15194
15252
  throw new Error("community rehearsal node is invalid");
15195
- const expected = NODES.get(node);
15196
- if (!expected)
15197
- throw new Error("community rehearsal accepts only the dedicated dev-fz-n1..n4 fleet");
15198
- return { name: node, ...expected };
15253
+ return node;
15254
+ };
15255
+ var validateCommunityRehearsalTopology = (value) => {
15256
+ if (!Array.isArray(value) || value.length !== 4)
15257
+ throw new Error("community rehearsal requires exactly four reviewed nodes");
15258
+ const rows = value.map((item, index) => {
15259
+ const row2 = exactObject(item);
15260
+ if (Object.keys(row2).some((key) => !["name", "address", "agency", "starter", "readPreferred"].includes(key)) || typeof row2.name !== "string" || !SAFE_NAME2.test(row2.name) || typeof row2.address !== "string" || isIP5(row2.address) !== 4 || typeof row2.agency !== "boolean" || typeof row2.starter !== "boolean" || typeof row2.readPreferred !== "boolean") {
15261
+ throw new Error(`community rehearsal topology node ${index + 1} is invalid`);
15262
+ }
15263
+ return row2;
15264
+ });
15265
+ if (new Set(rows.map(({ name }) => name)).size !== rows.length || new Set(rows.map(({ address }) => address)).size !== rows.length) {
15266
+ throw new Error("community rehearsal node names and addresses must be unique");
15267
+ }
15268
+ if (rows.filter(({ agency }) => agency).length !== 3 || rows.filter(({ starter }) => starter).length !== 1 || !rows.find(({ starter }) => starter).agency || rows.filter(({ readPreferred }) => readPreferred).length !== 1 || rows.find(({ readPreferred }) => readPreferred).agency) {
15269
+ throw new Error("community rehearsal requires three Agency voters, one voter starter and one non-voting read-preferred member");
15270
+ }
15271
+ return rows;
15199
15272
  };
15200
15273
  function parseCommunityRehearsalHostRequest(value) {
15201
15274
  const row2 = exactObject(value);
15202
- const node = exactNode(row2.node);
15275
+ const node = nodeName(row2.node);
15276
+ const topology = validateCommunityRehearsalTopology(row2.topology);
15277
+ const expected = topology.find((entry) => entry.name === node);
15278
+ if (!expected)
15279
+ throw new Error("community rehearsal node is not present in the reviewed topology");
15203
15280
  const action = row2.action;
15204
15281
  if (action === "prepare") {
15205
- const allowed = ["action", "node", "address", "agency", "release", "archivePath", "archiveSha256", "jwt"];
15206
- if (Object.keys(row2).some((key) => !allowed.includes(key)) || row2.address !== node.address || row2.agency !== node.agency || typeof row2.release !== "string" || !RELEASE.test(row2.release) || row2.archiveSha256 !== row2.release || row2.archivePath !== `/tmp/forgezero-community-${row2.release}.tar.gz` || typeof row2.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row2.jwt)) {
15282
+ const allowed = ["action", "node", "address", "agency", "topology", "release", "archivePath", "archiveSha256", "jwt"];
15283
+ if (Object.keys(row2).some((key) => !allowed.includes(key)) || row2.address !== expected.address || row2.agency !== expected.agency || typeof row2.release !== "string" || !RELEASE.test(row2.release) || row2.archiveSha256 !== row2.release || row2.archivePath !== `/tmp/forgezero-community-${row2.release}.tar.gz` || typeof row2.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row2.jwt)) {
15207
15284
  throw new Error("community rehearsal prepare request does not match the dedicated fleet contract");
15208
15285
  }
15209
- return row2;
15286
+ return { ...row2, topology };
15210
15287
  }
15211
15288
  if (action === "cleanup") {
15212
- if (Object.keys(row2).some((key) => !["action", "node"].includes(key))) {
15289
+ if (Object.keys(row2).some((key) => !["action", "node", "topology"].includes(key))) {
15213
15290
  throw new Error("community rehearsal cleanup has unknown fields");
15214
15291
  }
15215
- return { action, node: node.name };
15292
+ return { action, node, topology };
15216
15293
  }
15217
15294
  if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
15218
- if (Object.keys(row2).some((key) => !["action", "node"].includes(key)))
15295
+ if (Object.keys(row2).some((key) => !["action", "node", "topology"].includes(key)))
15219
15296
  throw new Error("community rehearsal host action has unknown fields");
15220
- return { action, node: node.name };
15297
+ return { action, node, topology };
15221
15298
  }
15222
15299
  if (action === "api") {
15223
- if (Object.keys(row2).some((key) => !["action", "node", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row2.operation)) || row2.key !== undefined && (typeof row2.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row2.key)) || row2.value !== undefined && (typeof row2.value !== "string" || row2.value.length > 4096)) {
15300
+ if (Object.keys(row2).some((key) => !["action", "node", "topology", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row2.operation)) || row2.key !== undefined && (typeof row2.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row2.key)) || row2.value !== undefined && (typeof row2.value !== "string" || row2.value.length > 4096)) {
15224
15301
  throw new Error("community rehearsal API request is invalid");
15225
15302
  }
15226
15303
  if ((row2.operation === "write" || row2.operation === "read") && typeof row2.key !== "string")
15227
15304
  throw new Error("community rehearsal API key is required");
15228
15305
  if ((row2.operation === "write" || row2.operation === "query") && typeof row2.value !== "string")
15229
15306
  throw new Error("community rehearsal API value is required");
15230
- return row2;
15307
+ return { ...row2, node, topology };
15231
15308
  }
15232
15309
  throw new Error("unsupported community rehearsal host action");
15233
15310
  }
15234
- var databaseUnit = (node) => {
15235
- const join12 = node.name === "dev-fz-n1" ? "" : " --starter.join=10.42.0.21";
15311
+ var databaseUnit = (node, topology) => {
15312
+ const starter = topology.find((entry) => entry.starter);
15313
+ const join12 = node.starter ? "" : ` --starter.join=${starter.address}`;
15236
15314
  const role = node.agency ? "" : " --cluster.start-agent=false --cluster.start-coordinator=true --cluster.start-dbserver=true";
15237
15315
  return `[Unit]
15238
15316
  Description=ForgeZero isolated Community ${COMMUNITY_REHEARSAL_VERSION} rehearsal cluster
@@ -15267,7 +15345,10 @@ LockPersonality=true
15267
15345
  WantedBy=multi-user.target
15268
15346
  `;
15269
15347
  };
15270
- var apiUnit = (node) => `[Unit]
15348
+ var apiUnit = (node, topology) => {
15349
+ const urls = topology.map(({ address }) => `http://${address}:8529`).join(",");
15350
+ const readPreferred = topology.filter(({ readPreferred: readPreferred2 }) => readPreferred2).map(({ address }) => `http://${address}:8529`).join(",");
15351
+ return `[Unit]
15271
15352
  Description=ForgeZero typed Community cluster rehearsal API
15272
15353
  After=forgezero-rehearsal-db.service network-online.target
15273
15354
  Requires=forgezero-rehearsal-db.service
@@ -15282,8 +15363,8 @@ Environment=HOST=127.0.0.1
15282
15363
  Environment=PORT=8787
15283
15364
  Environment=NODE_NAME=${node.name}
15284
15365
  Environment=ARANGO_URL=http://127.0.0.1:8529
15285
- Environment=ARANGO_URLS=http://10.42.0.21:8529,http://10.42.0.22:8529,http://10.42.0.23:8529,http://10.42.0.24:8529
15286
- Environment=ARANGO_READ_PREFERRED_URLS=http://10.42.0.24:8529
15366
+ Environment=ARANGO_URLS=${urls}
15367
+ Environment=ARANGO_READ_PREFERRED_URLS=${readPreferred}
15287
15368
  Environment=ARANGO_READ_PREFERRED_FALLBACK=error
15288
15369
  ExecStart=/usr/local/bin/bun src/index.ts
15289
15370
  Restart=always
@@ -15303,8 +15384,10 @@ LockPersonality=true
15303
15384
  [Install]
15304
15385
  WantedBy=multi-user.target
15305
15386
  `;
15387
+ };
15306
15388
  function planCommunityRehearsalPrepare(request) {
15307
- const node = exactNode(request.node);
15389
+ const topology = validateCommunityRehearsalTopology(request.topology);
15390
+ const node = topology.find(({ name }) => name === request.node);
15308
15391
  const releaseRoot = `${COMMUNITY_REHEARSAL_ROOT}/releases/${request.release}`;
15309
15392
  return [
15310
15393
  { kind: "exec", argv: ["/usr/sbin/arangod", "--version"] },
@@ -15319,13 +15402,13 @@ function planCommunityRehearsalPrepare(request) {
15319
15402
  ` },
15320
15403
  { kind: "exec", argv: ["/usr/bin/chmod", "0400", `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next`] },
15321
15404
  { kind: "exec", argv: ["/usr/bin/mv", "-f", `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next`, `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred`] },
15322
- { kind: "write", path: "/etc/systemd/system/forgezero-rehearsal-db.service", content: databaseUnit(node), mode: 420 },
15405
+ { kind: "write", path: "/etc/systemd/system/forgezero-rehearsal-db.service", content: databaseUnit(node, topology), mode: 420 },
15323
15406
  { kind: "exec", argv: ["/usr/bin/tar", "-xzf", request.archivePath, "-C", releaseRoot] },
15324
15407
  { kind: "exec", argv: ["/usr/bin/chown", "-R", "forgezero:forgezero", releaseRoot] },
15325
15408
  { kind: "exec", argv: ["/usr/sbin/runuser", "-u", "forgezero", "--", "/usr/local/bin/bun", "install", "--cwd", releaseRoot, "--frozen-lockfile", "--production"] },
15326
15409
  { kind: "unlink", path: `${COMMUNITY_REHEARSAL_ROOT}/current` },
15327
15410
  { kind: "symlink", target: releaseRoot, path: `${COMMUNITY_REHEARSAL_ROOT}/current` },
15328
- { kind: "write", path: "/etc/systemd/system/forgezero-community-api.service", content: apiUnit(node), mode: 420 },
15411
+ { kind: "write", path: "/etc/systemd/system/forgezero-community-api.service", content: apiUnit(node, topology), mode: 420 },
15329
15412
  { kind: "unlink", path: request.archivePath }
15330
15413
  ];
15331
15414
  }
@@ -15403,7 +15486,10 @@ var responseFor = async (request) => {
15403
15486
  async function runCommunityRehearsalHost(request) {
15404
15487
  if ((process.getuid?.() ?? -1) !== 0)
15405
15488
  throw new Error("community-rehearsal must run as root");
15406
- const node = exactNode(request.node);
15489
+ const topology = validateCommunityRehearsalTopology(request.topology);
15490
+ const node = topology.find((entry) => entry.name === request.node);
15491
+ if (!node)
15492
+ throw new Error("community rehearsal node is not present in the reviewed topology");
15407
15493
  if (request.action === "cleanup") {
15408
15494
  await applyOperations(planCommunityRehearsalCleanup());
15409
15495
  return { ok: true, action: request.action, node: node.name };
@@ -15440,7 +15526,8 @@ async function runCommunityRehearsalHost(request) {
15440
15526
  return { ok: true, action: request.action, node: node.name };
15441
15527
  }
15442
15528
  if (request.action === "database-ready" || request.action === "starter-ready" || request.action === "api-ready") {
15443
- const url = request.action === "database-ready" ? "http://127.0.0.1:8529/_api/version" : request.action === "starter-ready" ? "http://10.42.0.21:8528/version" : "http://127.0.0.1:8787/health";
15529
+ const starter = topology.find((entry) => entry.starter);
15530
+ const url = request.action === "database-ready" ? "http://127.0.0.1:8529/_api/version" : request.action === "starter-ready" ? `http://${starter.address}:8528/version` : "http://127.0.0.1:8787/health";
15444
15531
  const response2 = await fetch(url, { signal: AbortSignal.timeout(2000) }).catch(() => {
15445
15532
  return;
15446
15533
  });
@@ -16542,6 +16629,7 @@ export {
16542
16629
  tenantNodeApiUrl,
16543
16630
  systemdListenFd,
16544
16631
  systemdAgentEgressDirectives,
16632
+ supportedGuestImage,
16545
16633
  superviseAgentEgressPolicy,
16546
16634
  startSshBootstrapPull,
16547
16635
  startSoftwareHelper,
@@ -16631,6 +16719,7 @@ export {
16631
16719
  VERSION2 as VERSION,
16632
16720
  SYSTEMD_RESOLVED_STUB,
16633
16721
  SYSTEMD_RESOLVED_ADDRESS,
16722
+ SUPPORTED_GUEST_IMAGES,
16634
16723
  SUPPORTED_GUEST_IMAGE,
16635
16724
  SOFTWARE_HELPER_UNIT_PATH,
16636
16725
  SOFTWARE_HELPER_GROUP,