@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 };
@@ -747,34 +946,180 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
747
946
  };
748
947
  }
749
948
 
949
+ // src/deployment-connectivity.ts
950
+ import { createHash as createHash2 } from "node:crypto";
951
+ import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
952
+ import { dirname as dirname2 } from "node:path";
953
+ var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
954
+ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
955
+ var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
956
+ var NAME2 = /^[a-z][a-z0-9._-]{0,99}$/;
957
+ var idFor = (key) => createHash2("sha256").update(key).digest("hex").slice(0, 20);
958
+ var checked = async (host, argv2, label) => {
959
+ const result = await host.exec(argv2);
960
+ if (result.exitCode !== 0)
961
+ throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
962
+ return result.output.trim();
963
+ };
964
+ async function sealWithSystemd(name, path2, value) {
965
+ mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
966
+ const next = `${path2}.next`;
967
+ const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
968
+ stdin: "pipe",
969
+ stdout: "pipe",
970
+ stderr: "pipe",
971
+ env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
972
+ });
973
+ child.stdin.write(value);
974
+ child.stdin.end();
975
+ const [stdout, stderr, exitCode] = await Promise.all([
976
+ new Response(child.stdout).text(),
977
+ new Response(child.stderr).text(),
978
+ child.exited
979
+ ]);
980
+ if (exitCode !== 0)
981
+ throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
982
+ renameSync2(next, path2);
983
+ }
984
+ var defaultHost = {
985
+ seal: sealWithSystemd,
986
+ write(path2, content, mode) {
987
+ mkdirSync2(dirname2(path2), { recursive: true, mode: 493 });
988
+ const next = `${path2}.next`;
989
+ writeFileSync2(next, content, { mode });
990
+ renameSync2(next, path2);
991
+ },
992
+ async exec(argv2) {
993
+ const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
994
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
995
+ return { exitCode, output: `${stdout}${stderr}` };
996
+ },
997
+ sleep: (ms) => Bun.sleep(ms)
998
+ };
999
+ function validate(request) {
1000
+ if (!request.key || request.key.length > 256)
1001
+ throw new Error("deployment connectivity key is invalid");
1002
+ const publicIntent = request.intent.public;
1003
+ const privateIntent = request.intent.private;
1004
+ if (publicIntent?.mode === "cloudflare-tunnel") {
1005
+ if (!NAME2.test(publicIntent.credential) || !HOSTNAME.test(publicIntent.hostname) || !NAME2.test(publicIntent.tunnelKey) || publicIntent.service.protocol !== "http" || !Number.isInteger(publicIntent.service.port) || publicIntent.service.port < 1 || publicIntent.service.port > 65535 || !request.capabilities.public || !UUID.test(request.capabilities.public.tunnelId) || !TOKEN.test(request.capabilities.public.connectorToken)) {
1006
+ throw new Error("public deployment connectivity is malformed");
1007
+ }
1008
+ } else if (request.capabilities.public)
1009
+ throw new Error("unexpected public deployment capability");
1010
+ if (privateIntent?.mode === "cloudflare-warp") {
1011
+ if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
1012
+ throw new Error("private deployment connectivity is malformed");
1013
+ }
1014
+ } else if (request.capabilities.private)
1015
+ throw new Error("unexpected private deployment capability");
1016
+ }
1017
+ async function applyDeploymentConnectivity(request, host = defaultHost) {
1018
+ validate(request);
1019
+ const id = idFor(request.key);
1020
+ const evidence = { key: request.key };
1021
+ if (request.intent.public?.mode === "cloudflare-tunnel") {
1022
+ const capability = request.capabilities.public;
1023
+ const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
1024
+ const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
1025
+ const unit = `forgezero-cloudflared-${id}.service`;
1026
+ const unitPath = `/etc/systemd/system/${unit}`;
1027
+ const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
1028
+ await host.seal(credentialName, credentialPath, capability.connectorToken);
1029
+ host.write(unitPath, `[Unit]
1030
+ Description=ForgeZero deployment Tunnel ${id}
1031
+ After=network-online.target
1032
+ Wants=network-online.target
1033
+
1034
+ [Service]
1035
+ Type=simple
1036
+ DynamicUser=yes
1037
+ LoadCredentialEncrypted=${credentialName}:${credentialPath}
1038
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
1039
+ Restart=always
1040
+ RestartSec=5
1041
+ NoNewPrivileges=true
1042
+ PrivateTmp=true
1043
+ ProtectSystem=strict
1044
+ ProtectHome=true
1045
+
1046
+ [Install]
1047
+ WantedBy=multi-user.target
1048
+ `, 420);
1049
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
1050
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
1051
+ await checked(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
1052
+ evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
1053
+ }
1054
+ if (request.intent.private?.mode === "cloudflare-warp") {
1055
+ const capability = request.capabilities.private;
1056
+ const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
1057
+ const unit = "forgezero-deployment-mesh.service";
1058
+ await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
1059
+ host.write(`/etc/systemd/system/${unit}`, `[Unit]
1060
+ Description=ForgeZero deployment Mesh/WARP connector
1061
+ Requires=warp-svc.service
1062
+ After=network-online.target warp-svc.service
1063
+ Wants=network-online.target
1064
+
1065
+ [Service]
1066
+ Type=oneshot
1067
+ RemainAfterExit=yes
1068
+ LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
1069
+ ExecStart=/usr/local/bin/fz-agent mesh-config
1070
+ NoNewPrivileges=true
1071
+ PrivateTmp=true
1072
+ ProtectSystem=strict
1073
+ ProtectHome=true
1074
+ ReadWritePaths=/var/lib/cloudflare-warp
1075
+
1076
+ [Install]
1077
+ WantedBy=multi-user.target
1078
+ `, 420);
1079
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
1080
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
1081
+ for (let attempt = 0;attempt < 10; attempt += 1) {
1082
+ const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
1083
+ if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
1084
+ evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
1085
+ break;
1086
+ }
1087
+ await host.sleep(1000);
1088
+ }
1089
+ if (!evidence.private)
1090
+ throw new Error("WARP connector readiness failed");
1091
+ }
1092
+ return evidence;
1093
+ }
1094
+
750
1095
  // src/software-helper.ts
751
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
1096
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2 } from "node:fs";
752
1097
  import { connect, createServer } from "node:net";
753
- import { dirname as dirname3 } from "node:path";
1098
+ import { dirname as dirname5 } from "node:path";
754
1099
 
755
1100
  // src/service-supervisor.ts
756
- import { createHash as createHash2 } from "node:crypto";
757
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
758
- import { dirname, join as join2, resolve, sep } from "node:path";
1101
+ import { createHash as createHash3 } from "node:crypto";
1102
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
1103
+ import { dirname as dirname3, join as join2, resolve, sep } from "node:path";
759
1104
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
760
1105
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
761
1106
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
762
1107
  var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
763
- var idFor = (key) => createHash2("sha256").update(key).digest("hex").slice(0, 24);
1108
+ var idFor2 = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
764
1109
  var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
765
1110
  var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
766
- var defaultHost = {
1111
+ var defaultHost2 = {
767
1112
  write(path2, content, mode) {
768
- mkdirSync2(dirname(path2), { recursive: true, mode: 493 });
1113
+ mkdirSync3(dirname3(path2), { recursive: true, mode: 493 });
769
1114
  const next = `${path2}.next`;
770
- writeFileSync2(next, content, { mode });
771
- renameSync2(next, path2);
1115
+ writeFileSync3(next, content, { mode });
1116
+ renameSync3(next, path2);
772
1117
  },
773
1118
  read: (path2) => readFileSync2(path2, "utf8"),
774
1119
  exists: existsSync2,
775
1120
  list: (path2) => existsSync2(path2) ? readdirSync(path2) : [],
776
1121
  realpath: realpathSync,
777
- mkdir: (path2, mode) => mkdirSync2(path2, { recursive: true, mode }),
1122
+ mkdir: (path2, mode) => mkdirSync3(path2, { recursive: true, mode }),
778
1123
  remove: (path2) => rmSync2(path2, { force: true }),
779
1124
  async exec(argv2) {
780
1125
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
@@ -833,7 +1178,7 @@ function allocatedPorts(request, id, previous, states) {
833
1178
  if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
834
1179
  return [...previous.applicationPorts];
835
1180
  const width = allocation.to - allocation.from + 1;
836
- const start = Number.parseInt(createHash2("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
1181
+ const start = Number.parseInt(createHash3("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
837
1182
  for (let offset = 0;offset < width; offset += 1) {
838
1183
  const first = allocation.from + (start + offset) % width;
839
1184
  const candidate = Array.from({ length: required }, (_, index) => first + index);
@@ -891,12 +1236,12 @@ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
891
1236
  }
892
1237
  }
893
1238
  `;
894
- async function checked(host, argv2, code) {
1239
+ async function checked2(host, argv2, code) {
895
1240
  const result = await host.exec(argv2);
896
1241
  if (result.exitCode !== 0)
897
1242
  throw new Error(`${code}: ${result.output.slice(0, 512)}`);
898
1243
  }
899
- async function activateSupervisedService(request, host = defaultHost) {
1244
+ async function activateSupervisedService(request, host = defaultHost2) {
900
1245
  if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
901
1246
  throw new Error("SERVICE_IDENTITY_INVALID");
902
1247
  }
@@ -905,7 +1250,7 @@ async function activateSupervisedService(request, host = defaultHost) {
905
1250
  if (!within(root, release))
906
1251
  throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
907
1252
  request = { ...request, root, release, service: validateDeploymentService(request.service) };
908
- const id = idFor(request.key);
1253
+ const id = idFor2(request.key);
909
1254
  host.mkdir(SERVICE_STATE_DIRECTORY, 448);
910
1255
  host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
911
1256
  const previous = readState(host, statePath(id));
@@ -916,11 +1261,11 @@ async function activateSupervisedService(request, host = defaultHost) {
916
1261
  const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
917
1262
  host.write(configPath, appConfig({ ...request, release }, port), 292);
918
1263
  host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
919
- await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
1264
+ await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
920
1265
  if (request.service.strategy === "direct" && previous) {
921
1266
  await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
922
1267
  }
923
- await checked(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
1268
+ await checked2(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
924
1269
  let healthy = false;
925
1270
  for (let attempt = 0;attempt < 30; attempt += 1) {
926
1271
  const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
@@ -938,8 +1283,8 @@ async function activateSupervisedService(request, host = defaultHost) {
938
1283
  const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
939
1284
  try {
940
1285
  host.write(nginxPath, nginx(request, id, port), 420);
941
- await checked(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
942
- await checked(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
1286
+ await checked2(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
1287
+ await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
943
1288
  } catch (cause) {
944
1289
  if (oldNginx === null)
945
1290
  host.remove(nginxPath);
@@ -970,22 +1315,22 @@ async function activateSupervisedService(request, host = defaultHost) {
970
1315
  }
971
1316
 
972
1317
  // src/container-supervisor.ts
973
- import { createHash as createHash3 } from "node:crypto";
974
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
975
- import { dirname as dirname2, resolve as resolve2, sep as sep2 } from "node:path";
976
- var defaultHost2 = {
1318
+ import { createHash as createHash4 } from "node:crypto";
1319
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
1320
+ import { dirname as dirname4, resolve as resolve2, sep as sep2 } from "node:path";
1321
+ var defaultHost3 = {
977
1322
  realpath: realpathSync2,
978
1323
  exists: existsSync3,
979
1324
  read: (path2) => readFileSync3(path2, "utf8"),
980
1325
  write(path2, content, mode) {
981
- mkdirSync3(dirname2(path2), { recursive: true, mode: 493 });
1326
+ mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
982
1327
  const next = `${path2}.next`;
983
- writeFileSync3(next, content, { mode });
984
- renameSync3(next, path2);
1328
+ writeFileSync4(next, content, { mode });
1329
+ renameSync4(next, path2);
985
1330
  },
986
1331
  remove: (path2) => rmSync3(path2, { force: true }),
987
1332
  list: (path2) => existsSync3(path2) ? readdirSync2(path2) : [],
988
- mkdir: (path2, mode) => mkdirSync3(path2, { recursive: true, mode }),
1333
+ mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
989
1334
  async exec(argv2) {
990
1335
  const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
991
1336
  const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
@@ -1003,9 +1348,41 @@ var defaultHost2 = {
1003
1348
  now: Date.now
1004
1349
  };
1005
1350
  var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
1006
- var idFor2 = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
1351
+ var idFor3 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
1007
1352
  var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
1008
1353
  var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
1354
+ function backendFor(application) {
1355
+ const runtime = application.runtime;
1356
+ if (runtime.kind !== "container")
1357
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
1358
+ if (runtime.provider === "forgezero.docker") {
1359
+ if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
1360
+ throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
1361
+ return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
1362
+ }
1363
+ if (runtime.provider === "forgezero.containerd") {
1364
+ if ((runtime.runtimeClass ?? "runc") !== "runc")
1365
+ throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
1366
+ return {
1367
+ engine: "containerd",
1368
+ runtimeClass: "runc",
1369
+ prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
1370
+ runtimeName: "io.containerd.runc.v2"
1371
+ };
1372
+ }
1373
+ if (runtime.provider === "forgezero.kata") {
1374
+ if (runtime.runtimeClass !== "kata-qemu-snp")
1375
+ throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
1376
+ return {
1377
+ engine: "containerd",
1378
+ runtimeClass: "kata-qemu-snp",
1379
+ prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
1380
+ runtimeName: "io.containerd.kata.v2"
1381
+ };
1382
+ }
1383
+ throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
1384
+ }
1385
+ var command = (backend, ...argv2) => [...backend.prefix, ...argv2];
1009
1386
  function validateRequest(request, host) {
1010
1387
  if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
1011
1388
  throw new Error("CONTAINER_IDENTITY_INVALID");
@@ -1016,6 +1393,7 @@ function validateRequest(request, host) {
1016
1393
  const application = request.application;
1017
1394
  if (application.kind !== "application" || application.runtime.kind !== "container")
1018
1395
  throw new Error("CONTAINER_RUNTIME_REQUIRED");
1396
+ backendFor(application);
1019
1397
  if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
1020
1398
  throw new Error("CONTAINER_LIMITS_REQUIRED");
1021
1399
  if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
@@ -1026,23 +1404,24 @@ function validateRequest(request, host) {
1026
1404
  throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
1027
1405
  return { ...request, root, release };
1028
1406
  }
1029
- async function checked2(host, argv2, code) {
1407
+ async function checked3(host, argv2, code) {
1030
1408
  const result = await host.exec(argv2);
1031
1409
  if (result.exitCode !== 0)
1032
1410
  throw new Error(`${code}: ${result.output.slice(0, 512)}`);
1033
1411
  return result.output.trim();
1034
1412
  }
1035
- async function buildContainerImage(requestValue, host = defaultHost2) {
1413
+ async function buildContainerImage(requestValue, host = defaultHost3) {
1036
1414
  const request = validateRequest(requestValue, host);
1037
1415
  const runtime = request.application.runtime;
1038
1416
  if (runtime.kind !== "container")
1039
1417
  throw new Error("CONTAINER_RUNTIME_REQUIRED");
1418
+ const backend = backendFor(request.application);
1040
1419
  const source = runtime.image.source;
1041
1420
  if (source.kind === "registry") {
1042
1421
  if (!source.reference.includes("@sha256:"))
1043
1422
  throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
1044
- await checked2(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
1045
- const digest2 = await checked2(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
1423
+ await checked3(host, command(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
1424
+ const digest2 = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
1046
1425
  if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
1047
1426
  throw new Error("CONTAINER_DIGEST_INVALID");
1048
1427
  return { image: source.reference, digest: digest2 };
@@ -1051,23 +1430,9 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
1051
1430
  const dockerfile = host.realpath(resolve2(request.release, source.dockerfile));
1052
1431
  if (!within2(request.release, context) || !within2(context, dockerfile))
1053
1432
  throw new Error("CONTAINER_BUILD_PATH_INVALID");
1054
- const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
1055
- await checked2(host, [
1056
- "/usr/bin/docker",
1057
- "build",
1058
- "--pull",
1059
- ...request.noCache ? ["--no-cache"] : [],
1060
- "--file",
1061
- dockerfile,
1062
- "--tag",
1063
- tag,
1064
- "--label",
1065
- `net.forgezero.revision=${request.revision}`,
1066
- "--label",
1067
- `net.forgezero.component=${request.component}`,
1068
- context
1069
- ], "CONTAINER_BUILD_FAILED");
1070
- const digest = await checked2(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
1433
+ const tag = `forgezero/${idFor3(request.key)}:${request.revision}`;
1434
+ await checked3(host, command(backend, "build", "--pull", ...request.noCache ? ["--no-cache"] : [], "--file", dockerfile, "--tag", tag, "--label", `net.forgezero.revision=${request.revision}`, "--label", `net.forgezero.component=${request.component}`, context), "CONTAINER_BUILD_FAILED");
1435
+ const digest = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
1071
1436
  if (!/^sha256:[a-f0-9]{64}$/.test(digest))
1072
1437
  throw new Error("CONTAINER_DIGEST_INVALID");
1073
1438
  return { image: tag, digest };
@@ -1135,7 +1500,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
1135
1500
  }
1136
1501
  `;
1137
1502
  }
1138
- function runArgv(request, id, slot, port, networkName) {
1503
+ function runArgv(request, backend, id, slot, port, networkName) {
1139
1504
  const { application } = request;
1140
1505
  const resources = application.resources;
1141
1506
  const runtime = application.runtime;
@@ -1143,35 +1508,7 @@ function runArgv(request, id, slot, port, networkName) {
1143
1508
  throw new Error("CONTAINER_RUNTIME_REQUIRED");
1144
1509
  const memory = resources.memory;
1145
1510
  const network = application.network.container;
1146
- const argv2 = [
1147
- "/usr/bin/docker",
1148
- "run",
1149
- "--detach",
1150
- "--name",
1151
- nameFor(id, slot),
1152
- "--restart",
1153
- "unless-stopped",
1154
- "--label",
1155
- `net.forgezero.key=${request.key}`,
1156
- "--label",
1157
- `net.forgezero.revision=${request.revision}`,
1158
- "--cpus",
1159
- String(resources.cpu.limit),
1160
- "--cpu-shares",
1161
- String(resources.cpu.weight ?? 100),
1162
- "--memory",
1163
- `${memory.limitMiB}m`,
1164
- ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
1165
- ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
1166
- "--pids-limit",
1167
- String(resources.pids.limit),
1168
- "--security-opt",
1169
- "no-new-privileges=true",
1170
- "--cap-drop",
1171
- "ALL",
1172
- ...runtime.security.root === "read-only" ? ["--read-only"] : [],
1173
- ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
1174
- ];
1511
+ const argv2 = command(backend, "run", "--detach", "--name", nameFor(id, slot), "--restart", "unless-stopped", "--label", `net.forgezero.key=${request.key}`, "--label", `net.forgezero.revision=${request.revision}`, ...backend.runtimeName ? ["--runtime", backend.runtimeName] : [], "--cpus", String(resources.cpu.limit), "--cpu-shares", String(resources.cpu.weight ?? 100), "--memory", `${memory.limitMiB}m`, ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [], ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [], "--pids-limit", String(resources.pids.limit), "--security-opt", "no-new-privileges=true", "--cap-drop", "ALL", ...runtime.security.root === "read-only" ? ["--read-only"] : [], ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]);
1175
1512
  for (const mount of application.storage ?? []) {
1176
1513
  if (mount.class === "ephemeral")
1177
1514
  argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
@@ -1181,11 +1518,12 @@ function runArgv(request, id, slot, port, networkName) {
1181
1518
  argv2.push(request.image, ...runtime.entrypoint ?? []);
1182
1519
  return argv2;
1183
1520
  }
1184
- async function activateContainer(requestValue, host = defaultHost2) {
1521
+ async function activateContainer(requestValue, host = defaultHost3) {
1185
1522
  const request = validateRequest(requestValue, host);
1186
1523
  if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
1187
1524
  throw new Error("CONTAINER_IMAGE_INVALID");
1188
- const id = idFor2(request.key);
1525
+ const backend = backendFor(request.application);
1526
+ const id = idFor3(request.key);
1189
1527
  host.mkdir(SERVICE_STATE_DIRECTORY, 448);
1190
1528
  host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
1191
1529
  const previous = readState2(host, id);
@@ -1197,9 +1535,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
1197
1535
  if (containerNetwork.mode === "bridge") {
1198
1536
  networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
1199
1537
  if (networkName !== "bridge") {
1200
- const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
1538
+ const inspected = await host.exec(command(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
1201
1539
  if (inspected.exitCode !== 0) {
1202
- await checked2(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
1540
+ await checked3(host, command(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
1203
1541
  } else if (inspected.output.trim() !== id)
1204
1542
  throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
1205
1543
  }
@@ -1208,8 +1546,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
1208
1546
  const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
1209
1547
  const port = ports[nextSlot];
1210
1548
  const name = nameFor(id, nextSlot);
1211
- await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1212
- await checked2(host, runArgv(request, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
1549
+ await host.exec(command(backend, "rm", "--force", name));
1550
+ await checked3(host, runArgv(request, backend, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
1213
1551
  let healthy = false;
1214
1552
  const health = request.application.service.health;
1215
1553
  for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
@@ -1220,15 +1558,28 @@ async function activateContainer(requestValue, host = defaultHost2) {
1220
1558
  await host.sleep(health.intervalMs ?? 1000);
1221
1559
  }
1222
1560
  if (!healthy) {
1223
- await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1561
+ await host.exec(command(backend, "rm", "--force", name));
1224
1562
  throw new Error("CONTAINER_HEALTH_FAILED");
1225
1563
  }
1564
+ let runtimeEvidence;
1565
+ if (backend.engine === "containerd") {
1566
+ const raw = await checked3(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
1567
+ let observed = "";
1568
+ try {
1569
+ observed = String(JSON.parse(raw).Runtime?.Name ?? "");
1570
+ } catch {
1571
+ throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
1572
+ }
1573
+ if (observed !== backend.runtimeName)
1574
+ throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
1575
+ runtimeEvidence = observed;
1576
+ }
1226
1577
  const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
1227
1578
  const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
1228
1579
  host.write(nginxPath, nginx2(request, id, port), 420);
1229
1580
  try {
1230
- await checked2(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
1231
- await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
1581
+ await checked3(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
1582
+ await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
1232
1583
  } catch (cause) {
1233
1584
  if (previousNginx === null)
1234
1585
  host.remove(nginxPath);
@@ -1236,7 +1587,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
1236
1587
  host.write(nginxPath, previousNginx, 420);
1237
1588
  await host.exec(["/usr/sbin/nginx", "-t"]);
1238
1589
  await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
1239
- await host.exec(["/usr/bin/docker", "rm", "--force", name]);
1590
+ await host.exec(command(backend, "rm", "--force", name));
1240
1591
  throw cause;
1241
1592
  }
1242
1593
  const state = {
@@ -1249,13 +1600,18 @@ async function activateContainer(requestValue, host = defaultHost2) {
1249
1600
  applicationPorts: ports,
1250
1601
  activeSlot: nextSlot,
1251
1602
  healthPath: health.path,
1603
+ containerRuntime: {
1604
+ engine: backend.engine,
1605
+ runtimeClass: backend.runtimeClass,
1606
+ ...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
1607
+ },
1252
1608
  updatedAtTs: host.now()
1253
1609
  };
1254
1610
  host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
1255
1611
  `, 384);
1256
1612
  if (previous && previous.activeSlot !== nextSlot) {
1257
1613
  await host.sleep(request.application.rollout.drainMs ?? 30000);
1258
- await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id, previous.activeSlot)]);
1614
+ await host.exec(command(backend, "rm", "--force", nameFor(id, previous.activeSlot)));
1259
1615
  }
1260
1616
  return state;
1261
1617
  }
@@ -1270,11 +1626,12 @@ function startSoftwareHelper(options = {}) {
1270
1626
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
1271
1627
  if (existsSync4(socketPath))
1272
1628
  unlinkSync2(socketPath);
1273
- mkdirSync4(dirname3(socketPath), { recursive: true, mode: 488 });
1629
+ mkdirSync5(dirname5(socketPath), { recursive: true, mode: 488 });
1274
1630
  const ensure = options.ensure ?? ensureSoftwareRequirements;
1275
1631
  const activate = options.activate ?? activateSupervisedService;
1276
1632
  const buildContainer = options.buildContainer ?? buildContainerImage;
1277
1633
  const activateTypedContainer = options.activateContainer ?? activateContainer;
1634
+ const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
1278
1635
  let tail = Promise.resolve();
1279
1636
  let pending = 0;
1280
1637
  const server = createServer((socket) => {
@@ -1331,6 +1688,12 @@ function startSoftwareHelper(options = {}) {
1331
1688
  throw new Error("container deployment root is not owned by this helper");
1332
1689
  const state = await activateTypedContainer(containerRequest);
1333
1690
  socket.end(`${JSON.stringify({ ok: true, state })}
1691
+ `);
1692
+ return;
1693
+ }
1694
+ if (request.op === "apply-connectivity" && request.request) {
1695
+ const result = await applyConnectivity(request.request);
1696
+ socket.end(`${JSON.stringify({ ok: true, result })}
1334
1697
  `);
1335
1698
  return;
1336
1699
  }
@@ -1388,6 +1751,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
1388
1751
  function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
1389
1752
  return requestContainer("activate-container", request, socketPath, timeoutMs);
1390
1753
  }
1754
+ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
1755
+ return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
1756
+ }
1391
1757
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
1392
1758
  return new Promise((resolve3, reject) => {
1393
1759
  const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
@@ -1449,6 +1815,7 @@ export {
1449
1815
  startSoftwareHelper,
1450
1816
  requestSoftware,
1451
1817
  requestServiceActivation,
1818
+ requestDeploymentConnectivity,
1452
1819
  requestContainerBuild,
1453
1820
  requestContainerActivation,
1454
1821
  SOFTWARE_HELPER_UNIT_PATH,