@forgezero/agent 0.1.70 → 0.1.72

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
@@ -644,7 +644,7 @@ import type {
644
644
  <a id="forgezero-agent-cloudflare-bootstrap"></a>
645
645
  ## @forgezero/agent/cloudflare-bootstrap
646
646
 
647
- Attended two-token Cloudflare Tunnel/DNS reconciliation and per-node connector/KV runtime handoff for existing KV resources. This entry exposes 13 named value exports and 16 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
647
+ Attended two-token Cloudflare Tunnel/DNS reconciliation and per-node connector/KV runtime handoff for existing KV resources. This entry exposes 14 named value exports and 17 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
648
648
 
649
649
  ```text
650
650
  import {
@@ -659,6 +659,7 @@ import {
659
659
  runAttendedCloudflareBootstrap,
660
660
  validateCloudflareBootstrapCoordinates,
661
661
  validateCloudflareBootstrapTokens,
662
+ validateCloudflareBootstrapTopology,
662
663
  verifyCloudflareBootstrapAcceptance,
663
664
  writeOwnerBootstrapOutput,
664
665
  } from '@forgezero/agent/cloudflare-bootstrap';
@@ -676,6 +677,7 @@ import type {
676
677
  CloudflareBootstrapPhaseRunner,
677
678
  CloudflareBootstrapPlan,
678
679
  CloudflareBootstrapTokens,
680
+ CloudflareBootstrapTopology,
679
681
  CloudflareConnectorHandoff,
680
682
  CloudflareHostHandoff,
681
683
  CloudflareMeshCoordinates,
@@ -1023,7 +1025,7 @@ import type {
1023
1025
  <a id="forgezero-agent-metal-bootstrap"></a>
1024
1026
  ## @forgezero/agent/metal-bootstrap
1025
1027
 
1026
- Physical-metal bootstrap planning, application and status evidence. This entry exposes 10 named value exports and 6 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1028
+ Physical-metal bootstrap planning, application and status evidence. This entry exposes 10 named value exports and 7 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1027
1029
 
1028
1030
  ```text
1029
1031
  import {
@@ -1046,6 +1048,7 @@ import type {
1046
1048
  MetalBootstrapPlan,
1047
1049
  MetalBootstrapResult,
1048
1050
  MetalBootstrapStatus,
1051
+ MetalBootstrapValidationOptions,
1049
1052
  } from '@forgezero/agent/metal-bootstrap';
1050
1053
  ```
1051
1054
 
@@ -749,7 +749,7 @@ async function postSignedNode(options, path, body) {
749
749
  }
750
750
 
751
751
  // src/version.ts
752
- var VERSION3 = "0.1.70";
752
+ var VERSION3 = "0.1.72";
753
753
 
754
754
  // src/agent-heartbeat.ts
755
755
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
package/dist/bootstrap.js CHANGED
@@ -450,20 +450,17 @@ var privateMeshCidr = (value) => {
450
450
  }
451
451
  return `${address}/${prefix}`;
452
452
  };
453
- function validateCloudflareBootstrapCoordinates(input) {
453
+ function validateCloudflareBootstrapTopology(input) {
454
454
  if (!input || typeof input !== "object" || Array.isArray(input)) {
455
- throw new Error("Cloudflare bootstrap coordinates must be an object");
455
+ throw new Error("Cloudflare bootstrap topology must be an object");
456
456
  }
457
457
  const unsupported = Object.keys(input).filter((key) => ![
458
- "accountId",
459
- "zoneId",
460
- "kvNamespaceId",
461
458
  "meshDevicePolicyId",
462
459
  "realtime",
463
460
  "nodes"
464
461
  ].includes(key));
465
462
  if (unsupported.length)
466
- throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
463
+ throw new Error(`Cloudflare bootstrap topology contains unsupported field ${unsupported[0]}`);
467
464
  const nodeInputs = input.nodes;
468
465
  if (!Array.isArray(nodeInputs))
469
466
  throw new Error("Cloudflare bootstrap requires an explicit nodes array");
@@ -570,9 +567,6 @@ function validateCloudflareBootstrapCoordinates(input) {
570
567
  realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
571
568
  }
572
569
  return {
573
- accountId: validateId(input.accountId, "Cloudflare account id"),
574
- zoneId: validateId(input.zoneId, "Cloudflare zone id"),
575
- kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
576
570
  ...input.meshDevicePolicyId ? {
577
571
  meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
578
572
  } : {},
@@ -580,6 +574,32 @@ function validateCloudflareBootstrapCoordinates(input) {
580
574
  nodes
581
575
  };
582
576
  }
577
+ function validateCloudflareBootstrapCoordinates(input) {
578
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
579
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
580
+ }
581
+ const unsupported = Object.keys(input).filter((key) => ![
582
+ "accountId",
583
+ "zoneId",
584
+ "kvNamespaceId",
585
+ "meshDevicePolicyId",
586
+ "realtime",
587
+ "nodes"
588
+ ].includes(key));
589
+ if (unsupported.length)
590
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
591
+ const topology = validateCloudflareBootstrapTopology({
592
+ ...input.meshDevicePolicyId ? { meshDevicePolicyId: input.meshDevicePolicyId } : {},
593
+ ...input.realtime ? { realtime: input.realtime } : {},
594
+ nodes: input.nodes
595
+ });
596
+ return {
597
+ accountId: validateId(input.accountId, "Cloudflare account id"),
598
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
599
+ kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
600
+ ...topology
601
+ };
602
+ }
583
603
  function planCloudflareBootstrap(input, outputPath) {
584
604
  const coordinates = validateCloudflareBootstrapCoordinates(input);
585
605
  return {
@@ -1396,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1396
1416
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1397
1417
 
1398
1418
  // src/version.ts
1399
- var VERSION = "0.1.70";
1419
+ var VERSION = "0.1.72";
1400
1420
 
1401
1421
  // src/software.ts
1402
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1,4 +1,4 @@
1
- import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokens } from '../cloudflare-bootstrap';
1
+ import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokens, type CloudflareBootstrapNodeCoordinates } from '../cloudflare-bootstrap';
2
2
  import { discoverCloudflareBootstrapResources, type CloudflareBootstrapDiscovery } from '../cloudflare-edge';
3
3
  export interface CloudflareBootstrapCommandConfig {
4
4
  format: 1;
@@ -6,6 +6,29 @@ export interface CloudflareBootstrapCommandConfig {
6
6
  checkpointPath: string;
7
7
  coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
8
8
  }
9
+ /**
10
+ * Human-reviewable discovery coordinates used by the integrated platform
11
+ * launch. Cloudflare ids are deliberately absent: the two hidden tokens resolve
12
+ * and bind them immediately before apply.
13
+ */
14
+ export interface CloudflareBootstrapDiscoveryCommandConfig {
15
+ format: 1;
16
+ kind: 'forgezero-cloudflare-bootstrap-discovery-request';
17
+ checkpointPath: string;
18
+ discovery: {
19
+ zoneName: string;
20
+ kvNamespaceTitle: string;
21
+ workerScriptName?: string;
22
+ endpoint?: string;
23
+ producer?: string;
24
+ };
25
+ nodes: readonly CloudflareBootstrapNodeCoordinates[];
26
+ }
27
+ export interface CloudflareBootstrapCommandPlan {
28
+ checkpointPath: string;
29
+ nodes: readonly CloudflareBootstrapNodeCoordinates[];
30
+ discovery?: CloudflareBootstrapDiscoveryCommandConfig['discovery'];
31
+ }
9
32
  export interface CloudflareBootstrapCommandDependencies {
10
33
  run?: CloudflareBootstrapPhaseRunner;
11
34
  write?: (text: string) => void;
@@ -33,11 +56,24 @@ export declare function createCloudflareBootstrapCommandConfig(input: {
33
56
  checkpointPath: string;
34
57
  coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
35
58
  }): CloudflareBootstrapCommandConfig;
59
+ /** Create the secret-free, name-based request used by one-command genesis. */
60
+ export declare function createCloudflareBootstrapDiscoveryCommandConfig(input: {
61
+ checkpointPath: string;
62
+ discovery: CloudflareBootstrapDiscoveryCommandConfig['discovery'];
63
+ nodes: readonly CloudflareBootstrapNodeCoordinates[];
64
+ }): CloudflareBootstrapDiscoveryCommandConfig;
36
65
  /**
37
66
  * Read reviewed non-secret coordinates for the attended operator phase. Raw
38
67
  * token values and a persisted mode are not schema fields.
39
68
  */
40
69
  export declare function readCloudflareBootstrapCommandConfig(path: string, mode: 'plan' | 'apply'): AttendedCloudflareBootstrapRequest;
70
+ export declare function readCloudflareBootstrapDiscoveryCommandConfig(path: string): CloudflareBootstrapDiscoveryCommandConfig;
71
+ /** Read either supported request shape without contacting Cloudflare. */
72
+ export declare function readCloudflareBootstrapCommandPlan(path: string): CloudflareBootstrapCommandPlan;
73
+ /** Resolve name selectors with hidden credentials immediately before apply. */
74
+ export declare function resolveCloudflareBootstrapCommandConfig(path: string, tokens: CloudflareBootstrapTokens, dependencies?: {
75
+ discover?: typeof discoverCloudflareBootstrapCommandResources;
76
+ }): Promise<AttendedCloudflareBootstrapRequest>;
41
77
  export declare function runCloudflareBootstrapCommand(configPath: string, apply: boolean, tokens?: CloudflareBootstrapTokens, dependencies?: CloudflareBootstrapCommandDependencies): Promise<CloudflareBootstrapEvidence>;
42
78
  export declare function runCloudflareBootstrapVerificationCommand(checkpointPath: string, dependencies?: {
43
79
  verify?: typeof verifyCloudflareBootstrapAcceptance;
@@ -115,6 +115,13 @@ export interface CloudflareBootstrapDependencies {
115
115
  }
116
116
  export type CloudflareBootstrapPhaseRunner = (request: AttendedCloudflareBootstrapRequest) => Promise<CloudflareBootstrapEvidence>;
117
117
  export declare function validateCloudflareBootstrapTokens(input: unknown): CloudflareBootstrapTokens;
118
+ export interface CloudflareBootstrapTopology {
119
+ meshDevicePolicyId?: string;
120
+ realtime?: CloudflareRealtimeCoordinates;
121
+ nodes: CloudflareBootstrapNodeCoordinates[];
122
+ }
123
+ /** Validate the fleet/realtime shape without inventing resource identifiers. */
124
+ export declare function validateCloudflareBootstrapTopology(input: CloudflareBootstrapTopology): CloudflareBootstrapTopology;
118
125
  export declare function validateCloudflareBootstrapCoordinates(input: CloudflareBootstrapCoordinates): CloudflareBootstrapCoordinates;
119
126
  export declare function planCloudflareBootstrap(input: CloudflareBootstrapCoordinates, outputPath: string): CloudflareBootstrapPlan;
120
127
  export interface CloudflareConnectorHandoff {
@@ -450,20 +450,17 @@ var privateMeshCidr = (value) => {
450
450
  }
451
451
  return `${address}/${prefix}`;
452
452
  };
453
- function validateCloudflareBootstrapCoordinates(input) {
453
+ function validateCloudflareBootstrapTopology(input) {
454
454
  if (!input || typeof input !== "object" || Array.isArray(input)) {
455
- throw new Error("Cloudflare bootstrap coordinates must be an object");
455
+ throw new Error("Cloudflare bootstrap topology must be an object");
456
456
  }
457
457
  const unsupported = Object.keys(input).filter((key) => ![
458
- "accountId",
459
- "zoneId",
460
- "kvNamespaceId",
461
458
  "meshDevicePolicyId",
462
459
  "realtime",
463
460
  "nodes"
464
461
  ].includes(key));
465
462
  if (unsupported.length)
466
- throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
463
+ throw new Error(`Cloudflare bootstrap topology contains unsupported field ${unsupported[0]}`);
467
464
  const nodeInputs = input.nodes;
468
465
  if (!Array.isArray(nodeInputs))
469
466
  throw new Error("Cloudflare bootstrap requires an explicit nodes array");
@@ -570,9 +567,6 @@ function validateCloudflareBootstrapCoordinates(input) {
570
567
  realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
571
568
  }
572
569
  return {
573
- accountId: validateId(input.accountId, "Cloudflare account id"),
574
- zoneId: validateId(input.zoneId, "Cloudflare zone id"),
575
- kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
576
570
  ...input.meshDevicePolicyId ? {
577
571
  meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
578
572
  } : {},
@@ -580,6 +574,32 @@ function validateCloudflareBootstrapCoordinates(input) {
580
574
  nodes
581
575
  };
582
576
  }
577
+ function validateCloudflareBootstrapCoordinates(input) {
578
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
579
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
580
+ }
581
+ const unsupported = Object.keys(input).filter((key) => ![
582
+ "accountId",
583
+ "zoneId",
584
+ "kvNamespaceId",
585
+ "meshDevicePolicyId",
586
+ "realtime",
587
+ "nodes"
588
+ ].includes(key));
589
+ if (unsupported.length)
590
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
591
+ const topology = validateCloudflareBootstrapTopology({
592
+ ...input.meshDevicePolicyId ? { meshDevicePolicyId: input.meshDevicePolicyId } : {},
593
+ ...input.realtime ? { realtime: input.realtime } : {},
594
+ nodes: input.nodes
595
+ });
596
+ return {
597
+ accountId: validateId(input.accountId, "Cloudflare account id"),
598
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
599
+ kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
600
+ ...topology
601
+ };
602
+ }
583
603
  function planCloudflareBootstrap(input, outputPath) {
584
604
  const coordinates = validateCloudflareBootstrapCoordinates(input);
585
605
  return {
@@ -1211,6 +1231,7 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1211
1231
  export {
1212
1232
  writeOwnerBootstrapOutput,
1213
1233
  verifyCloudflareBootstrapAcceptance,
1234
+ validateCloudflareBootstrapTopology,
1214
1235
  validateCloudflareBootstrapTokens,
1215
1236
  validateCloudflareBootstrapCoordinates,
1216
1237
  runAttendedCloudflareBootstrap,
package/dist/fz-agent.js CHANGED
@@ -8588,7 +8588,7 @@ import { join as join3 } from "path";
8588
8588
  import { fileURLToPath } from "url";
8589
8589
 
8590
8590
  // src/version.ts
8591
- var VERSION2 = "0.1.70";
8591
+ var VERSION2 = "0.1.72";
8592
8592
 
8593
8593
  // src/ssh-bootstrap.ts
8594
8594
  class SshBootstrapError extends Error {
package/dist/fz.js CHANGED
@@ -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.70";
4832
+ var VERSION2 = "0.1.72";
4833
4833
 
4834
4834
  // src/software.ts
4835
4835
  var PINNED_BUN_VERSION = "1.3.14";
@@ -12551,20 +12551,17 @@ var privateMeshCidr = (value) => {
12551
12551
  }
12552
12552
  return `${address}/${prefix}`;
12553
12553
  };
12554
- function validateCloudflareBootstrapCoordinates(input) {
12554
+ function validateCloudflareBootstrapTopology(input) {
12555
12555
  if (!input || typeof input !== "object" || Array.isArray(input)) {
12556
- throw new Error("Cloudflare bootstrap coordinates must be an object");
12556
+ throw new Error("Cloudflare bootstrap topology must be an object");
12557
12557
  }
12558
12558
  const unsupported = Object.keys(input).filter((key) => ![
12559
- "accountId",
12560
- "zoneId",
12561
- "kvNamespaceId",
12562
12559
  "meshDevicePolicyId",
12563
12560
  "realtime",
12564
12561
  "nodes"
12565
12562
  ].includes(key));
12566
12563
  if (unsupported.length)
12567
- throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
12564
+ throw new Error(`Cloudflare bootstrap topology contains unsupported field ${unsupported[0]}`);
12568
12565
  const nodeInputs = input.nodes;
12569
12566
  if (!Array.isArray(nodeInputs))
12570
12567
  throw new Error("Cloudflare bootstrap requires an explicit nodes array");
@@ -12671,9 +12668,6 @@ function validateCloudflareBootstrapCoordinates(input) {
12671
12668
  realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
12672
12669
  }
12673
12670
  return {
12674
- accountId: validateId(input.accountId, "Cloudflare account id"),
12675
- zoneId: validateId(input.zoneId, "Cloudflare zone id"),
12676
- kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
12677
12671
  ...input.meshDevicePolicyId ? {
12678
12672
  meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
12679
12673
  } : {},
@@ -12681,6 +12675,32 @@ function validateCloudflareBootstrapCoordinates(input) {
12681
12675
  nodes
12682
12676
  };
12683
12677
  }
12678
+ function validateCloudflareBootstrapCoordinates(input) {
12679
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
12680
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
12681
+ }
12682
+ const unsupported = Object.keys(input).filter((key) => ![
12683
+ "accountId",
12684
+ "zoneId",
12685
+ "kvNamespaceId",
12686
+ "meshDevicePolicyId",
12687
+ "realtime",
12688
+ "nodes"
12689
+ ].includes(key));
12690
+ if (unsupported.length)
12691
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
12692
+ const topology = validateCloudflareBootstrapTopology({
12693
+ ...input.meshDevicePolicyId ? { meshDevicePolicyId: input.meshDevicePolicyId } : {},
12694
+ ...input.realtime ? { realtime: input.realtime } : {},
12695
+ nodes: input.nodes
12696
+ });
12697
+ return {
12698
+ accountId: validateId(input.accountId, "Cloudflare account id"),
12699
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
12700
+ kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
12701
+ ...topology
12702
+ };
12703
+ }
12684
12704
  function planCloudflareBootstrap(input, outputPath) {
12685
12705
  const coordinates = validateCloudflareBootstrapCoordinates(input);
12686
12706
  return {
@@ -14970,6 +14990,56 @@ function createCloudflareBootstrapCommandConfig(input) {
14970
14990
  coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
14971
14991
  };
14972
14992
  }
14993
+ var normalizeDiscovery = (input) => {
14994
+ const zoneName = input.zoneName.trim().toLowerCase().replace(/\.$/, "");
14995
+ const kvNamespaceTitle = input.kvNamespaceTitle.trim();
14996
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(zoneName)) {
14997
+ throw new Error("Cloudflare zone name is invalid");
14998
+ }
14999
+ if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
15000
+ throw new Error("Cloudflare KV namespace title is invalid");
15001
+ }
15002
+ const workerScriptName = input.workerScriptName?.trim();
15003
+ const endpoint2 = input.endpoint?.trim();
15004
+ const producer = input.producer?.trim();
15005
+ if ([workerScriptName, endpoint2, producer].filter((value) => value !== undefined).length !== 0 && (!workerScriptName || !endpoint2 || !producer)) {
15006
+ throw new Error("Cloudflare realtime discovery requires Worker name, endpoint, and producer together");
15007
+ }
15008
+ return {
15009
+ zoneName,
15010
+ kvNamespaceTitle,
15011
+ ...workerScriptName ? { workerScriptName, endpoint: endpoint2, producer } : {}
15012
+ };
15013
+ };
15014
+ function createCloudflareBootstrapDiscoveryCommandConfig(input) {
15015
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
15016
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
15017
+ }
15018
+ const discovery = normalizeDiscovery(input.discovery);
15019
+ const normalized = validateCloudflareBootstrapTopology({
15020
+ ...discovery.workerScriptName ? { realtime: {
15021
+ workerScriptName: discovery.workerScriptName,
15022
+ endpoint: discovery.endpoint,
15023
+ producer: discovery.producer
15024
+ } } : {},
15025
+ nodes: [...input.nodes]
15026
+ });
15027
+ return {
15028
+ format: 1,
15029
+ kind: "forgezero-cloudflare-bootstrap-discovery-request",
15030
+ checkpointPath: input.checkpointPath.trim(),
15031
+ discovery: {
15032
+ zoneName: discovery.zoneName,
15033
+ kvNamespaceTitle: discovery.kvNamespaceTitle,
15034
+ ...normalized.realtime ? {
15035
+ workerScriptName: normalized.realtime.workerScriptName,
15036
+ endpoint: normalized.realtime.endpoint,
15037
+ producer: normalized.realtime.producer
15038
+ } : {}
15039
+ },
15040
+ nodes: normalized.nodes
15041
+ };
15042
+ }
14973
15043
  function readOwnerConfig(path) {
14974
15044
  const absolute2 = resolve7(path);
14975
15045
  let descriptor;
@@ -15037,6 +15107,79 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
15037
15107
  checkpointPath: resolve7(baseDirectory, input.checkpointPath)
15038
15108
  };
15039
15109
  }
15110
+ function readCloudflareBootstrapDiscoveryCommandConfig(path) {
15111
+ const input = record2(readOwnerConfig(path), "Cloudflare bootstrap discovery config");
15112
+ const baseDirectory = dirname10(resolve7(path));
15113
+ exactKeys3(input, ["format", "kind", "checkpointPath", "discovery", "nodes"], "Cloudflare bootstrap discovery config");
15114
+ if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-discovery-request") {
15115
+ throw new Error("Cloudflare bootstrap discovery config format/kind is invalid");
15116
+ }
15117
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
15118
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
15119
+ }
15120
+ const discovery = record2(input.discovery, "Cloudflare bootstrap discovery selectors");
15121
+ exactKeys3(discovery, [
15122
+ "zoneName",
15123
+ "kvNamespaceTitle",
15124
+ "workerScriptName",
15125
+ "endpoint",
15126
+ "producer"
15127
+ ], "Cloudflare bootstrap discovery selectors");
15128
+ if (!Array.isArray(input.nodes))
15129
+ throw new Error("Cloudflare bootstrap discovery nodes must be an array");
15130
+ for (const node of input.nodes) {
15131
+ exactKeys3(record2(node, "Cloudflare bootstrap node"), [
15132
+ "nodeName",
15133
+ "hostname",
15134
+ "service",
15135
+ "tunnelName",
15136
+ "mesh"
15137
+ ], "Cloudflare bootstrap node");
15138
+ }
15139
+ const created = createCloudflareBootstrapDiscoveryCommandConfig({
15140
+ checkpointPath: resolve7(baseDirectory, input.checkpointPath),
15141
+ discovery,
15142
+ nodes: input.nodes
15143
+ });
15144
+ return created;
15145
+ }
15146
+ function readCloudflareBootstrapCommandPlan(path) {
15147
+ const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
15148
+ if (input.kind === "forgezero-cloudflare-bootstrap-discovery-request") {
15149
+ const config = readCloudflareBootstrapDiscoveryCommandConfig(path);
15150
+ return { checkpointPath: config.checkpointPath, nodes: config.nodes, discovery: config.discovery };
15151
+ }
15152
+ const request = readCloudflareBootstrapCommandConfig(path, "plan");
15153
+ return { checkpointPath: request.checkpointPath, nodes: request.coordinates.nodes };
15154
+ }
15155
+ async function resolveCloudflareBootstrapCommandConfig(path, tokens, dependencies = {}) {
15156
+ const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
15157
+ if (input.kind !== "forgezero-cloudflare-bootstrap-discovery-request") {
15158
+ const request = readCloudflareBootstrapCommandConfig(path, "apply");
15159
+ request.tokens = tokens;
15160
+ return request;
15161
+ }
15162
+ const config = readCloudflareBootstrapDiscoveryCommandConfig(path);
15163
+ const coordinates = await (dependencies.discover ?? discoverCloudflareBootstrapCommandResources)({
15164
+ zoneName: config.discovery.zoneName,
15165
+ kvNamespaceTitle: config.discovery.kvNamespaceTitle,
15166
+ ...config.discovery.workerScriptName ? { workerScriptName: config.discovery.workerScriptName } : {},
15167
+ tokens
15168
+ });
15169
+ return {
15170
+ mode: "apply",
15171
+ checkpointPath: config.checkpointPath,
15172
+ coordinates: validateCloudflareBootstrapCoordinates({
15173
+ ...coordinates,
15174
+ ...config.discovery.workerScriptName ? { realtime: {
15175
+ workerScriptName: config.discovery.workerScriptName,
15176
+ endpoint: config.discovery.endpoint,
15177
+ producer: config.discovery.producer
15178
+ } } : {},
15179
+ nodes: [...config.nodes]
15180
+ })
15181
+ };
15182
+ }
15040
15183
  async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
15041
15184
  const request = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
15042
15185
  if (apply)
@@ -15379,7 +15522,7 @@ var validUnit = (unit) => {
15379
15522
  throw new MetalBootstrapError("the OTLP collector must be independently supervised");
15380
15523
  }
15381
15524
  };
15382
- function validateMetalBootstrapConfig(config) {
15525
+ function validateMetalBootstrapConfig(config, options = {}) {
15383
15526
  if (!config || typeof config !== "object")
15384
15527
  throw new MetalBootstrapError("metal bootstrap config must be an object");
15385
15528
  exactKeys4(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
@@ -15423,7 +15566,7 @@ function validateMetalBootstrapConfig(config) {
15423
15566
  for (const image of Object.values(config.profile.images))
15424
15567
  if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve8(image.path) !== image.path || /[\0\r\n]/.test(image.path))
15425
15568
  throw new MetalBootstrapError("every pinned guest image must use the fixed image directory");
15426
- if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
15569
+ if (!options.allowHistoricalRelease && (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2)) {
15427
15570
  throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
15428
15571
  }
15429
15572
  let api;
@@ -15469,7 +15612,7 @@ function validateOwnerOnlyPath(path, requireRootOwner) {
15469
15612
  throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
15470
15613
  }
15471
15614
  }
15472
- function readMetalBootstrapConfig(path) {
15615
+ function readMetalBootstrapConfig(path, options = {}) {
15473
15616
  validateOwnerOnlyPath(path, false);
15474
15617
  const metadata = statSync(path);
15475
15618
  if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
@@ -15480,7 +15623,7 @@ function readMetalBootstrapConfig(path) {
15480
15623
  } catch {
15481
15624
  throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
15482
15625
  }
15483
- return validateMetalBootstrapConfig(parsed);
15626
+ return validateMetalBootstrapConfig(parsed, options);
15484
15627
  }
15485
15628
  function planMetalBootstrap(config) {
15486
15629
  validateMetalBootstrapConfig(config);
@@ -15981,6 +16124,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
15981
16124
  problems.push("metal helper socket is not ready");
15982
16125
  if (!updateSocketReady)
15983
16126
  problems.push("Agent update helper socket is not ready");
16127
+ const versionResult = await exec([AGENT_PATH, "--version"]);
16128
+ const installedAgentVersion = versionResult.exitCode === 0 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(versionResult.stdout.trim()) ? versionResult.stdout.trim() : undefined;
16129
+ if (!installedAgentVersion)
16130
+ problems.push("installed Metal Agent version is unavailable or malformed");
15984
16131
  return {
15985
16132
  initialized: existsSync10(STATE_PATH2),
15986
16133
  profileValid,
@@ -15990,6 +16137,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
15990
16137
  helperSocketReady,
15991
16138
  updateSocketReady,
15992
16139
  metalHostname,
16140
+ installedAgentVersion,
15993
16141
  problems
15994
16142
  };
15995
16143
  }
@@ -16173,7 +16321,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
16173
16321
  readOperatorPlatformBootstrapRequest(file);
16174
16322
  return writeOwnerJson2(path, request, "operator fleet request");
16175
16323
  }
16176
- var validateMetalRequestValue = (value) => {
16324
+ var validateMetalRequestValue = (value, options = {}) => {
16177
16325
  const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
16178
16326
  if (root.kind !== "metal-remote")
16179
16327
  throw new Error("operator metal bootstrap kind must be metal-remote");
@@ -16193,7 +16341,8 @@ var validateMetalRequestValue = (value) => {
16193
16341
  if (typeof root.metalConfigFile !== "string")
16194
16342
  throw new Error("metalConfigFile must be an absolute path");
16195
16343
  canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
16196
- readMetalBootstrapConfig(root.metalConfigFile);
16344
+ if (options.validateMetalConfig !== false)
16345
+ readMetalBootstrapConfig(root.metalConfigFile);
16197
16346
  const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
16198
16347
  const guest = (value2, label) => {
16199
16348
  const row2 = exactKeys5(value2, [
@@ -16252,9 +16401,9 @@ var validateMetalRequestValue = (value) => {
16252
16401
  }
16253
16402
  };
16254
16403
  };
16255
- function readOperatorMetalBootstrapRequest(path) {
16404
+ function readOperatorMetalBootstrapRequest(path, options = {}) {
16256
16405
  const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
16257
- return validateMetalRequestValue(value);
16406
+ return validateMetalRequestValue(value, options);
16258
16407
  }
16259
16408
  function writeOperatorMetalBootstrapRequest(path, request) {
16260
16409
  validateMetalRequestValue(request);
@@ -16303,7 +16452,7 @@ function writeOperatorGuestHostKeyEvidence(path, evidence, request) {
16303
16452
  return writeOwnerJson2(path, validateGuestHostKeyEvidenceValue(evidence, request), "operator guest host-key evidence");
16304
16453
  }
16305
16454
  function planOperatorMetalBootstrap(request, mode) {
16306
- const config = readMetalBootstrapConfig(request.metalConfigFile);
16455
+ const config = mode === "apply" ? readMetalBootstrapConfig(request.metalConfigFile) : undefined;
16307
16456
  const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
16308
16457
  if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
16309
16458
  throw new Error("the reviewed metal request has no rehearsal node");
@@ -16323,7 +16472,7 @@ function planOperatorMetalBootstrap(request, mode) {
16323
16472
  "remove transient local and remote staging data"
16324
16473
  ],
16325
16474
  genesisNodes,
16326
- secretInputs: config.agentSeedFile ? ["metal-agent-seed"] : []
16475
+ secretInputs: config?.agentSeedFile ? ["metal-agent-seed"] : []
16327
16476
  };
16328
16477
  }
16329
16478
  var secretSources = (config) => [
@@ -16475,7 +16624,10 @@ var defaultExec3 = async (argv2, options = {}) => {
16475
16624
  new Response(child.stderr).text(),
16476
16625
  child.exited
16477
16626
  ]);
16478
- return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
16627
+ return {
16628
+ exitCode,
16629
+ output: options.secret ? options.safeStdout && exitCode === 0 ? stdout.slice(0, 65536) : "" : `${stdout}${stderr}`.slice(0, 65536)
16630
+ };
16479
16631
  };
16480
16632
  var checked5 = async (exec, argv2, label, options) => {
16481
16633
  const result = await exec(argv2, options);
@@ -16529,9 +16681,13 @@ var safeRemoteArg = (value) => {
16529
16681
  if (!/^[A-Za-z0-9_+.,:@%/=\/-]{1,1024}$/.test(value))
16530
16682
  throw new Error(`unsafe remote argument: ${value}`);
16531
16683
  };
16532
- var remote = async (exec, request, knownHosts, argv2, label, secret = false, stdin) => {
16684
+ var remote = async (exec, request, knownHosts, argv2, label, secret = false, stdin, safeStdout = false) => {
16533
16685
  argv2.forEach(safeRemoteArg);
16534
- return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv2], label, { secret, stdin });
16686
+ return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv2], label, {
16687
+ secret,
16688
+ stdin,
16689
+ safeStdout
16690
+ });
16535
16691
  };
16536
16692
  var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
16537
16693
  safeRemoteArg(path);
@@ -16736,7 +16892,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
16736
16892
  const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
16737
16893
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
16738
16894
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
16739
- ` : undefined);
16895
+ ` : undefined, mode === "apply");
16740
16896
  return { plan, output };
16741
16897
  } finally {
16742
16898
  if (knownHosts) {
@@ -18175,11 +18331,13 @@ function writeMetalOperatorFiles(directory) {
18175
18331
  function writePlatformGenesisFleet(directory) {
18176
18332
  const output = genesisOutputDirectory(directory);
18177
18333
  const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
18178
- const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile);
18334
+ const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
18179
18335
  const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
18180
18336
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
18181
18337
  const fleet = metalRequest.genesis.nodes;
18182
- const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile).metalHostname;
18338
+ const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile, {
18339
+ allowHistoricalRelease: true
18340
+ }).metalHostname;
18183
18341
  const checkpointPath = join12(output, "cloudflare-handoff.json");
18184
18342
  const template = interactiveBootstrap("platform", fleet, checkpointPath);
18185
18343
  const region = {
@@ -18195,24 +18353,23 @@ function writePlatformGenesisFleet(directory) {
18195
18353
  cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18196
18354
  });
18197
18355
  const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
18198
- const cloudflareConfig = writeBootstrapConfig(join12(output, "cloudflare.json"), createCloudflareBootstrapCommandConfig({
18356
+ const cloudflareConfig = writeBootstrapConfig(join12(output, "cloudflare.json"), createCloudflareBootstrapDiscoveryCommandConfig({
18199
18357
  checkpointPath,
18200
- coordinates: {
18201
- accountId: bootstrapAnswer("Cloudflare account id"),
18202
- zoneId: bootstrapAnswer("Cloudflare zone id"),
18203
- kvNamespaceId: bootstrapAnswer("Existing Worker-bound KV namespace id"),
18204
- ...realtimeEnabled ? { realtime: {
18358
+ discovery: {
18359
+ zoneName: bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
18360
+ kvNamespaceTitle: bootstrapAnswer("Existing Worker-bound KV namespace title"),
18361
+ ...realtimeEnabled ? {
18205
18362
  workerScriptName: bootstrapAnswer("Existing Worker script name"),
18206
18363
  endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
18207
18364
  producer: bootstrapAnswer("Realtime producer identity", "platform-api")
18208
- } } : {},
18209
- nodes: fleet.map((guest, index) => ({
18210
- nodeName: guest.name,
18211
- hostname: nodes[index].nodeHostname,
18212
- service: "http://127.0.0.1:3000",
18213
- tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
18214
- }))
18215
- }
18365
+ } : {}
18366
+ },
18367
+ nodes: fleet.map((guest, index) => ({
18368
+ nodeName: guest.name,
18369
+ hostname: nodes[index].nodeHostname,
18370
+ service: "http://127.0.0.1:3000",
18371
+ tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
18372
+ }))
18216
18373
  }));
18217
18374
  const cloudflareAcceptance = join12(output, "cloudflare-acceptance.json");
18218
18375
  const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
@@ -18288,7 +18445,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
18288
18445
  const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
18289
18446
  const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
18290
18447
  const cloudflareHandoffFile = genesis ? cloudflareHostHandoffPath(genesisCloudflareCheckpoint, computeReference) : bootstrapAnswer("Node-specific Cloudflare host handoff (blank for no public edge)", "");
18291
- const cloudflareNodeName = cloudflareHandoffFile ? bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
18448
+ const cloudflareNodeName = cloudflareHandoffFile ? genesis ? computeReference : bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
18292
18449
  const databaseNetworkMode = "private-lan";
18293
18450
  const enrolmentSource = genesis ? "genesis-derived" : bootstrapAnswer("Enrolment source (genesis-derived/api-token)", "genesis-derived");
18294
18451
  return {
@@ -18365,10 +18522,10 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
18365
18522
  }
18366
18523
  async function runAttendedPlatformFleet(fleet, apply) {
18367
18524
  const plan = planOperatorPlatformFleetBootstrap(fleet, "apply");
18368
- const cloudflare = readCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, apply ? "apply" : "plan");
18525
+ const cloudflarePlan = readCloudflareBootstrapCommandPlan(fleet.cloudflareConfigFile);
18369
18526
  const coordinates = operatorPlatformFleetCoordinates(fleet);
18370
- if (cloudflare.coordinates.nodes.length !== coordinates.nodes.length || coordinates.nodes.some((node, index) => {
18371
- const edge = cloudflare.coordinates.nodes[index];
18527
+ if (cloudflarePlan.nodes.length !== coordinates.nodes.length || coordinates.nodes.some((node, index) => {
18528
+ const edge = cloudflarePlan.nodes[index];
18372
18529
  return edge?.nodeName !== node.computeReference || edge.hostname !== node.nodeHostname || edge.service !== "http://127.0.0.1:3000";
18373
18530
  })) {
18374
18531
  throw new Error("Cloudflare coordinates do not match the exact reviewed platform fleet");
@@ -18377,8 +18534,9 @@ async function runAttendedPlatformFleet(fleet, apply) {
18377
18534
  kind: "forgezero-attended-platform-bootstrap-plan",
18378
18535
  mutation: true,
18379
18536
  cloudflare: {
18380
- checkpointFile: cloudflare.checkpointPath,
18381
- nodes: cloudflare.coordinates.nodes.map(({ nodeName, hostname: hostname2 }) => ({ nodeName, hostname: hostname2 }))
18537
+ checkpointFile: cloudflarePlan.checkpointPath,
18538
+ ...cloudflarePlan.discovery ? { discovery: cloudflarePlan.discovery } : {},
18539
+ nodes: cloudflarePlan.nodes.map(({ nodeName, hostname: hostname2 }) => ({ nodeName, hostname: hostname2 }))
18382
18540
  },
18383
18541
  fleet: plan,
18384
18542
  acceptanceFile: fleet.cloudflareAcceptanceFile,
@@ -18386,7 +18544,7 @@ async function runAttendedPlatformFleet(fleet, apply) {
18386
18544
  };
18387
18545
  out.line(JSON.stringify(attendedPlan, null, 2));
18388
18546
  if (!apply) {
18389
- out.step("Review the complete Tunnel, hostname, bundle, credential and three-node plan, then repeat with --apply.");
18547
+ out.step("Review the generated plan, then apply its platform-fleet-remote.json with fz bootstrap platform fleet apply --apply.");
18390
18548
  return 0;
18391
18549
  }
18392
18550
  const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
@@ -18394,6 +18552,10 @@ async function runAttendedPlatformFleet(fleet, apply) {
18394
18552
  if (firstConfig.kind !== "platform")
18395
18553
  throw new Error("remote platform fleet requires platform configs");
18396
18554
  const secrets = await promptPlatformBootstrapSecrets(firstConfig);
18555
+ const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
18556
+ tunnelToken: secrets.cloudflareTunnelToken,
18557
+ apiToken: secrets.cloudflareApiToken
18558
+ });
18397
18559
  const result = await applyAttendedPlatformBootstrap(fleet, cloudflare, fleet.cloudflareAcceptanceFile, secrets);
18398
18560
  out.line(JSON.stringify(result, null, 2));
18399
18561
  if (!result.ok)
@@ -18492,7 +18654,9 @@ async function cmdBootstrap(options, args) {
18492
18654
  if ((mode === "host-keys" || mode === "rehearsal-host-keys") && !options.outputPath) {
18493
18655
  throw new Error(`${mode} requires --output <absolute-owner-only-evidence.json>`);
18494
18656
  }
18495
- const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath);
18657
+ const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath, {
18658
+ validateMetalConfig: mode === "apply"
18659
+ });
18496
18660
  const plan2 = planOperatorMetalBootstrap(request, mode);
18497
18661
  if (!options.apply) {
18498
18662
  out.line(JSON.stringify(plan2, null, 2));
@@ -28,6 +28,7 @@ export interface MetalBootstrapStatus {
28
28
  helperSocketReady: boolean;
29
29
  updateSocketReady: boolean;
30
30
  metalHostname?: string;
31
+ installedAgentVersion?: string;
31
32
  problems: string[];
32
33
  }
33
34
  export interface MetalBootstrapResult extends MetalBootstrapStatus {
@@ -43,10 +44,17 @@ export interface MetalBootstrapApplyOptions {
43
44
  export declare class MetalBootstrapError extends Error {
44
45
  }
45
46
  export declare function normalizeMetalPublicIdentity(value: string): string;
46
- export declare function validateMetalBootstrapConfig(config: MetalBootstrapConfig): MetalBootstrapConfig;
47
+ export interface MetalBootstrapValidationOptions {
48
+ /**
49
+ * Read identity/topology from a config that was already applied by an older
50
+ * Agent release. New installs and repairs must never set this flag.
51
+ */
52
+ allowHistoricalRelease?: boolean;
53
+ }
54
+ export declare function validateMetalBootstrapConfig(config: MetalBootstrapConfig, options?: MetalBootstrapValidationOptions): MetalBootstrapConfig;
47
55
  /** Reject symlinks and group/world access before parsing coordinates or entropy. */
48
56
  export declare function validateOwnerOnlyPath(path: string, requireRootOwner: boolean): void;
49
- export declare function readMetalBootstrapConfig(path: string): MetalBootstrapConfig;
57
+ export declare function readMetalBootstrapConfig(path: string, options?: MetalBootstrapValidationOptions): MetalBootstrapConfig;
50
58
  export declare function planMetalBootstrap(config: MetalBootstrapConfig): MetalBootstrapPlan;
51
59
  export declare function renderMetalUnits(config: MetalBootstrapConfig): Record<string, string>;
52
60
  export declare function applyMetalBootstrap(config: MetalBootstrapConfig, options: MetalBootstrapApplyOptions): Promise<MetalBootstrapResult>;
@@ -354,7 +354,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
354
354
  }
355
355
 
356
356
  // src/version.ts
357
- var VERSION = "0.1.70";
357
+ var VERSION = "0.1.72";
358
358
 
359
359
  // src/otel-collector.ts
360
360
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -579,7 +579,7 @@ var validUnit = (unit) => {
579
579
  throw new MetalBootstrapError("the OTLP collector must be independently supervised");
580
580
  }
581
581
  };
582
- function validateMetalBootstrapConfig(config) {
582
+ function validateMetalBootstrapConfig(config, options = {}) {
583
583
  if (!config || typeof config !== "object")
584
584
  throw new MetalBootstrapError("metal bootstrap config must be an object");
585
585
  exactKeys(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
@@ -623,7 +623,7 @@ function validateMetalBootstrapConfig(config) {
623
623
  for (const image of Object.values(config.profile.images))
624
624
  if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve(image.path) !== image.path || /[\0\r\n]/.test(image.path))
625
625
  throw new MetalBootstrapError("every pinned guest image must use the fixed image directory");
626
- if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
626
+ if (!options.allowHistoricalRelease && (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION)) {
627
627
  throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
628
628
  }
629
629
  let api;
@@ -669,7 +669,7 @@ function validateOwnerOnlyPath(path, requireRootOwner) {
669
669
  throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
670
670
  }
671
671
  }
672
- function readMetalBootstrapConfig(path) {
672
+ function readMetalBootstrapConfig(path, options = {}) {
673
673
  validateOwnerOnlyPath(path, false);
674
674
  const metadata = statSync(path);
675
675
  if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
@@ -680,7 +680,7 @@ function readMetalBootstrapConfig(path) {
680
680
  } catch {
681
681
  throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
682
682
  }
683
- return validateMetalBootstrapConfig(parsed);
683
+ return validateMetalBootstrapConfig(parsed, options);
684
684
  }
685
685
  function planMetalBootstrap(config) {
686
686
  validateMetalBootstrapConfig(config);
@@ -1181,6 +1181,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
1181
1181
  problems.push("metal helper socket is not ready");
1182
1182
  if (!updateSocketReady)
1183
1183
  problems.push("Agent update helper socket is not ready");
1184
+ const versionResult = await exec([AGENT_PATH, "--version"]);
1185
+ const installedAgentVersion = versionResult.exitCode === 0 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(versionResult.stdout.trim()) ? versionResult.stdout.trim() : undefined;
1186
+ if (!installedAgentVersion)
1187
+ problems.push("installed Metal Agent version is unavailable or malformed");
1184
1188
  return {
1185
1189
  initialized: existsSync(STATE_PATH),
1186
1190
  profileValid,
@@ -1190,6 +1194,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
1190
1194
  helperSocketReady,
1191
1195
  updateSocketReady,
1192
1196
  metalHostname,
1197
+ installedAgentVersion,
1193
1198
  problems
1194
1199
  };
1195
1200
  }
@@ -115,6 +115,7 @@ export interface OperatorCommandResult {
115
115
  export type OperatorCommand = (argv: readonly string[], options?: {
116
116
  stdin?: string;
117
117
  secret?: boolean;
118
+ safeStdout?: boolean;
118
119
  }) => Promise<OperatorCommandResult>;
119
120
  export interface OperatorPlatformBootstrapOptions {
120
121
  exec?: OperatorCommand;
@@ -131,7 +132,9 @@ export declare function writeOperatorPlatformBootstrapFleetRequest(path: string,
131
132
  configFile: string;
132
133
  acceptanceFile: string;
133
134
  }): string;
134
- export declare function readOperatorMetalBootstrapRequest(path: string): OperatorMetalBootstrapRequest;
135
+ export declare function readOperatorMetalBootstrapRequest(path: string, options?: {
136
+ validateMetalConfig?: boolean;
137
+ }): OperatorMetalBootstrapRequest;
135
138
  export declare function writeOperatorMetalBootstrapRequest(path: string, request: OperatorMetalBootstrapRequest): string;
136
139
  export declare function readOperatorGuestHostKeyEvidence(path: string, request?: OperatorMetalBootstrapRequest): OperatorGuestHostKeyEvidence;
137
140
  export declare function writeOperatorGuestHostKeyEvidence(path: string, evidence: OperatorGuestHostKeyEvidence, request?: OperatorMetalBootstrapRequest): string;
@@ -450,20 +450,17 @@ var privateMeshCidr = (value) => {
450
450
  }
451
451
  return `${address}/${prefix}`;
452
452
  };
453
- function validateCloudflareBootstrapCoordinates(input) {
453
+ function validateCloudflareBootstrapTopology(input) {
454
454
  if (!input || typeof input !== "object" || Array.isArray(input)) {
455
- throw new Error("Cloudflare bootstrap coordinates must be an object");
455
+ throw new Error("Cloudflare bootstrap topology must be an object");
456
456
  }
457
457
  const unsupported = Object.keys(input).filter((key) => ![
458
- "accountId",
459
- "zoneId",
460
- "kvNamespaceId",
461
458
  "meshDevicePolicyId",
462
459
  "realtime",
463
460
  "nodes"
464
461
  ].includes(key));
465
462
  if (unsupported.length)
466
- throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
463
+ throw new Error(`Cloudflare bootstrap topology contains unsupported field ${unsupported[0]}`);
467
464
  const nodeInputs = input.nodes;
468
465
  if (!Array.isArray(nodeInputs))
469
466
  throw new Error("Cloudflare bootstrap requires an explicit nodes array");
@@ -570,9 +567,6 @@ function validateCloudflareBootstrapCoordinates(input) {
570
567
  realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
571
568
  }
572
569
  return {
573
- accountId: validateId(input.accountId, "Cloudflare account id"),
574
- zoneId: validateId(input.zoneId, "Cloudflare zone id"),
575
- kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
576
570
  ...input.meshDevicePolicyId ? {
577
571
  meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
578
572
  } : {},
@@ -580,6 +574,32 @@ function validateCloudflareBootstrapCoordinates(input) {
580
574
  nodes
581
575
  };
582
576
  }
577
+ function validateCloudflareBootstrapCoordinates(input) {
578
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
579
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
580
+ }
581
+ const unsupported = Object.keys(input).filter((key) => ![
582
+ "accountId",
583
+ "zoneId",
584
+ "kvNamespaceId",
585
+ "meshDevicePolicyId",
586
+ "realtime",
587
+ "nodes"
588
+ ].includes(key));
589
+ if (unsupported.length)
590
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
591
+ const topology = validateCloudflareBootstrapTopology({
592
+ ...input.meshDevicePolicyId ? { meshDevicePolicyId: input.meshDevicePolicyId } : {},
593
+ ...input.realtime ? { realtime: input.realtime } : {},
594
+ nodes: input.nodes
595
+ });
596
+ return {
597
+ accountId: validateId(input.accountId, "Cloudflare account id"),
598
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
599
+ kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
600
+ ...topology
601
+ };
602
+ }
583
603
  function planCloudflareBootstrap(input, outputPath) {
584
604
  const coordinates = validateCloudflareBootstrapCoordinates(input);
585
605
  return {
@@ -1396,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1396
1416
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1397
1417
 
1398
1418
  // src/version.ts
1399
- var VERSION = "0.1.70";
1419
+ var VERSION = "0.1.72";
1400
1420
 
1401
1421
  // src/software.ts
1402
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -5213,7 +5233,7 @@ var validUnit = (unit) => {
5213
5233
  throw new MetalBootstrapError("the OTLP collector must be independently supervised");
5214
5234
  }
5215
5235
  };
5216
- function validateMetalBootstrapConfig(config) {
5236
+ function validateMetalBootstrapConfig(config, options = {}) {
5217
5237
  if (!config || typeof config !== "object")
5218
5238
  throw new MetalBootstrapError("metal bootstrap config must be an object");
5219
5239
  exactKeys2(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
@@ -5257,7 +5277,7 @@ function validateMetalBootstrapConfig(config) {
5257
5277
  for (const image of Object.values(config.profile.images))
5258
5278
  if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve5(image.path) !== image.path || /[\0\r\n]/.test(image.path))
5259
5279
  throw new MetalBootstrapError("every pinned guest image must use the fixed image directory");
5260
- if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
5280
+ if (!options.allowHistoricalRelease && (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION)) {
5261
5281
  throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
5262
5282
  }
5263
5283
  let api;
@@ -5303,7 +5323,7 @@ function validateOwnerOnlyPath(path, requireRootOwner) {
5303
5323
  throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
5304
5324
  }
5305
5325
  }
5306
- function readMetalBootstrapConfig(path) {
5326
+ function readMetalBootstrapConfig(path, options = {}) {
5307
5327
  validateOwnerOnlyPath(path, false);
5308
5328
  const metadata = statSync(path);
5309
5329
  if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
@@ -5314,7 +5334,7 @@ function readMetalBootstrapConfig(path) {
5314
5334
  } catch {
5315
5335
  throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
5316
5336
  }
5317
- return validateMetalBootstrapConfig(parsed);
5337
+ return validateMetalBootstrapConfig(parsed, options);
5318
5338
  }
5319
5339
  function planMetalBootstrap(config) {
5320
5340
  validateMetalBootstrapConfig(config);
@@ -5815,6 +5835,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
5815
5835
  problems.push("metal helper socket is not ready");
5816
5836
  if (!updateSocketReady)
5817
5837
  problems.push("Agent update helper socket is not ready");
5838
+ const versionResult = await exec([AGENT_PATH, "--version"]);
5839
+ const installedAgentVersion = versionResult.exitCode === 0 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(versionResult.stdout.trim()) ? versionResult.stdout.trim() : undefined;
5840
+ if (!installedAgentVersion)
5841
+ problems.push("installed Metal Agent version is unavailable or malformed");
5818
5842
  return {
5819
5843
  initialized: existsSync6(STATE_PATH2),
5820
5844
  profileValid,
@@ -5824,6 +5848,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
5824
5848
  helperSocketReady,
5825
5849
  updateSocketReady,
5826
5850
  metalHostname,
5851
+ installedAgentVersion,
5827
5852
  problems
5828
5853
  };
5829
5854
  }
@@ -6013,7 +6038,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
6013
6038
  readOperatorPlatformBootstrapRequest(file);
6014
6039
  return writeOwnerJson2(path, request, "operator fleet request");
6015
6040
  }
6016
- var validateMetalRequestValue = (value) => {
6041
+ var validateMetalRequestValue = (value, options = {}) => {
6017
6042
  const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
6018
6043
  if (root.kind !== "metal-remote")
6019
6044
  throw new Error("operator metal bootstrap kind must be metal-remote");
@@ -6033,7 +6058,8 @@ var validateMetalRequestValue = (value) => {
6033
6058
  if (typeof root.metalConfigFile !== "string")
6034
6059
  throw new Error("metalConfigFile must be an absolute path");
6035
6060
  canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
6036
- readMetalBootstrapConfig(root.metalConfigFile);
6061
+ if (options.validateMetalConfig !== false)
6062
+ readMetalBootstrapConfig(root.metalConfigFile);
6037
6063
  const genesis = exactKeys3(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
6038
6064
  const guest = (value2, label) => {
6039
6065
  const row = exactKeys3(value2, [
@@ -6092,9 +6118,9 @@ var validateMetalRequestValue = (value) => {
6092
6118
  }
6093
6119
  };
6094
6120
  };
6095
- function readOperatorMetalBootstrapRequest(path) {
6121
+ function readOperatorMetalBootstrapRequest(path, options = {}) {
6096
6122
  const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
6097
- return validateMetalRequestValue(value);
6123
+ return validateMetalRequestValue(value, options);
6098
6124
  }
6099
6125
  function writeOperatorMetalBootstrapRequest(path, request) {
6100
6126
  validateMetalRequestValue(request);
@@ -6143,7 +6169,7 @@ function writeOperatorGuestHostKeyEvidence(path, evidence, request) {
6143
6169
  return writeOwnerJson2(path, validateGuestHostKeyEvidenceValue(evidence, request), "operator guest host-key evidence");
6144
6170
  }
6145
6171
  function planOperatorMetalBootstrap(request, mode) {
6146
- const config = readMetalBootstrapConfig(request.metalConfigFile);
6172
+ const config = mode === "apply" ? readMetalBootstrapConfig(request.metalConfigFile) : undefined;
6147
6173
  const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
6148
6174
  if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
6149
6175
  throw new Error("the reviewed metal request has no rehearsal node");
@@ -6163,7 +6189,7 @@ function planOperatorMetalBootstrap(request, mode) {
6163
6189
  "remove transient local and remote staging data"
6164
6190
  ],
6165
6191
  genesisNodes,
6166
- secretInputs: config.agentSeedFile ? ["metal-agent-seed"] : []
6192
+ secretInputs: config?.agentSeedFile ? ["metal-agent-seed"] : []
6167
6193
  };
6168
6194
  }
6169
6195
  var secretSources = (config) => [
@@ -6315,7 +6341,10 @@ var defaultExec3 = async (argv, options = {}) => {
6315
6341
  new Response(child.stderr).text(),
6316
6342
  child.exited
6317
6343
  ]);
6318
- return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
6344
+ return {
6345
+ exitCode,
6346
+ output: options.secret ? options.safeStdout && exitCode === 0 ? stdout.slice(0, 65536) : "" : `${stdout}${stderr}`.slice(0, 65536)
6347
+ };
6319
6348
  };
6320
6349
  var checked5 = async (exec, argv, label, options) => {
6321
6350
  const result = await exec(argv, options);
@@ -6369,9 +6398,13 @@ var safeRemoteArg = (value) => {
6369
6398
  if (!/^[A-Za-z0-9_+.,:@%/=\/-]{1,1024}$/.test(value))
6370
6399
  throw new Error(`unsafe remote argument: ${value}`);
6371
6400
  };
6372
- var remote = async (exec, request, knownHosts, argv, label, secret = false, stdin) => {
6401
+ var remote = async (exec, request, knownHosts, argv, label, secret = false, stdin, safeStdout = false) => {
6373
6402
  argv.forEach(safeRemoteArg);
6374
- return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv], label, { secret, stdin });
6403
+ return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv], label, {
6404
+ secret,
6405
+ stdin,
6406
+ safeStdout
6407
+ });
6375
6408
  };
6376
6409
  var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
6377
6410
  safeRemoteArg(path);
@@ -6639,7 +6672,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
6639
6672
  const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
6640
6673
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
6641
6674
  const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
6642
- ` : undefined);
6675
+ ` : undefined, mode === "apply");
6643
6676
  return { plan, output };
6644
6677
  } finally {
6645
6678
  if (knownHosts) {
@@ -2125,7 +2125,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2125
2125
  }
2126
2126
 
2127
2127
  // src/version.ts
2128
- var VERSION3 = "0.1.70";
2128
+ var VERSION3 = "0.1.72";
2129
2129
 
2130
2130
  // src/egress-policy.ts
2131
2131
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/provision.js CHANGED
@@ -2125,7 +2125,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2125
2125
  }
2126
2126
 
2127
2127
  // src/version.ts
2128
- var VERSION3 = "0.1.70";
2128
+ var VERSION3 = "0.1.72";
2129
2129
 
2130
2130
  // src/egress-policy.ts
2131
2131
  import { realpathSync as realpathSync3 } from "node:fs";
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.70";
2
+ export declare const VERSION = "0.1.72";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",