@forgezero/agent 0.1.73 → 0.1.75

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.
Files changed (44) hide show
  1. package/README.md +565 -61
  2. package/dist/agent-heartbeat.d.ts +25 -1
  3. package/dist/agent-heartbeat.js +45 -5
  4. package/dist/bootstrap.d.ts +2 -0
  5. package/dist/bootstrap.js +22 -4
  6. package/dist/community-rehearsal-host.js +208 -9
  7. package/dist/credential-schema.d.ts +1 -1
  8. package/dist/credential-schema.js +1 -1
  9. package/dist/definition.js +206 -7
  10. package/dist/deploy-actions.d.ts +7 -0
  11. package/dist/deploy-compiler.js +191 -10
  12. package/dist/deploy-file.js +206 -7
  13. package/dist/deploy-plan-runner.d.ts +3 -0
  14. package/dist/deploy-plan-runner.js +190 -9
  15. package/dist/deploy-plan.js +187 -8
  16. package/dist/deploy-providers.d.ts +19 -0
  17. package/dist/deploy.d.ts +89 -3
  18. package/dist/deploy.js +49 -2
  19. package/dist/deployment-connectivity.d.ts +63 -0
  20. package/dist/deployment-connectivity.js +148 -0
  21. package/dist/deployment-pull.d.ts +2 -0
  22. package/dist/deployment-targets.d.ts +9 -0
  23. package/dist/deployment-targets.js +141 -0
  24. package/dist/deployment.d.ts +69 -0
  25. package/dist/fz-agent.js +1112 -302
  26. package/dist/fz.js +246 -18
  27. package/dist/index.d.ts +1 -0
  28. package/dist/metal-bootstrap.js +1 -1
  29. package/dist/metal-helper-socket.js +210 -11
  30. package/dist/metal-provision.js +210 -11
  31. package/dist/operator-bootstrap.js +22 -4
  32. package/dist/platform-bootstrap-runtime.d.ts +1 -0
  33. package/dist/platform-bootstrap-runtime.js +216 -10
  34. package/dist/platform-fleet-verification.js +523 -150
  35. package/dist/platform-genesis.js +206 -7
  36. package/dist/provision.js +465 -99
  37. package/dist/service-supervisor.d.ts +6 -0
  38. package/dist/software-helper.d.ts +3 -0
  39. package/dist/software-helper.js +465 -98
  40. package/dist/software.d.ts +4 -1
  41. package/dist/software.js +210 -8
  42. package/dist/ubuntu.js +206 -7
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +12 -4
@@ -144,22 +144,30 @@ import {
144
144
  accessSync,
145
145
  chmodSync,
146
146
  copyFileSync,
147
+ createReadStream,
147
148
  existsSync,
148
149
  mkdtempSync,
149
150
  mkdirSync,
150
151
  readFileSync,
151
152
  renameSync,
152
153
  rmSync,
154
+ statSync,
153
155
  symlinkSync,
154
156
  unlinkSync,
155
157
  writeFileSync
156
158
  } from "node:fs";
157
159
  import { tmpdir } from "node:os";
158
- import { join } from "node:path";
160
+ import { dirname, join } from "node:path";
159
161
  var PINNED_BUN_VERSION = "1.3.14";
160
162
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
161
163
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
162
164
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
165
+ var NERDCTL_VERSION = "2.3.5";
166
+ var BUILDKIT_VERSION = "0.32.2";
167
+ var KATA_CONTAINERS_VERSION = "4.0.0";
168
+ var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
169
+ var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
170
+ var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
163
171
  var OS_CATALOG = [
164
172
  {
165
173
  id: "ubuntu",
@@ -189,6 +197,8 @@ var OS_CATALOG = [
189
197
  var SOFTWARE_CATALOG = [
190
198
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
191
199
  { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
200
+ { id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
201
+ { id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
192
202
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
193
203
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
194
204
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -197,6 +207,7 @@ var SOFTWARE_CATALOG = [
197
207
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
198
208
  { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
199
209
  { id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
210
+ { id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
200
211
  { id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
201
212
  { id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
202
213
  { id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -205,6 +216,8 @@ var SOFTWARE_CATALOG = [
205
216
  var UBUNTU_2604_X64 = [
206
217
  { requirement: { id: "bun", version: "1.3.14" } },
207
218
  { requirement: { id: "docker", version: "ubuntu-26.04" } },
219
+ { requirement: { id: "containerd", version: "ubuntu-26.04" } },
220
+ { requirement: { id: "kata-containers", version: "4.0.0" } },
208
221
  { requirement: { id: "nginx", version: "ubuntu-26.04" } },
209
222
  { requirement: { id: "arangodb", version: "3.11.14" } },
210
223
  { requirement: { id: "cloudflared", version: "2026.7.3" } },
@@ -216,6 +229,7 @@ var UBUNTU_2604_X64 = [
216
229
  var UBUNTU_2404_X64 = [
217
230
  { requirement: { id: "bun", version: "1.3.14" } },
218
231
  { requirement: { id: "docker", version: "ubuntu-24.04" } },
232
+ { requirement: { id: "containerd", version: "ubuntu-24.04" } },
219
233
  { requirement: { id: "nginx", version: "ubuntu-24.04" } },
220
234
  { requirement: { id: "ufw", version: "ubuntu-24.04" } },
221
235
  { requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
@@ -232,6 +246,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
232
246
  "log-opts": { "max-size": "10m", "max-file": "3" }
233
247
  }, null, 2)}
234
248
  `;
249
+ var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
250
+ var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
251
+ var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
252
+ var CONTAINERD_CONFIG = (kata) => `version = 2
253
+ root = "/var/lib/forgezero/containerd"
254
+ state = "/run/forgezero/containerd"
255
+
256
+ [grpc]
257
+ address = "/run/containerd/containerd.sock"
258
+
259
+ [plugins."io.containerd.grpc.v1.cri".containerd]
260
+ snapshotter = "overlayfs"
261
+ default_runtime_name = "runc"
262
+
263
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
264
+ runtime_type = "io.containerd.runc.v2"
265
+ ${kata ? `
266
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
267
+ runtime_type = "io.containerd.kata.v2"
268
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
269
+ ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
270
+ ` : ""}`;
271
+ var BUILDKIT_UNIT = `[Unit]
272
+ Description=ForgeZero BuildKit daemon
273
+ After=network-online.target containerd.service
274
+ Wants=network-online.target
275
+ Requires=containerd.service
276
+
277
+ [Service]
278
+ Type=notify
279
+ ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
280
+ Restart=on-failure
281
+ RestartSec=2
282
+ NoNewPrivileges=yes
283
+ ProtectSystem=strict
284
+ ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
285
+
286
+ [Install]
287
+ WantedBy=multi-user.target
288
+ `;
235
289
  var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
236
290
  var runSoftwareCommand = async (argv, env = {}) => {
237
291
  let child;
@@ -251,14 +305,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
251
305
  return { exitCode, output: `${stdout}${stderr}` };
252
306
  };
253
307
  var run = runSoftwareCommand;
254
- var download = async (url, destination, sha256) => {
255
- const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
308
+ var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
309
+ const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
256
310
  if (!response.ok)
257
311
  throw new Error(`download failed with HTTP ${response.status}`);
258
- const bytes = new Uint8Array(await response.arrayBuffer());
259
- if (createHash("sha256").update(bytes).digest("hex") !== sha256)
312
+ const declared = Number(response.headers.get("content-length") ?? 0);
313
+ if (declared > maximumBytes)
314
+ throw new Error("download exceeds reviewed size bound");
315
+ if (existsSync(destination))
316
+ throw new Error("download destination already exists");
317
+ await Bun.write(destination, response);
318
+ chmodSync(destination, 384);
319
+ if (statSync(destination).size > maximumBytes) {
320
+ rmSync(destination, { force: true });
321
+ throw new Error("download exceeds reviewed size bound");
322
+ }
323
+ const digest = createHash("sha256");
324
+ for await (const chunk of createReadStream(destination))
325
+ digest.update(chunk);
326
+ if (digest.digest("hex") !== sha256) {
327
+ rmSync(destination, { force: true });
260
328
  throw new Error("download checksum mismatch");
261
- writeFileSync(destination, bytes, { mode: 384, flag: "wx" });
329
+ }
262
330
  };
263
331
  var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
264
332
  var aptInstall = async (name) => {
@@ -266,6 +334,106 @@ var aptInstall = async (name) => {
266
334
  const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
267
335
  return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
268
336
  };
337
+ var aptInstallMany = async (names) => {
338
+ const environment = { DEBIAN_FRONTEND: "noninteractive" };
339
+ const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
340
+ return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
341
+ };
342
+ var writeOwnedPolicy = (pathValue, content, mode = 420) => {
343
+ if (existsSync(pathValue)) {
344
+ if (readFileSync(pathValue, "utf8") !== content)
345
+ throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
346
+ return;
347
+ }
348
+ mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
349
+ writeFileSync(pathValue, content, { mode, flag: "wx" });
350
+ };
351
+ var installContainerdRuntime = async (directory, osVersion) => {
352
+ const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
353
+ if (installed.exitCode !== 0)
354
+ return installed;
355
+ const nerdctl = join(directory, "nerdctl.tar.gz");
356
+ await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
357
+ const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
358
+ if (nerdctlExtract.exitCode !== 0)
359
+ return nerdctlExtract;
360
+ chmodSync("/usr/local/bin/nerdctl", 493);
361
+ const buildkit = join(directory, "buildkit.tar.gz");
362
+ await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
363
+ const unpacked = join(directory, "buildkit");
364
+ mkdirSync(unpacked, { mode: 448 });
365
+ const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
366
+ if (buildkitExtract.exitCode !== 0)
367
+ return buildkitExtract;
368
+ for (const binary of ["buildctl", "buildkitd"]) {
369
+ copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
370
+ chmodSync(`/usr/local/bin/${binary}`, 493);
371
+ }
372
+ mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
373
+ const expected = CONTAINERD_CONFIG(false);
374
+ const kataExpected = CONTAINERD_CONFIG(true);
375
+ if (existsSync(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
376
+ return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
377
+ }
378
+ if (!existsSync(CONTAINERD_CONFIG_PATH))
379
+ writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
380
+ writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
381
+ mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
382
+ mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
383
+ const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
384
+ if (reload.exitCode !== 0)
385
+ return reload;
386
+ const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
387
+ return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
388
+ };
389
+ var installKataRuntime = async (directory) => {
390
+ for (const pathValue of ["/dev/kvm", "/dev/sev"])
391
+ if (!existsSync(pathValue))
392
+ return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
393
+ for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
394
+ if (!existsSync(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
395
+ return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
396
+ }
397
+ const dependencies = await aptInstallMany(["zstd"]);
398
+ if (dependencies.exitCode !== 0)
399
+ return dependencies;
400
+ const archive = join(directory, "kata.tar.zst");
401
+ await download(`https://github.com/kata-containers/kata-containers/releases/download/${KATA_CONTAINERS_VERSION}/kata-static-${KATA_CONTAINERS_VERSION}-amd64.tar.zst`, archive, KATA_SHA256, 2200000000);
402
+ const tar = join(directory, "kata.tar");
403
+ const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
404
+ if (expanded.exitCode !== 0)
405
+ return expanded;
406
+ const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
407
+ if (extracted.exitCode !== 0)
408
+ return extracted;
409
+ if (!existsSync("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync(KATA_SNP_CONFIG_PATH)) {
410
+ return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
411
+ }
412
+ const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
413
+ if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
414
+ return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
415
+ }
416
+ mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
417
+ if (existsSync(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
418
+ return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
419
+ }
420
+ if (!existsSync(KATA_ACTIVE_CONFIG_PATH))
421
+ copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
422
+ chmodSync(KATA_ACTIVE_CONFIG_PATH, 420);
423
+ try {
424
+ unlinkSync("/usr/local/bin/containerd-shim-kata-v2");
425
+ } catch {}
426
+ symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
427
+ const base = CONTAINERD_CONFIG(false);
428
+ const kata = CONTAINERD_CONFIG(true);
429
+ if (!existsSync(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
430
+ return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
431
+ }
432
+ if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
433
+ writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
434
+ const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
435
+ return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
436
+ };
269
437
  async function executeSoftwareOperation(operation) {
270
438
  const { software, version } = operation;
271
439
  if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
@@ -283,6 +451,26 @@ async function executeSoftwareOperation(operation) {
283
451
  const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
284
452
  return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
285
453
  }
454
+ if (software === "containerd") {
455
+ const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
456
+ run(["/usr/bin/containerd", "--version"]),
457
+ run(["/usr/local/bin/nerdctl", "--version"]),
458
+ run(["/usr/local/bin/buildkitd", "--version"]),
459
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
460
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
461
+ ]);
462
+ const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
463
+ const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
464
+ return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
465
+ }
466
+ if (software === "kata-containers") {
467
+ const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
468
+ const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
469
+ const activeConfig = existsSync(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
470
+ const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync(parameter) && /^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()));
471
+ const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
472
+ return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync(KATA_SNP_CONFIG_PATH) ? readFileSync(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
473
+ }
286
474
  if (software === "nginx") {
287
475
  const binary = await run(["/usr/sbin/nginx", "-v"]);
288
476
  return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
@@ -334,6 +522,17 @@ async function executeSoftwareOperation(operation) {
334
522
  }
335
523
  const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
336
524
  try {
525
+ if (software === "containerd")
526
+ return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
527
+ if (software === "kata-containers") {
528
+ const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
529
+ if (containerd.exitCode !== 0) {
530
+ const installed2 = await installContainerdRuntime(directory, "26.04");
531
+ if (installed2.exitCode !== 0)
532
+ return installed2;
533
+ }
534
+ return installKataRuntime(directory);
535
+ }
337
536
  if (software === "cloudflare-warp") {
338
537
  const key = join(directory, "cloudflare-warp-key.gpg");
339
538
  await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
@@ -415,7 +614,7 @@ function validateSoftwareRequirements(value, _options = {}) {
415
614
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
416
615
  throw new Error("software requirement contains an unknown field");
417
616
  }
418
- if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
617
+ if (!["bun", "docker", "containerd", "kata-containers", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
419
618
  throw new Error("software requirement coordinate is invalid");
420
619
  }
421
620
  const requirement = { id: row.id, version: row.version };
@@ -495,11 +694,11 @@ import {
495
694
  readFileSync as readFileSync2,
496
695
  readdirSync,
497
696
  rmSync as rmSync2,
498
- statSync,
697
+ statSync as statSync2,
499
698
  unlinkSync as unlinkSync2,
500
699
  writeFileSync as writeFileSync2
501
700
  } from "node:fs";
502
- import { dirname, isAbsolute, join as join2 } from "node:path";
701
+ import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
503
702
  import { isIP } from "node:net";
504
703
  var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
505
704
  var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
@@ -794,7 +993,7 @@ async function provisionMetalGuest(profile, claim, exec) {
794
993
  if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
795
994
  throw new MetalProvisionError(`image ${claim.spec.imageKey} is not configured locally`);
796
995
  }
797
- if (!statSync(image.path).isFile())
996
+ if (!statSync2(image.path).isFile())
798
997
  throw new MetalProvisionError("configured image is not a file");
799
998
  const digest = (await checked(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
800
999
  if (digest !== image.sha256)
@@ -879,7 +1078,7 @@ async function provisionMetalGuest(profile, claim, exec) {
879
1078
  }
880
1079
  const service = `forgezero-guest@${name}.service`;
881
1080
  const unitPath = join2(profile.unitDir, service);
882
- mkdirSync2(dirname(unitPath), { recursive: true });
1081
+ mkdirSync2(dirname2(unitPath), { recursive: true });
883
1082
  writeFileSync2(unitPath, guestUnit(spec), { mode: 420 });
884
1083
  await checked(exec, ["systemctl", "daemon-reload"]);
885
1084
  if (prior?.phase === "running") {
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1416
1416
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1417
1417
 
1418
1418
  // src/version.ts
1419
- var VERSION = "0.1.73";
1419
+ var VERSION = "0.1.75";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3060,6 +3060,10 @@ function validatePlatformSharedEnvironment(input) {
3060
3060
  const coordinators = input.databaseCoordinators.map(privateCoordinator);
3061
3061
  if (new Set(coordinators).size !== coordinators.length)
3062
3062
  throw new Error("databaseCoordinators must be unique.");
3063
+ const readPreferred = (input.databaseReadPreferredCoordinators ?? []).map(privateCoordinator);
3064
+ if (new Set(readPreferred).size !== readPreferred.length || readPreferred.some((url) => !coordinators.includes(url))) {
3065
+ throw new Error("databaseReadPreferredCoordinators must be a unique subset of databaseCoordinators.");
3066
+ }
3063
3067
  if (input.databaseNetworkMode !== "private-lan") {
3064
3068
  throw new Error("Attended platform bootstrap supports only private-lan database networking.");
3065
3069
  }
@@ -3133,6 +3137,7 @@ function validatePlatformSharedEnvironment(input) {
3133
3137
  return {
3134
3138
  ...input,
3135
3139
  databaseCoordinators: coordinators,
3140
+ databaseReadPreferredCoordinators: readPreferred,
3136
3141
  appOrigin: httpsOrigin("appOrigin", input.appOrigin),
3137
3142
  apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
3138
3143
  agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint),
@@ -3146,6 +3151,8 @@ function renderPlatformSharedEnvironment(input) {
3146
3151
  const entries = {
3147
3152
  ARANGO_URL: value.databaseCoordinators[0],
3148
3153
  ARANGO_URLS: value.databaseCoordinators.join(","),
3154
+ ARANGO_READ_PREFERRED_URLS: (value.databaseReadPreferredCoordinators ?? []).join(","),
3155
+ ARANGO_READ_PREFERRED_FALLBACK: "balanced",
3149
3156
  ARANGO_DB: "fz",
3150
3157
  FZ_DATABASE_MODE: "platform",
3151
3158
  ARANGO_USER: value.databaseUser,
@@ -3769,6 +3776,10 @@ function validateBootstrapConfig(value) {
3769
3776
  if (!["member", "none"].includes(value.database.agency) || value.database.role === "master" && value.database.agency !== "member" || value.database.role === "none" && value.database.agency !== "none") {
3770
3777
  throw new Error("database role and Agency participation disagree");
3771
3778
  }
3779
+ value.database.readPreferred ??= false;
3780
+ if (value.database.readPreferred && (value.database.role !== "joiner" || value.database.agency !== "none")) {
3781
+ throw new Error("read-preferred database routing requires an extra non-Agency joiner");
3782
+ }
3772
3783
  if (value.database.role !== "none" && !value.database.address)
3773
3784
  throw new Error("database nodes require a private address");
3774
3785
  if (value.database.role === "joiner" && !value.database.master)
@@ -3788,6 +3799,10 @@ function validateBootstrapConfig(value) {
3788
3799
  if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
3789
3800
  throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
3790
3801
  }
3802
+ const ownCoordinator = value.database.address ? `http://${value.database.address}:8529` : undefined;
3803
+ if (Boolean(ownCoordinator && (runtime.databaseReadPreferredCoordinators ?? []).includes(ownCoordinator)) !== value.database.readPreferred) {
3804
+ throw new Error("runtime read-preferred coordinator set disagrees with this node database routing intent");
3805
+ }
3791
3806
  if (runtime.deployProfile !== value.environment)
3792
3807
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
3793
3808
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
@@ -4099,6 +4114,7 @@ function bootstrapIdentity(config) {
4099
4114
  role: config.database.role,
4100
4115
  serverMode: config.database.serverMode,
4101
4116
  agency: config.database.agency,
4117
+ readPreferred: config.database.readPreferred,
4102
4118
  address: config.database.address ?? null,
4103
4119
  master: config.database.master ?? null,
4104
4120
  coordinators: config.database.coordinators
@@ -4134,7 +4150,7 @@ function parseStoredState(raw) {
4134
4150
  if (!value || typeof value !== "object" || Array.isArray(value))
4135
4151
  throw new Error("bootstrap state is malformed");
4136
4152
  const state = value;
4137
- if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && !["member", "none"].includes(state.databaseAgency ?? "")) {
4153
+ if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && (!["member", "none"].includes(state.databaseAgency ?? "") || typeof state.databaseReadPreferred !== "boolean")) {
4138
4154
  throw new Error("bootstrap state is legacy or incomplete; refusing an unbound repair");
4139
4155
  }
4140
4156
  return state;
@@ -4184,6 +4200,7 @@ function stateFor(config, cloudflare, previousCloudflareTunnelId) {
4184
4200
  environment: config.environment,
4185
4201
  databaseRole: config.database.role,
4186
4202
  databaseAgency: config.database.agency,
4203
+ databaseReadPreferred: config.database.readPreferred,
4187
4204
  databaseServerMode: config.database.serverMode,
4188
4205
  databaseAddress: config.database.address,
4189
4206
  databaseCoordinators: config.database.coordinators,
@@ -4688,7 +4705,7 @@ function strictBootstrapDocument(value) {
4688
4705
  exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
4689
4706
  if (root.cloudflareHandoff !== undefined)
4690
4707
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
4691
- exactKeys(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
4708
+ exactKeys(root.database, ["role", "agency", "readPreferred", "serverMode", "address", "master", "coordinators"], "database config");
4692
4709
  exactKeys(root.enrolment, ["source"], "platform enrolment config");
4693
4710
  const runtime = exactKeys(root.runtime, [
4694
4711
  "environment",
@@ -4732,7 +4749,8 @@ function strictBootstrapDocument(value) {
4732
4749
  "backup",
4733
4750
  "cloudflare",
4734
4751
  "realtime",
4735
- "initialInventory"
4752
+ "initialInventory",
4753
+ "databaseReadPreferredCoordinators"
4736
4754
  ], "runtime environment");
4737
4755
  const environment = runtime.environment;
4738
4756
  if (environment.initialInventory !== undefined) {
@@ -47,6 +47,7 @@ export interface PlatformSharedEnvironment {
47
47
  softwareProfile: PlatformSoftwareProfile;
48
48
  databaseRole: PlatformDatabaseRole;
49
49
  databaseCoordinators: string[];
50
+ databaseReadPreferredCoordinators?: string[];
50
51
  databaseAddress?: string;
51
52
  databaseMaster?: string;
52
53
  databaseNetworkMode: 'private-lan';