@forgezero/agent 0.1.64 → 0.1.67
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 +745 -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/cli/agent-install.d.ts +9 -0
- 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 +501 -258
- 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 +20 -4
- package/dist/operator-bootstrap.js +429 -203
- 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.67";
|
|
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",
|
|
@@ -6596,6 +6624,16 @@ async function localRunner(operation) {
|
|
|
6596
6624
|
`, exitCode: 1 };
|
|
6597
6625
|
}
|
|
6598
6626
|
}
|
|
6627
|
+
function resolveGitIdentityInstallOptions(environment) {
|
|
6628
|
+
const generateGitIdentity = environment.FZ_GENERATE_GIT_DEPLOY_KEY === "true";
|
|
6629
|
+
const explicitCredential = environment.FZ_GIT_CREDENTIAL_PATH?.trim() || undefined;
|
|
6630
|
+
const explicitPublicKey = environment.FZ_GIT_PUBLIC_KEY_PATH?.trim() || undefined;
|
|
6631
|
+
return {
|
|
6632
|
+
gitCredentialPath: explicitCredential ?? (generateGitIdentity ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined),
|
|
6633
|
+
gitPublicKeyPath: explicitPublicKey ?? (generateGitIdentity ? "/etc/forgezero/git/deploy.pub" : undefined),
|
|
6634
|
+
generateGitIdentity
|
|
6635
|
+
};
|
|
6636
|
+
}
|
|
6599
6637
|
function planInstall(options) {
|
|
6600
6638
|
const { capabilities, ...unit } = options;
|
|
6601
6639
|
return planProvision({ ...unit, mode: modeFor(capabilities) });
|
|
@@ -10415,13 +10453,13 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
10415
10453
|
return;
|
|
10416
10454
|
const expected = {
|
|
10417
10455
|
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
10418
|
-
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
10419
|
-
"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"] },
|
|
10420
10458
|
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
10421
10459
|
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
10422
10460
|
"forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
|
|
10423
|
-
"forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
|
|
10424
|
-
"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"] },
|
|
10425
10463
|
"forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
|
|
10426
10464
|
};
|
|
10427
10465
|
const contract = expected[requirement2.provider];
|
|
@@ -11577,6 +11615,143 @@ import {
|
|
|
11577
11615
|
import { dirname as dirname9 } from "path";
|
|
11578
11616
|
import { fileURLToPath } from "url";
|
|
11579
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
|
+
|
|
11580
11755
|
// src/platform-bootstrap-runtime.ts
|
|
11581
11756
|
var safeAtom = (name, value) => {
|
|
11582
11757
|
if (!value || /[\0\r\n]/.test(value))
|
|
@@ -11608,6 +11783,50 @@ var httpsOrigin = (name, raw) => {
|
|
|
11608
11783
|
}
|
|
11609
11784
|
return value.origin;
|
|
11610
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
|
+
}
|
|
11611
11830
|
var systemdValue = (name, raw) => {
|
|
11612
11831
|
if (/[\0\r\n]/.test(raw))
|
|
11613
11832
|
throw new Error(`${name} must be single-line.`);
|
|
@@ -11693,12 +11912,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
11693
11912
|
throw new Error("realtime.producer is invalid.");
|
|
11694
11913
|
}
|
|
11695
11914
|
}
|
|
11915
|
+
const initialInventory = input.initialInventory ? validatePlatformInitialInventory(input.initialInventory) : undefined;
|
|
11696
11916
|
return {
|
|
11697
11917
|
...input,
|
|
11698
11918
|
databaseCoordinators: coordinators,
|
|
11699
11919
|
appOrigin: httpsOrigin("appOrigin", input.appOrigin),
|
|
11700
11920
|
apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
|
|
11701
|
-
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
|
|
11921
|
+
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint),
|
|
11922
|
+
...initialInventory ? { initialInventory } : {}
|
|
11702
11923
|
};
|
|
11703
11924
|
}
|
|
11704
11925
|
function renderPlatformSharedEnvironment(input) {
|
|
@@ -11761,7 +11982,8 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
11761
11982
|
FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
|
|
11762
11983
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
11763
11984
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
11764
|
-
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? ""
|
|
11985
|
+
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
11986
|
+
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
11765
11987
|
};
|
|
11766
11988
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
11767
11989
|
` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
|
|
@@ -12053,12 +12275,12 @@ async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
|
|
|
12053
12275
|
}
|
|
12054
12276
|
const path = `/accounts/${config.accountId}/teamnet/routes`;
|
|
12055
12277
|
const routes = await cf(config, path, {}, fetcher);
|
|
12056
|
-
const
|
|
12057
|
-
if (
|
|
12058
|
-
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) {
|
|
12059
12281
|
throw new Error(`private route ${config.network} already belongs to another Tunnel`);
|
|
12060
12282
|
}
|
|
12061
|
-
return { route:
|
|
12283
|
+
return { route: current2, created: false };
|
|
12062
12284
|
}
|
|
12063
12285
|
const route = await cf(config, path, {
|
|
12064
12286
|
method: "POST",
|
|
@@ -12107,7 +12329,7 @@ async function ensureCloudflareWarpNetworkIncludes(config, fetcher = fetch) {
|
|
|
12107
12329
|
async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
12108
12330
|
const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
|
|
12109
12331
|
const dnsPath = `/zones/${config.zoneId}/dns_records`;
|
|
12110
|
-
const [
|
|
12332
|
+
const [current2, records] = await Promise.all([
|
|
12111
12333
|
cf(config, tunnelPath, {}, fetcher),
|
|
12112
12334
|
cf(config, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
|
|
12113
12335
|
]);
|
|
@@ -12118,7 +12340,7 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
12118
12340
|
if (existingRecord && (existingRecord.type !== "CNAME" || existingRecord.name && existingRecord.name.toLowerCase() !== config.hostname.toLowerCase())) {
|
|
12119
12341
|
throw new Error(`Cloudflare DNS hostname ${config.hostname} is already owned by an incompatible ${existingRecord.type ?? "unknown"} record`);
|
|
12120
12342
|
}
|
|
12121
|
-
const existing =
|
|
12343
|
+
const existing = current2.config?.ingress ?? [];
|
|
12122
12344
|
const catchAll = existing.filter((rule) => !("hostname" in rule));
|
|
12123
12345
|
const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
|
|
12124
12346
|
const desiredIngress = [
|
|
@@ -13205,7 +13427,7 @@ async function sha256File(path) {
|
|
|
13205
13427
|
});
|
|
13206
13428
|
return hash.digest("hex");
|
|
13207
13429
|
}
|
|
13208
|
-
var
|
|
13430
|
+
var bootstrapBundleBranch = (value) => {
|
|
13209
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("-")) {
|
|
13210
13432
|
throw new Error("bootstrap bundle branch is malformed");
|
|
13211
13433
|
}
|
|
@@ -13222,7 +13444,7 @@ function parseBootstrapBundleManifest(value) {
|
|
|
13222
13444
|
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
13223
13445
|
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
13224
13446
|
}
|
|
13225
|
-
|
|
13447
|
+
bootstrapBundleBranch(source.branch);
|
|
13226
13448
|
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
13227
13449
|
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
13228
13450
|
}
|
|
@@ -13267,7 +13489,7 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
13267
13489
|
const repositoryRoot = resolve6(input.repositoryRoot);
|
|
13268
13490
|
const outputPath = resolve6(input.outputPath);
|
|
13269
13491
|
const manifestPath = `${outputPath}.json`;
|
|
13270
|
-
const branch =
|
|
13492
|
+
const branch = bootstrapBundleBranch(input.branch);
|
|
13271
13493
|
if (!isAbsolute2(input.outputPath) || outputPath !== input.outputPath) {
|
|
13272
13494
|
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
13273
13495
|
}
|
|
@@ -13492,6 +13714,12 @@ function validateBootstrapConfig(value) {
|
|
|
13492
13714
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
13493
13715
|
}
|
|
13494
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
|
+
}
|
|
13495
13723
|
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
13496
13724
|
throw new Error("gitDeployKey must be boolean");
|
|
13497
13725
|
}
|
|
@@ -13523,6 +13751,7 @@ function validateBootstrapConfig(value) {
|
|
|
13523
13751
|
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
13524
13752
|
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
13525
13753
|
}
|
|
13754
|
+
bootstrapBundleBranch(value.bootstrapBundle.branch);
|
|
13526
13755
|
let api;
|
|
13527
13756
|
try {
|
|
13528
13757
|
api = new URL(value.apiUrl);
|
|
@@ -13577,9 +13806,19 @@ function validateBootstrapConfig(value) {
|
|
|
13577
13806
|
}
|
|
13578
13807
|
function planBootstrap(input, initialized = false) {
|
|
13579
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
|
+
}
|
|
13580
13819
|
const software = config.kind === "enrolled-compute" ? [
|
|
13581
|
-
...
|
|
13582
|
-
...config.bootstrapRunner && !
|
|
13820
|
+
...enrolledSoftware,
|
|
13821
|
+
...config.bootstrapRunner && !enrolledSoftware.some(({ id: id2 }) => id2 === "openssh-client") ? [{ id: "openssh-client", version: enrolledOsVersion }] : [],
|
|
13583
13822
|
...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
|
|
13584
13823
|
...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
|
|
13585
13824
|
] : [
|
|
@@ -13814,9 +14053,8 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
|
13814
14053
|
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13815
14054
|
}
|
|
13816
14055
|
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
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");
|
|
13820
14058
|
}
|
|
13821
14059
|
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
13822
14060
|
if (digest !== manifest.sha256)
|
|
@@ -13830,6 +14068,7 @@ function bootstrapIdentity(config) {
|
|
|
13830
14068
|
if (config.kind === "enrolled-compute")
|
|
13831
14069
|
return {
|
|
13832
14070
|
kind: config.kind,
|
|
14071
|
+
operatingSystem: config.operatingSystem,
|
|
13833
14072
|
apiUrl: config.apiUrl,
|
|
13834
14073
|
realm: config.realm,
|
|
13835
14074
|
nodeHostname: config.nodeHostname,
|
|
@@ -14443,7 +14682,7 @@ function strictBootstrapDocument(value) {
|
|
|
14443
14682
|
"bootstrapRunner"
|
|
14444
14683
|
], "bootstrap config");
|
|
14445
14684
|
if (root.kind === "platform") {
|
|
14446
|
-
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
14685
|
+
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile", "branch"], "bootstrap bundle config");
|
|
14447
14686
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14448
14687
|
if (root.cloudflareHandoff !== undefined)
|
|
14449
14688
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -14490,9 +14729,32 @@ function strictBootstrapDocument(value) {
|
|
|
14490
14729
|
"otlpTraceSampleRatio",
|
|
14491
14730
|
"backup",
|
|
14492
14731
|
"cloudflare",
|
|
14493
|
-
"realtime"
|
|
14732
|
+
"realtime",
|
|
14733
|
+
"initialInventory"
|
|
14494
14734
|
], "runtime environment");
|
|
14495
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
|
+
}
|
|
14496
14758
|
if (environment.email !== undefined) {
|
|
14497
14759
|
const email = exactKeys2(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
|
|
14498
14760
|
if (email.provider === "smtp")
|
|
@@ -14509,6 +14771,7 @@ function strictBootstrapDocument(value) {
|
|
|
14509
14771
|
if (environment.realtime !== undefined)
|
|
14510
14772
|
exactKeys2(environment.realtime, ["workerScriptName", "endpoint", "producer"], "realtime runtime config");
|
|
14511
14773
|
} else if (root.kind === "enrolled-compute") {
|
|
14774
|
+
exactKeys2(root.operatingSystem, ["id", "version", "architecture"], "enrolled compute operating system");
|
|
14512
14775
|
if (root.cloudflareHandoff !== undefined)
|
|
14513
14776
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
14514
14777
|
if (root.bootstrapRunner !== undefined)
|
|
@@ -14840,19 +15103,7 @@ import { join as join8 } from "path";
|
|
|
14840
15103
|
// src/metal-provision.ts
|
|
14841
15104
|
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join7 } from "path";
|
|
14842
15105
|
import { isIP as isIP4 } from "net";
|
|
14843
|
-
|
|
14844
|
-
// src/ubuntu.ts
|
|
14845
|
-
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
14846
|
-
key: "ubuntu-resolute-20260731",
|
|
14847
|
-
family: "ubuntu-26.04",
|
|
14848
|
-
version: "2026-07-31",
|
|
14849
|
-
label: "Ubuntu 26.04 LTS Resolute",
|
|
14850
|
-
url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
|
|
14851
|
-
sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
|
|
14852
|
-
});
|
|
14853
|
-
|
|
14854
|
-
// src/metal-provision.ts
|
|
14855
|
-
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}$/;
|
|
14856
15107
|
var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
|
|
14857
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)$/;
|
|
14858
15109
|
var LINUX_LIST = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/;
|
|
@@ -14878,7 +15129,7 @@ function membersOfLinuxList(value, label) {
|
|
|
14878
15129
|
return members;
|
|
14879
15130
|
}
|
|
14880
15131
|
function validateMetalProfile(profile) {
|
|
14881
|
-
if (!
|
|
15132
|
+
if (!SAFE_NAME2.test(profile.volumeGroup))
|
|
14882
15133
|
throw new MetalProvisionError("invalid volume group");
|
|
14883
15134
|
if (!DEVICE.test(profile.bridge))
|
|
14884
15135
|
throw new MetalProvisionError("invalid bridge");
|
|
@@ -14900,9 +15151,16 @@ function validateMetalProfile(profile) {
|
|
|
14900
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"))
|
|
14901
15152
|
throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
|
|
14902
15153
|
const imageKeys = Object.keys(profile.images);
|
|
14903
|
-
if (imageKeys.length
|
|
14904
|
-
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
|
+
}
|
|
14905
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");
|
|
14906
15164
|
if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
|
|
14907
15165
|
throw new MetalProvisionError("at least one exclusive CPU pool is required");
|
|
14908
15166
|
}
|
|
@@ -14911,7 +15169,7 @@ function validateMetalProfile(profile) {
|
|
|
14911
15169
|
const assignedMemory = new Set;
|
|
14912
15170
|
let poolsWithMemory = 0;
|
|
14913
15171
|
for (const pool of profile.cpuPools) {
|
|
14914
|
-
if (!
|
|
15172
|
+
if (!SAFE_NAME2.test(pool.key) || keys.has(pool.key))
|
|
14915
15173
|
throw new MetalProvisionError("invalid or duplicate CPU pool key");
|
|
14916
15174
|
keys.add(pool.key);
|
|
14917
15175
|
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
@@ -15153,10 +15411,9 @@ function validateMetalBootstrapConfig(config) {
|
|
|
15153
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) {
|
|
15154
15412
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
15155
15413
|
}
|
|
15156
|
-
const image
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
}
|
|
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");
|
|
15160
15417
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
|
|
15161
15418
|
throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
|
|
15162
15419
|
}
|
|
@@ -15458,9 +15715,11 @@ var preflight = async (config, exec) => {
|
|
|
15458
15715
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
15459
15716
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
15460
15717
|
}
|
|
15461
|
-
|
|
15462
|
-
|
|
15463
|
-
|
|
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
|
+
}
|
|
15464
15723
|
};
|
|
15465
15724
|
var assertSupportedMetalHost = () => {
|
|
15466
15725
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
@@ -15472,34 +15731,38 @@ var assertSupportedMetalHost = () => {
|
|
|
15472
15731
|
}
|
|
15473
15732
|
};
|
|
15474
15733
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
15475
|
-
const image
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
"--fail",
|
|
15484
|
-
"--location",
|
|
15485
|
-
"--proto",
|
|
15486
|
-
"=https",
|
|
15487
|
-
"--tlsv1.2",
|
|
15488
|
-
"--output",
|
|
15489
|
-
temporary,
|
|
15490
|
-
SUPPORTED_GUEST_IMAGE.url
|
|
15491
|
-
]);
|
|
15492
|
-
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
15493
|
-
if (digest !== image.sha256)
|
|
15494
|
-
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
15495
|
-
chmodSync5(temporary, 292);
|
|
15496
|
-
chownSync(temporary, 0, 0);
|
|
15497
|
-
renameSync9(temporary, image.path);
|
|
15498
|
-
} 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}`;
|
|
15499
15742
|
try {
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
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
|
+
}
|
|
15503
15766
|
}
|
|
15504
15767
|
};
|
|
15505
15768
|
async function applyMetalBootstrap(config, options) {
|
|
@@ -15659,10 +15922,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15659
15922
|
])).exitCode !== 0) {
|
|
15660
15923
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
15661
15924
|
}
|
|
15662
|
-
|
|
15663
|
-
|
|
15925
|
+
imageVerified = true;
|
|
15926
|
+
for (const image of Object.values(profile.images)) {
|
|
15927
|
+
if (!existsSync10(image.path)) {
|
|
15928
|
+
imageVerified = false;
|
|
15929
|
+
break;
|
|
15930
|
+
}
|
|
15664
15931
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
15665
|
-
|
|
15932
|
+
if (digest !== image.sha256) {
|
|
15933
|
+
imageVerified = false;
|
|
15934
|
+
break;
|
|
15935
|
+
}
|
|
15666
15936
|
}
|
|
15667
15937
|
if (!imageVerified)
|
|
15668
15938
|
problems.push("pinned guest image is absent or has the wrong digest");
|
|
@@ -15722,126 +15992,6 @@ import { isIP as isIP6 } from "net";
|
|
|
15722
15992
|
import { tmpdir } from "os";
|
|
15723
15993
|
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15724
15994
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15725
|
-
|
|
15726
|
-
// src/platform-genesis.ts
|
|
15727
|
-
var PLATFORM_GENESIS_ENVIRONMENTS = ["production", "development"];
|
|
15728
|
-
var productionShape = {
|
|
15729
|
-
physicalCores: 8,
|
|
15730
|
-
vcpu: 16,
|
|
15731
|
-
memoryGib: 24,
|
|
15732
|
-
diskGib: 200,
|
|
15733
|
-
egressGuaranteedMbps: 1000,
|
|
15734
|
-
egressBurstMbps: 2000
|
|
15735
|
-
};
|
|
15736
|
-
var developmentShape = {
|
|
15737
|
-
physicalCores: 2,
|
|
15738
|
-
vcpu: 4,
|
|
15739
|
-
memoryGib: 8,
|
|
15740
|
-
diskGib: 64,
|
|
15741
|
-
egressGuaranteedMbps: 100,
|
|
15742
|
-
egressBurstMbps: 250
|
|
15743
|
-
};
|
|
15744
|
-
var PLATFORM_GENESIS_FLEETS = {
|
|
15745
|
-
production: [1, 2, 3].map((node) => ({
|
|
15746
|
-
name: `fz-n${node}`,
|
|
15747
|
-
address: `10.42.0.${10 + node}`,
|
|
15748
|
-
cpuPoolKey: `node${node}`,
|
|
15749
|
-
...productionShape
|
|
15750
|
-
})),
|
|
15751
|
-
development: [1, 2, 3].map((node) => ({
|
|
15752
|
-
name: `dev-fz-n${node}`,
|
|
15753
|
-
address: `10.42.0.${20 + node}`,
|
|
15754
|
-
cpuPoolKey: `node${node + 3}`,
|
|
15755
|
-
...developmentShape
|
|
15756
|
-
}))
|
|
15757
|
-
};
|
|
15758
|
-
var DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST = {
|
|
15759
|
-
name: "dev-fz-n4",
|
|
15760
|
-
address: "10.42.0.24",
|
|
15761
|
-
cpuPoolKey: "node7",
|
|
15762
|
-
...developmentShape
|
|
15763
|
-
};
|
|
15764
|
-
var publicKey = (value) => {
|
|
15765
|
-
const normalized = value.trim();
|
|
15766
|
-
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(normalized)) {
|
|
15767
|
-
throw new Error("platform genesis SSH public key is malformed");
|
|
15768
|
-
}
|
|
15769
|
-
return normalized;
|
|
15770
|
-
};
|
|
15771
|
-
function platformGenesisClaims(environment, sshPublicKeys) {
|
|
15772
|
-
if (!PLATFORM_GENESIS_ENVIRONMENTS.includes(environment)) {
|
|
15773
|
-
throw new Error("platform genesis environment must be production or development");
|
|
15774
|
-
}
|
|
15775
|
-
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
15776
|
-
if (keys.length === 0)
|
|
15777
|
-
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
15778
|
-
return PLATFORM_GENESIS_FLEETS[environment].map((guest) => ({
|
|
15779
|
-
computeKey: `platform:${guest.name}`,
|
|
15780
|
-
claimToken: "offline-platform-genesis",
|
|
15781
|
-
claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
|
|
15782
|
-
attempt: 1,
|
|
15783
|
-
action: "create",
|
|
15784
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15785
|
-
access: { sshUser: "forgezero", sshPublicKeys: keys },
|
|
15786
|
-
spec: {
|
|
15787
|
-
reference: `platform:${guest.name}`,
|
|
15788
|
-
imageKey: SUPPORTED_GUEST_IMAGE.key,
|
|
15789
|
-
guestName: guest.name,
|
|
15790
|
-
guestAddress: guest.address,
|
|
15791
|
-
cpuPoolKey: guest.cpuPoolKey,
|
|
15792
|
-
physicalCores: guest.physicalCores,
|
|
15793
|
-
vcpu: guest.vcpu,
|
|
15794
|
-
memoryGib: guest.memoryGib,
|
|
15795
|
-
diskGib: guest.diskGib,
|
|
15796
|
-
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
15797
|
-
egressBurstMbps: guest.egressBurstMbps,
|
|
15798
|
-
confidential: true
|
|
15799
|
-
}
|
|
15800
|
-
}));
|
|
15801
|
-
}
|
|
15802
|
-
function platformCommunityRehearsalClaims(sshPublicKeys) {
|
|
15803
|
-
const keys = [...new Set(sshPublicKeys.map(publicKey))];
|
|
15804
|
-
if (keys.length === 0)
|
|
15805
|
-
throw new Error("platform genesis requires at least one operator SSH public key");
|
|
15806
|
-
const seed = platformGenesisClaims("development", keys);
|
|
15807
|
-
const guest = DEVELOPMENT_COMMUNITY_REHEARSAL_GUEST;
|
|
15808
|
-
return [...seed, {
|
|
15809
|
-
computeKey: `platform:${guest.name}`,
|
|
15810
|
-
claimToken: "offline-platform-genesis",
|
|
15811
|
-
claimExpiresAtTs: Number.MAX_SAFE_INTEGER,
|
|
15812
|
-
attempt: 1,
|
|
15813
|
-
action: "create",
|
|
15814
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15815
|
-
access: { sshUser: "forgezero", sshPublicKeys: keys },
|
|
15816
|
-
spec: {
|
|
15817
|
-
reference: `platform:${guest.name}`,
|
|
15818
|
-
imageKey: SUPPORTED_GUEST_IMAGE.key,
|
|
15819
|
-
guestName: guest.name,
|
|
15820
|
-
guestAddress: guest.address,
|
|
15821
|
-
cpuPoolKey: guest.cpuPoolKey,
|
|
15822
|
-
physicalCores: guest.physicalCores,
|
|
15823
|
-
vcpu: guest.vcpu,
|
|
15824
|
-
memoryGib: guest.memoryGib,
|
|
15825
|
-
diskGib: guest.diskGib,
|
|
15826
|
-
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
15827
|
-
egressBurstMbps: guest.egressBurstMbps,
|
|
15828
|
-
confidential: true
|
|
15829
|
-
}
|
|
15830
|
-
}];
|
|
15831
|
-
}
|
|
15832
|
-
function platformCommunityRehearsalDeletionClaims(sshPublicKeys) {
|
|
15833
|
-
return platformCommunityRehearsalClaims(sshPublicKeys).map((claim) => ({
|
|
15834
|
-
computeKey: claim.computeKey,
|
|
15835
|
-
claimToken: claim.claimToken,
|
|
15836
|
-
claimExpiresAtTs: claim.claimExpiresAtTs,
|
|
15837
|
-
attempt: claim.attempt,
|
|
15838
|
-
action: "delete",
|
|
15839
|
-
bootstrap: { kind: "platform-genesis" },
|
|
15840
|
-
spec: claim.spec
|
|
15841
|
-
}));
|
|
15842
|
-
}
|
|
15843
|
-
|
|
15844
|
-
// src/operator-bootstrap.ts
|
|
15845
15995
|
var REQUEST_LIMIT = 64 * 1024;
|
|
15846
15996
|
var SECRET_LIMIT = 256 * 1024;
|
|
15847
15997
|
var REMOTE_STAGE = "/run/forgezero-operator-bootstrap";
|
|
@@ -16021,10 +16171,44 @@ var validateMetalRequestValue = (value) => {
|
|
|
16021
16171
|
throw new Error("metalConfigFile must be an absolute path");
|
|
16022
16172
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
16023
16173
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
16024
|
-
const genesis = exactKeys5(root.genesis, ["
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
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);
|
|
16028
16212
|
if (!Array.isArray(genesis.sshPublicKeyFiles) || genesis.sshPublicKeyFiles.length === 0 || genesis.sshPublicKeyFiles.some((entry) => typeof entry !== "string")) {
|
|
16029
16213
|
throw new Error("operator metal genesis requires SSH public-key files");
|
|
16030
16214
|
}
|
|
@@ -16039,7 +16223,8 @@ var validateMetalRequestValue = (value) => {
|
|
|
16039
16223
|
agentSocket: targetValue.agentSocket
|
|
16040
16224
|
},
|
|
16041
16225
|
genesis: {
|
|
16042
|
-
|
|
16226
|
+
nodes,
|
|
16227
|
+
...rehearsalNode ? { rehearsalNode } : {},
|
|
16043
16228
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
16044
16229
|
}
|
|
16045
16230
|
};
|
|
@@ -16054,17 +16239,17 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
16054
16239
|
}
|
|
16055
16240
|
function planOperatorMetalBootstrap(request, mode) {
|
|
16056
16241
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
16057
|
-
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && request.genesis.environment !== "development") {
|
|
16058
|
-
throw new Error("the disposable four-node Community rehearsal is development-only");
|
|
16059
|
-
}
|
|
16060
16242
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16061
|
-
|
|
16243
|
+
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
16244
|
+
throw new Error("the reviewed metal request has no rehearsal node");
|
|
16245
|
+
}
|
|
16246
|
+
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);
|
|
16062
16247
|
return {
|
|
16063
16248
|
kind: "metal-remote",
|
|
16064
16249
|
mode,
|
|
16065
16250
|
mutation: mode !== "status",
|
|
16066
16251
|
target: { address: request.target.address, port: request.target.port, user: request.target.user },
|
|
16067
|
-
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
|
|
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"] : [
|
|
16068
16253
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
16069
16254
|
"copy pinned Bun and packaged fz artifacts to private staging",
|
|
16070
16255
|
"stage the strict owner-only metal config and optional seed handoff",
|
|
@@ -16136,9 +16321,7 @@ function validatedPlatformFleet(fleet) {
|
|
|
16136
16321
|
return config;
|
|
16137
16322
|
});
|
|
16138
16323
|
const environment = configs[0].environment;
|
|
16139
|
-
const expected =
|
|
16140
|
-
if (!expected || expected.length !== 3)
|
|
16141
|
-
throw new Error("operator fleet environment is invalid");
|
|
16324
|
+
const expected = configs.map((config) => ({ name: config.computeReference, address: config.database.address }));
|
|
16142
16325
|
const identity = fleetConfigIdentity(configs[0]);
|
|
16143
16326
|
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
16144
16327
|
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
@@ -16309,13 +16492,17 @@ async function stageConfig(config, directory) {
|
|
|
16309
16492
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
16310
16493
|
}
|
|
16311
16494
|
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
16495
|
+
if (bundle.manifest.branch !== config.bootstrapBundle.branch) {
|
|
16496
|
+
throw new Error("reviewed bootstrap config branch does not match the bundle manifest");
|
|
16497
|
+
}
|
|
16312
16498
|
const bundleFiles = [
|
|
16313
16499
|
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
16314
16500
|
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
16315
16501
|
];
|
|
16316
16502
|
rewritten.bootstrapBundle = {
|
|
16317
16503
|
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
16318
|
-
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json
|
|
16504
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`,
|
|
16505
|
+
branch: bundle.manifest.branch
|
|
16319
16506
|
};
|
|
16320
16507
|
const path = join10(directory, "platform-config.json");
|
|
16321
16508
|
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
@@ -16473,13 +16660,16 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16473
16660
|
}
|
|
16474
16661
|
if (mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup") {
|
|
16475
16662
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16663
|
+
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && !request.genesis.rehearsalNode) {
|
|
16664
|
+
throw new Error("the reviewed metal request has no rehearsal node");
|
|
16665
|
+
}
|
|
16476
16666
|
const outputs = [];
|
|
16477
|
-
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(
|
|
16667
|
+
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);
|
|
16478
16668
|
for (const claim of claims) {
|
|
16479
16669
|
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)}
|
|
16480
16670
|
`));
|
|
16481
16671
|
}
|
|
16482
|
-
return { plan, output: JSON.stringify({
|
|
16672
|
+
return { plan, output: JSON.stringify({ nodes: plan.genesisNodes, outputs }) };
|
|
16483
16673
|
}
|
|
16484
16674
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
16485
16675
|
const staged = stageMetalConfig(config, directory);
|
|
@@ -16512,15 +16702,36 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16512
16702
|
}
|
|
16513
16703
|
|
|
16514
16704
|
// src/platform-genesis-config.ts
|
|
16515
|
-
function platformGenesisBootstrapConfigs(template, nodes) {
|
|
16516
|
-
const
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16705
|
+
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
|
|
16706
|
+
const fleet = validatePlatformGenesisGuests(guests);
|
|
16707
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
|
|
16708
|
+
throw new Error("platform genesis requires the reviewed metal hostname");
|
|
16709
|
+
}
|
|
16710
|
+
if (fleet.some(({ confidential }) => !confidential)) {
|
|
16711
|
+
throw new Error("platform genesis requires confidential compute");
|
|
16712
|
+
}
|
|
16713
|
+
if (nodes.length !== 3)
|
|
16714
|
+
throw new Error("platform genesis requires exactly three node coordinates");
|
|
16520
16715
|
if (nodes.some((node) => !node.nodeHostname || !node.cloudflareHandoffFile)) {
|
|
16521
16716
|
throw new Error("every platform genesis node requires its public hostname and node-specific Cloudflare handoff");
|
|
16522
16717
|
}
|
|
16523
16718
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
16719
|
+
const initialInventory = {
|
|
16720
|
+
metalHostname,
|
|
16721
|
+
region: {
|
|
16722
|
+
key: template.runtime.environment.nodeRegion,
|
|
16723
|
+
label: region.label,
|
|
16724
|
+
country: region.country,
|
|
16725
|
+
...region.city ? { city: region.city } : {},
|
|
16726
|
+
confidentialCapable: true
|
|
16727
|
+
},
|
|
16728
|
+
computes: fleet.map((guest, index) => ({
|
|
16729
|
+
...guest,
|
|
16730
|
+
confidential: guest.confidential,
|
|
16731
|
+
databaseRole: index === 0 ? "master" : "joiner",
|
|
16732
|
+
databaseAgency: "member"
|
|
16733
|
+
}))
|
|
16734
|
+
};
|
|
16524
16735
|
return fleet.map((guest, index) => {
|
|
16525
16736
|
const node = nodes[index];
|
|
16526
16737
|
const master = fleet[0].address;
|
|
@@ -16551,6 +16762,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16551
16762
|
nodeHostname: node.nodeHostname,
|
|
16552
16763
|
seedSyncPeers: fleet.filter((_, peer) => peer !== index).map((peer) => `ws://${peer.address}:${config.runtime.bluePort}/internal/seed`),
|
|
16553
16764
|
seedSyncMembers: 3,
|
|
16765
|
+
initialInventory,
|
|
16554
16766
|
custodianEmail: index === 0 ? template.runtime.environment.custodianEmail : undefined
|
|
16555
16767
|
};
|
|
16556
16768
|
return validateBootstrapConfig(config);
|
|
@@ -17456,16 +17668,13 @@ async function cmdAgent(options, args) {
|
|
|
17456
17668
|
}
|
|
17457
17669
|
const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
|
|
17458
17670
|
const enrolStatePath = "/var/lib/forgezero/enrolment.json";
|
|
17459
|
-
const
|
|
17460
|
-
const gitPublicKeyPath = process.env.FZ_GIT_PUBLIC_KEY_PATH ?? "/etc/forgezero/git/deploy.pub";
|
|
17671
|
+
const gitIdentity = resolveGitIdentityInstallOptions(process.env);
|
|
17461
17672
|
const plan = planInstall({
|
|
17462
17673
|
capabilities: await readCapabilities(localRunner),
|
|
17463
17674
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET,
|
|
17464
17675
|
seedPath: process.env.FZ_SEED_PATH ?? "/var/lib/forgezero/node.seed",
|
|
17465
17676
|
seedCredentialPath: process.env.FZ_SEED_CREDENTIAL_PATH,
|
|
17466
|
-
|
|
17467
|
-
gitPublicKeyPath,
|
|
17468
|
-
generateGitIdentity: process.env.FZ_GENERATE_GIT_DEPLOY_KEY === "true",
|
|
17677
|
+
...gitIdentity,
|
|
17469
17678
|
controlSocketPath: process.env.FZ_CONTROL_SOCKET,
|
|
17470
17679
|
repository: process.env.FZ_DEPLOY_REPO,
|
|
17471
17680
|
branch: process.env.FZ_DEPLOY_BRANCH,
|
|
@@ -17550,11 +17759,11 @@ async function cmdAgent(options, args) {
|
|
|
17550
17759
|
for (const step3 of transcript)
|
|
17551
17760
|
out.ok(step3.label);
|
|
17552
17761
|
out.ok("Agent service and socket verified");
|
|
17553
|
-
if (existsSync12(gitPublicKeyPath)) {
|
|
17762
|
+
if (gitIdentity.gitPublicKeyPath && existsSync12(gitIdentity.gitPublicKeyPath)) {
|
|
17554
17763
|
out.line();
|
|
17555
17764
|
out.line(" Compatibility SSH deploy public key:");
|
|
17556
17765
|
out.line();
|
|
17557
|
-
out.line(` ${readFileSync15(gitPublicKeyPath, "utf8").trim()}`);
|
|
17766
|
+
out.line(` ${readFileSync15(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
|
|
17558
17767
|
out.line();
|
|
17559
17768
|
}
|
|
17560
17769
|
return 0;
|
|
@@ -17579,6 +17788,12 @@ var bootstrapNumber = (question, fallback) => {
|
|
|
17579
17788
|
throw new Error(`${question} must be an integer`);
|
|
17580
17789
|
return value;
|
|
17581
17790
|
};
|
|
17791
|
+
var bootstrapBoolean = (question, fallback) => {
|
|
17792
|
+
const value = bootstrapAnswer(`${question} (yes/no)`, fallback).toLowerCase();
|
|
17793
|
+
if (value !== "yes" && value !== "no")
|
|
17794
|
+
throw new Error(`${question} must be yes or no`);
|
|
17795
|
+
return value === "yes";
|
|
17796
|
+
};
|
|
17582
17797
|
async function bootstrapSecret(question) {
|
|
17583
17798
|
if (!process.stdin.isTTY)
|
|
17584
17799
|
throw new Error(`${question} requires an interactive terminal`);
|
|
@@ -17616,7 +17831,7 @@ var parseCpuPools = (value) => value.split(";").map((entry) => entry.trim()).fil
|
|
|
17616
17831
|
function interactiveMetalBootstrap() {
|
|
17617
17832
|
if (!process.stdin.isTTY)
|
|
17618
17833
|
throw new Error("interactive metal config generation requires a terminal");
|
|
17619
|
-
const confidential =
|
|
17834
|
+
const confidential = bootstrapBoolean("Require SEV-SNP confidential guests?", "yes");
|
|
17620
17835
|
const subnetPrefix = bootstrapAnswer("Guest IPv4 subnet prefix", "10.42.0");
|
|
17621
17836
|
return validateMetalBootstrapConfig({
|
|
17622
17837
|
kind: "metal",
|
|
@@ -17636,12 +17851,10 @@ function interactiveMetalBootstrap() {
|
|
|
17636
17851
|
unitDir: "/etc/systemd/system",
|
|
17637
17852
|
apiUrl: bootstrapAnswer("Platform public API URL"),
|
|
17638
17853
|
agentTelemetryEndpoint: bootstrapAnswer("Public HTTPS Agent OTLP endpoint"),
|
|
17639
|
-
images: {
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
}
|
|
17644
|
-
},
|
|
17854
|
+
images: Object.fromEntries(OS_CATALOG.filter(({ status }) => status === "active").map((image) => [
|
|
17855
|
+
image.imageKey,
|
|
17856
|
+
{ path: `/var/lib/forgezero/images/${image.imageKey}.img`, sha256: image.imageSha256 }
|
|
17857
|
+
])),
|
|
17645
17858
|
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")),
|
|
17646
17859
|
housekeepingCpus: bootstrapAnswer("Host housekeeping CPU list", "0-7,64-71"),
|
|
17647
17860
|
housekeepingMemoryNodes: bootstrapAnswer("Host housekeeping memory nodes", "0"),
|
|
@@ -17676,7 +17889,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17676
17889
|
};
|
|
17677
17890
|
const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
|
|
17678
17891
|
const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
|
|
17679
|
-
const realtimeEnabled =
|
|
17892
|
+
const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
|
|
17680
17893
|
const workerScriptName = realtimeEnabled ? bootstrapAnswer("Existing Worker script name") : undefined;
|
|
17681
17894
|
const discovered = await discoverCloudflareBootstrapCommandResources({
|
|
17682
17895
|
zoneName,
|
|
@@ -17687,12 +17900,12 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17687
17900
|
const nodeCount = bootstrapNumber("Number of public API nodes", "3");
|
|
17688
17901
|
if (nodeCount < 1 || nodeCount > 32)
|
|
17689
17902
|
throw new Error("Number of public API nodes must be between 1 and 32");
|
|
17690
|
-
const meshEnabled =
|
|
17903
|
+
const meshEnabled = bootstrapBoolean("Configure private Mesh/WARP routes?", "no");
|
|
17691
17904
|
const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
|
|
17692
17905
|
const nodes = Array.from({ length: nodeCount }, (_, index) => {
|
|
17693
17906
|
const ordinal = index + 1;
|
|
17694
17907
|
const nodeName = bootstrapAnswer(`Node ${ordinal} inventory name`);
|
|
17695
|
-
const nodeMesh = meshEnabled &&
|
|
17908
|
+
const nodeMesh = meshEnabled && bootstrapBoolean(`Configure Mesh routes for ${nodeName}?`, "yes");
|
|
17696
17909
|
return {
|
|
17697
17910
|
nodeName,
|
|
17698
17911
|
hostname: bootstrapAnswer(`${nodeName} public hostname`),
|
|
@@ -17702,7 +17915,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17702
17915
|
mesh: {
|
|
17703
17916
|
connectorName: bootstrapAnswer(`${nodeName} Mesh connector name`, `${nodeName}-mesh`),
|
|
17704
17917
|
routes: bootstrapList(`${nodeName} private CIDR routes`),
|
|
17705
|
-
highAvailability:
|
|
17918
|
+
highAvailability: bootstrapBoolean(`${nodeName} shares a highly available Mesh connector?`, "no")
|
|
17706
17919
|
}
|
|
17707
17920
|
} : {}
|
|
17708
17921
|
};
|
|
@@ -17749,6 +17962,28 @@ function operatorIdentityCoordinates() {
|
|
|
17749
17962
|
agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
|
|
17750
17963
|
};
|
|
17751
17964
|
}
|
|
17965
|
+
function interactiveGenesisGuests() {
|
|
17966
|
+
const node = (index) => ({
|
|
17967
|
+
name: bootstrapAnswer(`Guest ${index} name`),
|
|
17968
|
+
address: bootstrapAnswer(`Guest ${index} private IPv4`),
|
|
17969
|
+
imageKey: bootstrapAnswer(`Guest ${index} OS image key`, SUPPORTED_GUEST_IMAGE.key),
|
|
17970
|
+
cpuPoolKey: bootstrapAnswer(`Guest ${index} exclusive CPU pool key`),
|
|
17971
|
+
physicalCores: bootstrapNumber(`Guest ${index} physical cores`),
|
|
17972
|
+
vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
|
|
17973
|
+
memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
|
|
17974
|
+
diskGib: bootstrapNumber(`Guest ${index} disk GiB`),
|
|
17975
|
+
egressGuaranteedMbps: bootstrapNumber(`Guest ${index} guaranteed egress Mbps`),
|
|
17976
|
+
egressBurstMbps: bootstrapNumber(`Guest ${index} burst egress Mbps`),
|
|
17977
|
+
confidential: bootstrapBoolean(`Guest ${index} requires confidential SEV-SNP compute?`)
|
|
17978
|
+
});
|
|
17979
|
+
const nodes = [
|
|
17980
|
+
node(1),
|
|
17981
|
+
node(2),
|
|
17982
|
+
node(3)
|
|
17983
|
+
];
|
|
17984
|
+
const rehearsalNode = bootstrapBoolean("Configure an optional fourth rehearsal guest?", "yes") ? node(4) : undefined;
|
|
17985
|
+
return { nodes, ...rehearsalNode ? { rehearsalNode } : {} };
|
|
17986
|
+
}
|
|
17752
17987
|
function writeMetalOperatorFiles(directory) {
|
|
17753
17988
|
const output = genesisOutputDirectory(directory);
|
|
17754
17989
|
const config = interactiveMetalBootstrap();
|
|
@@ -17759,17 +17994,22 @@ function writeMetalOperatorFiles(directory) {
|
|
|
17759
17994
|
kind: "metal-remote",
|
|
17760
17995
|
metalConfigFile: metalConfig,
|
|
17761
17996
|
target: { ...target, ...identity },
|
|
17762
|
-
genesis: {
|
|
17763
|
-
environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
|
|
17764
|
-
sshPublicKeyFiles: [identity.identityPublicKeyFile]
|
|
17765
|
-
}
|
|
17997
|
+
genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
|
|
17766
17998
|
});
|
|
17767
17999
|
return { metalConfig, remoteRequest };
|
|
17768
18000
|
}
|
|
17769
18001
|
function writePlatformGenesisFleet(directory) {
|
|
17770
18002
|
const output = genesisOutputDirectory(directory);
|
|
17771
|
-
const
|
|
17772
|
-
const
|
|
18003
|
+
const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
18004
|
+
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile);
|
|
18005
|
+
const fleet = metalRequest.genesis.nodes;
|
|
18006
|
+
const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile).metalHostname;
|
|
18007
|
+
const template = interactiveBootstrap("platform", fleet);
|
|
18008
|
+
const region = {
|
|
18009
|
+
label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
|
|
18010
|
+
country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
|
|
18011
|
+
city: bootstrapAnswer("Region city")
|
|
18012
|
+
};
|
|
17773
18013
|
const nodes = fleet.map((guest, index) => index === 0 ? {
|
|
17774
18014
|
nodeHostname: template.nodeHostname,
|
|
17775
18015
|
cloudflareHandoffFile: template.cloudflareHandoff.handoffFile
|
|
@@ -17777,13 +18017,13 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17777
18017
|
nodeHostname: bootstrapAnswer(`${guest.name} public node hostname`),
|
|
17778
18018
|
cloudflareHandoffFile: bootstrapAnswer(`${guest.name} node-specific Cloudflare host handoff`)
|
|
17779
18019
|
});
|
|
17780
|
-
const configs = platformGenesisBootstrapConfigs(template, nodes).map((config) => {
|
|
18020
|
+
const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
|
|
17781
18021
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
17782
18022
|
writeBootstrapConfig(path, config);
|
|
17783
18023
|
return path;
|
|
17784
18024
|
});
|
|
17785
18025
|
const identity = operatorIdentityCoordinates();
|
|
17786
|
-
const useJump =
|
|
18026
|
+
const useJump = bootstrapBoolean("Reach genesis computes through the Metal SSH jump?", "yes");
|
|
17787
18027
|
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
17788
18028
|
const requests = configs.map((platformConfigFile, index) => {
|
|
17789
18029
|
const guest = fleet[index];
|
|
@@ -17797,14 +18037,15 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17797
18037
|
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
17798
18038
|
return { configs, requests, fleetRequest };
|
|
17799
18039
|
}
|
|
17800
|
-
function interactiveBootstrap(kind,
|
|
18040
|
+
function interactiveBootstrap(kind, genesisGuests) {
|
|
17801
18041
|
if (!process.stdin.isTTY)
|
|
17802
18042
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
17803
18043
|
const environment = bootstrapAnswer("Environment (production/development)", "production");
|
|
17804
18044
|
if (environment !== "production" && environment !== "development") {
|
|
17805
18045
|
throw new Error("Environment must be production or development");
|
|
17806
18046
|
}
|
|
17807
|
-
const
|
|
18047
|
+
const genesis = genesisGuests !== undefined;
|
|
18048
|
+
const fleet = genesisGuests;
|
|
17808
18049
|
const profile = genesis ? "platform-db-api" : bootstrapAnswer("Profile (platform-db-api/platform-api)", "platform-db-api");
|
|
17809
18050
|
const role = genesis ? "master" : profile === "platform-api" ? "none" : bootstrapAnswer("Database role (master/joiner)", "master");
|
|
17810
18051
|
const agency = genesis ? "member" : role === "master" ? "member" : role === "none" ? "none" : bootstrapAnswer("Agency participation (member/none)", "member");
|
|
@@ -17812,12 +18053,13 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17812
18053
|
const address = genesis ? fleet[0].address : role === "none" ? undefined : bootstrapAnswer("Private database address");
|
|
17813
18054
|
const master = role === "joiner" ? bootstrapAnswer("Master starter private address") : undefined;
|
|
17814
18055
|
const coordinators = genesis ? fleet.map((guest) => `http://${guest.address}:8529`) : bootstrapAnswer("Writable Coordinator origins (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
|
|
17815
|
-
const computeReference = genesis ? fleet[0].name : bootstrapAnswer("API-owned compute reference"
|
|
18056
|
+
const computeReference = genesis ? fleet[0].name : bootstrapAnswer("API-owned compute reference");
|
|
17816
18057
|
const nodeHostname = bootstrapAnswer(genesis ? `${computeReference} public node hostname` : "Public node hostname");
|
|
17817
18058
|
const apiUrl = bootstrapAnswer("Platform API URL");
|
|
17818
18059
|
const appOrigin = bootstrapAnswer("Public App origin");
|
|
17819
18060
|
const bundleFile = bootstrapAnswer("Bootstrap API Git bundle (absolute path)");
|
|
17820
18061
|
const manifestFile = bootstrapAnswer("Bootstrap bundle manifest (absolute path)", `${bundleFile}.json`);
|
|
18062
|
+
const bundleBranch = bootstrapAnswer("Bootstrap bundle Git branch");
|
|
17821
18063
|
const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
|
|
17822
18064
|
const collectorUnit = FORGEZERO_OTEL_COLLECTOR_UNIT;
|
|
17823
18065
|
const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
|
|
@@ -17830,7 +18072,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17830
18072
|
const smtpHost = emailProvider === "smtp" ? bootstrapAnswer("SMTP host") : undefined;
|
|
17831
18073
|
const smtpPort = emailProvider === "smtp" ? Number(bootstrapAnswer("SMTP port", "587")) : undefined;
|
|
17832
18074
|
const smtpUser = emailProvider === "smtp" ? bootstrapAnswer("SMTP user") : undefined;
|
|
17833
|
-
const jetemailEu = emailProvider === "jetemail" ?
|
|
18075
|
+
const jetemailEu = emailProvider === "jetemail" ? bootstrapBoolean("Use JetEmail EU processing?", "no") : undefined;
|
|
17834
18076
|
const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
|
|
17835
18077
|
const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
|
|
17836
18078
|
const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
|
|
@@ -17846,7 +18088,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17846
18088
|
computeReference,
|
|
17847
18089
|
nodeHostname,
|
|
17848
18090
|
apiUrl,
|
|
17849
|
-
bootstrapBundle: { bundleFile, manifestFile },
|
|
18091
|
+
bootstrapBundle: { bundleFile, manifestFile, branch: bundleBranch },
|
|
17850
18092
|
telemetryEndpoint,
|
|
17851
18093
|
database: {
|
|
17852
18094
|
role,
|
|
@@ -17900,12 +18142,12 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17900
18142
|
keepReleases: 5
|
|
17901
18143
|
},
|
|
17902
18144
|
firewall: {
|
|
17903
|
-
enabled:
|
|
18145
|
+
enabled: bootstrapBoolean("Enable host firewall?", "yes"),
|
|
17904
18146
|
sshPort: Number(bootstrapAnswer("SSH port", "22")),
|
|
17905
18147
|
privateCidrs: bootstrapAnswer("Private cluster CIDRs (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean)
|
|
17906
18148
|
},
|
|
17907
18149
|
installCloudflared: Boolean(cloudflareHandoffFile),
|
|
17908
|
-
installWarp: !genesis && cloudflareHandoffFile ?
|
|
18150
|
+
installWarp: !genesis && cloudflareHandoffFile ? bootstrapBoolean("Configure Cloudflare Mesh/WARP from this handoff?", "no") : false,
|
|
17909
18151
|
...cloudflareHandoffFile && cloudflareNodeName ? {
|
|
17910
18152
|
cloudflareHandoff: { handoffFile: cloudflareHandoffFile, nodeName: cloudflareNodeName }
|
|
17911
18153
|
} : {}
|
|
@@ -18494,9 +18736,9 @@ async function cmdDeploy(options, args) {
|
|
|
18494
18736
|
if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18495
18737
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18496
18738
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18497
|
-
const
|
|
18739
|
+
const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
18498
18740
|
const problems = [
|
|
18499
|
-
...
|
|
18741
|
+
...current2 ? [] : [`${DEPLOY_PLAN_FILE} is stale; run \`fz deploy compile\``],
|
|
18500
18742
|
...canonicalJson(expected.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : []
|
|
18501
18743
|
];
|
|
18502
18744
|
const summary = { source: expected.source, output: actual.path, digest: actual.digest, version: actual.plan.version, name: actual.plan.name, ready: problems.length === 0, problems };
|
|
@@ -18716,8 +18958,9 @@ function usage() {
|
|
|
18716
18958
|
Verify recovered data and bounded cluster topology
|
|
18717
18959
|
fz db consolidate Plan/apply a digest-bound offline schema consolidation
|
|
18718
18960
|
fz rehearse community-cluster-api
|
|
18719
|
-
Plan the
|
|
18720
|
-
--
|
|
18961
|
+
Plan the reviewed four-node Community 3.11.14 rehearsal;
|
|
18962
|
+
requires --request=<metal-remote.json>; mutation also requires
|
|
18963
|
+
the exact disposable-fleet confirmation printed by the plan
|
|
18721
18964
|
fz rehearse platform-fleet
|
|
18722
18965
|
Verify a development or production fleet; use --plan offline
|
|
18723
18966
|
fz rehearse platform-recovery
|