@forgezero/agent 0.1.63 → 0.1.65

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.
package/dist/fz.js CHANGED
@@ -4811,8 +4811,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
4813
  init_dist();
4814
- import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
4815
- import { basename as basename3, dirname as dirname13, isAbsolute as isAbsolute6, resolve as resolve10 } from "path";
4814
+ import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
4815
+ import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve11 } from "path";
4816
4816
  import { fileURLToPath as fileURLToPath3 } from "url";
4817
4817
  import { hostname } from "os";
4818
4818
 
@@ -4829,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4829
4829
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4830
4830
 
4831
4831
  // src/version.ts
4832
- var VERSION2 = "0.1.63";
4832
+ var VERSION2 = "0.1.65";
4833
4833
 
4834
4834
  // src/software.ts
4835
4835
  var PINNED_BUN_VERSION = "1.3.14";
@@ -5838,17 +5838,16 @@ function agentUnit(options) {
5838
5838
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
5839
5839
  throw new Error("migration pull and lifecycle profile must be supplied together");
5840
5840
  }
5841
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
5842
- if ([
5843
- options.pullBootstrap,
5844
- options.bootstrapSshCredentialPath,
5845
- options.bootstrapSshPublicKeyPath,
5846
- options.bootstrapTargetTelemetryEndpoint
5847
- ].some(Boolean) && !bootstrapEnabled) {
5848
- throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
5841
+ const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
5842
+ if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath)) {
5843
+ throw new Error("bootstrap SSH credential and public key paths must be supplied together");
5844
+ }
5845
+ const bootstrapEnabled = Boolean(options.pullBootstrap);
5846
+ if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
5847
+ throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
5849
5848
  }
5850
5849
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
5851
- const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5850
+ const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5852
5851
  const warpValues = [
5853
5852
  options.warpOrganization,
5854
5853
  options.warpClientIdCredentialPath,
@@ -6087,15 +6086,13 @@ function planProvision(options) {
6087
6086
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
6088
6087
  throw new Error("migration pull and lifecycle profile must be supplied together");
6089
6088
  }
6090
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
6091
- if ([
6092
- options.pullBootstrap,
6093
- options.bootstrapSshCredentialPath,
6094
- options.bootstrapSshPublicKeyPath,
6095
- options.bootstrapSshSourcePath,
6096
- options.bootstrapTargetTelemetryEndpoint
6097
- ].some(Boolean) && !bootstrapEnabled) {
6098
- throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
6089
+ const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
6090
+ if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath) || options.bootstrapSshSourcePath && !bootstrapIdentityEnabled) {
6091
+ throw new Error("bootstrap SSH credential and public key paths must be supplied together");
6092
+ }
6093
+ const bootstrapEnabled = Boolean(options.pullBootstrap);
6094
+ if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
6095
+ throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
6099
6096
  }
6100
6097
  const warpValues = [
6101
6098
  options.warpOrganization,
@@ -6105,13 +6102,13 @@ function planProvision(options) {
6105
6102
  const warpEnabled = warpValues.every(Boolean);
6106
6103
  if (warpValues.some(Boolean) && !warpEnabled)
6107
6104
  throw new Error("WARP configuration must be supplied together");
6108
- const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
6109
- if (Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath) || options.enrolTokenSourcePath && !enrolmentEnabled) {
6110
- throw new Error("direct enrolment credential and state paths must be supplied together");
6105
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath);
6106
+ if (options.enrolTokenCredentialPath && !options.enrolStatePath || options.enrolTokenSourcePath && (!options.enrolTokenCredentialPath || !options.enrolStatePath)) {
6107
+ throw new Error("direct enrolment credential requires its durable state path");
6111
6108
  }
6112
6109
  const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
6113
6110
  const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
6114
- const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
6111
+ const enrolStatePath = options.enrolStatePath ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
6115
6112
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
6116
6113
  const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
6117
6114
  const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
@@ -6123,8 +6120,8 @@ function planProvision(options) {
6123
6120
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
6124
6121
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
6125
6122
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
6126
- const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
6127
- const bootstrapSshPublicKeyPath = bootstrapEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
6123
+ const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
6124
+ const bootstrapSshPublicKeyPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
6128
6125
  const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
6129
6126
  const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
6130
6127
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
@@ -6224,7 +6221,7 @@ function planProvision(options) {
6224
6221
  step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
6225
6222
  step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
6226
6223
  ] : [],
6227
- ...bootstrapEnabled ? [
6224
+ ...bootstrapIdentityEnabled ? [
6228
6225
  step("bootstrap SSH public identity directory", { kind: "directories", directories: [
6229
6226
  { path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
6230
6227
  ] }),
@@ -6260,6 +6257,10 @@ function planProvision(options) {
6260
6257
  { argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
6261
6258
  { argv: ["/usr/bin/systemctl", "daemon-reload"] }
6262
6259
  ] })] : [],
6260
+ ...enrolStatePath && !enrolmentEnabled ? [step("retire consumed enrolment unit", { kind: "commands", commands: [
6261
+ { argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-agent-enrol.service"], acceptedExitCodes: [0, 1, 5] },
6262
+ { argv: ["/usr/bin/rm", "-f", ENROLMENT_UNIT_PATH] }
6263
+ ] })] : [],
6263
6264
  step("enable and converge services", { kind: "commands", commands: [
6264
6265
  { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
6265
6266
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
@@ -6595,6 +6596,16 @@ async function localRunner(operation) {
6595
6596
  `, exitCode: 1 };
6596
6597
  }
6597
6598
  }
6599
+ function resolveGitIdentityInstallOptions(environment) {
6600
+ const generateGitIdentity = environment.FZ_GENERATE_GIT_DEPLOY_KEY === "true";
6601
+ const explicitCredential = environment.FZ_GIT_CREDENTIAL_PATH?.trim() || undefined;
6602
+ const explicitPublicKey = environment.FZ_GIT_PUBLIC_KEY_PATH?.trim() || undefined;
6603
+ return {
6604
+ gitCredentialPath: explicitCredential ?? (generateGitIdentity ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined),
6605
+ gitPublicKeyPath: explicitPublicKey ?? (generateGitIdentity ? "/etc/forgezero/git/deploy.pub" : undefined),
6606
+ generateGitIdentity
6607
+ };
6608
+ }
6598
6609
  function planInstall(options) {
6599
6610
  const { capabilities, ...unit } = options;
6600
6611
  return planProvision({ ...unit, mode: modeFor(capabilities) });
@@ -14188,7 +14199,15 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
14188
14199
  if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
14189
14200
  await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
14190
14201
  }
14191
- await host.installAgent(config);
14202
+ if (alreadyEnrolled) {
14203
+ await host.installAgent(config, "bound");
14204
+ } else if (config.kind === "platform") {
14205
+ if (bootstrapManifest)
14206
+ await host.installAgent(config, "bootstrap");
14207
+ } else {
14208
+ await host.installAgent(config, "enrol");
14209
+ await host.installAgent(config, "bound");
14210
+ }
14192
14211
  if (config.kind === "platform" && config.firewall.enabled) {
14193
14212
  await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
14194
14213
  } else
@@ -14338,7 +14357,19 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
14338
14357
  `, 384);
14339
14358
  host.remove(config.bootstrapBundle.bundleFile);
14340
14359
  host.remove(config.bootstrapBundle.manifestFile);
14341
- await host.installAgent(config);
14360
+ }
14361
+ if (!alreadyEnrolled) {
14362
+ await checked3(host, [
14363
+ "curl",
14364
+ "--fail",
14365
+ "--silent",
14366
+ "--show-error",
14367
+ "--max-time",
14368
+ "10",
14369
+ `http://127.0.0.1:3000${runtime.healthPath}`
14370
+ ], "release-one API health");
14371
+ await host.installAgent(config, "enrol");
14372
+ await host.installAgent(config, "bound");
14342
14373
  }
14343
14374
  }
14344
14375
  if (config.cloudflareHandoff) {
@@ -14507,6 +14538,65 @@ function readBootstrapConfig(path) {
14507
14538
  }
14508
14539
  return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync9(path, "utf8"))));
14509
14540
  }
14541
+ function planBootstrapAgentInstall(config, phase, context) {
14542
+ const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
14543
+ if (phase === "bootstrap") {
14544
+ if (config.kind !== "platform" || hasBinding || !initialBundle) {
14545
+ throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
14546
+ }
14547
+ } else if (phase === "enrol") {
14548
+ if (hasBinding || !hasEnrolCredential) {
14549
+ throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
14550
+ }
14551
+ } else if (!hasBinding) {
14552
+ throw new Error("bound Agent convergence requires durable enrolment state");
14553
+ }
14554
+ const bound = phase === "bound";
14555
+ const enrolling = phase === "enrol";
14556
+ const authenticated = enrolling || bound;
14557
+ const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
14558
+ const options = {
14559
+ capabilities,
14560
+ socketPath: DEFAULT_SOCKET,
14561
+ seedPath: "/var/lib/forgezero/node.seed",
14562
+ controlSocketPath: CONTROL_SOCKET,
14563
+ repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
14564
+ branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
14565
+ bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
14566
+ bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
14567
+ profile: config.profile,
14568
+ deployRoot: config.deployRoot ?? "/opt/forgezero",
14569
+ deploymentCredentials: config.deploymentCredentials,
14570
+ publicApiUrl: config.apiUrl,
14571
+ gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
14572
+ gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
14573
+ generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
14574
+ pullDeployments: bound,
14575
+ pullMigrations: bound && config.kind === "platform",
14576
+ pullBootstrap: bound && bootstrapRunner,
14577
+ bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
14578
+ bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
14579
+ bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
14580
+ bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
14581
+ lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
14582
+ enforceEgress: true,
14583
+ nodeHostname: config.nodeHostname,
14584
+ telemetryEndpoint: config.telemetryEndpoint,
14585
+ binPath: "/usr/local/lib/forgezero/agent/fz-agent",
14586
+ sourceBinPath: PACKAGED_AGENT_BIN,
14587
+ ...enrolling ? {
14588
+ enrolTokenCredentialPath: ENROL_CREDENTIAL,
14589
+ enrolStatePath: "/var/lib/forgezero/enrolment.json"
14590
+ } : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
14591
+ ...authenticated ? {
14592
+ apiUrl: config.apiUrl,
14593
+ project: config.kind === "enrolled-compute" ? config.realm : "platform",
14594
+ environment: config.kind === "enrolled-compute" ? undefined : config.environment,
14595
+ nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
14596
+ } : {}
14597
+ };
14598
+ return planInstall(options);
14599
+ }
14510
14600
  function localBootstrapHost() {
14511
14601
  const execute = async (argv2, options = {}) => {
14512
14602
  const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
@@ -14554,10 +14644,10 @@ function localBootstrapHost() {
14554
14644
  throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
14555
14645
  return result;
14556
14646
  },
14557
- async installAgent(config) {
14647
+ async installAgent(config, phase) {
14558
14648
  const capabilities = await readCapabilities(localRunner);
14559
- const deployRoot = config.deployRoot ?? "/opt/forgezero";
14560
14649
  const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
14650
+ const hasEnrolCredential = existsSync9(ENROL_CREDENTIAL);
14561
14651
  const initialBundle = config.kind === "platform" && !existsSync9(BOOTSTRAP_RELEASE_EVIDENCE) ? {
14562
14652
  path: config.bootstrapBundle.bundleFile,
14563
14653
  manifestPath: config.bootstrapBundle.manifestFile,
@@ -14578,45 +14668,11 @@ function localBootstrapHost() {
14578
14668
  writeFileSync9(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
14579
14669
  `, { mode: 256 });
14580
14670
  }
14581
- const plan = planInstall({
14671
+ const plan = planBootstrapAgentInstall(config, phase, {
14582
14672
  capabilities,
14583
- socketPath: DEFAULT_SOCKET,
14584
- seedPath: "/var/lib/forgezero/node.seed",
14585
- controlSocketPath: "/run/forgezero/control.sock",
14586
- repository: initialBundle?.path ?? (config.kind === "enrolled-compute" ? config.repository : undefined),
14587
- branch: initialBundle?.manifest.branch ?? (config.kind === "enrolled-compute" ? config.branch : undefined),
14588
- bootstrapBundlePath: initialBundle?.path,
14589
- bootstrapBundleManifestPath: initialBundle?.manifestPath,
14590
- profile: config.kind === "platform" ? config.profile : config.profile,
14591
- deployRoot,
14592
- deploymentCredentials: config.deploymentCredentials,
14593
- publicApiUrl: config.apiUrl,
14594
- gitCredentialPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
14595
- gitPublicKeyPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
14596
- generateGitIdentity: config.kind === "enrolled-compute" && config.gitDeployKey === true,
14597
- pullDeployments: hasBinding,
14598
- pullMigrations: config.kind === "platform" && hasBinding,
14599
- pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
14600
- bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
14601
- bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
14602
- bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
14603
- bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
14604
- lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
14605
- enforceEgress: true,
14606
- nodeHostname: config.nodeHostname,
14607
- telemetryEndpoint: config.telemetryEndpoint,
14608
- binPath: "/usr/local/lib/forgezero/agent/fz-agent",
14609
- sourceBinPath: PACKAGED_AGENT_BIN,
14610
- ...existsSync9(ENROL_CREDENTIAL) || hasBinding ? {
14611
- enrolTokenCredentialPath: existsSync9(ENROL_CREDENTIAL) ? ENROL_CREDENTIAL : undefined,
14612
- enrolStatePath: "/var/lib/forgezero/enrolment.json"
14613
- } : {},
14614
- ...hasBinding ? {
14615
- apiUrl: config.apiUrl,
14616
- project: config.kind === "enrolled-compute" ? config.realm : "platform",
14617
- environment: config.kind === "enrolled-compute" ? undefined : config.environment,
14618
- nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
14619
- } : {}
14673
+ hasBinding,
14674
+ hasEnrolCredential,
14675
+ initialBundle
14620
14676
  });
14621
14677
  for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
14622
14678
  mkdirSync9(dirname9(unit.path), { recursive: true, mode: 493 });
@@ -15671,10 +15727,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
15671
15727
 
15672
15728
  // src/operator-bootstrap.ts
15673
15729
  import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
15674
- import { lstatSync as lstatSync7, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
15730
+ import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
15675
15731
  import { isIP as isIP6 } from "net";
15676
15732
  import { tmpdir } from "os";
15677
- import { basename as basename2, join as join10 } from "path";
15733
+ import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
15678
15734
  import { fileURLToPath as fileURLToPath2 } from "url";
15679
15735
 
15680
15736
  // src/platform-genesis.ts
@@ -15809,6 +15865,25 @@ var exactKeys5 = (value, keys, label) => {
15809
15865
  throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
15810
15866
  return record3;
15811
15867
  };
15868
+ var canonicalOutputPath = (path, label) => {
15869
+ if (!isAbsolute5(path) || resolve9(path) !== path || /[\r\n\0]/.test(path)) {
15870
+ throw new Error(`${label} must be a canonical absolute path`);
15871
+ }
15872
+ return path;
15873
+ };
15874
+ var writeOwnerJson2 = (path, value, label) => {
15875
+ canonicalOutputPath(path, label);
15876
+ mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
15877
+ const parent = lstatSync7(dirname13(path));
15878
+ const uid = process.getuid?.() ?? parent.uid;
15879
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
15880
+ throw new Error(`${label} parent must be an owner-only directory`);
15881
+ }
15882
+ writeFileSync12(path, `${JSON.stringify(value, null, 2)}
15883
+ `, { mode: 384, flag: "wx" });
15884
+ chmodSync6(path, 384);
15885
+ return path;
15886
+ };
15812
15887
  var ownerFile = (path, limit, label) => {
15813
15888
  if (!path.startsWith("/") || /[\r\n]/.test(path))
15814
15889
  throw new Error(`${label} path must be absolute`);
@@ -15867,8 +15942,10 @@ var validateHop = (value, label) => {
15867
15942
  }
15868
15943
  return hop;
15869
15944
  };
15870
- function readOperatorPlatformBootstrapRequest(path) {
15871
- const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
15945
+ function createOperatorSshHop(input) {
15946
+ return validateHop({ ...input, hostKeySha256: fingerprint(input.hostKey) }, "operator SSH hop");
15947
+ }
15948
+ var validatePlatformRequestValue = (value) => {
15872
15949
  const root = exactKeys5(value, ["kind", "target", "platformConfigFile"], "operator bootstrap request");
15873
15950
  if (root.kind !== "platform-remote")
15874
15951
  throw new Error("operator bootstrap kind must be platform-remote");
@@ -15887,6 +15964,7 @@ function readOperatorPlatformBootstrapRequest(path) {
15887
15964
  socketPath(targetValue.agentSocket);
15888
15965
  if (typeof root.platformConfigFile !== "string")
15889
15966
  throw new Error("platformConfigFile must be an absolute path");
15967
+ canonicalOutputPath(root.platformConfigFile, "platformConfigFile");
15890
15968
  const config = readBootstrapConfig(root.platformConfigFile);
15891
15969
  if (config.kind !== "platform")
15892
15970
  throw new Error("operator bootstrap requires a platform config");
@@ -15900,9 +15978,39 @@ function readOperatorPlatformBootstrapRequest(path) {
15900
15978
  ...targetValue.jump === undefined ? {} : { jump: validateHop(targetValue.jump, "operator bootstrap jump") }
15901
15979
  }
15902
15980
  };
15903
- }
15904
- function readOperatorMetalBootstrapRequest(path) {
15905
- const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
15981
+ };
15982
+ function readOperatorPlatformBootstrapRequest(path) {
15983
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
15984
+ return validatePlatformRequestValue(value);
15985
+ }
15986
+ function writeOperatorPlatformBootstrapRequest(path, request) {
15987
+ validatePlatformRequestValue(request);
15988
+ return writeOwnerJson2(path, request, "operator bootstrap request");
15989
+ }
15990
+ function readOperatorPlatformBootstrapFleetRequest(path) {
15991
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
15992
+ const root = exactKeys5(value, ["kind", "requests"], "operator fleet request");
15993
+ if (root.kind !== "platform-fleet-remote")
15994
+ throw new Error("operator fleet kind must be platform-fleet-remote");
15995
+ if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
15996
+ throw new Error("operator fleet requires exactly three request files");
15997
+ }
15998
+ const requests = root.requests.map((entry) => canonicalOutputPath(entry, "operator fleet request file"));
15999
+ if (new Set(requests).size !== 3)
16000
+ throw new Error("operator fleet request files must be unique");
16001
+ for (const request of requests)
16002
+ readOperatorPlatformBootstrapRequest(request);
16003
+ return { kind: "platform-fleet-remote", requests };
16004
+ }
16005
+ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
16006
+ const request = { kind: "platform-fleet-remote", requests: requestFiles };
16007
+ if (new Set(requestFiles).size !== 3)
16008
+ throw new Error("operator fleet request files must be unique");
16009
+ for (const file of requestFiles)
16010
+ readOperatorPlatformBootstrapRequest(file);
16011
+ return writeOwnerJson2(path, request, "operator fleet request");
16012
+ }
16013
+ var validateMetalRequestValue = (value) => {
15906
16014
  const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
15907
16015
  if (root.kind !== "metal-remote")
15908
16016
  throw new Error("operator metal bootstrap kind must be metal-remote");
@@ -15921,6 +16029,7 @@ function readOperatorMetalBootstrapRequest(path) {
15921
16029
  socketPath(targetValue.agentSocket);
15922
16030
  if (typeof root.metalConfigFile !== "string")
15923
16031
  throw new Error("metalConfigFile must be an absolute path");
16032
+ canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
15924
16033
  readMetalBootstrapConfig(root.metalConfigFile);
15925
16034
  const genesis = exactKeys5(root.genesis, ["environment", "sshPublicKeyFiles"], "operator metal genesis");
15926
16035
  if (typeof genesis.environment !== "string" || !PLATFORM_GENESIS_ENVIRONMENTS.includes(genesis.environment)) {
@@ -15944,6 +16053,14 @@ function readOperatorMetalBootstrapRequest(path) {
15944
16053
  sshPublicKeyFiles: genesis.sshPublicKeyFiles
15945
16054
  }
15946
16055
  };
16056
+ };
16057
+ function readOperatorMetalBootstrapRequest(path) {
16058
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
16059
+ return validateMetalRequestValue(value);
16060
+ }
16061
+ function writeOperatorMetalBootstrapRequest(path, request) {
16062
+ validateMetalRequestValue(request);
16063
+ return writeOwnerJson2(path, request, "operator metal request");
15947
16064
  }
15948
16065
  function planOperatorMetalBootstrap(request, mode) {
15949
16066
  const config = readMetalBootstrapConfig(request.metalConfigFile);
@@ -16003,6 +16120,87 @@ function planOperatorPlatformBootstrap(request, mode) {
16003
16120
  secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
16004
16121
  };
16005
16122
  }
16123
+ var fleetConfigIdentity = (config) => {
16124
+ const normalized = structuredClone(config);
16125
+ delete normalized.computeReference;
16126
+ delete normalized.nodeHostname;
16127
+ delete normalized.cloudflareHandoff;
16128
+ delete normalized.database.role;
16129
+ delete normalized.database.address;
16130
+ delete normalized.database.master;
16131
+ const environment = normalized.runtime.environment;
16132
+ delete environment.databaseRole;
16133
+ delete environment.databaseAddress;
16134
+ delete environment.databaseMaster;
16135
+ delete environment.nodeHostname;
16136
+ delete environment.seedSyncPeers;
16137
+ delete environment.custodianEmail;
16138
+ return JSON.stringify(normalized);
16139
+ };
16140
+ function validatedPlatformFleet(fleet) {
16141
+ const requests = fleet.requests.map(readOperatorPlatformBootstrapRequest);
16142
+ const configs = requests.map((request) => {
16143
+ const config = readBootstrapConfig(request.platformConfigFile);
16144
+ if (config.kind !== "platform")
16145
+ throw new Error("operator fleet requires platform configs");
16146
+ return config;
16147
+ });
16148
+ const environment = configs[0].environment;
16149
+ const expected = PLATFORM_GENESIS_FLEETS[environment];
16150
+ if (!expected || expected.length !== 3)
16151
+ throw new Error("operator fleet environment is invalid");
16152
+ const identity = fleetConfigIdentity(configs[0]);
16153
+ const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
16154
+ const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
16155
+ const coordinators = expected.map((guest) => `http://${guest.address}:8529`);
16156
+ const nodeHostnames = new Set;
16157
+ for (let index = 0;index < 3; index += 1) {
16158
+ const config = configs[index];
16159
+ const request = requests[index];
16160
+ const guest = expected[index];
16161
+ const role = index === 0 ? "master" : "joiner";
16162
+ if (config.environment !== environment || fleetConfigIdentity(config) !== identity) {
16163
+ throw new Error("operator fleet platform configs do not share one immutable identity");
16164
+ }
16165
+ if (config.computeReference !== guest.name || config.database.address !== guest.address || config.database.role !== role || config.database.agency !== "member" || (role === "master" ? config.database.master !== undefined : config.database.master !== expected[0].address) || config.database.coordinators.join(",") !== coordinators.join(",") || config.enrolment.source !== "genesis-derived" || config.cloudflareHandoff?.nodeName !== guest.name) {
16166
+ throw new Error(`operator fleet ${guest.name} does not match the exact genesis topology`);
16167
+ }
16168
+ if (request.target.address !== guest.address || request.target.port !== 22 || request.target.user !== "forgezero") {
16169
+ throw new Error(`operator fleet ${guest.name} SSH target must be forgezero@${guest.address}:22`);
16170
+ }
16171
+ if (`${request.target.identityPublicKeyFile}\x00${request.target.agentSocket}` !== operatorIdentity) {
16172
+ throw new Error("operator fleet must use one caller-approved SSH identity and agent socket");
16173
+ }
16174
+ if (JSON.stringify(request.target.jump ?? null) !== jumpIdentity) {
16175
+ throw new Error("operator fleet must use one pinned Metal jump or no jump on every node");
16176
+ }
16177
+ if (nodeHostnames.has(config.nodeHostname))
16178
+ throw new Error("operator fleet node hostnames must be unique");
16179
+ nodeHostnames.add(config.nodeHostname);
16180
+ }
16181
+ return { requests, configs, environment };
16182
+ }
16183
+ function planOperatorPlatformFleetBootstrap(fleet, mode) {
16184
+ const { requests, configs, environment } = validatedPlatformFleet(fleet);
16185
+ const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
16186
+ for (const config of configs.slice(1)) {
16187
+ if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
16188
+ throw new Error("operator fleet configs do not share one attended secret schema");
16189
+ }
16190
+ }
16191
+ return {
16192
+ kind: "platform-fleet-remote",
16193
+ mode,
16194
+ mutation: mode !== "status",
16195
+ environment,
16196
+ nodes: requests.map((request, index) => ({
16197
+ computeReference: configs[index].computeReference,
16198
+ address: request.target.address,
16199
+ ...request.target.jump ? { via: request.target.jump.address } : {}
16200
+ })),
16201
+ secretInputs
16202
+ };
16203
+ }
16006
16204
  var defaultExec3 = async (argv2, options = {}) => {
16007
16205
  const child = Bun.spawn([...argv2], {
16008
16206
  stdin: options.stdin === undefined ? "ignore" : "pipe",
@@ -16245,6 +16443,30 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
16245
16443
  rmSync7(directory, { recursive: true, force: true });
16246
16444
  }
16247
16445
  }
16446
+ async function applyOperatorPlatformFleetBootstrap(fleet, mode, options = {}) {
16447
+ const plan = planOperatorPlatformFleetBootstrap(fleet, mode);
16448
+ const { requests, configs } = validatedPlatformFleet(fleet);
16449
+ const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(configs[0], options.secrets) : undefined;
16450
+ if (secrets) {
16451
+ for (const config of configs.slice(1))
16452
+ validatePlatformBootstrapSecrets(config, secrets);
16453
+ }
16454
+ const settled = await Promise.allSettled(requests.map((request) => applyOperatorPlatformBootstrap(request, mode, { ...options, secrets })));
16455
+ const outcomes = settled.map((result, index) => {
16456
+ const config = configs[index];
16457
+ const address = requests[index].target.address;
16458
+ if (result.status === "fulfilled")
16459
+ return {
16460
+ computeReference: config.computeReference,
16461
+ address,
16462
+ ok: true,
16463
+ output: result.value.output.slice(0, 65536)
16464
+ };
16465
+ const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
16466
+ return { computeReference: config.computeReference, address, ok: false, error: message.slice(0, 4096) };
16467
+ });
16468
+ return { plan, ok: outcomes.every(({ ok }) => ok), outcomes };
16469
+ }
16248
16470
  async function applyOperatorMetalBootstrap(request, mode, options = {}) {
16249
16471
  const plan = planOperatorMetalBootstrap(request, mode);
16250
16472
  publicIdentity(request.target.identityPublicKeyFile);
@@ -16478,7 +16700,7 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
16478
16700
 
16479
16701
  // src/cli/maintenance.ts
16480
16702
  import { existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
16481
- import { isAbsolute as isAbsolute5, join as join11, relative as relative2, resolve as resolve9 } from "path";
16703
+ import { isAbsolute as isAbsolute6, join as join11, relative as relative2, resolve as resolve10 } from "path";
16482
16704
  var API_OPERATION_ENTRYPOINTS = {
16483
16705
  "dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
16484
16706
  "db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
@@ -16572,13 +16794,13 @@ function checkedEntrypoint(root, parts) {
16572
16794
  }
16573
16795
  const actual = realpathSync5(candidate);
16574
16796
  const within = relative2(root, actual);
16575
- if (within.startsWith("..") || isAbsolute5(within)) {
16797
+ if (within.startsWith("..") || isAbsolute6(within)) {
16576
16798
  throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
16577
16799
  }
16578
16800
  return actual;
16579
16801
  }
16580
16802
  function resolveRepositoryOperation(operation, requestedRoot) {
16581
- const root = realpathSync5(resolve9(requestedRoot));
16803
+ const root = realpathSync5(resolve10(requestedRoot));
16582
16804
  const name = manifestName(root);
16583
16805
  if (operation in API_OPERATION_ENTRYPOINTS) {
16584
16806
  const parts = API_OPERATION_ENTRYPOINTS[operation];
@@ -16879,7 +17101,7 @@ async function api(options, path, init) {
16879
17101
  return { status: response.status, body };
16880
17102
  }
16881
17103
  function sleep(ms) {
16882
- return new Promise((resolve11) => setTimeout(resolve11, ms));
17104
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
16883
17105
  }
16884
17106
  function browserCommand(url) {
16885
17107
  if (process.platform === "darwin")
@@ -17244,16 +17466,13 @@ async function cmdAgent(options, args) {
17244
17466
  }
17245
17467
  const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
17246
17468
  const enrolStatePath = "/var/lib/forgezero/enrolment.json";
17247
- const gitCredentialPath = process.env.FZ_GIT_CREDENTIAL_PATH ?? "/etc/forgezero/creds/git-deploy-key.cred";
17248
- const gitPublicKeyPath = process.env.FZ_GIT_PUBLIC_KEY_PATH ?? "/etc/forgezero/git/deploy.pub";
17469
+ const gitIdentity = resolveGitIdentityInstallOptions(process.env);
17249
17470
  const plan = planInstall({
17250
17471
  capabilities: await readCapabilities(localRunner),
17251
17472
  socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET,
17252
17473
  seedPath: process.env.FZ_SEED_PATH ?? "/var/lib/forgezero/node.seed",
17253
17474
  seedCredentialPath: process.env.FZ_SEED_CREDENTIAL_PATH,
17254
- gitCredentialPath,
17255
- gitPublicKeyPath,
17256
- generateGitIdentity: process.env.FZ_GENERATE_GIT_DEPLOY_KEY === "true",
17475
+ ...gitIdentity,
17257
17476
  controlSocketPath: process.env.FZ_CONTROL_SOCKET,
17258
17477
  repository: process.env.FZ_DEPLOY_REPO,
17259
17478
  branch: process.env.FZ_DEPLOY_BRANCH,
@@ -17313,7 +17532,7 @@ async function cmdAgent(options, args) {
17313
17532
  writeFileSync13(plan.unitPath, plan.unit, { mode: 420 });
17314
17533
  out.ok(`Wrote ${plan.unitPath}`);
17315
17534
  for (const auxiliary of plan.auxiliaryUnits) {
17316
- mkdirSync12(dirname13(auxiliary.path), { recursive: true, mode: 493 });
17535
+ mkdirSync13(dirname14(auxiliary.path), { recursive: true, mode: 493 });
17317
17536
  writeFileSync13(auxiliary.path, auxiliary.unit, { mode: 420 });
17318
17537
  out.ok(`Wrote ${auxiliary.path}`);
17319
17538
  }
@@ -17322,7 +17541,7 @@ async function cmdAgent(options, args) {
17322
17541
  const token = await bootstrapSecret("ForgeZero one-time enrolment token");
17323
17542
  if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
17324
17543
  throw new Error("A valid fze_ enrolment token was not provided.");
17325
- mkdirSync12(dirname13(enrolTokenCredentialPath), { recursive: true, mode: 448 });
17544
+ mkdirSync13(dirname14(enrolTokenCredentialPath), { recursive: true, mode: 448 });
17326
17545
  const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
17327
17546
  if (sealing.stdin && typeof sealing.stdin !== "number") {
17328
17547
  sealing.stdin.write(`${token}
@@ -17338,11 +17557,11 @@ async function cmdAgent(options, args) {
17338
17557
  for (const step3 of transcript)
17339
17558
  out.ok(step3.label);
17340
17559
  out.ok("Agent service and socket verified");
17341
- if (existsSync12(gitPublicKeyPath)) {
17560
+ if (gitIdentity.gitPublicKeyPath && existsSync12(gitIdentity.gitPublicKeyPath)) {
17342
17561
  out.line();
17343
17562
  out.line(" Compatibility SSH deploy public key:");
17344
17563
  out.line();
17345
- out.line(` ${readFileSync15(gitPublicKeyPath, "utf8").trim()}`);
17564
+ out.line(` ${readFileSync15(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
17346
17565
  out.line();
17347
17566
  }
17348
17567
  return 0;
@@ -17448,9 +17667,9 @@ function interactiveMetalBootstrap() {
17448
17667
  });
17449
17668
  }
17450
17669
  function writeBootstrapConfig(path, config) {
17451
- if (!isAbsolute6(path) || resolve10(path) !== path)
17670
+ if (!isAbsolute7(path) || resolve11(path) !== path)
17452
17671
  throw new Error("--output must be a canonical absolute path");
17453
- mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
17672
+ mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
17454
17673
  writeFileSync13(path, `${JSON.stringify(config, null, 2)}
17455
17674
  `, { mode: 384, flag: "wx" });
17456
17675
  return path;
@@ -17512,10 +17731,10 @@ async function interactiveCloudflareBootstrap() {
17512
17731
  });
17513
17732
  }
17514
17733
  function genesisOutputDirectory(path) {
17515
- if (!isAbsolute6(path) || resolve10(path) !== path)
17734
+ if (!isAbsolute7(path) || resolve11(path) !== path)
17516
17735
  throw new Error("--output must be a canonical absolute directory");
17517
17736
  if (!existsSync12(path))
17518
- mkdirSync12(path, { recursive: true, mode: 448 });
17737
+ mkdirSync13(path, { recursive: true, mode: 448 });
17519
17738
  const metadata = lstatSync10(path);
17520
17739
  const uid = process.getuid?.();
17521
17740
  if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
@@ -17523,6 +17742,37 @@ function genesisOutputDirectory(path) {
17523
17742
  }
17524
17743
  return path;
17525
17744
  }
17745
+ function interactiveOperatorHop(label, defaults) {
17746
+ return createOperatorSshHop({
17747
+ address: bootstrapAnswer(`${label} explicit SSH IP`, defaults.address),
17748
+ port: bootstrapNumber(`${label} SSH port`, "22"),
17749
+ user: bootstrapAnswer(`${label} SSH user`, defaults.user),
17750
+ hostKey: bootstrapAnswer(`${label} trusted ssh-ed25519 host public key`)
17751
+ });
17752
+ }
17753
+ function operatorIdentityCoordinates() {
17754
+ return {
17755
+ identityPublicKeyFile: bootstrapAnswer("Operator SSH public-key file (private key remains in Bitwarden SSH agent)"),
17756
+ agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
17757
+ };
17758
+ }
17759
+ function writeMetalOperatorFiles(directory) {
17760
+ const output = genesisOutputDirectory(directory);
17761
+ const config = interactiveMetalBootstrap();
17762
+ const metalConfig = writeBootstrapConfig(join12(output, "metal.json"), config);
17763
+ const identity = operatorIdentityCoordinates();
17764
+ const target = interactiveOperatorHop("Metal", { user: "root" });
17765
+ const remoteRequest = writeOperatorMetalBootstrapRequest(join12(output, "metal-remote.json"), {
17766
+ kind: "metal-remote",
17767
+ metalConfigFile: metalConfig,
17768
+ target: { ...target, ...identity },
17769
+ genesis: {
17770
+ environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
17771
+ sshPublicKeyFiles: [identity.identityPublicKeyFile]
17772
+ }
17773
+ });
17774
+ return { metalConfig, remoteRequest };
17775
+ }
17526
17776
  function writePlatformGenesisFleet(directory) {
17527
17777
  const output = genesisOutputDirectory(directory);
17528
17778
  const template = interactiveBootstrap("platform", true);
@@ -17539,7 +17789,20 @@ function writePlatformGenesisFleet(directory) {
17539
17789
  writeBootstrapConfig(path, config);
17540
17790
  return path;
17541
17791
  });
17542
- return { configs };
17792
+ const identity = operatorIdentityCoordinates();
17793
+ const useJump = bootstrapAnswer("Reach genesis computes through the Metal SSH jump? (yes/no)", "yes") === "yes";
17794
+ const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
17795
+ const requests = configs.map((platformConfigFile, index) => {
17796
+ const guest = fleet[index];
17797
+ const target = interactiveOperatorHop(`${guest.name} compute`, { address: guest.address, user: "forgezero" });
17798
+ return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
17799
+ kind: "platform-remote",
17800
+ platformConfigFile,
17801
+ target: { ...target, ...identity, ...jump ? { jump } : {} }
17802
+ });
17803
+ });
17804
+ const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
17805
+ return { configs, requests, fleetRequest };
17543
17806
  }
17544
17807
  function interactiveBootstrap(kind, genesis = false) {
17545
17808
  if (!process.stdin.isTTY)
@@ -17661,7 +17924,7 @@ async function cmdBootstrap(options, args) {
17661
17924
  if (operation === "config") {
17662
17925
  const kind = args[1];
17663
17926
  if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
17664
- throw new Error("Usage: fz bootstrap config metal|cloudflare --output <absolute-file> | fz bootstrap config platform --output <absolute-owner-only-directory>");
17927
+ throw new Error("Usage: fz bootstrap config cloudflare --output <absolute-file> | fz bootstrap config metal|platform --output <absolute-owner-only-directory>");
17665
17928
  }
17666
17929
  if (kind === "platform") {
17667
17930
  out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
@@ -17673,10 +17936,32 @@ async function cmdBootstrap(options, args) {
17673
17936
  out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
17674
17937
  return 0;
17675
17938
  }
17676
- const config2 = interactiveMetalBootstrap();
17677
- out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
17939
+ out.line(JSON.stringify({ kind, ...writeMetalOperatorFiles(options.outputPath), mode: "0600" }, null, 2));
17678
17940
  return 0;
17679
17941
  }
17942
+ if (operation === "platform" && args[1] === "fleet") {
17943
+ const mode = args[2];
17944
+ if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
17945
+ throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
17946
+ }
17947
+ if (!options.bootstrapConfigPath)
17948
+ throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
17949
+ const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
17950
+ const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
17951
+ if (!options.apply) {
17952
+ out.line(JSON.stringify(plan2, null, 2));
17953
+ out.step("Review all three pinned targets and shared secret names, then repeat with --apply.");
17954
+ return 0;
17955
+ }
17956
+ const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
17957
+ const firstConfig = readBootstrapConfig(first.platformConfigFile);
17958
+ if (firstConfig.kind !== "platform")
17959
+ throw new Error("remote platform fleet requires platform configs");
17960
+ const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
17961
+ const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode, { secrets: secrets2 });
17962
+ out.line(JSON.stringify(result2, null, 2));
17963
+ return result2.ok ? 0 : 1;
17964
+ }
17680
17965
  if (operation === "platform" && args[1] === "remote") {
17681
17966
  const mode = args[2];
17682
17967
  if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
@@ -17812,7 +18097,7 @@ async function cmdBootstrap(options, args) {
17812
18097
  return 0;
17813
18098
  }
17814
18099
  if (!["platform", "repair"].includes(operation)) {
17815
- throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
18100
+ throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
17816
18101
  }
17817
18102
  const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
17818
18103
  throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
@@ -18188,7 +18473,7 @@ async function cmdDeploy(options, args) {
18188
18473
  out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
18189
18474
  return 0;
18190
18475
  }
18191
- initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve10(options.projectRoot)), force: options.force });
18476
+ initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve11(options.projectRoot)), force: options.force });
18192
18477
  const compiled = await compileDeploymentProject(options.projectRoot);
18193
18478
  if (options.json)
18194
18479
  out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
@@ -18213,7 +18498,7 @@ async function cmdDeploy(options, args) {
18213
18498
  return problems.length === 0 ? 0 : 1;
18214
18499
  }
18215
18500
  if (operation === "check" || operation === "sync") {
18216
- if (existsSync12(resolve10(options.projectRoot, DEPLOY_SOURCE_FILE))) {
18501
+ if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
18217
18502
  const expected = await compileDeploymentProject(options.projectRoot, { write: false });
18218
18503
  const actual = inspectCompiledDeployment(options.projectRoot);
18219
18504
  const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
@@ -18390,10 +18675,13 @@ function usage() {
18390
18675
  fz bootstrap platform remote <apply|status>
18391
18676
  Run typed bootstrap from the operator laptop through
18392
18677
  a pinned host; apply copies and verifies that bundle
18678
+ fz bootstrap platform fleet <apply|status>
18679
+ Validate the exact three-node genesis identity, prompt
18680
+ shared secrets once and run all pinned nodes concurrently
18393
18681
  fz bootstrap platform Install/repair a typed elastic platform compute
18394
18682
  fz bootstrap config <metal|platform|cloudflare>
18395
- Write a validated owner-only metal/Cloudflare file or
18396
- strict three-node platform genesis directory; never apply locally
18683
+ Write validated owner-only metal config+remote request,
18684
+ Cloudflare file, or complete three-node platform request set
18397
18685
  fz bootstrap platform cloudflare
18398
18686
  Plan/apply attended Tunnel and DNS reconciliation
18399
18687
  using hidden management and KV/Worker runtime token prompts