@forgezero/agent 0.1.34 → 0.1.35

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.
@@ -0,0 +1,942 @@
1
+ // @bun
2
+ // src/metal-bootstrap.ts
3
+ import { createHash, randomBytes } from "crypto";
4
+ import {
5
+ chmodSync,
6
+ chownSync,
7
+ copyFileSync,
8
+ existsSync,
9
+ lstatSync,
10
+ mkdirSync as mkdirSync2,
11
+ readFileSync,
12
+ realpathSync,
13
+ renameSync,
14
+ statSync,
15
+ symlinkSync,
16
+ unlinkSync,
17
+ writeFileSync as writeFileSync2
18
+ } from "fs";
19
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
20
+ import { isIP as isIP2 } from "net";
21
+
22
+ // src/metal-isolation.ts
23
+ import { mkdirSync, writeFileSync } from "fs";
24
+ import { join as join2 } from "path";
25
+
26
+ // src/metal-provision.ts
27
+ import { dirname, isAbsolute, join } from "path";
28
+ import { isIP } from "net";
29
+
30
+ // src/ubuntu.ts
31
+ var SUPPORTED_GUEST_IMAGE = Object.freeze({
32
+ key: "ubuntu-resolute-20260731",
33
+ family: "ubuntu-26.04",
34
+ version: "2026-07-31",
35
+ label: "Ubuntu 26.04 LTS Resolute",
36
+ url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
37
+ sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
38
+ });
39
+
40
+ // src/metal-provision.ts
41
+ var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
42
+ var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
43
+ var IPV4_PREFIX = /^(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
44
+ var LINUX_LIST = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/;
45
+
46
+ class MetalProvisionError extends Error {
47
+ }
48
+ function membersOfLinuxList(value, label) {
49
+ if (!LINUX_LIST.test(value))
50
+ throw new MetalProvisionError(`invalid ${label} list`);
51
+ const members = [];
52
+ for (const part of value.split(",")) {
53
+ const [startText, endText = startText] = part.split("-");
54
+ const start = Number(startText);
55
+ const end = Number(endText);
56
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > 65535) {
57
+ throw new MetalProvisionError(`invalid ${label} list`);
58
+ }
59
+ for (let value2 = start;value2 <= end; value2 += 1)
60
+ members.push(value2);
61
+ }
62
+ if (new Set(members).size !== members.length)
63
+ throw new MetalProvisionError(`${label} list overlaps itself`);
64
+ return members;
65
+ }
66
+ function validateMetalProfile(profile) {
67
+ if (!SAFE_NAME.test(profile.volumeGroup))
68
+ throw new MetalProvisionError("invalid volume group");
69
+ if (!DEVICE.test(profile.bridge))
70
+ throw new MetalProvisionError("invalid bridge");
71
+ if (!IPV4_PREFIX.test(profile.subnetPrefix))
72
+ throw new MetalProvisionError("invalid subnet prefix");
73
+ if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
74
+ throw new MetalProvisionError("invalid guest address range");
75
+ for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
76
+ if (!isAbsolute(path))
77
+ throw new MetalProvisionError("metal paths must be absolute");
78
+ }
79
+ new URL(profile.apiUrl);
80
+ let telemetryEndpoint;
81
+ try {
82
+ telemetryEndpoint = new URL(profile.agentTelemetryEndpoint);
83
+ } catch {
84
+ throw new MetalProvisionError("Agent telemetry endpoint must be an absolute public HTTPS URL");
85
+ }
86
+ 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"))
87
+ throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
88
+ const imageKeys = Object.keys(profile.images);
89
+ if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
90
+ throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
91
+ }
92
+ if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
93
+ throw new MetalProvisionError("at least one exclusive CPU pool is required");
94
+ }
95
+ const keys = new Set;
96
+ const assigned = new Set;
97
+ const assignedMemory = new Set;
98
+ let poolsWithMemory = 0;
99
+ for (const pool of profile.cpuPools) {
100
+ if (!SAFE_NAME.test(pool.key) || keys.has(pool.key))
101
+ throw new MetalProvisionError("invalid or duplicate CPU pool key");
102
+ keys.add(pool.key);
103
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
104
+ if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
105
+ throw new MetalProvisionError("invalid CPU pool physical-core count");
106
+ }
107
+ for (const cpu of cpus) {
108
+ if (assigned.has(cpu))
109
+ throw new MetalProvisionError("CPU pools overlap");
110
+ assigned.add(cpu);
111
+ }
112
+ if (pool.memoryNodes) {
113
+ poolsWithMemory += 1;
114
+ for (const node of membersOfLinuxList(pool.memoryNodes, "memory-node")) {
115
+ if (assignedMemory.has(node))
116
+ throw new MetalProvisionError("guest memory-node pools overlap");
117
+ assignedMemory.add(node);
118
+ }
119
+ }
120
+ }
121
+ if (poolsWithMemory !== 0 && poolsWithMemory !== profile.cpuPools.length) {
122
+ throw new MetalProvisionError("every CPU pool must name memory nodes when NUMA isolation is enabled");
123
+ }
124
+ const housekeeping = membersOfLinuxList(profile.housekeepingCpus, "housekeeping CPU");
125
+ if (housekeeping.some((cpu) => assigned.has(cpu))) {
126
+ throw new MetalProvisionError("housekeeping CPUs overlap guest CPU pools");
127
+ }
128
+ if (profile.housekeepingMemoryNodes) {
129
+ const housekeepingMemory = membersOfLinuxList(profile.housekeepingMemoryNodes, "housekeeping memory-node");
130
+ if (housekeepingMemory.some((node) => assignedMemory.has(node))) {
131
+ throw new MetalProvisionError("housekeeping memory nodes overlap guest memory-node pools");
132
+ }
133
+ } else if (assignedMemory.size > 0) {
134
+ throw new MetalProvisionError("NUMA-isolated guest pools require housekeeping memory nodes");
135
+ }
136
+ }
137
+
138
+ // src/metal-isolation.ts
139
+ var members = (list) => list.split(",").flatMap((part) => {
140
+ const [first, last = first] = part.split("-").map(Number);
141
+ return Array.from({ length: last - first + 1 }, (_, index) => first + index);
142
+ });
143
+ var compact = (values) => {
144
+ const sorted = [...new Set(values)].sort((left, right) => left - right);
145
+ const ranges = [];
146
+ for (let index = 0;index < sorted.length; ) {
147
+ const first = sorted[index];
148
+ let last = first;
149
+ while (sorted[index + 1] === last + 1)
150
+ last = sorted[++index];
151
+ ranges.push(first === last ? String(first) : `${first}-${last}`);
152
+ index += 1;
153
+ }
154
+ return ranges.join(",");
155
+ };
156
+ var memoryDirective = (nodes) => nodes ? `AllowedMemoryNodes=${nodes}
157
+ ` : "";
158
+ function metalGuestSliceUnit(profile) {
159
+ validateMetalProfile(profile);
160
+ const cpus = compact(profile.cpuPools.flatMap((pool) => members(pool.cpus)));
161
+ const nodes = compact(profile.cpuPools.flatMap((pool) => pool.memoryNodes ? members(pool.memoryNodes) : [])) || undefined;
162
+ return `[Unit]
163
+ Description=ForgeZero exclusive guest CPU and memory boundary
164
+
165
+ [Slice]
166
+ AllowedCPUs=${cpus}
167
+ ${memoryDirective(nodes)}`;
168
+ }
169
+ function metalHousekeepingDropIn(profile, kind) {
170
+ validateMetalProfile(profile);
171
+ return `[${kind === "slice" ? "Slice" : "Scope"}]
172
+ AllowedCPUs=${profile.housekeepingCpus}
173
+ ${memoryDirective(profile.housekeepingMemoryNodes)}`;
174
+ }
175
+ var defaultExec = async (argv) => {
176
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
177
+ const [exitCode, stdout, stderr] = await Promise.all([
178
+ child.exited,
179
+ new Response(child.stdout).text(),
180
+ new Response(child.stderr).text()
181
+ ]);
182
+ return { exitCode, stdout, stderr };
183
+ };
184
+ var checked = async (exec, argv) => {
185
+ const result = await exec(argv);
186
+ if (result.exitCode !== 0)
187
+ throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
188
+ return result;
189
+ };
190
+ var requireGuestsInSlice = async (exec) => {
191
+ const active = await checked(exec, [
192
+ "systemctl",
193
+ "list-units",
194
+ "--type=service",
195
+ "--state=running",
196
+ "--plain",
197
+ "--no-legend",
198
+ "forgezero-guest@*.service"
199
+ ]);
200
+ for (const line of active.stdout.split(`
201
+ `)) {
202
+ const service = line.trim().split(/\s+/)[0];
203
+ if (!service)
204
+ continue;
205
+ const cgroup = await checked(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
206
+ if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
207
+ throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
208
+ }
209
+ }
210
+ };
211
+ async function applyMetalIsolation(profile, exec = defaultExec) {
212
+ validateMetalProfile(profile);
213
+ await requireGuestsInSlice(exec);
214
+ const unitDir = profile.unitDir;
215
+ mkdirSync(unitDir, { recursive: true });
216
+ writeFileSync(join2(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
217
+ for (const unit of ["system.slice", "user.slice"]) {
218
+ const directory = join2(unitDir, `${unit}.d`);
219
+ mkdirSync(directory, { recursive: true });
220
+ writeFileSync(join2(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
221
+ }
222
+ const initDirectory = join2(unitDir, "init.scope.d");
223
+ mkdirSync(initDirectory, { recursive: true });
224
+ writeFileSync(join2(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
225
+ await checked(exec, ["systemctl", "daemon-reload"]);
226
+ await requireGuestsInSlice(exec);
227
+ const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
228
+ if (profile.housekeepingMemoryNodes)
229
+ properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
230
+ for (const unit of ["system.slice", "user.slice", "init.scope"]) {
231
+ await checked(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
232
+ }
233
+ }
234
+
235
+ // src/egress-policy.ts
236
+ var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
237
+ var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
238
+ var BLOCKED_IPV4 = [
239
+ "0.0.0.0/8",
240
+ "10.0.0.0/8",
241
+ "100.64.0.0/10",
242
+ "127.0.0.0/8",
243
+ "168.63.129.16/32",
244
+ "169.254.0.0/16",
245
+ "172.16.0.0/12",
246
+ "192.0.0.0/24",
247
+ "192.0.2.0/24",
248
+ "192.88.99.0/24",
249
+ "192.168.0.0/16",
250
+ "198.18.0.0/15",
251
+ "198.51.100.0/24",
252
+ "203.0.113.0/24",
253
+ "224.0.0.0/4",
254
+ "240.0.0.0/4"
255
+ ];
256
+ var BLOCKED_IPV6 = [
257
+ "::/128",
258
+ "::1/128",
259
+ "::ffff:0:0/96",
260
+ "64:ff9b::/96",
261
+ "64:ff9b:1::/48",
262
+ "100::/64",
263
+ "fc00::/7",
264
+ "fec0::/10",
265
+ "fe80::/10",
266
+ "ff00::/8",
267
+ "2001::/32",
268
+ "2001:2::/48",
269
+ "2001:10::/28",
270
+ "2001:20::/28",
271
+ "2001:db8::/32",
272
+ "2002::/16",
273
+ "3fff::/20"
274
+ ];
275
+ var normalizeEgressTcpPorts = (ports) => {
276
+ for (const port of ports) {
277
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
278
+ throw new Error("Agent egress policy refuses an invalid loopback TCP port.");
279
+ }
280
+ }
281
+ return [...new Set(ports)].sort((left, right) => left - right);
282
+ };
283
+ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
284
+ const ports = normalizeEgressTcpPorts(loopbackTcpPorts);
285
+ return [
286
+ "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
287
+ `IPAddressAllow=${SYSTEMD_RESOLVED_ADDRESS}/32`,
288
+ ...ports.length > 0 ? ["IPAddressAllow=127.0.0.1/32", "IPAddressAllow=::1/128"] : [],
289
+ ...BLOCKED_IPV4.map((network) => `IPAddressDeny=${network}`),
290
+ ...BLOCKED_IPV6.map((network) => `IPAddressDeny=${network}`)
291
+ ].join(`
292
+ `);
293
+ }
294
+
295
+ // src/version.ts
296
+ var VERSION = "0.1.35";
297
+
298
+ // src/metal-bootstrap.ts
299
+ var PROFILE_PATH = "/etc/forgezero/metal.json";
300
+ var STATE_PATH = "/etc/forgezero/metal.initialized.json";
301
+ var SEED_CREDENTIAL_PATH = "/etc/forgezero/creds/metal-agent-seed.cred";
302
+ var UNIT_DIRECTORY = "/etc/systemd/system";
303
+ var AGENT_PATH = "/usr/local/bin/fz-agent";
304
+ var HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
305
+ var UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
306
+ var MAX_CONFIG_BYTES = 256 * 1024;
307
+ var SUPPORTED_BUN_VERSION = "1.3.14";
308
+ var SUPPORTED_BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
309
+
310
+ class MetalBootstrapError extends Error {
311
+ }
312
+ var defaultExec2 = async (argv, stdin) => {
313
+ const child = Bun.spawn([...argv], {
314
+ stdin: stdin === undefined ? undefined : new Blob([stdin]),
315
+ stdout: "pipe",
316
+ stderr: "pipe",
317
+ env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
318
+ });
319
+ const [exitCode, stdout, stderr] = await Promise.all([
320
+ child.exited,
321
+ new Response(child.stdout).text(),
322
+ new Response(child.stderr).text()
323
+ ]);
324
+ return { exitCode, stdout, stderr };
325
+ };
326
+ var runChecked = async (exec, argv, stdin) => {
327
+ const result = await exec(argv, stdin);
328
+ if (result.exitCode !== 0) {
329
+ throw new MetalBootstrapError(`${argv.join(" ")} failed: ${(result.stderr || result.stdout).trim() || `exit ${result.exitCode}`}`);
330
+ }
331
+ return result;
332
+ };
333
+ var exactKeys = (value, allowed, label) => {
334
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
335
+ if (unknown.length)
336
+ throw new MetalBootstrapError(`${label} contains unknown fields: ${unknown.join(", ")}`);
337
+ };
338
+ var validUnit = (unit) => {
339
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit)) {
340
+ throw new MetalBootstrapError("hostTelemetryUnit must be a systemd .service unit");
341
+ }
342
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-metal-helper|forgezero-db)\.service$/.test(unit)) {
343
+ throw new MetalBootstrapError("the OTLP collector must be independently supervised");
344
+ }
345
+ };
346
+ function validateMetalBootstrapConfig(config) {
347
+ if (!config || typeof config !== "object")
348
+ throw new MetalBootstrapError("metal bootstrap config must be an object");
349
+ exactKeys(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
350
+ if (config.kind !== "metal")
351
+ throw new MetalBootstrapError("metal bootstrap config kind must be metal");
352
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(config.metalHostname)) {
353
+ throw new MetalBootstrapError("invalid metal inventory hostname");
354
+ }
355
+ if (!config.profile || typeof config.profile !== "object")
356
+ throw new MetalBootstrapError("metal profile must be an object");
357
+ exactKeys(config.profile, [
358
+ "volumeGroup",
359
+ "bridge",
360
+ "subnetPrefix",
361
+ "addressStart",
362
+ "addressEnd",
363
+ "gateway",
364
+ "nameservers",
365
+ "stateDir",
366
+ "seedDir",
367
+ "unitDir",
368
+ "apiUrl",
369
+ "agentTelemetryEndpoint",
370
+ "images",
371
+ "cpuPools",
372
+ "housekeepingCpus",
373
+ "housekeepingMemoryNodes",
374
+ "bunVersion",
375
+ "bunInstallerSha256",
376
+ "agentVersion",
377
+ "confidential"
378
+ ], "metal profile");
379
+ if (Array.isArray(config.profile.cpuPools)) {
380
+ for (const pool of config.profile.cpuPools)
381
+ exactKeys(pool, ["key", "cpus", "physicalCores", "memoryNodes"], "metal CPU pool");
382
+ }
383
+ validateMetalProfile(config.profile);
384
+ if (config.profile.stateDir !== "/etc/forgezero/metal-guests" || config.profile.seedDir !== "/var/lib/forgezero/seed" || config.profile.unitDir !== UNIT_DIRECTORY || config.profile.legacyStateDir !== undefined) {
385
+ throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
386
+ }
387
+ const image = config.profile.images[Object.keys(config.profile.images)[0]];
388
+ if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
389
+ throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
390
+ }
391
+ if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunInstallerSha256 !== SUPPORTED_BUN_INSTALLER_SHA256 || config.profile.agentVersion !== VERSION) {
392
+ throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
393
+ }
394
+ let api;
395
+ try {
396
+ api = new URL(config.profile.apiUrl);
397
+ } catch {
398
+ throw new MetalBootstrapError("metal API must be a public HTTPS origin");
399
+ }
400
+ if (api.protocol !== "https:" || api.username || api.password || api.pathname !== "/" || api.search || api.hash || isIP2(api.hostname) !== 0 || !api.hostname.includes(".") || api.hostname.endsWith(".local")) {
401
+ throw new MetalBootstrapError("metal API must be a credential-free public HTTPS origin");
402
+ }
403
+ if (config.profile.nameservers?.some((address) => isIP2(address) === 0)) {
404
+ throw new MetalBootstrapError("metal nameservers must be literal IP addresses");
405
+ }
406
+ if (isIP2(`${config.profile.subnetPrefix}.1`) !== 4 || isIP2(config.profile.gateway) !== 4 || !config.profile.gateway.startsWith(`${config.profile.subnetPrefix}.`)) {
407
+ throw new MetalBootstrapError("metal gateway must be an IPv4 address in the reviewed subnet");
408
+ }
409
+ if (config.profile.confidential && (!Number.isSafeInteger(config.profile.confidential.cbitpos) || config.profile.confidential.cbitpos < 1 || config.profile.confidential.cbitpos > 63 || !Number.isSafeInteger(config.profile.confidential.reducedPhysBits) || config.profile.confidential.reducedPhysBits < 0 || config.profile.confidential.reducedPhysBits > 63 || !/^0x[0-9a-fA-F]{1,16}$/.test(config.profile.confidential.policy)))
410
+ throw new MetalBootstrapError("invalid confidential-compute coordinate");
411
+ if (config.hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
412
+ throw new MetalBootstrapError("metal host OTLP must use exact loopback http://127.0.0.1:4318");
413
+ }
414
+ validUnit(config.hostTelemetryUnit);
415
+ if (config.agentSeedFile)
416
+ validateOwnerOnlyPath(config.agentSeedFile, false);
417
+ return config;
418
+ }
419
+ function validateOwnerOnlyPath(path, requireRootOwner) {
420
+ if (!isAbsolute2(path) || resolve(path) !== path || path.includes("/../")) {
421
+ throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
422
+ }
423
+ const metadata = lstatSync(path);
424
+ if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync(path) !== path) {
425
+ throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
426
+ }
427
+ if ((metadata.mode & 63) !== 0)
428
+ throw new MetalBootstrapError("private bootstrap file must be owner-only");
429
+ const caller = typeof process.getuid === "function" ? process.getuid() : -1;
430
+ if (requireRootOwner && metadata.uid !== 0 || !requireRootOwner && metadata.uid !== 0 && metadata.uid !== caller) {
431
+ throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
432
+ }
433
+ }
434
+ function readMetalBootstrapConfig(path) {
435
+ validateOwnerOnlyPath(path, false);
436
+ const metadata = statSync(path);
437
+ if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
438
+ throw new MetalBootstrapError("metal bootstrap config size is invalid");
439
+ let parsed;
440
+ try {
441
+ parsed = JSON.parse(readFileSync(path, "utf8"));
442
+ } catch {
443
+ throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
444
+ }
445
+ return validateMetalBootstrapConfig(parsed);
446
+ }
447
+ function planMetalBootstrap(config) {
448
+ validateMetalBootstrapConfig(config);
449
+ return {
450
+ kind: "metal",
451
+ metalHostname: config.metalHostname,
452
+ profilePath: PROFILE_PATH,
453
+ units: [
454
+ "forgezero-metal-helper.service",
455
+ "forgezero-agent-update-helper.service",
456
+ "forgezero-metal-agent-egress.service",
457
+ "forgezero-metal-agent.service"
458
+ ],
459
+ steps: [
460
+ "install fixed KVM/LVM/cloud-init/nftables prerequisites",
461
+ "verify bridge, volume group, pinned guest image, KVM and optional SEV device",
462
+ "install the published Agent binary and root-owned immutable metal profile",
463
+ "seal or generate the metal identity seed as a systemd credential",
464
+ "apply reviewed CPU/NUMA host and guest isolation",
465
+ "install helper, update, egress and identity-only Agent units",
466
+ "enable units and prove services, local sockets, egress policy and OTLP collector"
467
+ ],
468
+ requiresRoot: true,
469
+ consumes: config.agentSeedFile ? [config.agentSeedFile] : []
470
+ };
471
+ }
472
+ var atomicWrite = (path, body, mode) => {
473
+ mkdirSync2(dirname2(path), { recursive: true, mode: 493 });
474
+ const temporary = `${path}.next-${process.pid}`;
475
+ writeFileSync2(temporary, body, { mode, flag: "wx" });
476
+ chmodSync(temporary, mode);
477
+ chownSync(temporary, 0, 0);
478
+ renameSync(temporary, path);
479
+ };
480
+ var validateAgentSourcePath = (source) => {
481
+ if (!isAbsolute2(source) || !lstatSync(source).isFile() || lstatSync(source).isSymbolicLink()) {
482
+ throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
483
+ }
484
+ };
485
+ var ensureAccount = async (exec) => {
486
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-metal"])).exitCode !== 0) {
487
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-metal"]);
488
+ }
489
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-update"])).exitCode !== 0) {
490
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-update"]);
491
+ }
492
+ if ((await exec(["/usr/bin/id", "forgezero-metal"])).exitCode !== 0) {
493
+ await runChecked(exec, [
494
+ "/usr/sbin/useradd",
495
+ "--system",
496
+ "--no-create-home",
497
+ "--shell",
498
+ "/usr/sbin/nologin",
499
+ "--gid",
500
+ "forgezero-metal",
501
+ "forgezero-metal"
502
+ ]);
503
+ }
504
+ await runChecked(exec, ["/usr/sbin/usermod", "-a", "-G", "forgezero-update", "forgezero-metal"]);
505
+ };
506
+ function renderMetalUnits(config) {
507
+ validateMetalBootstrapConfig(config);
508
+ const egress = systemdAgentEgressDirectives([4318]);
509
+ const updateEgress = systemdAgentEgressDirectives();
510
+ return {
511
+ "forgezero-metal-helper.service": `[Unit]
512
+ Description=ForgeZero constrained physical-host helper
513
+ After=local-fs.target
514
+
515
+ [Service]
516
+ Type=simple
517
+ User=root
518
+ Group=forgezero-metal
519
+ UMask=0007
520
+ RuntimeDirectory=forgezero-metal
521
+ RuntimeDirectoryMode=0770
522
+ ExecStart=${AGENT_PATH} metal-helper --profile=${PROFILE_PATH}
523
+ Restart=on-failure
524
+ RestartSec=5
525
+ TimeoutStopSec=10min
526
+ LimitCORE=0
527
+ PrivateTmp=true
528
+ ProtectHome=true
529
+ RestrictAddressFamilies=AF_UNIX
530
+
531
+ [Install]
532
+ WantedBy=multi-user.target
533
+ `,
534
+ "forgezero-agent-update-helper.service": `[Unit]
535
+ Description=ForgeZero verified Agent update helper
536
+ After=network-online.target
537
+ Wants=network-online.target
538
+
539
+ [Service]
540
+ Type=simple
541
+ User=root
542
+ Group=forgezero-update
543
+ Environment=FZ_AGENT_UPDATE_SOCKET=${UPDATE_SOCKET}
544
+ ExecStart=${AGENT_PATH} update-helper
545
+ Restart=always
546
+ RestartSec=2
547
+ RuntimeDirectory=forgezero-update
548
+ RuntimeDirectoryMode=0750
549
+ UMask=0007
550
+ LimitCORE=0
551
+ NoNewPrivileges=true
552
+ PrivateTmp=true
553
+ ProtectSystem=strict
554
+ ProtectHome=true
555
+ ProtectKernelTunables=true
556
+ ProtectKernelModules=true
557
+ ProtectControlGroups=true
558
+ RestrictSUIDSGID=true
559
+ RestrictRealtime=true
560
+ LockPersonality=true
561
+ ReadWritePaths=/opt/forgezero/agent /var/lib/forgezero
562
+ ${updateEgress}
563
+
564
+ [Install]
565
+ WantedBy=multi-user.target
566
+ `,
567
+ "forgezero-metal-agent-egress.service": `[Unit]
568
+ Description=ForgeZero physical Agent host egress policy
569
+ After=systemd-resolved.service nftables.service
570
+ Requires=systemd-resolved.service
571
+ Before=forgezero-metal-agent.service
572
+
573
+ [Service]
574
+ Type=notify
575
+ NotifyAccess=all
576
+ User=root
577
+ Group=root
578
+ ExecStart=${AGENT_PATH} egress-policy --user=forgezero-metal --loopback-user=forgezero-metal --loopback-tcp-port=4318 --public-tcp-port=443
579
+ Restart=on-failure
580
+ RestartSec=2
581
+ LimitCORE=0
582
+ NoNewPrivileges=true
583
+ PrivateTmp=true
584
+ ProtectSystem=strict
585
+ ProtectHome=true
586
+ ProtectKernelTunables=true
587
+ ProtectKernelModules=true
588
+ ProtectControlGroups=true
589
+ RestrictSUIDSGID=true
590
+ RestrictRealtime=true
591
+ MemoryDenyWriteExecute=true
592
+ LockPersonality=true
593
+ CapabilityBoundingSet=CAP_NET_ADMIN
594
+ RestrictAddressFamilies=AF_UNIX AF_NETLINK
595
+
596
+ [Install]
597
+ WantedBy=multi-user.target
598
+ `,
599
+ "forgezero-metal-agent.service": `[Unit]
600
+ Description=ForgeZero identity-only physical-host Agent
601
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
602
+ Wants=network-online.target ${config.hostTelemetryUnit}
603
+ Requires=forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
604
+ BindsTo=forgezero-metal-agent-egress.service
605
+
606
+ [Service]
607
+ Type=simple
608
+ User=forgezero-metal
609
+ Group=forgezero-metal
610
+ SupplementaryGroups=forgezero-update
611
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
612
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
613
+ Environment=FZ_AGENT_ROLE=metal
614
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
615
+ Environment=FZ_API=${config.profile.apiUrl}
616
+ Environment=FZ_METAL_HELPER_SOCKET=${HELPER_SOCKET}
617
+ Environment=FZ_DRAIN_DEADLINE_MS=120000
618
+ Environment=NODE_ENV=production
619
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
620
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent
621
+ ExecStart=${AGENT_PATH}
622
+ Restart=on-failure
623
+ RestartSec=5
624
+ TimeoutStopSec=130s
625
+ LimitCORE=0
626
+ NoNewPrivileges=true
627
+ PrivateTmp=true
628
+ ProtectSystem=strict
629
+ ProtectHome=true
630
+ ProtectKernelTunables=true
631
+ ProtectKernelModules=true
632
+ ProtectControlGroups=true
633
+ RestrictSUIDSGID=true
634
+ LockPersonality=true
635
+ ${egress}
636
+
637
+ [Install]
638
+ WantedBy=multi-user.target
639
+ `
640
+ };
641
+ }
642
+ var installAgentBinary = (source, version) => {
643
+ validateAgentSourcePath(source);
644
+ const release = `/opt/forgezero/agent/versions/${version}/dist`;
645
+ mkdirSync2(release, { recursive: true, mode: 493 });
646
+ copyFileSync(source, join3(release, "fz-agent.js"));
647
+ chmodSync(join3(release, "fz-agent.js"), 493);
648
+ chownSync(join3(release, "fz-agent.js"), 0, 0);
649
+ mkdirSync2("/opt/forgezero/agent", { recursive: true, mode: 493 });
650
+ for (const [link, target] of [
651
+ ["/opt/forgezero/agent/current.next", `versions/${version}`],
652
+ [AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
653
+ ]) {
654
+ try {
655
+ unlinkSync(link);
656
+ } catch {}
657
+ symlinkSync(target, link);
658
+ if (link.endsWith("current.next"))
659
+ renameSync(link, "/opt/forgezero/agent/current");
660
+ }
661
+ };
662
+ var preflight = async (config, exec) => {
663
+ await runChecked(exec, ["/usr/bin/systemctl", "is-active", "--quiet", config.hostTelemetryUnit]);
664
+ await runChecked(exec, [
665
+ "/usr/bin/curl",
666
+ "--silent",
667
+ "--show-error",
668
+ "--fail",
669
+ "--max-time",
670
+ "5",
671
+ "--request",
672
+ "POST",
673
+ "--header",
674
+ "Content-Type: application/json",
675
+ "--data-binary",
676
+ "{}",
677
+ `${config.hostTelemetryEndpoint}/v1/metrics`
678
+ ]);
679
+ await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
680
+ await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
681
+ if (!existsSync("/dev/kvm"))
682
+ throw new MetalBootstrapError("/dev/kvm is required");
683
+ if (config.profile.confidential) {
684
+ if (!existsSync("/dev/sev"))
685
+ throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
686
+ await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
687
+ }
688
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", config.profile.images[Object.keys(config.profile.images)[0]].path])).stdout.split(/\s+/)[0];
689
+ if (digest !== config.profile.images[Object.keys(config.profile.images)[0]].sha256)
690
+ throw new MetalBootstrapError("pinned guest image digest mismatch");
691
+ };
692
+ var assertSupportedMetalHost = () => {
693
+ if (process.platform !== "linux" || process.arch !== "x64") {
694
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
695
+ }
696
+ const release = readFileSync("/etc/os-release", "utf8");
697
+ if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
698
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
699
+ }
700
+ };
701
+ var ensurePinnedGuestImage = async (config, exec) => {
702
+ const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
703
+ if (existsSync(image.path))
704
+ return;
705
+ mkdirSync2(dirname2(image.path), { recursive: true, mode: 493 });
706
+ const temporary = `${image.path}.next-${process.pid}`;
707
+ try {
708
+ await runChecked(exec, [
709
+ "/usr/bin/curl",
710
+ "--fail",
711
+ "--location",
712
+ "--proto",
713
+ "=https",
714
+ "--tlsv1.2",
715
+ "--output",
716
+ temporary,
717
+ SUPPORTED_GUEST_IMAGE.url
718
+ ]);
719
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
720
+ if (digest !== image.sha256)
721
+ throw new MetalBootstrapError("downloaded guest image digest mismatch");
722
+ chmodSync(temporary, 292);
723
+ chownSync(temporary, 0, 0);
724
+ renameSync(temporary, image.path);
725
+ } catch (cause) {
726
+ try {
727
+ unlinkSync(temporary);
728
+ } catch {}
729
+ throw cause;
730
+ }
731
+ };
732
+ async function applyMetalBootstrap(config, options) {
733
+ validateMetalBootstrapConfig(config);
734
+ if ((options.getuid ?? process.getuid)?.() !== 0)
735
+ throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
736
+ assertSupportedMetalHost();
737
+ if (existsSync(STATE_PATH) && !options.repair)
738
+ throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
739
+ if (config.agentSeedFile)
740
+ validateOwnerOnlyPath(config.agentSeedFile, true);
741
+ if (config.agentSeedFile && existsSync(SEED_CREDENTIAL_PATH)) {
742
+ throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
743
+ }
744
+ validateAgentSourcePath(options.agentSourcePath);
745
+ const exec = options.exec ?? defaultExec2;
746
+ await runChecked(exec, ["/usr/bin/apt-get", "update"]);
747
+ await runChecked(exec, [
748
+ "/usr/bin/apt-get",
749
+ "install",
750
+ "-y",
751
+ "--no-install-recommends",
752
+ "qemu-system-x86",
753
+ "qemu-utils",
754
+ "cloud-image-utils",
755
+ "lvm2",
756
+ "nftables",
757
+ "curl"
758
+ ]);
759
+ await ensurePinnedGuestImage(config, exec);
760
+ await preflight(config, exec);
761
+ await ensureAccount(exec);
762
+ installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
763
+ mkdirSync2("/etc/forgezero/creds", { recursive: true, mode: 448 });
764
+ mkdirSync2(config.profile.stateDir, { recursive: true, mode: 448 });
765
+ mkdirSync2(config.profile.seedDir, { recursive: true, mode: 448 });
766
+ const persistedProfile = {
767
+ ...config.profile,
768
+ metalHostname: config.metalHostname,
769
+ hostTelemetryEndpoint: config.hostTelemetryEndpoint,
770
+ hostTelemetryUnit: config.hostTelemetryUnit
771
+ };
772
+ atomicWrite(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
773
+ `, 384);
774
+ if (!existsSync(SEED_CREDENTIAL_PATH)) {
775
+ const seed = config.agentSeedFile ? readFileSync(config.agentSeedFile, "utf8").trim() : randomBytes(32).toString("base64url");
776
+ if (seed.length < 32 || /[\0\r\n]/.test(seed))
777
+ throw new MetalBootstrapError("metal Agent seed is invalid");
778
+ await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
779
+ `);
780
+ chmodSync(SEED_CREDENTIAL_PATH, 256);
781
+ chownSync(SEED_CREDENTIAL_PATH, 0, 0);
782
+ if (config.agentSeedFile)
783
+ unlinkSync(config.agentSeedFile);
784
+ }
785
+ await applyMetalIsolation(config.profile, (argv) => exec(argv));
786
+ for (const [unit, body] of Object.entries(renderMetalUnits(config)))
787
+ atomicWrite(join3(UNIT_DIRECTORY, unit), body, 420);
788
+ await runChecked(exec, ["/usr/bin/systemctl", "daemon-reload"]);
789
+ await runChecked(exec, [
790
+ "/usr/bin/systemctl",
791
+ "enable",
792
+ "--now",
793
+ "forgezero-agent-update-helper.service",
794
+ "forgezero-metal-helper.service",
795
+ "forgezero-metal-agent-egress.service",
796
+ "forgezero-metal-agent.service"
797
+ ]);
798
+ for (let attempt = 0;attempt < 100 && (!socketReady(HELPER_SOCKET) || !socketReady(UPDATE_SOCKET)); attempt += 1) {
799
+ await Bun.sleep(100);
800
+ }
801
+ const status = await metalBootstrapStatus(exec);
802
+ const operationalProblems = options.repair ? status.problems.filter((problem) => problem !== "metal initialized state does not bind the current profile") : status.problems;
803
+ if (operationalProblems.length)
804
+ throw new MetalBootstrapError(`metal bootstrap verification failed: ${operationalProblems.join("; ")}`);
805
+ const identity = await runChecked(exec, [
806
+ "/usr/bin/systemd-run",
807
+ "--pipe",
808
+ "--wait",
809
+ "--quiet",
810
+ "--collect",
811
+ `--property=LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}`,
812
+ "--setenv=FZ_SEED_CREDENTIAL=metal-agent-seed",
813
+ AGENT_PATH,
814
+ "identity"
815
+ ]);
816
+ const publicIdentity = identity.stdout.trim();
817
+ if (!publicIdentity || /[\r\n]/.test(publicIdentity))
818
+ throw new MetalBootstrapError("metal public identity proof was invalid");
819
+ const state = {
820
+ initializedAt: new Date().toISOString(),
821
+ role: "metal",
822
+ metalHostname: config.metalHostname,
823
+ profileSha256: createHash("sha256").update(JSON.stringify(config.profile)).digest("hex")
824
+ };
825
+ atomicWrite(STATE_PATH, `${JSON.stringify(state, null, 2)}
826
+ `, 384);
827
+ return { ...status, initialized: true, problems: [], metalHostname: config.metalHostname, publicIdentity };
828
+ }
829
+ var socketReady = (path) => {
830
+ try {
831
+ return lstatSync(path).isSocket();
832
+ } catch {
833
+ return false;
834
+ }
835
+ };
836
+ async function metalBootstrapStatus(exec = defaultExec2) {
837
+ const problems = [];
838
+ let profileValid = false, imageVerified = false, metalHostname, profileSha256;
839
+ let profileMode = null;
840
+ if (existsSync(PROFILE_PATH)) {
841
+ try {
842
+ const metadata = statSync(PROFILE_PATH);
843
+ profileMode = metadata.mode & 511;
844
+ if (profileMode !== 384 || metadata.uid !== 0)
845
+ problems.push("metal profile is not root-owned mode 0600");
846
+ const persisted = JSON.parse(readFileSync(PROFILE_PATH, "utf8"));
847
+ const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
848
+ validateMetalProfile(profile);
849
+ profileValid = true;
850
+ profileSha256 = createHash("sha256").update(JSON.stringify(profile)).digest("hex");
851
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
852
+ problems.push("persisted metal host coordinates are invalid");
853
+ }
854
+ try {
855
+ validUnit(hostTelemetryUnit);
856
+ } catch {
857
+ problems.push("persisted metal OTLP collector unit is invalid");
858
+ }
859
+ if ((await exec(["/usr/bin/systemctl", "is-active", "--quiet", hostTelemetryUnit])).exitCode !== 0) {
860
+ problems.push(`${hostTelemetryUnit} is inactive`);
861
+ } else if ((await exec([
862
+ "/usr/bin/curl",
863
+ "--silent",
864
+ "--show-error",
865
+ "--fail",
866
+ "--max-time",
867
+ "5",
868
+ "--request",
869
+ "POST",
870
+ "--header",
871
+ "Content-Type: application/json",
872
+ "--data-binary",
873
+ "{}",
874
+ `${hostTelemetryEndpoint}/v1/metrics`
875
+ ])).exitCode !== 0) {
876
+ problems.push("local OTLP metrics receiver did not accept a proof request");
877
+ }
878
+ const image = profile.images[Object.keys(profile.images)[0]];
879
+ if (existsSync(image.path)) {
880
+ const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
881
+ imageVerified = digest === image.sha256;
882
+ }
883
+ if (!imageVerified)
884
+ problems.push("pinned guest image is absent or has the wrong digest");
885
+ } catch (cause) {
886
+ problems.push(`metal profile invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
887
+ }
888
+ } else
889
+ problems.push("metal profile is missing");
890
+ if (existsSync(STATE_PATH)) {
891
+ try {
892
+ const state = JSON.parse(readFileSync(STATE_PATH, "utf8"));
893
+ metalHostname = state.metalHostname;
894
+ if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
895
+ problems.push("metal initialized state does not bind the current profile");
896
+ }
897
+ } catch {
898
+ problems.push("metal initialized state is invalid");
899
+ }
900
+ }
901
+ const units = {};
902
+ for (const unit of [
903
+ "forgezero-metal-helper.service",
904
+ "forgezero-agent-update-helper.service",
905
+ "forgezero-metal-agent-egress.service",
906
+ "forgezero-metal-agent.service"
907
+ ]) {
908
+ if (!existsSync(join3(UNIT_DIRECTORY, unit)))
909
+ units[unit] = "missing";
910
+ else
911
+ units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
912
+ if (units[unit] !== "active")
913
+ problems.push(`${unit} is ${units[unit]}`);
914
+ }
915
+ const helperSocketReady = socketReady(HELPER_SOCKET);
916
+ const updateSocketReady = socketReady(UPDATE_SOCKET);
917
+ if (!helperSocketReady)
918
+ problems.push("metal helper socket is not ready");
919
+ if (!updateSocketReady)
920
+ problems.push("Agent update helper socket is not ready");
921
+ return {
922
+ initialized: existsSync(STATE_PATH),
923
+ profileValid,
924
+ profileMode,
925
+ imageVerified,
926
+ units,
927
+ helperSocketReady,
928
+ updateSocketReady,
929
+ metalHostname,
930
+ problems
931
+ };
932
+ }
933
+ export {
934
+ validateOwnerOnlyPath,
935
+ validateMetalBootstrapConfig,
936
+ renderMetalUnits,
937
+ readMetalBootstrapConfig,
938
+ planMetalBootstrap,
939
+ metalBootstrapStatus,
940
+ applyMetalBootstrap,
941
+ MetalBootstrapError
942
+ };