@forgezero/agent 0.1.65 → 0.1.68
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 +751 -157
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +1 -0
- package/dist/bootstrap-bundle.js +4 -3
- package/dist/bootstrap.d.ts +7 -1
- package/dist/bootstrap.js +264 -13
- package/dist/community-rehearsal-host.d.ts +12 -0
- package/dist/community-rehearsal-host.js +99 -36
- package/dist/definition.js +41 -4
- package/dist/deploy-compiler.js +4 -4
- package/dist/deploy-file.js +41 -4
- package/dist/deploy-plan-runner.js +4 -4
- package/dist/deploy-plan.js +4 -4
- package/dist/fz-agent.js +165 -76
- package/dist/fz.js +619 -262
- package/dist/index.d.ts +1 -1
- package/dist/metal-bootstrap.js +120 -46
- package/dist/metal-helper-socket.js +406 -65
- package/dist/metal-provision.js +402 -61
- package/dist/operator-bootstrap.d.ts +48 -5
- package/dist/operator-bootstrap.js +594 -205
- package/dist/platform-bootstrap-runtime.d.ts +33 -0
- package/dist/platform-bootstrap-runtime.js +551 -44
- package/dist/platform-fleet-verification.js +242 -18
- package/dist/platform-genesis-config.d.ts +7 -1
- package/dist/platform-genesis.d.ts +16 -17
- package/dist/platform-genesis.js +464 -0
- package/dist/provision.js +42 -5
- package/dist/software-helper.js +41 -4
- package/dist/software.d.ts +8 -2
- package/dist/software.js +41 -4
- package/dist/ssh-bootstrap.d.ts +5 -0
- package/dist/ubuntu.d.ts +15 -14
- package/dist/ubuntu.js +345 -9
- package/dist/version.d.ts +1 -1
- package/package.json +8 -4
package/dist/fz.js
CHANGED
|
@@ -4634,7 +4634,7 @@ var VaultError, runtimeEnvironment = () => typeof process !== "undefined" && pro
|
|
|
4634
4634
|
} catch {
|
|
4635
4635
|
return;
|
|
4636
4636
|
}
|
|
4637
|
-
}, VERSION = "0.1.
|
|
4637
|
+
}, VERSION = "0.1.13";
|
|
4638
4638
|
var init_dist = __esm(() => {
|
|
4639
4639
|
init_identity();
|
|
4640
4640
|
VaultError = class VaultError extends Error {
|
|
@@ -4829,13 +4829,36 @@ 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.68";
|
|
4833
4833
|
|
|
4834
4834
|
// src/software.ts
|
|
4835
4835
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
4836
4836
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
4837
4837
|
var OS_CATALOG = [
|
|
4838
|
-
{
|
|
4838
|
+
{
|
|
4839
|
+
id: "ubuntu",
|
|
4840
|
+
version: "26.04",
|
|
4841
|
+
architecture: "x64",
|
|
4842
|
+
status: "active",
|
|
4843
|
+
imageKey: "ubuntu-resolute-20260731",
|
|
4844
|
+
imageLabel: "Ubuntu 26.04 LTS Resolute",
|
|
4845
|
+
imageVersion: "2026-07-31",
|
|
4846
|
+
imageUrl: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
|
|
4847
|
+
imageSha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05",
|
|
4848
|
+
confidentialModes: ["sev-snp"]
|
|
4849
|
+
},
|
|
4850
|
+
{
|
|
4851
|
+
id: "ubuntu",
|
|
4852
|
+
version: "24.04",
|
|
4853
|
+
architecture: "x64",
|
|
4854
|
+
status: "active",
|
|
4855
|
+
imageKey: "ubuntu-noble-20260705",
|
|
4856
|
+
imageLabel: "Ubuntu 24.04 LTS Noble",
|
|
4857
|
+
imageVersion: "2026-07-05",
|
|
4858
|
+
imageUrl: "https://cloud-images.ubuntu.com/releases/noble/release-20260705/ubuntu-24.04-server-cloudimg-amd64.img",
|
|
4859
|
+
imageSha256: "ffe6203da54deeb6db5d2a98a83f9ec8e55f149d3f7ba622e1abe5fa966ee3d6",
|
|
4860
|
+
confidentialModes: []
|
|
4861
|
+
}
|
|
4839
4862
|
];
|
|
4840
4863
|
var SOFTWARE_CATALOG = [
|
|
4841
4864
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -4846,7 +4869,12 @@ var SOFTWARE_CATALOG = [
|
|
|
4846
4869
|
{ id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4847
4870
|
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4848
4871
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4849
|
-
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4872
|
+
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4873
|
+
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4874
|
+
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4875
|
+
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4876
|
+
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4877
|
+
{ id: "git", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4850
4878
|
];
|
|
4851
4879
|
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
4852
4880
|
"data-root": "/var/lib/docker",
|
|
@@ -10425,13 +10453,13 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
10425
10453
|
return;
|
|
10426
10454
|
const expected = {
|
|
10427
10455
|
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
10428
|
-
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
10429
|
-
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
10456
|
+
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
10457
|
+
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
10430
10458
|
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
10431
10459
|
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
10432
10460
|
"forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
|
|
10433
|
-
"forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
|
|
10434
|
-
"forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
|
|
10461
|
+
"forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
|
|
10462
|
+
"forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "mode"] },
|
|
10435
10463
|
"forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
|
|
10436
10464
|
};
|
|
10437
10465
|
const contract = expected[requirement2.provider];
|
|
@@ -11587,6 +11615,143 @@ import {
|
|
|
11587
11615
|
import { dirname as dirname9 } from "path";
|
|
11588
11616
|
import { fileURLToPath } from "url";
|
|
11589
11617
|
|
|
11618
|
+
// src/ubuntu.ts
|
|
11619
|
+
var current = OS_CATALOG[0];
|
|
11620
|
+
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
11621
|
+
key: current.imageKey,
|
|
11622
|
+
family: `${current.id}-${current.version}`,
|
|
11623
|
+
version: current.imageVersion,
|
|
11624
|
+
label: current.imageLabel,
|
|
11625
|
+
url: current.imageUrl,
|
|
11626
|
+
sha256: current.imageSha256
|
|
11627
|
+
});
|
|
11628
|
+
var SUPPORTED_GUEST_IMAGES = Object.freeze(OS_CATALOG.filter(({ status }) => status === "active").map(({ id: id2, imageKey, imageLabel, imageVersion, version, confidentialModes }) => ({
|
|
11629
|
+
key: imageKey,
|
|
11630
|
+
label: imageLabel,
|
|
11631
|
+
family: `${id2}-${version}`,
|
|
11632
|
+
version: imageVersion,
|
|
11633
|
+
confidential: confidentialModes.some((mode) => mode === "sev-snp")
|
|
11634
|
+
})));
|
|
11635
|
+
function assertSupportedGuestImage(imageKey, confidential = false) {
|
|
11636
|
+
const selected = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
|
|
11637
|
+
if (!selected) {
|
|
11638
|
+
throw new Error(`unsupported guest image ${imageKey}; supported images: ${SUPPORTED_GUEST_IMAGES.map(({ key }) => key).join(", ")}`);
|
|
11639
|
+
}
|
|
11640
|
+
if (confidential && !selected.confidentialModes.some((mode) => mode === "sev-snp")) {
|
|
11641
|
+
throw new Error(`guest image ${imageKey} is not approved for SEV-SNP`);
|
|
11642
|
+
}
|
|
11643
|
+
}
|
|
11644
|
+
|
|
11645
|
+
// src/platform-genesis.ts
|
|
11646
|
+
var SAFE_NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
11647
|
+
var SAFE_POOL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
|
|
11648
|
+
var PRIVATE_V4 = /^(?:10\.(?:\d{1,3}\.){2}\d{1,3}|192\.168\.(?:\d{1,3})\.(?:\d{1,3})|172\.(?:1[6-9]|2\d|3[01])\.(?:\d{1,3})\.(?:\d{1,3}))$/;
|
|
11649
|
+
var integer2 = (value, minimum, maximum, label) => {
|
|
11650
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
11651
|
+
throw new Error(`platform genesis ${label} is outside the supported range`);
|
|
11652
|
+
}
|
|
11653
|
+
return value;
|
|
11654
|
+
};
|
|
11655
|
+
function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
11656
|
+
if (!Array.isArray(guests) || guests.length !== expectedCount) {
|
|
11657
|
+
throw new Error(`platform genesis requires exactly ${expectedCount} reviewed guest requests`);
|
|
11658
|
+
}
|
|
11659
|
+
const names = new Set;
|
|
11660
|
+
const addresses = new Set;
|
|
11661
|
+
const pools = new Set;
|
|
11662
|
+
return guests.map((input) => {
|
|
11663
|
+
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
11664
|
+
throw new Error("platform genesis guest name is malformed");
|
|
11665
|
+
}
|
|
11666
|
+
if (!PRIVATE_V4.test(input.address))
|
|
11667
|
+
throw new Error("platform genesis guest address must be private IPv4");
|
|
11668
|
+
if (typeof input.confidential !== "boolean")
|
|
11669
|
+
throw new Error("platform genesis confidential requirement is missing");
|
|
11670
|
+
assertSupportedGuestImage(input.imageKey, input.confidential);
|
|
11671
|
+
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
11672
|
+
throw new Error("platform genesis CPU pool key is malformed");
|
|
11673
|
+
}
|
|
11674
|
+
if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
|
|
11675
|
+
throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
|
|
11676
|
+
}
|
|
11677
|
+
names.add(input.name);
|
|
11678
|
+
addresses.add(input.address);
|
|
11679
|
+
if (input.cpuPoolKey !== undefined)
|
|
11680
|
+
pools.add(input.cpuPoolKey);
|
|
11681
|
+
const physicalCores = integer2(input.physicalCores, 1, 256, "physical core count");
|
|
11682
|
+
const vcpu = integer2(input.vcpu, 1, 512, "vCPU count");
|
|
11683
|
+
if (vcpu < physicalCores)
|
|
11684
|
+
throw new Error("platform genesis vCPU count cannot be smaller than physical cores");
|
|
11685
|
+
const memoryGib = integer2(input.memoryGib, 1, 8192, "memory");
|
|
11686
|
+
const diskGib = integer2(input.diskGib, 8, 65536, "disk");
|
|
11687
|
+
const egressGuaranteedMbps = integer2(input.egressGuaranteedMbps, 0, 1e6, "guaranteed egress");
|
|
11688
|
+
const egressBurstMbps = integer2(input.egressBurstMbps, egressGuaranteedMbps, 1e6, "burst egress");
|
|
11689
|
+
return {
|
|
11690
|
+
name: input.name,
|
|
11691
|
+
address: input.address,
|
|
11692
|
+
imageKey: input.imageKey,
|
|
11693
|
+
...input.cpuPoolKey ? { cpuPoolKey: input.cpuPoolKey } : {},
|
|
11694
|
+
physicalCores,
|
|
11695
|
+
vcpu,
|
|
11696
|
+
memoryGib,
|
|
11697
|
+
diskGib,
|
|
11698
|
+
egressGuaranteedMbps,
|
|
11699
|
+
egressBurstMbps,
|
|
11700
|
+
confidential: input.confidential
|
|
11701
|
+
};
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
var publicKey = (value) => {
|
|
11705
|
+
const normalized = value.trim();
|
|
11706
|
+
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(normalized)) {
|
|
11707
|
+
throw new Error("platform genesis SSH public key is malformed");
|
|
11708
|
+
}
|
|
11709
|
+
return normalized;
|
|
11710
|
+
};
|
|
11711
|
+
var claimFor = (guest, keys, action) => {
|
|
11712
|
+
const common = {
|
|
11713
|
+
computeKey: `platform:${guest.name}`,
|
|
11714
|
+
claimToken: "offline-platform-genesis",
|
|
11715
|
+
claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
|
|
11716
|
+
attempt: 1,
|
|
11717
|
+
bootstrap: { kind: "platform-genesis" },
|
|
11718
|
+
spec: {
|
|
11719
|
+
reference: `platform:${guest.name}`,
|
|
11720
|
+
imageKey: guest.imageKey,
|
|
11721
|
+
guestName: guest.name,
|
|
11722
|
+
guestAddress: guest.address,
|
|
11723
|
+
...guest.cpuPoolKey ? { cpuPoolKey: guest.cpuPoolKey } : {},
|
|
11724
|
+
physicalCores: guest.physicalCores,
|
|
11725
|
+
vcpu: guest.vcpu,
|
|
11726
|
+
memoryGib: guest.memoryGib,
|
|
11727
|
+
diskGib: guest.diskGib,
|
|
11728
|
+
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
11729
|
+
egressBurstMbps: guest.egressBurstMbps,
|
|
11730
|
+
confidential: guest.confidential
|
|
11731
|
+
}
|
|
11732
|
+
};
|
|
11733
|
+
if (action === "delete")
|
|
11734
|
+
return { ...common, action: "delete" };
|
|
11735
|
+
return { ...common, action: "create", access: { sshUser: "forgezero", sshPublicKeys: [...keys] } };
|
|
11736
|
+
};
|
|
11737
|
+
function platformGenesisClaims(guests, sshPublicKeys) {
|
|
11738
|
+
const reviewed = validatePlatformGenesisGuests(guests);
|
|
11739
|
+
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
11740
|
+
if (keys.length === 0)
|
|
11741
|
+
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
11742
|
+
return reviewed.map((guest) => claimFor(guest, keys, "create"));
|
|
11743
|
+
}
|
|
11744
|
+
function platformCommunityRehearsalClaims(seedGuests, rehearsalGuest, sshPublicKeys) {
|
|
11745
|
+
const reviewed = validatePlatformGenesisGuests([...seedGuests, rehearsalGuest], 4);
|
|
11746
|
+
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
11747
|
+
if (keys.length === 0)
|
|
11748
|
+
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
11749
|
+
return reviewed.map((guest) => claimFor(guest, keys, "create"));
|
|
11750
|
+
}
|
|
11751
|
+
function platformCommunityRehearsalDeletionClaims(seedGuests, rehearsalGuest) {
|
|
11752
|
+
return validatePlatformGenesisGuests([...seedGuests, rehearsalGuest], 4).map((guest) => claimFor(guest, [], "delete"));
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11590
11755
|
// src/platform-bootstrap-runtime.ts
|
|
11591
11756
|
var safeAtom = (name, value) => {
|
|
11592
11757
|
if (!value || /[\0\r\n]/.test(value))
|
|
@@ -11618,6 +11783,50 @@ var httpsOrigin = (name, raw) => {
|
|
|
11618
11783
|
}
|
|
11619
11784
|
return value.origin;
|
|
11620
11785
|
};
|
|
11786
|
+
function validatePlatformInitialInventory(value) {
|
|
11787
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
|
|
11788
|
+
throw new Error("initial platform inventory coordinates are invalid");
|
|
11789
|
+
}
|
|
11790
|
+
safeAtom("initialInventory.region.label", value.region.label);
|
|
11791
|
+
if (value.region.city !== undefined)
|
|
11792
|
+
safeAtom("initialInventory.region.city", value.region.city);
|
|
11793
|
+
const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
|
|
11794
|
+
if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
|
|
11795
|
+
"name",
|
|
11796
|
+
"address",
|
|
11797
|
+
"imageKey",
|
|
11798
|
+
"cpuPoolKey",
|
|
11799
|
+
"physicalCores",
|
|
11800
|
+
"vcpu",
|
|
11801
|
+
"memoryGib",
|
|
11802
|
+
"diskGib",
|
|
11803
|
+
"egressGuaranteedMbps",
|
|
11804
|
+
"egressBurstMbps",
|
|
11805
|
+
"confidential",
|
|
11806
|
+
"databaseRole",
|
|
11807
|
+
"databaseAgency"
|
|
11808
|
+
].includes(key)))
|
|
11809
|
+
throw new Error("initial platform compute contains unknown fields");
|
|
11810
|
+
return compute;
|
|
11811
|
+
}));
|
|
11812
|
+
for (let index = 0;index < computes.length; index += 1) {
|
|
11813
|
+
const declared = value.computes[index];
|
|
11814
|
+
const expectedRole = index === 0 ? "master" : "joiner";
|
|
11815
|
+
if (declared.databaseRole !== expectedRole || declared.databaseAgency !== "member" || declared.confidential !== true) {
|
|
11816
|
+
throw new Error("initial platform inventory requires one master, two joiners, three Agency members and confidential compute");
|
|
11817
|
+
}
|
|
11818
|
+
}
|
|
11819
|
+
return {
|
|
11820
|
+
metalHostname: value.metalHostname,
|
|
11821
|
+
region: { ...value.region },
|
|
11822
|
+
computes: value.computes.map((compute, index) => ({
|
|
11823
|
+
...computes[index],
|
|
11824
|
+
databaseRole: compute.databaseRole,
|
|
11825
|
+
databaseAgency: compute.databaseAgency,
|
|
11826
|
+
confidential: true
|
|
11827
|
+
}))
|
|
11828
|
+
};
|
|
11829
|
+
}
|
|
11621
11830
|
var systemdValue = (name, raw) => {
|
|
11622
11831
|
if (/[\0\r\n]/.test(raw))
|
|
11623
11832
|
throw new Error(`${name} must be single-line.`);
|
|
@@ -11703,12 +11912,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
11703
11912
|
throw new Error("realtime.producer is invalid.");
|
|
11704
11913
|
}
|
|
11705
11914
|
}
|
|
11915
|
+
const initialInventory = input.initialInventory ? validatePlatformInitialInventory(input.initialInventory) : undefined;
|
|
11706
11916
|
return {
|
|
11707
11917
|
...input,
|
|
11708
11918
|
databaseCoordinators: coordinators,
|
|
11709
11919
|
appOrigin: httpsOrigin("appOrigin", input.appOrigin),
|
|
11710
11920
|
apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
|
|
11711
|
-
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
|
|
11921
|
+
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint),
|
|
11922
|
+
...initialInventory ? { initialInventory } : {}
|
|
11712
11923
|
};
|
|
11713
11924
|
}
|
|
11714
11925
|
function renderPlatformSharedEnvironment(input) {
|
|
@@ -11771,7 +11982,8 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
11771
11982
|
FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
|
|
11772
11983
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
11773
11984
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
11774
|
-
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? ""
|
|
11985
|
+
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
11986
|
+
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
11775
11987
|
};
|
|
11776
11988
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
11777
11989
|
` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
|
|
@@ -12063,12 +12275,12 @@ async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
|
|
|
12063
12275
|
}
|
|
12064
12276
|
const path = `/accounts/${config.accountId}/teamnet/routes`;
|
|
12065
12277
|
const routes = await cf(config, path, {}, fetcher);
|
|
12066
|
-
const
|
|
12067
|
-
if (
|
|
12068
|
-
if (
|
|
12278
|
+
const current2 = routes.find((route2) => !route2.deleted_at && route2.network === config.network && (route2.virtual_network_id ?? "") === (config.virtualNetworkId ?? ""));
|
|
12279
|
+
if (current2) {
|
|
12280
|
+
if (current2.tunnel_id !== config.tunnelId) {
|
|
12069
12281
|
throw new Error(`private route ${config.network} already belongs to another Tunnel`);
|
|
12070
12282
|
}
|
|
12071
|
-
return { route:
|
|
12283
|
+
return { route: current2, created: false };
|
|
12072
12284
|
}
|
|
12073
12285
|
const route = await cf(config, path, {
|
|
12074
12286
|
method: "POST",
|
|
@@ -12117,7 +12329,7 @@ async function ensureCloudflareWarpNetworkIncludes(config, fetcher = fetch) {
|
|
|
12117
12329
|
async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
12118
12330
|
const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
|
|
12119
12331
|
const dnsPath = `/zones/${config.zoneId}/dns_records`;
|
|
12120
|
-
const [
|
|
12332
|
+
const [current2, records] = await Promise.all([
|
|
12121
12333
|
cf(config, tunnelPath, {}, fetcher),
|
|
12122
12334
|
cf(config, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
|
|
12123
12335
|
]);
|
|
@@ -12128,7 +12340,7 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
12128
12340
|
if (existingRecord && (existingRecord.type !== "CNAME" || existingRecord.name && existingRecord.name.toLowerCase() !== config.hostname.toLowerCase())) {
|
|
12129
12341
|
throw new Error(`Cloudflare DNS hostname ${config.hostname} is already owned by an incompatible ${existingRecord.type ?? "unknown"} record`);
|
|
12130
12342
|
}
|
|
12131
|
-
const existing =
|
|
12343
|
+
const existing = current2.config?.ingress ?? [];
|
|
12132
12344
|
const catchAll = existing.filter((rule) => !("hostname" in rule));
|
|
12133
12345
|
const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
|
|
12134
12346
|
const desiredIngress = [
|
|
@@ -13215,7 +13427,7 @@ async function sha256File(path) {
|
|
|
13215
13427
|
});
|
|
13216
13428
|
return hash.digest("hex");
|
|
13217
13429
|
}
|
|
13218
|
-
var
|
|
13430
|
+
var bootstrapBundleBranch = (value) => {
|
|
13219
13431
|
if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
|
|
13220
13432
|
throw new Error("bootstrap bundle branch is malformed");
|
|
13221
13433
|
}
|
|
@@ -13232,7 +13444,7 @@ function parseBootstrapBundleManifest(value) {
|
|
|
13232
13444
|
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
13233
13445
|
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
13234
13446
|
}
|
|
13235
|
-
|
|
13447
|
+
bootstrapBundleBranch(source.branch);
|
|
13236
13448
|
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
13237
13449
|
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
13238
13450
|
}
|
|
@@ -13277,7 +13489,7 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
13277
13489
|
const repositoryRoot = resolve6(input.repositoryRoot);
|
|
13278
13490
|
const outputPath = resolve6(input.outputPath);
|
|
13279
13491
|
const manifestPath = `${outputPath}.json`;
|
|
13280
|
-
const branch =
|
|
13492
|
+
const branch = bootstrapBundleBranch(input.branch);
|
|
13281
13493
|
if (!isAbsolute2(input.outputPath) || outputPath !== input.outputPath) {
|
|
13282
13494
|
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
13283
13495
|
}
|
|
@@ -13502,6 +13714,12 @@ function validateBootstrapConfig(value) {
|
|
|
13502
13714
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
13503
13715
|
}
|
|
13504
13716
|
if (value.kind === "enrolled-compute") {
|
|
13717
|
+
const os = OS_CATALOG.find((entry) => entry.status === "active" && entry.id === value.operatingSystem?.id && entry.version === value.operatingSystem?.version && entry.architecture === value.operatingSystem?.architecture);
|
|
13718
|
+
if (!os)
|
|
13719
|
+
throw new Error("enrolled compute operating system is not in the active typed OS catalog");
|
|
13720
|
+
if (value.operatingSystem.version === "24.04" && (value.installCloudflared || value.installWarp)) {
|
|
13721
|
+
throw new Error("Cloudflare Tunnel and WARP require the reviewed Ubuntu 26.04 strategy");
|
|
13722
|
+
}
|
|
13505
13723
|
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
13506
13724
|
throw new Error("gitDeployKey must be boolean");
|
|
13507
13725
|
}
|
|
@@ -13533,6 +13751,7 @@ function validateBootstrapConfig(value) {
|
|
|
13533
13751
|
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
13534
13752
|
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
13535
13753
|
}
|
|
13754
|
+
bootstrapBundleBranch(value.bootstrapBundle.branch);
|
|
13536
13755
|
let api;
|
|
13537
13756
|
try {
|
|
13538
13757
|
api = new URL(value.apiUrl);
|
|
@@ -13587,9 +13806,19 @@ function validateBootstrapConfig(value) {
|
|
|
13587
13806
|
}
|
|
13588
13807
|
function planBootstrap(input, initialized = false) {
|
|
13589
13808
|
const config = validateBootstrapConfig(structuredClone(input));
|
|
13809
|
+
const enrolledOsVersion = config.kind === "enrolled-compute" ? `ubuntu-${config.operatingSystem.version}` : undefined;
|
|
13810
|
+
const enrolledSoftware = config.kind === "enrolled-compute" ? [...config.software ?? []] : [];
|
|
13811
|
+
if (config.kind === "enrolled-compute") {
|
|
13812
|
+
if (!enrolledSoftware.some(({ id: id2 }) => id2 === "bun"))
|
|
13813
|
+
enrolledSoftware.push({ id: "bun", version: "1.3.14" });
|
|
13814
|
+
if (!enrolledSoftware.some(({ id: id2 }) => id2 === "git"))
|
|
13815
|
+
enrolledSoftware.push({ id: "git", version: enrolledOsVersion });
|
|
13816
|
+
if (!enrolledSoftware.some(({ id: id2 }) => id2 === "ufw"))
|
|
13817
|
+
enrolledSoftware.push({ id: "ufw", version: enrolledOsVersion });
|
|
13818
|
+
}
|
|
13590
13819
|
const software = config.kind === "enrolled-compute" ? [
|
|
13591
|
-
...
|
|
13592
|
-
...config.bootstrapRunner && !
|
|
13820
|
+
...enrolledSoftware,
|
|
13821
|
+
...config.bootstrapRunner && !enrolledSoftware.some(({ id: id2 }) => id2 === "openssh-client") ? [{ id: "openssh-client", version: enrolledOsVersion }] : [],
|
|
13593
13822
|
...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
|
|
13594
13823
|
...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
|
|
13595
13824
|
] : [
|
|
@@ -13824,9 +14053,8 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
|
13824
14053
|
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13825
14054
|
}
|
|
13826
14055
|
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
14056
|
+
if (manifest.branch !== config.bootstrapBundle.branch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
14057
|
+
throw new Error("bootstrap bundle manifest disagrees with the reviewed bootstrap bundle coordinates");
|
|
13830
14058
|
}
|
|
13831
14059
|
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
13832
14060
|
if (digest !== manifest.sha256)
|
|
@@ -13840,6 +14068,7 @@ function bootstrapIdentity(config) {
|
|
|
13840
14068
|
if (config.kind === "enrolled-compute")
|
|
13841
14069
|
return {
|
|
13842
14070
|
kind: config.kind,
|
|
14071
|
+
operatingSystem: config.operatingSystem,
|
|
13843
14072
|
apiUrl: config.apiUrl,
|
|
13844
14073
|
realm: config.realm,
|
|
13845
14074
|
nodeHostname: config.nodeHostname,
|
|
@@ -14453,7 +14682,7 @@ function strictBootstrapDocument(value) {
|
|
|
14453
14682
|
"bootstrapRunner"
|
|
14454
14683
|
], "bootstrap config");
|
|
14455
14684
|
if (root.kind === "platform") {
|
|
14456
|
-
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
14685
|
+
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile", "branch"], "bootstrap bundle config");
|
|
14457
14686
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14458
14687
|
if (root.cloudflareHandoff !== undefined)
|
|
14459
14688
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -14500,9 +14729,32 @@ function strictBootstrapDocument(value) {
|
|
|
14500
14729
|
"otlpTraceSampleRatio",
|
|
14501
14730
|
"backup",
|
|
14502
14731
|
"cloudflare",
|
|
14503
|
-
"realtime"
|
|
14732
|
+
"realtime",
|
|
14733
|
+
"initialInventory"
|
|
14504
14734
|
], "runtime environment");
|
|
14505
14735
|
const environment = runtime.environment;
|
|
14736
|
+
if (environment.initialInventory !== undefined) {
|
|
14737
|
+
const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
|
|
14738
|
+
exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
|
|
14739
|
+
if (!Array.isArray(inventory.computes))
|
|
14740
|
+
throw new Error("initial inventory computes must be an array");
|
|
14741
|
+
for (const compute of inventory.computes)
|
|
14742
|
+
exactKeys2(compute, [
|
|
14743
|
+
"name",
|
|
14744
|
+
"address",
|
|
14745
|
+
"imageKey",
|
|
14746
|
+
"cpuPoolKey",
|
|
14747
|
+
"physicalCores",
|
|
14748
|
+
"vcpu",
|
|
14749
|
+
"memoryGib",
|
|
14750
|
+
"diskGib",
|
|
14751
|
+
"egressGuaranteedMbps",
|
|
14752
|
+
"egressBurstMbps",
|
|
14753
|
+
"confidential",
|
|
14754
|
+
"databaseRole",
|
|
14755
|
+
"databaseAgency"
|
|
14756
|
+
], "initial inventory compute");
|
|
14757
|
+
}
|
|
14506
14758
|
if (environment.email !== undefined) {
|
|
14507
14759
|
const email = exactKeys2(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
|
|
14508
14760
|
if (email.provider === "smtp")
|
|
@@ -14519,6 +14771,7 @@ function strictBootstrapDocument(value) {
|
|
|
14519
14771
|
if (environment.realtime !== undefined)
|
|
14520
14772
|
exactKeys2(environment.realtime, ["workerScriptName", "endpoint", "producer"], "realtime runtime config");
|
|
14521
14773
|
} else if (root.kind === "enrolled-compute") {
|
|
14774
|
+
exactKeys2(root.operatingSystem, ["id", "version", "architecture"], "enrolled compute operating system");
|
|
14522
14775
|
if (root.cloudflareHandoff !== undefined)
|
|
14523
14776
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
14524
14777
|
if (root.bootstrapRunner !== undefined)
|
|
@@ -14850,19 +15103,7 @@ import { join as join8 } from "path";
|
|
|
14850
15103
|
// src/metal-provision.ts
|
|
14851
15104
|
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join7 } from "path";
|
|
14852
15105
|
import { isIP as isIP4 } from "net";
|
|
14853
|
-
|
|
14854
|
-
// src/ubuntu.ts
|
|
14855
|
-
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
14856
|
-
key: "ubuntu-resolute-20260731",
|
|
14857
|
-
family: "ubuntu-26.04",
|
|
14858
|
-
version: "2026-07-31",
|
|
14859
|
-
label: "Ubuntu 26.04 LTS Resolute",
|
|
14860
|
-
url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
|
|
14861
|
-
sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
|
|
14862
|
-
});
|
|
14863
|
-
|
|
14864
|
-
// src/metal-provision.ts
|
|
14865
|
-
var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
15106
|
+
var SAFE_NAME2 = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
14866
15107
|
var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
|
|
14867
15108
|
var IPV4_PREFIX = /^(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
|
|
14868
15109
|
var LINUX_LIST = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/;
|
|
@@ -14888,7 +15129,7 @@ function membersOfLinuxList(value, label) {
|
|
|
14888
15129
|
return members;
|
|
14889
15130
|
}
|
|
14890
15131
|
function validateMetalProfile(profile) {
|
|
14891
|
-
if (!
|
|
15132
|
+
if (!SAFE_NAME2.test(profile.volumeGroup))
|
|
14892
15133
|
throw new MetalProvisionError("invalid volume group");
|
|
14893
15134
|
if (!DEVICE.test(profile.bridge))
|
|
14894
15135
|
throw new MetalProvisionError("invalid bridge");
|
|
@@ -14910,9 +15151,16 @@ function validateMetalProfile(profile) {
|
|
|
14910
15151
|
if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP4(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
|
|
14911
15152
|
throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
|
|
14912
15153
|
const imageKeys = Object.keys(profile.images);
|
|
14913
|
-
if (imageKeys.length
|
|
14914
|
-
throw new MetalProvisionError(
|
|
15154
|
+
if (imageKeys.length === 0)
|
|
15155
|
+
throw new MetalProvisionError("metal profile requires at least one pinned guest image");
|
|
15156
|
+
for (const imageKey of imageKeys) {
|
|
15157
|
+
const catalog = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
|
|
15158
|
+
if (!catalog || profile.images[imageKey]?.sha256 !== catalog.imageSha256) {
|
|
15159
|
+
throw new MetalProvisionError(`metal profile image ${imageKey} does not match the pinned OS catalog`);
|
|
15160
|
+
}
|
|
14915
15161
|
}
|
|
15162
|
+
if (profile.confidential && !imageKeys.some((key) => OS_CATALOG.find((entry) => entry.imageKey === key)?.confidentialModes.some((mode) => mode === "sev-snp")))
|
|
15163
|
+
throw new MetalProvisionError("confidential metal profile requires a pinned SEV-SNP guest image");
|
|
14916
15164
|
if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
|
|
14917
15165
|
throw new MetalProvisionError("at least one exclusive CPU pool is required");
|
|
14918
15166
|
}
|
|
@@ -14921,7 +15169,7 @@ function validateMetalProfile(profile) {
|
|
|
14921
15169
|
const assignedMemory = new Set;
|
|
14922
15170
|
let poolsWithMemory = 0;
|
|
14923
15171
|
for (const pool of profile.cpuPools) {
|
|
14924
|
-
if (!
|
|
15172
|
+
if (!SAFE_NAME2.test(pool.key) || keys.has(pool.key))
|
|
14925
15173
|
throw new MetalProvisionError("invalid or duplicate CPU pool key");
|
|
14926
15174
|
keys.add(pool.key);
|
|
14927
15175
|
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
@@ -15163,10 +15411,9 @@ function validateMetalBootstrapConfig(config) {
|
|
|
15163
15411
|
if (config.profile.stateDir !== "/etc/forgezero/metal-guests" || config.profile.seedDir !== "/var/lib/forgezero/seed" || config.profile.unitDir !== UNIT_DIRECTORY || config.profile.legacyStateDir !== undefined) {
|
|
15164
15412
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
15165
15413
|
}
|
|
15166
|
-
const image
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
}
|
|
15414
|
+
for (const image of Object.values(config.profile.images))
|
|
15415
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve8(image.path) !== image.path || /[\0\r\n]/.test(image.path))
|
|
15416
|
+
throw new MetalBootstrapError("every pinned guest image must use the fixed image directory");
|
|
15170
15417
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
|
|
15171
15418
|
throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
|
|
15172
15419
|
}
|
|
@@ -15468,9 +15715,11 @@ var preflight = async (config, exec) => {
|
|
|
15468
15715
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
15469
15716
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
15470
15717
|
}
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15718
|
+
for (const image of Object.values(config.profile.images)) {
|
|
15719
|
+
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
15720
|
+
if (digest !== image.sha256)
|
|
15721
|
+
throw new MetalBootstrapError("pinned guest image digest mismatch");
|
|
15722
|
+
}
|
|
15474
15723
|
};
|
|
15475
15724
|
var assertSupportedMetalHost = () => {
|
|
15476
15725
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
@@ -15482,34 +15731,38 @@ var assertSupportedMetalHost = () => {
|
|
|
15482
15731
|
}
|
|
15483
15732
|
};
|
|
15484
15733
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
15485
|
-
const image
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
"--fail",
|
|
15494
|
-
"--location",
|
|
15495
|
-
"--proto",
|
|
15496
|
-
"=https",
|
|
15497
|
-
"--tlsv1.2",
|
|
15498
|
-
"--output",
|
|
15499
|
-
temporary,
|
|
15500
|
-
SUPPORTED_GUEST_IMAGE.url
|
|
15501
|
-
]);
|
|
15502
|
-
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
15503
|
-
if (digest !== image.sha256)
|
|
15504
|
-
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
15505
|
-
chmodSync5(temporary, 292);
|
|
15506
|
-
chownSync(temporary, 0, 0);
|
|
15507
|
-
renameSync9(temporary, image.path);
|
|
15508
|
-
} catch (cause) {
|
|
15734
|
+
for (const [imageKey, image] of Object.entries(config.profile.images)) {
|
|
15735
|
+
if (existsSync10(image.path))
|
|
15736
|
+
continue;
|
|
15737
|
+
const catalog = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
|
|
15738
|
+
if (!catalog)
|
|
15739
|
+
throw new MetalBootstrapError(`guest image ${imageKey} is not active`);
|
|
15740
|
+
mkdirSync11(dirname12(image.path), { recursive: true, mode: 493 });
|
|
15741
|
+
const temporary = `${image.path}.next-${process.pid}`;
|
|
15509
15742
|
try {
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15743
|
+
await runChecked(exec, [
|
|
15744
|
+
"/usr/bin/curl",
|
|
15745
|
+
"--fail",
|
|
15746
|
+
"--location",
|
|
15747
|
+
"--proto",
|
|
15748
|
+
"=https",
|
|
15749
|
+
"--tlsv1.2",
|
|
15750
|
+
"--output",
|
|
15751
|
+
temporary,
|
|
15752
|
+
catalog.imageUrl
|
|
15753
|
+
]);
|
|
15754
|
+
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
15755
|
+
if (digest !== image.sha256)
|
|
15756
|
+
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
15757
|
+
chmodSync5(temporary, 292);
|
|
15758
|
+
chownSync(temporary, 0, 0);
|
|
15759
|
+
renameSync9(temporary, image.path);
|
|
15760
|
+
} catch (cause) {
|
|
15761
|
+
try {
|
|
15762
|
+
unlinkSync2(temporary);
|
|
15763
|
+
} catch {}
|
|
15764
|
+
throw cause;
|
|
15765
|
+
}
|
|
15513
15766
|
}
|
|
15514
15767
|
};
|
|
15515
15768
|
async function applyMetalBootstrap(config, options) {
|
|
@@ -15669,10 +15922,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15669
15922
|
])).exitCode !== 0) {
|
|
15670
15923
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
15671
15924
|
}
|
|
15672
|
-
|
|
15673
|
-
|
|
15925
|
+
imageVerified = true;
|
|
15926
|
+
for (const image of Object.values(profile.images)) {
|
|
15927
|
+
if (!existsSync10(image.path)) {
|
|
15928
|
+
imageVerified = false;
|
|
15929
|
+
break;
|
|
15930
|
+
}
|
|
15674
15931
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
15675
|
-
|
|
15932
|
+
if (digest !== image.sha256) {
|
|
15933
|
+
imageVerified = false;
|
|
15934
|
+
break;
|
|
15935
|
+
}
|
|
15676
15936
|
}
|
|
15677
15937
|
if (!imageVerified)
|
|
15678
15938
|
problems.push("pinned guest image is absent or has the wrong digest");
|
|
@@ -15732,126 +15992,6 @@ import { isIP as isIP6 } from "net";
|
|
|
15732
15992
|
import { tmpdir } from "os";
|
|
15733
15993
|
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15734
15994
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15735
|
-
|
|
15736
|
-
// src/platform-genesis.ts
|
|
15737
|
-
var PLATFORM_GENESIS_ENVIRONMENTS = ["production", "development"];
|
|
15738
|
-
var productionShape = {
|
|
15739
|
-
physicalCores: 8,
|
|
15740
|
-
vcpu: 16,
|
|
15741
|
-
memoryGib: 24,
|
|
15742
|
-
diskGib: 200,
|
|
15743
|
-
egressGuaranteedMbps: 1000,
|
|
15744
|
-
egressBurstMbps: 2000
|
|
15745
|
-
};
|
|
15746
|
-
var developmentShape = {
|
|
15747
|
-
physicalCores: 2,
|
|
15748
|
-
vcpu: 4,
|
|
15749
|
-
memoryGib: 8,
|
|
15750
|
-
diskGib: 64,
|
|
15751
|
-
egressGuaranteedMbps: 100,
|
|
15752
|
-
egressBurstMbps: 250
|
|
15753
|
-
};
|
|
15754
|
-
var PLATFORM_GENESIS_FLEETS = {
|
|
15755
|
-
production: [1, 2, 3].map((node) => ({
|
|
15756
|
-
name: `fz-n${node}`,
|
|
15757
|
-
address: `10.42.0.${10 + node}`,
|
|
15758
|
-
cpuPoolKey: `node${node}`,
|
|
15759
|
-
...productionShape
|
|
15760
|
-
})),
|
|
15761
|
-
development: [1, 2, 3].map((node) => ({
|
|
15762
|
-
name: `dev-fz-n${node}`,
|
|
15763
|
-
address: `10.42.0.${20 + node}`,
|
|
15764
|
-
cpuPoolKey: `node${node + 3}`,
|
|
15765
|
-
...developmentShape
|
|
15766
|
-
}))
|
|
15767
|
-
};
|
|
15768
|
-
var DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST = {
|
|
15769
|
-
name: "dev-fz-n4",
|
|
15770
|
-
address: "10.42.0.24",
|
|
15771
|
-
cpuPoolKey: "node7",
|
|
15772
|
-
...developmentShape
|
|
15773
|
-
};
|
|
15774
|
-
var publicKey = (value) => {
|
|
15775
|
-
const normalized = value.trim();
|
|
15776
|
-
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(normalized)) {
|
|
15777
|
-
throw new Error("platform genesis SSH public key is malformed");
|
|
15778
|
-
}
|
|
15779
|
-
return normalized;
|
|
15780
|
-
};
|
|
15781
|
-
function platformGenesisClaims(environment, sshPublicKeys) {
|
|
15782
|
-
if (!PLATFORM_GENESIS_ENVIRONMENTS.includes(environment)) {
|
|
15783
|
-
throw new Error("platform genesis environment must be production or development");
|
|
15784
|
-
}
|
|
15785
|
-
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
15786
|
-
if (keys.length === 0)
|
|
15787
|
-
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
15788
|
-
return PLATFORM_GENESIS_FLEETS[environment].map((guest) => ({
|
|
15789
|
-
computeKey: `platform:${guest.name}`,
|
|
15790
|
-
claimToken: "offline-platform-genesis",
|
|
15791
|
-
claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
|
|
15792
|
-
attempt: 1,
|
|
15793
|
-
action: "create",
|
|
15794
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15795
|
-
access: { sshUser: "forgezero", sshPublicKeys: keys },
|
|
15796
|
-
spec: {
|
|
15797
|
-
reference: `platform:${guest.name}`,
|
|
15798
|
-
imageKey: SUPPORTED_GUEST_IMAGE.key,
|
|
15799
|
-
guestName: guest.name,
|
|
15800
|
-
guestAddress: guest.address,
|
|
15801
|
-
cpuPoolKey: guest.cpuPoolKey,
|
|
15802
|
-
physicalCores: guest.physicalCores,
|
|
15803
|
-
vcpu: guest.vcpu,
|
|
15804
|
-
memoryGib: guest.memoryGib,
|
|
15805
|
-
diskGib: guest.diskGib,
|
|
15806
|
-
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
15807
|
-
egressBurstMbps: guest.egressBurstMbps,
|
|
15808
|
-
confidential: true
|
|
15809
|
-
}
|
|
15810
|
-
}));
|
|
15811
|
-
}
|
|
15812
|
-
function platformCommunityRehearsalClaims(sshPublicKeys) {
|
|
15813
|
-
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
15814
|
-
if (keys.length === 0)
|
|
15815
|
-
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
15816
|
-
const seed = platformGenesisClaims("development", keys);
|
|
15817
|
-
const guest = DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST;
|
|
15818
|
-
return [...seed, {
|
|
15819
|
-
computeKey: `platform:${guest.name}`,
|
|
15820
|
-
claimToken: "offline-platform-genesis",
|
|
15821
|
-
claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
|
|
15822
|
-
attempt: 1,
|
|
15823
|
-
action: "create",
|
|
15824
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15825
|
-
access: { sshUser: "forgezero", sshPublicKeys: keys },
|
|
15826
|
-
spec: {
|
|
15827
|
-
reference: `platform:${guest.name}`,
|
|
15828
|
-
imageKey: SUPPORTED_GUEST_IMAGE.key,
|
|
15829
|
-
guestName: guest.name,
|
|
15830
|
-
guestAddress: guest.address,
|
|
15831
|
-
cpuPoolKey: guest.cpuPoolKey,
|
|
15832
|
-
physicalCores: guest.physicalCores,
|
|
15833
|
-
vcpu: guest.vcpu,
|
|
15834
|
-
memoryGib: guest.memoryGib,
|
|
15835
|
-
diskGib: guest.diskGib,
|
|
15836
|
-
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
15837
|
-
egressBurstMbps: guest.egressBurstMbps,
|
|
15838
|
-
confidential: true
|
|
15839
|
-
}
|
|
15840
|
-
}];
|
|
15841
|
-
}
|
|
15842
|
-
function platformCommunityRehearsalDeletionClaims(sshPublicKeys) {
|
|
15843
|
-
return platformCommunityRehearsalClaims(sshPublicKeys).map((claim) => ({
|
|
15844
|
-
computeKey: claim.computeKey,
|
|
15845
|
-
claimToken: claim.claimToken,
|
|
15846
|
-
claimExpiresAtTs: claim.claimExpiresAtTs,
|
|
15847
|
-
attempt: claim.attempt,
|
|
15848
|
-
action: "delete",
|
|
15849
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15850
|
-
spec: claim.spec
|
|
15851
|
-
}));
|
|
15852
|
-
}
|
|
15853
|
-
|
|
15854
|
-
// src/operator-bootstrap.ts
|
|
15855
15995
|
var REQUEST_LIMIT = 64 * 1024;
|
|
15856
15996
|
var SECRET_LIMIT = 256 * 1024;
|
|
15857
15997
|
var REMOTE_STAGE = "/run/forgezero-operator-bootstrap";
|
|
@@ -16031,10 +16171,44 @@ var validateMetalRequestValue = (value) => {
|
|
|
16031
16171
|
throw new Error("metalConfigFile must be an absolute path");
|
|
16032
16172
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
16033
16173
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
16034
|
-
const genesis = exactKeys5(root.genesis, ["
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16174
|
+
const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
|
|
16175
|
+
const guest = (value2, label) => {
|
|
16176
|
+
const row2 = exactKeys5(value2, [
|
|
16177
|
+
"name",
|
|
16178
|
+
"address",
|
|
16179
|
+
"imageKey",
|
|
16180
|
+
"cpuPoolKey",
|
|
16181
|
+
"physicalCores",
|
|
16182
|
+
"vcpu",
|
|
16183
|
+
"memoryGib",
|
|
16184
|
+
"diskGib",
|
|
16185
|
+
"egressGuaranteedMbps",
|
|
16186
|
+
"egressBurstMbps",
|
|
16187
|
+
"confidential"
|
|
16188
|
+
], label);
|
|
16189
|
+
if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || typeof row2.confidential !== "boolean") {
|
|
16190
|
+
throw new Error(`${label} has invalid resource coordinates`);
|
|
16191
|
+
}
|
|
16192
|
+
return {
|
|
16193
|
+
name: row2.name,
|
|
16194
|
+
address: row2.address,
|
|
16195
|
+
imageKey: row2.imageKey,
|
|
16196
|
+
...row2.cpuPoolKey ? { cpuPoolKey: row2.cpuPoolKey } : {},
|
|
16197
|
+
physicalCores: row2.physicalCores,
|
|
16198
|
+
vcpu: row2.vcpu,
|
|
16199
|
+
memoryGib: row2.memoryGib,
|
|
16200
|
+
diskGib: row2.diskGib,
|
|
16201
|
+
egressGuaranteedMbps: row2.egressGuaranteedMbps,
|
|
16202
|
+
egressBurstMbps: row2.egressBurstMbps,
|
|
16203
|
+
confidential: row2.confidential
|
|
16204
|
+
};
|
|
16205
|
+
};
|
|
16206
|
+
if (!Array.isArray(genesis.nodes))
|
|
16207
|
+
throw new Error("operator metal genesis nodes must be an array");
|
|
16208
|
+
const nodes = validatePlatformGenesisGuests(genesis.nodes.map((row2, index) => guest(row2, `operator metal genesis node ${index + 1}`)));
|
|
16209
|
+
const rehearsalNode = genesis.rehearsalNode === undefined ? undefined : guest(genesis.rehearsalNode, "operator metal rehearsal node");
|
|
16210
|
+
if (rehearsalNode)
|
|
16211
|
+
validatePlatformGenesisGuests([...nodes, rehearsalNode], 4);
|
|
16038
16212
|
if (!Array.isArray(genesis.sshPublicKeyFiles) || genesis.sshPublicKeyFiles.length === 0 || genesis.sshPublicKeyFiles.some((entry) => typeof entry !== "string")) {
|
|
16039
16213
|
throw new Error("operator metal genesis requires SSH public-key files");
|
|
16040
16214
|
}
|
|
@@ -16049,7 +16223,8 @@ var validateMetalRequestValue = (value) => {
|
|
|
16049
16223
|
agentSocket: targetValue.agentSocket
|
|
16050
16224
|
},
|
|
16051
16225
|
genesis: {
|
|
16052
|
-
|
|
16226
|
+
nodes,
|
|
16227
|
+
...rehearsalNode ? { rehearsalNode } : {},
|
|
16053
16228
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
16054
16229
|
}
|
|
16055
16230
|
};
|
|
@@ -16062,26 +16237,69 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
16062
16237
|
validateMetalRequestValue(request);
|
|
16063
16238
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
16064
16239
|
}
|
|
16240
|
+
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
16241
|
+
const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
16242
|
+
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
16243
|
+
throw new Error("operator guest host-key evidence is malformed");
|
|
16244
|
+
}
|
|
16245
|
+
const nodes = root.nodes.map((value2, index) => {
|
|
16246
|
+
const row2 = exactKeys5(value2, ["name", "address", "hostKey", "hostKeySha256"], `operator guest host key ${index + 1}`);
|
|
16247
|
+
if (typeof row2.name !== "string" || typeof row2.address !== "string") {
|
|
16248
|
+
throw new Error("operator guest host-key identity is malformed");
|
|
16249
|
+
}
|
|
16250
|
+
const hop = validateHop({
|
|
16251
|
+
address: row2.address,
|
|
16252
|
+
port: 22,
|
|
16253
|
+
user: "forgezero",
|
|
16254
|
+
hostKey: row2.hostKey,
|
|
16255
|
+
hostKeySha256: row2.hostKeySha256
|
|
16256
|
+
}, `operator guest host key ${row2.name}`);
|
|
16257
|
+
return { name: row2.name, address: hop.address, hostKey: hop.hostKey, hostKeySha256: hop.hostKeySha256 };
|
|
16258
|
+
});
|
|
16259
|
+
if (new Set(nodes.map(({ name }) => name)).size !== nodes.length || new Set(nodes.map(({ address }) => address)).size !== nodes.length) {
|
|
16260
|
+
throw new Error("operator guest host-key identities must be unique");
|
|
16261
|
+
}
|
|
16262
|
+
if (request) {
|
|
16263
|
+
const expected = nodes.length === request.genesis.nodes.length ? [...request.genesis.nodes] : [...request.genesis.nodes, ...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []];
|
|
16264
|
+
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)) {
|
|
16265
|
+
throw new Error("operator guest host-key evidence does not match the reviewed Metal request");
|
|
16266
|
+
}
|
|
16267
|
+
}
|
|
16268
|
+
return {
|
|
16269
|
+
format: 1,
|
|
16270
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
16271
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
16272
|
+
nodes
|
|
16273
|
+
};
|
|
16274
|
+
};
|
|
16275
|
+
function readOperatorGuestHostKeyEvidence(path, request) {
|
|
16276
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator guest host-key evidence")));
|
|
16277
|
+
return validateGuestHostKeyEvidenceValue(value, request);
|
|
16278
|
+
}
|
|
16279
|
+
function writeOperatorGuestHostKeyEvidence(path, evidence, request) {
|
|
16280
|
+
return writeOwnerJson2(path, validateGuestHostKeyEvidenceValue(evidence, request), "operator guest host-key evidence");
|
|
16281
|
+
}
|
|
16065
16282
|
function planOperatorMetalBootstrap(request, mode) {
|
|
16066
16283
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
16067
|
-
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && request.genesis.environment !== "development") {
|
|
16068
|
-
throw new Error("the disposable four-node Community rehearsal is development-only");
|
|
16069
|
-
}
|
|
16070
16284
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16071
|
-
|
|
16285
|
+
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
16286
|
+
throw new Error("the reviewed metal request has no rehearsal node");
|
|
16287
|
+
}
|
|
16288
|
+
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);
|
|
16289
|
+
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));
|
|
16072
16290
|
return {
|
|
16073
16291
|
kind: "metal-remote",
|
|
16074
16292
|
mode,
|
|
16075
|
-
mutation:
|
|
16293
|
+
mutation: !["status", "host-keys", "rehearsal-host-keys"].includes(mode),
|
|
16076
16294
|
target: { address: request.target.address, port: request.target.port, user: request.target.user },
|
|
16077
|
-
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
|
|
16295
|
+
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"] : [
|
|
16078
16296
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
16079
16297
|
"copy pinned Bun and packaged fz artifacts to private staging",
|
|
16080
16298
|
"stage the strict owner-only metal config and optional seed handoff",
|
|
16081
16299
|
"run typed fz bootstrap metal --apply",
|
|
16082
16300
|
"remove transient local and remote staging data"
|
|
16083
16301
|
],
|
|
16084
|
-
genesisNodes
|
|
16302
|
+
genesisNodes,
|
|
16085
16303
|
secretInputs: config.agentSeedFile ? ["metal-agent-seed"] : []
|
|
16086
16304
|
};
|
|
16087
16305
|
}
|
|
@@ -16146,9 +16364,7 @@ function validatedPlatformFleet(fleet) {
|
|
|
16146
16364
|
return config;
|
|
16147
16365
|
});
|
|
16148
16366
|
const environment = configs[0].environment;
|
|
16149
|
-
const expected =
|
|
16150
|
-
if (!expected || expected.length !== 3)
|
|
16151
|
-
throw new Error("operator fleet environment is invalid");
|
|
16367
|
+
const expected = configs.map((config) => ({ name: config.computeReference, address: config.database.address }));
|
|
16152
16368
|
const identity = fleetConfigIdentity(configs[0]);
|
|
16153
16369
|
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
16154
16370
|
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
@@ -16306,6 +16522,53 @@ var copy = async (exec, request, knownHosts, local, remotePath, secret = false)
|
|
|
16306
16522
|
`${destination2(request.target)}:${remotePath}`
|
|
16307
16523
|
], "secure copy", { secret });
|
|
16308
16524
|
};
|
|
16525
|
+
var guestRows = (request) => [
|
|
16526
|
+
...request.genesis.nodes,
|
|
16527
|
+
...request.genesis.rehearsalNode ? [request.genesis.rehearsalNode] : []
|
|
16528
|
+
];
|
|
16529
|
+
var parsedScannedHostKey = (output, address) => {
|
|
16530
|
+
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(" "));
|
|
16531
|
+
const unique = [...new Set(keys)];
|
|
16532
|
+
if (unique.length !== 1 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}$/.test(unique[0])) {
|
|
16533
|
+
throw new Error(`guest ${address} did not return one exact Ed25519 host key`);
|
|
16534
|
+
}
|
|
16535
|
+
return unique[0];
|
|
16536
|
+
};
|
|
16537
|
+
async function collectOperatorGuestHostKeys(request, options = {}, includeRehearsal = false) {
|
|
16538
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
16539
|
+
socketPath(request.target.agentSocket);
|
|
16540
|
+
const exec = options.exec ?? defaultExec3;
|
|
16541
|
+
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-host-keys-"));
|
|
16542
|
+
try {
|
|
16543
|
+
const knownHosts = writeKnownHosts(request, directory);
|
|
16544
|
+
const nodes = [];
|
|
16545
|
+
for (const guest of includeRehearsal ? guestRows(request) : request.genesis.nodes) {
|
|
16546
|
+
const output = await remote(exec, request, knownHosts, [
|
|
16547
|
+
"/usr/bin/ssh-keyscan",
|
|
16548
|
+
"-T",
|
|
16549
|
+
"10",
|
|
16550
|
+
"-t",
|
|
16551
|
+
"ed25519",
|
|
16552
|
+
guest.address
|
|
16553
|
+
], `collect ${guest.name} SSH host key`);
|
|
16554
|
+
const hostKey = parsedScannedHostKey(output, guest.address);
|
|
16555
|
+
nodes.push({
|
|
16556
|
+
name: guest.name,
|
|
16557
|
+
address: guest.address,
|
|
16558
|
+
hostKey,
|
|
16559
|
+
hostKeySha256: fingerprint(hostKey)
|
|
16560
|
+
});
|
|
16561
|
+
}
|
|
16562
|
+
return validateGuestHostKeyEvidenceValue({
|
|
16563
|
+
format: 1,
|
|
16564
|
+
kind: "forgezero-operator-guest-host-keys",
|
|
16565
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
16566
|
+
nodes
|
|
16567
|
+
}, request);
|
|
16568
|
+
} finally {
|
|
16569
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16570
|
+
}
|
|
16571
|
+
}
|
|
16309
16572
|
async function stageConfig(config, directory) {
|
|
16310
16573
|
const rewritten = structuredClone(config);
|
|
16311
16574
|
const staged = [];
|
|
@@ -16319,13 +16582,17 @@ async function stageConfig(config, directory) {
|
|
|
16319
16582
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
16320
16583
|
}
|
|
16321
16584
|
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
16585
|
+
if (bundle.manifest.branch !== config.bootstrapBundle.branch) {
|
|
16586
|
+
throw new Error("reviewed bootstrap config branch does not match the bundle manifest");
|
|
16587
|
+
}
|
|
16322
16588
|
const bundleFiles = [
|
|
16323
16589
|
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
16324
16590
|
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
16325
16591
|
];
|
|
16326
16592
|
rewritten.bootstrapBundle = {
|
|
16327
16593
|
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
16328
|
-
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json
|
|
16594
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`,
|
|
16595
|
+
branch: bundle.manifest.branch
|
|
16329
16596
|
};
|
|
16330
16597
|
const path = join10(directory, "platform-config.json");
|
|
16331
16598
|
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
@@ -16481,15 +16748,24 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16481
16748
|
const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
|
|
16482
16749
|
return { plan, output: output2 };
|
|
16483
16750
|
}
|
|
16751
|
+
if (mode === "host-keys" || mode === "rehearsal-host-keys") {
|
|
16752
|
+
return {
|
|
16753
|
+
plan,
|
|
16754
|
+
output: JSON.stringify(await collectOperatorGuestHostKeys(request, { ...options, exec }, mode === "rehearsal-host-keys"))
|
|
16755
|
+
};
|
|
16756
|
+
}
|
|
16484
16757
|
if (mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup") {
|
|
16485
16758
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16759
|
+
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
16760
|
+
throw new Error("the reviewed metal request has no rehearsal node");
|
|
16761
|
+
}
|
|
16486
16762
|
const outputs = [];
|
|
16487
|
-
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(
|
|
16763
|
+
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);
|
|
16488
16764
|
for (const claim of claims) {
|
|
16489
16765
|
outputs.push(await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz-agent", "metal-apply", "--claim=-"], `provision ${claim.spec.guestName}`, false, `${JSON.stringify(claim)}
|
|
16490
16766
|
`));
|
|
16491
16767
|
}
|
|
16492
|
-
return { plan, output: JSON.stringify({
|
|
16768
|
+
return { plan, output: JSON.stringify({ nodes: plan.genesisNodes, outputs }) };
|
|
16493
16769
|
}
|
|
16494
16770
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
16495
16771
|
const staged = stageMetalConfig(config, directory);
|
|
@@ -16522,15 +16798,36 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16522
16798
|
}
|
|
16523
16799
|
|
|
16524
16800
|
// src/platform-genesis-config.ts
|
|
16525
|
-
function platformGenesisBootstrapConfigs(template, nodes) {
|
|
16526
|
-
const
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16801
|
+
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
|
|
16802
|
+
const fleet = validatePlatformGenesisGuests(guests);
|
|
16803
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
|
|
16804
|
+
throw new Error("platform genesis requires the reviewed metal hostname");
|
|
16805
|
+
}
|
|
16806
|
+
if (fleet.some(({ confidential }) => !confidential)) {
|
|
16807
|
+
throw new Error("platform genesis requires confidential compute");
|
|
16808
|
+
}
|
|
16809
|
+
if (nodes.length !== 3)
|
|
16810
|
+
throw new Error("platform genesis requires exactly three node coordinates");
|
|
16530
16811
|
if (nodes.some((node) => !node.nodeHostname || !node.cloudflareHandoffFile)) {
|
|
16531
16812
|
throw new Error("every platform genesis node requires its public hostname and node-specific Cloudflare handoff");
|
|
16532
16813
|
}
|
|
16533
16814
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
16815
|
+
const initialInventory = {
|
|
16816
|
+
metalHostname,
|
|
16817
|
+
region: {
|
|
16818
|
+
key: template.runtime.environment.nodeRegion,
|
|
16819
|
+
label: region.label,
|
|
16820
|
+
country: region.country,
|
|
16821
|
+
...region.city ? { city: region.city } : {},
|
|
16822
|
+
confidentialCapable: true
|
|
16823
|
+
},
|
|
16824
|
+
computes: fleet.map((guest, index) => ({
|
|
16825
|
+
...guest,
|
|
16826
|
+
confidential: guest.confidential,
|
|
16827
|
+
databaseRole: index === 0 ? "master" : "joiner",
|
|
16828
|
+
databaseAgency: "member"
|
|
16829
|
+
}))
|
|
16830
|
+
};
|
|
16534
16831
|
return fleet.map((guest, index) => {
|
|
16535
16832
|
const node = nodes[index];
|
|
16536
16833
|
const master = fleet[0].address;
|
|
@@ -16561,6 +16858,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16561
16858
|
nodeHostname: node.nodeHostname,
|
|
16562
16859
|
seedSyncPeers: fleet.filter((_, peer) => peer !== index).map((peer) => `ws://${peer.address}:${config.runtime.bluePort}/internal/seed`),
|
|
16563
16860
|
seedSyncMembers: 3,
|
|
16861
|
+
initialInventory,
|
|
16564
16862
|
custodianEmail: index === 0 ? template.runtime.environment.custodianEmail : undefined
|
|
16565
16863
|
};
|
|
16566
16864
|
return validateBootstrapConfig(config);
|
|
@@ -17586,6 +17884,12 @@ var bootstrapNumber = (question, fallback) => {
|
|
|
17586
17884
|
throw new Error(`${question} must be an integer`);
|
|
17587
17885
|
return value;
|
|
17588
17886
|
};
|
|
17887
|
+
var bootstrapBoolean = (question, fallback) => {
|
|
17888
|
+
const value = bootstrapAnswer(`${question} (yes/no)`, fallback).toLowerCase();
|
|
17889
|
+
if (value !== "yes" && value !== "no")
|
|
17890
|
+
throw new Error(`${question} must be yes or no`);
|
|
17891
|
+
return value === "yes";
|
|
17892
|
+
};
|
|
17589
17893
|
async function bootstrapSecret(question) {
|
|
17590
17894
|
if (!process.stdin.isTTY)
|
|
17591
17895
|
throw new Error(`${question} requires an interactive terminal`);
|
|
@@ -17623,7 +17927,7 @@ var parseCpuPools = (value) => value.split(";").map((entry) => entry.trim()).fil
|
|
|
17623
17927
|
function interactiveMetalBootstrap() {
|
|
17624
17928
|
if (!process.stdin.isTTY)
|
|
17625
17929
|
throw new Error("interactive metal config generation requires a terminal");
|
|
17626
|
-
const confidential =
|
|
17930
|
+
const confidential = bootstrapBoolean("Require SEV-SNP confidential guests?", "yes");
|
|
17627
17931
|
const subnetPrefix = bootstrapAnswer("Guest IPv4 subnet prefix", "10.42.0");
|
|
17628
17932
|
return validateMetalBootstrapConfig({
|
|
17629
17933
|
kind: "metal",
|
|
@@ -17643,12 +17947,10 @@ function interactiveMetalBootstrap() {
|
|
|
17643
17947
|
unitDir: "/etc/systemd/system",
|
|
17644
17948
|
apiUrl: bootstrapAnswer("Platform public API URL"),
|
|
17645
17949
|
agentTelemetryEndpoint: bootstrapAnswer("Public HTTPS Agent OTLP endpoint"),
|
|
17646
|
-
images: {
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
}
|
|
17651
|
-
},
|
|
17950
|
+
images: Object.fromEntries(OS_CATALOG.filter(({ status }) => status === "active").map((image) => [
|
|
17951
|
+
image.imageKey,
|
|
17952
|
+
{ path: `/var/lib/forgezero/images/${image.imageKey}.img`, sha256: image.imageSha256 }
|
|
17953
|
+
])),
|
|
17652
17954
|
cpuPools: parseCpuPools(bootstrapAnswer("CPU pools (key|cpus|physical-cores|memory-nodes;...)", "node1|8-15,72-79|8|1;node2|16-23,80-87|8|2;node3|24-31,88-95|8|3;node4|32-39,96-103|8|4;node5|40-47,104-111|8|5;node6|48-55,112-119|8|6;node7|56-63,120-127|8|7")),
|
|
17653
17955
|
housekeepingCpus: bootstrapAnswer("Host housekeeping CPU list", "0-7,64-71"),
|
|
17654
17956
|
housekeepingMemoryNodes: bootstrapAnswer("Host housekeeping memory nodes", "0"),
|
|
@@ -17683,7 +17985,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17683
17985
|
};
|
|
17684
17986
|
const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
|
|
17685
17987
|
const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
|
|
17686
|
-
const realtimeEnabled =
|
|
17988
|
+
const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
|
|
17687
17989
|
const workerScriptName = realtimeEnabled ? bootstrapAnswer("Existing Worker script name") : undefined;
|
|
17688
17990
|
const discovered = await discoverCloudflareBootstrapCommandResources({
|
|
17689
17991
|
zoneName,
|
|
@@ -17694,12 +17996,12 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17694
17996
|
const nodeCount = bootstrapNumber("Number of public API nodes", "3");
|
|
17695
17997
|
if (nodeCount < 1 || nodeCount > 32)
|
|
17696
17998
|
throw new Error("Number of public API nodes must be between 1 and 32");
|
|
17697
|
-
const meshEnabled =
|
|
17999
|
+
const meshEnabled = bootstrapBoolean("Configure private Mesh/WARP routes?", "no");
|
|
17698
18000
|
const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
|
|
17699
18001
|
const nodes = Array.from({ length: nodeCount }, (_, index) => {
|
|
17700
18002
|
const ordinal = index + 1;
|
|
17701
18003
|
const nodeName = bootstrapAnswer(`Node ${ordinal} inventory name`);
|
|
17702
|
-
const nodeMesh = meshEnabled &&
|
|
18004
|
+
const nodeMesh = meshEnabled && bootstrapBoolean(`Configure Mesh routes for ${nodeName}?`, "yes");
|
|
17703
18005
|
return {
|
|
17704
18006
|
nodeName,
|
|
17705
18007
|
hostname: bootstrapAnswer(`${nodeName} public hostname`),
|
|
@@ -17709,7 +18011,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17709
18011
|
mesh: {
|
|
17710
18012
|
connectorName: bootstrapAnswer(`${nodeName} Mesh connector name`, `${nodeName}-mesh`),
|
|
17711
18013
|
routes: bootstrapList(`${nodeName} private CIDR routes`),
|
|
17712
|
-
highAvailability:
|
|
18014
|
+
highAvailability: bootstrapBoolean(`${nodeName} shares a highly available Mesh connector?`, "no")
|
|
17713
18015
|
}
|
|
17714
18016
|
} : {}
|
|
17715
18017
|
};
|
|
@@ -17756,6 +18058,28 @@ function operatorIdentityCoordinates() {
|
|
|
17756
18058
|
agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
|
|
17757
18059
|
};
|
|
17758
18060
|
}
|
|
18061
|
+
function interactiveGenesisGuests() {
|
|
18062
|
+
const node = (index) => ({
|
|
18063
|
+
name: bootstrapAnswer(`Guest ${index} name`),
|
|
18064
|
+
address: bootstrapAnswer(`Guest ${index} private IPv4`),
|
|
18065
|
+
imageKey: bootstrapAnswer(`Guest ${index} OS image key`, SUPPORTED_GUEST_IMAGE.key),
|
|
18066
|
+
cpuPoolKey: bootstrapAnswer(`Guest ${index} exclusive CPU pool key`),
|
|
18067
|
+
physicalCores: bootstrapNumber(`Guest ${index} physical cores`),
|
|
18068
|
+
vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
|
|
18069
|
+
memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
|
|
18070
|
+
diskGib: bootstrapNumber(`Guest ${index} disk GiB`),
|
|
18071
|
+
egressGuaranteedMbps: bootstrapNumber(`Guest ${index} guaranteed egress Mbps`),
|
|
18072
|
+
egressBurstMbps: bootstrapNumber(`Guest ${index} burst egress Mbps`),
|
|
18073
|
+
confidential: bootstrapBoolean(`Guest ${index} requires confidential SEV-SNP compute?`)
|
|
18074
|
+
});
|
|
18075
|
+
const nodes = [
|
|
18076
|
+
node(1),
|
|
18077
|
+
node(2),
|
|
18078
|
+
node(3)
|
|
18079
|
+
];
|
|
18080
|
+
const rehearsalNode = bootstrapBoolean("Configure an optional fourth rehearsal guest?", "yes") ? node(4) : undefined;
|
|
18081
|
+
return { nodes, ...rehearsalNode ? { rehearsalNode } : {} };
|
|
18082
|
+
}
|
|
17759
18083
|
function writeMetalOperatorFiles(directory) {
|
|
17760
18084
|
const output = genesisOutputDirectory(directory);
|
|
17761
18085
|
const config = interactiveMetalBootstrap();
|
|
@@ -17766,17 +18090,24 @@ function writeMetalOperatorFiles(directory) {
|
|
|
17766
18090
|
kind: "metal-remote",
|
|
17767
18091
|
metalConfigFile: metalConfig,
|
|
17768
18092
|
target: { ...target, ...identity },
|
|
17769
|
-
genesis: {
|
|
17770
|
-
environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
|
|
17771
|
-
sshPublicKeyFiles: [identity.identityPublicKeyFile]
|
|
17772
|
-
}
|
|
18093
|
+
genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
|
|
17773
18094
|
});
|
|
17774
18095
|
return { metalConfig, remoteRequest };
|
|
17775
18096
|
}
|
|
17776
18097
|
function writePlatformGenesisFleet(directory) {
|
|
17777
18098
|
const output = genesisOutputDirectory(directory);
|
|
17778
|
-
const
|
|
17779
|
-
const
|
|
18099
|
+
const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
18100
|
+
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile);
|
|
18101
|
+
const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
18102
|
+
const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
18103
|
+
const fleet = metalRequest.genesis.nodes;
|
|
18104
|
+
const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile).metalHostname;
|
|
18105
|
+
const template = interactiveBootstrap("platform", fleet);
|
|
18106
|
+
const region = {
|
|
18107
|
+
label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
|
|
18108
|
+
country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
|
|
18109
|
+
city: bootstrapAnswer("Region city")
|
|
18110
|
+
};
|
|
17780
18111
|
const nodes = fleet.map((guest, index) => index === 0 ? {
|
|
17781
18112
|
nodeHostname: template.nodeHostname,
|
|
17782
18113
|
cloudflareHandoffFile: template.cloudflareHandoff.handoffFile
|
|
@@ -17784,34 +18115,47 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17784
18115
|
nodeHostname: bootstrapAnswer(`${guest.name} public node hostname`),
|
|
17785
18116
|
cloudflareHandoffFile: bootstrapAnswer(`${guest.name} node-specific Cloudflare host handoff`)
|
|
17786
18117
|
});
|
|
17787
|
-
const configs = platformGenesisBootstrapConfigs(template, nodes).map((config) => {
|
|
18118
|
+
const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
|
|
17788
18119
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
17789
18120
|
writeBootstrapConfig(path, config);
|
|
17790
18121
|
return path;
|
|
17791
18122
|
});
|
|
17792
|
-
const identity = operatorIdentityCoordinates();
|
|
17793
|
-
const useJump = bootstrapAnswer("Reach genesis computes through the Metal SSH jump? (yes/no)", "yes") === "yes";
|
|
17794
|
-
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
17795
18123
|
const requests = configs.map((platformConfigFile, index) => {
|
|
17796
18124
|
const guest = fleet[index];
|
|
17797
|
-
const
|
|
18125
|
+
const host = guestHostKeys.nodes.find(({ name }) => name === guest.name);
|
|
17798
18126
|
return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
|
|
17799
18127
|
kind: "platform-remote",
|
|
17800
18128
|
platformConfigFile,
|
|
17801
|
-
target: {
|
|
18129
|
+
target: {
|
|
18130
|
+
address: guest.address,
|
|
18131
|
+
port: 22,
|
|
18132
|
+
user: "forgezero",
|
|
18133
|
+
hostKey: host.hostKey,
|
|
18134
|
+
hostKeySha256: host.hostKeySha256,
|
|
18135
|
+
identityPublicKeyFile: metalRequest.target.identityPublicKeyFile,
|
|
18136
|
+
agentSocket: metalRequest.target.agentSocket,
|
|
18137
|
+
jump: {
|
|
18138
|
+
address: metalRequest.target.address,
|
|
18139
|
+
port: metalRequest.target.port,
|
|
18140
|
+
user: metalRequest.target.user,
|
|
18141
|
+
hostKey: metalRequest.target.hostKey,
|
|
18142
|
+
hostKeySha256: metalRequest.target.hostKeySha256
|
|
18143
|
+
}
|
|
18144
|
+
}
|
|
17802
18145
|
});
|
|
17803
18146
|
});
|
|
17804
18147
|
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
17805
18148
|
return { configs, requests, fleetRequest };
|
|
17806
18149
|
}
|
|
17807
|
-
function interactiveBootstrap(kind,
|
|
18150
|
+
function interactiveBootstrap(kind, genesisGuests) {
|
|
17808
18151
|
if (!process.stdin.isTTY)
|
|
17809
18152
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
17810
18153
|
const environment = bootstrapAnswer("Environment (production/development)", "production");
|
|
17811
18154
|
if (environment !== "production" && environment !== "development") {
|
|
17812
18155
|
throw new Error("Environment must be production or development");
|
|
17813
18156
|
}
|
|
17814
|
-
const
|
|
18157
|
+
const genesis = genesisGuests !== undefined;
|
|
18158
|
+
const fleet = genesisGuests;
|
|
17815
18159
|
const profile = genesis ? "platform-db-api" : bootstrapAnswer("Profile (platform-db-api/platform-api)", "platform-db-api");
|
|
17816
18160
|
const role = genesis ? "master" : profile === "platform-api" ? "none" : bootstrapAnswer("Database role (master/joiner)", "master");
|
|
17817
18161
|
const agency = genesis ? "member" : role === "master" ? "member" : role === "none" ? "none" : bootstrapAnswer("Agency participation (member/none)", "member");
|
|
@@ -17819,12 +18163,13 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17819
18163
|
const address = genesis ? fleet[0].address : role === "none" ? undefined : bootstrapAnswer("Private database address");
|
|
17820
18164
|
const master = role === "joiner" ? bootstrapAnswer("Master starter private address") : undefined;
|
|
17821
18165
|
const coordinators = genesis ? fleet.map((guest) => `http://${guest.address}:8529`) : bootstrapAnswer("Writable Coordinator origins (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
|
|
17822
|
-
const computeReference = genesis ? fleet[0].name : bootstrapAnswer("API-owned compute reference"
|
|
18166
|
+
const computeReference = genesis ? fleet[0].name : bootstrapAnswer("API-owned compute reference");
|
|
17823
18167
|
const nodeHostname = bootstrapAnswer(genesis ? `${computeReference} public node hostname` : "Public node hostname");
|
|
17824
18168
|
const apiUrl = bootstrapAnswer("Platform API URL");
|
|
17825
18169
|
const appOrigin = bootstrapAnswer("Public App origin");
|
|
17826
18170
|
const bundleFile = bootstrapAnswer("Bootstrap API Git bundle (absolute path)");
|
|
17827
18171
|
const manifestFile = bootstrapAnswer("Bootstrap bundle manifest (absolute path)", `${bundleFile}.json`);
|
|
18172
|
+
const bundleBranch = bootstrapAnswer("Bootstrap bundle Git branch");
|
|
17828
18173
|
const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
|
|
17829
18174
|
const collectorUnit = FORGEZERO_OTEL_COLLECTOR_UNIT;
|
|
17830
18175
|
const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
|
|
@@ -17837,7 +18182,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17837
18182
|
const smtpHost = emailProvider === "smtp" ? bootstrapAnswer("SMTP host") : undefined;
|
|
17838
18183
|
const smtpPort = emailProvider === "smtp" ? Number(bootstrapAnswer("SMTP port", "587")) : undefined;
|
|
17839
18184
|
const smtpUser = emailProvider === "smtp" ? bootstrapAnswer("SMTP user") : undefined;
|
|
17840
|
-
const jetemailEu = emailProvider === "jetemail" ?
|
|
18185
|
+
const jetemailEu = emailProvider === "jetemail" ? bootstrapBoolean("Use JetEmail EU processing?", "no") : undefined;
|
|
17841
18186
|
const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
|
|
17842
18187
|
const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
|
|
17843
18188
|
const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
|
|
@@ -17853,7 +18198,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17853
18198
|
computeReference,
|
|
17854
18199
|
nodeHostname,
|
|
17855
18200
|
apiUrl,
|
|
17856
|
-
bootstrapBundle: { bundleFile, manifestFile },
|
|
18201
|
+
bootstrapBundle: { bundleFile, manifestFile, branch: bundleBranch },
|
|
17857
18202
|
telemetryEndpoint,
|
|
17858
18203
|
database: {
|
|
17859
18204
|
role,
|
|
@@ -17907,12 +18252,12 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17907
18252
|
keepReleases: 5
|
|
17908
18253
|
},
|
|
17909
18254
|
firewall: {
|
|
17910
|
-
enabled:
|
|
18255
|
+
enabled: bootstrapBoolean("Enable host firewall?", "yes"),
|
|
17911
18256
|
sshPort: Number(bootstrapAnswer("SSH port", "22")),
|
|
17912
18257
|
privateCidrs: bootstrapAnswer("Private cluster CIDRs (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean)
|
|
17913
18258
|
},
|
|
17914
18259
|
installCloudflared: Boolean(cloudflareHandoffFile),
|
|
17915
|
-
installWarp: !genesis && cloudflareHandoffFile ?
|
|
18260
|
+
installWarp: !genesis && cloudflareHandoffFile ? bootstrapBoolean("Configure Cloudflare Mesh/WARP from this handoff?", "no") : false,
|
|
17916
18261
|
...cloudflareHandoffFile && cloudflareNodeName ? {
|
|
17917
18262
|
cloudflareHandoff: { handoffFile: cloudflareHandoffFile, nodeName: cloudflareNodeName }
|
|
17918
18263
|
} : {}
|
|
@@ -17997,11 +18342,14 @@ async function cmdBootstrap(options, args) {
|
|
|
17997
18342
|
}
|
|
17998
18343
|
if (operation === "metal" && args[1] === "remote") {
|
|
17999
18344
|
const mode = args[2];
|
|
18000
|
-
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "status"].includes(mode) || args[3] !== undefined) {
|
|
18001
|
-
throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
18345
|
+
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "host-keys", "rehearsal-host-keys", "status"].includes(mode) || args[3] !== undefined) {
|
|
18346
|
+
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]");
|
|
18002
18347
|
}
|
|
18003
18348
|
if (!options.bootstrapConfigPath)
|
|
18004
18349
|
throw new Error("remote metal bootstrap requires --bootstrap-config");
|
|
18350
|
+
if ((mode === "host-keys" || mode === "rehearsal-host-keys") && !options.outputPath) {
|
|
18351
|
+
throw new Error(`${mode} requires --output <absolute-owner-only-evidence.json>`);
|
|
18352
|
+
}
|
|
18005
18353
|
const request = readOperatorMetalBootstrapRequest(options.bootstrapConfigPath);
|
|
18006
18354
|
const plan2 = planOperatorMetalBootstrap(request, mode);
|
|
18007
18355
|
if (!options.apply) {
|
|
@@ -18009,7 +18357,15 @@ async function cmdBootstrap(options, args) {
|
|
|
18009
18357
|
out.step("Review the pinned metal target and exact genesis nodes, then repeat with --apply to contact the host.");
|
|
18010
18358
|
return 0;
|
|
18011
18359
|
}
|
|
18012
|
-
|
|
18360
|
+
const result2 = await applyOperatorMetalBootstrap(request, mode);
|
|
18361
|
+
if (mode === "host-keys" || mode === "rehearsal-host-keys") {
|
|
18362
|
+
const evidence = JSON.parse(result2.output);
|
|
18363
|
+
const path = writeOperatorGuestHostKeyEvidence(options.outputPath, evidence, request);
|
|
18364
|
+
readOperatorGuestHostKeyEvidence(path, request);
|
|
18365
|
+
out.line(JSON.stringify({ plan: result2.plan, evidencePath: path, mode: "0600" }, null, 2));
|
|
18366
|
+
return 0;
|
|
18367
|
+
}
|
|
18368
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
18013
18369
|
return 0;
|
|
18014
18370
|
}
|
|
18015
18371
|
if (operation === "platform" && args[1] === "cloudflare") {
|
|
@@ -18097,7 +18453,7 @@ async function cmdBootstrap(options, args) {
|
|
|
18097
18453
|
return 0;
|
|
18098
18454
|
}
|
|
18099
18455
|
if (!["platform", "repair"].includes(operation)) {
|
|
18100
|
-
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]");
|
|
18456
|
+
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]");
|
|
18101
18457
|
}
|
|
18102
18458
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
18103
18459
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -18501,9 +18857,9 @@ async function cmdDeploy(options, args) {
|
|
|
18501
18857
|
if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18502
18858
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18503
18859
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18504
|
-
const
|
|
18860
|
+
const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
18505
18861
|
const problems = [
|
|
18506
|
-
...
|
|
18862
|
+
...current2 ? [] : [`${DEPLOY_PLAN_FILE} is stale; run \`fz deploy compile\``],
|
|
18507
18863
|
...canonicalJson(expected.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : []
|
|
18508
18864
|
];
|
|
18509
18865
|
const summary = { source: expected.source, output: actual.path, digest: actual.digest, version: actual.plan.version, name: actual.plan.name, ready: problems.length === 0, problems };
|
|
@@ -18692,7 +19048,7 @@ function usage() {
|
|
|
18692
19048
|
Persist secret-free public-node acceptance, then remove
|
|
18693
19049
|
the completed non-secret checkpoint and node handoffs
|
|
18694
19050
|
fz bootstrap metal Plan/install an identity-only physical provisioner
|
|
18695
|
-
fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status>
|
|
19051
|
+
fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>
|
|
18696
19052
|
Install/bootstrap one pinned blank metal target, then
|
|
18697
19053
|
provision the exact selected three-node genesis seed
|
|
18698
19054
|
fz bootstrap status Verify persisted profile and supervised units
|
|
@@ -18723,8 +19079,9 @@ function usage() {
|
|
|
18723
19079
|
Verify recovered data and bounded cluster topology
|
|
18724
19080
|
fz db consolidate Plan/apply a digest-bound offline schema consolidation
|
|
18725
19081
|
fz rehearse community-cluster-api
|
|
18726
|
-
Plan the
|
|
18727
|
-
--
|
|
19082
|
+
Plan the reviewed four-node Community 3.11.14 rehearsal;
|
|
19083
|
+
requires --request=<metal-remote.json>; mutation also requires
|
|
19084
|
+
the exact disposable-fleet confirmation printed by the plan
|
|
18728
19085
|
fz rehearse platform-fleet
|
|
18729
19086
|
Verify a development or production fleet; use --plan offline
|
|
18730
19087
|
fz rehearse platform-recovery
|