@forgezero/agent 0.1.39 → 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 +91 -18
- package/dist/agent-heartbeat.js +16 -14
- package/dist/agent-update-helper.js +15 -13
- package/dist/agent-update.js +15 -13
- package/dist/bootstrap.d.ts +13 -0
- package/dist/bootstrap.js +244 -832
- package/dist/cli/index.d.ts +0 -2
- package/dist/cloudflare-bootstrap.d.ts +21 -128
- package/dist/cloudflare-bootstrap.js +146 -788
- 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/fz-agent.js +199 -47
- package/dist/fz.js +536 -1544
- package/dist/index.d.ts +33 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/platform-bootstrap-runtime.d.ts +5 -0
- package/dist/platform-bootstrap-runtime.js +25 -6
- 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
|
@@ -27,7 +27,6 @@ authorization, then continues custody in the browser. Passkey PRF output and
|
|
|
27
27
|
both recovery phrases never enter the terminal:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
fz keys
|
|
31
30
|
fz status
|
|
32
31
|
fz genesis --mode 2-of-3 --api https://api.example --app https://console.example
|
|
33
32
|
fz unlock --phrase-file /secure/offline-phrase.txt # run once per participating custodian
|
|
@@ -56,6 +55,24 @@ sudo fz bootstrap status
|
|
|
56
55
|
sudo fz bootstrap repair --bootstrap-config ./original.json --apply
|
|
57
56
|
```
|
|
58
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
|
+
|
|
59
76
|
Platform database participation is explicit in the private JSON configuration.
|
|
60
77
|
The three genesis nodes use `database.agency: "member"`; later DB+API joiners
|
|
61
78
|
normally use `database.agency: "none"`, which starts a Coordinator and DBServer
|
|
@@ -93,31 +110,87 @@ but remains separate from the root host install so its management token never
|
|
|
93
110
|
enters the API or Agent service environment:
|
|
94
111
|
|
|
95
112
|
```bash
|
|
96
|
-
chmod 600 cloudflare-bootstrap.json cloudflare-management.token
|
|
113
|
+
chmod 600 cloudflare-bootstrap.json cloudflare-management.token cloudflare-runtime-kv.token
|
|
97
114
|
fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json
|
|
98
115
|
fz bootstrap platform cloudflare --bootstrap-config ./cloudflare-bootstrap.json --apply
|
|
99
116
|
# After every host is bootstrapped and publishing healthy KV state:
|
|
100
117
|
fz bootstrap platform cloudflare verify --bootstrap-config ./cloudflare-handoff.json
|
|
101
118
|
```
|
|
102
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
|
+
|
|
103
125
|
The reviewed JSON has `format: 1`, kind
|
|
104
126
|
`forgezero-cloudflare-bootstrap-request`, a `checkpointPath`, the typed
|
|
105
|
-
`coordinates`, and
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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`.
|
|
121
194
|
|
|
122
195
|
## What changes for an application
|
|
123
196
|
|
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, "");
|
|
@@ -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
|
@@ -19,6 +19,12 @@ export interface PlatformBootstrapConfig {
|
|
|
19
19
|
branch: string;
|
|
20
20
|
deployRoot?: string;
|
|
21
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>;
|
|
22
28
|
database: {
|
|
23
29
|
role: DatabaseBootstrapRole;
|
|
24
30
|
/** Genesis master/joiners are Agency members; elastic joiners normally use `none`. */
|
|
@@ -69,6 +75,8 @@ export interface TenantBootstrapConfig {
|
|
|
69
75
|
realm: string;
|
|
70
76
|
nodeHostname: string;
|
|
71
77
|
telemetryEndpoint: string;
|
|
78
|
+
/** Same Vault-first, same-name systemd fallback contract as platform deploys. */
|
|
79
|
+
deploymentCredentials?: Record<string, string>;
|
|
72
80
|
enrolTokenFile: string;
|
|
73
81
|
repository?: string;
|
|
74
82
|
branch?: string;
|
|
@@ -76,6 +84,11 @@ export interface TenantBootstrapConfig {
|
|
|
76
84
|
deployRoot?: string;
|
|
77
85
|
software?: SoftwareRequirement[];
|
|
78
86
|
installCloudflared?: boolean;
|
|
87
|
+
/** Optional node-specific attended Tunnel/KV handoff, identical to platform hosts. */
|
|
88
|
+
cloudflareHandoff?: {
|
|
89
|
+
handoffFile: string;
|
|
90
|
+
nodeName: string;
|
|
91
|
+
};
|
|
79
92
|
/** Local-only runner authority. The API receives only the resulting node scope. */
|
|
80
93
|
bootstrapRunner?: {
|
|
81
94
|
sshPrivateKeyFile: string;
|