@forgezero/agent 0.1.55 → 0.1.57

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.
@@ -749,7 +749,7 @@ async function postSignedNode(options, path, body) {
749
749
  }
750
750
 
751
751
  // src/version.ts
752
- var VERSION3 = "0.1.55";
752
+ var VERSION3 = "0.1.57";
753
753
 
754
754
  // src/agent-heartbeat.ts
755
755
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
package/dist/bootstrap.js CHANGED
@@ -1207,7 +1207,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1207
1207
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1208
1208
 
1209
1209
  // src/version.ts
1210
- var VERSION = "0.1.55";
1210
+ var VERSION = "0.1.57";
1211
1211
 
1212
1212
  // src/software.ts
1213
1213
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1,4 +1,11 @@
1
- import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
1
+ import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokenFiles } from '../cloudflare-bootstrap';
2
+ export interface CloudflareBootstrapCommandConfig {
3
+ format: 1;
4
+ kind: 'forgezero-cloudflare-bootstrap-request';
5
+ checkpointPath: string;
6
+ coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
7
+ tokenFiles: CloudflareBootstrapTokenFiles;
8
+ }
2
9
  export interface CloudflareBootstrapCommandDependencies {
3
10
  run?: CloudflareBootstrapPhaseRunner;
4
11
  write?: (text: string) => void;
@@ -9,6 +16,15 @@ export interface CloudflareBootstrapFinalizeConfig {
9
16
  checkpointPath: string;
10
17
  acceptancePath: string;
11
18
  }
19
+ /**
20
+ * Construct the exact reviewed JSON shape used by both the interactive CLI and
21
+ * the strict file reader. Token values are deliberately not accepted here.
22
+ */
23
+ export declare function createCloudflareBootstrapCommandConfig(input: {
24
+ checkpointPath: string;
25
+ coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
26
+ tokenFiles: CloudflareBootstrapTokenFiles;
27
+ }): CloudflareBootstrapCommandConfig;
12
28
  /**
13
29
  * Read reviewed non-secret coordinates and token-file paths for the attended
14
30
  * operator phase. Raw token values and a persisted mode are not schema fields.
@@ -2,6 +2,7 @@ import { type SoftwareExec } from './software';
2
2
  export declare const COMMUNITY_REHEARSAL_VERSION = "3.11.14";
3
3
  export declare const COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
4
4
  export declare const COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
5
+ export declare const COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
5
6
  export declare const COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
6
7
  export type CommunityRehearsalApiOperation = 'health' | 'init' | 'write' | 'read' | 'query' | 'cluster';
7
8
  export type CommunityRehearsalHostRequest = {
@@ -13,6 +14,9 @@ export type CommunityRehearsalHostRequest = {
13
14
  archivePath: string;
14
15
  archiveSha256: string;
15
16
  jwt: string;
17
+ } | {
18
+ action: 'cleanup';
19
+ node: string;
16
20
  } | {
17
21
  action: 'database-enable' | 'api-enable' | 'database-ready' | 'starter-ready' | 'api-ready' | 'database-status' | 'api-status';
18
22
  node: string;
@@ -25,7 +29,7 @@ export type CommunityRehearsalHostRequest = {
25
29
  };
26
30
  export type CommunityHostOperation = {
27
31
  kind: 'remove-tree';
28
- path: typeof COMMUNITY_DATABASE_ROOT;
32
+ path: typeof COMMUNITY_DATABASE_ROOT | typeof COMMUNITY_REHEARSAL_ROOT | typeof COMMUNITY_CREDENTIAL_PARENT;
29
33
  } | {
30
34
  kind: 'write';
31
35
  path: string;
@@ -49,5 +53,7 @@ export declare function parseCommunityRehearsalHostRequest(value: unknown): Comm
49
53
  export declare function planCommunityRehearsalPrepare(request: Extract<CommunityRehearsalHostRequest, {
50
54
  action: 'prepare';
51
55
  }>): CommunityHostOperation[];
56
+ /** Remove only the dedicated rehearsal units, credentials, release and data. */
57
+ export declare function planCommunityRehearsalCleanup(): CommunityHostOperation[];
52
58
  /** Execute only the dedicated four-node rehearsal operation selected by a typed request. */
53
59
  export declare function runCommunityRehearsalHost(request: CommunityRehearsalHostRequest): Promise<Record<string, unknown>>;
@@ -285,6 +285,7 @@ import { dirname } from "node:path";
285
285
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
286
286
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
287
287
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
288
+ var COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
288
289
  var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
289
290
  var NODES = new Map([
290
291
  ["dev-fz-n1", { address: "10.42.0.21", agency: true }],
@@ -326,6 +327,12 @@ function parseCommunityRehearsalHostRequest(value) {
326
327
  }
327
328
  return row;
328
329
  }
330
+ if (action === "cleanup") {
331
+ if (Object.keys(row).some((key) => !["action", "node"].includes(key))) {
332
+ throw new Error("community rehearsal cleanup has unknown fields");
333
+ }
334
+ return { action, node: node.name };
335
+ }
329
336
  if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
330
337
  if (Object.keys(row).some((key) => !["action", "node"].includes(key)))
331
338
  throw new Error("community rehearsal host action has unknown fields");
@@ -441,6 +448,19 @@ function planCommunityRehearsalPrepare(request) {
441
448
  { kind: "unlink", path: request.archivePath }
442
449
  ];
443
450
  }
451
+ function planCommunityRehearsalCleanup() {
452
+ return [
453
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-community-api.service"], accepted: [0, 1, 5] },
454
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] },
455
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-community-api.service" },
456
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-rehearsal-db.service" },
457
+ { kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
458
+ { kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
459
+ { kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
460
+ { kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
461
+ { kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
462
+ ];
463
+ }
444
464
  async function exec(argv, stdin) {
445
465
  const child = Bun.spawn([...argv], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
446
466
  if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -503,6 +523,10 @@ async function runCommunityRehearsalHost(request) {
503
523
  if ((process.getuid?.() ?? -1) !== 0)
504
524
  throw new Error("community-rehearsal must run as root");
505
525
  const node = exactNode(request.node);
526
+ if (request.action === "cleanup") {
527
+ await applyOperations(planCommunityRehearsalCleanup());
528
+ return { ok: true, action: request.action, node: node.name };
529
+ }
506
530
  if (request.action === "prepare") {
507
531
  const metadata = lstatSync(request.archivePath);
508
532
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash2("sha256").update(readFileSync2(request.archivePath)).digest("hex") !== request.archiveSha256) {
@@ -554,10 +578,12 @@ async function runCommunityRehearsalHost(request) {
554
578
  export {
555
579
  runCommunityRehearsalHost,
556
580
  planCommunityRehearsalPrepare,
581
+ planCommunityRehearsalCleanup,
557
582
  parseCommunityRehearsalHostRequest,
558
583
  ensureCommunityRehearsalArango,
559
584
  COMMUNITY_REHEARSAL_VERSION,
560
585
  COMMUNITY_REHEARSAL_ROOT,
561
586
  COMMUNITY_DATABASE_ROOT,
562
- COMMUNITY_CREDENTIAL_ROOT
587
+ COMMUNITY_CREDENTIAL_ROOT,
588
+ COMMUNITY_CREDENTIAL_PARENT
563
589
  };
package/dist/fz-agent.js CHANGED
@@ -8448,7 +8448,7 @@ function assertSupportedGuestImage(imageKey) {
8448
8448
  }
8449
8449
 
8450
8450
  // src/version.ts
8451
- var VERSION2 = "0.1.55";
8451
+ var VERSION2 = "0.1.57";
8452
8452
 
8453
8453
  // src/ssh-bootstrap.ts
8454
8454
  class SshBootstrapError extends Error {
@@ -15003,6 +15003,7 @@ import { dirname as dirname11 } from "path";
15003
15003
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
15004
15004
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
15005
15005
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
15006
+ var COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
15006
15007
  var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
15007
15008
  var NODES = new Map([
15008
15009
  ["dev-fz-n1", { address: "10.42.0.21", agency: true }],
@@ -15044,6 +15045,12 @@ function parseCommunityRehearsalHostRequest(value) {
15044
15045
  }
15045
15046
  return row2;
15046
15047
  }
15048
+ if (action === "cleanup") {
15049
+ if (Object.keys(row2).some((key) => !["action", "node"].includes(key))) {
15050
+ throw new Error("community rehearsal cleanup has unknown fields");
15051
+ }
15052
+ return { action, node: node.name };
15053
+ }
15047
15054
  if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
15048
15055
  if (Object.keys(row2).some((key) => !["action", "node"].includes(key)))
15049
15056
  throw new Error("community rehearsal host action has unknown fields");
@@ -15159,6 +15166,19 @@ function planCommunityRehearsalPrepare(request) {
15159
15166
  { kind: "unlink", path: request.archivePath }
15160
15167
  ];
15161
15168
  }
15169
+ function planCommunityRehearsalCleanup() {
15170
+ return [
15171
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-community-api.service"], accepted: [0, 1, 5] },
15172
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] },
15173
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-community-api.service" },
15174
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-rehearsal-db.service" },
15175
+ { kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
15176
+ { kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
15177
+ { kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
15178
+ { kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
15179
+ { kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
15180
+ ];
15181
+ }
15162
15182
  async function exec(argv2, stdin) {
15163
15183
  const child = Bun.spawn([...argv2], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
15164
15184
  if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -15221,6 +15241,10 @@ async function runCommunityRehearsalHost(request) {
15221
15241
  if ((process.getuid?.() ?? -1) !== 0)
15222
15242
  throw new Error("community-rehearsal must run as root");
15223
15243
  const node = exactNode(request.node);
15244
+ if (request.action === "cleanup") {
15245
+ await applyOperations(planCommunityRehearsalCleanup());
15246
+ return { ok: true, action: request.action, node: node.name };
15247
+ }
15224
15248
  if (request.action === "prepare") {
15225
15249
  const metadata = lstatSync6(request.archivePath);
15226
15250
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash10("sha256").update(readFileSync17(request.archivePath)).digest("hex") !== request.archiveSha256) {
package/dist/fz.js CHANGED
@@ -4830,7 +4830,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4830
4830
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4831
4831
 
4832
4832
  // src/version.ts
4833
- var VERSION2 = "0.1.55";
4833
+ var VERSION2 = "0.1.57";
4834
4834
 
4835
4835
  // src/software.ts
4836
4836
  var PINNED_BUN_VERSION = "1.3.14";
@@ -14347,6 +14347,26 @@ var record2 = (value, label) => {
14347
14347
  throw new Error(`${label} must be an object`);
14348
14348
  return value;
14349
14349
  };
14350
+ function createCloudflareBootstrapCommandConfig(input) {
14351
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
14352
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
14353
+ }
14354
+ for (const key of ["tunnelTokenFile", "apiTokenFile"]) {
14355
+ if (typeof input.tokenFiles?.[key] !== "string" || !input.tokenFiles[key].trim()) {
14356
+ throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
14357
+ }
14358
+ }
14359
+ return {
14360
+ format: 1,
14361
+ kind: "forgezero-cloudflare-bootstrap-request",
14362
+ checkpointPath: input.checkpointPath.trim(),
14363
+ coordinates: validateCloudflareBootstrapCoordinates(input.coordinates),
14364
+ tokenFiles: {
14365
+ tunnelTokenFile: input.tokenFiles.tunnelTokenFile.trim(),
14366
+ apiTokenFile: input.tokenFiles.apiTokenFile.trim()
14367
+ }
14368
+ };
14369
+ }
14350
14370
  function readOwnerConfig(path) {
14351
14371
  const absolute2 = resolve6(path);
14352
14372
  let descriptor;
@@ -15413,6 +15433,12 @@ var PLATFORM_GENESIS_FLEETS = {
15413
15433
  ...developmentShape
15414
15434
  }))
15415
15435
  };
15436
+ var DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST = {
15437
+ name: "dev-fz-n4",
15438
+ address: "10.42.0.24",
15439
+ cpuPoolKey: "node7",
15440
+ ...developmentShape
15441
+ };
15416
15442
  var publicKey = (value) => {
15417
15443
  const normalized = value.trim();
15418
15444
  if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(normalized)) {
@@ -15451,6 +15477,36 @@ function platformGenesisClaims(environment, sshPublicKeys) {
15451
15477
  }
15452
15478
  }));
15453
15479
  }
15480
+ function platformCommunityRehearsalClaims(sshPublicKeys) {
15481
+ const keys = [...new Set(sshPublicKeys.map(publicKey))];
15482
+ if (keys.length === 0)
15483
+ throw new Error("platform genesis requires at least one operator SSH public key");
15484
+ const seed = platformGenesisClaims("development", keys);
15485
+ const guest = DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST;
15486
+ return [...seed, {
15487
+ computeKey: `platform:${guest.name}`,
15488
+ claimToken: "offline-platform-genesis",
15489
+ claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
15490
+ attempt: 1,
15491
+ action: "create",
15492
+ bootstrap: { kind: "platform-genesis" },
15493
+ access: { sshUser: "forgezero", sshPublicKeys: keys },
15494
+ spec: {
15495
+ reference: `platform:${guest.name}`,
15496
+ imageKey: SUPPORTED_GUEST_IMAGE.key,
15497
+ guestName: guest.name,
15498
+ guestAddress: guest.address,
15499
+ cpuPoolKey: guest.cpuPoolKey,
15500
+ physicalCores: guest.physicalCores,
15501
+ vcpu: guest.vcpu,
15502
+ memoryGib: guest.memoryGib,
15503
+ diskGib: guest.diskGib,
15504
+ egressGuaranteedMbps: guest.egressGuaranteedMbps,
15505
+ egressBurstMbps: guest.egressBurstMbps,
15506
+ confidential: true
15507
+ }
15508
+ }];
15509
+ }
15454
15510
 
15455
15511
  // src/operator-bootstrap.ts
15456
15512
  var REQUEST_LIMIT = 64 * 1024;
@@ -15604,13 +15660,17 @@ function readOperatorMetalBootstrapRequest(path) {
15604
15660
  }
15605
15661
  function planOperatorMetalBootstrap(request, mode) {
15606
15662
  const config = readMetalBootstrapConfig(request.metalConfigFile);
15607
- const claims = platformGenesisClaims(request.genesis.environment, request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path)));
15663
+ if (mode === "rehearsal" && request.genesis.environment !== "development") {
15664
+ throw new Error("the disposable four-node Community rehearsal is development-only");
15665
+ }
15666
+ const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
15667
+ const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
15608
15668
  return {
15609
15669
  kind: "metal-remote",
15610
15670
  mode,
15611
15671
  mutation: mode !== "status",
15612
15672
  target: { address: request.target.address, port: request.target.port, user: request.target.user },
15613
- steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" ? ["verify pinned SSH transport", "send three fixed platform-genesis claims to the constrained Metal helper"] : [
15673
+ steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the fixed three-node seed plus ordinary n4 Community rehearsal claim to the constrained Metal helper" : "send three fixed platform-genesis claims to the constrained Metal helper"] : [
15614
15674
  "verify pinned SSH transport and caller-approved SSH agent",
15615
15675
  "copy pinned Bun and packaged fz artifacts to private staging",
15616
15676
  "stage the strict owner-only metal config and optional seed handoff",
@@ -15905,10 +15965,11 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
15905
15965
  const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
15906
15966
  return { plan, output: output2 };
15907
15967
  }
15908
- if (mode === "genesis") {
15968
+ if (mode === "genesis" || mode === "rehearsal") {
15909
15969
  const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
15910
15970
  const outputs = [];
15911
- for (const claim of platformGenesisClaims(request.genesis.environment, keys)) {
15971
+ const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
15972
+ for (const claim of claims) {
15912
15973
  outputs.push(await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz-agent", "metal-apply", "--claim=-"], `provision ${claim.spec.guestName}`, false, `${JSON.stringify(claim)}
15913
15974
  `));
15914
15975
  }
@@ -17077,6 +17138,55 @@ function writeBootstrapConfig(path, config) {
17077
17138
  `, { mode: 384, flag: "wx" });
17078
17139
  return path;
17079
17140
  }
17141
+ function interactiveCloudflareBootstrap() {
17142
+ if (!process.stdin.isTTY)
17143
+ throw new Error("interactive Cloudflare config generation requires a terminal");
17144
+ const nodeCount = bootstrapNumber("Number of public API nodes", "3");
17145
+ if (nodeCount < 1 || nodeCount > 32)
17146
+ throw new Error("Number of public API nodes must be between 1 and 32");
17147
+ const realtimeEnabled = bootstrapAnswer("Configure existing Worker realtime fan-out? (yes/no)", "yes") === "yes";
17148
+ const meshEnabled = bootstrapAnswer("Configure private Mesh/WARP routes? (yes/no)", "no") === "yes";
17149
+ const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
17150
+ const nodes = Array.from({ length: nodeCount }, (_, index) => {
17151
+ const ordinal = index + 1;
17152
+ const nodeName = bootstrapAnswer(`Node ${ordinal} inventory name`);
17153
+ const nodeMesh = meshEnabled && bootstrapAnswer(`Configure Mesh routes for ${nodeName}? (yes/no)`, "yes") === "yes";
17154
+ return {
17155
+ nodeName,
17156
+ hostname: bootstrapAnswer(`${nodeName} public hostname`),
17157
+ service: bootstrapAnswer(`${nodeName} loopback API service`, "http://127.0.0.1:3000"),
17158
+ tunnelName: bootstrapAnswer(`${nodeName} Tunnel name`, nodeName),
17159
+ ...nodeMesh ? {
17160
+ mesh: {
17161
+ connectorName: bootstrapAnswer(`${nodeName} Mesh connector name`, `${nodeName}-mesh`),
17162
+ routes: bootstrapList(`${nodeName} private CIDR routes`),
17163
+ highAvailability: bootstrapAnswer(`${nodeName} shares a highly available Mesh connector? (yes/no)`, "no") === "yes"
17164
+ }
17165
+ } : {}
17166
+ };
17167
+ });
17168
+ return createCloudflareBootstrapCommandConfig({
17169
+ checkpointPath: bootstrapAnswer("Owner-only resumable checkpoint path", "./cloudflare-handoff.json"),
17170
+ coordinates: {
17171
+ accountId: bootstrapAnswer("Cloudflare account id"),
17172
+ zoneId: bootstrapAnswer("Cloudflare zone id"),
17173
+ kvNamespaceId: bootstrapAnswer("Existing Worker-bound KV namespace id"),
17174
+ ...meshDevicePolicyId ? { meshDevicePolicyId } : {},
17175
+ ...realtimeEnabled ? {
17176
+ realtime: {
17177
+ workerScriptName: bootstrapAnswer("Existing Worker script name"),
17178
+ endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
17179
+ producer: bootstrapAnswer("Realtime producer identity", "platform-api")
17180
+ }
17181
+ } : {},
17182
+ nodes
17183
+ },
17184
+ tokenFiles: {
17185
+ tunnelTokenFile: bootstrapAnswer("Owner-only CF_TUNNEL_TOKEN file", "./CF_TUNNEL_TOKEN"),
17186
+ apiTokenFile: bootstrapAnswer("Owner-only CF_API_TOKEN file", "./CF_API_TOKEN")
17187
+ }
17188
+ });
17189
+ }
17080
17190
  function genesisOutputDirectory(path) {
17081
17191
  if (!isAbsolute5(path) || resolve9(path) !== path)
17082
17192
  throw new Error("--output must be a canonical absolute directory");
@@ -17249,13 +17359,19 @@ async function cmdBootstrap(options, args) {
17249
17359
  const operation = args[0] ?? "status";
17250
17360
  if (operation === "config") {
17251
17361
  const kind = args[1];
17252
- if (kind !== "metal" && kind !== "platform" || args[2] !== undefined || !options.outputPath) {
17253
- throw new Error("Usage: fz bootstrap config metal --output <absolute-file> | fz bootstrap config platform --output <absolute-owner-only-directory>");
17362
+ if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
17363
+ throw new Error("Usage: fz bootstrap config metal|cloudflare --output <absolute-file> | fz bootstrap config platform --output <absolute-owner-only-directory>");
17254
17364
  }
17255
17365
  if (kind === "platform") {
17256
17366
  out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
17257
17367
  return 0;
17258
17368
  }
17369
+ if (kind === "cloudflare") {
17370
+ const path = writeBootstrapConfig(options.outputPath, interactiveCloudflareBootstrap());
17371
+ readCloudflareBootstrapCommandConfig(path, "plan");
17372
+ out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
17373
+ return 0;
17374
+ }
17259
17375
  const config2 = interactiveMetalBootstrap();
17260
17376
  out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
17261
17377
  return 0;
@@ -17279,8 +17395,8 @@ async function cmdBootstrap(options, args) {
17279
17395
  }
17280
17396
  if (operation === "metal" && args[1] === "remote") {
17281
17397
  const mode = args[2];
17282
- if (!mode || !["apply", "genesis", "status"].includes(mode) || args[3] !== undefined) {
17283
- throw new Error("Usage: fz bootstrap metal remote <apply|genesis|status> --bootstrap-config <owner-only-request.json> [--apply]");
17398
+ if (!mode || !["apply", "genesis", "rehearsal", "status"].includes(mode) || args[3] !== undefined) {
17399
+ throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|status> --bootstrap-config <owner-only-request.json> [--apply]");
17284
17400
  }
17285
17401
  if (!options.bootstrapConfigPath)
17286
17402
  throw new Error("remote metal bootstrap requires --bootstrap-config");
@@ -17391,7 +17507,7 @@ async function cmdBootstrap(options, args) {
17391
17507
  return 0;
17392
17508
  }
17393
17509
  if (!["platform", "repair"].includes(operation)) {
17394
- throw new Error("Usage: fz bootstrap config <metal|platform>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|status>]|status|repair [--bootstrap-config <path>] [--apply]");
17510
+ throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|status>]|status|repair [--bootstrap-config <path>] [--apply]");
17395
17511
  }
17396
17512
  const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
17397
17513
  throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
@@ -17956,9 +18072,9 @@ function usage() {
17956
18072
  Run typed bootstrap from the operator laptop through
17957
18073
  a pinned host and caller-approved SSH agent
17958
18074
  fz bootstrap platform Install/repair a typed elastic platform compute
17959
- fz bootstrap config <metal|platform>
17960
- Write a validated owner-only metal file or strict
17961
- three-node platform genesis directory; never apply locally
18075
+ fz bootstrap config <metal|platform|cloudflare>
18076
+ Write a validated owner-only metal/Cloudflare file or
18077
+ strict three-node platform genesis directory; never apply locally
17962
18078
  fz bootstrap platform cloudflare
17963
18079
  Plan/apply attended Tunnel and DNS reconciliation
17964
18080
  using supplied management and KV/Worker runtime token files
@@ -17969,7 +18085,7 @@ function usage() {
17969
18085
  Persist secret-free public-node acceptance, then remove
17970
18086
  the capability-bearing checkpoint and node handoffs
17971
18087
  fz bootstrap metal Plan/install an identity-only physical provisioner
17972
- fz bootstrap metal remote <apply|genesis|status>
18088
+ fz bootstrap metal remote <apply|genesis|rehearsal|status>
17973
18089
  Install/bootstrap one pinned blank metal target, then
17974
18090
  provision the exact selected three-node genesis seed
17975
18091
  fz bootstrap status Verify persisted profile and supervised units
@@ -292,7 +292,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
292
292
  }
293
293
 
294
294
  // src/version.ts
295
- var VERSION = "0.1.55";
295
+ var VERSION = "0.1.57";
296
296
 
297
297
  // src/otel-collector.ts
298
298
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -25,7 +25,7 @@ export interface OperatorMetalBootstrapRequest {
25
25
  };
26
26
  }
27
27
  export type OperatorPlatformBootstrapMode = 'prepare' | 'apply' | 'status';
28
- export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'status';
28
+ export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'rehearsal' | 'status';
29
29
  export interface OperatorPlatformBootstrapPlan {
30
30
  kind: 'platform-remote';
31
31
  mode: OperatorPlatformBootstrapMode;
@@ -1207,7 +1207,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1207
1207
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1208
1208
 
1209
1209
  // src/version.ts
1210
- var VERSION = "0.1.55";
1210
+ var VERSION = "0.1.57";
1211
1211
 
1212
1212
  // src/software.ts
1213
1213
  var PINNED_BUN_VERSION = "1.3.14";
@@ -5273,6 +5273,12 @@ var PLATFORM_GENESIS_FLEETS = {
5273
5273
  ...developmentShape
5274
5274
  }))
5275
5275
  };
5276
+ var DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST = {
5277
+ name: "dev-fz-n4",
5278
+ address: "10.42.0.24",
5279
+ cpuPoolKey: "node7",
5280
+ ...developmentShape
5281
+ };
5276
5282
  var publicKey = (value) => {
5277
5283
  const normalized = value.trim();
5278
5284
  if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(normalized)) {
@@ -5311,6 +5317,36 @@ function platformGenesisClaims(environment, sshPublicKeys) {
5311
5317
  }
5312
5318
  }));
5313
5319
  }
5320
+ function platformCommunityRehearsalClaims(sshPublicKeys) {
5321
+ const keys = [...new Set(sshPublicKeys.map(publicKey))];
5322
+ if (keys.length === 0)
5323
+ throw new Error("platform genesis requires at least one operator SSH public key");
5324
+ const seed = platformGenesisClaims("development", keys);
5325
+ const guest = DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST;
5326
+ return [...seed, {
5327
+ computeKey: `platform:${guest.name}`,
5328
+ claimToken: "offline-platform-genesis",
5329
+ claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
5330
+ attempt: 1,
5331
+ action: "create",
5332
+ bootstrap: { kind: "platform-genesis" },
5333
+ access: { sshUser: "forgezero", sshPublicKeys: keys },
5334
+ spec: {
5335
+ reference: `platform:${guest.name}`,
5336
+ imageKey: SUPPORTED_GUEST_IMAGE.key,
5337
+ guestName: guest.name,
5338
+ guestAddress: guest.address,
5339
+ cpuPoolKey: guest.cpuPoolKey,
5340
+ physicalCores: guest.physicalCores,
5341
+ vcpu: guest.vcpu,
5342
+ memoryGib: guest.memoryGib,
5343
+ diskGib: guest.diskGib,
5344
+ egressGuaranteedMbps: guest.egressGuaranteedMbps,
5345
+ egressBurstMbps: guest.egressBurstMbps,
5346
+ confidential: true
5347
+ }
5348
+ }];
5349
+ }
5314
5350
 
5315
5351
  // src/operator-bootstrap.ts
5316
5352
  var REQUEST_LIMIT = 64 * 1024;
@@ -5464,13 +5500,17 @@ function readOperatorMetalBootstrapRequest(path) {
5464
5500
  }
5465
5501
  function planOperatorMetalBootstrap(request, mode) {
5466
5502
  const config = readMetalBootstrapConfig(request.metalConfigFile);
5467
- const claims = platformGenesisClaims(request.genesis.environment, request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path)));
5503
+ if (mode === "rehearsal" && request.genesis.environment !== "development") {
5504
+ throw new Error("the disposable four-node Community rehearsal is development-only");
5505
+ }
5506
+ const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
5507
+ const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
5468
5508
  return {
5469
5509
  kind: "metal-remote",
5470
5510
  mode,
5471
5511
  mutation: mode !== "status",
5472
5512
  target: { address: request.target.address, port: request.target.port, user: request.target.user },
5473
- steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" ? ["verify pinned SSH transport", "send three fixed platform-genesis claims to the constrained Metal helper"] : [
5513
+ steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the fixed three-node seed plus ordinary n4 Community rehearsal claim to the constrained Metal helper" : "send three fixed platform-genesis claims to the constrained Metal helper"] : [
5474
5514
  "verify pinned SSH transport and caller-approved SSH agent",
5475
5515
  "copy pinned Bun and packaged fz artifacts to private staging",
5476
5516
  "stage the strict owner-only metal config and optional seed handoff",
@@ -5765,10 +5805,11 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
5765
5805
  const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
5766
5806
  return { plan, output: output2 };
5767
5807
  }
5768
- if (mode === "genesis") {
5808
+ if (mode === "genesis" || mode === "rehearsal") {
5769
5809
  const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
5770
5810
  const outputs = [];
5771
- for (const claim of platformGenesisClaims(request.genesis.environment, keys)) {
5811
+ const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
5812
+ for (const claim of claims) {
5772
5813
  outputs.push(await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz-agent", "metal-apply", "--claim=-"], `provision ${claim.spec.guestName}`, false, `${JSON.stringify(claim)}
5773
5814
  `));
5774
5815
  }
@@ -2086,7 +2086,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2086
2086
  }
2087
2087
 
2088
2088
  // src/version.ts
2089
- var VERSION3 = "0.1.55";
2089
+ var VERSION3 = "0.1.57";
2090
2090
 
2091
2091
  // src/egress-policy.ts
2092
2092
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -14,4 +14,13 @@ export interface PlatformGenesisGuest {
14
14
  }
15
15
  /** Package-owned provisioning coordinates for the exact three-node platform seed. */
16
16
  export declare const PLATFORM_GENESIS_FLEETS: Record<PlatformGenesisEnvironment, readonly PlatformGenesisGuest[]>;
17
+ /**
18
+ * The disposable Community acceptance fleet adds one ordinary post-genesis
19
+ * Coordinator/DBServer host. It is deliberately not part of the three-member
20
+ * platform seed and never acquires Agency authority from this provisioning
21
+ * claim. The rehearsal controller installs that database role later.
22
+ */
23
+ export declare const DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST: PlatformGenesisGuest;
17
24
  export declare function platformGenesisClaims(environment: PlatformGenesisEnvironment, sshPublicKeys: readonly string[]): readonly RemoteProvisionClaim[];
25
+ /** Exact disposable four-host development fleet: three seed guests plus n4. */
26
+ export declare function platformCommunityRehearsalClaims(sshPublicKeys: readonly string[]): readonly RemoteProvisionClaim[];
package/dist/provision.js CHANGED
@@ -2086,7 +2086,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2086
2086
  }
2087
2087
 
2088
2088
  // src/version.ts
2089
- var VERSION3 = "0.1.55";
2089
+ var VERSION3 = "0.1.57";
2090
2090
 
2091
2091
  // src/egress-policy.ts
2092
2092
  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.55";
2
+ export declare const VERSION = "0.1.57";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",