@forgezero/agent 0.1.128 → 0.1.130

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/README.md CHANGED
@@ -214,7 +214,7 @@ fz unlock --phrase-file /secure/offline-phrase.txt
214
214
 
215
215
  ## Use website operations from a project or an AI agent
216
216
 
217
- Every UI operation is an API operation. `fz ui routes --json` discovers the exact actions allowed by the signed-in account’s current realm, lifecycle stage and grants from the API’s enforced matrix, so the CLI and AI agents carry no copied route list. `fz api` (also `fz ui`) executes authenticated JSON GET, POST, PUT, PATCH and DELETE without requiring a browser visit for ordinary work. Paths are forced onto the signed-in origin, query values are repeatable, bodies can be inline, stdin or a file, and server grants plus fresh-proof rules remain authoritative. `fz project init|sync|check` separately gives AI tools one Git-persisted project memory.
217
+ Every UI operation is an API operation. `fz ui routes --json` discovers the exact actions allowed by the signed-in account’s current workspace, lifecycle stage and permissions from the API’s enforced matrix, so the CLI and AI agents carry no copied route list. `fz api` (also `fz ui`) executes authenticated JSON GET, POST, PUT, PATCH and DELETE without requiring a browser visit for ordinary work. Paths are forced onto the signed-in origin, query values are repeatable, bodies can be inline, stdin or a file, and server permissions plus fresh-proof rules remain authoritative. `fz project init|sync|check` separately gives AI tools one Git-persisted project memory.
218
218
 
219
219
  ```text
220
220
  fz ui routes --json
@@ -917,7 +917,7 @@ async function postSignedNode(options, path, body) {
917
917
  }
918
918
 
919
919
  // src/version.ts
920
- var VERSION3 = "0.1.128";
920
+ var VERSION3 = "0.1.130";
921
921
 
922
922
  // src/agent-heartbeat.ts
923
923
  function readAgentHostMetrics() {
@@ -210,6 +210,7 @@ export declare function interruptedPlatformReleaseIdentityDigests(config: Platfo
210
210
  export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
211
211
  export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost, secrets?: BootstrapSecrets, dependencies?: {
212
212
  fetcher?: typeof fetch;
213
+ reuseBoundPlatformCredentials?: boolean;
213
214
  }): Promise<BootstrapResult>;
214
215
  export declare function readBootstrapConfig(path: string): BootstrapConfig;
215
216
  interface BootstrapAgentInitialBundle {
package/dist/bootstrap.js CHANGED
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1460
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1461
1461
 
1462
1462
  // src/version.ts
1463
- var VERSION = "0.1.128";
1463
+ var VERSION = "0.1.130";
1464
1464
 
1465
1465
  // src/software.ts
1466
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -4664,6 +4664,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4664
4664
  installed = parseStoredState(host.read(STATE_PATH));
4665
4665
  let bootstrapManifest;
4666
4666
  let releaseEvidence;
4667
+ let redundantBootstrapBundle = false;
4667
4668
  if (config.kind === "platform") {
4668
4669
  const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
4669
4670
  if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
@@ -4684,8 +4685,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4684
4685
  if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
4685
4686
  throw new Error("bootstrap release evidence is malformed");
4686
4687
  releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4687
- if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
4688
+ if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
4688
4689
  bootstrapManifest = reviewed;
4690
+ } else {
4691
+ redundantBootstrapBundle = true;
4692
+ }
4689
4693
  } else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4690
4694
  let evidence;
4691
4695
  try {
@@ -4757,7 +4761,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4757
4761
  }
4758
4762
  allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
4759
4763
  }
4760
- const platformPrivate = config.kind === "platform" ? (() => {
4764
+ const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
4765
+ if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
4766
+ throw new Error("stored-credential promotion requires an installed enrolled platform compute");
4767
+ }
4768
+ const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
4761
4769
  if (!secrets)
4762
4770
  throw new Error("platform apply requires attended credentials on stdin");
4763
4771
  const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
@@ -4800,7 +4808,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4800
4808
  if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
4801
4809
  await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
4802
4810
  }
4803
- if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4811
+ if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4804
4812
  await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
4805
4813
  }
4806
4814
  const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
@@ -4858,24 +4866,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4858
4866
  await verifyBootstrapBundleOnHost(host, config, true);
4859
4867
  }
4860
4868
  if (config.kind === "platform") {
4861
- const root = platformPrivate.root;
4862
- if (!host.exists(JWT_CREDENTIAL)) {
4863
- await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4864
- }
4865
- if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4866
- await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4867
- }
4868
- await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4869
- await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4870
4869
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4871
- for (const [name, source] of Object.entries({
4872
- ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4873
- "backup.s3.secretAccessKey": platformPrivate.backup
4874
- })) {
4875
- if (source) {
4876
- const destination2 = `${CREDS}/${name}.cred`;
4877
- if (!host.exists(destination2)) {
4878
- await seal(host, name, destination2, source);
4870
+ if (reuseBoundPlatformCredentials) {
4871
+ const required = [
4872
+ ...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
4873
+ SEED_CREDENTIAL,
4874
+ BACKUP_RECOVERY_CREDENTIAL,
4875
+ ...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
4876
+ ...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
4877
+ ...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
4878
+ ...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
4879
+ ];
4880
+ const missing = required.find((path) => !host.exists(path));
4881
+ if (missing)
4882
+ throw new Error(`stored platform credential is missing: ${missing}`);
4883
+ } else {
4884
+ const root = platformPrivate.root;
4885
+ if (!host.exists(JWT_CREDENTIAL)) {
4886
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4887
+ }
4888
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4889
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4890
+ }
4891
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4892
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4893
+ for (const [name, source] of Object.entries({
4894
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4895
+ "backup.s3.secretAccessKey": platformPrivate.backup
4896
+ })) {
4897
+ if (source) {
4898
+ const destination2 = `${CREDS}/${name}.cred`;
4899
+ if (!host.exists(destination2))
4900
+ await seal(host, name, destination2, source);
4879
4901
  }
4880
4902
  }
4881
4903
  }
@@ -4944,6 +4966,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4944
4966
  await checked3(host, ["nginx", "-t"], "nginx configuration");
4945
4967
  await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
4946
4968
  await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
4969
+ if (alreadyEnrolled && !bootstrapManifest) {
4970
+ const healthCommand = [
4971
+ "curl",
4972
+ "--fail",
4973
+ "--silent",
4974
+ "--show-error",
4975
+ "--max-time",
4976
+ "10",
4977
+ `http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
4978
+ ];
4979
+ const health = await host.exec(healthCommand);
4980
+ if (health.exitCode !== 0) {
4981
+ const activeSlotFile = `${runtime.slotsDirectory}/.active`;
4982
+ if (!host.exists(activeSlotFile))
4983
+ throw new Error("enrolled platform active API slot evidence is missing");
4984
+ const activeSlot = host.read(activeSlotFile).trim();
4985
+ if (activeSlot !== "blue" && activeSlot !== "green") {
4986
+ throw new Error("enrolled platform active API slot evidence is malformed");
4987
+ }
4988
+ await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
4989
+ let ready = false;
4990
+ let lastHealthOutput = "";
4991
+ for (let attempt = 0;attempt < 30; attempt += 1) {
4992
+ const repairedHealth = await host.exec(healthCommand);
4993
+ if (repairedHealth.exitCode === 0) {
4994
+ ready = true;
4995
+ break;
4996
+ }
4997
+ lastHealthOutput = repairedHealth.output.trim();
4998
+ if (attempt < 29)
4999
+ await (host.sleep?.(1000) ?? Bun.sleep(1000));
5000
+ }
5001
+ if (!ready)
5002
+ throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
5003
+ }
5004
+ if (redundantBootstrapBundle) {
5005
+ host.remove(config.bootstrapBundle.bundleFile);
5006
+ host.remove(config.bootstrapBundle.manifestFile);
5007
+ }
5008
+ }
4947
5009
  if (config.database.role === "master") {
4948
5010
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
4949
5011
  if (!host.exists(invite)) {
@@ -5040,7 +5102,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
5040
5102
  host.remove(config.cloudflareHandoff.handoffFile);
5041
5103
  }
5042
5104
  let launch;
5043
- if (config.kind === "platform" && config.database.role === "master") {
5105
+ if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
5044
5106
  const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
5045
5107
  if (!host.exists(invitePath))
5046
5108
  throw new Error("platform invite is missing after deployment");
package/dist/fz-agent.js CHANGED
@@ -9640,9 +9640,47 @@ async function writeAndCloseProcessInput(input, value) {
9640
9640
  }
9641
9641
 
9642
9642
  // src/version.ts
9643
- var VERSION2 = "0.1.128";
9643
+ var VERSION2 = "0.1.130";
9644
9644
 
9645
9645
  // src/ssh-bootstrap.ts
9646
+ function deriveMetalAdmissionProvisioning(lscpu) {
9647
+ const rows = lscpu.split(`
9648
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
9649
+ const fields = line.split(",");
9650
+ if (fields.length !== 4 || fields.some((field) => !/^\d+$/.test(field))) {
9651
+ throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
9652
+ }
9653
+ return { cpu: Number(fields[0]), core: `${fields[2]}:${fields[1]}` };
9654
+ });
9655
+ if (rows.length < 2 || new Set(rows.map(({ cpu }) => cpu)).size !== rows.length) {
9656
+ throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
9657
+ }
9658
+ const cores = new Map;
9659
+ for (const row2 of rows)
9660
+ cores.set(row2.core, [...cores.get(row2.core) ?? [], row2.cpu]);
9661
+ if (cores.size < 2)
9662
+ throw new SshBootstrapError("METAL_CPU_CAPACITY_INSUFFICIENT");
9663
+ const ordered = [...cores.values()].map((cpus2) => cpus2.sort((a, b) => a - b)).sort((left, right) => left[0] - right[0]);
9664
+ const threadsPerCore = ordered[0].length;
9665
+ if (threadsPerCore < 1 || threadsPerCore > 8 || ordered.some((cpus2) => cpus2.length !== threadsPerCore)) {
9666
+ throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
9667
+ }
9668
+ const linuxList = (values) => [...values].sort((a, b) => a - b).join(",");
9669
+ const housekeeping = ordered.shift();
9670
+ const guestCpus = Array.from({ length: threadsPerCore }, (_, thread) => ordered.map((cpus2) => cpus2[thread])).flat();
9671
+ return {
9672
+ volumeGroup: "fzvg",
9673
+ bridge: "fzbr0",
9674
+ subnetPrefix: "10.42.0",
9675
+ addressStart: 20,
9676
+ addressEnd: 200,
9677
+ gateway: "10.42.0.1",
9678
+ nameservers: ["1.1.1.1", "1.0.0.1"],
9679
+ cpuPools: [{ key: "host-capacity", cpus: linuxList(guestCpus), physicalCores: ordered.length }],
9680
+ housekeepingCpus: linuxList(housekeeping)
9681
+ };
9682
+ }
9683
+
9646
9684
  class SshBootstrapError extends Error {
9647
9685
  code;
9648
9686
  constructor(code, message = code) {
@@ -9677,7 +9715,7 @@ var validateClaim = (claim) => {
9677
9715
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(claim.computeKey) || !/^[A-Za-z0-9_-]{16,128}$/.test(claim.claimToken) || !/^fze_[A-Za-z0-9_-]{32,128}$/.test(claim.enrolmentToken) || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(claim.realm) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.projectKey) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.environmentKey))
9678
9716
  throw new SshBootstrapError("CLAIM_INVALID");
9679
9717
  const { host, port, user, hostKeySha256, nodeHostname, operatingSystem } = claim.target;
9680
- if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
9718
+ if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || hostKeySha256 !== "" && !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
9681
9719
  throw new SshBootstrapError("CLAIM_INVALID");
9682
9720
  }
9683
9721
  if (operatingSystem?.id !== "ubuntu" || !["24.04", "26.04"].includes(operatingSystem.version) || operatingSystem.architecture !== "x64")
@@ -9738,11 +9776,14 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
9738
9776
  const candidates = scanned.split(`
9739
9777
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
9740
9778
  let matched;
9779
+ let observedFingerprint = "";
9741
9780
  for (const line of candidates) {
9742
- const fingerprint = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
9781
+ const result = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
9743
9782
  ` });
9744
- if (fingerprint.split(/\s+/).includes(claim.target.hostKeySha256)) {
9783
+ const fingerprint = result.split(/\s+/).find((part) => /^SHA256:[A-Za-z0-9+/]{43}$/.test(part)) ?? "";
9784
+ if (fingerprint && (!claim.target.hostKeySha256 || fingerprint === claim.target.hostKeySha256)) {
9745
9785
  matched = line;
9786
+ observedFingerprint = fingerprint;
9746
9787
  break;
9747
9788
  }
9748
9789
  }
@@ -9754,7 +9795,7 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
9754
9795
  const path2 = join3(directory, "known_hosts");
9755
9796
  writeFileSync5(path2, `${pinned.hostKeyAlias} ${key}
9756
9797
  `, { mode: 384, flag: "wx" });
9757
- return path2;
9798
+ return { path: path2, fingerprint: observedFingerprint };
9758
9799
  }
9759
9800
  var BUN_RELEASE_URL = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip";
9760
9801
  var verifiedBunArchive = async (directory) => {
@@ -9809,7 +9850,9 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
9809
9850
  if (!resolved || resolved.protocol !== "ssh")
9810
9851
  throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
9811
9852
  pinned = resolved;
9812
- const knownHosts = await pinnedKnownHost(claim, pinned, directory, exec);
9853
+ const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
9854
+ claim.target.hostKeySha256 = observedHost.fingerprint;
9855
+ const knownHosts = observedHost.path;
9813
9856
  const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
9814
9857
  remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
9815
9858
  if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
@@ -10153,7 +10196,9 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
10153
10196
  if (!resolved || resolved.protocol !== "ssh")
10154
10197
  throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
10155
10198
  pinned = resolved;
10156
- const knownHosts = await pinnedKnownHost(claim, pinned, directory, exec);
10199
+ const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
10200
+ claim.target.hostKeySha256 = observedHost.fingerprint;
10201
+ const knownHosts = observedHost.path;
10157
10202
  const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
10158
10203
  const destination = target(claim, pinned);
10159
10204
  remoteDirectory = await checked(exec, ["ssh", ...ssh, destination, "--", "mktemp", "-d", "/tmp/forgezero-metal-admission.XXXXXXXX"], "SSH_UNREACHABLE");
@@ -10173,6 +10218,13 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
10173
10218
  writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
10174
10219
  await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
10175
10220
  await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-m", "0600", `${remoteDirectory}/authorized_keys`, `${home}/.ssh/authorized_keys`]);
10221
+ await remote(exec, ssh, destination, "METAL_STORAGE_UNAVAILABLE", ["/usr/bin/sudo", "-n", "/usr/sbin/vgs", "fzvg"]);
10222
+ await remote(exec, ssh, destination, "METAL_NETWORK_UNAVAILABLE", ["/usr/bin/sudo", "-n", "/usr/sbin/ip", "link", "show", "fzbr0"]);
10223
+ const topology = await remote(exec, ssh, destination, "METAL_CPU_TOPOLOGY_INVALID", [
10224
+ "/usr/bin/lscpu",
10225
+ "--parse=CPU,CORE,SOCKET,NODE"
10226
+ ]);
10227
+ const provisioning = deriveMetalAdmissionProvisioning(topology);
10176
10228
  const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
10177
10229
  const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
10178
10230
  for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
@@ -10191,7 +10243,7 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
10191
10243
  hostTelemetryEndpoint: "http://127.0.0.1:4318",
10192
10244
  hostTelemetryUnit: "forgezero-otel-collector.service",
10193
10245
  profile: {
10194
- ...claim.provisioning,
10246
+ ...provisioning,
10195
10247
  stateDir: "/etc/forgezero/metal-guests",
10196
10248
  seedDir: "/var/lib/forgezero/seed",
10197
10249
  unitDir: "/etc/systemd/system",
package/dist/fz.js CHANGED
@@ -5788,7 +5788,7 @@ var VaultRuntimeClientError, json = (value) => ({ method: "POST", body: JSON.str
5788
5788
  } catch {
5789
5789
  return;
5790
5790
  }
5791
- }, VERSION = "0.1.25";
5791
+ }, VERSION = "0.1.27";
5792
5792
  var init_dist = __esm(() => {
5793
5793
  init_vault_runtime();
5794
5794
  init_identity();
@@ -6007,7 +6007,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
6007
6007
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
6008
6008
 
6009
6009
  // src/version.ts
6010
- var VERSION2 = "0.1.128";
6010
+ var VERSION2 = "0.1.130";
6011
6011
 
6012
6012
  // src/software.ts
6013
6013
  var PINNED_BUN_VERSION = "1.3.14";
@@ -15284,6 +15284,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15284
15284
  installed = parseStoredState(host.read(STATE_PATH));
15285
15285
  let bootstrapManifest;
15286
15286
  let releaseEvidence;
15287
+ let redundantBootstrapBundle = false;
15287
15288
  if (config.kind === "platform") {
15288
15289
  const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
15289
15290
  if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
@@ -15304,8 +15305,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15304
15305
  if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
15305
15306
  throw new Error("bootstrap release evidence is malformed");
15306
15307
  releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
15307
- if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
15308
+ if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
15308
15309
  bootstrapManifest = reviewed;
15310
+ } else {
15311
+ redundantBootstrapBundle = true;
15312
+ }
15309
15313
  } else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
15310
15314
  let evidence;
15311
15315
  try {
@@ -15377,7 +15381,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15377
15381
  }
15378
15382
  allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
15379
15383
  }
15380
- const platformPrivate = config.kind === "platform" ? (() => {
15384
+ const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
15385
+ if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
15386
+ throw new Error("stored-credential promotion requires an installed enrolled platform compute");
15387
+ }
15388
+ const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
15381
15389
  if (!secrets)
15382
15390
  throw new Error("platform apply requires attended credentials on stdin");
15383
15391
  const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
@@ -15420,7 +15428,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15420
15428
  if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
15421
15429
  await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
15422
15430
  }
15423
- if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
15431
+ if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
15424
15432
  await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
15425
15433
  }
15426
15434
  const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
@@ -15478,24 +15486,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15478
15486
  await verifyBootstrapBundleOnHost(host, config, true);
15479
15487
  }
15480
15488
  if (config.kind === "platform") {
15481
- const root = platformPrivate.root;
15482
- if (!host.exists(JWT_CREDENTIAL)) {
15483
- await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
15484
- }
15485
- if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
15486
- await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
15487
- }
15488
- await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
15489
- await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
15490
15489
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
15491
- for (const [name, source] of Object.entries({
15492
- ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
15493
- "backup.s3.secretAccessKey": platformPrivate.backup
15494
- })) {
15495
- if (source) {
15496
- const destination2 = `${CREDS}/${name}.cred`;
15497
- if (!host.exists(destination2)) {
15498
- await seal(host, name, destination2, source);
15490
+ if (reuseBoundPlatformCredentials) {
15491
+ const required = [
15492
+ ...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
15493
+ SEED_CREDENTIAL,
15494
+ BACKUP_RECOVERY_CREDENTIAL,
15495
+ ...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
15496
+ ...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
15497
+ ...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
15498
+ ...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
15499
+ ];
15500
+ const missing = required.find((path) => !host.exists(path));
15501
+ if (missing)
15502
+ throw new Error(`stored platform credential is missing: ${missing}`);
15503
+ } else {
15504
+ const root = platformPrivate.root;
15505
+ if (!host.exists(JWT_CREDENTIAL)) {
15506
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
15507
+ }
15508
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
15509
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
15510
+ }
15511
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
15512
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
15513
+ for (const [name, source] of Object.entries({
15514
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
15515
+ "backup.s3.secretAccessKey": platformPrivate.backup
15516
+ })) {
15517
+ if (source) {
15518
+ const destination2 = `${CREDS}/${name}.cred`;
15519
+ if (!host.exists(destination2))
15520
+ await seal(host, name, destination2, source);
15499
15521
  }
15500
15522
  }
15501
15523
  }
@@ -15564,6 +15586,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15564
15586
  await checked3(host, ["nginx", "-t"], "nginx configuration");
15565
15587
  await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
15566
15588
  await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
15589
+ if (alreadyEnrolled && !bootstrapManifest) {
15590
+ const healthCommand = [
15591
+ "curl",
15592
+ "--fail",
15593
+ "--silent",
15594
+ "--show-error",
15595
+ "--max-time",
15596
+ "10",
15597
+ `http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
15598
+ ];
15599
+ const health = await host.exec(healthCommand);
15600
+ if (health.exitCode !== 0) {
15601
+ const activeSlotFile = `${runtime.slotsDirectory}/.active`;
15602
+ if (!host.exists(activeSlotFile))
15603
+ throw new Error("enrolled platform active API slot evidence is missing");
15604
+ const activeSlot = host.read(activeSlotFile).trim();
15605
+ if (activeSlot !== "blue" && activeSlot !== "green") {
15606
+ throw new Error("enrolled platform active API slot evidence is malformed");
15607
+ }
15608
+ await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
15609
+ let ready = false;
15610
+ let lastHealthOutput = "";
15611
+ for (let attempt = 0;attempt < 30; attempt += 1) {
15612
+ const repairedHealth = await host.exec(healthCommand);
15613
+ if (repairedHealth.exitCode === 0) {
15614
+ ready = true;
15615
+ break;
15616
+ }
15617
+ lastHealthOutput = repairedHealth.output.trim();
15618
+ if (attempt < 29)
15619
+ await (host.sleep?.(1000) ?? Bun.sleep(1000));
15620
+ }
15621
+ if (!ready)
15622
+ throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
15623
+ }
15624
+ if (redundantBootstrapBundle) {
15625
+ host.remove(config.bootstrapBundle.bundleFile);
15626
+ host.remove(config.bootstrapBundle.manifestFile);
15627
+ }
15628
+ }
15567
15629
  if (config.database.role === "master") {
15568
15630
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
15569
15631
  if (!host.exists(invite)) {
@@ -15660,7 +15722,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15660
15722
  host.remove(config.cloudflareHandoff.handoffFile);
15661
15723
  }
15662
15724
  let launch;
15663
- if (config.kind === "platform" && config.database.role === "master") {
15725
+ if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
15664
15726
  const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
15665
15727
  if (!host.exists(invitePath))
15666
15728
  throw new Error("platform invite is missing after deployment");
@@ -17621,7 +17683,14 @@ function planOperatorPlatformBootstrap(request, mode) {
17621
17683
  user: request.target.user,
17622
17684
  ...request.target.jump ? { via: request.target.jump.address } : {}
17623
17685
  },
17624
- steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
17686
+ steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
17687
+ "verify pinned SSH transport and caller-approved SSH agent",
17688
+ "copy pinned Bun and packaged fz artifacts to a private staging directory",
17689
+ "verify and copy the one immutable API Git bundle plus its manifest",
17690
+ "stage the rewritten owner-only platform config without plaintext credentials",
17691
+ "promote the exact release using the credentials already sealed on the enrolled compute",
17692
+ "remove transient local and remote staging data"
17693
+ ] : [
17625
17694
  "verify pinned SSH transport and caller-approved SSH agent",
17626
17695
  "copy pinned Bun and packaged fz artifacts to a private staging directory",
17627
17696
  "verify and copy the one immutable API Git bundle plus its manifest",
@@ -18124,7 +18193,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
18124
18193
  allowStoredCloudflareCredentials: mode === "repair"
18125
18194
  }) : undefined;
18126
18195
  const staged = await stageConfig(config, directory, {
18127
- reuseBoundCloudflare: mode === "repair"
18196
+ reuseBoundCloudflare: mode === "repair" || mode === "promote"
18128
18197
  });
18129
18198
  await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
18130
18199
  await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
@@ -18141,15 +18210,15 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
18141
18210
  "-n",
18142
18211
  "/usr/local/bin/fz",
18143
18212
  "bootstrap",
18144
- mode === "repair" ? "repair" : "platform",
18145
- "credentials-stdin"
18213
+ mode === "apply" ? "platform" : "repair",
18214
+ mode === "promote" ? "stored-credentials" : "credentials-stdin"
18146
18215
  ];
18147
18216
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
18148
18217
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
18149
18218
  ` : undefined, mode === "apply");
18150
18219
  return { plan, output };
18151
18220
  } finally {
18152
- if (knownHosts) {
18221
+ if (knownHosts && mode !== "status") {
18153
18222
  await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/rm", "-rf", "--", REMOTE_STAGE, remoteTemp], "remote bootstrap cleanup", true).catch(() => {
18154
18223
  return;
18155
18224
  });
@@ -20067,6 +20136,31 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
20067
20136
  owner: owner ?? forgeZeroLaunchOwnerInput()
20068
20137
  });
20069
20138
  }
20139
+ var GENESIS_HOST_KEY_ATTEMPTS = 12;
20140
+ var GENESIS_HOST_KEY_RETRY_MS = 2000;
20141
+ async function reconcileForgeZeroGenesisGuests(outputDirectory) {
20142
+ const base = dirname14(outputDirectory);
20143
+ const metalRequestFile = join13(base, "metal", "metal-remote.json");
20144
+ const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
20145
+ const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
20146
+ await applyOperatorMetalBootstrap(metalRequest, "genesis");
20147
+ let lastFailure;
20148
+ for (let attempt = 1;attempt <= GENESIS_HOST_KEY_ATTEMPTS; attempt += 1) {
20149
+ try {
20150
+ const result = await applyOperatorMetalBootstrap(metalRequest, "host-keys");
20151
+ const evidence = JSON.parse(result.output);
20152
+ writeDerivedLaunchFile(guestHostKeysFile, (temporary) => writeOperatorGuestHostKeyEvidence(temporary, evidence, metalRequest));
20153
+ readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
20154
+ return;
20155
+ } catch (cause) {
20156
+ lastFailure = cause;
20157
+ if (attempt < GENESIS_HOST_KEY_ATTEMPTS) {
20158
+ await new Promise((resolve14) => setTimeout(resolve14, GENESIS_HOST_KEY_RETRY_MS));
20159
+ }
20160
+ }
20161
+ }
20162
+ throw new Error(`reviewed genesis guests did not become SSH-ready: ${lastFailure instanceof Error ? lastFailure.message : String(lastFailure)}`);
20163
+ }
20070
20164
  function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
20071
20165
  if (!process.stdin.isTTY)
20072
20166
  throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
@@ -20266,6 +20360,8 @@ async function cmdBootstrap(options, args) {
20266
20360
  }
20267
20361
  const outputDirectory = options.outputPath ?? join13(homedir2(), ".forgezero-secure", environment, "genesis");
20268
20362
  const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
20363
+ if (options.apply)
20364
+ await reconcileForgeZeroGenesisGuests(outputDirectory);
20269
20365
  const fleet = readOperatorPlatformBootstrapFleetRequest((await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner)).fleetRequest);
20270
20366
  if (planOperatorPlatformFleetBootstrap(fleet, "apply").environment !== environment) {
20271
20367
  throw new Error("existing platform fleet does not match the selected launch profile");
@@ -20277,8 +20373,8 @@ async function cmdBootstrap(options, args) {
20277
20373
  }
20278
20374
  if (operation === "platform" && args[1] === "fleet") {
20279
20375
  const mode = args[2];
20280
- if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
20281
- throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
20376
+ if (!mode || !["apply", "promote", "status"].includes(mode) || args[3] !== undefined) {
20377
+ throw new Error("Usage: fz bootstrap platform fleet <apply|promote|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
20282
20378
  }
20283
20379
  if (!options.bootstrapConfigPath)
20284
20380
  throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
@@ -20288,12 +20384,23 @@ async function cmdBootstrap(options, args) {
20288
20384
  out.line(JSON.stringify(result2, null, 2));
20289
20385
  return result2.ok ? 0 : 1;
20290
20386
  }
20387
+ if (mode === "promote") {
20388
+ const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
20389
+ out.line(JSON.stringify(plan2, null, 2));
20390
+ if (!options.apply) {
20391
+ out.step("Review the pinned secret-free promotion, then repeat with --apply.");
20392
+ return 0;
20393
+ }
20394
+ const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
20395
+ out.line(JSON.stringify(result2, null, 2));
20396
+ return result2.ok ? 0 : 1;
20397
+ }
20291
20398
  return await runAttendedPlatformFleet(fleet, options.apply);
20292
20399
  }
20293
20400
  if (operation === "platform" && args[1] === "remote") {
20294
20401
  const mode = args[2];
20295
- if (!mode || !["apply", "repair", "status"].includes(mode) || args[3] !== undefined) {
20296
- throw new Error("Usage: fz bootstrap platform remote <apply|repair|status> --bootstrap-config <owner-only-request.json> [--apply]");
20402
+ if (!mode || !["apply", "repair", "promote", "status"].includes(mode) || args[3] !== undefined) {
20403
+ throw new Error("Usage: fz bootstrap platform remote <apply|repair|promote|status> --bootstrap-config <owner-only-request.json> [--apply]");
20297
20404
  }
20298
20405
  if (!options.bootstrapConfigPath)
20299
20406
  throw new Error("remote platform bootstrap requires --bootstrap-config");
@@ -20406,8 +20513,9 @@ async function cmdBootstrap(options, args) {
20406
20513
  return 0;
20407
20514
  }
20408
20515
  const credentialStdin = (operation === "platform" || operation === "repair") && args[1] === "credentials-stdin";
20409
- if (credentialStdin && args[2] !== undefined)
20410
- throw new Error("platform credential stdin accepts no additional positional arguments");
20516
+ const storedCredentials = operation === "repair" && args[1] === "stored-credentials";
20517
+ if ((credentialStdin || storedCredentials) && args[2] !== undefined)
20518
+ throw new Error("platform credential mode accepts no additional positional arguments");
20411
20519
  const installedBootstrapKind = () => resolveInstalledBootstrapKind({
20412
20520
  metal: existsSync13(METAL_BOOTSTRAP_STATE_PATH),
20413
20521
  compute: existsSync13(BOOTSTRAP_STATE_PATH)
@@ -20451,7 +20559,7 @@ async function cmdBootstrap(options, args) {
20451
20559
  return 0;
20452
20560
  }
20453
20561
  if (!["platform", "repair"].includes(operation)) {
20454
- throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [launch|bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|genesis-cleanup|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
20562
+ throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [launch|bundle|fleet <apply|promote|status>|remote <apply|repair|promote|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|genesis-cleanup|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
20455
20563
  }
20456
20564
  const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
20457
20565
  throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
@@ -20468,7 +20576,7 @@ async function cmdBootstrap(options, args) {
20468
20576
  out.step("Review the plan, then repeat with --apply as root.");
20469
20577
  return 0;
20470
20578
  }
20471
- const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config, undefined, {
20579
+ const secrets = config.kind === "platform" && !credentialStdin && !storedCredentials ? await promptPlatformBootstrapSecrets(config, undefined, {
20472
20580
  reuseBoundCloudflare: operation === "repair"
20473
20581
  }) : undefined;
20474
20582
  let stdinSecrets = secrets;
@@ -20486,7 +20594,9 @@ async function cmdBootstrap(options, args) {
20486
20594
  allowStoredCloudflareCredentials: operation === "repair"
20487
20595
  });
20488
20596
  }
20489
- const result = await applyBootstrap(config, undefined, stdinSecrets);
20597
+ const result = await applyBootstrap(config, undefined, stdinSecrets, {
20598
+ reuseBoundPlatformCredentials: storedCredentials
20599
+ });
20490
20600
  out.line(JSON.stringify(result, null, 2));
20491
20601
  return 0;
20492
20602
  } catch (cause) {
@@ -21114,10 +21224,10 @@ function usage() {
21114
21224
  One profile-driven attended launch. Public topology,
21115
21225
  bundle and edge names are derived; the owner supplies
21116
21226
  only email settings, hidden provider tokens and APPLY
21117
- fz bootstrap platform remote <apply|repair|status>
21227
+ fz bootstrap platform remote <apply|repair|promote|status>
21118
21228
  Run typed bootstrap from the operator laptop through
21119
- a pinned host; apply copies and verifies that bundle
21120
- fz bootstrap platform fleet <apply|status>
21229
+ a pinned host; promote reuses sealed host credentials
21230
+ fz bootstrap platform fleet <apply|promote|status>
21121
21231
  One attended genesis operation: reconcile public
21122
21232
  hostnames/Tunnels, seal credentials, deploy the bundle,
21123
21233
  run all three nodes, verify ingress and print the invite
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.128";
369
+ var VERSION = "0.1.130";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -89,7 +89,7 @@ export interface OperatorPlatformFleetCoordinates {
89
89
  databaseRole: 'master' | 'joiner';
90
90
  }[];
91
91
  }
92
- export type OperatorPlatformBootstrapMode = 'apply' | 'repair' | 'status';
92
+ export type OperatorPlatformBootstrapMode = 'apply' | 'repair' | 'promote' | 'status';
93
93
  export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'genesis-cleanup' | 'rehearsal' | 'rehearsal-cleanup' | 'host-keys' | 'rehearsal-host-keys' | 'status';
94
94
  export interface OperatorPlatformBootstrapPlan {
95
95
  kind: 'platform-remote';
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1460
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1461
1461
 
1462
1462
  // src/version.ts
1463
- var VERSION = "0.1.128";
1463
+ var VERSION = "0.1.130";
1464
1464
 
1465
1465
  // src/software.ts
1466
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -4664,6 +4664,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4664
4664
  installed = parseStoredState(host.read(STATE_PATH));
4665
4665
  let bootstrapManifest;
4666
4666
  let releaseEvidence;
4667
+ let redundantBootstrapBundle = false;
4667
4668
  if (config.kind === "platform") {
4668
4669
  const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
4669
4670
  if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
@@ -4684,8 +4685,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4684
4685
  if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
4685
4686
  throw new Error("bootstrap release evidence is malformed");
4686
4687
  releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
4687
- if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
4688
+ if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
4688
4689
  bootstrapManifest = reviewed;
4690
+ } else {
4691
+ redundantBootstrapBundle = true;
4692
+ }
4689
4693
  } else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
4690
4694
  let evidence;
4691
4695
  try {
@@ -4757,7 +4761,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4757
4761
  }
4758
4762
  allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
4759
4763
  }
4760
- const platformPrivate = config.kind === "platform" ? (() => {
4764
+ const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
4765
+ if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
4766
+ throw new Error("stored-credential promotion requires an installed enrolled platform compute");
4767
+ }
4768
+ const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
4761
4769
  if (!secrets)
4762
4770
  throw new Error("platform apply requires attended credentials on stdin");
4763
4771
  const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
@@ -4800,7 +4808,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4800
4808
  if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
4801
4809
  await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
4802
4810
  }
4803
- if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4811
+ if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4804
4812
  await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
4805
4813
  }
4806
4814
  const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
@@ -4858,24 +4866,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4858
4866
  await verifyBootstrapBundleOnHost(host, config, true);
4859
4867
  }
4860
4868
  if (config.kind === "platform") {
4861
- const root = platformPrivate.root;
4862
- if (!host.exists(JWT_CREDENTIAL)) {
4863
- await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4864
- }
4865
- if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4866
- await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4867
- }
4868
- await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4869
- await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4870
4869
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4871
- for (const [name, source] of Object.entries({
4872
- ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4873
- "backup.s3.secretAccessKey": platformPrivate.backup
4874
- })) {
4875
- if (source) {
4876
- const destination2 = `${CREDS}/${name}.cred`;
4877
- if (!host.exists(destination2)) {
4878
- await seal(host, name, destination2, source);
4870
+ if (reuseBoundPlatformCredentials) {
4871
+ const required = [
4872
+ ...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
4873
+ SEED_CREDENTIAL,
4874
+ BACKUP_RECOVERY_CREDENTIAL,
4875
+ ...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
4876
+ ...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
4877
+ ...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
4878
+ ...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
4879
+ ];
4880
+ const missing = required.find((path) => !host.exists(path));
4881
+ if (missing)
4882
+ throw new Error(`stored platform credential is missing: ${missing}`);
4883
+ } else {
4884
+ const root = platformPrivate.root;
4885
+ if (!host.exists(JWT_CREDENTIAL)) {
4886
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4887
+ }
4888
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4889
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4890
+ }
4891
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4892
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4893
+ for (const [name, source] of Object.entries({
4894
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4895
+ "backup.s3.secretAccessKey": platformPrivate.backup
4896
+ })) {
4897
+ if (source) {
4898
+ const destination2 = `${CREDS}/${name}.cred`;
4899
+ if (!host.exists(destination2))
4900
+ await seal(host, name, destination2, source);
4879
4901
  }
4880
4902
  }
4881
4903
  }
@@ -4944,6 +4966,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4944
4966
  await checked3(host, ["nginx", "-t"], "nginx configuration");
4945
4967
  await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
4946
4968
  await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
4969
+ if (alreadyEnrolled && !bootstrapManifest) {
4970
+ const healthCommand = [
4971
+ "curl",
4972
+ "--fail",
4973
+ "--silent",
4974
+ "--show-error",
4975
+ "--max-time",
4976
+ "10",
4977
+ `http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
4978
+ ];
4979
+ const health = await host.exec(healthCommand);
4980
+ if (health.exitCode !== 0) {
4981
+ const activeSlotFile = `${runtime.slotsDirectory}/.active`;
4982
+ if (!host.exists(activeSlotFile))
4983
+ throw new Error("enrolled platform active API slot evidence is missing");
4984
+ const activeSlot = host.read(activeSlotFile).trim();
4985
+ if (activeSlot !== "blue" && activeSlot !== "green") {
4986
+ throw new Error("enrolled platform active API slot evidence is malformed");
4987
+ }
4988
+ await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
4989
+ let ready = false;
4990
+ let lastHealthOutput = "";
4991
+ for (let attempt = 0;attempt < 30; attempt += 1) {
4992
+ const repairedHealth = await host.exec(healthCommand);
4993
+ if (repairedHealth.exitCode === 0) {
4994
+ ready = true;
4995
+ break;
4996
+ }
4997
+ lastHealthOutput = repairedHealth.output.trim();
4998
+ if (attempt < 29)
4999
+ await (host.sleep?.(1000) ?? Bun.sleep(1000));
5000
+ }
5001
+ if (!ready)
5002
+ throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
5003
+ }
5004
+ if (redundantBootstrapBundle) {
5005
+ host.remove(config.bootstrapBundle.bundleFile);
5006
+ host.remove(config.bootstrapBundle.manifestFile);
5007
+ }
5008
+ }
4947
5009
  if (config.database.role === "master") {
4948
5010
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
4949
5011
  if (!host.exists(invite)) {
@@ -5040,7 +5102,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
5040
5102
  host.remove(config.cloudflareHandoff.handoffFile);
5041
5103
  }
5042
5104
  let launch;
5043
- if (config.kind === "platform" && config.database.role === "master") {
5105
+ if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
5044
5106
  const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
5045
5107
  if (!host.exists(invitePath))
5046
5108
  throw new Error("platform invite is missing after deployment");
@@ -6737,7 +6799,14 @@ function planOperatorPlatformBootstrap(request, mode) {
6737
6799
  user: request.target.user,
6738
6800
  ...request.target.jump ? { via: request.target.jump.address } : {}
6739
6801
  },
6740
- steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
6802
+ steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
6803
+ "verify pinned SSH transport and caller-approved SSH agent",
6804
+ "copy pinned Bun and packaged fz artifacts to a private staging directory",
6805
+ "verify and copy the one immutable API Git bundle plus its manifest",
6806
+ "stage the rewritten owner-only platform config without plaintext credentials",
6807
+ "promote the exact release using the credentials already sealed on the enrolled compute",
6808
+ "remove transient local and remote staging data"
6809
+ ] : [
6741
6810
  "verify pinned SSH transport and caller-approved SSH agent",
6742
6811
  "copy pinned Bun and packaged fz artifacts to a private staging directory",
6743
6812
  "verify and copy the one immutable API Git bundle plus its manifest",
@@ -7303,7 +7372,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7303
7372
  allowStoredCloudflareCredentials: mode === "repair"
7304
7373
  }) : undefined;
7305
7374
  const staged = await stageConfig(config, directory, {
7306
- reuseBoundCloudflare: mode === "repair"
7375
+ reuseBoundCloudflare: mode === "repair" || mode === "promote"
7307
7376
  });
7308
7377
  await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
7309
7378
  await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
@@ -7320,15 +7389,15 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
7320
7389
  "-n",
7321
7390
  "/usr/local/bin/fz",
7322
7391
  "bootstrap",
7323
- mode === "repair" ? "repair" : "platform",
7324
- "credentials-stdin"
7392
+ mode === "apply" ? "platform" : "repair",
7393
+ mode === "promote" ? "stored-credentials" : "credentials-stdin"
7325
7394
  ];
7326
7395
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
7327
7396
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
7328
7397
  ` : undefined, mode === "apply");
7329
7398
  return { plan, output };
7330
7399
  } finally {
7331
- if (knownHosts) {
7400
+ if (knownHosts && mode !== "status") {
7332
7401
  await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/rm", "-rf", "--", REMOTE_STAGE, remoteTemp], "remote bootstrap cleanup", true).catch(() => {
7333
7402
  return;
7334
7403
  });
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3020
3020
  }
3021
3021
 
3022
3022
  // src/version.ts
3023
- var VERSION3 = "0.1.128";
3023
+ var VERSION3 = "0.1.130";
3024
3024
 
3025
3025
  // src/egress-policy.ts
3026
3026
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/provision.js CHANGED
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
3020
3020
  }
3021
3021
 
3022
3022
  // src/version.ts
3023
- var VERSION3 = "0.1.128";
3023
+ var VERSION3 = "0.1.130";
3024
3024
 
3025
3025
  // src/egress-policy.ts
3026
3026
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -1,6 +1,7 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import type { AgentOperationTelemetry } from './telemetry-runtime';
3
3
  import { type GitHostResolver } from './git-egress';
4
+ import type { MetalBootstrapConfig } from './metal-bootstrap';
4
5
  export interface RemoteSshBootstrapClaim {
5
6
  computeKey: string;
6
7
  computeReference: string;
@@ -50,30 +51,16 @@ export interface RemoteMetalAdmissionClaim {
50
51
  runnerNodeKey: string;
51
52
  runnerSshPublicKey: string;
52
53
  challenge: string;
53
- provisioning: {
54
- volumeGroup: string;
55
- bridge: string;
56
- subnetPrefix: string;
57
- addressStart: number;
58
- addressEnd: number;
59
- gateway: string;
60
- nameservers: string[];
61
- cpuPools: Array<{
62
- key: string;
63
- cpus: string;
64
- physicalCores: number;
65
- memoryNodes?: string;
66
- }>;
67
- housekeepingCpus: string;
68
- housekeepingMemoryNodes?: string;
69
- confidential?: {
70
- cbitpos: number;
71
- reducedPhysBits: number;
72
- policy: string;
73
- };
74
- };
75
54
  target: RemoteSshBootstrapClaim['target'];
76
55
  }
56
+ type MetalAdmissionProvisioning = Pick<MetalBootstrapConfig['profile'], 'volumeGroup' | 'bridge' | 'subnetPrefix' | 'addressStart' | 'addressEnd' | 'gateway' | 'nameservers' | 'cpuPools' | 'housekeepingCpus'>;
57
+ /**
58
+ * Convert runner-observed Linux CPU topology into the fixed ForgeZero host
59
+ * boundary. The browser never authors this capacity. One physical core and all
60
+ * of its SMT siblings remain reserved for the host; every other complete core
61
+ * becomes the initial allocatable pool.
62
+ */
63
+ export declare function deriveMetalAdmissionProvisioning(lscpu: string): MetalAdmissionProvisioning;
77
64
  export interface MetalAdmissionProof {
78
65
  jobKey: string;
79
66
  tenantKey: string;
@@ -181,3 +168,4 @@ export declare function startMetalAdmissionPull(options: SshBootstrapPullOptions
181
168
  stop(): Promise<void>;
182
169
  readonly active: boolean;
183
170
  };
171
+ export {};
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.128";
2
+ export declare const VERSION = "0.1.130";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.128",
3
+ "version": "0.1.130",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -11,13 +11,13 @@
11
11
  "typescript": "^5.6.0",
12
12
  "@types/bun": "latest",
13
13
  "@types/node": "^22.0.0",
14
- "@forgezero/access": "0.1.13",
14
+ "@forgezero/access": "0.1.14",
15
15
  "@noble/curves": "^2.2.0",
16
16
  "@noble/post-quantum": "^0.6.1"
17
17
  },
18
18
  "dependencies": {
19
- "@forgezero/runtime": "0.1.19",
20
- "@forgezero/vault": "0.1.25",
19
+ "@forgezero/runtime": "0.1.21",
20
+ "@forgezero/vault": "0.1.27",
21
21
  "@noble/curves": "2.2.0",
22
22
  "@noble/hashes": "2.2.0",
23
23
  "@noble/post-quantum": "0.6.1",