@forgezero/agent 0.1.67 → 0.1.69
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 +7 -1
- package/dist/agent-heartbeat.js +1 -1
- package/dist/attended-platform-bootstrap.d.ts +22 -0
- package/dist/bootstrap.js +2 -1
- package/dist/fz-agent.js +1 -1
- package/dist/fz.js +292 -39
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +36 -2
- package/dist/operator-bootstrap.js +192 -8
- package/dist/platform-fleet-verification.js +1 -1
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -532,22 +532,26 @@ await readBootstrapBundle(built.bundlePath, built.manifestPath);
|
|
|
532
532
|
<a id="forgezero-agent-operator-bootstrap"></a>
|
|
533
533
|
## @forgezero/agent/operator-bootstrap
|
|
534
534
|
|
|
535
|
-
Pinned-host operator transport that stages owner-only handoffs and invokes the same typed platform bootstrap through the caller-approved SSH agent. This entry exposes
|
|
535
|
+
Pinned-host operator transport that stages owner-only handoffs and invokes the same typed platform bootstrap through the caller-approved SSH agent. This entry exposes 19 named value exports and 17 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
536
536
|
|
|
537
537
|
```text
|
|
538
538
|
import {
|
|
539
539
|
applyOperatorMetalBootstrap,
|
|
540
540
|
applyOperatorPlatformBootstrap,
|
|
541
541
|
applyOperatorPlatformFleetBootstrap,
|
|
542
|
+
collectOperatorGuestHostKeys,
|
|
542
543
|
createOperatorSshHop,
|
|
543
544
|
operatorPlatformFleetCoordinates,
|
|
544
545
|
planOperatorMetalBootstrap,
|
|
545
546
|
planOperatorPlatformBootstrap,
|
|
546
547
|
planOperatorPlatformFleetBootstrap,
|
|
548
|
+
readOperatorGuestHostKeyEvidence,
|
|
547
549
|
readOperatorMetalBootstrapRequest,
|
|
548
550
|
readOperatorPlatformBootstrapFleetRequest,
|
|
549
551
|
readOperatorPlatformBootstrapRequest,
|
|
550
552
|
verifyOperatorPlatformFleet,
|
|
553
|
+
withOperatorGuestTransport,
|
|
554
|
+
writeOperatorGuestHostKeyEvidence,
|
|
551
555
|
writeOperatorMetalBootstrapRequest,
|
|
552
556
|
writeOperatorPlatformBootstrapFleetRequest,
|
|
553
557
|
writeOperatorPlatformBootstrapRequest,
|
|
@@ -556,6 +560,8 @@ import {
|
|
|
556
560
|
import type {
|
|
557
561
|
OperatorCommand,
|
|
558
562
|
OperatorCommandResult,
|
|
563
|
+
OperatorGuestHostKeyEvidence,
|
|
564
|
+
OperatorGuestTransport,
|
|
559
565
|
OperatorMetalBootstrapMode,
|
|
560
566
|
OperatorMetalBootstrapPlan,
|
|
561
567
|
OperatorMetalBootstrapRequest,
|
package/dist/agent-heartbeat.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { finalizeCloudflareBootstrapAcceptance, runAttendedCloudflareBootstrap, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapEvidence } from './cloudflare-bootstrap';
|
|
2
|
+
import { applyOperatorPlatformFleetBootstrap, type OperatorPlatformBootstrapFleetRequest, type OperatorPlatformBootstrapFleetResult } from './operator-bootstrap';
|
|
3
|
+
import type { PlatformBootstrapSecrets } from './bootstrap';
|
|
4
|
+
export interface AttendedPlatformBootstrapResult {
|
|
5
|
+
kind: 'forgezero-attended-platform-bootstrap';
|
|
6
|
+
ok: boolean;
|
|
7
|
+
edge: CloudflareBootstrapEvidence;
|
|
8
|
+
fleet: OperatorPlatformBootstrapFleetResult;
|
|
9
|
+
acceptance?: CloudflareBootstrapAcceptanceEvidence;
|
|
10
|
+
inviteUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AttendedPlatformBootstrapDependencies {
|
|
13
|
+
cloudflare?: typeof runAttendedCloudflareBootstrap;
|
|
14
|
+
fleet?: typeof applyOperatorPlatformFleetBootstrap;
|
|
15
|
+
finalize?: typeof finalizeCloudflareBootstrapAcceptance;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The sole attended genesis composition boundary. All non-secret coordinates
|
|
19
|
+
* are checked before the first Cloudflare or SSH mutation. Secret values exist
|
|
20
|
+
* only in this call and the authenticated SSH stdin used by the fleet runner.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyAttendedPlatformBootstrap(fleetRequest: OperatorPlatformBootstrapFleetRequest, cloudflareRequest: AttendedCloudflareBootstrapRequest, acceptancePath: string, secrets: PlatformBootstrapSecrets, dependencies?: AttendedPlatformBootstrapDependencies): Promise<AttendedPlatformBootstrapResult>;
|
package/dist/bootstrap.js
CHANGED
|
@@ -1396,7 +1396,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1396
1396
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1397
1397
|
|
|
1398
1398
|
// src/version.ts
|
|
1399
|
-
var VERSION = "0.1.
|
|
1399
|
+
var VERSION = "0.1.69";
|
|
1400
1400
|
|
|
1401
1401
|
// src/software.ts
|
|
1402
1402
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4380,6 +4380,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4380
4380
|
}
|
|
4381
4381
|
bindBootstrapIntent(host, config);
|
|
4382
4382
|
const plan = planBootstrap(config, host.exists(STATE_PATH));
|
|
4383
|
+
await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
|
|
4383
4384
|
const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
4384
4385
|
host.mkdir(CREDS, 448);
|
|
4385
4386
|
host.mkdir("/var/lib/forgezero", 448);
|
package/dist/fz-agent.js
CHANGED
|
@@ -8588,7 +8588,7 @@ import { join as join3 } from "path";
|
|
|
8588
8588
|
import { fileURLToPath } from "url";
|
|
8589
8589
|
|
|
8590
8590
|
// src/version.ts
|
|
8591
|
-
var VERSION2 = "0.1.
|
|
8591
|
+
var VERSION2 = "0.1.69";
|
|
8592
8592
|
|
|
8593
8593
|
// src/ssh-bootstrap.ts
|
|
8594
8594
|
class SshBootstrapError extends Error {
|
package/dist/fz.js
CHANGED
|
@@ -4829,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4829
4829
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4830
4830
|
|
|
4831
4831
|
// src/version.ts
|
|
4832
|
-
var VERSION2 = "0.1.
|
|
4832
|
+
var VERSION2 = "0.1.69";
|
|
4833
4833
|
|
|
4834
4834
|
// src/software.ts
|
|
4835
4835
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -14399,6 +14399,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14399
14399
|
}
|
|
14400
14400
|
bindBootstrapIntent(host, config);
|
|
14401
14401
|
const plan = planBootstrap(config, host.exists(STATE_PATH));
|
|
14402
|
+
await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
|
|
14402
14403
|
const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
14403
14404
|
host.mkdir(CREDS, 448);
|
|
14404
14405
|
host.mkdir("/var/lib/forgezero", 448);
|
|
@@ -15076,6 +15077,14 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
15076
15077
|
return evidence;
|
|
15077
15078
|
}
|
|
15078
15079
|
|
|
15080
|
+
// src/operator-bootstrap.ts
|
|
15081
|
+
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15082
|
+
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15083
|
+
import { isIP as isIP6 } from "net";
|
|
15084
|
+
import { tmpdir } from "os";
|
|
15085
|
+
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15086
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15087
|
+
|
|
15079
15088
|
// src/metal-bootstrap.ts
|
|
15080
15089
|
import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
|
|
15081
15090
|
import {
|
|
@@ -15986,12 +15995,6 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15986
15995
|
}
|
|
15987
15996
|
|
|
15988
15997
|
// src/operator-bootstrap.ts
|
|
15989
|
-
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15990
|
-
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15991
|
-
import { isIP as isIP6 } from "net";
|
|
15992
|
-
import { tmpdir } from "os";
|
|
15993
|
-
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15994
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15995
15998
|
var REQUEST_LIMIT = 64 * 1024;
|
|
15996
15999
|
var SECRET_LIMIT = 256 * 1024;
|
|
15997
16000
|
var REMOTE_STAGE = "/run/forgezero-operator-bootstrap";
|
|
@@ -16129,7 +16132,12 @@ function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
|
16129
16132
|
}
|
|
16130
16133
|
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
16131
16134
|
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
16132
|
-
const root = exactKeys5(value, [
|
|
16135
|
+
const root = exactKeys5(value, [
|
|
16136
|
+
"kind",
|
|
16137
|
+
"requests",
|
|
16138
|
+
"cloudflareConfigFile",
|
|
16139
|
+
"cloudflareAcceptanceFile"
|
|
16140
|
+
], "operator fleet request");
|
|
16133
16141
|
if (root.kind !== "platform-fleet-remote")
|
|
16134
16142
|
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
16135
16143
|
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
@@ -16140,10 +16148,25 @@ function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
|
16140
16148
|
throw new Error("operator fleet request files must be unique");
|
|
16141
16149
|
for (const request of requests)
|
|
16142
16150
|
readOperatorPlatformBootstrapRequest(request);
|
|
16143
|
-
|
|
16151
|
+
if (typeof root.cloudflareConfigFile !== "string" || typeof root.cloudflareAcceptanceFile !== "string") {
|
|
16152
|
+
throw new Error("operator fleet requires Cloudflare coordinates and an acceptance evidence path");
|
|
16153
|
+
}
|
|
16154
|
+
const cloudflareConfigFile = canonicalOutputPath(root.cloudflareConfigFile, "operator fleet Cloudflare config");
|
|
16155
|
+
const cloudflareAcceptanceFile = canonicalOutputPath(root.cloudflareAcceptanceFile, "operator fleet Cloudflare acceptance evidence");
|
|
16156
|
+
return {
|
|
16157
|
+
kind: "platform-fleet-remote",
|
|
16158
|
+
requests,
|
|
16159
|
+
cloudflareConfigFile,
|
|
16160
|
+
cloudflareAcceptanceFile
|
|
16161
|
+
};
|
|
16144
16162
|
}
|
|
16145
|
-
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
16146
|
-
const request = {
|
|
16163
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudflare) {
|
|
16164
|
+
const request = {
|
|
16165
|
+
kind: "platform-fleet-remote",
|
|
16166
|
+
requests: requestFiles,
|
|
16167
|
+
cloudflareConfigFile: canonicalOutputPath(cloudflare.configFile, "operator fleet Cloudflare config"),
|
|
16168
|
+
cloudflareAcceptanceFile: canonicalOutputPath(cloudflare.acceptanceFile, "operator fleet Cloudflare acceptance evidence")
|
|
16169
|
+
};
|
|
16147
16170
|
if (new Set(requestFiles).size !== 3)
|
|
16148
16171
|
throw new Error("operator fleet request files must be unique");
|
|
16149
16172
|
for (const file of requestFiles)
|
|
@@ -16237,6 +16260,48 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
16237
16260
|
validateMetalRequestValue(request);
|
|
16238
16261
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
16239
16262
|
}
|
|
16263
|
+
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
16264
|
+
const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
16265
|
+
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
16266
|
+
throw new Error("operator guest host-key evidence is malformed");
|
|
16267
|
+
}
|
|
16268
|
+
const nodes = root.nodes.map((value2, index) => {
|
|
16269
|
+
const row2 = exactKeys5(value2, ["name", "address", "hostKey", "hostKeySha256"], `operator guest host key ${index + 1}`);
|
|
16270
|
+
if (typeof row2.name !== "string" || typeof row2.address !== "string") {
|
|
16271
|
+
throw new Error("operator guest host-key identity is malformed");
|
|
16272
|
+
}
|
|
16273
|
+
const hop = validateHop({
|
|
16274
|
+
address: row2.address,
|
|
16275
|
+
port: 22,
|
|
16276
|
+
user: "forgezero",
|
|
16277
|
+
hostKey: row2.hostKey,
|
|
16278
|
+
hostKeySha256: row2.hostKeySha256
|
|
16279
|
+
}, `operator guest host key ${row2.name}`);
|
|
16280
|
+
return { name: row2.name, address: hop.address, hostKey: hop.hostKey, hostKeySha256: hop.hostKeySha256 };
|
|
16281
|
+
});
|
|
16282
|
+
if (new Set(nodes.map(({ name }) => name)).size !== nodes.length || new Set(nodes.map(({ address }) => address)).size !== nodes.length) {
|
|
16283
|
+
throw new Error("operator guest host-key identities must be unique");
|
|
16284
|
+
}
|
|
16285
|
+
if (request) {
|
|
16286
|
+
const expected = nodes.length === request.genesis.nodes.length ? [...request.genesis.nodes] : [...request.genesis.nodes, ...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []];
|
|
16287
|
+
if (root.metalHostKeySha256 !== request.target.hostKeySha256 || ![request.genesis.nodes.length, request.genesis.nodes.length + (request.genesis.rehearsalNode ? 1 : 0)].includes(nodes.length) || nodes.length !== expected.length || expected.some((guest, index) => nodes[index]?.name !== guest.name || nodes[index]?.address !== guest.address)) {
|
|
16288
|
+
throw new Error("operator guest host-key evidence does not match the reviewed Metal request");
|
|
16289
|
+
}
|
|
16290
|
+
}
|
|
16291
|
+
return {
|
|
16292
|
+
format: 1,
|
|
16293
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
16294
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
16295
|
+
nodes
|
|
16296
|
+
};
|
|
16297
|
+
};
|
|
16298
|
+
function readOperatorGuestHostKeyEvidence(path, request) {
|
|
16299
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator guest host-key evidence")));
|
|
16300
|
+
return validateGuestHostKeyEvidenceValue(value, request);
|
|
16301
|
+
}
|
|
16302
|
+
function writeOperatorGuestHostKeyEvidence(path, evidence, request) {
|
|
16303
|
+
return writeOwnerJson2(path, validateGuestHostKeyEvidenceValue(evidence, request), "operator guest host-key evidence");
|
|
16304
|
+
}
|
|
16240
16305
|
function planOperatorMetalBootstrap(request, mode) {
|
|
16241
16306
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
16242
16307
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
@@ -16244,19 +16309,20 @@ function planOperatorMetalBootstrap(request, mode) {
|
|
|
16244
16309
|
throw new Error("the reviewed metal request has no rehearsal node");
|
|
16245
16310
|
}
|
|
16246
16311
|
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(request.genesis.nodes, request.genesis.rehearsalNode) : platformCommunityRehearsalClaims(request.genesis.nodes, request.genesis.rehearsalNode, keys) : platformGenesisClaims(request.genesis.nodes, keys);
|
|
16312
|
+
const genesisNodes = mode === "host-keys" ? request.genesis.nodes.map(({ name }) => name) : mode === "rehearsal-host-keys" ? guestRows(request).map(({ name }) => name) : claims.map((claim) => claim.spec.guestName).filter((name) => Boolean(name));
|
|
16247
16313
|
return {
|
|
16248
16314
|
kind: "metal-remote",
|
|
16249
16315
|
mode,
|
|
16250
|
-
mutation:
|
|
16316
|
+
mutation: !["status", "host-keys", "rehearsal-host-keys"].includes(mode),
|
|
16251
16317
|
target: { address: request.target.address, port: request.target.port, user: request.target.user },
|
|
16252
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the three reviewed seed claims plus the reviewed fourth Community rehearsal claim to the constrained Metal helper" : mode === "rehearsal-cleanup" ? "delete the exact four reviewed disposable guests and their owned disks, units, seed data and inventory" : "send the three reviewed platform-genesis claims to the constrained Metal helper"] : [
|
|
16318
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "host-keys" || mode === "rehearsal-host-keys" ? ["verify pinned SSH transport", "collect exact Ed25519 host keys for the reviewed guest addresses"] : mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the three reviewed seed claims plus the reviewed fourth Community rehearsal claim to the constrained Metal helper" : mode === "rehearsal-cleanup" ? "delete the exact four reviewed disposable guests and their owned disks, units, seed data and inventory" : "send the three reviewed platform-genesis claims to the constrained Metal helper"] : [
|
|
16253
16319
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
16254
16320
|
"copy pinned Bun and packaged fz artifacts to private staging",
|
|
16255
16321
|
"stage the strict owner-only metal config and optional seed handoff",
|
|
16256
16322
|
"run typed fz bootstrap metal --apply",
|
|
16257
16323
|
"remove transient local and remote staging data"
|
|
16258
16324
|
],
|
|
16259
|
-
genesisNodes
|
|
16325
|
+
genesisNodes,
|
|
16260
16326
|
secretInputs: config.agentSeedFile ? ["metal-agent-seed"] : []
|
|
16261
16327
|
};
|
|
16262
16328
|
}
|
|
@@ -16353,6 +16419,20 @@ function validatedPlatformFleet(fleet) {
|
|
|
16353
16419
|
}
|
|
16354
16420
|
return { requests, configs, environment };
|
|
16355
16421
|
}
|
|
16422
|
+
function operatorPlatformFleetCoordinates(fleet) {
|
|
16423
|
+
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
16424
|
+
return {
|
|
16425
|
+
environment,
|
|
16426
|
+
appOrigin: configs[0].runtime.environment.appOrigin,
|
|
16427
|
+
apiOrigin: configs[0].runtime.environment.apiOrigin,
|
|
16428
|
+
nodes: configs.map((config, index) => ({
|
|
16429
|
+
computeReference: config.computeReference,
|
|
16430
|
+
address: requests[index].target.address,
|
|
16431
|
+
nodeHostname: config.nodeHostname,
|
|
16432
|
+
databaseRole: config.database.role
|
|
16433
|
+
}))
|
|
16434
|
+
};
|
|
16435
|
+
}
|
|
16356
16436
|
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
16357
16437
|
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
16358
16438
|
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
@@ -16479,6 +16559,53 @@ var copy = async (exec, request, knownHosts, local, remotePath, secret = false)
|
|
|
16479
16559
|
`${destination2(request.target)}:${remotePath}`
|
|
16480
16560
|
], "secure copy", { secret });
|
|
16481
16561
|
};
|
|
16562
|
+
var guestRows = (request) => [
|
|
16563
|
+
...request.genesis.nodes,
|
|
16564
|
+
...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []
|
|
16565
|
+
];
|
|
16566
|
+
var parsedScannedHostKey = (output, address) => {
|
|
16567
|
+
const keys = output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith(`${address} ssh-ed25519 `)).map((line) => line.split(/\s+/).slice(1, 3).join(" "));
|
|
16568
|
+
const unique = [...new Set(keys)];
|
|
16569
|
+
if (unique.length !== 1 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}$/.test(unique[0])) {
|
|
16570
|
+
throw new Error(`guest ${address} did not return one exact Ed25519 host key`);
|
|
16571
|
+
}
|
|
16572
|
+
return unique[0];
|
|
16573
|
+
};
|
|
16574
|
+
async function collectOperatorGuestHostKeys(request, options = {}, includeRehearsal = false) {
|
|
16575
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
16576
|
+
socketPath(request.target.agentSocket);
|
|
16577
|
+
const exec = options.exec ?? defaultExec3;
|
|
16578
|
+
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-host-keys-"));
|
|
16579
|
+
try {
|
|
16580
|
+
const knownHosts = writeKnownHosts(request, directory);
|
|
16581
|
+
const nodes = [];
|
|
16582
|
+
for (const guest of includeRehearsal ? guestRows(request) : request.genesis.nodes) {
|
|
16583
|
+
const output = await remote(exec, request, knownHosts, [
|
|
16584
|
+
"/usr/bin/ssh-keyscan",
|
|
16585
|
+
"-T",
|
|
16586
|
+
"10",
|
|
16587
|
+
"-t",
|
|
16588
|
+
"ed25519",
|
|
16589
|
+
guest.address
|
|
16590
|
+
], `collect ${guest.name} SSH host key`);
|
|
16591
|
+
const hostKey = parsedScannedHostKey(output, guest.address);
|
|
16592
|
+
nodes.push({
|
|
16593
|
+
name: guest.name,
|
|
16594
|
+
address: guest.address,
|
|
16595
|
+
hostKey,
|
|
16596
|
+
hostKeySha256: fingerprint(hostKey)
|
|
16597
|
+
});
|
|
16598
|
+
}
|
|
16599
|
+
return validateGuestHostKeyEvidenceValue({
|
|
16600
|
+
format: 1,
|
|
16601
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
16602
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
16603
|
+
nodes
|
|
16604
|
+
}, request);
|
|
16605
|
+
} finally {
|
|
16606
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16607
|
+
}
|
|
16608
|
+
}
|
|
16482
16609
|
async function stageConfig(config, directory) {
|
|
16483
16610
|
const rewritten = structuredClone(config);
|
|
16484
16611
|
const staged = [];
|
|
@@ -16658,6 +16785,12 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16658
16785
|
const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
|
|
16659
16786
|
return { plan, output: output2 };
|
|
16660
16787
|
}
|
|
16788
|
+
if (mode === "host-keys" || mode === "rehearsal-host-keys") {
|
|
16789
|
+
return {
|
|
16790
|
+
plan,
|
|
16791
|
+
output: JSON.stringify(await collectOperatorGuestHostKeys(request, { ...options, exec }, mode === "rehearsal-host-keys"))
|
|
16792
|
+
};
|
|
16793
|
+
}
|
|
16661
16794
|
if (mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup") {
|
|
16662
16795
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16663
16796
|
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
@@ -16701,6 +16834,47 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16701
16834
|
}
|
|
16702
16835
|
}
|
|
16703
16836
|
|
|
16837
|
+
// src/attended-platform-bootstrap.ts
|
|
16838
|
+
var invitationFrom = (fleet, master) => {
|
|
16839
|
+
const output = fleet.outcomes.find(({ computeReference }) => computeReference === master)?.output;
|
|
16840
|
+
try {
|
|
16841
|
+
const parsed = JSON.parse(output ?? "");
|
|
16842
|
+
if (typeof parsed.launch?.inviteUrl === "string")
|
|
16843
|
+
return parsed.launch.inviteUrl;
|
|
16844
|
+
} catch {}
|
|
16845
|
+
throw new Error("platform bootstrap completed without the initial custodian invitation");
|
|
16846
|
+
};
|
|
16847
|
+
async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, acceptancePath, secrets, dependencies = {}) {
|
|
16848
|
+
if (cloudflareRequest.mode !== "apply")
|
|
16849
|
+
throw new Error("attended platform bootstrap requires Cloudflare apply mode");
|
|
16850
|
+
if (!secrets.cloudflareTunnelToken || !secrets.cloudflareApiToken) {
|
|
16851
|
+
throw new Error("attended platform bootstrap requires both Cloudflare tokens");
|
|
16852
|
+
}
|
|
16853
|
+
const fleetCoordinates = operatorPlatformFleetCoordinates(fleetRequest);
|
|
16854
|
+
if (cloudflareRequest.coordinates.nodes.length !== fleetCoordinates.nodes.length || fleetCoordinates.nodes.some((node, index) => {
|
|
16855
|
+
const edge2 = cloudflareRequest.coordinates.nodes[index];
|
|
16856
|
+
return edge2?.nodeName !== node.computeReference || edge2.hostname !== node.nodeHostname || edge2.service !== "http://127.0.0.1:3000";
|
|
16857
|
+
})) {
|
|
16858
|
+
throw new Error("Cloudflare coordinates do not match the exact reviewed platform fleet");
|
|
16859
|
+
}
|
|
16860
|
+
const edge = await (dependencies.cloudflare ?? runAttendedCloudflareBootstrap)({
|
|
16861
|
+
...cloudflareRequest,
|
|
16862
|
+
tokens: {
|
|
16863
|
+
tunnelToken: secrets.cloudflareTunnelToken,
|
|
16864
|
+
apiToken: secrets.cloudflareApiToken
|
|
16865
|
+
}
|
|
16866
|
+
});
|
|
16867
|
+
const fleet = await (dependencies.fleet ?? applyOperatorPlatformFleetBootstrap)(fleetRequest, "apply", { secrets });
|
|
16868
|
+
if (!fleet.ok)
|
|
16869
|
+
return { kind: "forgezero-attended-platform-bootstrap", ok: false, edge, fleet };
|
|
16870
|
+
const inviteUrl = invitationFrom(fleet, fleetCoordinates.nodes[0].computeReference);
|
|
16871
|
+
const acceptance = await (dependencies.finalize ?? finalizeCloudflareBootstrapAcceptance)({
|
|
16872
|
+
checkpointPath: edge.checkpointFile,
|
|
16873
|
+
acceptancePath
|
|
16874
|
+
});
|
|
16875
|
+
return { kind: "forgezero-attended-platform-bootstrap", ok: true, edge, fleet, acceptance, inviteUrl };
|
|
16876
|
+
}
|
|
16877
|
+
|
|
16704
16878
|
// src/platform-genesis-config.ts
|
|
16705
16879
|
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
|
|
16706
16880
|
const fleet = validatePlatformGenesisGuests(guests);
|
|
@@ -18002,9 +18176,12 @@ function writePlatformGenesisFleet(directory) {
|
|
|
18002
18176
|
const output = genesisOutputDirectory(directory);
|
|
18003
18177
|
const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
18004
18178
|
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile);
|
|
18179
|
+
const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
18180
|
+
const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
18005
18181
|
const fleet = metalRequest.genesis.nodes;
|
|
18006
18182
|
const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile).metalHostname;
|
|
18007
|
-
const
|
|
18183
|
+
const checkpointPath = join12(output, "cloudflare-handoff.json");
|
|
18184
|
+
const template = interactiveBootstrap("platform", fleet, checkpointPath);
|
|
18008
18185
|
const region = {
|
|
18009
18186
|
label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
|
|
18010
18187
|
country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
|
|
@@ -18012,32 +18189,65 @@ function writePlatformGenesisFleet(directory) {
|
|
|
18012
18189
|
};
|
|
18013
18190
|
const nodes = fleet.map((guest, index) => index === 0 ? {
|
|
18014
18191
|
nodeHostname: template.nodeHostname,
|
|
18015
|
-
cloudflareHandoffFile:
|
|
18192
|
+
cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
|
|
18016
18193
|
} : {
|
|
18017
18194
|
nodeHostname: bootstrapAnswer(`${guest.name} public node hostname`),
|
|
18018
|
-
cloudflareHandoffFile:
|
|
18195
|
+
cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
|
|
18019
18196
|
});
|
|
18197
|
+
const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
|
|
18198
|
+
const cloudflareConfig = writeBootstrapConfig(join12(output, "cloudflare.json"), createCloudflareBootstrapCommandConfig({
|
|
18199
|
+
checkpointPath,
|
|
18200
|
+
coordinates: {
|
|
18201
|
+
accountId: bootstrapAnswer("Cloudflare account id"),
|
|
18202
|
+
zoneId: bootstrapAnswer("Cloudflare zone id"),
|
|
18203
|
+
kvNamespaceId: bootstrapAnswer("Existing Worker-bound KV namespace id"),
|
|
18204
|
+
...realtimeEnabled ? { realtime: {
|
|
18205
|
+
workerScriptName: bootstrapAnswer("Existing Worker script name"),
|
|
18206
|
+
endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
|
|
18207
|
+
producer: bootstrapAnswer("Realtime producer identity", "platform-api")
|
|
18208
|
+
} } : {},
|
|
18209
|
+
nodes: fleet.map((guest, index) => ({
|
|
18210
|
+
nodeName: guest.name,
|
|
18211
|
+
hostname: nodes[index].nodeHostname,
|
|
18212
|
+
service: "http://127.0.0.1:3000",
|
|
18213
|
+
tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
|
|
18214
|
+
}))
|
|
18215
|
+
}
|
|
18216
|
+
}));
|
|
18217
|
+
const cloudflareAcceptance = join12(output, "cloudflare-acceptance.json");
|
|
18020
18218
|
const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
|
|
18021
18219
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
18022
18220
|
writeBootstrapConfig(path, config);
|
|
18023
18221
|
return path;
|
|
18024
18222
|
});
|
|
18025
|
-
const identity = operatorIdentityCoordinates();
|
|
18026
|
-
const useJump = bootstrapBoolean("Reach genesis computes through the Metal SSH jump?", "yes");
|
|
18027
|
-
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
18028
18223
|
const requests = configs.map((platformConfigFile, index) => {
|
|
18029
18224
|
const guest = fleet[index];
|
|
18030
|
-
const
|
|
18225
|
+
const host = guestHostKeys.nodes.find(({ name }) => name === guest.name);
|
|
18031
18226
|
return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
|
|
18032
18227
|
kind: "platform-remote",
|
|
18033
18228
|
platformConfigFile,
|
|
18034
|
-
target: {
|
|
18229
|
+
target: {
|
|
18230
|
+
address: guest.address,
|
|
18231
|
+
port: 22,
|
|
18232
|
+
user: "forgezero",
|
|
18233
|
+
hostKey: host.hostKey,
|
|
18234
|
+
hostKeySha256: host.hostKeySha256,
|
|
18235
|
+
identityPublicKeyFile: metalRequest.target.identityPublicKeyFile,
|
|
18236
|
+
agentSocket: metalRequest.target.agentSocket,
|
|
18237
|
+
jump: {
|
|
18238
|
+
address: metalRequest.target.address,
|
|
18239
|
+
port: metalRequest.target.port,
|
|
18240
|
+
user: metalRequest.target.user,
|
|
18241
|
+
hostKey: metalRequest.target.hostKey,
|
|
18242
|
+
hostKeySha256: metalRequest.target.hostKeySha256
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18035
18245
|
});
|
|
18036
18246
|
});
|
|
18037
|
-
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
18038
|
-
return { configs, requests, fleetRequest };
|
|
18247
|
+
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests, { configFile: cloudflareConfig, acceptanceFile: cloudflareAcceptance });
|
|
18248
|
+
return { configs, requests, fleetRequest, cloudflareConfig, cloudflareAcceptance };
|
|
18039
18249
|
}
|
|
18040
|
-
function interactiveBootstrap(kind, genesisGuests) {
|
|
18250
|
+
function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
|
|
18041
18251
|
if (!process.stdin.isTTY)
|
|
18042
18252
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
18043
18253
|
const environment = bootstrapAnswer("Environment (production/development)", "production");
|
|
@@ -18077,7 +18287,7 @@ function interactiveBootstrap(kind, genesisGuests) {
|
|
|
18077
18287
|
const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
|
|
18078
18288
|
const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
|
|
18079
18289
|
const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
|
|
18080
|
-
const cloudflareHandoffFile =
|
|
18290
|
+
const cloudflareHandoffFile = genesis ? cloudflareHostHandoffPath(genesisCloudflareCheckpoint, computeReference) : bootstrapAnswer("Node-specific Cloudflare host handoff (blank for no public edge)", "");
|
|
18081
18291
|
const cloudflareNodeName = cloudflareHandoffFile ? bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
|
|
18082
18292
|
const databaseNetworkMode = "private-lan";
|
|
18083
18293
|
const enrolmentSource = genesis ? "genesis-derived" : bootstrapAnswer("Enrolment source (genesis-derived/api-token)", "genesis-derived");
|
|
@@ -18183,19 +18393,49 @@ async function cmdBootstrap(options, args) {
|
|
|
18183
18393
|
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
18184
18394
|
const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
|
|
18185
18395
|
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
18396
|
+
const cloudflare = readCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, options.apply && mode === "apply" ? "apply" : "plan");
|
|
18397
|
+
const coordinates = operatorPlatformFleetCoordinates(fleet);
|
|
18398
|
+
if (cloudflare.coordinates.nodes.length !== coordinates.nodes.length || coordinates.nodes.some((node, index) => {
|
|
18399
|
+
const edge = cloudflare.coordinates.nodes[index];
|
|
18400
|
+
return edge?.nodeName !== node.computeReference || edge.hostname !== node.nodeHostname || edge.service !== "http://127.0.0.1:3000";
|
|
18401
|
+
})) {
|
|
18402
|
+
throw new Error("Cloudflare coordinates do not match the exact reviewed platform fleet");
|
|
18403
|
+
}
|
|
18186
18404
|
if (!options.apply) {
|
|
18187
|
-
out.line(JSON.stringify(
|
|
18188
|
-
|
|
18405
|
+
out.line(JSON.stringify({
|
|
18406
|
+
kind: "forgezero-attended-platform-bootstrap-plan",
|
|
18407
|
+
mutation: true,
|
|
18408
|
+
cloudflare: {
|
|
18409
|
+
checkpointFile: cloudflare.checkpointPath,
|
|
18410
|
+
nodes: cloudflare.coordinates.nodes.map(({ nodeName, hostname: hostname2 }) => ({ nodeName, hostname: hostname2 }))
|
|
18411
|
+
},
|
|
18412
|
+
fleet: plan2,
|
|
18413
|
+
acceptanceFile: fleet.cloudflareAcceptanceFile,
|
|
18414
|
+
completion: ["public ingress verified", "initial custodian invitation printed"]
|
|
18415
|
+
}, null, 2));
|
|
18416
|
+
out.step("Review the complete Tunnel, hostname, bundle, credential and three-node plan, then repeat with --apply.");
|
|
18189
18417
|
return 0;
|
|
18190
18418
|
}
|
|
18191
18419
|
const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
|
|
18192
18420
|
const firstConfig = readBootstrapConfig(first.platformConfigFile);
|
|
18193
18421
|
if (firstConfig.kind !== "platform")
|
|
18194
18422
|
throw new Error("remote platform fleet requires platform configs");
|
|
18423
|
+
if (mode === "status") {
|
|
18424
|
+
const result3 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
|
|
18425
|
+
out.line(JSON.stringify(result3, null, 2));
|
|
18426
|
+
return result3.ok ? 0 : 1;
|
|
18427
|
+
}
|
|
18195
18428
|
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
|
|
18196
|
-
|
|
18429
|
+
if (!secrets2?.cloudflareTunnelToken || !secrets2.cloudflareApiToken) {
|
|
18430
|
+
throw new Error("attended platform bootstrap requires both Cloudflare tokens");
|
|
18431
|
+
}
|
|
18432
|
+
const result2 = await applyAttendedPlatformBootstrap(fleet, cloudflare, fleet.cloudflareAcceptanceFile, secrets2);
|
|
18197
18433
|
out.line(JSON.stringify(result2, null, 2));
|
|
18198
|
-
|
|
18434
|
+
if (!result2.ok)
|
|
18435
|
+
return 1;
|
|
18436
|
+
out.line();
|
|
18437
|
+
out.step(`Initial custodian invitation: ${result2.inviteUrl}`);
|
|
18438
|
+
return 0;
|
|
18199
18439
|
}
|
|
18200
18440
|
if (operation === "platform" && args[1] === "remote") {
|
|
18201
18441
|
const mode = args[2];
|
|
@@ -18232,11 +18472,14 @@ async function cmdBootstrap(options, args) {
|
|
|
18232
18472
|
}
|
|
18233
18473
|
if (operation === "metal" && args[1] === "remote") {
|
|
18234
18474
|
const mode = args[2];
|
|
18235
|
-
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "status"].includes(mode) || args[3] !== undefined) {
|
|
18236
|
-
throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
18475
|
+
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "host-keys", "rehearsal-host-keys", "status"].includes(mode) || args[3] !== undefined) {
|
|
18476
|
+
throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status> --bootstrap-config <owner-only-request.json> [--output <owner-only-evidence.json>] [--apply]");
|
|
18237
18477
|
}
|
|
18238
18478
|
if (!options.bootstrapConfigPath)
|
|
18239
18479
|
throw new Error("remote metal bootstrap requires --bootstrap-config");
|
|
18480
|
+
if ((mode === "host-keys" || mode === "rehearsal-host-keys") && !options.outputPath) {
|
|
18481
|
+
throw new Error(`${mode} requires --output <absolute-owner-only-evidence.json>`);
|
|
18482
|
+
}
|
|
18240
18483
|
const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath);
|
|
18241
18484
|
const plan2 = planOperatorMetalBootstrap(request, mode);
|
|
18242
18485
|
if (!options.apply) {
|
|
@@ -18244,7 +18487,15 @@ async function cmdBootstrap(options, args) {
|
|
|
18244
18487
|
out.step("Review the pinned metal target and exact genesis nodes, then repeat with --apply to contact the host.");
|
|
18245
18488
|
return 0;
|
|
18246
18489
|
}
|
|
18247
|
-
|
|
18490
|
+
const result2 = await applyOperatorMetalBootstrap(request, mode);
|
|
18491
|
+
if (mode === "host-keys" || mode === "rehearsal-host-keys") {
|
|
18492
|
+
const evidence = JSON.parse(result2.output);
|
|
18493
|
+
const path = writeOperatorGuestHostKeyEvidence(options.outputPath, evidence, request);
|
|
18494
|
+
readOperatorGuestHostKeyEvidence(path, request);
|
|
18495
|
+
out.line(JSON.stringify({ plan: result2.plan, evidencePath: path, mode: "0600" }, null, 2));
|
|
18496
|
+
return 0;
|
|
18497
|
+
}
|
|
18498
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
18248
18499
|
return 0;
|
|
18249
18500
|
}
|
|
18250
18501
|
if (operation === "platform" && args[1] === "cloudflare") {
|
|
@@ -18332,7 +18583,7 @@ async function cmdBootstrap(options, args) {
|
|
|
18332
18583
|
return 0;
|
|
18333
18584
|
}
|
|
18334
18585
|
if (!["platform", "repair"].includes(operation)) {
|
|
18335
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
18586
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
18336
18587
|
}
|
|
18337
18588
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
18338
18589
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -18911,14 +19162,16 @@ function usage() {
|
|
|
18911
19162
|
Run typed bootstrap from the operator laptop through
|
|
18912
19163
|
a pinned host; apply copies and verifies that bundle
|
|
18913
19164
|
fz bootstrap platform fleet <apply|status>
|
|
18914
|
-
|
|
18915
|
-
|
|
19165
|
+
One attended genesis operation: reconcile public
|
|
19166
|
+
hostnames/Tunnels, seal credentials, deploy the bundle,
|
|
19167
|
+
run all three nodes, verify ingress and print the invite
|
|
18916
19168
|
fz bootstrap platform Install/repair a typed elastic platform compute
|
|
18917
19169
|
fz bootstrap config <metal|platform|cloudflare>
|
|
18918
19170
|
Write validated owner-only metal config+remote request,
|
|
18919
|
-
Cloudflare file, or complete three-node
|
|
19171
|
+
low-level Cloudflare file, or the complete three-node
|
|
19172
|
+
platform + Cloudflare request set
|
|
18920
19173
|
fz bootstrap platform cloudflare
|
|
18921
|
-
|
|
19174
|
+
Low-level resume/diagnostic Tunnel and DNS reconciliation
|
|
18922
19175
|
using hidden management and KV/Worker runtime token prompts
|
|
18923
19176
|
fz bootstrap platform cloudflare verify
|
|
18924
19177
|
From the operator laptop, prove every ordinary public
|
|
@@ -18927,7 +19180,7 @@ function usage() {
|
|
|
18927
19180
|
Persist secret-free public-node acceptance, then remove
|
|
18928
19181
|
the completed non-secret checkpoint and node handoffs
|
|
18929
19182
|
fz bootstrap metal Plan/install an identity-only physical provisioner
|
|
18930
|
-
fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status>
|
|
19183
|
+
fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>
|
|
18931
19184
|
Install/bootstrap one pinned blank metal target, then
|
|
18932
19185
|
provision the exact selected three-node genesis seed
|
|
18933
19186
|
fz bootstrap status Verify persisted profile and supervised units
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -354,7 +354,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// src/version.ts
|
|
357
|
-
var VERSION = "0.1.
|
|
357
|
+
var VERSION = "0.1.69";
|
|
358
358
|
|
|
359
359
|
// src/otel-collector.ts
|
|
360
360
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -26,9 +26,24 @@ export interface OperatorMetalBootstrapRequest {
|
|
|
26
26
|
sshPublicKeyFiles: string[];
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
export interface OperatorGuestHostKeyEvidence {
|
|
30
|
+
format: 1;
|
|
31
|
+
kind: 'forgezero-operator-guest-host-keys';
|
|
32
|
+
metalHostKeySha256: string;
|
|
33
|
+
nodes: readonly {
|
|
34
|
+
name: string;
|
|
35
|
+
address: string;
|
|
36
|
+
hostKey: string;
|
|
37
|
+
hostKeySha256: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
29
40
|
export interface OperatorPlatformBootstrapFleetRequest {
|
|
30
41
|
kind: 'platform-fleet-remote';
|
|
31
42
|
requests: readonly [string, string, string];
|
|
43
|
+
/** Reviewed non-secret coordinates for the attended Tunnel/DNS/KV/DO phase. */
|
|
44
|
+
cloudflareConfigFile: string;
|
|
45
|
+
/** Secret-free evidence retained after checkpoint and host handoffs are consumed. */
|
|
46
|
+
cloudflareAcceptanceFile: string;
|
|
32
47
|
}
|
|
33
48
|
export interface OperatorPlatformBootstrapFleetPlan {
|
|
34
49
|
kind: 'platform-fleet-remote';
|
|
@@ -66,7 +81,7 @@ export interface OperatorPlatformFleetCoordinates {
|
|
|
66
81
|
}[];
|
|
67
82
|
}
|
|
68
83
|
export type OperatorPlatformBootstrapMode = 'apply' | 'status';
|
|
69
|
-
export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'rehearsal' | 'rehearsal-cleanup' | 'status';
|
|
84
|
+
export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'rehearsal' | 'rehearsal-cleanup' | 'host-keys' | 'rehearsal-host-keys' | 'status';
|
|
70
85
|
export interface OperatorPlatformBootstrapPlan {
|
|
71
86
|
kind: 'platform-remote';
|
|
72
87
|
mode: OperatorPlatformBootstrapMode;
|
|
@@ -112,14 +127,33 @@ export declare function createOperatorSshHop(input: Omit<OperatorSshHop, 'hostKe
|
|
|
112
127
|
export declare function readOperatorPlatformBootstrapRequest(path: string): OperatorPlatformBootstrapRequest;
|
|
113
128
|
export declare function writeOperatorPlatformBootstrapRequest(path: string, request: OperatorPlatformBootstrapRequest): string;
|
|
114
129
|
export declare function readOperatorPlatformBootstrapFleetRequest(path: string): OperatorPlatformBootstrapFleetRequest;
|
|
115
|
-
export declare function writeOperatorPlatformBootstrapFleetRequest(path: string, requestFiles: readonly [string, string, string]
|
|
130
|
+
export declare function writeOperatorPlatformBootstrapFleetRequest(path: string, requestFiles: readonly [string, string, string], cloudflare: {
|
|
131
|
+
configFile: string;
|
|
132
|
+
acceptanceFile: string;
|
|
133
|
+
}): string;
|
|
116
134
|
export declare function readOperatorMetalBootstrapRequest(path: string): OperatorMetalBootstrapRequest;
|
|
117
135
|
export declare function writeOperatorMetalBootstrapRequest(path: string, request: OperatorMetalBootstrapRequest): string;
|
|
136
|
+
export declare function readOperatorGuestHostKeyEvidence(path: string, request?: OperatorMetalBootstrapRequest): OperatorGuestHostKeyEvidence;
|
|
137
|
+
export declare function writeOperatorGuestHostKeyEvidence(path: string, evidence: OperatorGuestHostKeyEvidence, request?: OperatorMetalBootstrapRequest): string;
|
|
118
138
|
export declare function planOperatorMetalBootstrap(request: OperatorMetalBootstrapRequest, mode: OperatorMetalBootstrapMode): OperatorMetalBootstrapPlan;
|
|
119
139
|
export declare function planOperatorPlatformBootstrap(request: OperatorPlatformBootstrapRequest, mode: OperatorPlatformBootstrapMode): OperatorPlatformBootstrapPlan;
|
|
120
140
|
/** Exact non-secret coordinates derived from the same reviewed files bootstrap applies. */
|
|
121
141
|
export declare function operatorPlatformFleetCoordinates(fleet: OperatorPlatformBootstrapFleetRequest): OperatorPlatformFleetCoordinates;
|
|
122
142
|
export declare function planOperatorPlatformFleetBootstrap(fleet: OperatorPlatformBootstrapFleetRequest, mode: OperatorPlatformBootstrapMode): OperatorPlatformBootstrapFleetPlan;
|
|
143
|
+
export declare function collectOperatorGuestHostKeys(request: OperatorMetalBootstrapRequest, options?: OperatorPlatformBootstrapOptions, includeRehearsal?: boolean): Promise<OperatorGuestHostKeyEvidence>;
|
|
144
|
+
export interface OperatorGuestTransport {
|
|
145
|
+
run(nodeName: string, argv: readonly string[], options?: {
|
|
146
|
+
stdin?: string;
|
|
147
|
+
secret?: boolean;
|
|
148
|
+
allowFailure?: boolean;
|
|
149
|
+
}): Promise<OperatorCommandResult>;
|
|
150
|
+
copy(nodeName: string, localPath: string, remotePath: string): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Execute a bounded callback through the same pinned Metal jump, operator
|
|
154
|
+
* public identity selector and reviewed guest host keys used by bootstrap.
|
|
155
|
+
*/
|
|
156
|
+
export declare function withOperatorGuestTransport<T>(request: OperatorMetalBootstrapRequest, evidence: OperatorGuestHostKeyEvidence, callback: (transport: OperatorGuestTransport) => Promise<T>, options?: OperatorPlatformBootstrapOptions): Promise<T>;
|
|
123
157
|
/**
|
|
124
158
|
* Run one reviewed operator action. No private SSH key, credential value or
|
|
125
159
|
* management token is placed in argv; OpenSSH selects the matching private key
|
|
@@ -1396,7 +1396,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1396
1396
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1397
1397
|
|
|
1398
1398
|
// src/version.ts
|
|
1399
|
-
var VERSION = "0.1.
|
|
1399
|
+
var VERSION = "0.1.69";
|
|
1400
1400
|
|
|
1401
1401
|
// src/software.ts
|
|
1402
1402
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4380,6 +4380,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4380
4380
|
}
|
|
4381
4381
|
bindBootstrapIntent(host, config);
|
|
4382
4382
|
const plan = planBootstrap(config, host.exists(STATE_PATH));
|
|
4383
|
+
await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
|
|
4383
4384
|
const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
4384
4385
|
host.mkdir(CREDS, 448);
|
|
4385
4386
|
host.mkdir("/var/lib/forgezero", 448);
|
|
@@ -5971,7 +5972,12 @@ function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
|
5971
5972
|
}
|
|
5972
5973
|
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
5973
5974
|
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
5974
|
-
const root = exactKeys3(value, [
|
|
5975
|
+
const root = exactKeys3(value, [
|
|
5976
|
+
"kind",
|
|
5977
|
+
"requests",
|
|
5978
|
+
"cloudflareConfigFile",
|
|
5979
|
+
"cloudflareAcceptanceFile"
|
|
5980
|
+
], "operator fleet request");
|
|
5975
5981
|
if (root.kind !== "platform-fleet-remote")
|
|
5976
5982
|
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
5977
5983
|
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
@@ -5982,10 +5988,25 @@ function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
|
5982
5988
|
throw new Error("operator fleet request files must be unique");
|
|
5983
5989
|
for (const request of requests)
|
|
5984
5990
|
readOperatorPlatformBootstrapRequest(request);
|
|
5985
|
-
|
|
5991
|
+
if (typeof root.cloudflareConfigFile !== "string" || typeof root.cloudflareAcceptanceFile !== "string") {
|
|
5992
|
+
throw new Error("operator fleet requires Cloudflare coordinates and an acceptance evidence path");
|
|
5993
|
+
}
|
|
5994
|
+
const cloudflareConfigFile = canonicalOutputPath(root.cloudflareConfigFile, "operator fleet Cloudflare config");
|
|
5995
|
+
const cloudflareAcceptanceFile = canonicalOutputPath(root.cloudflareAcceptanceFile, "operator fleet Cloudflare acceptance evidence");
|
|
5996
|
+
return {
|
|
5997
|
+
kind: "platform-fleet-remote",
|
|
5998
|
+
requests,
|
|
5999
|
+
cloudflareConfigFile,
|
|
6000
|
+
cloudflareAcceptanceFile
|
|
6001
|
+
};
|
|
5986
6002
|
}
|
|
5987
|
-
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
5988
|
-
const request = {
|
|
6003
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudflare) {
|
|
6004
|
+
const request = {
|
|
6005
|
+
kind: "platform-fleet-remote",
|
|
6006
|
+
requests: requestFiles,
|
|
6007
|
+
cloudflareConfigFile: canonicalOutputPath(cloudflare.configFile, "operator fleet Cloudflare config"),
|
|
6008
|
+
cloudflareAcceptanceFile: canonicalOutputPath(cloudflare.acceptanceFile, "operator fleet Cloudflare acceptance evidence")
|
|
6009
|
+
};
|
|
5989
6010
|
if (new Set(requestFiles).size !== 3)
|
|
5990
6011
|
throw new Error("operator fleet request files must be unique");
|
|
5991
6012
|
for (const file of requestFiles)
|
|
@@ -6079,6 +6100,48 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
6079
6100
|
validateMetalRequestValue(request);
|
|
6080
6101
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
6081
6102
|
}
|
|
6103
|
+
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
6104
|
+
const root = exactKeys3(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
6105
|
+
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
6106
|
+
throw new Error("operator guest host-key evidence is malformed");
|
|
6107
|
+
}
|
|
6108
|
+
const nodes = root.nodes.map((value2, index) => {
|
|
6109
|
+
const row = exactKeys3(value2, ["name", "address", "hostKey", "hostKeySha256"], `operator guest host key ${index + 1}`);
|
|
6110
|
+
if (typeof row.name !== "string" || typeof row.address !== "string") {
|
|
6111
|
+
throw new Error("operator guest host-key identity is malformed");
|
|
6112
|
+
}
|
|
6113
|
+
const hop = validateHop({
|
|
6114
|
+
address: row.address,
|
|
6115
|
+
port: 22,
|
|
6116
|
+
user: "forgezero",
|
|
6117
|
+
hostKey: row.hostKey,
|
|
6118
|
+
hostKeySha256: row.hostKeySha256
|
|
6119
|
+
}, `operator guest host key ${row.name}`);
|
|
6120
|
+
return { name: row.name, address: hop.address, hostKey: hop.hostKey, hostKeySha256: hop.hostKeySha256 };
|
|
6121
|
+
});
|
|
6122
|
+
if (new Set(nodes.map(({ name }) => name)).size !== nodes.length || new Set(nodes.map(({ address }) => address)).size !== nodes.length) {
|
|
6123
|
+
throw new Error("operator guest host-key identities must be unique");
|
|
6124
|
+
}
|
|
6125
|
+
if (request) {
|
|
6126
|
+
const expected = nodes.length === request.genesis.nodes.length ? [...request.genesis.nodes] : [...request.genesis.nodes, ...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []];
|
|
6127
|
+
if (root.metalHostKeySha256 !== request.target.hostKeySha256 || ![request.genesis.nodes.length, request.genesis.nodes.length + (request.genesis.rehearsalNode ? 1 : 0)].includes(nodes.length) || nodes.length !== expected.length || expected.some((guest, index) => nodes[index]?.name !== guest.name || nodes[index]?.address !== guest.address)) {
|
|
6128
|
+
throw new Error("operator guest host-key evidence does not match the reviewed Metal request");
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
6131
|
+
return {
|
|
6132
|
+
format: 1,
|
|
6133
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
6134
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
6135
|
+
nodes
|
|
6136
|
+
};
|
|
6137
|
+
};
|
|
6138
|
+
function readOperatorGuestHostKeyEvidence(path, request) {
|
|
6139
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator guest host-key evidence")));
|
|
6140
|
+
return validateGuestHostKeyEvidenceValue(value, request);
|
|
6141
|
+
}
|
|
6142
|
+
function writeOperatorGuestHostKeyEvidence(path, evidence, request) {
|
|
6143
|
+
return writeOwnerJson2(path, validateGuestHostKeyEvidenceValue(evidence, request), "operator guest host-key evidence");
|
|
6144
|
+
}
|
|
6082
6145
|
function planOperatorMetalBootstrap(request, mode) {
|
|
6083
6146
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
6084
6147
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
@@ -6086,19 +6149,20 @@ function planOperatorMetalBootstrap(request, mode) {
|
|
|
6086
6149
|
throw new Error("the reviewed metal request has no rehearsal node");
|
|
6087
6150
|
}
|
|
6088
6151
|
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(request.genesis.nodes, request.genesis.rehearsalNode) : platformCommunityRehearsalClaims(request.genesis.nodes, request.genesis.rehearsalNode, keys) : platformGenesisClaims(request.genesis.nodes, keys);
|
|
6152
|
+
const genesisNodes = mode === "host-keys" ? request.genesis.nodes.map(({ name }) => name) : mode === "rehearsal-host-keys" ? guestRows(request).map(({ name }) => name) : claims.map((claim) => claim.spec.guestName).filter((name) => Boolean(name));
|
|
6089
6153
|
return {
|
|
6090
6154
|
kind: "metal-remote",
|
|
6091
6155
|
mode,
|
|
6092
|
-
mutation:
|
|
6156
|
+
mutation: !["status", "host-keys", "rehearsal-host-keys"].includes(mode),
|
|
6093
6157
|
target: { address: request.target.address, port: request.target.port, user: request.target.user },
|
|
6094
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the three reviewed seed claims plus the reviewed fourth Community rehearsal claim to the constrained Metal helper" : mode === "rehearsal-cleanup" ? "delete the exact four reviewed disposable guests and their owned disks, units, seed data and inventory" : "send the three reviewed platform-genesis claims to the constrained Metal helper"] : [
|
|
6158
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "host-keys" || mode === "rehearsal-host-keys" ? ["verify pinned SSH transport", "collect exact Ed25519 host keys for the reviewed guest addresses"] : mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the three reviewed seed claims plus the reviewed fourth Community rehearsal claim to the constrained Metal helper" : mode === "rehearsal-cleanup" ? "delete the exact four reviewed disposable guests and their owned disks, units, seed data and inventory" : "send the three reviewed platform-genesis claims to the constrained Metal helper"] : [
|
|
6095
6159
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6096
6160
|
"copy pinned Bun and packaged fz artifacts to private staging",
|
|
6097
6161
|
"stage the strict owner-only metal config and optional seed handoff",
|
|
6098
6162
|
"run typed fz bootstrap metal --apply",
|
|
6099
6163
|
"remove transient local and remote staging data"
|
|
6100
6164
|
],
|
|
6101
|
-
genesisNodes
|
|
6165
|
+
genesisNodes,
|
|
6102
6166
|
secretInputs: config.agentSeedFile ? ["metal-agent-seed"] : []
|
|
6103
6167
|
};
|
|
6104
6168
|
}
|
|
@@ -6335,6 +6399,116 @@ var copy = async (exec, request, knownHosts, local, remotePath, secret = false)
|
|
|
6335
6399
|
`${destination2(request.target)}:${remotePath}`
|
|
6336
6400
|
], "secure copy", { secret });
|
|
6337
6401
|
};
|
|
6402
|
+
var guestRows = (request) => [
|
|
6403
|
+
...request.genesis.nodes,
|
|
6404
|
+
...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []
|
|
6405
|
+
];
|
|
6406
|
+
var parsedScannedHostKey = (output, address) => {
|
|
6407
|
+
const keys = output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith(`${address} ssh-ed25519 `)).map((line) => line.split(/\s+/).slice(1, 3).join(" "));
|
|
6408
|
+
const unique = [...new Set(keys)];
|
|
6409
|
+
if (unique.length !== 1 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}$/.test(unique[0])) {
|
|
6410
|
+
throw new Error(`guest ${address} did not return one exact Ed25519 host key`);
|
|
6411
|
+
}
|
|
6412
|
+
return unique[0];
|
|
6413
|
+
};
|
|
6414
|
+
async function collectOperatorGuestHostKeys(request, options = {}, includeRehearsal = false) {
|
|
6415
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
6416
|
+
socketPath(request.target.agentSocket);
|
|
6417
|
+
const exec = options.exec ?? defaultExec3;
|
|
6418
|
+
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-host-keys-"));
|
|
6419
|
+
try {
|
|
6420
|
+
const knownHosts = writeKnownHosts(request, directory);
|
|
6421
|
+
const nodes = [];
|
|
6422
|
+
for (const guest of includeRehearsal ? guestRows(request) : request.genesis.nodes) {
|
|
6423
|
+
const output = await remote(exec, request, knownHosts, [
|
|
6424
|
+
"/usr/bin/ssh-keyscan",
|
|
6425
|
+
"-T",
|
|
6426
|
+
"10",
|
|
6427
|
+
"-t",
|
|
6428
|
+
"ed25519",
|
|
6429
|
+
guest.address
|
|
6430
|
+
], `collect ${guest.name} SSH host key`);
|
|
6431
|
+
const hostKey = parsedScannedHostKey(output, guest.address);
|
|
6432
|
+
nodes.push({
|
|
6433
|
+
name: guest.name,
|
|
6434
|
+
address: guest.address,
|
|
6435
|
+
hostKey,
|
|
6436
|
+
hostKeySha256: fingerprint(hostKey)
|
|
6437
|
+
});
|
|
6438
|
+
}
|
|
6439
|
+
return validateGuestHostKeyEvidenceValue({
|
|
6440
|
+
format: 1,
|
|
6441
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
6442
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
6443
|
+
nodes
|
|
6444
|
+
}, request);
|
|
6445
|
+
} finally {
|
|
6446
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
async function withOperatorGuestTransport(request, evidence, callback, options = {}) {
|
|
6450
|
+
const reviewed = validateGuestHostKeyEvidenceValue(evidence, request);
|
|
6451
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
6452
|
+
socketPath(request.target.agentSocket);
|
|
6453
|
+
const exec = options.exec ?? defaultExec3;
|
|
6454
|
+
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-guest-transport-"));
|
|
6455
|
+
try {
|
|
6456
|
+
const byName = new Map(reviewed.nodes.map((node) => [node.name, node]));
|
|
6457
|
+
const requests = new Map;
|
|
6458
|
+
const knownHosts = new Map;
|
|
6459
|
+
for (const node of reviewed.nodes) {
|
|
6460
|
+
const nodeDirectory = join6(directory, node.name);
|
|
6461
|
+
mkdirSync8(nodeDirectory, { mode: 448 });
|
|
6462
|
+
const guestRequest = {
|
|
6463
|
+
target: {
|
|
6464
|
+
address: node.address,
|
|
6465
|
+
port: 22,
|
|
6466
|
+
user: "forgezero",
|
|
6467
|
+
hostKey: node.hostKey,
|
|
6468
|
+
hostKeySha256: node.hostKeySha256,
|
|
6469
|
+
identityPublicKeyFile: request.target.identityPublicKeyFile,
|
|
6470
|
+
agentSocket: request.target.agentSocket,
|
|
6471
|
+
jump: {
|
|
6472
|
+
address: request.target.address,
|
|
6473
|
+
port: request.target.port,
|
|
6474
|
+
user: request.target.user,
|
|
6475
|
+
hostKey: request.target.hostKey,
|
|
6476
|
+
hostKeySha256: request.target.hostKeySha256
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
};
|
|
6480
|
+
requests.set(node.name, guestRequest);
|
|
6481
|
+
knownHosts.set(node.name, writeKnownHosts(guestRequest, nodeDirectory));
|
|
6482
|
+
}
|
|
6483
|
+
const transport = {
|
|
6484
|
+
async run(nodeName, argv, runOptions = {}) {
|
|
6485
|
+
if (!byName.has(nodeName))
|
|
6486
|
+
throw new Error(`guest transport node ${nodeName} is not reviewed`);
|
|
6487
|
+
argv.forEach(safeRemoteArg);
|
|
6488
|
+
const nodeRequest = requests.get(nodeName);
|
|
6489
|
+
const result = await exec([
|
|
6490
|
+
"ssh",
|
|
6491
|
+
...sshOptions(nodeRequest, knownHosts.get(nodeName)),
|
|
6492
|
+
destination2(nodeRequest.target),
|
|
6493
|
+
"--",
|
|
6494
|
+
...argv
|
|
6495
|
+
], { stdin: runOptions.stdin, secret: runOptions.secret });
|
|
6496
|
+
if (result.exitCode !== 0 && !runOptions.allowFailure) {
|
|
6497
|
+
throw new Error(`${nodeName} remote operation failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
6498
|
+
}
|
|
6499
|
+
return result;
|
|
6500
|
+
},
|
|
6501
|
+
async copy(nodeName, localPath, remotePath) {
|
|
6502
|
+
if (!byName.has(nodeName))
|
|
6503
|
+
throw new Error(`guest transport node ${nodeName} is not reviewed`);
|
|
6504
|
+
await copy(exec, requests.get(nodeName), knownHosts.get(nodeName), localPath, remotePath);
|
|
6505
|
+
}
|
|
6506
|
+
};
|
|
6507
|
+
return await callback(transport);
|
|
6508
|
+
} finally {
|
|
6509
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
6510
|
+
}
|
|
6511
|
+
}
|
|
6338
6512
|
async function stageConfig(config, directory) {
|
|
6339
6513
|
const rewritten = structuredClone(config);
|
|
6340
6514
|
const staged = [];
|
|
@@ -6546,6 +6720,12 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
6546
6720
|
const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
|
|
6547
6721
|
return { plan, output: output2 };
|
|
6548
6722
|
}
|
|
6723
|
+
if (mode === "host-keys" || mode === "rehearsal-host-keys") {
|
|
6724
|
+
return {
|
|
6725
|
+
plan,
|
|
6726
|
+
output: JSON.stringify(await collectOperatorGuestHostKeys(request, { ...options, exec }, mode === "rehearsal-host-keys"))
|
|
6727
|
+
};
|
|
6728
|
+
}
|
|
6549
6729
|
if (mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup") {
|
|
6550
6730
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
6551
6731
|
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
@@ -6592,15 +6772,19 @@ export {
|
|
|
6592
6772
|
writeOperatorPlatformBootstrapRequest,
|
|
6593
6773
|
writeOperatorPlatformBootstrapFleetRequest,
|
|
6594
6774
|
writeOperatorMetalBootstrapRequest,
|
|
6775
|
+
writeOperatorGuestHostKeyEvidence,
|
|
6776
|
+
withOperatorGuestTransport,
|
|
6595
6777
|
verifyOperatorPlatformFleet,
|
|
6596
6778
|
readOperatorPlatformBootstrapRequest,
|
|
6597
6779
|
readOperatorPlatformBootstrapFleetRequest,
|
|
6598
6780
|
readOperatorMetalBootstrapRequest,
|
|
6781
|
+
readOperatorGuestHostKeyEvidence,
|
|
6599
6782
|
planOperatorPlatformFleetBootstrap,
|
|
6600
6783
|
planOperatorPlatformBootstrap,
|
|
6601
6784
|
planOperatorMetalBootstrap,
|
|
6602
6785
|
operatorPlatformFleetCoordinates,
|
|
6603
6786
|
createOperatorSshHop,
|
|
6787
|
+
collectOperatorGuestHostKeys,
|
|
6604
6788
|
applyOperatorPlatformFleetBootstrap,
|
|
6605
6789
|
applyOperatorPlatformBootstrap,
|
|
6606
6790
|
applyOperatorMetalBootstrap
|
|
@@ -2125,7 +2125,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2125
2125
|
}
|
|
2126
2126
|
|
|
2127
2127
|
// src/version.ts
|
|
2128
|
-
var VERSION3 = "0.1.
|
|
2128
|
+
var VERSION3 = "0.1.69";
|
|
2129
2129
|
|
|
2130
2130
|
// src/egress-policy.ts
|
|
2131
2131
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/provision.js
CHANGED
|
@@ -2125,7 +2125,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2125
2125
|
}
|
|
2126
2126
|
|
|
2127
2127
|
// src/version.ts
|
|
2128
|
-
var VERSION3 = "0.1.
|
|
2128
|
+
var VERSION3 = "0.1.69";
|
|
2129
2129
|
|
|
2130
2130
|
// src/egress-policy.ts
|
|
2131
2131
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.69";
|