@forgezero/agent 0.1.74 → 0.1.76

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 (43) hide show
  1. package/README.md +126 -17
  2. package/dist/agent-heartbeat.d.ts +25 -1
  3. package/dist/agent-heartbeat.js +45 -5
  4. package/dist/bootstrap.js +10 -7
  5. package/dist/community-rehearsal-host.js +218 -13
  6. package/dist/credential-schema.d.ts +1 -1
  7. package/dist/credential-schema.js +1 -1
  8. package/dist/definition.js +206 -7
  9. package/dist/deploy-actions.d.ts +7 -0
  10. package/dist/deploy-compiler.js +191 -10
  11. package/dist/deploy-file.js +206 -7
  12. package/dist/deploy-plan-runner.d.ts +3 -0
  13. package/dist/deploy-plan-runner.js +190 -9
  14. package/dist/deploy-plan.js +187 -8
  15. package/dist/deploy-providers.d.ts +19 -0
  16. package/dist/deploy.d.ts +89 -3
  17. package/dist/deploy.js +49 -2
  18. package/dist/deployment-connectivity.d.ts +63 -0
  19. package/dist/deployment-connectivity.js +155 -0
  20. package/dist/deployment-pull.d.ts +2 -0
  21. package/dist/deployment-targets.d.ts +9 -0
  22. package/dist/deployment-targets.js +141 -0
  23. package/dist/deployment.d.ts +69 -0
  24. package/dist/fz-agent.js +1122 -313
  25. package/dist/fz.js +234 -26
  26. package/dist/index.d.ts +1 -0
  27. package/dist/metal-bootstrap.js +1 -1
  28. package/dist/metal-helper-socket.js +210 -11
  29. package/dist/metal-provision.js +210 -11
  30. package/dist/operator-bootstrap.js +11 -9
  31. package/dist/platform-bootstrap-runtime.js +209 -10
  32. package/dist/platform-fleet-verification.js +525 -155
  33. package/dist/platform-genesis.js +206 -7
  34. package/dist/process-input.d.ts +11 -0
  35. package/dist/provision.js +472 -99
  36. package/dist/service-supervisor.d.ts +6 -0
  37. package/dist/software-helper.d.ts +3 -0
  38. package/dist/software-helper.js +472 -98
  39. package/dist/software.d.ts +4 -1
  40. package/dist/software.js +210 -8
  41. package/dist/ubuntu.js +206 -7
  42. package/dist/version.d.ts +1 -1
  43. 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.74";
1419
+ var VERSION = "0.1.76";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1549,6 +1549,12 @@ var defaultHost2 = {
1549
1549
  now: Date.now
1550
1550
  };
1551
1551
 
1552
+ // src/process-input.ts
1553
+ async function writeAndCloseProcessInput(input, value) {
1554
+ input.write(value);
1555
+ await input.end();
1556
+ }
1557
+
1552
1558
  // src/software-helper.ts
1553
1559
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
1554
1560
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
@@ -2575,10 +2581,8 @@ var fixed = async (argv, stdin) => {
2575
2581
  stderr: "pipe",
2576
2582
  env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C", DEBIAN_FRONTEND: "noninteractive" }
2577
2583
  });
2578
- if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
2579
- child.stdin.write(stdin);
2580
- child.stdin.end();
2581
- }
2584
+ if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
2585
+ await writeAndCloseProcessInput(child.stdin, stdin);
2582
2586
  const [stdout, stderr, exitCode] = await Promise.all([
2583
2587
  new Response(child.stdout).text(),
2584
2588
  new Response(child.stderr).text(),
@@ -4874,8 +4878,7 @@ function localBootstrapHost() {
4874
4878
  const execute = async (argv, options = {}) => {
4875
4879
  const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
4876
4880
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
4877
- child.stdin.write(options.stdin);
4878
- child.stdin.end();
4881
+ await writeAndCloseProcessInput(child.stdin, options.stdin);
4879
4882
  }
4880
4883
  const [stdout, stderr, exitCode] = await Promise.all([
4881
4884
  new Response(child.stdout).text(),
@@ -6351,8 +6354,7 @@ var defaultExec3 = async (argv, options = {}) => {
6351
6354
  }
6352
6355
  });
6353
6356
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
6354
- child.stdin.write(options.stdin);
6355
- child.stdin.end();
6357
+ await writeAndCloseProcessInput(child.stdin, options.stdin);
6356
6358
  }
6357
6359
  const [stdout, stderr, exitCode] = await Promise.all([
6358
6360
  new Response(child.stdout).text(),