@forgezero/agent 0.1.69 → 0.1.71

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,
@@ -749,7 +749,7 @@ async function postSignedNode(options, path, body) {
749
749
  }
750
750
 
751
751
  // src/version.ts
752
- var VERSION3 = "0.1.69";
752
+ var VERSION3 = "0.1.71";
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.69";
1419
+ var VERSION = "0.1.71";
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.69";
8591
+ var VERSION2 = "0.1.71";
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.69";
4832
+ var VERSION2 = "0.1.71";
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)
@@ -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) {
@@ -18195,24 +18351,23 @@ function writePlatformGenesisFleet(directory) {
18195
18351
  cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18196
18352
  });
18197
18353
  const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
18198
- const cloudflareConfig = writeBootstrapConfig(join12(output, "cloudflare.json"), createCloudflareBootstrapCommandConfig({
18354
+ const cloudflareConfig = writeBootstrapConfig(join12(output, "cloudflare.json"), createCloudflareBootstrapDiscoveryCommandConfig({
18199
18355
  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: {
18356
+ discovery: {
18357
+ zoneName: bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
18358
+ kvNamespaceTitle: bootstrapAnswer("Existing Worker-bound KV namespace title"),
18359
+ ...realtimeEnabled ? {
18205
18360
  workerScriptName: bootstrapAnswer("Existing Worker script name"),
18206
18361
  endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
18207
18362
  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
- }
18363
+ } : {}
18364
+ },
18365
+ nodes: fleet.map((guest, index) => ({
18366
+ nodeName: guest.name,
18367
+ hostname: nodes[index].nodeHostname,
18368
+ service: "http://127.0.0.1:3000",
18369
+ tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
18370
+ }))
18216
18371
  }));
18217
18372
  const cloudflareAcceptance = join12(output, "cloudflare-acceptance.json");
18218
18373
  const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
@@ -18363,6 +18518,50 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
18363
18518
  } : {}
18364
18519
  };
18365
18520
  }
18521
+ async function runAttendedPlatformFleet(fleet, apply) {
18522
+ const plan = planOperatorPlatformFleetBootstrap(fleet, "apply");
18523
+ const cloudflarePlan = readCloudflareBootstrapCommandPlan(fleet.cloudflareConfigFile);
18524
+ const coordinates = operatorPlatformFleetCoordinates(fleet);
18525
+ if (cloudflarePlan.nodes.length !== coordinates.nodes.length || coordinates.nodes.some((node, index) => {
18526
+ const edge = cloudflarePlan.nodes[index];
18527
+ return edge?.nodeName !== node.computeReference || edge.hostname !== node.nodeHostname || edge.service !== "http://127.0.0.1:3000";
18528
+ })) {
18529
+ throw new Error("Cloudflare coordinates do not match the exact reviewed platform fleet");
18530
+ }
18531
+ const attendedPlan = {
18532
+ kind: "forgezero-attended-platform-bootstrap-plan",
18533
+ mutation: true,
18534
+ cloudflare: {
18535
+ checkpointFile: cloudflarePlan.checkpointPath,
18536
+ ...cloudflarePlan.discovery ? { discovery: cloudflarePlan.discovery } : {},
18537
+ nodes: cloudflarePlan.nodes.map(({ nodeName, hostname: hostname2 }) => ({ nodeName, hostname: hostname2 }))
18538
+ },
18539
+ fleet: plan,
18540
+ acceptanceFile: fleet.cloudflareAcceptanceFile,
18541
+ completion: ["public ingress verified", "initial custodian invitation printed"]
18542
+ };
18543
+ out.line(JSON.stringify(attendedPlan, null, 2));
18544
+ if (!apply) {
18545
+ out.step("Review the generated plan, then apply its platform-fleet-remote.json with fz bootstrap platform fleet apply --apply.");
18546
+ return 0;
18547
+ }
18548
+ const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
18549
+ const firstConfig = readBootstrapConfig(first.platformConfigFile);
18550
+ if (firstConfig.kind !== "platform")
18551
+ throw new Error("remote platform fleet requires platform configs");
18552
+ const secrets = await promptPlatformBootstrapSecrets(firstConfig);
18553
+ const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
18554
+ tunnelToken: secrets.cloudflareTunnelToken,
18555
+ apiToken: secrets.cloudflareApiToken
18556
+ });
18557
+ const result = await applyAttendedPlatformBootstrap(fleet, cloudflare, fleet.cloudflareAcceptanceFile, secrets);
18558
+ out.line(JSON.stringify(result, null, 2));
18559
+ if (!result.ok)
18560
+ return 1;
18561
+ out.line();
18562
+ out.step(`Initial custodian invitation: ${result.inviteUrl}`);
18563
+ return 0;
18564
+ }
18366
18565
  async function cmdBootstrap(options, args) {
18367
18566
  try {
18368
18567
  const operation = args[0] ?? "status";
@@ -18384,6 +18583,17 @@ async function cmdBootstrap(options, args) {
18384
18583
  out.line(JSON.stringify({ kind, ...writeMetalOperatorFiles(options.outputPath), mode: "0600" }, null, 2));
18385
18584
  return 0;
18386
18585
  }
18586
+ if (operation === "platform" && args[1] === "launch") {
18587
+ if (args[2] !== undefined || !options.outputPath || options.bootstrapConfigPath) {
18588
+ throw new Error("Usage: fz bootstrap platform launch --output <absolute-owner-only-directory> [--apply]");
18589
+ }
18590
+ const generated = writePlatformGenesisFleet(options.outputPath);
18591
+ const fleet = readOperatorPlatformBootstrapFleetRequest(generated.fleetRequest);
18592
+ if (options.apply && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
18593
+ throw new Error("attended platform bootstrap was not confirmed");
18594
+ }
18595
+ return await runAttendedPlatformFleet(fleet, options.apply);
18596
+ }
18387
18597
  if (operation === "platform" && args[1] === "fleet") {
18388
18598
  const mode = args[2];
18389
18599
  if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
@@ -18392,50 +18602,12 @@ async function cmdBootstrap(options, args) {
18392
18602
  if (!options.bootstrapConfigPath)
18393
18603
  throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
18394
18604
  const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
18395
- const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
18396
- const cloudflare = readCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, options.apply && mode === "apply" ? "apply" : "plan");
18397
- const coordinates = operatorPlatformFleetCoordinates(fleet);
18398
- if (cloudflare.coordinates.nodes.length !== coordinates.nodes.length || coordinates.nodes.some((node, index) => {
18399
- const edge = cloudflare.coordinates.nodes[index];
18400
- return edge?.nodeName !== node.computeReference || edge.hostname !== node.nodeHostname || edge.service !== "http://127.0.0.1:3000";
18401
- })) {
18402
- throw new Error("Cloudflare coordinates do not match the exact reviewed platform fleet");
18403
- }
18404
- if (!options.apply) {
18405
- out.line(JSON.stringify({
18406
- kind: "forgezero-attended-platform-bootstrap-plan",
18407
- mutation: true,
18408
- cloudflare: {
18409
- checkpointFile: cloudflare.checkpointPath,
18410
- nodes: cloudflare.coordinates.nodes.map(({ nodeName, hostname: hostname2 }) => ({ nodeName, hostname: hostname2 }))
18411
- },
18412
- fleet: plan2,
18413
- acceptanceFile: fleet.cloudflareAcceptanceFile,
18414
- completion: ["public ingress verified", "initial custodian invitation printed"]
18415
- }, null, 2));
18416
- out.step("Review the complete Tunnel, hostname, bundle, credential and three-node plan, then repeat with --apply.");
18417
- return 0;
18418
- }
18419
- const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
18420
- const firstConfig = readBootstrapConfig(first.platformConfigFile);
18421
- if (firstConfig.kind !== "platform")
18422
- throw new Error("remote platform fleet requires platform configs");
18423
18605
  if (mode === "status") {
18424
- const result3 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
18425
- out.line(JSON.stringify(result3, null, 2));
18426
- return result3.ok ? 0 : 1;
18606
+ const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
18607
+ out.line(JSON.stringify(result2, null, 2));
18608
+ return result2.ok ? 0 : 1;
18427
18609
  }
18428
- const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
18429
- if (!secrets2?.cloudflareTunnelToken || !secrets2.cloudflareApiToken) {
18430
- throw new Error("attended platform bootstrap requires both Cloudflare tokens");
18431
- }
18432
- const result2 = await applyAttendedPlatformBootstrap(fleet, cloudflare, fleet.cloudflareAcceptanceFile, secrets2);
18433
- out.line(JSON.stringify(result2, null, 2));
18434
- if (!result2.ok)
18435
- return 1;
18436
- out.line();
18437
- out.step(`Initial custodian invitation: ${result2.inviteUrl}`);
18438
- return 0;
18610
+ return await runAttendedPlatformFleet(fleet, options.apply);
18439
18611
  }
18440
18612
  if (operation === "platform" && args[1] === "remote") {
18441
18613
  const mode = args[2];
@@ -18480,7 +18652,9 @@ async function cmdBootstrap(options, args) {
18480
18652
  if ((mode === "host-keys" || mode === "rehearsal-host-keys") && !options.outputPath) {
18481
18653
  throw new Error(`${mode} requires --output <absolute-owner-only-evidence.json>`);
18482
18654
  }
18483
- const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath);
18655
+ const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath, {
18656
+ validateMetalConfig: mode === "apply"
18657
+ });
18484
18658
  const plan2 = planOperatorMetalBootstrap(request, mode);
18485
18659
  if (!options.apply) {
18486
18660
  out.line(JSON.stringify(plan2, null, 2));
@@ -18583,7 +18757,7 @@ async function cmdBootstrap(options, args) {
18583
18757
  return 0;
18584
18758
  }
18585
18759
  if (!["platform", "repair"].includes(operation)) {
18586
- throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
18760
+ 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|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
18587
18761
  }
18588
18762
  const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
18589
18763
  throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
@@ -19158,6 +19332,9 @@ function usage() {
19158
19332
  local socket instead of holding an API key
19159
19333
  fz bootstrap platform bundle
19160
19334
  Build one clean exact-revision API Git bundle for genesis
19335
+ fz bootstrap platform launch
19336
+ Prompt, review and run the complete attended platform
19337
+ bootstrap; --output is the owner-only working directory
19161
19338
  fz bootstrap platform remote <apply|status>
19162
19339
  Run typed bootstrap from the operator laptop through
19163
19340
  a pinned host; apply copies and verifies that bundle
@@ -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 {
@@ -354,7 +354,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
354
354
  }
355
355
 
356
356
  // src/version.ts
357
- var VERSION = "0.1.69";
357
+ var VERSION = "0.1.71";
358
358
 
359
359
  // src/otel-collector.ts
360
360
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -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.69";
1419
+ var VERSION = "0.1.71";
1400
1420
 
1401
1421
  // src/software.ts
1402
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -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.69";
2128
+ var VERSION3 = "0.1.71";
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.69";
2128
+ var VERSION3 = "0.1.71";
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.69";
2
+ export declare const VERSION = "0.1.71";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",