@forgezero/agent 0.1.40 → 0.1.42

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 (64) hide show
  1. package/README.md +573 -424
  2. package/dist/agent-heartbeat.js +6 -3
  3. package/dist/agent-update-helper.js +5 -2
  4. package/dist/agent-update.js +5 -2
  5. package/dist/bootstrap.d.ts +17 -8
  6. package/dist/bootstrap.js +1504 -512
  7. package/dist/cli/agent-install.d.ts +6 -5
  8. package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
  9. package/dist/cli/maintenance.d.ts +23 -0
  10. package/dist/cli/run.d.ts +3 -1
  11. package/dist/cli/session-store.d.ts +5 -0
  12. package/dist/cloudflare-bootstrap.d.ts +73 -35
  13. package/dist/cloudflare-bootstrap.js +587 -90
  14. package/dist/cloudflare-edge.d.ts +64 -12
  15. package/dist/cloudflare-edge.js +103 -8
  16. package/dist/community-rehearsal-host.d.ts +51 -0
  17. package/dist/community-rehearsal-host.js +272 -0
  18. package/dist/credential-schema.d.ts +54 -0
  19. package/dist/credential-schema.js +47 -0
  20. package/dist/definition.d.ts +31 -5
  21. package/dist/definition.js +271 -44
  22. package/dist/deploy-file.js +294 -68
  23. package/dist/deployment-runner.js +18 -5
  24. package/dist/deployment.d.ts +13 -1
  25. package/dist/fz-agent.js +9162 -7564
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +5679 -5077
  28. package/dist/git-ssh.d.ts +5 -0
  29. package/dist/guest-enrolment.d.ts +2 -0
  30. package/dist/guest-enrolment.js +1 -0
  31. package/dist/host-maintenance.d.ts +39 -0
  32. package/dist/host-maintenance.js +135 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/mesh-connector.d.ts +16 -0
  35. package/dist/mesh-connector.js +46 -0
  36. package/dist/metal-bootstrap.js +145 -7
  37. package/dist/metal-helper-socket.js +61 -31
  38. package/dist/metal-provision.d.ts +2 -2
  39. package/dist/metal-provision.js +62 -32
  40. package/dist/operator-bootstrap.d.ts +90 -0
  41. package/dist/operator-bootstrap.js +5704 -0
  42. package/dist/otel-collector.d.ts +18 -0
  43. package/dist/pipeline.d.ts +3 -2
  44. package/dist/pipeline.js +1 -1
  45. package/dist/platform-bootstrap-runtime.d.ts +39 -21
  46. package/dist/platform-bootstrap-runtime.js +182 -59
  47. package/dist/platform-fleet-verification.d.ts +19 -0
  48. package/dist/platform-fleet-verification.js +3873 -0
  49. package/dist/platform-genesis-config.d.ts +7 -0
  50. package/dist/platform-genesis.d.ts +17 -0
  51. package/dist/provision.d.ts +76 -3
  52. package/dist/provision.js +1061 -229
  53. package/dist/recovery-host.d.ts +7 -0
  54. package/dist/recovery-host.js +124 -0
  55. package/dist/service-supervisor.d.ts +42 -0
  56. package/dist/software-helper.d.ts +4 -0
  57. package/dist/software-helper.js +865 -63
  58. package/dist/software.d.ts +14 -3
  59. package/dist/software.js +163 -37
  60. package/dist/ssh-bootstrap.d.ts +97 -0
  61. package/dist/supervised-app.d.ts +2 -0
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +175 -164
  64. package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ export declare class GitSshError extends Error {
3
+ }
4
+ export declare function gitSshArgv(args: readonly string[]): string[];
5
+ export declare function runGitSsh(args?: string[]): Promise<number>;
@@ -29,6 +29,8 @@ export interface GuestEnrolmentOptions {
29
29
  keys: NodeKeyPair;
30
30
  label?: string;
31
31
  gitDeployPublicKey?: string;
32
+ /** Public half of the runner-only SSH identity; safe to show for authorized_keys. */
33
+ bootstrapSshPublicKey?: string;
32
34
  privateNetworkAttachment?: GuestPrivateNetworkAttachment;
33
35
  /** Stable API ingress identity; signed with the rest of the enrolment body. */
34
36
  edgeHostname?: string;
@@ -147,6 +147,7 @@ async function enrolGuestIdentity(options) {
147
147
  token,
148
148
  label: options.label,
149
149
  gitDeployPublicKey: options.gitDeployPublicKey,
150
+ bootstrapSshPublicKey: options.bootstrapSshPublicKey,
150
151
  privateNetworkAttachment: options.privateNetworkAttachment,
151
152
  edgeHostname: options.edgeHostname?.trim() || undefined,
152
153
  publicKeys: {
@@ -0,0 +1,39 @@
1
+ export type HostMaintenanceOperation = 'transactions' | 'schema-plan' | 'schema-apply';
2
+ export interface HostMaintenanceRequest {
3
+ operation: HostMaintenanceOperation;
4
+ prepareBlankFzTest?: boolean;
5
+ confirmation?: string;
6
+ fenceEvidencePath?: string;
7
+ }
8
+ export interface HostMaintenancePlan {
9
+ operation: HostMaintenanceOperation;
10
+ mutation: boolean;
11
+ activeSlot: 'blue' | 'green';
12
+ workingDirectory: string;
13
+ unit: string;
14
+ credentialNames: readonly string[];
15
+ argv: readonly string[];
16
+ }
17
+ export interface HostMaintenanceResult {
18
+ plan: HostMaintenancePlan;
19
+ exitCode: number;
20
+ output: string;
21
+ }
22
+ export interface HostMaintenanceRuntime {
23
+ uid(): number;
24
+ read(path: string): string;
25
+ inspect(path: string): {
26
+ regular: boolean;
27
+ symbolic: boolean;
28
+ uid: number;
29
+ links: number;
30
+ mode: number;
31
+ size: number;
32
+ };
33
+ exec(argv: readonly string[]): Promise<{
34
+ exitCode: number;
35
+ output: string;
36
+ }>;
37
+ }
38
+ export declare function planHostMaintenance(request: HostMaintenanceRequest, runtime?: HostMaintenanceRuntime): HostMaintenancePlan;
39
+ export declare function applyHostMaintenance(request: HostMaintenanceRequest, runtime?: HostMaintenanceRuntime): Promise<HostMaintenanceResult>;
@@ -0,0 +1,135 @@
1
+ // @bun
2
+ // src/host-maintenance.ts
3
+ import { lstatSync, readFileSync } from "fs";
4
+ var ROOT = "/opt/forgezero";
5
+ var SLOT_FILE = `${ROOT}/.forge-slot`;
6
+ var SHARED_ENV = `${ROOT}/shared/.env`;
7
+ var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
8
+ var FZ = "/usr/local/bin/fz";
9
+ var localRuntime = () => ({
10
+ uid: () => process.getuid?.() ?? -1,
11
+ read: (path) => readFileSync(path, "utf8"),
12
+ inspect(path) {
13
+ const value = lstatSync(path);
14
+ return {
15
+ regular: value.isFile(),
16
+ symbolic: value.isSymbolicLink(),
17
+ uid: value.uid,
18
+ links: value.nlink,
19
+ mode: value.mode,
20
+ size: value.size
21
+ };
22
+ },
23
+ async exec(argv) {
24
+ const child = Bun.spawn([...argv], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
25
+ const [stdout, stderr, exitCode] = await Promise.all([
26
+ new Response(child.stdout).text(),
27
+ new Response(child.stderr).text(),
28
+ child.exited
29
+ ]);
30
+ return { exitCode, output: `${stdout}${stderr}`.slice(0, 256 * 1024) };
31
+ }
32
+ });
33
+ var trustedFile = (runtime, path, label, modeMask, max) => {
34
+ const metadata = runtime.inspect(path);
35
+ if (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & modeMask) !== 0 || metadata.size < 1 || metadata.size > max) {
36
+ throw new Error(`${label} is not a trusted root-owned regular file`);
37
+ }
38
+ };
39
+ function activeSlot(runtime) {
40
+ trustedFile(runtime, SLOT_FILE, "active slot marker", 18, 64);
41
+ const slot = runtime.read(SLOT_FILE).trim();
42
+ if (slot !== "blue" && slot !== "green")
43
+ throw new Error("active slot marker must be blue or green");
44
+ return slot;
45
+ }
46
+ var confirmation = (value) => {
47
+ if (!value || !/^apply-schema-consolidation-fz-[a-f0-9]{64}$/.test(value)) {
48
+ throw new Error("schema apply requires the exact fz plan confirmation");
49
+ }
50
+ return value;
51
+ };
52
+ function planHostMaintenance(request, runtime = localRuntime()) {
53
+ if (!["transactions", "schema-plan", "schema-apply"].includes(request.operation)) {
54
+ throw new Error("unsupported host maintenance operation");
55
+ }
56
+ const slot = activeSlot(runtime);
57
+ const workingDirectory = `${ROOT}/slots/${slot}`;
58
+ const common = [
59
+ "/usr/bin/systemd-run",
60
+ "--quiet",
61
+ "--wait",
62
+ "--pipe",
63
+ "--collect",
64
+ `--unit=forgezero-${request.operation}`,
65
+ "--property=User=forgezero",
66
+ `--property=WorkingDirectory=${workingDirectory}`,
67
+ `--property=EnvironmentFile=${SHARED_ENV}`,
68
+ "--property=Environment=NODE_ENV=production",
69
+ `--property=LoadCredentialEncrypted=arangodb-jwt:${JWT}`
70
+ ];
71
+ let child;
72
+ const credentials = ["arangodb-jwt"];
73
+ if (request.operation === "transactions") {
74
+ if (request.confirmation || request.fenceEvidencePath)
75
+ throw new Error("transaction rehearsal does not accept schema coordinates");
76
+ child = [
77
+ "--property=Environment=FZ_DATABASE_MODE=test",
78
+ "--property=Environment=ARANGO_DB=fz_test",
79
+ FZ,
80
+ "rehearse",
81
+ "transactions",
82
+ "--root",
83
+ workingDirectory,
84
+ "--confirm-staging-fz-test",
85
+ ...request.prepareBlankFzTest ? ["--prepare-blank-fz-test"] : []
86
+ ];
87
+ } else if (request.operation === "schema-plan") {
88
+ if (request.prepareBlankFzTest || request.confirmation || request.fenceEvidencePath) {
89
+ throw new Error("schema plan accepts no mutation coordinates");
90
+ }
91
+ child = [FZ, "db", "consolidate", "--root", workingDirectory, "--json"];
92
+ } else {
93
+ if (request.prepareBlankFzTest)
94
+ throw new Error("schema apply does not accept staging preparation");
95
+ const evidence = request.fenceEvidencePath;
96
+ if (!evidence?.startsWith("/") || /[\r\n:]/.test(evidence))
97
+ throw new Error("schema apply requires an absolute fence-evidence path");
98
+ trustedFile(runtime, evidence, "schema fence evidence", 63, 2 * 1024 * 1024);
99
+ credentials.push("schema-fence-evidence");
100
+ child = [
101
+ "--property=Environment=FZ_RECOVERY_FLEET_FENCED=all-api-and-scheduler-replicas-are-stopped",
102
+ `--property=LoadCredential=schema-fence-evidence:${evidence}`,
103
+ FZ,
104
+ "db",
105
+ "consolidate",
106
+ "--root",
107
+ workingDirectory,
108
+ "--json",
109
+ "--apply",
110
+ `--confirm=${confirmation(request.confirmation)}`
111
+ ];
112
+ }
113
+ return {
114
+ operation: request.operation,
115
+ mutation: request.operation !== "schema-plan",
116
+ activeSlot: slot,
117
+ workingDirectory,
118
+ unit: `forgezero-${request.operation}`,
119
+ credentialNames: credentials,
120
+ argv: [...common, ...child]
121
+ };
122
+ }
123
+ async function applyHostMaintenance(request, runtime = localRuntime()) {
124
+ if (runtime.uid() !== 0)
125
+ throw new Error("fz host --apply must run as root");
126
+ const plan = planHostMaintenance(request, runtime);
127
+ const result = await runtime.exec(plan.argv);
128
+ if (result.exitCode !== 0)
129
+ throw new Error(`host maintenance failed (${result.exitCode}): ${result.output.trim()}`);
130
+ return { plan, ...result };
131
+ }
132
+ export {
133
+ planHostMaintenance,
134
+ applyHostMaintenance
135
+ };
package/dist/index.d.ts CHANGED
@@ -18,8 +18,8 @@ export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from '.
18
18
  export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
19
19
  export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, GuestAccess, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
20
20
  export { pullMigrationOnce, startMigrationPull } from './migration-pull';
21
- export { executeSshBootstrap, pullSshBootstrapOnce, startSshBootstrapPull } from './ssh-bootstrap';
22
- export type { RemoteSshBootstrapClaim, SshBootstrapPullOptions, SshBootstrapResult } from './ssh-bootstrap';
21
+ export { executeMetalAdmission, executeSshBootstrap, pullMetalAdmissionOnce, pullSshBootstrapOnce, startMetalAdmissionPull, startSshBootstrapPull } from './ssh-bootstrap';
22
+ export type { MetalAdmissionProof, MetalAdmissionResult, RemoteMetalAdmissionClaim, RemoteSshBootstrapClaim, SshBootstrapPullOptions, SshBootstrapResult } from './ssh-bootstrap';
23
23
  export type { MigrationAction, MigrationEvidence, MigrationNetwork, MigrationPullOptions, MigrationPullResult, RemoteMigrationClaim } from './migration-pull';
24
24
  export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
25
25
  export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
@@ -32,6 +32,8 @@ export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper }
32
32
  export { DEFAULT_LIFECYCLE_HELPER_SOCKET, executeLifecycleAction, loadLifecycleProfile, requestLifecycleAction, startLifecycleHelper, validateLifecycleProfile } from './lifecycle-helper';
33
33
  export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './lifecycle-helper';
34
34
  export { materializeWarpMdm, renderWarpMdm } from './warp-config';
35
+ export { configureMeshConnector, readMeshConnectorCredential } from './mesh-connector';
36
+ export type { MeshConnectorCommandResult, MeshConnectorExec } from './mesh-connector';
35
37
  export { compareVersions, restoreAgentRelease, selectAgentRelease, stageAgentRelease, validateAgentRelease, DEFAULT_AGENT_RELEASE_ROOT, DEFAULT_AGENT_UPDATE_SOCKET, MAX_AGENT_TARBALL_BYTES } from './agent-update';
36
38
  export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResult } from './agent-update';
37
39
  export { activateAgentRelease, probeAgentSocket, readAgentUpdateReceipt, recoverInterruptedAgentUpdate, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_JOURNAL, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
@@ -0,0 +1,16 @@
1
+ export interface MeshConnectorCommandResult {
2
+ exitCode: number;
3
+ output: string;
4
+ }
5
+ export type MeshConnectorExec = (argv: readonly string[]) => Promise<MeshConnectorCommandResult>;
6
+ /** Read one systemd credential without accepting an arbitrary filesystem path. */
7
+ export declare function readMeshConnectorCredential(name?: string): string;
8
+ /**
9
+ * Register and connect one Cloudflare Mesh node. The management API token is
10
+ * never present; this consumes only the connector-specific registration token
11
+ * returned by the attended bootstrap.
12
+ */
13
+ export declare function configureMeshConnector(input: {
14
+ token: string;
15
+ exec: MeshConnectorExec;
16
+ }): Promise<void>;
@@ -0,0 +1,46 @@
1
+ // src/mesh-connector.ts
2
+ import { constants, closeSync, fstatSync, openSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
5
+ function readMeshConnectorCredential(name = "CF_WARP_CONNECTOR_TOKEN") {
6
+ if (!/^[A-Z_][A-Z0-9_]{0,63}$/.test(name))
7
+ throw new Error("Mesh connector credential name is invalid");
8
+ const directory = process.env.CREDENTIALS_DIRECTORY;
9
+ if (!directory?.startsWith("/run/credentials/"))
10
+ throw new Error("Mesh connector requires a systemd credential directory");
11
+ const path = join(directory, name);
12
+ let descriptor;
13
+ try {
14
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
15
+ const metadata = fstatSync(descriptor);
16
+ if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 40 || metadata.size > 16384) {
17
+ throw new Error("Mesh connector systemd credential is malformed");
18
+ }
19
+ const token = readFileSync(descriptor, "utf8").trim();
20
+ if (!TOKEN.test(token))
21
+ throw new Error("Mesh connector systemd credential is malformed");
22
+ return token;
23
+ } finally {
24
+ if (descriptor !== undefined)
25
+ closeSync(descriptor);
26
+ }
27
+ }
28
+ async function configureMeshConnector(input) {
29
+ if (!TOKEN.test(input.token))
30
+ throw new Error("Mesh connector token is malformed");
31
+ const registration = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "connector", "new", input.token]);
32
+ if (registration.exitCode !== 0 && !/already|registered/i.test(registration.output)) {
33
+ throw new Error("Cloudflare Mesh connector registration failed");
34
+ }
35
+ const connected = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "connect"]);
36
+ if (connected.exitCode !== 0)
37
+ throw new Error("Cloudflare Mesh connector connect failed");
38
+ const status = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
39
+ if (status.exitCode !== 0 || !/\bconnected\b/i.test(status.output) || /\bdisconnected\b/i.test(status.output)) {
40
+ throw new Error("Cloudflare Mesh connector did not become connected");
41
+ }
42
+ }
43
+ export {
44
+ readMeshConnectorCredential,
45
+ configureMeshConnector
46
+ };
@@ -233,7 +233,6 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
233
233
  }
234
234
 
235
235
  // src/egress-policy.ts
236
- var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
237
236
  var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
238
237
  var BLOCKED_IPV4 = [
239
238
  "0.0.0.0/8",
@@ -293,7 +292,137 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
293
292
  }
294
293
 
295
294
  // src/version.ts
296
- var VERSION = "0.1.40";
295
+ var VERSION = "0.1.42";
296
+
297
+ // src/otel-collector.ts
298
+ var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
299
+ var FORGEZERO_OTEL_COLLECTOR_VERSION = "0.157.0";
300
+ var FORGEZERO_OTEL_COLLECTOR_SHA256 = "2937cf24892af55b143c072fddece17862239cf78280620029276493eb81beae";
301
+ var ARCHIVE = "/run/forgezero-otelcol.tar.gz";
302
+ var EXTRACT = "/run/forgezero-otelcol-release";
303
+ var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
304
+ var CONFIG = "/etc/forgezero/otelcol.yaml";
305
+ var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
306
+ var checked2 = async (host, argv) => {
307
+ const result = await host.exec(argv);
308
+ const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
309
+ if (result.exitCode !== 0)
310
+ throw new Error(`OTLP collector operation failed: ${argv.join(" ")}${output.trim() ? `: ${output.trim()}` : ""}`);
311
+ return output.trim();
312
+ };
313
+ var destination = (value) => {
314
+ let url;
315
+ try {
316
+ url = new URL(value);
317
+ } catch {
318
+ throw new Error("OTLP collector export endpoint must be an absolute HTTPS URL");
319
+ }
320
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port && url.port !== "443") {
321
+ throw new Error("OTLP collector export endpoint must be credential-free HTTPS");
322
+ }
323
+ return url.toString().replace(/\/$/, "");
324
+ };
325
+ function renderForgeZeroOtelCollector(exportEndpoint) {
326
+ const endpoint = destination(exportEndpoint);
327
+ return {
328
+ config: `receivers:
329
+ otlp:
330
+ protocols:
331
+ http:
332
+ endpoint: 127.0.0.1:4318
333
+ exporters:
334
+ otlphttp:
335
+ endpoint: ${JSON.stringify(endpoint)}
336
+ service:
337
+ telemetry:
338
+ metrics:
339
+ address: 127.0.0.1:8888
340
+ pipelines:
341
+ traces:
342
+ receivers: [otlp]
343
+ exporters: [otlphttp]
344
+ metrics:
345
+ receivers: [otlp]
346
+ exporters: [otlphttp]
347
+ logs:
348
+ receivers: [otlp]
349
+ exporters: [otlphttp]
350
+ `,
351
+ unit: `[Unit]
352
+ Description=ForgeZero independently supervised OpenTelemetry Collector
353
+ After=network-online.target
354
+ Wants=network-online.target
355
+
356
+ [Service]
357
+ Type=simple
358
+ User=forgezero-otel
359
+ Group=forgezero-otel
360
+ ExecStart=${BINARY} --config=${CONFIG}
361
+ Restart=always
362
+ RestartSec=2
363
+ LimitCORE=0
364
+ NoNewPrivileges=true
365
+ PrivateTmp=true
366
+ ProtectSystem=strict
367
+ ProtectHome=true
368
+ ProtectKernelTunables=true
369
+ ProtectKernelModules=true
370
+ ProtectControlGroups=true
371
+ RestrictSUIDSGID=true
372
+ RestrictRealtime=true
373
+ LockPersonality=true
374
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
375
+
376
+ [Install]
377
+ WantedBy=multi-user.target
378
+ `
379
+ };
380
+ }
381
+ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
382
+ const rendered = renderForgeZeroOtelCollector(exportEndpoint);
383
+ const observed = await host.exec([BINARY, "--version"]);
384
+ const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
385
+ if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
386
+ await checked2(host, [
387
+ "/usr/bin/curl",
388
+ "--fail",
389
+ "--silent",
390
+ "--show-error",
391
+ "--location",
392
+ "--proto",
393
+ "=https",
394
+ "--output",
395
+ ARCHIVE,
396
+ `https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
397
+ ]);
398
+ const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
399
+ if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
400
+ throw new Error("OTLP collector archive checksum mismatch");
401
+ await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
402
+ await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
403
+ await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
404
+ await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
405
+ }
406
+ if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
407
+ await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
408
+ }
409
+ if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
410
+ await checked2(host, [
411
+ "/usr/sbin/useradd",
412
+ "--system",
413
+ "--no-create-home",
414
+ "--shell",
415
+ "/usr/sbin/nologin",
416
+ "--gid",
417
+ "forgezero-otel",
418
+ "forgezero-otel"
419
+ ]);
420
+ }
421
+ host.write(CONFIG, rendered.config, 420);
422
+ host.write(UNIT, rendered.unit, 420);
423
+ await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
424
+ await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
425
+ }
297
426
 
298
427
  // src/metal-bootstrap.ts
299
428
  var PROFILE_PATH = "/etc/forgezero/metal.json";
@@ -306,7 +435,7 @@ var HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
306
435
  var UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
307
436
  var MAX_CONFIG_BYTES = 256 * 1024;
308
437
  var SUPPORTED_BUN_VERSION = "1.3.14";
309
- var SUPPORTED_BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
438
+ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
310
439
 
311
440
  class MetalBootstrapError extends Error {
312
441
  }
@@ -373,7 +502,7 @@ function validateMetalBootstrapConfig(config) {
373
502
  "housekeepingCpus",
374
503
  "housekeepingMemoryNodes",
375
504
  "bunVersion",
376
- "bunInstallerSha256",
505
+ "bunReleaseSha256",
377
506
  "agentVersion",
378
507
  "confidential"
379
508
  ], "metal profile");
@@ -389,7 +518,7 @@ function validateMetalBootstrapConfig(config) {
389
518
  if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
390
519
  throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
391
520
  }
392
- if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunInstallerSha256 !== SUPPORTED_BUN_INSTALLER_SHA256 || config.profile.agentVersion !== VERSION) {
521
+ if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
393
522
  throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
394
523
  }
395
524
  let api;
@@ -413,6 +542,9 @@ function validateMetalBootstrapConfig(config) {
413
542
  throw new MetalBootstrapError("metal host OTLP must use exact loopback http://127.0.0.1:4318");
414
543
  }
415
544
  validUnit(config.hostTelemetryUnit);
545
+ if (config.hostTelemetryUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
546
+ throw new MetalBootstrapError(`metal bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
547
+ }
416
548
  if (config.agentSeedFile)
417
549
  validateOwnerOnlyPath(config.agentSeedFile, false);
418
550
  return config;
@@ -452,13 +584,14 @@ function planMetalBootstrap(config) {
452
584
  metalHostname: config.metalHostname,
453
585
  profilePath: PROFILE_PATH,
454
586
  units: [
587
+ FORGEZERO_OTEL_COLLECTOR_UNIT,
455
588
  "forgezero-metal-helper.service",
456
589
  "forgezero-agent-update-helper.service",
457
590
  "forgezero-metal-agent-egress.service",
458
591
  "forgezero-metal-agent.service"
459
592
  ],
460
593
  steps: [
461
- "install fixed KVM/LVM/cloud-init/nftables prerequisites",
594
+ "install fixed KVM/LVM/cloud-init/nftables prerequisites and the checksum-verified OTLP collector",
462
595
  "verify bridge, volume group, pinned guest image, KVM and optional SEV device",
463
596
  "install the published Agent binary and root-owned immutable metal profile",
464
597
  "seal or generate the metal identity seed as a systemd credential",
@@ -755,8 +888,13 @@ async function applyMetalBootstrap(config, options) {
755
888
  "cloud-image-utils",
756
889
  "lvm2",
757
890
  "nftables",
758
- "curl"
891
+ "curl",
892
+ "ca-certificates"
759
893
  ]);
894
+ await ensureForgeZeroOtelCollector({
895
+ write: atomicWrite,
896
+ exec
897
+ }, config.profile.agentTelemetryEndpoint);
760
898
  await ensurePinnedGuestImage(config, exec);
761
899
  await preflight(config, exec);
762
900
  await ensureAccount(exec);
@@ -339,45 +339,70 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
339
339
  encoding: b64
340
340
  content: ${base64(content)}
341
341
  `;
342
- function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
342
+ function guestBootstrapOperations(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
343
343
  validateMetalProfile(profile);
344
344
  const agentBun = "/usr/local/lib/forgezero/bun";
345
- const telemetryEndpoint = `'${profile.agentTelemetryEndpoint.replace(/'/g, `'\\''`)}'`;
346
- const attestationSetup = attested ? `# The report device is not part of the encryption path, so a guest can appear
347
- # healthy and encrypted while attestation is silently impossible. Install and
348
- # load the driver shipped by the one pinned image before the agent starts. Do
349
- # not ask an evolving apt repository for an exact old kernel package: the image
350
- # can keep booting after Canonical removes that package from the mirror.
351
- if [[ ! -c /dev/sev-guest ]]; then modprobe sev-guest; fi
352
- test -c /dev/sev-guest
353
- printf 'sev-guest
354
- ' >/etc/modules-load.d/sev-guest.conf
355
- ` : "";
356
- return `#!/usr/bin/env bash
357
- set -Eeuo pipefail
358
- ${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
359
- curl -fsSL https://bun.sh/install -o /run/fz-bun-install
360
- printf '%s %s
361
- ' '${profile.bunInstallerSha256}' /run/fz-bun-install | sha256sum -c -
362
- HOME=/root BUN_INSTALL=${agentBun} BUN_VERSION=${profile.bunVersion} bash /run/fz-bun-install
363
- install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
364
- rm -f /run/fz-bun-install
365
- fi
366
- if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
367
- env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
368
- fi
369
- env FZ_API=${profile.apiUrl} OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_AGENT_EGRESS_ENFORCE=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
370
- `;
345
+ return [
346
+ ...attested ? [
347
+ ["/usr/sbin/modprobe", "sev-guest"],
348
+ ["/usr/bin/test", "-c", "/dev/sev-guest"]
349
+ ] : [],
350
+ [
351
+ "/usr/bin/curl",
352
+ "--fail",
353
+ "--silent",
354
+ "--show-error",
355
+ "--location",
356
+ "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip",
357
+ "--output",
358
+ "/run/forgezero-bun.zip"
359
+ ],
360
+ ["/usr/bin/sha256sum", "--check", "/etc/forgezero/bun.sha256"],
361
+ ["/usr/bin/unzip", "-q", "/run/forgezero-bun.zip", "-d", "/run/forgezero-bun"],
362
+ ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/runtime", agentBun],
363
+ ["/usr/bin/install", "-m", "0755", "/run/forgezero-bun/bun-linux-x64/bun", "/usr/local/lib/forgezero/runtime/bun"],
364
+ ["/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun"],
365
+ [
366
+ "/usr/bin/env",
367
+ `BUN_INSTALL=${agentBun}`,
368
+ "/usr/local/bin/bun",
369
+ "add",
370
+ "-g",
371
+ "--no-cache",
372
+ "--force",
373
+ `@forgezero/agent@${profile.agentVersion}`
374
+ ],
375
+ [
376
+ "/usr/bin/env",
377
+ `FZ_API=${profile.apiUrl}`,
378
+ `OTEL_EXPORTER_OTLP_ENDPOINT=${profile.agentTelemetryEndpoint}`,
379
+ "FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent",
380
+ "FZ_AGENT_USER=forgezero-agent",
381
+ "FZ_SOCKET_PATH=/run/forgezero/vault.sock",
382
+ "FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred",
383
+ "FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred",
384
+ "FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub",
385
+ "FZ_DEPLOY_ROOT=/opt/forgezero",
386
+ `FZ_DEPLOY_PULL=${hasEnrolment}`,
387
+ `FZ_AGENT_EGRESS_ENFORCE=${hasEnrolment}`,
388
+ `FZ_NODE_LABEL=${nodeLabel}`,
389
+ `${agentBun}/bin/fz`,
390
+ "agent",
391
+ "install",
392
+ "--apply",
393
+ ...hasEnrolment ? ["--enrol"] : []
394
+ ]
395
+ ];
371
396
  }
372
397
  function cloudInit(profile, claim, manifest) {
373
398
  const enrolled = Boolean(claim.enrolment);
374
- const bootstrap = guestBootstrapScript(profile, claim.spec.confidential, enrolled, manifest.name);
399
+ const bootstrap = guestBootstrapOperations(profile, claim.spec.confidential, enrolled, manifest.name);
375
400
  const access = claim.access;
376
401
  const users = access ? `users:
377
402
  - default
378
403
  - name: ${access.sshUser}
379
404
  groups: [sudo]
380
- shell: /bin/bash
405
+ shell: /bin/dash
381
406
  sudo: ALL=(ALL) NOPASSWD:ALL
382
407
  ssh_authorized_keys:
383
408
  ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
@@ -385,6 +410,10 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
385
410
  ` : "";
386
411
  const enrolmentFiles = enrolled ? yamlFile("/run/forgezero-enrol-token", `${claim.enrolment.token}
387
412
  `, "0600") : "";
413
+ const attestationFile = claim.spec.confidential ? yamlFile("/etc/modules-load.d/sev-guest.conf", `sev-guest
414
+ `, "0644") : "";
415
+ const bunChecksum = yamlFile("/etc/forgezero/bun.sha256", `${profile.bunReleaseSha256} /run/forgezero-bun.zip
416
+ `, "0600");
388
417
  return {
389
418
  userData: `#cloud-config
390
419
  package_update: true
@@ -393,8 +422,9 @@ ${users}disable_root: true
393
422
  # every dynamically claimed repository must be cloneable on a clean image.
394
423
  packages: [curl, ca-certificates, openssl, openssh-client, git, unzip${claim.spec.confidential ? ", python3" : ""}]
395
424
  write_files:
396
- ${enrolmentFiles}${yamlFile("/usr/local/sbin/forgezero-guest-bootstrap", bootstrap, "0700")}runcmd:
397
- - [ bash, /usr/local/sbin/forgezero-guest-bootstrap ]
425
+ ${enrolmentFiles}${attestationFile}${bunChecksum}runcmd:
426
+ ${bootstrap.map((argv) => ` - ${JSON.stringify(argv)}`).join(`
427
+ `)}
398
428
  `,
399
429
  metaData: `instance-id: ${manifest.name}-${claim.attempt}
400
430
  local-hostname: ${manifest.name}
@@ -37,7 +37,7 @@ export interface MetalProvisionProfile {
37
37
  /** NUMA nodes reserved for host allocations when the topology permits it. */
38
38
  housekeepingMemoryNodes?: string;
39
39
  bunVersion: string;
40
- bunInstallerSha256: string;
40
+ bunReleaseSha256: string;
41
41
  agentVersion: string;
42
42
  confidential?: {
43
43
  cbitpos: number;
@@ -72,7 +72,7 @@ export declare function validateMetalProfile(profile: MetalProvisionProfile): vo
72
72
  export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
73
73
  /** Stable tight-fit allocation of one exclusive CPU/NUMA pool. */
74
74
  export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuPool;
75
- export declare function guestBootstrapScript(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): string;
75
+ export declare function guestBootstrapOperations(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): readonly (readonly string[])[];
76
76
  export declare function cloudInit(profile: MetalProvisionProfile, claim: CreateRemoteProvisionClaim, manifest: GuestManifest): {
77
77
  userData: string;
78
78
  metaData: string;