@forgezero/agent 0.1.128 → 0.1.130
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 +1 -1
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +84 -22
- package/dist/fz-agent.js +60 -8
- package/dist/fz.js +150 -40
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +96 -27
- package/dist/platform-fleet-verification.js +1 -1
- package/dist/provision.js +1 -1
- package/dist/ssh-bootstrap.d.ts +10 -22
- package/dist/version.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ fz unlock --phrase-file /secure/offline-phrase.txt
|
|
|
214
214
|
|
|
215
215
|
## Use website operations from a project or an AI agent
|
|
216
216
|
|
|
217
|
-
Every UI operation is an API operation. `fz ui routes --json` discovers the exact actions allowed by the signed-in account’s current
|
|
217
|
+
Every UI operation is an API operation. `fz ui routes --json` discovers the exact actions allowed by the signed-in account’s current workspace, lifecycle stage and permissions from the API’s enforced matrix, so the CLI and AI agents carry no copied route list. `fz api` (also `fz ui`) executes authenticated JSON GET, POST, PUT, PATCH and DELETE without requiring a browser visit for ordinary work. Paths are forced onto the signed-in origin, query values are repeatable, bodies can be inline, stdin or a file, and server permissions plus fresh-proof rules remain authoritative. `fz project init|sync|check` separately gives AI tools one Git-persisted project memory.
|
|
218
218
|
|
|
219
219
|
```text
|
|
220
220
|
fz ui routes --json
|
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.d.ts
CHANGED
|
@@ -210,6 +210,7 @@ export declare function interruptedPlatformReleaseIdentityDigests(config: Platfo
|
|
|
210
210
|
export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
|
|
211
211
|
export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost, secrets?: BootstrapSecrets, dependencies?: {
|
|
212
212
|
fetcher?: typeof fetch;
|
|
213
|
+
reuseBoundPlatformCredentials?: boolean;
|
|
213
214
|
}): Promise<BootstrapResult>;
|
|
214
215
|
export declare function readBootstrapConfig(path: string): BootstrapConfig;
|
|
215
216
|
interface BootstrapAgentInitialBundle {
|
package/dist/bootstrap.js
CHANGED
|
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1460
1460
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1461
1461
|
|
|
1462
1462
|
// src/version.ts
|
|
1463
|
-
var VERSION = "0.1.
|
|
1463
|
+
var VERSION = "0.1.130";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4664,6 +4664,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4664
4664
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4665
4665
|
let bootstrapManifest;
|
|
4666
4666
|
let releaseEvidence;
|
|
4667
|
+
let redundantBootstrapBundle = false;
|
|
4667
4668
|
if (config.kind === "platform") {
|
|
4668
4669
|
const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
|
|
4669
4670
|
if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
|
|
@@ -4684,8 +4685,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4684
4685
|
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
4685
4686
|
throw new Error("bootstrap release evidence is malformed");
|
|
4686
4687
|
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
4687
|
-
if (evidence.revision !== reviewed.revision || evidence.
|
|
4688
|
+
if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
|
|
4688
4689
|
bootstrapManifest = reviewed;
|
|
4690
|
+
} else {
|
|
4691
|
+
redundantBootstrapBundle = true;
|
|
4692
|
+
}
|
|
4689
4693
|
} else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
4690
4694
|
let evidence;
|
|
4691
4695
|
try {
|
|
@@ -4757,7 +4761,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4757
4761
|
}
|
|
4758
4762
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
4759
4763
|
}
|
|
4760
|
-
const
|
|
4764
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
4765
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
4766
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
4767
|
+
}
|
|
4768
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
4761
4769
|
if (!secrets)
|
|
4762
4770
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4763
4771
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -4800,7 +4808,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4800
4808
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
4801
4809
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
4802
4810
|
}
|
|
4803
|
-
if (config.kind === "platform" && platformPrivate
|
|
4811
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
4804
4812
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
4805
4813
|
}
|
|
4806
4814
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -4858,24 +4866,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4858
4866
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4859
4867
|
}
|
|
4860
4868
|
if (config.kind === "platform") {
|
|
4861
|
-
const root = platformPrivate.root;
|
|
4862
|
-
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4863
|
-
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4864
|
-
}
|
|
4865
|
-
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4866
|
-
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4867
|
-
}
|
|
4868
|
-
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4869
|
-
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4870
4869
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4870
|
+
if (reuseBoundPlatformCredentials) {
|
|
4871
|
+
const required = [
|
|
4872
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
4873
|
+
SEED_CREDENTIAL,
|
|
4874
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
4875
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
4876
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
4877
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
4878
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
4879
|
+
];
|
|
4880
|
+
const missing = required.find((path) => !host.exists(path));
|
|
4881
|
+
if (missing)
|
|
4882
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
4883
|
+
} else {
|
|
4884
|
+
const root = platformPrivate.root;
|
|
4885
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4886
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4887
|
+
}
|
|
4888
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4889
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4890
|
+
}
|
|
4891
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4892
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4893
|
+
for (const [name, source] of Object.entries({
|
|
4894
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4895
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4896
|
+
})) {
|
|
4897
|
+
if (source) {
|
|
4898
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
4899
|
+
if (!host.exists(destination2))
|
|
4900
|
+
await seal(host, name, destination2, source);
|
|
4879
4901
|
}
|
|
4880
4902
|
}
|
|
4881
4903
|
}
|
|
@@ -4944,6 +4966,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4944
4966
|
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
4945
4967
|
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
4946
4968
|
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
4969
|
+
if (alreadyEnrolled && !bootstrapManifest) {
|
|
4970
|
+
const healthCommand = [
|
|
4971
|
+
"curl",
|
|
4972
|
+
"--fail",
|
|
4973
|
+
"--silent",
|
|
4974
|
+
"--show-error",
|
|
4975
|
+
"--max-time",
|
|
4976
|
+
"10",
|
|
4977
|
+
`http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
|
|
4978
|
+
];
|
|
4979
|
+
const health = await host.exec(healthCommand);
|
|
4980
|
+
if (health.exitCode !== 0) {
|
|
4981
|
+
const activeSlotFile = `${runtime.slotsDirectory}/.active`;
|
|
4982
|
+
if (!host.exists(activeSlotFile))
|
|
4983
|
+
throw new Error("enrolled platform active API slot evidence is missing");
|
|
4984
|
+
const activeSlot = host.read(activeSlotFile).trim();
|
|
4985
|
+
if (activeSlot !== "blue" && activeSlot !== "green") {
|
|
4986
|
+
throw new Error("enrolled platform active API slot evidence is malformed");
|
|
4987
|
+
}
|
|
4988
|
+
await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
|
|
4989
|
+
let ready = false;
|
|
4990
|
+
let lastHealthOutput = "";
|
|
4991
|
+
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
4992
|
+
const repairedHealth = await host.exec(healthCommand);
|
|
4993
|
+
if (repairedHealth.exitCode === 0) {
|
|
4994
|
+
ready = true;
|
|
4995
|
+
break;
|
|
4996
|
+
}
|
|
4997
|
+
lastHealthOutput = repairedHealth.output.trim();
|
|
4998
|
+
if (attempt < 29)
|
|
4999
|
+
await (host.sleep?.(1000) ?? Bun.sleep(1000));
|
|
5000
|
+
}
|
|
5001
|
+
if (!ready)
|
|
5002
|
+
throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
|
|
5003
|
+
}
|
|
5004
|
+
if (redundantBootstrapBundle) {
|
|
5005
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
5006
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
4947
5009
|
if (config.database.role === "master") {
|
|
4948
5010
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
4949
5011
|
if (!host.exists(invite)) {
|
|
@@ -5040,7 +5102,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
5040
5102
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
5041
5103
|
}
|
|
5042
5104
|
let launch;
|
|
5043
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
5105
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
5044
5106
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
5045
5107
|
if (!host.exists(invitePath))
|
|
5046
5108
|
throw new Error("platform invite is missing after deployment");
|
package/dist/fz-agent.js
CHANGED
|
@@ -9640,9 +9640,47 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
9640
9640
|
}
|
|
9641
9641
|
|
|
9642
9642
|
// src/version.ts
|
|
9643
|
-
var VERSION2 = "0.1.
|
|
9643
|
+
var VERSION2 = "0.1.130";
|
|
9644
9644
|
|
|
9645
9645
|
// src/ssh-bootstrap.ts
|
|
9646
|
+
function deriveMetalAdmissionProvisioning(lscpu) {
|
|
9647
|
+
const rows = lscpu.split(`
|
|
9648
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
9649
|
+
const fields = line.split(",");
|
|
9650
|
+
if (fields.length !== 4 || fields.some((field) => !/^\d+$/.test(field))) {
|
|
9651
|
+
throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
|
|
9652
|
+
}
|
|
9653
|
+
return { cpu: Number(fields[0]), core: `${fields[2]}:${fields[1]}` };
|
|
9654
|
+
});
|
|
9655
|
+
if (rows.length < 2 || new Set(rows.map(({ cpu }) => cpu)).size !== rows.length) {
|
|
9656
|
+
throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
|
|
9657
|
+
}
|
|
9658
|
+
const cores = new Map;
|
|
9659
|
+
for (const row2 of rows)
|
|
9660
|
+
cores.set(row2.core, [...cores.get(row2.core) ?? [], row2.cpu]);
|
|
9661
|
+
if (cores.size < 2)
|
|
9662
|
+
throw new SshBootstrapError("METAL_CPU_CAPACITY_INSUFFICIENT");
|
|
9663
|
+
const ordered = [...cores.values()].map((cpus2) => cpus2.sort((a, b) => a - b)).sort((left, right) => left[0] - right[0]);
|
|
9664
|
+
const threadsPerCore = ordered[0].length;
|
|
9665
|
+
if (threadsPerCore < 1 || threadsPerCore > 8 || ordered.some((cpus2) => cpus2.length !== threadsPerCore)) {
|
|
9666
|
+
throw new SshBootstrapError("METAL_CPU_TOPOLOGY_INVALID");
|
|
9667
|
+
}
|
|
9668
|
+
const linuxList = (values) => [...values].sort((a, b) => a - b).join(",");
|
|
9669
|
+
const housekeeping = ordered.shift();
|
|
9670
|
+
const guestCpus = Array.from({ length: threadsPerCore }, (_, thread) => ordered.map((cpus2) => cpus2[thread])).flat();
|
|
9671
|
+
return {
|
|
9672
|
+
volumeGroup: "fzvg",
|
|
9673
|
+
bridge: "fzbr0",
|
|
9674
|
+
subnetPrefix: "10.42.0",
|
|
9675
|
+
addressStart: 20,
|
|
9676
|
+
addressEnd: 200,
|
|
9677
|
+
gateway: "10.42.0.1",
|
|
9678
|
+
nameservers: ["1.1.1.1", "1.0.0.1"],
|
|
9679
|
+
cpuPools: [{ key: "host-capacity", cpus: linuxList(guestCpus), physicalCores: ordered.length }],
|
|
9680
|
+
housekeepingCpus: linuxList(housekeeping)
|
|
9681
|
+
};
|
|
9682
|
+
}
|
|
9683
|
+
|
|
9646
9684
|
class SshBootstrapError extends Error {
|
|
9647
9685
|
code;
|
|
9648
9686
|
constructor(code, message = code) {
|
|
@@ -9677,7 +9715,7 @@ var validateClaim = (claim) => {
|
|
|
9677
9715
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(claim.computeKey) || !/^[A-Za-z0-9_-]{16,128}$/.test(claim.claimToken) || !/^fze_[A-Za-z0-9_-]{32,128}$/.test(claim.enrolmentToken) || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(claim.realm) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.projectKey) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.environmentKey))
|
|
9678
9716
|
throw new SshBootstrapError("CLAIM_INVALID");
|
|
9679
9717
|
const { host, port, user, hostKeySha256, nodeHostname, operatingSystem } = claim.target;
|
|
9680
|
-
if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
|
|
9718
|
+
if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || hostKeySha256 !== "" && !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
|
|
9681
9719
|
throw new SshBootstrapError("CLAIM_INVALID");
|
|
9682
9720
|
}
|
|
9683
9721
|
if (operatingSystem?.id !== "ubuntu" || !["24.04", "26.04"].includes(operatingSystem.version) || operatingSystem.architecture !== "x64")
|
|
@@ -9738,11 +9776,14 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
|
|
|
9738
9776
|
const candidates = scanned.split(`
|
|
9739
9777
|
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
9740
9778
|
let matched;
|
|
9779
|
+
let observedFingerprint = "";
|
|
9741
9780
|
for (const line of candidates) {
|
|
9742
|
-
const
|
|
9781
|
+
const result = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
|
|
9743
9782
|
` });
|
|
9744
|
-
|
|
9783
|
+
const fingerprint = result.split(/\s+/).find((part) => /^SHA256:[A-Za-z0-9+/]{43}$/.test(part)) ?? "";
|
|
9784
|
+
if (fingerprint && (!claim.target.hostKeySha256 || fingerprint === claim.target.hostKeySha256)) {
|
|
9745
9785
|
matched = line;
|
|
9786
|
+
observedFingerprint = fingerprint;
|
|
9746
9787
|
break;
|
|
9747
9788
|
}
|
|
9748
9789
|
}
|
|
@@ -9754,7 +9795,7 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
|
|
|
9754
9795
|
const path2 = join3(directory, "known_hosts");
|
|
9755
9796
|
writeFileSync5(path2, `${pinned.hostKeyAlias} ${key}
|
|
9756
9797
|
`, { mode: 384, flag: "wx" });
|
|
9757
|
-
return path2;
|
|
9798
|
+
return { path: path2, fingerprint: observedFingerprint };
|
|
9758
9799
|
}
|
|
9759
9800
|
var BUN_RELEASE_URL = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip";
|
|
9760
9801
|
var verifiedBunArchive = async (directory) => {
|
|
@@ -9809,7 +9850,9 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
|
9809
9850
|
if (!resolved || resolved.protocol !== "ssh")
|
|
9810
9851
|
throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
|
|
9811
9852
|
pinned = resolved;
|
|
9812
|
-
const
|
|
9853
|
+
const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
|
|
9854
|
+
claim.target.hostKeySha256 = observedHost.fingerprint;
|
|
9855
|
+
const knownHosts = observedHost.path;
|
|
9813
9856
|
const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
|
|
9814
9857
|
remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
|
|
9815
9858
|
if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
|
|
@@ -10153,7 +10196,9 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
|
10153
10196
|
if (!resolved || resolved.protocol !== "ssh")
|
|
10154
10197
|
throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
|
|
10155
10198
|
pinned = resolved;
|
|
10156
|
-
const
|
|
10199
|
+
const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
|
|
10200
|
+
claim.target.hostKeySha256 = observedHost.fingerprint;
|
|
10201
|
+
const knownHosts = observedHost.path;
|
|
10157
10202
|
const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
|
|
10158
10203
|
const destination = target(claim, pinned);
|
|
10159
10204
|
remoteDirectory = await checked(exec, ["ssh", ...ssh, destination, "--", "mktemp", "-d", "/tmp/forgezero-metal-admission.XXXXXXXX"], "SSH_UNREACHABLE");
|
|
@@ -10173,6 +10218,13 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
|
10173
10218
|
writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
|
|
10174
10219
|
await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
|
|
10175
10220
|
await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-m", "0600", `${remoteDirectory}/authorized_keys`, `${home}/.ssh/authorized_keys`]);
|
|
10221
|
+
await remote(exec, ssh, destination, "METAL_STORAGE_UNAVAILABLE", ["/usr/bin/sudo", "-n", "/usr/sbin/vgs", "fzvg"]);
|
|
10222
|
+
await remote(exec, ssh, destination, "METAL_NETWORK_UNAVAILABLE", ["/usr/bin/sudo", "-n", "/usr/sbin/ip", "link", "show", "fzbr0"]);
|
|
10223
|
+
const topology = await remote(exec, ssh, destination, "METAL_CPU_TOPOLOGY_INVALID", [
|
|
10224
|
+
"/usr/bin/lscpu",
|
|
10225
|
+
"--parse=CPU,CORE,SOCKET,NODE"
|
|
10226
|
+
]);
|
|
10227
|
+
const provisioning = deriveMetalAdmissionProvisioning(topology);
|
|
10176
10228
|
const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
|
|
10177
10229
|
const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
|
|
10178
10230
|
for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
|
|
@@ -10191,7 +10243,7 @@ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
|
10191
10243
|
hostTelemetryEndpoint: "http://127.0.0.1:4318",
|
|
10192
10244
|
hostTelemetryUnit: "forgezero-otel-collector.service",
|
|
10193
10245
|
profile: {
|
|
10194
|
-
...
|
|
10246
|
+
...provisioning,
|
|
10195
10247
|
stateDir: "/etc/forgezero/metal-guests",
|
|
10196
10248
|
seedDir: "/var/lib/forgezero/seed",
|
|
10197
10249
|
unitDir: "/etc/systemd/system",
|
package/dist/fz.js
CHANGED
|
@@ -5788,7 +5788,7 @@ var VaultRuntimeClientError, json = (value) => ({ method: "POST", body: JSON.str
|
|
|
5788
5788
|
} catch {
|
|
5789
5789
|
return;
|
|
5790
5790
|
}
|
|
5791
|
-
}, VERSION = "0.1.
|
|
5791
|
+
}, VERSION = "0.1.27";
|
|
5792
5792
|
var init_dist = __esm(() => {
|
|
5793
5793
|
init_vault_runtime();
|
|
5794
5794
|
init_identity();
|
|
@@ -6007,7 +6007,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
6007
6007
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
6008
6008
|
|
|
6009
6009
|
// src/version.ts
|
|
6010
|
-
var VERSION2 = "0.1.
|
|
6010
|
+
var VERSION2 = "0.1.130";
|
|
6011
6011
|
|
|
6012
6012
|
// src/software.ts
|
|
6013
6013
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -15284,6 +15284,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15284
15284
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
15285
15285
|
let bootstrapManifest;
|
|
15286
15286
|
let releaseEvidence;
|
|
15287
|
+
let redundantBootstrapBundle = false;
|
|
15287
15288
|
if (config.kind === "platform") {
|
|
15288
15289
|
const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
|
|
15289
15290
|
if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
|
|
@@ -15304,8 +15305,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15304
15305
|
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
15305
15306
|
throw new Error("bootstrap release evidence is malformed");
|
|
15306
15307
|
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
15307
|
-
if (evidence.revision !== reviewed.revision || evidence.
|
|
15308
|
+
if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
|
|
15308
15309
|
bootstrapManifest = reviewed;
|
|
15310
|
+
} else {
|
|
15311
|
+
redundantBootstrapBundle = true;
|
|
15312
|
+
}
|
|
15309
15313
|
} else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
15310
15314
|
let evidence;
|
|
15311
15315
|
try {
|
|
@@ -15377,7 +15381,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15377
15381
|
}
|
|
15378
15382
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
15379
15383
|
}
|
|
15380
|
-
const
|
|
15384
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
15385
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
15386
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
15387
|
+
}
|
|
15388
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
15381
15389
|
if (!secrets)
|
|
15382
15390
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
15383
15391
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -15420,7 +15428,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15420
15428
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
15421
15429
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
15422
15430
|
}
|
|
15423
|
-
if (config.kind === "platform" && platformPrivate
|
|
15431
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
15424
15432
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
15425
15433
|
}
|
|
15426
15434
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -15478,24 +15486,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15478
15486
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
15479
15487
|
}
|
|
15480
15488
|
if (config.kind === "platform") {
|
|
15481
|
-
const root = platformPrivate.root;
|
|
15482
|
-
if (!host.exists(JWT_CREDENTIAL)) {
|
|
15483
|
-
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
15484
|
-
}
|
|
15485
|
-
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
15486
|
-
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
15487
|
-
}
|
|
15488
|
-
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
15489
|
-
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
15490
15489
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15490
|
+
if (reuseBoundPlatformCredentials) {
|
|
15491
|
+
const required = [
|
|
15492
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
15493
|
+
SEED_CREDENTIAL,
|
|
15494
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
15495
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
15496
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
15497
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
15498
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
15499
|
+
];
|
|
15500
|
+
const missing = required.find((path) => !host.exists(path));
|
|
15501
|
+
if (missing)
|
|
15502
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
15503
|
+
} else {
|
|
15504
|
+
const root = platformPrivate.root;
|
|
15505
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
15506
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
15507
|
+
}
|
|
15508
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
15509
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
15510
|
+
}
|
|
15511
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
15512
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
15513
|
+
for (const [name, source] of Object.entries({
|
|
15514
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
15515
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
15516
|
+
})) {
|
|
15517
|
+
if (source) {
|
|
15518
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
15519
|
+
if (!host.exists(destination2))
|
|
15520
|
+
await seal(host, name, destination2, source);
|
|
15499
15521
|
}
|
|
15500
15522
|
}
|
|
15501
15523
|
}
|
|
@@ -15564,6 +15586,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15564
15586
|
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
15565
15587
|
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
15566
15588
|
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
15589
|
+
if (alreadyEnrolled && !bootstrapManifest) {
|
|
15590
|
+
const healthCommand = [
|
|
15591
|
+
"curl",
|
|
15592
|
+
"--fail",
|
|
15593
|
+
"--silent",
|
|
15594
|
+
"--show-error",
|
|
15595
|
+
"--max-time",
|
|
15596
|
+
"10",
|
|
15597
|
+
`http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
|
|
15598
|
+
];
|
|
15599
|
+
const health = await host.exec(healthCommand);
|
|
15600
|
+
if (health.exitCode !== 0) {
|
|
15601
|
+
const activeSlotFile = `${runtime.slotsDirectory}/.active`;
|
|
15602
|
+
if (!host.exists(activeSlotFile))
|
|
15603
|
+
throw new Error("enrolled platform active API slot evidence is missing");
|
|
15604
|
+
const activeSlot = host.read(activeSlotFile).trim();
|
|
15605
|
+
if (activeSlot !== "blue" && activeSlot !== "green") {
|
|
15606
|
+
throw new Error("enrolled platform active API slot evidence is malformed");
|
|
15607
|
+
}
|
|
15608
|
+
await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
|
|
15609
|
+
let ready = false;
|
|
15610
|
+
let lastHealthOutput = "";
|
|
15611
|
+
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
15612
|
+
const repairedHealth = await host.exec(healthCommand);
|
|
15613
|
+
if (repairedHealth.exitCode === 0) {
|
|
15614
|
+
ready = true;
|
|
15615
|
+
break;
|
|
15616
|
+
}
|
|
15617
|
+
lastHealthOutput = repairedHealth.output.trim();
|
|
15618
|
+
if (attempt < 29)
|
|
15619
|
+
await (host.sleep?.(1000) ?? Bun.sleep(1000));
|
|
15620
|
+
}
|
|
15621
|
+
if (!ready)
|
|
15622
|
+
throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
|
|
15623
|
+
}
|
|
15624
|
+
if (redundantBootstrapBundle) {
|
|
15625
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
15626
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
15627
|
+
}
|
|
15628
|
+
}
|
|
15567
15629
|
if (config.database.role === "master") {
|
|
15568
15630
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
15569
15631
|
if (!host.exists(invite)) {
|
|
@@ -15660,7 +15722,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15660
15722
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
15661
15723
|
}
|
|
15662
15724
|
let launch;
|
|
15663
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
15725
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
15664
15726
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
15665
15727
|
if (!host.exists(invitePath))
|
|
15666
15728
|
throw new Error("platform invite is missing after deployment");
|
|
@@ -17621,7 +17683,14 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
17621
17683
|
user: request.target.user,
|
|
17622
17684
|
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
17623
17685
|
},
|
|
17624
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
17686
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
|
|
17687
|
+
"verify pinned SSH transport and caller-approved SSH agent",
|
|
17688
|
+
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
17689
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
17690
|
+
"stage the rewritten owner-only platform config without plaintext credentials",
|
|
17691
|
+
"promote the exact release using the credentials already sealed on the enrolled compute",
|
|
17692
|
+
"remove transient local and remote staging data"
|
|
17693
|
+
] : [
|
|
17625
17694
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
17626
17695
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
17627
17696
|
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
@@ -18124,7 +18193,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
18124
18193
|
allowStoredCloudflareCredentials: mode === "repair"
|
|
18125
18194
|
}) : undefined;
|
|
18126
18195
|
const staged = await stageConfig(config, directory, {
|
|
18127
|
-
reuseBoundCloudflare: mode === "repair"
|
|
18196
|
+
reuseBoundCloudflare: mode === "repair" || mode === "promote"
|
|
18128
18197
|
});
|
|
18129
18198
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
18130
18199
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -18141,15 +18210,15 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
18141
18210
|
"-n",
|
|
18142
18211
|
"/usr/local/bin/fz",
|
|
18143
18212
|
"bootstrap",
|
|
18144
|
-
mode === "
|
|
18145
|
-
"credentials-stdin"
|
|
18213
|
+
mode === "apply" ? "platform" : "repair",
|
|
18214
|
+
mode === "promote" ? "stored-credentials" : "credentials-stdin"
|
|
18146
18215
|
];
|
|
18147
18216
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
18148
18217
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
18149
18218
|
` : undefined, mode === "apply");
|
|
18150
18219
|
return { plan, output };
|
|
18151
18220
|
} finally {
|
|
18152
|
-
if (knownHosts) {
|
|
18221
|
+
if (knownHosts && mode !== "status") {
|
|
18153
18222
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/rm", "-rf", "--", REMOTE_STAGE, remoteTemp], "remote bootstrap cleanup", true).catch(() => {
|
|
18154
18223
|
return;
|
|
18155
18224
|
});
|
|
@@ -20067,6 +20136,31 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
|
|
|
20067
20136
|
owner: owner ?? forgeZeroLaunchOwnerInput()
|
|
20068
20137
|
});
|
|
20069
20138
|
}
|
|
20139
|
+
var GENESIS_HOST_KEY_ATTEMPTS = 12;
|
|
20140
|
+
var GENESIS_HOST_KEY_RETRY_MS = 2000;
|
|
20141
|
+
async function reconcileForgeZeroGenesisGuests(outputDirectory) {
|
|
20142
|
+
const base = dirname14(outputDirectory);
|
|
20143
|
+
const metalRequestFile = join13(base, "metal", "metal-remote.json");
|
|
20144
|
+
const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
|
|
20145
|
+
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
|
|
20146
|
+
await applyOperatorMetalBootstrap(metalRequest, "genesis");
|
|
20147
|
+
let lastFailure;
|
|
20148
|
+
for (let attempt = 1;attempt <= GENESIS_HOST_KEY_ATTEMPTS; attempt += 1) {
|
|
20149
|
+
try {
|
|
20150
|
+
const result = await applyOperatorMetalBootstrap(metalRequest, "host-keys");
|
|
20151
|
+
const evidence = JSON.parse(result.output);
|
|
20152
|
+
writeDerivedLaunchFile(guestHostKeysFile, (temporary) => writeOperatorGuestHostKeyEvidence(temporary, evidence, metalRequest));
|
|
20153
|
+
readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
20154
|
+
return;
|
|
20155
|
+
} catch (cause) {
|
|
20156
|
+
lastFailure = cause;
|
|
20157
|
+
if (attempt < GENESIS_HOST_KEY_ATTEMPTS) {
|
|
20158
|
+
await new Promise((resolve14) => setTimeout(resolve14, GENESIS_HOST_KEY_RETRY_MS));
|
|
20159
|
+
}
|
|
20160
|
+
}
|
|
20161
|
+
}
|
|
20162
|
+
throw new Error(`reviewed genesis guests did not become SSH-ready: ${lastFailure instanceof Error ? lastFailure.message : String(lastFailure)}`);
|
|
20163
|
+
}
|
|
20070
20164
|
function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
|
|
20071
20165
|
if (!process.stdin.isTTY)
|
|
20072
20166
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
@@ -20266,6 +20360,8 @@ async function cmdBootstrap(options, args) {
|
|
|
20266
20360
|
}
|
|
20267
20361
|
const outputDirectory = options.outputPath ?? join13(homedir2(), ".forgezero-secure", environment, "genesis");
|
|
20268
20362
|
const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
|
|
20363
|
+
if (options.apply)
|
|
20364
|
+
await reconcileForgeZeroGenesisGuests(outputDirectory);
|
|
20269
20365
|
const fleet = readOperatorPlatformBootstrapFleetRequest((await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner)).fleetRequest);
|
|
20270
20366
|
if (planOperatorPlatformFleetBootstrap(fleet, "apply").environment !== environment) {
|
|
20271
20367
|
throw new Error("existing platform fleet does not match the selected launch profile");
|
|
@@ -20277,8 +20373,8 @@ async function cmdBootstrap(options, args) {
|
|
|
20277
20373
|
}
|
|
20278
20374
|
if (operation === "platform" && args[1] === "fleet") {
|
|
20279
20375
|
const mode = args[2];
|
|
20280
|
-
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
20281
|
-
throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
20376
|
+
if (!mode || !["apply", "promote", "status"].includes(mode) || args[3] !== undefined) {
|
|
20377
|
+
throw new Error("Usage: fz bootstrap platform fleet <apply|promote|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
20282
20378
|
}
|
|
20283
20379
|
if (!options.bootstrapConfigPath)
|
|
20284
20380
|
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
@@ -20288,12 +20384,23 @@ async function cmdBootstrap(options, args) {
|
|
|
20288
20384
|
out.line(JSON.stringify(result2, null, 2));
|
|
20289
20385
|
return result2.ok ? 0 : 1;
|
|
20290
20386
|
}
|
|
20387
|
+
if (mode === "promote") {
|
|
20388
|
+
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
20389
|
+
out.line(JSON.stringify(plan2, null, 2));
|
|
20390
|
+
if (!options.apply) {
|
|
20391
|
+
out.step("Review the pinned secret-free promotion, then repeat with --apply.");
|
|
20392
|
+
return 0;
|
|
20393
|
+
}
|
|
20394
|
+
const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
|
|
20395
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
20396
|
+
return result2.ok ? 0 : 1;
|
|
20397
|
+
}
|
|
20291
20398
|
return await runAttendedPlatformFleet(fleet, options.apply);
|
|
20292
20399
|
}
|
|
20293
20400
|
if (operation === "platform" && args[1] === "remote") {
|
|
20294
20401
|
const mode = args[2];
|
|
20295
|
-
if (!mode || !["apply", "repair", "status"].includes(mode) || args[3] !== undefined) {
|
|
20296
|
-
throw new Error("Usage: fz bootstrap platform remote <apply|repair|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
20402
|
+
if (!mode || !["apply", "repair", "promote", "status"].includes(mode) || args[3] !== undefined) {
|
|
20403
|
+
throw new Error("Usage: fz bootstrap platform remote <apply|repair|promote|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
20297
20404
|
}
|
|
20298
20405
|
if (!options.bootstrapConfigPath)
|
|
20299
20406
|
throw new Error("remote platform bootstrap requires --bootstrap-config");
|
|
@@ -20406,8 +20513,9 @@ async function cmdBootstrap(options, args) {
|
|
|
20406
20513
|
return 0;
|
|
20407
20514
|
}
|
|
20408
20515
|
const credentialStdin = (operation === "platform" || operation === "repair") && args[1] === "credentials-stdin";
|
|
20409
|
-
|
|
20410
|
-
|
|
20516
|
+
const storedCredentials = operation === "repair" && args[1] === "stored-credentials";
|
|
20517
|
+
if ((credentialStdin || storedCredentials) && args[2] !== undefined)
|
|
20518
|
+
throw new Error("platform credential mode accepts no additional positional arguments");
|
|
20411
20519
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
20412
20520
|
metal: existsSync13(METAL_BOOTSTRAP_STATE_PATH),
|
|
20413
20521
|
compute: existsSync13(BOOTSTRAP_STATE_PATH)
|
|
@@ -20451,7 +20559,7 @@ async function cmdBootstrap(options, args) {
|
|
|
20451
20559
|
return 0;
|
|
20452
20560
|
}
|
|
20453
20561
|
if (!["platform", "repair"].includes(operation)) {
|
|
20454
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [launch|bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|genesis-cleanup|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
20562
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [launch|bundle|fleet <apply|promote|status>|remote <apply|repair|promote|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|genesis-cleanup|rehearsal|rehearsal-cleanup|host-keys|rehearsal-host-keys|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
20455
20563
|
}
|
|
20456
20564
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
20457
20565
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -20468,7 +20576,7 @@ async function cmdBootstrap(options, args) {
|
|
|
20468
20576
|
out.step("Review the plan, then repeat with --apply as root.");
|
|
20469
20577
|
return 0;
|
|
20470
20578
|
}
|
|
20471
|
-
const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config, undefined, {
|
|
20579
|
+
const secrets = config.kind === "platform" && !credentialStdin && !storedCredentials ? await promptPlatformBootstrapSecrets(config, undefined, {
|
|
20472
20580
|
reuseBoundCloudflare: operation === "repair"
|
|
20473
20581
|
}) : undefined;
|
|
20474
20582
|
let stdinSecrets = secrets;
|
|
@@ -20486,7 +20594,9 @@ async function cmdBootstrap(options, args) {
|
|
|
20486
20594
|
allowStoredCloudflareCredentials: operation === "repair"
|
|
20487
20595
|
});
|
|
20488
20596
|
}
|
|
20489
|
-
const result = await applyBootstrap(config, undefined, stdinSecrets
|
|
20597
|
+
const result = await applyBootstrap(config, undefined, stdinSecrets, {
|
|
20598
|
+
reuseBoundPlatformCredentials: storedCredentials
|
|
20599
|
+
});
|
|
20490
20600
|
out.line(JSON.stringify(result, null, 2));
|
|
20491
20601
|
return 0;
|
|
20492
20602
|
} catch (cause) {
|
|
@@ -21114,10 +21224,10 @@ function usage() {
|
|
|
21114
21224
|
One profile-driven attended launch. Public topology,
|
|
21115
21225
|
bundle and edge names are derived; the owner supplies
|
|
21116
21226
|
only email settings, hidden provider tokens and APPLY
|
|
21117
|
-
fz bootstrap platform remote <apply|repair|status>
|
|
21227
|
+
fz bootstrap platform remote <apply|repair|promote|status>
|
|
21118
21228
|
Run typed bootstrap from the operator laptop through
|
|
21119
|
-
a pinned host;
|
|
21120
|
-
fz bootstrap platform fleet <apply|status>
|
|
21229
|
+
a pinned host; promote reuses sealed host credentials
|
|
21230
|
+
fz bootstrap platform fleet <apply|promote|status>
|
|
21121
21231
|
One attended genesis operation: reconcile public
|
|
21122
21232
|
hostnames/Tunnels, seal credentials, deploy the bundle,
|
|
21123
21233
|
run all three nodes, verify ingress and print the invite
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
// src/version.ts
|
|
369
|
-
var VERSION = "0.1.
|
|
369
|
+
var VERSION = "0.1.130";
|
|
370
370
|
|
|
371
371
|
// src/otel-collector.ts
|
|
372
372
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -89,7 +89,7 @@ export interface OperatorPlatformFleetCoordinates {
|
|
|
89
89
|
databaseRole: 'master' | 'joiner';
|
|
90
90
|
}[];
|
|
91
91
|
}
|
|
92
|
-
export type OperatorPlatformBootstrapMode = 'apply' | 'repair' | 'status';
|
|
92
|
+
export type OperatorPlatformBootstrapMode = 'apply' | 'repair' | 'promote' | 'status';
|
|
93
93
|
export type OperatorMetalBootstrapMode = 'apply' | 'genesis' | 'genesis-cleanup' | 'rehearsal' | 'rehearsal-cleanup' | 'host-keys' | 'rehearsal-host-keys' | 'status';
|
|
94
94
|
export interface OperatorPlatformBootstrapPlan {
|
|
95
95
|
kind: 'platform-remote';
|
|
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1460
1460
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1461
1461
|
|
|
1462
1462
|
// src/version.ts
|
|
1463
|
-
var VERSION = "0.1.
|
|
1463
|
+
var VERSION = "0.1.130";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4664,6 +4664,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4664
4664
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4665
4665
|
let bootstrapManifest;
|
|
4666
4666
|
let releaseEvidence;
|
|
4667
|
+
let redundantBootstrapBundle = false;
|
|
4667
4668
|
if (config.kind === "platform") {
|
|
4668
4669
|
const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
|
|
4669
4670
|
if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
|
|
@@ -4684,8 +4685,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4684
4685
|
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
4685
4686
|
throw new Error("bootstrap release evidence is malformed");
|
|
4686
4687
|
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
4687
|
-
if (evidence.revision !== reviewed.revision || evidence.
|
|
4688
|
+
if (evidence.revision !== reviewed.revision || evidence.branch !== reviewed.branch) {
|
|
4688
4689
|
bootstrapManifest = reviewed;
|
|
4690
|
+
} else {
|
|
4691
|
+
redundantBootstrapBundle = true;
|
|
4692
|
+
}
|
|
4689
4693
|
} else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
4690
4694
|
let evidence;
|
|
4691
4695
|
try {
|
|
@@ -4757,7 +4761,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4757
4761
|
}
|
|
4758
4762
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
4759
4763
|
}
|
|
4760
|
-
const
|
|
4764
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
4765
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
4766
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
4767
|
+
}
|
|
4768
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
4761
4769
|
if (!secrets)
|
|
4762
4770
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4763
4771
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -4800,7 +4808,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4800
4808
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
4801
4809
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
4802
4810
|
}
|
|
4803
|
-
if (config.kind === "platform" && platformPrivate
|
|
4811
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
4804
4812
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
4805
4813
|
}
|
|
4806
4814
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -4858,24 +4866,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4858
4866
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4859
4867
|
}
|
|
4860
4868
|
if (config.kind === "platform") {
|
|
4861
|
-
const root = platformPrivate.root;
|
|
4862
|
-
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4863
|
-
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4864
|
-
}
|
|
4865
|
-
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4866
|
-
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4867
|
-
}
|
|
4868
|
-
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4869
|
-
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4870
4869
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4870
|
+
if (reuseBoundPlatformCredentials) {
|
|
4871
|
+
const required = [
|
|
4872
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
4873
|
+
SEED_CREDENTIAL,
|
|
4874
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
4875
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
4876
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
4877
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
4878
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
4879
|
+
];
|
|
4880
|
+
const missing = required.find((path) => !host.exists(path));
|
|
4881
|
+
if (missing)
|
|
4882
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
4883
|
+
} else {
|
|
4884
|
+
const root = platformPrivate.root;
|
|
4885
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4886
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4887
|
+
}
|
|
4888
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4889
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4890
|
+
}
|
|
4891
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4892
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4893
|
+
for (const [name, source] of Object.entries({
|
|
4894
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4895
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4896
|
+
})) {
|
|
4897
|
+
if (source) {
|
|
4898
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
4899
|
+
if (!host.exists(destination2))
|
|
4900
|
+
await seal(host, name, destination2, source);
|
|
4879
4901
|
}
|
|
4880
4902
|
}
|
|
4881
4903
|
}
|
|
@@ -4944,6 +4966,46 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4944
4966
|
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
4945
4967
|
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
4946
4968
|
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
4969
|
+
if (alreadyEnrolled && !bootstrapManifest) {
|
|
4970
|
+
const healthCommand = [
|
|
4971
|
+
"curl",
|
|
4972
|
+
"--fail",
|
|
4973
|
+
"--silent",
|
|
4974
|
+
"--show-error",
|
|
4975
|
+
"--max-time",
|
|
4976
|
+
"10",
|
|
4977
|
+
`http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
|
|
4978
|
+
];
|
|
4979
|
+
const health = await host.exec(healthCommand);
|
|
4980
|
+
if (health.exitCode !== 0) {
|
|
4981
|
+
const activeSlotFile = `${runtime.slotsDirectory}/.active`;
|
|
4982
|
+
if (!host.exists(activeSlotFile))
|
|
4983
|
+
throw new Error("enrolled platform active API slot evidence is missing");
|
|
4984
|
+
const activeSlot = host.read(activeSlotFile).trim();
|
|
4985
|
+
if (activeSlot !== "blue" && activeSlot !== "green") {
|
|
4986
|
+
throw new Error("enrolled platform active API slot evidence is malformed");
|
|
4987
|
+
}
|
|
4988
|
+
await checked3(host, ["systemctl", "start", `forgezero@${activeSlot}.service`], "active API slot repair");
|
|
4989
|
+
let ready = false;
|
|
4990
|
+
let lastHealthOutput = "";
|
|
4991
|
+
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
4992
|
+
const repairedHealth = await host.exec(healthCommand);
|
|
4993
|
+
if (repairedHealth.exitCode === 0) {
|
|
4994
|
+
ready = true;
|
|
4995
|
+
break;
|
|
4996
|
+
}
|
|
4997
|
+
lastHealthOutput = repairedHealth.output.trim();
|
|
4998
|
+
if (attempt < 29)
|
|
4999
|
+
await (host.sleep?.(1000) ?? Bun.sleep(1000));
|
|
5000
|
+
}
|
|
5001
|
+
if (!ready)
|
|
5002
|
+
throw new Error(`repaired API health failed: ${lastHealthOutput || "readiness deadline exceeded"}`);
|
|
5003
|
+
}
|
|
5004
|
+
if (redundantBootstrapBundle) {
|
|
5005
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
5006
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
4947
5009
|
if (config.database.role === "master") {
|
|
4948
5010
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
4949
5011
|
if (!host.exists(invite)) {
|
|
@@ -5040,7 +5102,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
5040
5102
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
5041
5103
|
}
|
|
5042
5104
|
let launch;
|
|
5043
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
5105
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
5044
5106
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
5045
5107
|
if (!host.exists(invitePath))
|
|
5046
5108
|
throw new Error("platform invite is missing after deployment");
|
|
@@ -6737,7 +6799,14 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
6737
6799
|
user: request.target.user,
|
|
6738
6800
|
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
6739
6801
|
},
|
|
6740
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
6802
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
|
|
6803
|
+
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6804
|
+
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
6805
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
6806
|
+
"stage the rewritten owner-only platform config without plaintext credentials",
|
|
6807
|
+
"promote the exact release using the credentials already sealed on the enrolled compute",
|
|
6808
|
+
"remove transient local and remote staging data"
|
|
6809
|
+
] : [
|
|
6741
6810
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6742
6811
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
6743
6812
|
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
@@ -7303,7 +7372,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7303
7372
|
allowStoredCloudflareCredentials: mode === "repair"
|
|
7304
7373
|
}) : undefined;
|
|
7305
7374
|
const staged = await stageConfig(config, directory, {
|
|
7306
|
-
reuseBoundCloudflare: mode === "repair"
|
|
7375
|
+
reuseBoundCloudflare: mode === "repair" || mode === "promote"
|
|
7307
7376
|
});
|
|
7308
7377
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
7309
7378
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -7320,15 +7389,15 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7320
7389
|
"-n",
|
|
7321
7390
|
"/usr/local/bin/fz",
|
|
7322
7391
|
"bootstrap",
|
|
7323
|
-
mode === "
|
|
7324
|
-
"credentials-stdin"
|
|
7392
|
+
mode === "apply" ? "platform" : "repair",
|
|
7393
|
+
mode === "promote" ? "stored-credentials" : "credentials-stdin"
|
|
7325
7394
|
];
|
|
7326
7395
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
7327
7396
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
7328
7397
|
` : undefined, mode === "apply");
|
|
7329
7398
|
return { plan, output };
|
|
7330
7399
|
} finally {
|
|
7331
|
-
if (knownHosts) {
|
|
7400
|
+
if (knownHosts && mode !== "status") {
|
|
7332
7401
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/rm", "-rf", "--", REMOTE_STAGE, remoteTemp], "remote bootstrap cleanup", true).catch(() => {
|
|
7333
7402
|
return;
|
|
7334
7403
|
});
|
|
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
3020
3020
|
}
|
|
3021
3021
|
|
|
3022
3022
|
// src/version.ts
|
|
3023
|
-
var VERSION3 = "0.1.
|
|
3023
|
+
var VERSION3 = "0.1.130";
|
|
3024
3024
|
|
|
3025
3025
|
// src/egress-policy.ts
|
|
3026
3026
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/provision.js
CHANGED
|
@@ -3020,7 +3020,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
3020
3020
|
}
|
|
3021
3021
|
|
|
3022
3022
|
// src/version.ts
|
|
3023
|
-
var VERSION3 = "0.1.
|
|
3023
|
+
var VERSION3 = "0.1.130";
|
|
3024
3024
|
|
|
3025
3025
|
// src/egress-policy.ts
|
|
3026
3026
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/ssh-bootstrap.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NodeKeyPair } from '@forgezero/runtime/identity';
|
|
2
2
|
import type { AgentOperationTelemetry } from './telemetry-runtime';
|
|
3
3
|
import { type GitHostResolver } from './git-egress';
|
|
4
|
+
import type { MetalBootstrapConfig } from './metal-bootstrap';
|
|
4
5
|
export interface RemoteSshBootstrapClaim {
|
|
5
6
|
computeKey: string;
|
|
6
7
|
computeReference: string;
|
|
@@ -50,30 +51,16 @@ export interface RemoteMetalAdmissionClaim {
|
|
|
50
51
|
runnerNodeKey: string;
|
|
51
52
|
runnerSshPublicKey: string;
|
|
52
53
|
challenge: string;
|
|
53
|
-
provisioning: {
|
|
54
|
-
volumeGroup: string;
|
|
55
|
-
bridge: string;
|
|
56
|
-
subnetPrefix: string;
|
|
57
|
-
addressStart: number;
|
|
58
|
-
addressEnd: number;
|
|
59
|
-
gateway: string;
|
|
60
|
-
nameservers: string[];
|
|
61
|
-
cpuPools: Array<{
|
|
62
|
-
key: string;
|
|
63
|
-
cpus: string;
|
|
64
|
-
physicalCores: number;
|
|
65
|
-
memoryNodes?: string;
|
|
66
|
-
}>;
|
|
67
|
-
housekeepingCpus: string;
|
|
68
|
-
housekeepingMemoryNodes?: string;
|
|
69
|
-
confidential?: {
|
|
70
|
-
cbitpos: number;
|
|
71
|
-
reducedPhysBits: number;
|
|
72
|
-
policy: string;
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
54
|
target: RemoteSshBootstrapClaim['target'];
|
|
76
55
|
}
|
|
56
|
+
type MetalAdmissionProvisioning = Pick<MetalBootstrapConfig['profile'], 'volumeGroup' | 'bridge' | 'subnetPrefix' | 'addressStart' | 'addressEnd' | 'gateway' | 'nameservers' | 'cpuPools' | 'housekeepingCpus'>;
|
|
57
|
+
/**
|
|
58
|
+
* Convert runner-observed Linux CPU topology into the fixed ForgeZero host
|
|
59
|
+
* boundary. The browser never authors this capacity. One physical core and all
|
|
60
|
+
* of its SMT siblings remain reserved for the host; every other complete core
|
|
61
|
+
* becomes the initial allocatable pool.
|
|
62
|
+
*/
|
|
63
|
+
export declare function deriveMetalAdmissionProvisioning(lscpu: string): MetalAdmissionProvisioning;
|
|
77
64
|
export interface MetalAdmissionProof {
|
|
78
65
|
jobKey: string;
|
|
79
66
|
tenantKey: string;
|
|
@@ -181,3 +168,4 @@ export declare function startMetalAdmissionPull(options: SshBootstrapPullOptions
|
|
|
181
168
|
stop(): Promise<void>;
|
|
182
169
|
readonly active: boolean;
|
|
183
170
|
};
|
|
171
|
+
export {};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.130";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.130",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"typescript": "^5.6.0",
|
|
12
12
|
"@types/bun": "latest",
|
|
13
13
|
"@types/node": "^22.0.0",
|
|
14
|
-
"@forgezero/access": "0.1.
|
|
14
|
+
"@forgezero/access": "0.1.14",
|
|
15
15
|
"@noble/curves": "^2.2.0",
|
|
16
16
|
"@noble/post-quantum": "^0.6.1"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@forgezero/runtime": "0.1.
|
|
20
|
-
"@forgezero/vault": "0.1.
|
|
19
|
+
"@forgezero/runtime": "0.1.21",
|
|
20
|
+
"@forgezero/vault": "0.1.27",
|
|
21
21
|
"@noble/curves": "2.2.0",
|
|
22
22
|
"@noble/hashes": "2.2.0",
|
|
23
23
|
"@noble/post-quantum": "0.6.1",
|