@forgezero/agent 0.1.128 → 0.1.129
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 +39 -21
- package/dist/fz-agent.js +60 -8
- package/dist/fz.js +104 -38
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +50 -25
- 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.129";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4757,7 +4757,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4757
4757
|
}
|
|
4758
4758
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
4759
4759
|
}
|
|
4760
|
-
const
|
|
4760
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
4761
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
4762
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
4763
|
+
}
|
|
4764
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
4761
4765
|
if (!secrets)
|
|
4762
4766
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4763
4767
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -4800,7 +4804,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4800
4804
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
4801
4805
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
4802
4806
|
}
|
|
4803
|
-
if (config.kind === "platform" && platformPrivate
|
|
4807
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
4804
4808
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
4805
4809
|
}
|
|
4806
4810
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -4858,24 +4862,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4858
4862
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4859
4863
|
}
|
|
4860
4864
|
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
4865
|
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
|
-
|
|
4866
|
+
if (reuseBoundPlatformCredentials) {
|
|
4867
|
+
const required = [
|
|
4868
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
4869
|
+
SEED_CREDENTIAL,
|
|
4870
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
4871
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
4872
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
4873
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
4874
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
4875
|
+
];
|
|
4876
|
+
const missing = required.find((path) => !host.exists(path));
|
|
4877
|
+
if (missing)
|
|
4878
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
4879
|
+
} else {
|
|
4880
|
+
const root = platformPrivate.root;
|
|
4881
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4882
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4883
|
+
}
|
|
4884
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4885
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4886
|
+
}
|
|
4887
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4888
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4889
|
+
for (const [name, source] of Object.entries({
|
|
4890
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4891
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4892
|
+
})) {
|
|
4893
|
+
if (source) {
|
|
4894
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
4895
|
+
if (!host.exists(destination2))
|
|
4896
|
+
await seal(host, name, destination2, source);
|
|
4879
4897
|
}
|
|
4880
4898
|
}
|
|
4881
4899
|
}
|
|
@@ -5040,7 +5058,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
5040
5058
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
5041
5059
|
}
|
|
5042
5060
|
let launch;
|
|
5043
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
5061
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
5044
5062
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
5045
5063
|
if (!host.exists(invitePath))
|
|
5046
5064
|
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.129";
|
|
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.26";
|
|
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.129";
|
|
6011
6011
|
|
|
6012
6012
|
// src/software.ts
|
|
6013
6013
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -15377,7 +15377,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15377
15377
|
}
|
|
15378
15378
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
15379
15379
|
}
|
|
15380
|
-
const
|
|
15380
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
15381
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
15382
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
15383
|
+
}
|
|
15384
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
15381
15385
|
if (!secrets)
|
|
15382
15386
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
15383
15387
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -15420,7 +15424,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15420
15424
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
15421
15425
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
15422
15426
|
}
|
|
15423
|
-
if (config.kind === "platform" && platformPrivate
|
|
15427
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
15424
15428
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
15425
15429
|
}
|
|
15426
15430
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -15478,24 +15482,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15478
15482
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
15479
15483
|
}
|
|
15480
15484
|
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
15485
|
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
|
-
|
|
15486
|
+
if (reuseBoundPlatformCredentials) {
|
|
15487
|
+
const required = [
|
|
15488
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
15489
|
+
SEED_CREDENTIAL,
|
|
15490
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
15491
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
15492
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
15493
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
15494
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
15495
|
+
];
|
|
15496
|
+
const missing = required.find((path) => !host.exists(path));
|
|
15497
|
+
if (missing)
|
|
15498
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
15499
|
+
} else {
|
|
15500
|
+
const root = platformPrivate.root;
|
|
15501
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
15502
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
15503
|
+
}
|
|
15504
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
15505
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
15506
|
+
}
|
|
15507
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
15508
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
15509
|
+
for (const [name, source] of Object.entries({
|
|
15510
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
15511
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
15512
|
+
})) {
|
|
15513
|
+
if (source) {
|
|
15514
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
15515
|
+
if (!host.exists(destination2))
|
|
15516
|
+
await seal(host, name, destination2, source);
|
|
15499
15517
|
}
|
|
15500
15518
|
}
|
|
15501
15519
|
}
|
|
@@ -15660,7 +15678,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15660
15678
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
15661
15679
|
}
|
|
15662
15680
|
let launch;
|
|
15663
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
15681
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
15664
15682
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
15665
15683
|
if (!host.exists(invitePath))
|
|
15666
15684
|
throw new Error("platform invite is missing after deployment");
|
|
@@ -17621,7 +17639,14 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
17621
17639
|
user: request.target.user,
|
|
17622
17640
|
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
17623
17641
|
},
|
|
17624
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
17642
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
|
|
17643
|
+
"verify pinned SSH transport and caller-approved SSH agent",
|
|
17644
|
+
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
17645
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
17646
|
+
"stage the rewritten owner-only platform config without plaintext credentials",
|
|
17647
|
+
"promote the exact release using the credentials already sealed on the enrolled compute",
|
|
17648
|
+
"remove transient local and remote staging data"
|
|
17649
|
+
] : [
|
|
17625
17650
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
17626
17651
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
17627
17652
|
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
@@ -18124,7 +18149,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
18124
18149
|
allowStoredCloudflareCredentials: mode === "repair"
|
|
18125
18150
|
}) : undefined;
|
|
18126
18151
|
const staged = await stageConfig(config, directory, {
|
|
18127
|
-
reuseBoundCloudflare: mode === "repair"
|
|
18152
|
+
reuseBoundCloudflare: mode === "repair" || mode === "promote"
|
|
18128
18153
|
});
|
|
18129
18154
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
18130
18155
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -18141,8 +18166,8 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
18141
18166
|
"-n",
|
|
18142
18167
|
"/usr/local/bin/fz",
|
|
18143
18168
|
"bootstrap",
|
|
18144
|
-
mode === "
|
|
18145
|
-
"credentials-stdin"
|
|
18169
|
+
mode === "apply" ? "platform" : "repair",
|
|
18170
|
+
mode === "promote" ? "stored-credentials" : "credentials-stdin"
|
|
18146
18171
|
];
|
|
18147
18172
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
18148
18173
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
@@ -20067,6 +20092,31 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
|
|
|
20067
20092
|
owner: owner ?? forgeZeroLaunchOwnerInput()
|
|
20068
20093
|
});
|
|
20069
20094
|
}
|
|
20095
|
+
var GENESIS_HOST_KEY_ATTEMPTS = 12;
|
|
20096
|
+
var GENESIS_HOST_KEY_RETRY_MS = 2000;
|
|
20097
|
+
async function reconcileForgeZeroGenesisGuests(outputDirectory) {
|
|
20098
|
+
const base = dirname14(outputDirectory);
|
|
20099
|
+
const metalRequestFile = join13(base, "metal", "metal-remote.json");
|
|
20100
|
+
const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
|
|
20101
|
+
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
|
|
20102
|
+
await applyOperatorMetalBootstrap(metalRequest, "genesis");
|
|
20103
|
+
let lastFailure;
|
|
20104
|
+
for (let attempt = 1;attempt <= GENESIS_HOST_KEY_ATTEMPTS; attempt += 1) {
|
|
20105
|
+
try {
|
|
20106
|
+
const result = await applyOperatorMetalBootstrap(metalRequest, "host-keys");
|
|
20107
|
+
const evidence = JSON.parse(result.output);
|
|
20108
|
+
writeDerivedLaunchFile(guestHostKeysFile, (temporary) => writeOperatorGuestHostKeyEvidence(temporary, evidence, metalRequest));
|
|
20109
|
+
readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
20110
|
+
return;
|
|
20111
|
+
} catch (cause) {
|
|
20112
|
+
lastFailure = cause;
|
|
20113
|
+
if (attempt < GENESIS_HOST_KEY_ATTEMPTS) {
|
|
20114
|
+
await new Promise((resolve14) => setTimeout(resolve14, GENESIS_HOST_KEY_RETRY_MS));
|
|
20115
|
+
}
|
|
20116
|
+
}
|
|
20117
|
+
}
|
|
20118
|
+
throw new Error(`reviewed genesis guests did not become SSH-ready: ${lastFailure instanceof Error ? lastFailure.message : String(lastFailure)}`);
|
|
20119
|
+
}
|
|
20070
20120
|
function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
|
|
20071
20121
|
if (!process.stdin.isTTY)
|
|
20072
20122
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
@@ -20266,6 +20316,8 @@ async function cmdBootstrap(options, args) {
|
|
|
20266
20316
|
}
|
|
20267
20317
|
const outputDirectory = options.outputPath ?? join13(homedir2(), ".forgezero-secure", environment, "genesis");
|
|
20268
20318
|
const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
|
|
20319
|
+
if (options.apply)
|
|
20320
|
+
await reconcileForgeZeroGenesisGuests(outputDirectory);
|
|
20269
20321
|
const fleet = readOperatorPlatformBootstrapFleetRequest((await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner)).fleetRequest);
|
|
20270
20322
|
if (planOperatorPlatformFleetBootstrap(fleet, "apply").environment !== environment) {
|
|
20271
20323
|
throw new Error("existing platform fleet does not match the selected launch profile");
|
|
@@ -20277,8 +20329,8 @@ async function cmdBootstrap(options, args) {
|
|
|
20277
20329
|
}
|
|
20278
20330
|
if (operation === "platform" && args[1] === "fleet") {
|
|
20279
20331
|
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]");
|
|
20332
|
+
if (!mode || !["apply", "promote", "status"].includes(mode) || args[3] !== undefined) {
|
|
20333
|
+
throw new Error("Usage: fz bootstrap platform fleet <apply|promote|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
20282
20334
|
}
|
|
20283
20335
|
if (!options.bootstrapConfigPath)
|
|
20284
20336
|
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
@@ -20288,12 +20340,23 @@ async function cmdBootstrap(options, args) {
|
|
|
20288
20340
|
out.line(JSON.stringify(result2, null, 2));
|
|
20289
20341
|
return result2.ok ? 0 : 1;
|
|
20290
20342
|
}
|
|
20343
|
+
if (mode === "promote") {
|
|
20344
|
+
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
20345
|
+
out.line(JSON.stringify(plan2, null, 2));
|
|
20346
|
+
if (!options.apply) {
|
|
20347
|
+
out.step("Review the pinned secret-free promotion, then repeat with --apply.");
|
|
20348
|
+
return 0;
|
|
20349
|
+
}
|
|
20350
|
+
const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode);
|
|
20351
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
20352
|
+
return result2.ok ? 0 : 1;
|
|
20353
|
+
}
|
|
20291
20354
|
return await runAttendedPlatformFleet(fleet, options.apply);
|
|
20292
20355
|
}
|
|
20293
20356
|
if (operation === "platform" && args[1] === "remote") {
|
|
20294
20357
|
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]");
|
|
20358
|
+
if (!mode || !["apply", "repair", "promote", "status"].includes(mode) || args[3] !== undefined) {
|
|
20359
|
+
throw new Error("Usage: fz bootstrap platform remote <apply|repair|promote|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
20297
20360
|
}
|
|
20298
20361
|
if (!options.bootstrapConfigPath)
|
|
20299
20362
|
throw new Error("remote platform bootstrap requires --bootstrap-config");
|
|
@@ -20406,8 +20469,9 @@ async function cmdBootstrap(options, args) {
|
|
|
20406
20469
|
return 0;
|
|
20407
20470
|
}
|
|
20408
20471
|
const credentialStdin = (operation === "platform" || operation === "repair") && args[1] === "credentials-stdin";
|
|
20409
|
-
|
|
20410
|
-
|
|
20472
|
+
const storedCredentials = operation === "repair" && args[1] === "stored-credentials";
|
|
20473
|
+
if ((credentialStdin || storedCredentials) && args[2] !== undefined)
|
|
20474
|
+
throw new Error("platform credential mode accepts no additional positional arguments");
|
|
20411
20475
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
20412
20476
|
metal: existsSync13(METAL_BOOTSTRAP_STATE_PATH),
|
|
20413
20477
|
compute: existsSync13(BOOTSTRAP_STATE_PATH)
|
|
@@ -20451,7 +20515,7 @@ async function cmdBootstrap(options, args) {
|
|
|
20451
20515
|
return 0;
|
|
20452
20516
|
}
|
|
20453
20517
|
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]");
|
|
20518
|
+
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
20519
|
}
|
|
20456
20520
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
20457
20521
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -20468,7 +20532,7 @@ async function cmdBootstrap(options, args) {
|
|
|
20468
20532
|
out.step("Review the plan, then repeat with --apply as root.");
|
|
20469
20533
|
return 0;
|
|
20470
20534
|
}
|
|
20471
|
-
const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config, undefined, {
|
|
20535
|
+
const secrets = config.kind === "platform" && !credentialStdin && !storedCredentials ? await promptPlatformBootstrapSecrets(config, undefined, {
|
|
20472
20536
|
reuseBoundCloudflare: operation === "repair"
|
|
20473
20537
|
}) : undefined;
|
|
20474
20538
|
let stdinSecrets = secrets;
|
|
@@ -20486,7 +20550,9 @@ async function cmdBootstrap(options, args) {
|
|
|
20486
20550
|
allowStoredCloudflareCredentials: operation === "repair"
|
|
20487
20551
|
});
|
|
20488
20552
|
}
|
|
20489
|
-
const result = await applyBootstrap(config, undefined, stdinSecrets
|
|
20553
|
+
const result = await applyBootstrap(config, undefined, stdinSecrets, {
|
|
20554
|
+
reuseBoundPlatformCredentials: storedCredentials
|
|
20555
|
+
});
|
|
20490
20556
|
out.line(JSON.stringify(result, null, 2));
|
|
20491
20557
|
return 0;
|
|
20492
20558
|
} catch (cause) {
|
|
@@ -21114,10 +21180,10 @@ function usage() {
|
|
|
21114
21180
|
One profile-driven attended launch. Public topology,
|
|
21115
21181
|
bundle and edge names are derived; the owner supplies
|
|
21116
21182
|
only email settings, hidden provider tokens and APPLY
|
|
21117
|
-
fz bootstrap platform remote <apply|repair|status>
|
|
21183
|
+
fz bootstrap platform remote <apply|repair|promote|status>
|
|
21118
21184
|
Run typed bootstrap from the operator laptop through
|
|
21119
|
-
a pinned host;
|
|
21120
|
-
fz bootstrap platform fleet <apply|status>
|
|
21185
|
+
a pinned host; promote reuses sealed host credentials
|
|
21186
|
+
fz bootstrap platform fleet <apply|promote|status>
|
|
21121
21187
|
One attended genesis operation: reconcile public
|
|
21122
21188
|
hostnames/Tunnels, seal credentials, deploy the bundle,
|
|
21123
21189
|
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.129";
|
|
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.129";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4757,7 +4757,11 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4757
4757
|
}
|
|
4758
4758
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
4759
4759
|
}
|
|
4760
|
-
const
|
|
4760
|
+
const reuseBoundPlatformCredentials = config.kind === "platform" && dependencies.reuseBoundPlatformCredentials === true;
|
|
4761
|
+
if (reuseBoundPlatformCredentials && (!installed || !host.exists("/var/lib/forgezero/enrolment.json"))) {
|
|
4762
|
+
throw new Error("stored-credential promotion requires an installed enrolled platform compute");
|
|
4763
|
+
}
|
|
4764
|
+
const platformPrivate = config.kind === "platform" && !reuseBoundPlatformCredentials ? (() => {
|
|
4761
4765
|
if (!secrets)
|
|
4762
4766
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4763
4767
|
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
@@ -4800,7 +4804,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4800
4804
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
4801
4805
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
4802
4806
|
}
|
|
4803
|
-
if (config.kind === "platform" && platformPrivate
|
|
4807
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
4804
4808
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
4805
4809
|
}
|
|
4806
4810
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -4858,24 +4862,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4858
4862
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4859
4863
|
}
|
|
4860
4864
|
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
4865
|
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
|
-
|
|
4866
|
+
if (reuseBoundPlatformCredentials) {
|
|
4867
|
+
const required = [
|
|
4868
|
+
...config.database.role !== "none" ? [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL] : [],
|
|
4869
|
+
SEED_CREDENTIAL,
|
|
4870
|
+
BACKUP_RECOVERY_CREDENTIAL,
|
|
4871
|
+
...emailCredentialName ? [`${CREDS}/${emailCredentialName}.cred`] : [],
|
|
4872
|
+
...config.runtime.environment.backup ? [`${CREDS}/backup.s3.secretAccessKey.cred`] : [],
|
|
4873
|
+
...installed?.cloudflare ? [CF_API_CREDENTIAL, CF_TUNNEL_API_CREDENTIAL, TUNNEL_CREDENTIAL] : [],
|
|
4874
|
+
...installed?.realtime ? [REALTIME_PUBLISH_CREDENTIAL, REALTIME_TICKET_CREDENTIAL] : []
|
|
4875
|
+
];
|
|
4876
|
+
const missing = required.find((path) => !host.exists(path));
|
|
4877
|
+
if (missing)
|
|
4878
|
+
throw new Error(`stored platform credential is missing: ${missing}`);
|
|
4879
|
+
} else {
|
|
4880
|
+
const root = platformPrivate.root;
|
|
4881
|
+
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4882
|
+
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4883
|
+
}
|
|
4884
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4885
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4886
|
+
}
|
|
4887
|
+
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4888
|
+
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4889
|
+
for (const [name, source] of Object.entries({
|
|
4890
|
+
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4891
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4892
|
+
})) {
|
|
4893
|
+
if (source) {
|
|
4894
|
+
const destination2 = `${CREDS}/${name}.cred`;
|
|
4895
|
+
if (!host.exists(destination2))
|
|
4896
|
+
await seal(host, name, destination2, source);
|
|
4879
4897
|
}
|
|
4880
4898
|
}
|
|
4881
4899
|
}
|
|
@@ -5040,7 +5058,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
5040
5058
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
5041
5059
|
}
|
|
5042
5060
|
let launch;
|
|
5043
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
5061
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
5044
5062
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
5045
5063
|
if (!host.exists(invitePath))
|
|
5046
5064
|
throw new Error("platform invite is missing after deployment");
|
|
@@ -6737,7 +6755,14 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
6737
6755
|
user: request.target.user,
|
|
6738
6756
|
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
6739
6757
|
},
|
|
6740
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
6758
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "promote" ? [
|
|
6759
|
+
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6760
|
+
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
6761
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
6762
|
+
"stage the rewritten owner-only platform config without plaintext credentials",
|
|
6763
|
+
"promote the exact release using the credentials already sealed on the enrolled compute",
|
|
6764
|
+
"remove transient local and remote staging data"
|
|
6765
|
+
] : [
|
|
6741
6766
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6742
6767
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
6743
6768
|
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
@@ -7303,7 +7328,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7303
7328
|
allowStoredCloudflareCredentials: mode === "repair"
|
|
7304
7329
|
}) : undefined;
|
|
7305
7330
|
const staged = await stageConfig(config, directory, {
|
|
7306
|
-
reuseBoundCloudflare: mode === "repair"
|
|
7331
|
+
reuseBoundCloudflare: mode === "repair" || mode === "promote"
|
|
7307
7332
|
});
|
|
7308
7333
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
7309
7334
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -7320,8 +7345,8 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7320
7345
|
"-n",
|
|
7321
7346
|
"/usr/local/bin/fz",
|
|
7322
7347
|
"bootstrap",
|
|
7323
|
-
mode === "
|
|
7324
|
-
"credentials-stdin"
|
|
7348
|
+
mode === "apply" ? "platform" : "repair",
|
|
7349
|
+
mode === "promote" ? "stored-credentials" : "credentials-stdin"
|
|
7325
7350
|
];
|
|
7326
7351
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
7327
7352
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
@@ -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.129";
|
|
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.129";
|
|
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.129";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.129",
|
|
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.20",
|
|
20
|
+
"@forgezero/vault": "0.1.26",
|
|
21
21
|
"@noble/curves": "2.2.0",
|
|
22
22
|
"@noble/hashes": "2.2.0",
|
|
23
23
|
"@noble/post-quantum": "0.6.1",
|