@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/dist/host-maintenance.js
CHANGED
|
@@ -62,7 +62,7 @@ function planHostMaintenance(request, runtime = localRuntime()) {
|
|
|
62
62
|
"--pipe",
|
|
63
63
|
"--collect",
|
|
64
64
|
`--unit=forgezero-${request.operation}`,
|
|
65
|
-
"--property=User=forgezero",
|
|
65
|
+
"--property=User=forgezero-api",
|
|
66
66
|
`--property=WorkingDirectory=${workingDirectory}`,
|
|
67
67
|
`--property=EnvironmentFile=${SHARED_ENV}`,
|
|
68
68
|
"--property=Environment=NODE_ENV=production",
|
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' | '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";
|
|
@@ -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");
|
|
@@ -6713,12 +6734,12 @@ function planOperatorMetalBootstrap(request, mode) {
|
|
|
6713
6734
|
var secretSources = (config) => [
|
|
6714
6735
|
...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
|
|
6715
6736
|
];
|
|
6716
|
-
var attendedSecretNames = (config) => [
|
|
6737
|
+
var attendedSecretNames = (config, options = {}) => [
|
|
6717
6738
|
"cluster-bootstrap-code",
|
|
6718
6739
|
config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
|
|
6719
6740
|
...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
|
|
6720
6741
|
...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
|
|
6721
|
-
|
|
6742
|
+
...!options.reuseBoundCloudflare && (config.cloudflareHandoff || config.runtime.environment.cloudflare) ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
|
|
6722
6743
|
];
|
|
6723
6744
|
function planOperatorPlatformBootstrap(request, mode) {
|
|
6724
6745
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
@@ -6734,7 +6755,14 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
6734
6755
|
user: request.target.user,
|
|
6735
6756
|
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
6736
6757
|
},
|
|
6737
|
-
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
|
+
] : [
|
|
6738
6766
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
6739
6767
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
6740
6768
|
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
@@ -6742,7 +6770,10 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
6742
6770
|
"run typed fz bootstrap platform --apply from the local bundle",
|
|
6743
6771
|
"remove transient local and remote staging data"
|
|
6744
6772
|
],
|
|
6745
|
-
secretInputs: mode === "apply"
|
|
6773
|
+
secretInputs: mode === "apply" || mode === "repair" ? [
|
|
6774
|
+
...attendedSecretNames(config, { reuseBoundCloudflare: mode === "repair" }),
|
|
6775
|
+
...mode === "repair" ? [] : secretSources(config).map(([name]) => name)
|
|
6776
|
+
] : []
|
|
6746
6777
|
};
|
|
6747
6778
|
}
|
|
6748
6779
|
var fleetConfigIdentity = (config) => {
|
|
@@ -7172,10 +7203,10 @@ async function withOperatorGuestTransport(request, evidence, callback, options =
|
|
|
7172
7203
|
rmSync6(directory, { recursive: true, force: true });
|
|
7173
7204
|
}
|
|
7174
7205
|
}
|
|
7175
|
-
async function stageConfig(config, directory) {
|
|
7206
|
+
async function stageConfig(config, directory, options = {}) {
|
|
7176
7207
|
const rewritten = structuredClone(config);
|
|
7177
7208
|
const staged = [];
|
|
7178
|
-
for (const [name, source] of secretSources(config)) {
|
|
7209
|
+
for (const [name, source] of options.reuseBoundCloudflare ? [] : secretSources(config)) {
|
|
7179
7210
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
7180
7211
|
const local = join6(directory, name);
|
|
7181
7212
|
writeFileSync8(local, bytes, { mode: 384, flag: "wx" });
|
|
@@ -7184,6 +7215,9 @@ async function stageConfig(config, directory) {
|
|
|
7184
7215
|
if (name === "cloudflare-handoff")
|
|
7185
7216
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
7186
7217
|
}
|
|
7218
|
+
if (options.reuseBoundCloudflare && rewritten.cloudflareHandoff) {
|
|
7219
|
+
rewritten.cloudflareHandoff.handoffFile = `${REMOTE_STAGE}/cloudflare-handoff`;
|
|
7220
|
+
}
|
|
7187
7221
|
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
7188
7222
|
if (bundle.manifest.branch !== config.bootstrapBundle.branch) {
|
|
7189
7223
|
throw new Error("reviewed bootstrap config branch does not match the bundle manifest");
|
|
@@ -7290,8 +7324,12 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7290
7324
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
7291
7325
|
if (config.kind !== "platform")
|
|
7292
7326
|
throw new Error("operator bootstrap requires a platform config");
|
|
7293
|
-
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets
|
|
7294
|
-
|
|
7327
|
+
const secrets = mode === "apply" || mode === "repair" ? validatePlatformBootstrapSecrets(config, options.secrets, {
|
|
7328
|
+
allowStoredCloudflareCredentials: mode === "repair"
|
|
7329
|
+
}) : undefined;
|
|
7330
|
+
const staged = await stageConfig(config, directory, {
|
|
7331
|
+
reuseBoundCloudflare: mode === "repair" || mode === "promote"
|
|
7332
|
+
});
|
|
7295
7333
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
7296
7334
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
7297
7335
|
for (const name of staged.files)
|
|
@@ -7302,7 +7340,14 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
7302
7340
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
|
|
7303
7341
|
for (const { name } of staged.bundleFiles)
|
|
7304
7342
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0644", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap bundle", true);
|
|
7305
|
-
const command = [
|
|
7343
|
+
const command = [
|
|
7344
|
+
"/usr/bin/sudo",
|
|
7345
|
+
"-n",
|
|
7346
|
+
"/usr/local/bin/fz",
|
|
7347
|
+
"bootstrap",
|
|
7348
|
+
mode === "apply" ? "platform" : "repair",
|
|
7349
|
+
mode === "promote" ? "stored-credentials" : "credentials-stdin"
|
|
7350
|
+
];
|
|
7306
7351
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
7307
7352
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
7308
7353
|
` : undefined, mode === "apply");
|
|
@@ -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/recovery-host.d.ts
CHANGED
|
@@ -4,4 +4,13 @@ export interface RecoveryHostResult {
|
|
|
4
4
|
stderr: string;
|
|
5
5
|
}
|
|
6
6
|
export type RecoveryHostExec = (argv: readonly string[]) => Promise<RecoveryHostResult>;
|
|
7
|
+
export interface RecoveryBootstrapCoordinates {
|
|
8
|
+
profile: string;
|
|
9
|
+
role: string;
|
|
10
|
+
software: string;
|
|
11
|
+
apiOrigin: string;
|
|
12
|
+
edgeHostname: string;
|
|
13
|
+
databaseAddress?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function validateRecoveryBootstrapState(value: unknown, expected: RecoveryBootstrapCoordinates): void;
|
|
7
16
|
export declare function runRecoveryHost(args: readonly string[], run?: RecoveryHostExec): Promise<void>;
|
package/dist/recovery-host.js
CHANGED
|
@@ -28,6 +28,26 @@ var nonEmpty = (path) => {
|
|
|
28
28
|
if (!existsSync(path) || !statSync(path).isFile() || statSync(path).size < 1)
|
|
29
29
|
throw new Error(`required file missing: ${path}`);
|
|
30
30
|
};
|
|
31
|
+
function validateRecoveryBootstrapState(value, expected) {
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33
|
+
throw new Error("bootstrap state is malformed");
|
|
34
|
+
const state = value;
|
|
35
|
+
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)
|
|
36
|
+
throw new Error("bootstrap state does not match reviewed recovery coordinates");
|
|
37
|
+
}
|
|
38
|
+
var exactBootstrapState = (expected) => {
|
|
39
|
+
const path = "/var/lib/forgezero/bootstrap.json";
|
|
40
|
+
nonEmpty(path);
|
|
41
|
+
if (statSync(path).size > 64 * 1024)
|
|
42
|
+
throw new Error("bootstrap state is oversized");
|
|
43
|
+
let value;
|
|
44
|
+
try {
|
|
45
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error("bootstrap state is malformed");
|
|
48
|
+
}
|
|
49
|
+
validateRecoveryBootstrapState(value, expected);
|
|
50
|
+
};
|
|
31
51
|
var option = (args, name) => {
|
|
32
52
|
const value = args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
33
53
|
if (!value || /[\0\r\n]/.test(value))
|
|
@@ -51,15 +71,11 @@ async function runRecoveryHost(args, run = execute) {
|
|
|
51
71
|
const apiOrigin = option(args, "api-origin");
|
|
52
72
|
const edgeHostname = option(args, "edge-hostname");
|
|
53
73
|
const databaseAddress = args.find((arg) => arg.startsWith("--db-address="))?.slice(13);
|
|
54
|
-
|
|
55
|
-
exactState("/opt/forgezero/.
|
|
56
|
-
exactState("/opt/forgezero/.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
exactState("/opt/forgezero/shared/.env", "ARANGO_DB=fz");
|
|
60
|
-
exactState("/opt/forgezero/shared/.env", "FZ_DATABASE_MODE=platform");
|
|
61
|
-
exactState("/opt/forgezero/shared/.env", `API_ORIGIN=${apiOrigin}`);
|
|
62
|
-
exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME=${edgeHostname}`);
|
|
74
|
+
exactBootstrapState({ profile, role, software, apiOrigin, edgeHostname, ...databaseAddress ? { databaseAddress } : {} });
|
|
75
|
+
exactState("/opt/forgezero/shared/.env", 'ARANGO_DB="fz"');
|
|
76
|
+
exactState("/opt/forgezero/shared/.env", 'FZ_DATABASE_MODE="platform"');
|
|
77
|
+
exactState("/opt/forgezero/shared/.env", `API_ORIGIN="${apiOrigin}"`);
|
|
78
|
+
exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME="${edgeHostname}"`);
|
|
63
79
|
nonEmpty("/etc/forgezero/creds/arangodb-jwt.cred");
|
|
64
80
|
const hasDatabase = role !== "none";
|
|
65
81
|
if (hasDatabase) {
|
|
@@ -103,7 +119,7 @@ async function runRecoveryHost(args, run = execute) {
|
|
|
103
119
|
throw new Error("maintenance accepts only fz db argv");
|
|
104
120
|
const slot = activeSlot();
|
|
105
121
|
const properties = [
|
|
106
|
-
"--property=User=forgezero",
|
|
122
|
+
"--property=User=forgezero-api",
|
|
107
123
|
`--property=WorkingDirectory=/opt/forgezero/slots/${slot}`,
|
|
108
124
|
"--property=Environment=NODE_ENV=production",
|
|
109
125
|
"--property=EnvironmentFile=/opt/forgezero/shared/.env",
|
|
@@ -120,5 +136,6 @@ async function runRecoveryHost(args, run = execute) {
|
|
|
120
136
|
throw new Error("unknown recovery-host action");
|
|
121
137
|
}
|
|
122
138
|
export {
|
|
139
|
+
validateRecoveryBootstrapState,
|
|
123
140
|
runRecoveryHost
|
|
124
141
|
};
|
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;
|
|
@@ -10,6 +11,7 @@ export interface RemoteSshBootstrapClaim {
|
|
|
10
11
|
realm: string;
|
|
11
12
|
projectKey: string;
|
|
12
13
|
environmentKey: string;
|
|
14
|
+
runnerSshPublicKey: string;
|
|
13
15
|
target: {
|
|
14
16
|
host: string;
|
|
15
17
|
port: number;
|
|
@@ -25,7 +27,12 @@ export interface RemoteSshBootstrapClaim {
|
|
|
25
27
|
source: 'runner';
|
|
26
28
|
} | {
|
|
27
29
|
source: 'vault';
|
|
30
|
+
kind: 'private-key';
|
|
28
31
|
privateKey: string;
|
|
32
|
+
} | {
|
|
33
|
+
source: 'vault';
|
|
34
|
+
kind: 'password';
|
|
35
|
+
password: string;
|
|
29
36
|
};
|
|
30
37
|
};
|
|
31
38
|
enrolmentToken: string;
|
|
@@ -42,31 +49,18 @@ export interface RemoteMetalAdmissionClaim {
|
|
|
42
49
|
label: string;
|
|
43
50
|
regionKey: string;
|
|
44
51
|
runnerNodeKey: string;
|
|
52
|
+
runnerSshPublicKey: string;
|
|
45
53
|
challenge: string;
|
|
46
|
-
provisioning: {
|
|
47
|
-
volumeGroup: string;
|
|
48
|
-
bridge: string;
|
|
49
|
-
subnetPrefix: string;
|
|
50
|
-
addressStart: number;
|
|
51
|
-
addressEnd: number;
|
|
52
|
-
gateway: string;
|
|
53
|
-
nameservers: string[];
|
|
54
|
-
cpuPools: Array<{
|
|
55
|
-
key: string;
|
|
56
|
-
cpus: string;
|
|
57
|
-
physicalCores: number;
|
|
58
|
-
memoryNodes?: string;
|
|
59
|
-
}>;
|
|
60
|
-
housekeepingCpus: string;
|
|
61
|
-
housekeepingMemoryNodes?: string;
|
|
62
|
-
confidential?: {
|
|
63
|
-
cbitpos: number;
|
|
64
|
-
reducedPhysBits: number;
|
|
65
|
-
policy: string;
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
54
|
target: RemoteSshBootstrapClaim['target'];
|
|
69
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;
|
|
70
64
|
export interface MetalAdmissionProof {
|
|
71
65
|
jobKey: string;
|
|
72
66
|
tenantKey: string;
|
|
@@ -109,6 +103,7 @@ export interface BootstrapCommandResult {
|
|
|
109
103
|
export type BootstrapCommand = (argv: readonly string[], options?: {
|
|
110
104
|
stdin?: string;
|
|
111
105
|
secret?: boolean;
|
|
106
|
+
env?: Record<string, string>;
|
|
112
107
|
}) => Promise<BootstrapCommandResult>;
|
|
113
108
|
export interface SshBootstrapPullOptions {
|
|
114
109
|
apiUrl: string;
|
|
@@ -173,3 +168,4 @@ export declare function startMetalAdmissionPull(options: SshBootstrapPullOptions
|
|
|
173
168
|
stop(): Promise<void>;
|
|
174
169
|
readonly active: boolean;
|
|
175
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",
|