@forgezero/agent 0.1.64 → 0.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,34 +138,368 @@ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
138
138
  }
139
139
  var tapFor = (guestIndex) => `tap${guestIndex}`;
140
140
 
141
+ // src/software.ts
142
+ import { createHash } from "node:crypto";
143
+ import {
144
+ accessSync,
145
+ chmodSync,
146
+ copyFileSync,
147
+ existsSync,
148
+ mkdtempSync,
149
+ mkdirSync,
150
+ readFileSync,
151
+ renameSync,
152
+ rmSync,
153
+ symlinkSync,
154
+ unlinkSync,
155
+ writeFileSync
156
+ } from "node:fs";
157
+ import { tmpdir } from "node:os";
158
+ import { join } from "node:path";
159
+ var PINNED_BUN_VERSION = "1.3.14";
160
+ var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
161
+ var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
162
+ var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
163
+ var OS_CATALOG = [
164
+ {
165
+ id: "ubuntu",
166
+ version: "26.04",
167
+ architecture: "x64",
168
+ status: "active",
169
+ imageKey: "ubuntu-resolute-20260731",
170
+ imageLabel: "Ubuntu 26.04 LTS Resolute",
171
+ imageVersion: "2026-07-31",
172
+ imageUrl: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
173
+ imageSha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05",
174
+ confidentialModes: ["sev-snp"]
175
+ },
176
+ {
177
+ id: "ubuntu",
178
+ version: "24.04",
179
+ architecture: "x64",
180
+ status: "active",
181
+ imageKey: "ubuntu-noble-20260705",
182
+ imageLabel: "Ubuntu 24.04 LTS Noble",
183
+ imageVersion: "2026-07-05",
184
+ imageUrl: "https://cloud-images.ubuntu.com/releases/noble/release-20260705/ubuntu-24.04-server-cloudimg-amd64.img",
185
+ imageSha256: "ffe6203da54deeb6db5d2a98a83f9ec8e55f149d3f7ba622e1abe5fa966ee3d6",
186
+ confidentialModes: []
187
+ }
188
+ ];
189
+ var SOFTWARE_CATALOG = [
190
+ { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
191
+ { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
192
+ { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
193
+ { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
194
+ { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
195
+ { id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
196
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
197
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
198
+ { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
199
+ { id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
200
+ { id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
201
+ { id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
202
+ { id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
203
+ { id: "git", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
204
+ ];
205
+ var UBUNTU_2604_X64 = [
206
+ { requirement: { id: "bun", version: "1.3.14" } },
207
+ { requirement: { id: "docker", version: "ubuntu-26.04" } },
208
+ { requirement: { id: "nginx", version: "ubuntu-26.04" } },
209
+ { requirement: { id: "arangodb", version: "3.11.14" } },
210
+ { requirement: { id: "cloudflared", version: "2026.7.3" } },
211
+ { requirement: { id: "cloudflare-warp", version: "2026.6.822.0-min" } },
212
+ { requirement: { id: "ufw", version: "ubuntu-26.04" } },
213
+ { requirement: { id: "openssh-client", version: "ubuntu-26.04" } },
214
+ { requirement: { id: "git", version: "ubuntu-26.04" } }
215
+ ];
216
+ var UBUNTU_2404_X64 = [
217
+ { requirement: { id: "bun", version: "1.3.14" } },
218
+ { requirement: { id: "docker", version: "ubuntu-24.04" } },
219
+ { requirement: { id: "nginx", version: "ubuntu-24.04" } },
220
+ { requirement: { id: "ufw", version: "ubuntu-24.04" } },
221
+ { requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
222
+ { requirement: { id: "git", version: "ubuntu-24.04" } }
223
+ ];
224
+ var strategiesFor = (observation) => observation.os.id === "ubuntu" && observation.architecture === "x64" ? observation.os.versionId === "26.04" ? UBUNTU_2604_X64 : observation.os.versionId === "24.04" ? UBUNTU_2404_X64 : undefined : undefined;
225
+ var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
226
+ var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
227
+ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
228
+ "data-root": "/var/lib/docker",
229
+ "storage-driver": "overlay2",
230
+ "live-restore": true,
231
+ "log-driver": "local",
232
+ "log-opts": { "max-size": "10m", "max-file": "3" }
233
+ }, null, 2)}
234
+ `;
235
+ var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
236
+ var runSoftwareCommand = async (argv, env = {}) => {
237
+ let child;
238
+ try {
239
+ child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: path, LANG: "C", LC_ALL: "C", ...env } });
240
+ } catch (cause) {
241
+ const failure = softwareSpawnFailure(cause, argv);
242
+ if (failure)
243
+ return failure;
244
+ throw cause;
245
+ }
246
+ const [stdout, stderr, exitCode] = await Promise.all([
247
+ new Response(child.stdout).text(),
248
+ new Response(child.stderr).text(),
249
+ child.exited
250
+ ]);
251
+ return { exitCode, output: `${stdout}${stderr}` };
252
+ };
253
+ var run = runSoftwareCommand;
254
+ var download = async (url, destination, sha256) => {
255
+ const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
256
+ if (!response.ok)
257
+ 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)
260
+ throw new Error("download checksum mismatch");
261
+ writeFileSync(destination, bytes, { mode: 384, flag: "wx" });
262
+ };
263
+ var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
264
+ var aptInstall = async (name) => {
265
+ const environment = { DEBIAN_FRONTEND: "noninteractive" };
266
+ const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
267
+ return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
268
+ };
269
+ async function executeSoftwareOperation(operation) {
270
+ const { software, version } = operation;
271
+ if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
272
+ return { exitCode: 2, output: "unsupported software operation" };
273
+ }
274
+ if (operation.kind === "check") {
275
+ if (software === "bun")
276
+ return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
277
+ if (software === "docker") {
278
+ const binary = await run(["/usr/bin/docker", "--version"]);
279
+ if (!successful(binary, /Docker version/))
280
+ return { ...binary, exitCode: 1 };
281
+ if (!existsSync(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
282
+ return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
283
+ const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
284
+ return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
285
+ }
286
+ if (software === "nginx") {
287
+ const binary = await run(["/usr/sbin/nginx", "-v"]);
288
+ return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
289
+ }
290
+ if (software === "arangodb") {
291
+ const binary = await run(["/usr/sbin/arangod", "--version"]);
292
+ if (!successful(binary, /3\.11\.14/))
293
+ return { ...binary, exitCode: 1 };
294
+ const [active, enabled] = await Promise.all([
295
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "arangodb3.service"]),
296
+ run(["/usr/bin/systemctl", "is-enabled", "--quiet", "arangodb3.service"])
297
+ ]);
298
+ return active.exitCode !== 0 && enabled.exitCode !== 0 ? { exitCode: 0, output: binary.output } : { exitCode: 1, output: "vendor standalone unit remains active or enabled" };
299
+ }
300
+ if (software === "cloudflared")
301
+ return run(["/usr/local/bin/cloudflared", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /2026\.7\.3/) ? 0 : 1 }));
302
+ if (software === "cloudflare-warp")
303
+ return run(["/usr/bin/warp-cli", "--version"]).then((result) => {
304
+ const match = result.output.match(/(\d{4})\.(\d+)\.(\d+)\.(\d+)/);
305
+ const observed = match?.slice(1).map(Number);
306
+ const minimum = [2026, 6, 822, 0];
307
+ const supported = observed && observed.some((part, index) => part > minimum[index] && observed.slice(0, index).every((prior, priorIndex) => prior === minimum[priorIndex])) || observed?.every((part, index) => part === minimum[index]);
308
+ return { ...result, exitCode: result.exitCode === 0 && supported ? 0 : 1 };
309
+ });
310
+ const binaries = software === "ufw" ? ["/usr/sbin/ufw"] : software === "git" ? ["/usr/bin/git"] : ["/usr/bin/ssh", "/usr/bin/scp", "/usr/bin/ssh-keyscan", "/usr/bin/ssh-keygen"];
311
+ try {
312
+ binaries.forEach((binary) => accessSync(binary));
313
+ return { exitCode: 0, output: "" };
314
+ } catch (cause) {
315
+ return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
316
+ }
317
+ }
318
+ if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client" || software === "git") {
319
+ const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
320
+ const installed = await aptInstall(packageName);
321
+ if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
322
+ return installed;
323
+ if (software === "docker") {
324
+ mkdirSync("/etc/docker", { recursive: true, mode: 493 });
325
+ if (existsSync(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
326
+ return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
327
+ }
328
+ if (!existsSync(DOCKER_DAEMON_PATH))
329
+ writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
330
+ const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
331
+ return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
332
+ }
333
+ return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
334
+ }
335
+ const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
336
+ try {
337
+ if (software === "cloudflare-warp") {
338
+ const key = join(directory, "cloudflare-warp-key.gpg");
339
+ await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
340
+ mkdirSync("/usr/share/keyrings", { recursive: true, mode: 493 });
341
+ const dearmored = await run([
342
+ "/usr/bin/gpg",
343
+ "--batch",
344
+ "--yes",
345
+ "--dearmor",
346
+ "-o",
347
+ "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg",
348
+ key
349
+ ]);
350
+ if (dearmored.exitCode !== 0)
351
+ return dearmored;
352
+ mkdirSync("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
353
+ writeFileSync("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ resolute main
354
+ `, { mode: 420 });
355
+ return aptInstall("cloudflare-warp");
356
+ }
357
+ if (software === "bun") {
358
+ const archive = join(directory, "bun.zip");
359
+ await download("https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip", archive, BUN_RELEASE_SHA256);
360
+ const unpacked = join(directory, "unpacked");
361
+ mkdirSync(unpacked, { mode: 448 });
362
+ const unzipped = await run(["/usr/bin/unzip", "-q", archive, "-d", unpacked]);
363
+ if (unzipped.exitCode !== 0)
364
+ return unzipped;
365
+ mkdirSync("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
366
+ copyFileSync(join(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
367
+ chmodSync("/usr/local/lib/forgezero/runtime/bun.next", 493);
368
+ renameSync("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
369
+ try {
370
+ unlinkSync("/usr/local/bin/bun");
371
+ } catch {}
372
+ symlinkSync("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
373
+ return { exitCode: 0, output: "" };
374
+ }
375
+ if (software === "cloudflared") {
376
+ const binary = join(directory, "cloudflared");
377
+ await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
378
+ chmodSync(binary, 493);
379
+ copyFileSync(binary, "/usr/local/bin/cloudflared");
380
+ chmodSync("/usr/local/bin/cloudflared", 493);
381
+ return { exitCode: 0, output: "" };
382
+ }
383
+ const deb = join(directory, "arangodb.deb");
384
+ await download("https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb", deb, ARANGO_SHA256);
385
+ let installed = await run(["/usr/bin/dpkg", "-i", deb], { DEBIAN_FRONTEND: "noninteractive" });
386
+ if (installed.exitCode !== 0)
387
+ installed = await run(["/usr/bin/apt-get", "-y", "-f", "install"], { DEBIAN_FRONTEND: "noninteractive" });
388
+ if (installed.exitCode !== 0)
389
+ return installed;
390
+ await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
391
+ return { exitCode: 0, output: "" };
392
+ } finally {
393
+ rmSync(directory, { recursive: true, force: true });
394
+ }
395
+ }
396
+ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
397
+ const values = Object.fromEntries(osRelease.split(`
398
+ `).flatMap((line) => {
399
+ const separator = line.indexOf("=");
400
+ return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
401
+ }));
402
+ return {
403
+ os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
404
+ architecture
405
+ };
406
+ }
407
+ function validateSoftwareRequirements(value, _options = {}) {
408
+ if (!Array.isArray(value) || value.length > 32)
409
+ throw new Error("software requirements must be an array of at most 32 entries");
410
+ const seen = new Set;
411
+ return value.map((item) => {
412
+ if (!item || typeof item !== "object" || Array.isArray(item))
413
+ throw new Error("software requirement must be an object");
414
+ const row = item;
415
+ if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
416
+ throw new Error("software requirement contains an unknown field");
417
+ }
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)) {
419
+ throw new Error("software requirement coordinate is invalid");
420
+ }
421
+ const requirement = { id: row.id, version: row.version };
422
+ if (seen.has(requirement.id))
423
+ throw new Error(`duplicate software requirement: ${requirement.id}`);
424
+ seen.add(requirement.id);
425
+ const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
426
+ if (!catalog || catalog.status !== "active") {
427
+ throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
428
+ }
429
+ return requirement;
430
+ });
431
+ }
432
+ async function ensureSoftwareRequirements(requirementsInput, options) {
433
+ const requirements = validateSoftwareRequirements(requirementsInput);
434
+ const observation = options.observation ?? observeSoftwareHost();
435
+ const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
436
+ if (!os || os.status !== "active") {
437
+ throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
438
+ }
439
+ const results = [];
440
+ for (const requirement of requirements) {
441
+ const strategy = strategiesFor(observation)?.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
442
+ if (!strategy)
443
+ throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
444
+ const before = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
445
+ if (before.exitCode === 0) {
446
+ results.push({ ...requirement, changed: false });
447
+ continue;
448
+ }
449
+ const installed = await options.exec({ kind: "install", software: requirement.id, version: requirement.version });
450
+ if (installed.exitCode !== 0)
451
+ throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
452
+ const after = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
453
+ if (after.exitCode !== 0)
454
+ throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
455
+ results.push({ ...requirement, changed: true });
456
+ }
457
+ return results;
458
+ }
459
+
141
460
  // src/ubuntu.ts
461
+ var current = OS_CATALOG[0];
142
462
  var SUPPORTED_GUEST_IMAGE = Object.freeze({
143
- key: "ubuntu-resolute-20260731",
144
- family: "ubuntu-26.04",
145
- version: "2026-07-31",
146
- label: "Ubuntu 26.04 LTS Resolute",
147
- url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
148
- sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
463
+ key: current.imageKey,
464
+ family: `${current.id}-${current.version}`,
465
+ version: current.imageVersion,
466
+ label: current.imageLabel,
467
+ url: current.imageUrl,
468
+ sha256: current.imageSha256
149
469
  });
150
- function assertSupportedGuestImage(imageKey) {
151
- if (imageKey !== SUPPORTED_GUEST_IMAGE.key) {
152
- throw new Error(`unsupported guest image ${imageKey}; ForgeZero currently supports only ${SUPPORTED_GUEST_IMAGE.key}`);
470
+ var SUPPORTED_GUEST_IMAGES = Object.freeze(OS_CATALOG.filter(({ status }) => status === "active").map(({ id, imageKey, imageLabel, imageVersion, version, confidentialModes }) => ({
471
+ key: imageKey,
472
+ label: imageLabel,
473
+ family: `${id}-${version}`,
474
+ version: imageVersion,
475
+ confidential: confidentialModes.some((mode) => mode === "sev-snp")
476
+ })));
477
+ function supportedGuestImage(imageKey) {
478
+ return SUPPORTED_GUEST_IMAGES.find(({ key }) => key === imageKey);
479
+ }
480
+ function assertSupportedGuestImage(imageKey, confidential = false) {
481
+ const selected = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
482
+ if (!selected) {
483
+ throw new Error(`unsupported guest image ${imageKey}; supported images: ${SUPPORTED_GUEST_IMAGES.map(({ key }) => key).join(", ")}`);
484
+ }
485
+ if (confidential && !selected.confidentialModes.some((mode) => mode === "sev-snp")) {
486
+ throw new Error(`guest image ${imageKey} is not approved for SEV-SNP`);
153
487
  }
154
488
  }
155
489
 
156
490
  // src/metal-provision.ts
157
- import { createHash } from "node:crypto";
491
+ import { createHash as createHash2 } from "node:crypto";
158
492
  import {
159
- existsSync,
160
- mkdirSync,
161
- readFileSync,
493
+ existsSync as existsSync2,
494
+ mkdirSync as mkdirSync2,
495
+ readFileSync as readFileSync2,
162
496
  readdirSync,
163
- rmSync,
497
+ rmSync as rmSync2,
164
498
  statSync,
165
- unlinkSync,
166
- writeFileSync
499
+ unlinkSync as unlinkSync2,
500
+ writeFileSync as writeFileSync2
167
501
  } from "node:fs";
168
- import { dirname, isAbsolute, join } from "node:path";
502
+ import { dirname, isAbsolute, join as join2 } from "node:path";
169
503
  import { isIP } from "node:net";
170
504
  var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
171
505
  var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
@@ -193,8 +527,8 @@ function membersOfLinuxList(value, label) {
193
527
  throw new MetalProvisionError(`${label} list overlaps itself`);
194
528
  return members;
195
529
  }
196
- var guestNameFor = (computeKey) => `fzg-${createHash("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
197
- var tapNameFor = (computeKey) => `fzt${createHash("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
530
+ var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
531
+ var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
198
532
  var macForAddress = (address) => {
199
533
  const octets = address.split(".").map(Number);
200
534
  if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
@@ -211,8 +545,8 @@ function validateMetalProfile(profile) {
211
545
  throw new MetalProvisionError("invalid subnet prefix");
212
546
  if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
213
547
  throw new MetalProvisionError("invalid guest address range");
214
- for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
215
- if (!isAbsolute(path))
548
+ for (const path2 of [profile.stateDir, profile.seedDir, profile.unitDir]) {
549
+ if (!isAbsolute(path2))
216
550
  throw new MetalProvisionError("metal paths must be absolute");
217
551
  }
218
552
  new URL(profile.apiUrl);
@@ -225,9 +559,16 @@ function validateMetalProfile(profile) {
225
559
  if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
226
560
  throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
227
561
  const imageKeys = Object.keys(profile.images);
228
- if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
229
- throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
562
+ if (imageKeys.length === 0)
563
+ throw new MetalProvisionError("metal profile requires at least one pinned guest image");
564
+ for (const imageKey of imageKeys) {
565
+ const catalog = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
566
+ if (!catalog || profile.images[imageKey]?.sha256 !== catalog.imageSha256) {
567
+ throw new MetalProvisionError(`metal profile image ${imageKey} does not match the pinned OS catalog`);
568
+ }
230
569
  }
570
+ if (profile.confidential && !imageKeys.some((key) => OS_CATALOG.find((entry) => entry.imageKey === key)?.confidentialModes.some((mode) => mode === "sev-snp")))
571
+ throw new MetalProvisionError("confidential metal profile requires a pinned SEV-SNP guest image");
231
572
  if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
232
573
  throw new MetalProvisionError("at least one exclusive CPU pool is required");
233
574
  }
@@ -274,9 +615,9 @@ function validateMetalProfile(profile) {
274
615
  }
275
616
  }
276
617
  var readManifests = (stateDir) => {
277
- if (!existsSync(stateDir))
618
+ if (!existsSync2(stateDir))
278
619
  return [];
279
- return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync(join(stateDir, name), "utf8")));
620
+ return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync2(join2(stateDir, name), "utf8")));
280
621
  };
281
622
  function allocateAddress(profile, computeKey, rows) {
282
623
  const existing = rows.find((row) => row.computeKey === computeKey);
@@ -284,7 +625,7 @@ function allocateAddress(profile, computeKey, rows) {
284
625
  return existing.address;
285
626
  const used = new Set(rows.map((row) => row.address));
286
627
  const width = profile.addressEnd - profile.addressStart + 1;
287
- const start = createHash("sha256").update(computeKey).digest().readUInt16BE(0) % width;
628
+ const start = createHash2("sha256").update(computeKey).digest().readUInt16BE(0) % width;
288
629
  for (let offset = 0;offset < width; offset += 1) {
289
630
  const last = profile.addressStart + (start + offset) % width;
290
631
  const address = `${profile.subnetPrefix}.${last}`;
@@ -334,7 +675,7 @@ function allocateCpuPool(profile, claim, rows) {
334
675
  return selected;
335
676
  }
336
677
  var base64 = (value) => Buffer.from(value).toString("base64");
337
- var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)}
678
+ var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
338
679
  permissions: '${permissions}'
339
680
  encoding: b64
340
681
  content: ${base64(content)}
@@ -446,7 +787,7 @@ var checked = async (exec, argv) => {
446
787
  };
447
788
  async function provisionMetalGuest(profile, claim, exec) {
448
789
  validateMetalProfile(profile);
449
- assertSupportedGuestImage(claim.spec.imageKey);
790
+ assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
450
791
  if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
451
792
  throw new MetalProvisionError("invalid compute claim");
452
793
  const image = profile.images[claim.spec.imageKey];
@@ -458,8 +799,8 @@ async function provisionMetalGuest(profile, claim, exec) {
458
799
  const digest = (await checked(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
459
800
  if (digest !== image.sha256)
460
801
  throw new MetalProvisionError("configured image checksum mismatch");
461
- for (const path of [profile.stateDir, profile.seedDir, profile.unitDir])
462
- mkdirSync(path, { recursive: true, mode: 448 });
802
+ for (const path2 of [profile.stateDir, profile.seedDir, profile.unitDir])
803
+ mkdirSync2(path2, { recursive: true, mode: 448 });
463
804
  const manifests = readManifests(profile.stateDir);
464
805
  if (claim.access) {
465
806
  if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(claim.access.sshUser)) {
@@ -472,7 +813,7 @@ async function provisionMetalGuest(profile, claim, exec) {
472
813
  const conflictingName = manifests.find((row) => row.name === name && row.computeKey !== claim.computeKey);
473
814
  if (conflictingName)
474
815
  throw new MetalProvisionError("requested guest name is already allocated");
475
- const manifestPath = join(profile.stateDir, `${name}.json`);
816
+ const manifestPath = join2(profile.stateDir, `${name}.json`);
476
817
  const prior = manifests.find((row) => row.computeKey === claim.computeKey);
477
818
  if (prior && prior.reference !== claim.spec.reference)
478
819
  throw new MetalProvisionError("compute identity conflicts with host inventory");
@@ -489,7 +830,7 @@ async function provisionMetalGuest(profile, claim, exec) {
489
830
  allowedMemoryNodes: cpuPool.memoryNodes,
490
831
  phase: "allocating"
491
832
  };
492
- const save = () => writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
833
+ const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
493
834
  `, { mode: 384 });
494
835
  save();
495
836
  const lv = `/dev/${profile.volumeGroup}/${name}`;
@@ -501,11 +842,11 @@ async function provisionMetalGuest(profile, claim, exec) {
501
842
  manifest.phase = "image-ready";
502
843
  save();
503
844
  }
504
- const seedBase = join(profile.seedDir, name);
845
+ const seedBase = join2(profile.seedDir, name);
505
846
  const init = cloudInit(profile, claim, manifest);
506
- writeFileSync(`${seedBase}-user-data`, init.userData, { mode: 384 });
507
- writeFileSync(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
508
- writeFileSync(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
847
+ writeFileSync2(`${seedBase}-user-data`, init.userData, { mode: 384 });
848
+ writeFileSync2(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
849
+ writeFileSync2(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
509
850
  const seed = `${seedBase}-seed.iso`;
510
851
  await checked(exec, [
511
852
  "cloud-localds",
@@ -537,9 +878,9 @@ async function provisionMetalGuest(profile, claim, exec) {
537
878
  throw new MetalProvisionError("confidential compute requested but host SNP profile is absent");
538
879
  }
539
880
  const service = `forgezero-guest@${name}.service`;
540
- const unitPath = join(profile.unitDir, service);
541
- mkdirSync(dirname(unitPath), { recursive: true });
542
- writeFileSync(unitPath, guestUnit(spec), { mode: 420 });
881
+ const unitPath = join2(profile.unitDir, service);
882
+ mkdirSync2(dirname(unitPath), { recursive: true });
883
+ writeFileSync2(unitPath, guestUnit(spec), { mode: 420 });
543
884
  await checked(exec, ["systemctl", "daemon-reload"]);
544
885
  if (prior?.phase === "running") {
545
886
  await checked(exec, ["systemctl", "restart", service]);
@@ -555,11 +896,11 @@ function legacyPlatformManifest(profile, claim, name) {
555
896
  if (claim.bootstrap?.kind !== "platform-genesis" || claim.computeKey !== `platform:${name}` || claim.spec.reference !== `platform:${name}` || claim.spec.guestAddress === undefined)
556
897
  return;
557
898
  const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
558
- const legacyPath = join(legacyDir, `${name}.conf`);
559
- if (!existsSync(legacyPath))
899
+ const legacyPath = join2(legacyDir, `${name}.conf`);
900
+ if (!existsSync2(legacyPath))
560
901
  return;
561
902
  const values = new Map;
562
- for (const line of readFileSync(legacyPath, "utf8").split(/\r?\n/)) {
903
+ for (const line of readFileSync2(legacyPath, "utf8").split(/\r?\n/)) {
563
904
  if (!line || line.startsWith("#"))
564
905
  continue;
565
906
  const match = /^([A-Z][A-Z0-9_]*)=([^\s'"`$;|&<>]+)$/.exec(line);
@@ -589,13 +930,13 @@ async function removeMetalGuest(profile, claim, exec) {
589
930
  if (claim.action !== "delete")
590
931
  throw new MetalProvisionError("create claim cannot remove a guest");
591
932
  const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
592
- const manifestPath = join(profile.stateDir, `${name}.json`);
933
+ const manifestPath = join2(profile.stateDir, `${name}.json`);
593
934
  const service = `forgezero-guest@${name}.service`;
594
- const unitPath = join(profile.unitDir, service);
935
+ const unitPath = join2(profile.unitDir, service);
595
936
  const lv = `/dev/${profile.volumeGroup}/${name}`;
596
- const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
937
+ const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
597
938
  if (!manifest) {
598
- const seedBase = join(profile.seedDir, name);
939
+ const seedBase = join2(profile.seedDir, name);
599
940
  const localArtifacts = [
600
941
  unitPath,
601
942
  `${seedBase}-seed.iso`,
@@ -603,7 +944,7 @@ async function removeMetalGuest(profile, claim, exec) {
603
944
  `${seedBase}-meta-data`,
604
945
  `${seedBase}-network-config`
605
946
  ];
606
- if (localArtifacts.some(existsSync)) {
947
+ if (localArtifacts.some(existsSync2)) {
607
948
  throw new MetalProvisionError("guest artifacts exist without owned inventory");
608
949
  }
609
950
  const [volume, active] = await Promise.all([
@@ -620,7 +961,7 @@ async function removeMetalGuest(profile, claim, exec) {
620
961
  if (!currentIdentity && !legacyPlatformIdentity) {
621
962
  throw new MetalProvisionError("compute identity conflicts with host inventory");
622
963
  }
623
- if (existsSync(unitPath))
964
+ if (existsSync2(unitPath))
624
965
  await checked(exec, ["systemctl", "disable", "--now", service]);
625
966
  else if (legacyPlatformIdentity)
626
967
  await checked(exec, ["systemctl", "disable", "--now", service]);
@@ -629,30 +970,30 @@ async function removeMetalGuest(profile, claim, exec) {
629
970
  }
630
971
  if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
631
972
  await checked(exec, ["lvremove", "-fy", lv]);
632
- for (const path of [
973
+ for (const path2 of [
633
974
  unitPath,
634
- join(profile.seedDir, `${name}-seed.iso`),
635
- join(profile.seedDir, `${name}-user-data`),
636
- join(profile.seedDir, `${name}-meta-data`),
637
- join(profile.seedDir, `${name}-network-config`),
975
+ join2(profile.seedDir, `${name}-seed.iso`),
976
+ join2(profile.seedDir, `${name}-user-data`),
977
+ join2(profile.seedDir, `${name}-meta-data`),
978
+ join2(profile.seedDir, `${name}-network-config`),
638
979
  manifestPath
639
980
  ])
640
- if (existsSync(path))
641
- unlinkSync(path);
981
+ if (existsSync2(path2))
982
+ unlinkSync2(path2);
642
983
  if (legacyPlatformIdentity) {
643
- const legacyConfig = join(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
644
- const legacyDropIn = join(profile.unitDir, `${service}.d`);
645
- if (existsSync(legacyConfig))
646
- unlinkSync(legacyConfig);
647
- if (existsSync(legacyDropIn))
648
- rmSync(legacyDropIn, { recursive: true });
984
+ const legacyConfig = join2(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
985
+ const legacyDropIn = join2(profile.unitDir, `${service}.d`);
986
+ if (existsSync2(legacyConfig))
987
+ unlinkSync2(legacyConfig);
988
+ if (existsSync2(legacyDropIn))
989
+ rmSync2(legacyDropIn, { recursive: true });
649
990
  }
650
991
  await checked(exec, ["systemctl", "daemon-reload"]);
651
992
  return {};
652
993
  }
653
994
 
654
995
  // src/metal-helper-socket.ts
655
- import { chmodSync, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "node:fs";
996
+ import { chmodSync as chmodSync2, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
656
997
  import { connect, createServer } from "node:net";
657
998
  var DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
658
999
  var MAX_REQUEST_BYTES = 32 * 1024;
@@ -685,8 +1026,8 @@ command exceeded ${COMMAND_TIMEOUT_MS}ms`.trim() : stderr
685
1026
  function startMetalHelper(options) {
686
1027
  validateMetalProfile(options.profile);
687
1028
  const socketPath = options.socketPath ?? DEFAULT_METAL_HELPER_SOCKET;
688
- if (existsSync2(socketPath))
689
- unlinkSync2(socketPath);
1029
+ if (existsSync3(socketPath))
1030
+ unlinkSync3(socketPath);
690
1031
  let tail = Promise.resolve();
691
1032
  const server = createServer((socket) => {
692
1033
  let buffer = "";
@@ -732,7 +1073,7 @@ function startMetalHelper(options) {
732
1073
  });
733
1074
  socket.on("error", () => socket.destroy());
734
1075
  });
735
- server.listen(socketPath, () => chmodSync(socketPath, 432));
1076
+ server.listen(socketPath, () => chmodSync2(socketPath, 432));
736
1077
  return {
737
1078
  server,
738
1079
  async stop() {