@forgezero/agent 0.1.38 → 0.1.40

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
@@ -15,7 +15,10 @@ fz --help
15
15
 
16
16
  The one public package installs both commands. Keeping bootstrap and the daemon
17
17
  in one version prevents a newly installed `fz` from provisioning a different
18
- agent protocol.
18
+ agent protocol. Both executable artifacts bundle their ForgeZero runtime
19
+ dependencies, so the constrained SSH bootstrap can transfer the reviewed pair
20
+ to a new host without relying on a checkout, `node_modules`, or a second remote
21
+ package-resolution step. Library subpath exports remain modular packages.
19
22
 
20
23
  The operator command starts browser-first platform genesis, inspects status,
21
24
  wraps processes, and installs the daemon. Genesis opens the one-use founder
@@ -24,7 +27,6 @@ authorization, then continues custody in the browser. Passkey PRF output and
24
27
  both recovery phrases never enter the terminal:
25
28
 
26
29
  ```bash
27
- fz keys
28
30
  fz status
29
31
  fz genesis --mode 2-of-3 --api https://api.example --app https://console.example
30
32
  fz unlock --phrase-file /secure/offline-phrase.txt # run once per participating custodian
@@ -43,13 +45,51 @@ script and no second implementation for tenant or platform machines:
43
45
 
44
46
  ```bash
45
47
  fz bootstrap platform # interactive plan, no mutation
46
- sudo fz bootstrap platform --apply # initial DB+API or API-only host
48
+ sudo fz bootstrap platform prepare --bootstrap-config ./platform.json --apply
49
+ # Register the printed read-only deploy key. For genesis, run --apply on all
50
+ # three Agency members concurrently; later agency:none joiners run alone.
51
+ sudo fz bootstrap platform --bootstrap-config ./platform.json --apply
47
52
  sudo fz bootstrap tenant --bootstrap-config ./tenant.json --apply
48
53
  sudo fz bootstrap metal --bootstrap-config ./metal.json --apply
49
54
  sudo fz bootstrap status
50
55
  sudo fz bootstrap repair --bootstrap-config ./original.json --apply
51
56
  ```
52
57
 
58
+ Interactive bootstrap prompts only for missing typed inputs and validates every
59
+ answer before it changes the host. It must then render a secret-free plan, apply
60
+ idempotently, and verify the external and local result. Initial platform bring-up
61
+ has exactly three attended owner gates:
62
+
63
+ 1. SMTP host, port, sender, optional user, and an owner-only password file; the
64
+ resulting provider must pass its configured health/send acceptance.
65
+ 2. The separate Cloudflare phase consumes owner-only management and KV-runtime
66
+ token files, reconciles Tunnel/hostname state, and emits the node handoff; the
67
+ normal API/Agent never receives the management token.
68
+ 3. `prepare` prints that machine's read-only Git deploy public key. The owner adds
69
+ it to the repository and confirms completion; bootstrap then proves repository
70
+ access before the first deployment.
71
+
72
+ Browser authentication is deliberately not automated away. If npm or another
73
+ provider prints an authentication URL, leave the command attached to its TTY,
74
+ open the URL, complete the passkey flow, and let the same command continue.
75
+
76
+ Platform database participation is explicit in the private JSON configuration.
77
+ The three genesis nodes use `database.agency: "member"`; later DB+API joiners
78
+ normally use `database.agency: "none"`, which starts a Coordinator and DBServer
79
+ without adding a fourth Agency member. Enrolment is likewise explicit:
80
+ `enrolment.source: "genesis-derived"` only for the offline genesis fleet, or
81
+ `"api-token"` with `tokenFile` for every API-authorized later node. No behavior
82
+ is inferred from a hostname or numeric suffix.
83
+
84
+ Repair is not a second installation path: the CLI detects the installed
85
+ platform/tenant or metal state and dispatches to that profile's validator. It
86
+ fails closed if both state formats are present or neither is present. A private
87
+ intent journal binds an interrupted first apply, and later repair refuses a
88
+ config whose immutable host identity differs from the installed profile. Status checks the persisted
89
+ identity record, both Agent sockets, profile-owned systemd units and, for a
90
+ platform host, the active API health endpoint, nginx configuration and live
91
+ Coordinator-mode evidence.
92
+
53
93
  Platform bootstrap supports an elastic Community ArangoDB 3.11.14 fleet. The
54
94
  first three database-capable computes establish the writable cluster; later
55
95
  database joiners and API-only computes use the same command with their typed
@@ -70,21 +110,87 @@ but remains separate from the root host install so its management token never
70
110
  enters the API or Agent service environment:
71
111
 
72
112
  ```bash
73
- chmod 600 cloudflare-bootstrap.json cloudflare-management.token
113
+ chmod 600 cloudflare-bootstrap.json cloudflare-management.token cloudflare-runtime-kv.token
74
114
  fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json
75
115
  fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json --apply
116
+ # After every host is bootstrapped and publishing healthy KV state:
117
+ fz bootstrap platform cloudflare verify --bootstrap-config ./cloudflare-handoff.json
76
118
  ```
77
119
 
120
+ Tenant-owned public origins use the identical reviewed request and handoff with
121
+ `fz bootstrap tenant cloudflare ...`; `fz bootstrap tenant` consumes only that
122
+ tenant node's handoff. Both host kinds seal the connector and supplied KV token
123
+ and can repair after the plaintext handoff has been removed.
124
+
78
125
  The reviewed JSON has `format: 1`, kind
79
126
  `forgezero-cloudflare-bootstrap-request`, a `checkpointPath`, the typed
80
- `coordinates`, and `tokenFiles` containing file paths only. `coordinates.nodes`
81
- explicitly lists each node name, origin hostname, loopback service, Tunnel name
82
- and Access application name. Relative token, checkpoint and Worker project
83
- paths resolve beside the config file. Plan is offline. Apply creates/reuses one
84
- shared KV namespace, Worker and Access service token plus one Tunnel, Access
85
- application, ingress rule and DNS record per node. The command prints only
86
- resource IDs; connector, Access and requested runtime-token values stay in the
87
- atomically updated owner-only checkpoint for direct systemd-credential handoff.
127
+ `coordinates`, and token **file paths only**. The recommended token split is:
128
+
129
+ ```json
130
+ {
131
+ "format": 1,
132
+ "kind": "forgezero-cloudflare-bootstrap-request",
133
+ "checkpointPath": "./cloudflare-handoff.json",
134
+ "coordinates": {
135
+ "accountId": "0123456789abcdef0123456789abcdef",
136
+ "zoneId": "0123456789abcdef0123456789abcdef",
137
+ "hostname": "dev-api-n1.example.com",
138
+ "service": "http://127.0.0.1:3000",
139
+ "tunnelName": "dev-api-n1",
140
+ "kvNamespaceId": "0123456789abcdef0123456789abcdef",
141
+ "nodes": [
142
+ {
143
+ "nodeName": "dev-api-n1",
144
+ "hostname": "dev-api-n1.example.com",
145
+ "service": "http://127.0.0.1:3000",
146
+ "tunnelName": "dev-api-n1"
147
+ }
148
+ ]
149
+ },
150
+ "tokenFiles": {
151
+ "managementApiTokenFile": "./cloudflare-management.token",
152
+ "runtimeApiTokenFile": "./cloudflare-runtime-kv.token"
153
+ }
154
+ }
155
+ ```
156
+
157
+ The management token has exact-account **Cloudflare Tunnel Write** and
158
+ exact-zone **DNS Write**. The runtime token has exact-account **Workers KV
159
+ Storage Write**. Apply creates or adopts one remotely managed Tunnel per node,
160
+ checkpoints its connector token, configures the public-hostname ingress, and
161
+ reconciles the exact proxied CNAME. DNS reconciliation lists the hostname
162
+ without a type filter: it updates one existing CNAME, creates only when absent,
163
+ and refuses ambiguity or an incompatible A/AAAA/other owner before changing
164
+ Tunnel ingress. It does not create a Worker, KV namespace, Access application,
165
+ Access service token, or Cloudflare API token.
166
+
167
+ The Worker and its KV/DO bindings are deployed separately through Cloudflare's
168
+ Git integration. Durable Objects are reached through the Worker's binding, not
169
+ with a Cloudflare REST token. The complete 0600 checkpoint stays on the
170
+ operator machine; `<checkpoint>.hosts/<node>.json` contains only that node's
171
+ connector plus the supplied KV runtime token. Host bootstrap seals them as the
172
+ `cloudflared-token` and `cloudflare-kv-token` systemd credentials and removes
173
+ the plaintext node handoff. The API resolves `cloudflare.kvApiToken` from the
174
+ platform Vault first and falls back to the same systemd credential while the
175
+ Vault is locked or unavailable. Management credentials are never persisted or
176
+ loaded into the API/Agent service.
177
+
178
+ Platform and tenant bootstrap configs may declare a bounded
179
+ `deploymentCredentials` map from the exact pipeline secret name to its
180
+ encrypted `.cred` source, for example
181
+ `{"SMTP_PASSWORD":"/etc/forgezero/creds/SMTP_PASSWORD.cred"}`. The supervised
182
+ Agent loads only those named credentials. A deployment asks for the same name;
183
+ the Agent reads its project-scoped in-memory Vault replica first and uses the
184
+ same-name systemd credential only until the replica can supply it. There is no
185
+ directory scan, cross-project lookup, or differently named fallback.
186
+
187
+ This is a versioned runtime schema, not a naming convention. The enrolled
188
+ binding supplies the exact project and environment; `credential-schema.ts`
189
+ maps a requested name to that environment's Vault cache coordinate. Platform
190
+ and tenant computes use this Vault-first policy. Physical metal has no project
191
+ Vault and loads only its fixed `metal-agent-seed` systemd credential. Attended
192
+ operator Cloudflare/bootstrap tokens are owner-only input files and are never a
193
+ runtime fallback. The schema is exported as `@forgezero/agent/credential-schema`.
88
194
 
89
195
  ## What changes for an application
90
196
 
@@ -11,6 +11,11 @@ export interface AgentObservation {
11
11
  architecture: string;
12
12
  mode: 'attested' | 'enrolled';
13
13
  update?: AgentUpdateReceipt;
14
+ deploymentIntake?: {
15
+ generation: number;
16
+ state: 'running' | 'paused';
17
+ appliedAtTs: number;
18
+ };
14
19
  }
15
20
  export interface AgentHeartbeatResponse {
16
21
  ok: true;
@@ -22,6 +27,10 @@ export interface AgentHeartbeatResponse {
22
27
  leaseExpiresAtTs: number;
23
28
  release: AgentRelease;
24
29
  };
30
+ deploymentIntake?: {
31
+ generation: number;
32
+ state: 'running' | 'paused';
33
+ };
25
34
  }
26
35
  export interface AgentHeartbeatOptions {
27
36
  apiUrl: string;
@@ -33,6 +42,10 @@ export interface AgentHeartbeatOptions {
33
42
  fetch?: (input: URL, init: RequestInit) => Promise<Response>;
34
43
  requestTimeoutMs?: number;
35
44
  observation?: () => AgentObservation;
45
+ /** Last applied server-owned deployment-intake generation, reported on the next heartbeat. */
46
+ deploymentIntakeObservation?: () => AgentObservation['deploymentIntake'];
47
+ /** Apply the authenticated desired state before any new deployment claim is allowed. */
48
+ applyDeploymentIntake?: (directive: NonNullable<AgentHeartbeatResponse['deploymentIntake']>) => Promise<void> | void;
36
49
  receiptPath?: string;
37
50
  now?: () => number;
38
51
  /** Stop new lifecycle/deploy claims and await current jobs before replacement. */
@@ -93,6 +93,8 @@ var checked = async (run, input, label) => {
93
93
  return result;
94
94
  };
95
95
  async function validateReleaseDirectory(directory, release, run) {
96
+ chmodSync(directory, 493);
97
+ chmodSync(join(directory, "dist"), 493);
96
98
  const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
97
99
  if (manifest.name !== release.package || manifest.version !== release.version) {
98
100
  throw new Error("agent update manifest does not match the selected release");
@@ -143,19 +145,19 @@ async function stageAgentRelease(releaseInput, options) {
143
145
  if (!timingSafeEqual(actual, expected))
144
146
  throw new Error("agent update integrity mismatch");
145
147
  writeFileSync(archive, bytes, { mode: 384, flag: "wx" });
146
- await checked(run, {
147
- command: "/usr/bin/tar",
148
- args: [
149
- "-xzf",
150
- archive,
151
- "-C",
152
- unpacked,
153
- "--strip-components=1",
154
- "package/package.json",
155
- "package/dist/fz-agent.js",
156
- "package/dist/fz.js"
157
- ]
158
- }, "agent update extraction");
148
+ for (const [member, relative] of [
149
+ ["package/package.json", "package.json"],
150
+ ["package/dist/fz-agent.js", "dist/fz-agent.js"],
151
+ ["package/dist/fz.js", "dist/fz.js"]
152
+ ]) {
153
+ const extracted = await checked(run, {
154
+ command: "/usr/bin/tar",
155
+ args: ["-xOzf", archive, member]
156
+ }, `agent update extraction of ${member}`);
157
+ const destination = join(unpacked, relative);
158
+ mkdirSync(dirname(destination), { recursive: true, mode: 448 });
159
+ writeFileSync(destination, extracted.output, { mode: 384, flag: "wx" });
160
+ }
159
161
  await validateReleaseDirectory(unpacked, release, run);
160
162
  if (!existsSync(finalDirectory)) {
161
163
  renameSync(unpacked, finalDirectory);
@@ -744,7 +746,7 @@ async function postSignedNode(options, path, body) {
744
746
  }
745
747
 
746
748
  // src/version.ts
747
- var VERSION3 = "0.1.38";
749
+ var VERSION3 = "0.1.40";
748
750
 
749
751
  // src/agent-heartbeat.ts
750
752
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
@@ -770,7 +772,15 @@ async function heartbeatAgentOnce(options) {
770
772
  const update = observation.update ?? readAgentUpdateReceipt(options.receiptPath ?? AGENT_UPDATE_RECEIPT);
771
773
  if (update)
772
774
  observation = { ...observation, update };
775
+ const deploymentIntake = options.deploymentIntakeObservation?.();
776
+ if (deploymentIntake)
777
+ observation = { ...observation, deploymentIntake };
773
778
  const response = await postSignedNode(options, "v1/node/heartbeat", observation);
779
+ if (response.deploymentIntake !== undefined) {
780
+ if (!Number.isSafeInteger(response.deploymentIntake.generation) || response.deploymentIntake.generation < 0 || response.deploymentIntake.state !== "running" && response.deploymentIntake.state !== "paused")
781
+ throw new Error("agent heartbeat deployment-intake directive is invalid");
782
+ await options.applyDeploymentIntake?.(response.deploymentIntake);
783
+ }
774
784
  const desired = response.desiredAgentUpdate ?? (response.desiredAgentRelease ? {
775
785
  attemptId: `legacy:${response.desiredAgentRelease.version}`,
776
786
  leaseExpiresAtTs: Number.MAX_SAFE_INTEGER,
@@ -93,6 +93,8 @@ var checked = async (run, input, label) => {
93
93
  return result;
94
94
  };
95
95
  async function validateReleaseDirectory(directory, release, run) {
96
+ chmodSync(directory, 493);
97
+ chmodSync(join(directory, "dist"), 493);
96
98
  const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
97
99
  if (manifest.name !== release.package || manifest.version !== release.version) {
98
100
  throw new Error("agent update manifest does not match the selected release");
@@ -143,19 +145,19 @@ async function stageAgentRelease(releaseInput, options) {
143
145
  if (!timingSafeEqual(actual, expected))
144
146
  throw new Error("agent update integrity mismatch");
145
147
  writeFileSync(archive, bytes, { mode: 384, flag: "wx" });
146
- await checked(run, {
147
- command: "/usr/bin/tar",
148
- args: [
149
- "-xzf",
150
- archive,
151
- "-C",
152
- unpacked,
153
- "--strip-components=1",
154
- "package/package.json",
155
- "package/dist/fz-agent.js",
156
- "package/dist/fz.js"
157
- ]
158
- }, "agent update extraction");
148
+ for (const [member, relative] of [
149
+ ["package/package.json", "package.json"],
150
+ ["package/dist/fz-agent.js", "dist/fz-agent.js"],
151
+ ["package/dist/fz.js", "dist/fz.js"]
152
+ ]) {
153
+ const extracted = await checked(run, {
154
+ command: "/usr/bin/tar",
155
+ args: ["-xOzf", archive, member]
156
+ }, `agent update extraction of ${member}`);
157
+ const destination = join(unpacked, relative);
158
+ mkdirSync(dirname(destination), { recursive: true, mode: 448 });
159
+ writeFileSync(destination, extracted.output, { mode: 384, flag: "wx" });
160
+ }
159
161
  await validateReleaseDirectory(unpacked, release, run);
160
162
  if (!existsSync(finalDirectory)) {
161
163
  renameSync(unpacked, finalDirectory);
@@ -93,6 +93,8 @@ var checked = async (run, input, label) => {
93
93
  return result;
94
94
  };
95
95
  async function validateReleaseDirectory(directory, release, run) {
96
+ chmodSync(directory, 493);
97
+ chmodSync(join(directory, "dist"), 493);
96
98
  const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
97
99
  if (manifest.name !== release.package || manifest.version !== release.version) {
98
100
  throw new Error("agent update manifest does not match the selected release");
@@ -143,19 +145,19 @@ async function stageAgentRelease(releaseInput, options) {
143
145
  if (!timingSafeEqual(actual, expected))
144
146
  throw new Error("agent update integrity mismatch");
145
147
  writeFileSync(archive, bytes, { mode: 384, flag: "wx" });
146
- await checked(run, {
147
- command: "/usr/bin/tar",
148
- args: [
149
- "-xzf",
150
- archive,
151
- "-C",
152
- unpacked,
153
- "--strip-components=1",
154
- "package/package.json",
155
- "package/dist/fz-agent.js",
156
- "package/dist/fz.js"
157
- ]
158
- }, "agent update extraction");
148
+ for (const [member, relative] of [
149
+ ["package/package.json", "package.json"],
150
+ ["package/dist/fz-agent.js", "dist/fz-agent.js"],
151
+ ["package/dist/fz.js", "dist/fz.js"]
152
+ ]) {
153
+ const extracted = await checked(run, {
154
+ command: "/usr/bin/tar",
155
+ args: ["-xOzf", archive, member]
156
+ }, `agent update extraction of ${member}`);
157
+ const destination = join(unpacked, relative);
158
+ mkdirSync(dirname(destination), { recursive: true, mode: 448 });
159
+ writeFileSync(destination, extracted.output, { mode: 384, flag: "wx" });
160
+ }
159
161
  await validateReleaseDirectory(unpacked, release, run);
160
162
  if (!existsSync(finalDirectory)) {
161
163
  renameSync(unpacked, finalDirectory);
@@ -4,6 +4,8 @@ import { type PlatformSharedEnvironment } from './platform-bootstrap-runtime';
4
4
  export declare const PLATFORM_BOOTSTRAP_PROFILES: readonly ["platform-db-api", "platform-api"];
5
5
  export type PlatformBootstrapProfile = (typeof PLATFORM_BOOTSTRAP_PROFILES)[number];
6
6
  export type DatabaseBootstrapRole = 'master' | 'joiner' | 'none';
7
+ export type DatabaseAgencyParticipation = 'member' | 'none';
8
+ export type PlatformEnrolmentSource = 'genesis-derived' | 'api-token';
7
9
  export type BootstrapEnvironment = 'production' | 'development';
8
10
  export interface PlatformBootstrapConfig {
9
11
  kind: 'platform';
@@ -17,8 +19,16 @@ export interface PlatformBootstrapConfig {
17
19
  branch: string;
18
20
  deployRoot?: string;
19
21
  telemetryEndpoint: string;
22
+ /**
23
+ * Pipeline secret name -> encrypted systemd credential source. The Agent asks
24
+ * its project Vault replica first and uses only the same-name entry here while
25
+ * that replica is unavailable or has not received the secret yet.
26
+ */
27
+ deploymentCredentials?: Record<string, string>;
20
28
  database: {
21
29
  role: DatabaseBootstrapRole;
30
+ /** Genesis master/joiners are Agency members; elastic joiners normally use `none`. */
31
+ agency: DatabaseAgencyParticipation;
22
32
  /** Cluster mode must remain default. Bootstrap verifies it and never mutates it. */
23
33
  serverMode: 'default';
24
34
  address?: string;
@@ -27,8 +37,12 @@ export interface PlatformBootstrapConfig {
27
37
  /** Owner/root-only file containing the shared 64-hex bootstrap code. */
28
38
  bootstrapSecretFile: string;
29
39
  };
30
- /** Required for every post-genesis compute; initial n1..n3 derive their one-use capability. */
31
- platformEnrolTokenFile?: string;
40
+ /** Explicit authority source; never inferred from a node number. */
41
+ enrolment: {
42
+ source: PlatformEnrolmentSource;
43
+ /** Required only for an API-issued post-genesis capability. */
44
+ tokenFile?: string;
45
+ };
32
46
  runtime: {
33
47
  environment: PlatformSharedEnvironment;
34
48
  serviceUser: string;
@@ -49,9 +63,9 @@ export interface PlatformBootstrapConfig {
49
63
  };
50
64
  /** Install the connector binary only. Resource creation belongs to attended Cloudflare bootstrap. */
51
65
  installCloudflared?: boolean;
52
- /** Completed attended resource checkpoint; root seals only this node's runtime capabilities. */
66
+ /** Node-specific attended handoff; a host must never receive the fleet checkpoint. */
53
67
  cloudflareHandoff?: {
54
- checkpointFile: string;
68
+ handoffFile: string;
55
69
  nodeName: string;
56
70
  };
57
71
  }
@@ -61,6 +75,8 @@ export interface TenantBootstrapConfig {
61
75
  realm: string;
62
76
  nodeHostname: string;
63
77
  telemetryEndpoint: string;
78
+ /** Same Vault-first, same-name systemd fallback contract as platform deploys. */
79
+ deploymentCredentials?: Record<string, string>;
64
80
  enrolTokenFile: string;
65
81
  repository?: string;
66
82
  branch?: string;
@@ -68,6 +84,11 @@ export interface TenantBootstrapConfig {
68
84
  deployRoot?: string;
69
85
  software?: SoftwareRequirement[];
70
86
  installCloudflared?: boolean;
87
+ /** Optional node-specific attended Tunnel/KV handoff, identical to platform hosts. */
88
+ cloudflareHandoff?: {
89
+ handoffFile: string;
90
+ nodeName: string;
91
+ };
71
92
  /** Local-only runner authority. The API receives only the resulting node scope. */
72
93
  bootstrapRunner?: {
73
94
  sshPrivateKeyFile: string;
@@ -97,6 +118,13 @@ export interface BootstrapResult {
97
118
  inviteUrl: string;
98
119
  };
99
120
  }
121
+ export interface PlatformBootstrapPreparation {
122
+ kind: 'platform';
123
+ prepared: true;
124
+ identityDigest: string;
125
+ gitPublicKey: string;
126
+ next: 'register this read-only deploy key, then run --apply concurrently on all three genesis Agency members';
127
+ }
100
128
  export interface BootstrapStatus {
101
129
  initialized: boolean;
102
130
  kind?: BootstrapConfig['kind'];
@@ -104,6 +132,12 @@ export interface BootstrapStatus {
104
132
  services: Record<string, boolean>;
105
133
  problems: string[];
106
134
  }
135
+ export type InstalledBootstrapKind = 'compute' | 'metal';
136
+ /** One host has one bootstrap authority. Dual state is never resolved by precedence. */
137
+ export declare function resolveInstalledBootstrapKind(states: {
138
+ compute: boolean;
139
+ metal: boolean;
140
+ }): InstalledBootstrapKind | undefined;
107
141
  export interface BootstrapHost {
108
142
  uid(): number;
109
143
  exists(path: string): boolean;
@@ -122,11 +156,22 @@ export interface BootstrapHost {
122
156
  exec(argv: readonly string[], options?: {
123
157
  stdin?: string;
124
158
  }): Promise<SoftwareCommandResult>;
159
+ sleep?(milliseconds: number): Promise<void>;
125
160
  ensureSoftware(requirements: readonly SoftwareRequirement[]): Promise<unknown>;
126
161
  installAgent(config: BootstrapConfig, enrolTokenSourcePath?: string): Promise<ProvisionPlan>;
127
162
  }
163
+ export declare const BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
128
164
  export declare function validateBootstrapConfig(value: BootstrapConfig): BootstrapConfig;
129
165
  export declare function planBootstrap(input: BootstrapConfig, initialized?: boolean): BootstrapPlan;
166
+ export declare function bootstrapIdentityDigest(config: BootstrapConfig): string;
167
+ /**
168
+ * Install only the common Agent boundary and generate its machine-local Git
169
+ * identity. Private repositories cannot be deployed until the owner registers
170
+ * this public key, so pretending one monolithic first command can do both is a
171
+ * sequencing bug. The bound intent makes the later apply resumable and refuses
172
+ * a different config after this first privileged mutation.
173
+ */
174
+ export declare function preparePlatformBootstrap(input: PlatformBootstrapConfig, host?: BootstrapHost): Promise<PlatformBootstrapPreparation>;
130
175
  export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
131
176
  export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost): Promise<BootstrapResult>;
132
177
  export declare function readBootstrapConfig(path: string): BootstrapConfig;