@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
@@ -4,22 +4,30 @@ import {
4
4
  accessSync,
5
5
  chmodSync,
6
6
  copyFileSync,
7
+ createReadStream,
7
8
  existsSync,
8
9
  mkdtempSync,
9
10
  mkdirSync,
10
11
  readFileSync,
11
12
  renameSync,
12
13
  rmSync,
14
+ statSync,
13
15
  symlinkSync,
14
16
  unlinkSync,
15
17
  writeFileSync
16
18
  } from "node:fs";
17
19
  import { tmpdir } from "node:os";
18
- import { join } from "node:path";
20
+ import { dirname, join } from "node:path";
19
21
  var PINNED_BUN_VERSION = "1.3.14";
20
22
  var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
21
23
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
22
24
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
25
+ var NERDCTL_VERSION = "2.3.5";
26
+ var BUILDKIT_VERSION = "0.32.2";
27
+ var KATA_CONTAINERS_VERSION = "4.0.0";
28
+ var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
29
+ var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
30
+ var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
23
31
  var OS_CATALOG = [
24
32
  {
25
33
  id: "ubuntu",
@@ -49,6 +57,8 @@ var OS_CATALOG = [
49
57
  var SOFTWARE_CATALOG = [
50
58
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
51
59
  { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
60
+ { id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
61
+ { id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
52
62
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
53
63
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
54
64
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -57,6 +67,7 @@ var SOFTWARE_CATALOG = [
57
67
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
58
68
  { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
59
69
  { id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
70
+ { id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
60
71
  { id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
61
72
  { id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
62
73
  { id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -65,6 +76,8 @@ var SOFTWARE_CATALOG = [
65
76
  var UBUNTU_2604_X64 = [
66
77
  { requirement: { id: "bun", version: "1.3.14" } },
67
78
  { requirement: { id: "docker", version: "ubuntu-26.04" } },
79
+ { requirement: { id: "containerd", version: "ubuntu-26.04" } },
80
+ { requirement: { id: "kata-containers", version: "4.0.0" } },
68
81
  { requirement: { id: "nginx", version: "ubuntu-26.04" } },
69
82
  { requirement: { id: "arangodb", version: "3.11.14" } },
70
83
  { requirement: { id: "cloudflared", version: "2026.7.3" } },
@@ -76,6 +89,7 @@ var UBUNTU_2604_X64 = [
76
89
  var UBUNTU_2404_X64 = [
77
90
  { requirement: { id: "bun", version: "1.3.14" } },
78
91
  { requirement: { id: "docker", version: "ubuntu-24.04" } },
92
+ { requirement: { id: "containerd", version: "ubuntu-24.04" } },
79
93
  { requirement: { id: "nginx", version: "ubuntu-24.04" } },
80
94
  { requirement: { id: "ufw", version: "ubuntu-24.04" } },
81
95
  { requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
@@ -92,6 +106,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
92
106
  "log-opts": { "max-size": "10m", "max-file": "3" }
93
107
  }, null, 2)}
94
108
  `;
109
+ var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
110
+ var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
111
+ var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
112
+ var CONTAINERD_CONFIG = (kata) => `version = 2
113
+ root = "/var/lib/forgezero/containerd"
114
+ state = "/run/forgezero/containerd"
115
+
116
+ [grpc]
117
+ address = "/run/containerd/containerd.sock"
118
+
119
+ [plugins."io.containerd.grpc.v1.cri".containerd]
120
+ snapshotter = "overlayfs"
121
+ default_runtime_name = "runc"
122
+
123
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
124
+ runtime_type = "io.containerd.runc.v2"
125
+ ${kata ? `
126
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
127
+ runtime_type = "io.containerd.kata.v2"
128
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
129
+ ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
130
+ ` : ""}`;
131
+ var BUILDKIT_UNIT = `[Unit]
132
+ Description=ForgeZero BuildKit daemon
133
+ After=network-online.target containerd.service
134
+ Wants=network-online.target
135
+ Requires=containerd.service
136
+
137
+ [Service]
138
+ Type=notify
139
+ ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
140
+ Restart=on-failure
141
+ RestartSec=2
142
+ NoNewPrivileges=yes
143
+ ProtectSystem=strict
144
+ ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
145
+
146
+ [Install]
147
+ WantedBy=multi-user.target
148
+ `;
95
149
  var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
96
150
  var runSoftwareCommand = async (argv, env = {}) => {
97
151
  let child;
@@ -111,14 +165,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
111
165
  return { exitCode, output: `${stdout}${stderr}` };
112
166
  };
113
167
  var run = runSoftwareCommand;
114
- var download = async (url, destination, sha256) => {
115
- const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
168
+ var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
169
+ const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
116
170
  if (!response.ok)
117
171
  throw new Error(`download failed with HTTP ${response.status}`);
118
- const bytes = new Uint8Array(await response.arrayBuffer());
119
- if (createHash("sha256").update(bytes).digest("hex") !== sha256)
172
+ const declared = Number(response.headers.get("content-length") ?? 0);
173
+ if (declared > maximumBytes)
174
+ throw new Error("download exceeds reviewed size bound");
175
+ if (existsSync(destination))
176
+ throw new Error("download destination already exists");
177
+ await Bun.write(destination, response);
178
+ chmodSync(destination, 384);
179
+ if (statSync(destination).size > maximumBytes) {
180
+ rmSync(destination, { force: true });
181
+ throw new Error("download exceeds reviewed size bound");
182
+ }
183
+ const digest = createHash("sha256");
184
+ for await (const chunk of createReadStream(destination))
185
+ digest.update(chunk);
186
+ if (digest.digest("hex") !== sha256) {
187
+ rmSync(destination, { force: true });
120
188
  throw new Error("download checksum mismatch");
121
- writeFileSync(destination, bytes, { mode: 384, flag: "wx" });
189
+ }
122
190
  };
123
191
  var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
124
192
  var aptInstall = async (name) => {
@@ -126,6 +194,106 @@ var aptInstall = async (name) => {
126
194
  const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
127
195
  return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
128
196
  };
197
+ var aptInstallMany = async (names) => {
198
+ const environment = { DEBIAN_FRONTEND: "noninteractive" };
199
+ const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
200
+ return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
201
+ };
202
+ var writeOwnedPolicy = (pathValue, content, mode = 420) => {
203
+ if (existsSync(pathValue)) {
204
+ if (readFileSync(pathValue, "utf8") !== content)
205
+ throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
206
+ return;
207
+ }
208
+ mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
209
+ writeFileSync(pathValue, content, { mode, flag: "wx" });
210
+ };
211
+ var installContainerdRuntime = async (directory, osVersion) => {
212
+ const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
213
+ if (installed.exitCode !== 0)
214
+ return installed;
215
+ const nerdctl = join(directory, "nerdctl.tar.gz");
216
+ await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
217
+ const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
218
+ if (nerdctlExtract.exitCode !== 0)
219
+ return nerdctlExtract;
220
+ chmodSync("/usr/local/bin/nerdctl", 493);
221
+ const buildkit = join(directory, "buildkit.tar.gz");
222
+ await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
223
+ const unpacked = join(directory, "buildkit");
224
+ mkdirSync(unpacked, { mode: 448 });
225
+ const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
226
+ if (buildkitExtract.exitCode !== 0)
227
+ return buildkitExtract;
228
+ for (const binary of ["buildctl", "buildkitd"]) {
229
+ copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
230
+ chmodSync(`/usr/local/bin/${binary}`, 493);
231
+ }
232
+ mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
233
+ const expected = CONTAINERD_CONFIG(false);
234
+ const kataExpected = CONTAINERD_CONFIG(true);
235
+ if (existsSync(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
236
+ return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
237
+ }
238
+ if (!existsSync(CONTAINERD_CONFIG_PATH))
239
+ writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
240
+ writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
241
+ mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
242
+ mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
243
+ const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
244
+ if (reload.exitCode !== 0)
245
+ return reload;
246
+ const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
247
+ return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
248
+ };
249
+ var installKataRuntime = async (directory) => {
250
+ for (const pathValue of ["/dev/kvm", "/dev/sev"])
251
+ if (!existsSync(pathValue))
252
+ return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
253
+ for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
254
+ if (!existsSync(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
255
+ return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
256
+ }
257
+ const dependencies = await aptInstallMany(["zstd"]);
258
+ if (dependencies.exitCode !== 0)
259
+ return dependencies;
260
+ const archive = join(directory, "kata.tar.zst");
261
+ 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);
262
+ const tar = join(directory, "kata.tar");
263
+ const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
264
+ if (expanded.exitCode !== 0)
265
+ return expanded;
266
+ const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
267
+ if (extracted.exitCode !== 0)
268
+ return extracted;
269
+ if (!existsSync("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync(KATA_SNP_CONFIG_PATH)) {
270
+ return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
271
+ }
272
+ const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
273
+ if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
274
+ return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
275
+ }
276
+ mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
277
+ if (existsSync(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
278
+ return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
279
+ }
280
+ if (!existsSync(KATA_ACTIVE_CONFIG_PATH))
281
+ copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
282
+ chmodSync(KATA_ACTIVE_CONFIG_PATH, 420);
283
+ try {
284
+ unlinkSync("/usr/local/bin/containerd-shim-kata-v2");
285
+ } catch {}
286
+ symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
287
+ const base = CONTAINERD_CONFIG(false);
288
+ const kata = CONTAINERD_CONFIG(true);
289
+ if (!existsSync(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
290
+ return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
291
+ }
292
+ if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
293
+ writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
294
+ const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
295
+ return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
296
+ };
129
297
  async function executeSoftwareOperation(operation) {
130
298
  const { software, version } = operation;
131
299
  if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
@@ -143,6 +311,26 @@ async function executeSoftwareOperation(operation) {
143
311
  const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
144
312
  return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
145
313
  }
314
+ if (software === "containerd") {
315
+ const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
316
+ run(["/usr/bin/containerd", "--version"]),
317
+ run(["/usr/local/bin/nerdctl", "--version"]),
318
+ run(["/usr/local/bin/buildkitd", "--version"]),
319
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
320
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
321
+ ]);
322
+ const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
323
+ const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
324
+ 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" };
325
+ }
326
+ if (software === "kata-containers") {
327
+ const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
328
+ const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
329
+ const activeConfig = existsSync(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
330
+ 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()));
331
+ const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
332
+ 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" };
333
+ }
146
334
  if (software === "nginx") {
147
335
  const binary = await run(["/usr/sbin/nginx", "-v"]);
148
336
  return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
@@ -194,6 +382,17 @@ async function executeSoftwareOperation(operation) {
194
382
  }
195
383
  const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
196
384
  try {
385
+ if (software === "containerd")
386
+ return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
387
+ if (software === "kata-containers") {
388
+ const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
389
+ if (containerd.exitCode !== 0) {
390
+ const installed2 = await installContainerdRuntime(directory, "26.04");
391
+ if (installed2.exitCode !== 0)
392
+ return installed2;
393
+ }
394
+ return installKataRuntime(directory);
395
+ }
197
396
  if (software === "cloudflare-warp") {
198
397
  const key = join(directory, "cloudflare-warp-key.gpg");
199
398
  await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
@@ -275,7 +474,7 @@ function validateSoftwareRequirements(value, _options = {}) {
275
474
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
276
475
  throw new Error("software requirement contains an unknown field");
277
476
  }
278
- 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)) {
477
+ 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)) {
279
478
  throw new Error("software requirement coordinate is invalid");
280
479
  }
281
480
  const requirement = { id: row.id, version: row.version };
@@ -470,7 +669,7 @@ import {
470
669
  realpathSync,
471
670
  renameSync as renameSync2,
472
671
  rmSync as rmSync2,
473
- statSync,
672
+ statSync as statSync2,
474
673
  symlinkSync as symlinkSync2,
475
674
  writeFileSync as writeFileSync2
476
675
  } from "node:fs";
@@ -565,6 +764,10 @@ function validatePlatformSharedEnvironment(input) {
565
764
  const coordinators = input.databaseCoordinators.map(privateCoordinator);
566
765
  if (new Set(coordinators).size !== coordinators.length)
567
766
  throw new Error("databaseCoordinators must be unique.");
767
+ const readPreferred = (input.databaseReadPreferredCoordinators ?? []).map(privateCoordinator);
768
+ if (new Set(readPreferred).size !== readPreferred.length || readPreferred.some((url) => !coordinators.includes(url))) {
769
+ throw new Error("databaseReadPreferredCoordinators must be a unique subset of databaseCoordinators.");
770
+ }
568
771
  if (input.databaseNetworkMode !== "private-lan") {
569
772
  throw new Error("Attended platform bootstrap supports only private-lan database networking.");
570
773
  }
@@ -638,6 +841,7 @@ function validatePlatformSharedEnvironment(input) {
638
841
  return {
639
842
  ...input,
640
843
  databaseCoordinators: coordinators,
844
+ databaseReadPreferredCoordinators: readPreferred,
641
845
  appOrigin: httpsOrigin("appOrigin", input.appOrigin),
642
846
  apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
643
847
  agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint),
@@ -651,6 +855,8 @@ function renderPlatformSharedEnvironment(input) {
651
855
  const entries = {
652
856
  ARANGO_URL: value.databaseCoordinators[0],
653
857
  ARANGO_URLS: value.databaseCoordinators.join(","),
858
+ ARANGO_READ_PREFERRED_URLS: (value.databaseReadPreferredCoordinators ?? []).join(","),
859
+ ARANGO_READ_PREFERRED_FALLBACK: "balanced",
654
860
  ARANGO_DB: "fz",
655
861
  FZ_DATABASE_MODE: "platform",
656
862
  ARANGO_USER: value.databaseUser,
@@ -907,7 +1113,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
907
1113
  if (!release.startsWith(`${releases}/`) || release === releases)
908
1114
  throw new Error("release is outside configured releases directory");
909
1115
  for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
910
- const metadata = statSync(join2(release, required));
1116
+ const metadata = statSync2(join2(release, required));
911
1117
  if (!metadata.isFile() || metadata.size < 1)
912
1118
  throw new Error(`release is incomplete: ${required}`);
913
1119
  }
@@ -982,7 +1188,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
982
1188
  await sleep(normalized.drainDeadlineMs);
983
1189
  await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
984
1190
  }
985
- const old = readdirSync(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join2(releases, entry.name)).sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs).slice(normalized.keepReleases);
1191
+ const old = readdirSync(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join2(releases, entry.name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs).slice(normalized.keepReleases);
986
1192
  for (const path2 of old)
987
1193
  if (path2 !== release)
988
1194
  rmSync2(path2, { recursive: true, force: true });