@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 +118 -12
- package/dist/agent-heartbeat.d.ts +13 -0
- package/dist/agent-heartbeat.js +24 -14
- package/dist/agent-update-helper.js +15 -13
- package/dist/agent-update.js +15 -13
- package/dist/bootstrap.d.ts +49 -4
- package/dist/bootstrap.js +660 -791
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cloudflare-bootstrap.d.ts +36 -114
- package/dist/cloudflare-bootstrap.js +237 -738
- package/dist/cloudflare-edge.d.ts +3 -151
- package/dist/cloudflare-edge.js +28 -248
- package/dist/credential-schema.d.ts +60 -0
- package/dist/credential-schema.js +335 -0
- package/dist/definition.js +2 -2
- package/dist/deploy-file.js +2 -2
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/fz-agent.js +6407 -58
- package/dist/fz.js +10668 -1151
- package/dist/index.d.ts +33 -0
- package/dist/metal-bootstrap.d.ts +1 -0
- package/dist/metal-bootstrap.js +5 -3
- package/dist/platform-bootstrap-runtime.d.ts +5 -28
- package/dist/platform-bootstrap-runtime.js +26 -77
- package/dist/project-context.js +1 -1
- package/dist/provision.js +18 -16
- package/dist/software-helper.js +2 -2
- package/dist/software.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +8 -4
- package/dist/cli/custody.d.ts +0 -35
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 --
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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. */
|
package/dist/agent-heartbeat.js
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
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.
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
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);
|
package/dist/agent-update.js
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
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);
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
31
|
-
|
|
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
|
-
/**
|
|
66
|
+
/** Node-specific attended handoff; a host must never receive the fleet checkpoint. */
|
|
53
67
|
cloudflareHandoff?: {
|
|
54
|
-
|
|
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;
|