@forgezero/agent 0.1.125 → 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 +4 -1
- package/dist/bootstrap.js +45 -24
- package/dist/fz-agent.js +151 -37
- package/dist/fz.js +7597 -7303
- package/dist/host-maintenance.js +1 -1
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +78 -33
- package/dist/platform-fleet-verification.js +1 -1
- package/dist/provision.js +1 -1
- package/dist/recovery-host.d.ts +9 -0
- package/dist/recovery-host.js +27 -10
- package/dist/ssh-bootstrap.d.ts +18 -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
|
@@ -123,7 +123,9 @@ export interface EnrolledComputeBootstrapSecrets {
|
|
|
123
123
|
}
|
|
124
124
|
export type BootstrapSecrets = PlatformBootstrapSecrets | EnrolledComputeBootstrapSecrets;
|
|
125
125
|
export declare function validateEnrolledComputeBootstrapSecrets(config: EnrolledComputeActivationConfig, input: unknown): EnrolledComputeBootstrapSecrets;
|
|
126
|
-
export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown
|
|
126
|
+
export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown, options?: {
|
|
127
|
+
allowStoredCloudflareCredentials?: boolean;
|
|
128
|
+
}): PlatformBootstrapSecrets;
|
|
127
129
|
export interface BootstrapStep {
|
|
128
130
|
id: string;
|
|
129
131
|
label: string;
|
|
@@ -208,6 +210,7 @@ export declare function interruptedPlatformReleaseIdentityDigests(config: Platfo
|
|
|
208
210
|
export declare function bootstrapStatus(host?: BootstrapHost): Promise<BootstrapStatus>;
|
|
209
211
|
export declare function applyBootstrap(input: BootstrapConfig, host?: BootstrapHost, secrets?: BootstrapSecrets, dependencies?: {
|
|
210
212
|
fetcher?: typeof fetch;
|
|
213
|
+
reuseBoundPlatformCredentials?: boolean;
|
|
211
214
|
}): Promise<BootstrapResult>;
|
|
212
215
|
export declare function readBootstrapConfig(path: string): BootstrapConfig;
|
|
213
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";
|
|
@@ -3805,7 +3805,7 @@ function validateEnrolledComputeBootstrapSecrets(config, input) {
|
|
|
3805
3805
|
...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
|
|
3806
3806
|
};
|
|
3807
3807
|
}
|
|
3808
|
-
function validatePlatformBootstrapSecrets(config, input) {
|
|
3808
|
+
function validatePlatformBootstrapSecrets(config, input, options = {}) {
|
|
3809
3809
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3810
3810
|
throw new Error("bootstrap credential input must be an object");
|
|
3811
3811
|
const source = input;
|
|
@@ -3833,7 +3833,8 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3833
3833
|
throw new Error("backup credential is malformed");
|
|
3834
3834
|
}
|
|
3835
3835
|
const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
|
|
3836
|
-
|
|
3836
|
+
const cloudflareCredentialsSupplied = Boolean(cloudflareTunnelToken || cloudflareApiToken);
|
|
3837
|
+
if (!cloudflareConfigured && cloudflareCredentialsSupplied || cloudflareConfigured && !cloudflareCredentialsSupplied && !options.allowStoredCloudflareCredentials || Boolean(cloudflareTunnelToken) !== Boolean(cloudflareApiToken)) {
|
|
3837
3838
|
throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
|
|
3838
3839
|
}
|
|
3839
3840
|
if (cloudflareTunnelToken)
|
|
@@ -4756,10 +4757,16 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4756
4757
|
}
|
|
4757
4758
|
allowInterruptedReleaseRebind = !exactIdentity && (boundedReleaseResume || approvedIdentityMigration);
|
|
4758
4759
|
}
|
|
4759
|
-
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 ? (() => {
|
|
4760
4765
|
if (!secrets)
|
|
4761
4766
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
4762
|
-
const checked4 = validatePlatformBootstrapSecrets(config, secrets
|
|
4767
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
|
|
4768
|
+
allowStoredCloudflareCredentials: Boolean(installed?.cloudflare && config.cloudflareHandoff && !cloudflare)
|
|
4769
|
+
});
|
|
4763
4770
|
return {
|
|
4764
4771
|
root: checked4.clusterBootstrapCode,
|
|
4765
4772
|
email: checked4.emailSecret,
|
|
@@ -4797,7 +4804,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4797
4804
|
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
4798
4805
|
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
4799
4806
|
}
|
|
4800
|
-
if (config.kind === "platform" && platformPrivate
|
|
4807
|
+
if (config.kind === "platform" && platformPrivate?.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
4801
4808
|
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
4802
4809
|
}
|
|
4803
4810
|
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
@@ -4855,24 +4862,38 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4855
4862
|
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4856
4863
|
}
|
|
4857
4864
|
if (config.kind === "platform") {
|
|
4858
|
-
const root = platformPrivate.root;
|
|
4859
|
-
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4860
|
-
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4861
|
-
}
|
|
4862
|
-
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4863
|
-
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4864
|
-
}
|
|
4865
|
-
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4866
|
-
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4867
4865
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
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);
|
|
4876
4897
|
}
|
|
4877
4898
|
}
|
|
4878
4899
|
}
|
|
@@ -5037,7 +5058,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
5037
5058
|
host.remove(config.cloudflareHandoff.handoffFile);
|
|
5038
5059
|
}
|
|
5039
5060
|
let launch;
|
|
5040
|
-
if (config.kind === "platform" && config.database.role === "master") {
|
|
5061
|
+
if (config.kind === "platform" && config.database.role === "master" && !reuseBoundPlatformCredentials) {
|
|
5041
5062
|
const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
5042
5063
|
if (!host.exists(invitePath))
|
|
5043
5064
|
throw new Error("platform invite is missing after deployment");
|
package/dist/fz-agent.js
CHANGED
|
@@ -4049,7 +4049,7 @@ function combineKEMS(realSeedLen, realMsgLen, expandSeed, combiner, ...kems) {
|
|
|
4049
4049
|
var x25519kem = /* @__PURE__ */ ecdhKem(x25519);
|
|
4050
4050
|
var ml_kem768_x25519 = /* @__PURE__ */ (() => combineKEMS(32, 32, expandSeedXof(shake256), (pk, ct, ss) => sha3_256(concatBytes2(ss[0], ss[1], ct[1], pk[1], asciiToBytes("\\.//^\\"))), ml_kem768, x25519kem))();
|
|
4051
4051
|
|
|
4052
|
-
//
|
|
4052
|
+
// ../runtime/dist/identity.js
|
|
4053
4053
|
var ENCODER = new TextEncoder;
|
|
4054
4054
|
var b64 = toBase64Url;
|
|
4055
4055
|
var un64 = fromBase64Url;
|
|
@@ -4196,7 +4196,7 @@ function signRequest(keys, nodeKey, args) {
|
|
|
4196
4196
|
};
|
|
4197
4197
|
}
|
|
4198
4198
|
|
|
4199
|
-
//
|
|
4199
|
+
// ../vault/dist/index.js
|
|
4200
4200
|
var DEFAULT_SOCKET = "/run/forgezero/vault.sock";
|
|
4201
4201
|
|
|
4202
4202
|
// src/socket.ts
|
|
@@ -4692,7 +4692,7 @@ import { chmodSync as chmodSync4, existsSync as existsSync4, lstatSync as lstatS
|
|
|
4692
4692
|
import { createHash as createHash5, randomUUID } from "crypto";
|
|
4693
4693
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
4694
4694
|
|
|
4695
|
-
//
|
|
4695
|
+
// ../runtime/dist/queue.js
|
|
4696
4696
|
class QueueStoppedError extends Error {
|
|
4697
4697
|
constructor() {
|
|
4698
4698
|
super("queue: stopped before this task could run");
|
|
@@ -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) {
|
|
@@ -9655,7 +9693,7 @@ var defaultExec = async (argv2, options = {}) => {
|
|
|
9655
9693
|
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
9656
9694
|
stdout: "pipe",
|
|
9657
9695
|
stderr: "pipe",
|
|
9658
|
-
env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C" }
|
|
9696
|
+
env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C", ...options.env }
|
|
9659
9697
|
});
|
|
9660
9698
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
9661
9699
|
await writeAndCloseProcessInput(child.stdin, options.stdin);
|
|
@@ -9677,12 +9715,12 @@ 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")
|
|
9684
9722
|
throw new SshBootstrapError("CLAIM_INVALID");
|
|
9685
|
-
if (!claim.target.credential || claim.target.credential.source !== "runner" && claim.target.credential.source !== "vault" || claim.target.credential.source === "vault" && (typeof claim.target.credential.privateKey !== "string" || claim.target.credential.privateKey.length > 32768 || !claim.target.credential.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") || !claim.target.credential.privateKey.trimEnd().endsWith("-----END OPENSSH PRIVATE KEY-----"))) {
|
|
9723
|
+
if (!/^ssh-(?:ed25519|rsa|ecdsa-[^\s]+) [A-Za-z0-9+/]+={0,3}(?:\s+.*)?$/.test(claim.runnerSshPublicKey) || !claim.target.credential || claim.target.credential.source !== "runner" && claim.target.credential.source !== "vault" || claim.target.credential.source === "vault" && (claim.target.credential.kind === "private-key" && (typeof claim.target.credential.privateKey !== "string" || claim.target.credential.privateKey.length > 32768 || !claim.target.credential.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") || !claim.target.credential.privateKey.trimEnd().endsWith("-----END OPENSSH PRIVATE KEY-----")) || claim.target.credential.kind === "password" && (typeof claim.target.credential.password !== "string" || claim.target.credential.password.length < 1 || claim.target.credential.password.length > 4096))) {
|
|
9686
9724
|
throw new SshBootstrapError("CLAIM_INVALID");
|
|
9687
9725
|
}
|
|
9688
9726
|
};
|
|
@@ -9693,14 +9731,7 @@ var target = (claim, pinned) => {
|
|
|
9693
9731
|
var sshOptions = (claim, pinned, knownHosts, keyPath) => [
|
|
9694
9732
|
"-F",
|
|
9695
9733
|
"/dev/null",
|
|
9696
|
-
"-o",
|
|
9697
|
-
"BatchMode=yes",
|
|
9698
|
-
"-o",
|
|
9699
|
-
"IdentitiesOnly=yes",
|
|
9700
|
-
"-o",
|
|
9701
|
-
`IdentityFile=${keyPath}`,
|
|
9702
|
-
"-o",
|
|
9703
|
-
"IdentityAgent=none",
|
|
9734
|
+
...claim.target.credential.source === "vault" && claim.target.credential.kind === "password" ? ["-o", "BatchMode=no", "-o", "IdentitiesOnly=yes", "-o", "PubkeyAuthentication=no", "-o", "PreferredAuthentications=password,keyboard-interactive", "-o", "NumberOfPasswordPrompts=1"] : ["-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", `IdentityFile=${keyPath}`, "-o", "IdentityAgent=none"],
|
|
9704
9735
|
"-o",
|
|
9705
9736
|
`UserKnownHostsFile=${knownHosts}`,
|
|
9706
9737
|
"-o",
|
|
@@ -9745,11 +9776,14 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
|
|
|
9745
9776
|
const candidates = scanned.split(`
|
|
9746
9777
|
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
9747
9778
|
let matched;
|
|
9779
|
+
let observedFingerprint = "";
|
|
9748
9780
|
for (const line of candidates) {
|
|
9749
|
-
const
|
|
9781
|
+
const result = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
|
|
9750
9782
|
` });
|
|
9751
|
-
|
|
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)) {
|
|
9752
9785
|
matched = line;
|
|
9786
|
+
observedFingerprint = fingerprint;
|
|
9753
9787
|
break;
|
|
9754
9788
|
}
|
|
9755
9789
|
}
|
|
@@ -9761,7 +9795,7 @@ async function pinnedKnownHost(claim, pinned, directory, exec) {
|
|
|
9761
9795
|
const path2 = join3(directory, "known_hosts");
|
|
9762
9796
|
writeFileSync5(path2, `${pinned.hostKeyAlias} ${key}
|
|
9763
9797
|
`, { mode: 384, flag: "wx" });
|
|
9764
|
-
return path2;
|
|
9798
|
+
return { path: path2, fingerprint: observedFingerprint };
|
|
9765
9799
|
}
|
|
9766
9800
|
var BUN_RELEASE_URL = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip";
|
|
9767
9801
|
var verifiedBunArchive = async (directory) => {
|
|
@@ -9787,28 +9821,57 @@ async function executeSshBootstrap(claim, options) {
|
|
|
9787
9821
|
const api = new URL(options.platformApiUrl);
|
|
9788
9822
|
if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash)
|
|
9789
9823
|
throw new SshBootstrapError("TARGET_API_INVALID");
|
|
9790
|
-
const
|
|
9824
|
+
const baseExec = options.exec ?? defaultExec;
|
|
9791
9825
|
const directory = mkdtempSync2(join3(tmpdir2(), "forgezero-ssh-bootstrap-"));
|
|
9792
9826
|
chmodSync7(directory, 448);
|
|
9827
|
+
let exec = baseExec;
|
|
9793
9828
|
let sshKeyPath = options.sshKeyPath;
|
|
9794
9829
|
let remoteDirectory = "";
|
|
9795
9830
|
let pinned;
|
|
9796
9831
|
try {
|
|
9797
|
-
if (claim.target.credential.source === "vault") {
|
|
9832
|
+
if (claim.target.credential.source === "vault" && claim.target.credential.kind === "private-key") {
|
|
9798
9833
|
sshKeyPath = join3(directory, "identity");
|
|
9799
9834
|
writeFileSync5(sshKeyPath, `${claim.target.credential.privateKey.trim()}
|
|
9800
9835
|
`, { mode: 384, flag: "wx" });
|
|
9801
9836
|
}
|
|
9837
|
+
if (claim.target.credential.source === "vault" && claim.target.credential.kind === "password") {
|
|
9838
|
+
const askpass = join3(directory, "askpass");
|
|
9839
|
+
writeFileSync5(askpass, `#!/usr/bin/env bun
|
|
9840
|
+
process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
9841
|
+
`, { mode: 448, flag: "wx" });
|
|
9842
|
+
const password = claim.target.credential.password;
|
|
9843
|
+
exec = (argv2, command = {}) => baseExec(argv2, {
|
|
9844
|
+
...command,
|
|
9845
|
+
env: { ...command.env, DISPLAY: "forgezero:0", SSH_ASKPASS: askpass, SSH_ASKPASS_REQUIRE: "force", FZ_SSH_PASSWORD: password }
|
|
9846
|
+
});
|
|
9847
|
+
}
|
|
9802
9848
|
const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
|
|
9803
9849
|
const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
|
|
9804
9850
|
if (!resolved || resolved.protocol !== "ssh")
|
|
9805
9851
|
throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
|
|
9806
9852
|
pinned = resolved;
|
|
9807
|
-
const
|
|
9853
|
+
const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
|
|
9854
|
+
claim.target.hostKeySha256 = observedHost.fingerprint;
|
|
9855
|
+
const knownHosts = observedHost.path;
|
|
9808
9856
|
const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
|
|
9809
9857
|
remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
|
|
9810
9858
|
if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
|
|
9811
9859
|
throw new SshBootstrapError("REMOTE_PATH_INVALID");
|
|
9860
|
+
const destination = target(claim, pinned);
|
|
9861
|
+
const passwd = await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/getent", "passwd", claim.target.user]);
|
|
9862
|
+
const home = passwd.split(":")[5] ?? "";
|
|
9863
|
+
if (!home.startsWith("/") || /[\r\n]/.test(home))
|
|
9864
|
+
throw new SshBootstrapError("SSH_IDENTITY_INSTALL_FAILED");
|
|
9865
|
+
await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-d", "-m", "0700", `${home}/.ssh`]);
|
|
9866
|
+
const existing = await remote(exec, ssh, destination, "SSH_IDENTITY_READ_FAILED", ["/usr/bin/cat", `${home}/.ssh/authorized_keys`]).catch(() => "");
|
|
9867
|
+
const authorized = [...new Set([...existing.split(`
|
|
9868
|
+
`).map((line) => line.trim()).filter(Boolean), claim.runnerSshPublicKey])].join(`
|
|
9869
|
+
`) + `
|
|
9870
|
+
`;
|
|
9871
|
+
const authorizedPath = join3(directory, "authorized_keys");
|
|
9872
|
+
writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
|
|
9873
|
+
await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
|
|
9874
|
+
await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-m", "0600", `${remoteDirectory}/authorized_keys`, `${home}/.ssh/authorized_keys`]);
|
|
9812
9875
|
const config = {
|
|
9813
9876
|
kind: "enrolled-compute",
|
|
9814
9877
|
operatingSystem: claim.target.operatingSystem,
|
|
@@ -9844,7 +9907,6 @@ async function executeSshBootstrap(claim, options) {
|
|
|
9844
9907
|
join3(directory, name),
|
|
9845
9908
|
`${target(claim, pinned)}:${remoteDirectory}/${name}`
|
|
9846
9909
|
], "SCP_FAILED", { secret: name === "enrol.token" });
|
|
9847
|
-
const destination = target(claim, pinned);
|
|
9848
9910
|
const version = await remote(exec, ssh, destination, "REMOTE_BUN_CHECK_FAILED", ["/usr/local/bin/bun", "--version"]).catch(() => "");
|
|
9849
9911
|
if (version !== PINNED_BUN_VERSION) {
|
|
9850
9912
|
await remote(exec, ssh, destination, "REMOTE_PREREQUISITES_FAILED", [
|
|
@@ -10093,6 +10155,7 @@ var validateMetalClaim = (claim) => {
|
|
|
10093
10155
|
realm: claim.tenantKey,
|
|
10094
10156
|
projectKey: claim.projectKey,
|
|
10095
10157
|
environmentKey: claim.environmentKey,
|
|
10158
|
+
runnerSshPublicKey: claim.runnerSshPublicKey,
|
|
10096
10159
|
target: claim.target,
|
|
10097
10160
|
enrolmentToken: `fze_${"A".repeat(43)}`
|
|
10098
10161
|
});
|
|
@@ -10104,29 +10167,64 @@ async function executeMetalAdmission(claim, options) {
|
|
|
10104
10167
|
if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash || telemetry.protocol !== "https:" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
|
|
10105
10168
|
throw new SshBootstrapError("TARGET_COORDINATE_INVALID");
|
|
10106
10169
|
}
|
|
10107
|
-
const
|
|
10170
|
+
const baseExec = options.exec ?? defaultExec;
|
|
10108
10171
|
const directory = mkdtempSync2(join3(tmpdir2(), "forgezero-metal-admission-"));
|
|
10109
10172
|
chmodSync7(directory, 448);
|
|
10173
|
+
let exec = baseExec;
|
|
10110
10174
|
let sshKeyPath = options.sshKeyPath;
|
|
10111
10175
|
let remoteDirectory = "";
|
|
10112
10176
|
let pinned;
|
|
10113
10177
|
try {
|
|
10114
|
-
if (claim.target.credential.source === "vault") {
|
|
10178
|
+
if (claim.target.credential.source === "vault" && claim.target.credential.kind === "private-key") {
|
|
10115
10179
|
sshKeyPath = join3(directory, "identity");
|
|
10116
10180
|
writeFileSync5(sshKeyPath, `${claim.target.credential.privateKey.trim()}
|
|
10117
10181
|
`, { mode: 384, flag: "wx" });
|
|
10118
10182
|
}
|
|
10183
|
+
if (claim.target.credential.source === "vault" && claim.target.credential.kind === "password") {
|
|
10184
|
+
const askpass = join3(directory, "askpass");
|
|
10185
|
+
writeFileSync5(askpass, `#!/usr/bin/env bun
|
|
10186
|
+
process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
|
|
10187
|
+
`, { mode: 448, flag: "wx" });
|
|
10188
|
+
const password = claim.target.credential.password;
|
|
10189
|
+
exec = (argv2, command = {}) => baseExec(argv2, {
|
|
10190
|
+
...command,
|
|
10191
|
+
env: { ...command.env, DISPLAY: "forgezero:0", SSH_ASKPASS: askpass, SSH_ASKPASS_REQUIRE: "force", FZ_SSH_PASSWORD: password }
|
|
10192
|
+
});
|
|
10193
|
+
}
|
|
10119
10194
|
const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
|
|
10120
10195
|
const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
|
|
10121
10196
|
if (!resolved || resolved.protocol !== "ssh")
|
|
10122
10197
|
throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
|
|
10123
10198
|
pinned = resolved;
|
|
10124
|
-
const
|
|
10199
|
+
const observedHost = await pinnedKnownHost(claim, pinned, directory, exec);
|
|
10200
|
+
claim.target.hostKeySha256 = observedHost.fingerprint;
|
|
10201
|
+
const knownHosts = observedHost.path;
|
|
10125
10202
|
const ssh = sshOptions(claim, pinned, knownHosts, sshKeyPath);
|
|
10126
10203
|
const destination = target(claim, pinned);
|
|
10127
10204
|
remoteDirectory = await checked(exec, ["ssh", ...ssh, destination, "--", "mktemp", "-d", "/tmp/forgezero-metal-admission.XXXXXXXX"], "SSH_UNREACHABLE");
|
|
10128
10205
|
if (!/^\/tmp\/forgezero-metal-admission\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
|
|
10129
10206
|
throw new SshBootstrapError("REMOTE_PATH_INVALID");
|
|
10207
|
+
const passwd = await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/getent", "passwd", claim.target.user]);
|
|
10208
|
+
const home = passwd.split(":")[5] ?? "";
|
|
10209
|
+
if (!home.startsWith("/") || /[\r\n]/.test(home))
|
|
10210
|
+
throw new SshBootstrapError("SSH_IDENTITY_INSTALL_FAILED");
|
|
10211
|
+
await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-d", "-m", "0700", `${home}/.ssh`]);
|
|
10212
|
+
const existing = await remote(exec, ssh, destination, "SSH_IDENTITY_READ_FAILED", ["/usr/bin/cat", `${home}/.ssh/authorized_keys`]).catch(() => "");
|
|
10213
|
+
const authorized = [...new Set([...existing.split(`
|
|
10214
|
+
`).map((line) => line.trim()).filter(Boolean), claim.runnerSshPublicKey])].join(`
|
|
10215
|
+
`) + `
|
|
10216
|
+
`;
|
|
10217
|
+
const authorizedPath = join3(directory, "authorized_keys");
|
|
10218
|
+
writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
|
|
10219
|
+
await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
|
|
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);
|
|
10130
10228
|
const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
|
|
10131
10229
|
const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
|
|
10132
10230
|
for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
|
|
@@ -10145,7 +10243,7 @@ async function executeMetalAdmission(claim, options) {
|
|
|
10145
10243
|
hostTelemetryEndpoint: "http://127.0.0.1:4318",
|
|
10146
10244
|
hostTelemetryUnit: "forgezero-otel-collector.service",
|
|
10147
10245
|
profile: {
|
|
10148
|
-
...
|
|
10246
|
+
...provisioning,
|
|
10149
10247
|
stateDir: "/etc/forgezero/metal-guests",
|
|
10150
10248
|
seedDir: "/var/lib/forgezero/seed",
|
|
10151
10249
|
unitDir: "/etc/systemd/system",
|
|
@@ -11363,7 +11461,7 @@ function requestDeploymentCommand(input, socketPath = DEFAULT_DEPLOYMENT_RUNNER_
|
|
|
11363
11461
|
// src/snp-attestation.ts
|
|
11364
11462
|
import { existsSync as existsSync10 } from "fs";
|
|
11365
11463
|
|
|
11366
|
-
//
|
|
11464
|
+
// ../runtime/dist/snp.js
|
|
11367
11465
|
var REPORT_BYTES = 1184;
|
|
11368
11466
|
|
|
11369
11467
|
// src/snp-attestation.ts
|
|
@@ -15302,6 +15400,26 @@ var nonEmpty = (path2) => {
|
|
|
15302
15400
|
if (!existsSync20(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
|
|
15303
15401
|
throw new Error(`required file missing: ${path2}`);
|
|
15304
15402
|
};
|
|
15403
|
+
function validateRecoveryBootstrapState(value, expected) {
|
|
15404
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15405
|
+
throw new Error("bootstrap state is malformed");
|
|
15406
|
+
const state = value;
|
|
15407
|
+
if (state.format !== 2 || state.kind !== "platform" || state.environment !== expected.profile || state.databaseRole !== expected.role || state.profile !== expected.software || state.apiUrl !== expected.apiOrigin || state.nodeHostname !== expected.edgeHostname || expected.databaseAddress !== undefined && state.databaseAddress !== expected.databaseAddress)
|
|
15408
|
+
throw new Error("bootstrap state does not match reviewed recovery coordinates");
|
|
15409
|
+
}
|
|
15410
|
+
var exactBootstrapState = (expected) => {
|
|
15411
|
+
const path2 = "/var/lib/forgezero/bootstrap.json";
|
|
15412
|
+
nonEmpty(path2);
|
|
15413
|
+
if (statSync5(path2).size > 64 * 1024)
|
|
15414
|
+
throw new Error("bootstrap state is oversized");
|
|
15415
|
+
let value;
|
|
15416
|
+
try {
|
|
15417
|
+
value = JSON.parse(readFileSync15(path2, "utf8"));
|
|
15418
|
+
} catch {
|
|
15419
|
+
throw new Error("bootstrap state is malformed");
|
|
15420
|
+
}
|
|
15421
|
+
validateRecoveryBootstrapState(value, expected);
|
|
15422
|
+
};
|
|
15305
15423
|
var option = (args, name) => {
|
|
15306
15424
|
const value = args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
15307
15425
|
if (!value || /[\0\r\n]/.test(value))
|
|
@@ -15325,15 +15443,11 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
15325
15443
|
const apiOrigin = option(args, "api-origin");
|
|
15326
15444
|
const edgeHostname = option(args, "edge-hostname");
|
|
15327
15445
|
const databaseAddress = args.find((arg) => arg.startsWith("--db-address="))?.slice(13);
|
|
15328
|
-
|
|
15329
|
-
exactState("/opt/forgezero/.
|
|
15330
|
-
exactState("/opt/forgezero/.
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
exactState("/opt/forgezero/shared/.env", "ARANGO_DB=fz");
|
|
15334
|
-
exactState("/opt/forgezero/shared/.env", "FZ_DATABASE_MODE=platform");
|
|
15335
|
-
exactState("/opt/forgezero/shared/.env", `API_ORIGIN=${apiOrigin}`);
|
|
15336
|
-
exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME=${edgeHostname}`);
|
|
15446
|
+
exactBootstrapState({ profile, role, software, apiOrigin, edgeHostname, ...databaseAddress ? { databaseAddress } : {} });
|
|
15447
|
+
exactState("/opt/forgezero/shared/.env", 'ARANGO_DB="fz"');
|
|
15448
|
+
exactState("/opt/forgezero/shared/.env", 'FZ_DATABASE_MODE="platform"');
|
|
15449
|
+
exactState("/opt/forgezero/shared/.env", `API_ORIGIN="${apiOrigin}"`);
|
|
15450
|
+
exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME="${edgeHostname}"`);
|
|
15337
15451
|
nonEmpty("/etc/forgezero/creds/arangodb-jwt.cred");
|
|
15338
15452
|
const hasDatabase = role !== "none";
|
|
15339
15453
|
if (hasDatabase) {
|
|
@@ -15377,7 +15491,7 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
15377
15491
|
throw new Error("maintenance accepts only fz db argv");
|
|
15378
15492
|
const slot = activeSlot();
|
|
15379
15493
|
const properties = [
|
|
15380
|
-
"--property=User=forgezero",
|
|
15494
|
+
"--property=User=forgezero-api",
|
|
15381
15495
|
`--property=WorkingDirectory=/opt/forgezero/slots/${slot}`,
|
|
15382
15496
|
"--property=Environment=NODE_ENV=production",
|
|
15383
15497
|
"--property=EnvironmentFile=/opt/forgezero/shared/.env",
|