@forgezero/agent 0.1.62 → 0.1.64
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 +45 -16
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +31 -0
- package/dist/bootstrap-bundle.js +165 -0
- package/dist/bootstrap.d.ts +24 -18
- package/dist/bootstrap.js +509 -269
- package/dist/cli/agent-install.d.ts +2 -0
- package/dist/community-rehearsal-host.js +7 -5
- package/dist/definition.js +7 -5
- package/dist/deploy-file.js +7 -5
- package/dist/deployment.d.ts +27 -0
- package/dist/fz-agent.js +594 -421
- package/dist/fz.js +847 -372
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +0 -2
- package/dist/metal-provision.js +0 -2
- package/dist/operator-bootstrap.d.ts +43 -1
- package/dist/operator-bootstrap.js +788 -367
- package/dist/platform-bootstrap-runtime.d.ts +0 -2
- package/dist/platform-bootstrap-runtime.js +0 -4
- package/dist/platform-fleet-verification.js +190 -98
- package/dist/provision.d.ts +4 -1
- package/dist/provision.js +41 -32
- package/dist/software-helper.js +7 -5
- package/dist/software.d.ts +1 -1
- package/dist/software.js +7 -5
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/fz.js
CHANGED
|
@@ -4811,8 +4811,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
4813
|
init_dist();
|
|
4814
|
-
import { existsSync as
|
|
4815
|
-
import { basename as basename3, dirname as
|
|
4814
|
+
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
4815
|
+
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve11 } from "path";
|
|
4816
4816
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4817
4817
|
import { hostname } from "os";
|
|
4818
4818
|
|
|
@@ -4829,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4829
4829
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4830
4830
|
|
|
4831
4831
|
// src/version.ts
|
|
4832
|
-
var VERSION2 = "0.1.
|
|
4832
|
+
var VERSION2 = "0.1.64";
|
|
4833
4833
|
|
|
4834
4834
|
// src/software.ts
|
|
4835
4835
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4845,7 +4845,8 @@ var SOFTWARE_CATALOG = [
|
|
|
4845
4845
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4846
4846
|
{ id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4847
4847
|
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4848
|
-
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4848
|
+
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4849
|
+
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4849
4850
|
];
|
|
4850
4851
|
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
4851
4852
|
"data-root": "/var/lib/docker",
|
|
@@ -4866,7 +4867,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
4866
4867
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
4867
4868
|
throw new Error("software requirement contains an unknown field");
|
|
4868
4869
|
}
|
|
4869
|
-
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4870
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4870
4871
|
throw new Error("software requirement coordinate is invalid");
|
|
4871
4872
|
}
|
|
4872
4873
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -5427,7 +5428,11 @@ function agentEgressUnit(options) {
|
|
|
5427
5428
|
const user = options.user ?? "forgezero";
|
|
5428
5429
|
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
|
|
5429
5430
|
throw new Error("invalid Agent service user");
|
|
5430
|
-
const
|
|
5431
|
+
const bootstrapBundleEnabled = Boolean(options.bootstrapBundlePath && options.bootstrapBundleManifestPath);
|
|
5432
|
+
if (Boolean(options.bootstrapBundlePath) !== Boolean(options.bootstrapBundleManifestPath) || bootstrapBundleEnabled && ![options.bootstrapBundlePath, options.bootstrapBundleManifestPath].every((path) => path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
5433
|
+
throw new Error("bootstrap bundle and manifest must be supplied together as absolute paths");
|
|
5434
|
+
}
|
|
5435
|
+
const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
|
|
5431
5436
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
5432
5437
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
5433
5438
|
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
@@ -5833,17 +5838,16 @@ function agentUnit(options) {
|
|
|
5833
5838
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
5834
5839
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
5835
5840
|
}
|
|
5836
|
-
const
|
|
5837
|
-
if (
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
5841
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
5842
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath)) {
|
|
5843
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
5844
|
+
}
|
|
5845
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
5846
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
5847
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
5844
5848
|
}
|
|
5845
5849
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
5846
|
-
const bootstrapSshCredentialPath =
|
|
5850
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
5847
5851
|
const warpValues = [
|
|
5848
5852
|
options.warpOrganization,
|
|
5849
5853
|
options.warpClientIdCredentialPath,
|
|
@@ -5897,6 +5901,8 @@ function agentUnit(options) {
|
|
|
5897
5901
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
5898
5902
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
5899
5903
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
5904
|
+
options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
|
|
5905
|
+
options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
|
|
5900
5906
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
5901
5907
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
5902
5908
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
@@ -6080,15 +6086,13 @@ function planProvision(options) {
|
|
|
6080
6086
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
6081
6087
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
6082
6088
|
}
|
|
6083
|
-
const
|
|
6084
|
-
if (
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
].some(Boolean) && !bootstrapEnabled) {
|
|
6091
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
6089
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
6090
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath) || options.bootstrapSshSourcePath && !bootstrapIdentityEnabled) {
|
|
6091
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
6092
|
+
}
|
|
6093
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
6094
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
6095
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
6092
6096
|
}
|
|
6093
6097
|
const warpValues = [
|
|
6094
6098
|
options.warpOrganization,
|
|
@@ -6098,13 +6102,13 @@ function planProvision(options) {
|
|
|
6098
6102
|
const warpEnabled = warpValues.every(Boolean);
|
|
6099
6103
|
if (warpValues.some(Boolean) && !warpEnabled)
|
|
6100
6104
|
throw new Error("WARP configuration must be supplied together");
|
|
6101
|
-
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath
|
|
6102
|
-
if (
|
|
6103
|
-
throw new Error("direct enrolment credential
|
|
6105
|
+
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath);
|
|
6106
|
+
if (options.enrolTokenCredentialPath && !options.enrolStatePath || options.enrolTokenSourcePath && (!options.enrolTokenCredentialPath || !options.enrolStatePath)) {
|
|
6107
|
+
throw new Error("direct enrolment credential requires its durable state path");
|
|
6104
6108
|
}
|
|
6105
6109
|
const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
6106
6110
|
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
6107
|
-
const enrolStatePath =
|
|
6111
|
+
const enrolStatePath = options.enrolStatePath ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
6108
6112
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
6109
6113
|
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
6110
6114
|
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
@@ -6116,8 +6120,8 @@ function planProvision(options) {
|
|
|
6116
6120
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
6117
6121
|
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
6118
6122
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
6119
|
-
const bootstrapSshCredentialPath =
|
|
6120
|
-
const bootstrapSshPublicKeyPath =
|
|
6123
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
6124
|
+
const bootstrapSshPublicKeyPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
|
|
6121
6125
|
const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
|
|
6122
6126
|
const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
6123
6127
|
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
@@ -6217,7 +6221,7 @@ function planProvision(options) {
|
|
|
6217
6221
|
step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
|
|
6218
6222
|
step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
|
|
6219
6223
|
] : [],
|
|
6220
|
-
...
|
|
6224
|
+
...bootstrapIdentityEnabled ? [
|
|
6221
6225
|
step("bootstrap SSH public identity directory", { kind: "directories", directories: [
|
|
6222
6226
|
{ path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
|
|
6223
6227
|
] }),
|
|
@@ -6253,6 +6257,10 @@ function planProvision(options) {
|
|
|
6253
6257
|
{ argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
|
|
6254
6258
|
{ argv: ["/usr/bin/systemctl", "daemon-reload"] }
|
|
6255
6259
|
] })] : [],
|
|
6260
|
+
...enrolStatePath && !enrolmentEnabled ? [step("retire consumed enrolment unit", { kind: "commands", commands: [
|
|
6261
|
+
{ argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-agent-enrol.service"], acceptedExitCodes: [0, 1, 5] },
|
|
6262
|
+
{ argv: ["/usr/bin/rm", "-f", ENROLMENT_UNIT_PATH] }
|
|
6263
|
+
] })] : [],
|
|
6256
6264
|
step("enable and converge services", { kind: "commands", commands: [
|
|
6257
6265
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
6258
6266
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
@@ -11555,18 +11563,18 @@ function removeSession(api, realm, path = defaultSessionPath()) {
|
|
|
11555
11563
|
}
|
|
11556
11564
|
|
|
11557
11565
|
// src/bootstrap.ts
|
|
11558
|
-
import { createHash as
|
|
11566
|
+
import { createHash as createHash4, createHmac as createHmac2, randomBytes as randomBytes7 } from "crypto";
|
|
11559
11567
|
import {
|
|
11560
|
-
chmodSync as
|
|
11561
|
-
existsSync as
|
|
11562
|
-
lstatSync as
|
|
11563
|
-
mkdirSync as
|
|
11564
|
-
readFileSync as
|
|
11565
|
-
renameSync as
|
|
11566
|
-
rmSync as
|
|
11567
|
-
writeFileSync as
|
|
11568
|
+
chmodSync as chmodSync4,
|
|
11569
|
+
existsSync as existsSync9,
|
|
11570
|
+
lstatSync as lstatSync5,
|
|
11571
|
+
mkdirSync as mkdirSync9,
|
|
11572
|
+
readFileSync as readFileSync9,
|
|
11573
|
+
renameSync as renameSync8,
|
|
11574
|
+
rmSync as rmSync6,
|
|
11575
|
+
writeFileSync as writeFileSync9
|
|
11568
11576
|
} from "fs";
|
|
11569
|
-
import { dirname as
|
|
11577
|
+
import { dirname as dirname9 } from "path";
|
|
11570
11578
|
import { fileURLToPath } from "url";
|
|
11571
11579
|
|
|
11572
11580
|
// src/platform-bootstrap-runtime.ts
|
|
@@ -11654,8 +11662,6 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
11654
11662
|
databaseUser: input.databaseUser,
|
|
11655
11663
|
sharedDirectory: input.sharedDirectory,
|
|
11656
11664
|
seedSyncEpoch: input.seedSyncEpoch,
|
|
11657
|
-
repository: input.repository,
|
|
11658
|
-
branch: input.branch,
|
|
11659
11665
|
deployProfile: input.deployProfile
|
|
11660
11666
|
}))
|
|
11661
11667
|
safeAtom(name, value);
|
|
@@ -11738,8 +11744,6 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
11738
11744
|
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
11739
11745
|
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
11740
11746
|
FZ_PROFILE: value.deployProfile,
|
|
11741
|
-
FZ_REPO: value.repository,
|
|
11742
|
-
FZ_BRANCH: value.branch,
|
|
11743
11747
|
FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
|
|
11744
11748
|
FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
|
|
11745
11749
|
FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
|
|
@@ -13158,6 +13162,164 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
13158
13162
|
|
|
13159
13163
|
// src/bootstrap.ts
|
|
13160
13164
|
init_dist();
|
|
13165
|
+
|
|
13166
|
+
// src/bootstrap-bundle.ts
|
|
13167
|
+
import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
|
|
13168
|
+
import {
|
|
13169
|
+
chmodSync as chmodSync3,
|
|
13170
|
+
createReadStream,
|
|
13171
|
+
existsSync as existsSync8,
|
|
13172
|
+
lstatSync as lstatSync4,
|
|
13173
|
+
mkdirSync as mkdirSync8,
|
|
13174
|
+
readFileSync as readFileSync8,
|
|
13175
|
+
renameSync as renameSync7,
|
|
13176
|
+
rmSync as rmSync5,
|
|
13177
|
+
writeFileSync as writeFileSync8
|
|
13178
|
+
} from "fs";
|
|
13179
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve6 } from "path";
|
|
13180
|
+
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
13181
|
+
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
13182
|
+
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
13183
|
+
var run = async (argv2) => {
|
|
13184
|
+
const child = Bun.spawn([...argv2], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
13185
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
13186
|
+
new Response(child.stdout).text(),
|
|
13187
|
+
new Response(child.stderr).text(),
|
|
13188
|
+
child.exited
|
|
13189
|
+
]);
|
|
13190
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 65536) };
|
|
13191
|
+
};
|
|
13192
|
+
var checked2 = async (exec, argv2, label) => {
|
|
13193
|
+
const result = await exec(argv2);
|
|
13194
|
+
if (result.exitCode !== 0)
|
|
13195
|
+
throw new Error(`${label} failed${result.output.trim() ? `: ${result.output.trim()}` : ""}`);
|
|
13196
|
+
return result.output.trim();
|
|
13197
|
+
};
|
|
13198
|
+
async function sha256File(path) {
|
|
13199
|
+
const hash = createHash3("sha256");
|
|
13200
|
+
await new Promise((resolveDone, reject) => {
|
|
13201
|
+
const stream = createReadStream(path);
|
|
13202
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
13203
|
+
stream.on("error", reject);
|
|
13204
|
+
stream.on("end", resolveDone);
|
|
13205
|
+
});
|
|
13206
|
+
return hash.digest("hex");
|
|
13207
|
+
}
|
|
13208
|
+
var branchName = (value) => {
|
|
13209
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
|
|
13210
|
+
throw new Error("bootstrap bundle branch is malformed");
|
|
13211
|
+
}
|
|
13212
|
+
return value;
|
|
13213
|
+
};
|
|
13214
|
+
function parseBootstrapBundleManifest(value) {
|
|
13215
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13216
|
+
throw new Error("bootstrap bundle manifest must be an object");
|
|
13217
|
+
const source = value;
|
|
13218
|
+
const allowed = ["format", "kind", "branch", "revision", "sha256", "bytes", "createdAt"];
|
|
13219
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
13220
|
+
if (unknown.length)
|
|
13221
|
+
throw new Error(`bootstrap bundle manifest contains unknown field ${unknown[0]}`);
|
|
13222
|
+
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
13223
|
+
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
13224
|
+
}
|
|
13225
|
+
branchName(source.branch);
|
|
13226
|
+
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
13227
|
+
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
13228
|
+
}
|
|
13229
|
+
if (typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256)) {
|
|
13230
|
+
throw new Error("bootstrap bundle digest is malformed");
|
|
13231
|
+
}
|
|
13232
|
+
if (!Number.isSafeInteger(source.bytes) || Number(source.bytes) < 1 || Number(source.bytes) > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
13233
|
+
throw new Error("bootstrap bundle size is outside the supported boundary");
|
|
13234
|
+
}
|
|
13235
|
+
if (typeof source.createdAt !== "string" || Number.isNaN(Date.parse(source.createdAt))) {
|
|
13236
|
+
throw new Error("bootstrap bundle creation time is malformed");
|
|
13237
|
+
}
|
|
13238
|
+
return source;
|
|
13239
|
+
}
|
|
13240
|
+
function ownerRegularFile(path, maximum, label) {
|
|
13241
|
+
if (!isAbsolute2(path) || resolve6(path) !== path || /[\r\n\0]/.test(path)) {
|
|
13242
|
+
throw new Error(`${label} path must be canonical and absolute`);
|
|
13243
|
+
}
|
|
13244
|
+
const value = lstatSync4(path);
|
|
13245
|
+
const uid = process.getuid?.() ?? value.uid;
|
|
13246
|
+
if (!value.isFile() || value.isSymbolicLink() || value.uid !== uid || value.nlink !== 1 || (value.mode & 63) !== 0 || value.size < 1 || value.size > maximum) {
|
|
13247
|
+
throw new Error(`${label} must be an owner-only regular file with one link and at most ${maximum} bytes`);
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13250
|
+
async function readBootstrapBundle(bundlePath, manifestPath = `${bundlePath}.json`) {
|
|
13251
|
+
ownerRegularFile(bundlePath, MAX_BOOTSTRAP_BUNDLE_BYTES, "bootstrap bundle");
|
|
13252
|
+
ownerRegularFile(manifestPath, 16 * 1024, "bootstrap bundle manifest");
|
|
13253
|
+
let parsed;
|
|
13254
|
+
try {
|
|
13255
|
+
parsed = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
13256
|
+
} catch {
|
|
13257
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13258
|
+
}
|
|
13259
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13260
|
+
const size = lstatSync4(bundlePath).size;
|
|
13261
|
+
if (size !== manifest.bytes || await sha256File(bundlePath) !== manifest.sha256) {
|
|
13262
|
+
throw new Error("bootstrap bundle bytes do not match their manifest");
|
|
13263
|
+
}
|
|
13264
|
+
return { bundlePath, manifestPath, manifest };
|
|
13265
|
+
}
|
|
13266
|
+
async function buildBootstrapBundle(input, exec = run) {
|
|
13267
|
+
const repositoryRoot = resolve6(input.repositoryRoot);
|
|
13268
|
+
const outputPath = resolve6(input.outputPath);
|
|
13269
|
+
const manifestPath = `${outputPath}.json`;
|
|
13270
|
+
const branch = branchName(input.branch);
|
|
13271
|
+
if (!isAbsolute2(input.outputPath) || outputPath !== input.outputPath) {
|
|
13272
|
+
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
13273
|
+
}
|
|
13274
|
+
if (existsSync8(outputPath) || existsSync8(manifestPath)) {
|
|
13275
|
+
throw new Error("bootstrap bundle output already exists");
|
|
13276
|
+
}
|
|
13277
|
+
const root = lstatSync4(repositoryRoot);
|
|
13278
|
+
if (!root.isDirectory() || root.isSymbolicLink())
|
|
13279
|
+
throw new Error("bootstrap bundle source must be a real directory");
|
|
13280
|
+
mkdirSync8(dirname8(outputPath), { recursive: true, mode: 448 });
|
|
13281
|
+
const parent = lstatSync4(dirname8(outputPath));
|
|
13282
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
13283
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
13284
|
+
throw new Error("bootstrap bundle output directory must be caller-owned and owner-only");
|
|
13285
|
+
}
|
|
13286
|
+
const dirty = await checked2(exec, ["git", "-C", repositoryRoot, "status", "--porcelain=v1", "--untracked-files=no"], "Git worktree check");
|
|
13287
|
+
if (dirty)
|
|
13288
|
+
throw new Error("bootstrap bundle source has tracked changes; commit the reviewed API release first");
|
|
13289
|
+
const revision = (await checked2(exec, ["git", "-C", repositoryRoot, "rev-parse", "--verify", `refs/heads/${branch}^{commit}`], "Git branch resolution")).toLowerCase();
|
|
13290
|
+
if (!/^[a-f0-9]{40}$/.test(revision))
|
|
13291
|
+
throw new Error("bootstrap bundle branch did not resolve to one exact commit");
|
|
13292
|
+
const temporary = `${outputPath}.next.${process.pid}.${randomBytes6(6).toString("hex")}`;
|
|
13293
|
+
try {
|
|
13294
|
+
await checked2(exec, ["git", "-C", repositoryRoot, "bundle", "create", temporary, `refs/heads/${branch}`], "Git bundle creation");
|
|
13295
|
+
chmodSync3(temporary, 384);
|
|
13296
|
+
const file = lstatSync4(temporary);
|
|
13297
|
+
if (!file.isFile() || file.isSymbolicLink() || file.size < 1 || file.size > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
13298
|
+
throw new Error("created bootstrap bundle is outside the supported boundary");
|
|
13299
|
+
}
|
|
13300
|
+
await checked2(exec, ["git", "bundle", "verify", temporary], "Git bundle verification");
|
|
13301
|
+
const manifest = {
|
|
13302
|
+
format: BOOTSTRAP_BUNDLE_FORMAT,
|
|
13303
|
+
kind: BOOTSTRAP_BUNDLE_KIND,
|
|
13304
|
+
branch,
|
|
13305
|
+
revision,
|
|
13306
|
+
sha256: await sha256File(temporary),
|
|
13307
|
+
bytes: file.size,
|
|
13308
|
+
createdAt: new Date().toISOString()
|
|
13309
|
+
};
|
|
13310
|
+
const manifestTemporary = `${manifestPath}.next.${process.pid}.${randomBytes6(6).toString("hex")}`;
|
|
13311
|
+
writeFileSync8(manifestTemporary, `${JSON.stringify(manifest, null, 2)}
|
|
13312
|
+
`, { mode: 384, flag: "wx" });
|
|
13313
|
+
renameSync7(temporary, outputPath);
|
|
13314
|
+
renameSync7(manifestTemporary, manifestPath);
|
|
13315
|
+
return { bundlePath: outputPath, manifestPath, manifest };
|
|
13316
|
+
} catch (cause) {
|
|
13317
|
+
rmSync5(temporary, { force: true });
|
|
13318
|
+
throw cause;
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13321
|
+
|
|
13322
|
+
// src/bootstrap.ts
|
|
13161
13323
|
var PLATFORM_BOOTSTRAP_PROFILES = [
|
|
13162
13324
|
"platform-db-api",
|
|
13163
13325
|
"platform-api"
|
|
@@ -13257,7 +13419,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
|
|
|
13257
13419
|
var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
13258
13420
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
13259
13421
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
13260
|
-
var
|
|
13422
|
+
var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
|
|
13261
13423
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
13262
13424
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
13263
13425
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -13330,6 +13492,9 @@ function validateBootstrapConfig(value) {
|
|
|
13330
13492
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
13331
13493
|
}
|
|
13332
13494
|
if (value.kind === "enrolled-compute") {
|
|
13495
|
+
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
13496
|
+
throw new Error("gitDeployKey must be boolean");
|
|
13497
|
+
}
|
|
13333
13498
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
|
|
13334
13499
|
throw new Error("tenant realm is malformed");
|
|
13335
13500
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
@@ -13355,6 +13520,9 @@ function validateBootstrapConfig(value) {
|
|
|
13355
13520
|
throw new Error("unsupported platform software profile");
|
|
13356
13521
|
if (!["production", "development"].includes(value.environment))
|
|
13357
13522
|
throw new Error("platform environment must be production or development");
|
|
13523
|
+
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
13524
|
+
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
13525
|
+
}
|
|
13358
13526
|
let api;
|
|
13359
13527
|
try {
|
|
13360
13528
|
api = new URL(value.apiUrl);
|
|
@@ -13387,7 +13555,7 @@ function validateBootstrapConfig(value) {
|
|
|
13387
13555
|
if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
13388
13556
|
throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
13389
13557
|
}
|
|
13390
|
-
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.
|
|
13558
|
+
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
13391
13559
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
13392
13560
|
}
|
|
13393
13561
|
if (runtime.deployProfile !== value.environment)
|
|
@@ -13417,6 +13585,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
13417
13585
|
] : [
|
|
13418
13586
|
...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
|
|
13419
13587
|
{ id: "bun", version: "1.3.14" },
|
|
13588
|
+
{ id: "git", version: "ubuntu-26.04" },
|
|
13420
13589
|
{ id: "nginx", version: "ubuntu-26.04" },
|
|
13421
13590
|
...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
|
|
13422
13591
|
...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
|
|
@@ -13445,7 +13614,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
13445
13614
|
]
|
|
13446
13615
|
};
|
|
13447
13616
|
}
|
|
13448
|
-
var
|
|
13617
|
+
var checked3 = async (host, argv2, label, options) => {
|
|
13449
13618
|
const result = await host.exec(argv2, options);
|
|
13450
13619
|
if (result.exitCode !== 0)
|
|
13451
13620
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
@@ -13626,6 +13795,37 @@ async function seal(host, name, destination2, value) {
|
|
|
13626
13795
|
if (result.exitCode !== 0)
|
|
13627
13796
|
throw new Error(`could not seal ${name}: ${result.output.trim()}`);
|
|
13628
13797
|
}
|
|
13798
|
+
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
13799
|
+
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
13800
|
+
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
13801
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
13802
|
+
throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
|
|
13803
|
+
}
|
|
13804
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
13805
|
+
throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
|
|
13806
|
+
}
|
|
13807
|
+
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
13808
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
13809
|
+
}
|
|
13810
|
+
let parsed;
|
|
13811
|
+
try {
|
|
13812
|
+
parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
|
|
13813
|
+
} catch {
|
|
13814
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13815
|
+
}
|
|
13816
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13817
|
+
const expectedBranch = config.environment === "production" ? "main" : "dev";
|
|
13818
|
+
if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
13819
|
+
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
13820
|
+
}
|
|
13821
|
+
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
13822
|
+
if (digest !== manifest.sha256)
|
|
13823
|
+
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
13824
|
+
if (verifyGit) {
|
|
13825
|
+
await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
|
|
13826
|
+
}
|
|
13827
|
+
return manifest;
|
|
13828
|
+
}
|
|
13629
13829
|
function bootstrapIdentity(config) {
|
|
13630
13830
|
if (config.kind === "enrolled-compute")
|
|
13631
13831
|
return {
|
|
@@ -13652,8 +13852,7 @@ function bootstrapIdentity(config) {
|
|
|
13652
13852
|
computeReference: config.computeReference,
|
|
13653
13853
|
nodeHostname: config.nodeHostname,
|
|
13654
13854
|
apiUrl: config.apiUrl,
|
|
13655
|
-
|
|
13656
|
-
branch: config.branch,
|
|
13855
|
+
bootstrapBundle: config.bootstrapBundle,
|
|
13657
13856
|
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
13658
13857
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
13659
13858
|
database: {
|
|
@@ -13683,7 +13882,7 @@ function bootstrapIdentity(config) {
|
|
|
13683
13882
|
};
|
|
13684
13883
|
}
|
|
13685
13884
|
function bootstrapIdentityDigest(config) {
|
|
13686
|
-
return
|
|
13885
|
+
return createHash4("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
13687
13886
|
}
|
|
13688
13887
|
function parseStoredState(raw) {
|
|
13689
13888
|
let value;
|
|
@@ -13733,34 +13932,6 @@ function bindBootstrapIntent(host, config) {
|
|
|
13733
13932
|
}
|
|
13734
13933
|
return identityDigest;
|
|
13735
13934
|
}
|
|
13736
|
-
async function preparePlatformBootstrap(input, host = localBootstrapHost()) {
|
|
13737
|
-
const config = validateBootstrapConfig(structuredClone(input));
|
|
13738
|
-
if (config.kind !== "platform")
|
|
13739
|
-
throw new Error("platform preparation requires a platform bootstrap config");
|
|
13740
|
-
if (host.uid() !== 0)
|
|
13741
|
-
throw new Error("fz bootstrap platform prepare --apply must run as root");
|
|
13742
|
-
if (host.exists(STATE_PATH)) {
|
|
13743
|
-
const installed = parseStoredState(host.read(STATE_PATH));
|
|
13744
|
-
if (installed.kind !== "platform" || installed.identityDigest !== bootstrapIdentityDigest(config)) {
|
|
13745
|
-
throw new Error("platform preparation coordinates do not match the installed host identity");
|
|
13746
|
-
}
|
|
13747
|
-
}
|
|
13748
|
-
const identityDigest = bindBootstrapIntent(host, config);
|
|
13749
|
-
await host.installAgent(config);
|
|
13750
|
-
if (!host.exists(GIT_PUBLIC_KEY))
|
|
13751
|
-
throw new Error("Agent installation did not produce its public Git deploy key");
|
|
13752
|
-
const gitPublicKey = host.read(GIT_PUBLIC_KEY).trim();
|
|
13753
|
-
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(gitPublicKey)) {
|
|
13754
|
-
throw new Error("Agent public Git deploy key is malformed");
|
|
13755
|
-
}
|
|
13756
|
-
return {
|
|
13757
|
-
kind: "platform",
|
|
13758
|
-
prepared: true,
|
|
13759
|
-
identityDigest,
|
|
13760
|
-
gitPublicKey,
|
|
13761
|
-
next: "register this read-only deploy key, then run --apply concurrently on all three genesis Agency members"
|
|
13762
|
-
};
|
|
13763
|
-
}
|
|
13764
13935
|
function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
13765
13936
|
return `${JSON.stringify({
|
|
13766
13937
|
format: 2,
|
|
@@ -13914,6 +14085,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
13914
14085
|
let installed;
|
|
13915
14086
|
if (host.exists(STATE_PATH))
|
|
13916
14087
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
14088
|
+
const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
13917
14089
|
let cloudflare;
|
|
13918
14090
|
if (config.cloudflareHandoff) {
|
|
13919
14091
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -13971,14 +14143,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
13971
14143
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
13972
14144
|
if (!secrets)
|
|
13973
14145
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
13974
|
-
const
|
|
14146
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets);
|
|
13975
14147
|
return {
|
|
13976
|
-
root:
|
|
13977
|
-
email:
|
|
13978
|
-
enrolmentToken:
|
|
13979
|
-
backup:
|
|
13980
|
-
cloudflareTunnelToken:
|
|
13981
|
-
cloudflareApiToken:
|
|
14148
|
+
root: checked4.clusterBootstrapCode,
|
|
14149
|
+
email: checked4.emailSecret,
|
|
14150
|
+
enrolmentToken: checked4.enrolmentToken,
|
|
14151
|
+
backup: checked4.backupS3Secret,
|
|
14152
|
+
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
14153
|
+
cloudflareApiToken: checked4.cloudflareApiToken
|
|
13982
14154
|
};
|
|
13983
14155
|
})() : undefined;
|
|
13984
14156
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -14017,24 +14189,35 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14017
14189
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14018
14190
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
14019
14191
|
}
|
|
14020
|
-
|
|
14192
|
+
if (alreadyEnrolled) {
|
|
14193
|
+
await host.installAgent(config, "bound");
|
|
14194
|
+
} else if (config.kind === "platform") {
|
|
14195
|
+
if (bootstrapManifest)
|
|
14196
|
+
await host.installAgent(config, "bootstrap");
|
|
14197
|
+
} else {
|
|
14198
|
+
await host.installAgent(config, "enrol");
|
|
14199
|
+
await host.installAgent(config, "bound");
|
|
14200
|
+
}
|
|
14021
14201
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14022
14202
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
|
|
14023
14203
|
} else
|
|
14024
14204
|
await host.ensureSoftware(plan.software);
|
|
14025
14205
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14026
|
-
await
|
|
14027
|
-
await
|
|
14028
|
-
await
|
|
14206
|
+
await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
|
|
14207
|
+
await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
|
|
14208
|
+
await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
|
|
14029
14209
|
for (const cidr of config.firewall.privateCidrs) {
|
|
14030
14210
|
if (config.database.role !== "none")
|
|
14031
|
-
await
|
|
14211
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
|
|
14032
14212
|
for (const port of [config.runtime.bluePort, config.runtime.greenPort])
|
|
14033
|
-
await
|
|
14213
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
|
|
14034
14214
|
}
|
|
14035
|
-
await
|
|
14215
|
+
await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
|
|
14036
14216
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 !== "ufw"));
|
|
14037
14217
|
}
|
|
14218
|
+
if (config.kind === "platform" && bootstrapManifest) {
|
|
14219
|
+
await verifyBootstrapBundleOnHost(host, config, true);
|
|
14220
|
+
}
|
|
14038
14221
|
if (config.kind === "platform") {
|
|
14039
14222
|
const root = platformPrivate.root;
|
|
14040
14223
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
@@ -14089,8 +14272,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14089
14272
|
keepReleases: runtime.keepReleases,
|
|
14090
14273
|
drainDeadlineMs: runtime.environment.drainDeadlineMs
|
|
14091
14274
|
});
|
|
14092
|
-
await
|
|
14093
|
-
await
|
|
14275
|
+
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
14276
|
+
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
14094
14277
|
});
|
|
14095
14278
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
14096
14279
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
@@ -14099,7 +14282,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14099
14282
|
host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
|
|
14100
14283
|
`, 420);
|
|
14101
14284
|
}
|
|
14102
|
-
await
|
|
14285
|
+
await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
|
|
14103
14286
|
host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
|
|
14104
14287
|
host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
|
|
14105
14288
|
host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
|
|
@@ -14108,30 +14291,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14108
14291
|
host.write("/etc/forgezero/deploy.env", activation.environment, 420);
|
|
14109
14292
|
host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
|
|
14110
14293
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
14111
|
-
await
|
|
14294
|
+
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
14112
14295
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
14113
|
-
await
|
|
14114
|
-
const otlpStatus = (await
|
|
14296
|
+
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
14297
|
+
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
14115
14298
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
14116
14299
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
14117
|
-
await
|
|
14118
|
-
await
|
|
14119
|
-
await
|
|
14300
|
+
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
14301
|
+
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
14302
|
+
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
14120
14303
|
if (config.database.role === "master") {
|
|
14121
14304
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
14122
14305
|
if (!host.exists(invite)) {
|
|
14123
|
-
host.write(invite, `plt_${
|
|
14306
|
+
host.write(invite, `plt_${randomBytes7(24).toString("hex")}
|
|
14124
14307
|
`, 384);
|
|
14125
|
-
await
|
|
14308
|
+
await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
14126
14309
|
}
|
|
14127
14310
|
}
|
|
14128
14311
|
if (config.database.role !== "none") {
|
|
14129
14312
|
host.mkdir("/var/lib/forgezero-cluster", 448);
|
|
14130
|
-
await
|
|
14313
|
+
await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
|
|
14131
14314
|
host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
|
|
14132
14315
|
host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
|
|
14133
|
-
await
|
|
14134
|
-
await
|
|
14316
|
+
await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
|
|
14317
|
+
await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
|
|
14135
14318
|
const evidence = {
|
|
14136
14319
|
expectedMode: "default",
|
|
14137
14320
|
role: "COORDINATOR",
|
|
@@ -14142,15 +14325,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14142
14325
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
14143
14326
|
`, 384);
|
|
14144
14327
|
}
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14328
|
+
if (bootstrapManifest) {
|
|
14329
|
+
await checked3(host, [
|
|
14330
|
+
"runuser",
|
|
14331
|
+
"-u",
|
|
14332
|
+
"forgezero-agent",
|
|
14333
|
+
"--",
|
|
14334
|
+
"/usr/local/bin/fz-agent",
|
|
14335
|
+
"deploy",
|
|
14336
|
+
`--revision=${bootstrapManifest.revision}`,
|
|
14337
|
+
...config.database.role === "master" ? ["--release-executor"] : []
|
|
14338
|
+
], "initial Agent deployment");
|
|
14339
|
+
host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
|
|
14340
|
+
format: 1,
|
|
14341
|
+
kind: "forgezero-bootstrap-release",
|
|
14342
|
+
revision: bootstrapManifest.revision,
|
|
14343
|
+
sha256: bootstrapManifest.sha256,
|
|
14344
|
+
branch: bootstrapManifest.branch,
|
|
14345
|
+
deployedAt: new Date().toISOString()
|
|
14346
|
+
}, null, 2)}
|
|
14347
|
+
`, 384);
|
|
14348
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
14349
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
14350
|
+
}
|
|
14351
|
+
if (!alreadyEnrolled) {
|
|
14352
|
+
await checked3(host, [
|
|
14353
|
+
"curl",
|
|
14354
|
+
"--fail",
|
|
14355
|
+
"--silent",
|
|
14356
|
+
"--show-error",
|
|
14357
|
+
"--max-time",
|
|
14358
|
+
"10",
|
|
14359
|
+
`http://127.0.0.1:3000${runtime.healthPath}`
|
|
14360
|
+
], "release-one API health");
|
|
14361
|
+
await host.installAgent(config, "enrol");
|
|
14362
|
+
await host.installAgent(config, "bound");
|
|
14363
|
+
}
|
|
14154
14364
|
}
|
|
14155
14365
|
if (config.cloudflareHandoff) {
|
|
14156
14366
|
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
@@ -14159,8 +14369,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14159
14369
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
14160
14370
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
14161
14371
|
host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
|
|
14162
|
-
await
|
|
14163
|
-
await
|
|
14372
|
+
await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
|
|
14373
|
+
await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
|
|
14164
14374
|
const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
|
|
14165
14375
|
if (!tunnelId)
|
|
14166
14376
|
throw new Error("Cloudflare tunnel identity is missing after handoff validation");
|
|
@@ -14170,8 +14380,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14170
14380
|
if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
|
|
14171
14381
|
throw new Error("sealed Cloudflare Mesh connector credential is missing");
|
|
14172
14382
|
host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
|
|
14173
|
-
await
|
|
14174
|
-
await
|
|
14383
|
+
await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
|
|
14384
|
+
await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
|
|
14175
14385
|
}
|
|
14176
14386
|
host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
|
|
14177
14387
|
const status = await bootstrapStatus(host);
|
|
@@ -14216,8 +14426,7 @@ function strictBootstrapDocument(value) {
|
|
|
14216
14426
|
"computeReference",
|
|
14217
14427
|
"nodeHostname",
|
|
14218
14428
|
"apiUrl",
|
|
14219
|
-
"
|
|
14220
|
-
"branch",
|
|
14429
|
+
"bootstrapBundle",
|
|
14221
14430
|
"deployRoot",
|
|
14222
14431
|
"telemetryEndpoint",
|
|
14223
14432
|
"database",
|
|
@@ -14230,9 +14439,11 @@ function strictBootstrapDocument(value) {
|
|
|
14230
14439
|
"realm",
|
|
14231
14440
|
"software",
|
|
14232
14441
|
"deploymentCredentials",
|
|
14442
|
+
"gitDeployKey",
|
|
14233
14443
|
"bootstrapRunner"
|
|
14234
14444
|
], "bootstrap config");
|
|
14235
14445
|
if (root.kind === "platform") {
|
|
14446
|
+
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
14236
14447
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14237
14448
|
if (root.cloudflareHandoff !== undefined)
|
|
14238
14449
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -14274,8 +14485,6 @@ function strictBootstrapDocument(value) {
|
|
|
14274
14485
|
"agentOtlpEndpoint",
|
|
14275
14486
|
"custodianEmail",
|
|
14276
14487
|
"email",
|
|
14277
|
-
"repository",
|
|
14278
|
-
"branch",
|
|
14279
14488
|
"deployProfile",
|
|
14280
14489
|
"otlpFlushIntervalMs",
|
|
14281
14490
|
"otlpTraceSampleRatio",
|
|
@@ -14313,11 +14522,70 @@ function strictBootstrapDocument(value) {
|
|
|
14313
14522
|
return value;
|
|
14314
14523
|
}
|
|
14315
14524
|
function readBootstrapConfig(path) {
|
|
14316
|
-
const metadata =
|
|
14525
|
+
const metadata = lstatSync5(path);
|
|
14317
14526
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
14318
14527
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
14319
14528
|
}
|
|
14320
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
14529
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync9(path, "utf8"))));
|
|
14530
|
+
}
|
|
14531
|
+
function planBootstrapAgentInstall(config, phase, context) {
|
|
14532
|
+
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
14533
|
+
if (phase === "bootstrap") {
|
|
14534
|
+
if (config.kind !== "platform" || hasBinding || !initialBundle) {
|
|
14535
|
+
throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
|
|
14536
|
+
}
|
|
14537
|
+
} else if (phase === "enrol") {
|
|
14538
|
+
if (hasBinding || !hasEnrolCredential) {
|
|
14539
|
+
throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
|
|
14540
|
+
}
|
|
14541
|
+
} else if (!hasBinding) {
|
|
14542
|
+
throw new Error("bound Agent convergence requires durable enrolment state");
|
|
14543
|
+
}
|
|
14544
|
+
const bound = phase === "bound";
|
|
14545
|
+
const enrolling = phase === "enrol";
|
|
14546
|
+
const authenticated = enrolling || bound;
|
|
14547
|
+
const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
|
|
14548
|
+
const options = {
|
|
14549
|
+
capabilities,
|
|
14550
|
+
socketPath: DEFAULT_SOCKET,
|
|
14551
|
+
seedPath: "/var/lib/forgezero/node.seed",
|
|
14552
|
+
controlSocketPath: CONTROL_SOCKET,
|
|
14553
|
+
repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
|
|
14554
|
+
branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
|
|
14555
|
+
bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
|
|
14556
|
+
bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
|
|
14557
|
+
profile: config.profile,
|
|
14558
|
+
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
14559
|
+
deploymentCredentials: config.deploymentCredentials,
|
|
14560
|
+
publicApiUrl: config.apiUrl,
|
|
14561
|
+
gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
14562
|
+
gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
14563
|
+
generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
14564
|
+
pullDeployments: bound,
|
|
14565
|
+
pullMigrations: bound && config.kind === "platform",
|
|
14566
|
+
pullBootstrap: bound && bootstrapRunner,
|
|
14567
|
+
bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
14568
|
+
bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
14569
|
+
bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
14570
|
+
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
14571
|
+
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
14572
|
+
enforceEgress: true,
|
|
14573
|
+
nodeHostname: config.nodeHostname,
|
|
14574
|
+
telemetryEndpoint: config.telemetryEndpoint,
|
|
14575
|
+
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14576
|
+
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14577
|
+
...enrolling ? {
|
|
14578
|
+
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14579
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
14580
|
+
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
14581
|
+
...authenticated ? {
|
|
14582
|
+
apiUrl: config.apiUrl,
|
|
14583
|
+
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14584
|
+
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
14585
|
+
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
14586
|
+
} : {}
|
|
14587
|
+
};
|
|
14588
|
+
return planInstall(options);
|
|
14321
14589
|
}
|
|
14322
14590
|
function localBootstrapHost() {
|
|
14323
14591
|
const execute = async (argv2, options = {}) => {
|
|
@@ -14335,19 +14603,19 @@ function localBootstrapHost() {
|
|
|
14335
14603
|
};
|
|
14336
14604
|
return {
|
|
14337
14605
|
uid: () => process.getuid?.() ?? -1,
|
|
14338
|
-
exists:
|
|
14339
|
-
read: (path) =>
|
|
14606
|
+
exists: existsSync9,
|
|
14607
|
+
read: (path) => readFileSync9(path, "utf8"),
|
|
14340
14608
|
write(path, content, mode) {
|
|
14341
|
-
|
|
14609
|
+
mkdirSync9(dirname9(path), { recursive: true, mode: 493 });
|
|
14342
14610
|
const temporary = `${path}.next.${process.pid}`;
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14611
|
+
writeFileSync9(temporary, content, { mode });
|
|
14612
|
+
chmodSync4(temporary, mode);
|
|
14613
|
+
renameSync8(temporary, path);
|
|
14346
14614
|
},
|
|
14347
|
-
mkdir: (path, mode) =>
|
|
14348
|
-
remove: (path) =>
|
|
14615
|
+
mkdir: (path, mode) => mkdirSync9(path, { recursive: true, mode }),
|
|
14616
|
+
remove: (path) => rmSync6(path, { force: true }),
|
|
14349
14617
|
inspect(path) {
|
|
14350
|
-
const value =
|
|
14618
|
+
const value = lstatSync5(path);
|
|
14351
14619
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
14352
14620
|
},
|
|
14353
14621
|
exec: execute,
|
|
@@ -14366,10 +14634,15 @@ function localBootstrapHost() {
|
|
|
14366
14634
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
14367
14635
|
return result;
|
|
14368
14636
|
},
|
|
14369
|
-
async installAgent(config) {
|
|
14637
|
+
async installAgent(config, phase) {
|
|
14370
14638
|
const capabilities = await readCapabilities(localRunner);
|
|
14371
|
-
const
|
|
14372
|
-
const
|
|
14639
|
+
const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
|
|
14640
|
+
const hasEnrolCredential = existsSync9(ENROL_CREDENTIAL);
|
|
14641
|
+
const initialBundle = config.kind === "platform" && !existsSync9(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
14642
|
+
path: config.bootstrapBundle.bundleFile,
|
|
14643
|
+
manifestPath: config.bootstrapBundle.manifestFile,
|
|
14644
|
+
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
|
|
14645
|
+
} : undefined;
|
|
14373
14646
|
if (config.kind === "platform") {
|
|
14374
14647
|
const lifecycle = config.database.role === "none" ? {
|
|
14375
14648
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -14381,49 +14654,19 @@ function localBootstrapHost() {
|
|
|
14381
14654
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
14382
14655
|
databasePorts: [8529]
|
|
14383
14656
|
};
|
|
14384
|
-
|
|
14385
|
-
|
|
14657
|
+
mkdirSync9(dirname9(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
14658
|
+
writeFileSync9(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
14386
14659
|
`, { mode: 256 });
|
|
14387
14660
|
}
|
|
14388
|
-
const plan =
|
|
14661
|
+
const plan = planBootstrapAgentInstall(config, phase, {
|
|
14389
14662
|
capabilities,
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
repository: config.repository,
|
|
14394
|
-
branch: config.branch,
|
|
14395
|
-
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
14396
|
-
deployRoot,
|
|
14397
|
-
deploymentCredentials: config.deploymentCredentials,
|
|
14398
|
-
publicApiUrl: config.apiUrl,
|
|
14399
|
-
gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
|
|
14400
|
-
gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
|
|
14401
|
-
generateGitIdentity: true,
|
|
14402
|
-
pullDeployments: hasBinding,
|
|
14403
|
-
pullMigrations: config.kind === "platform" && hasBinding,
|
|
14404
|
-
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
14405
|
-
bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
14406
|
-
bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
14407
|
-
bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
14408
|
-
bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
|
|
14409
|
-
lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
14410
|
-
enforceEgress: true,
|
|
14411
|
-
nodeHostname: config.nodeHostname,
|
|
14412
|
-
telemetryEndpoint: config.telemetryEndpoint,
|
|
14413
|
-
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14414
|
-
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14415
|
-
...hasBinding ? {
|
|
14416
|
-
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14417
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
14418
|
-
apiUrl: config.apiUrl,
|
|
14419
|
-
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14420
|
-
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
14421
|
-
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
14422
|
-
} : {}
|
|
14663
|
+
hasBinding,
|
|
14664
|
+
hasEnrolCredential,
|
|
14665
|
+
initialBundle
|
|
14423
14666
|
});
|
|
14424
14667
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
14425
|
-
|
|
14426
|
-
|
|
14668
|
+
mkdirSync9(dirname9(unit.path), { recursive: true, mode: 493 });
|
|
14669
|
+
writeFileSync9(unit.path, unit.unit, { mode: 420 });
|
|
14427
14670
|
}
|
|
14428
14671
|
await applyPlan(plan, localRunner);
|
|
14429
14672
|
return plan;
|
|
@@ -14432,8 +14675,8 @@ function localBootstrapHost() {
|
|
|
14432
14675
|
}
|
|
14433
14676
|
|
|
14434
14677
|
// src/cli/cloudflare-bootstrap.ts
|
|
14435
|
-
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as
|
|
14436
|
-
import { dirname as
|
|
14678
|
+
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync10 } from "fs";
|
|
14679
|
+
import { dirname as dirname10, resolve as resolve7 } from "path";
|
|
14437
14680
|
async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
|
|
14438
14681
|
return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
|
|
14439
14682
|
zoneName: input.zoneName,
|
|
@@ -14464,7 +14707,7 @@ function createCloudflareBootstrapCommandConfig(input) {
|
|
|
14464
14707
|
};
|
|
14465
14708
|
}
|
|
14466
14709
|
function readOwnerConfig(path) {
|
|
14467
|
-
const absolute2 =
|
|
14710
|
+
const absolute2 = resolve7(path);
|
|
14468
14711
|
let descriptor;
|
|
14469
14712
|
try {
|
|
14470
14713
|
descriptor = openSync(absolute2, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
@@ -14473,7 +14716,7 @@ function readOwnerConfig(path) {
|
|
|
14473
14716
|
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
|
|
14474
14717
|
throw new Error(`${absolute2} must be one operator-owned 0600 regular file`);
|
|
14475
14718
|
}
|
|
14476
|
-
return JSON.parse(
|
|
14719
|
+
return JSON.parse(readFileSync10(descriptor, "utf8"));
|
|
14477
14720
|
} catch (cause) {
|
|
14478
14721
|
if (cause instanceof SyntaxError)
|
|
14479
14722
|
throw new Error(`${absolute2} is not valid Cloudflare bootstrap JSON`);
|
|
@@ -14487,7 +14730,7 @@ function readOwnerConfig(path) {
|
|
|
14487
14730
|
}
|
|
14488
14731
|
function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
14489
14732
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
|
|
14490
|
-
const baseDirectory =
|
|
14733
|
+
const baseDirectory = dirname10(resolve7(path));
|
|
14491
14734
|
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"], "Cloudflare bootstrap config");
|
|
14492
14735
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
|
|
14493
14736
|
throw new Error("Cloudflare bootstrap config format/kind is invalid");
|
|
@@ -14527,7 +14770,7 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
14527
14770
|
return {
|
|
14528
14771
|
mode,
|
|
14529
14772
|
coordinates,
|
|
14530
|
-
checkpointPath:
|
|
14773
|
+
checkpointPath: resolve7(baseDirectory, input.checkpointPath)
|
|
14531
14774
|
};
|
|
14532
14775
|
}
|
|
14533
14776
|
async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
|
|
@@ -14540,14 +14783,14 @@ async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependen
|
|
|
14540
14783
|
return evidence;
|
|
14541
14784
|
}
|
|
14542
14785
|
async function runCloudflareBootstrapVerificationCommand(checkpointPath, dependencies = {}) {
|
|
14543
|
-
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(
|
|
14786
|
+
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(resolve7(checkpointPath));
|
|
14544
14787
|
(dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
|
|
14545
14788
|
`);
|
|
14546
14789
|
return evidence;
|
|
14547
14790
|
}
|
|
14548
14791
|
function readCloudflareBootstrapFinalizeConfig(path) {
|
|
14549
14792
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap finalize config");
|
|
14550
|
-
const baseDirectory =
|
|
14793
|
+
const baseDirectory = dirname10(resolve7(path));
|
|
14551
14794
|
exactKeys3(input, ["format", "kind", "checkpointPath", "acceptancePath"], "Cloudflare bootstrap finalize config");
|
|
14552
14795
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-finalize") {
|
|
14553
14796
|
throw new Error("Cloudflare bootstrap finalize config format/kind is invalid");
|
|
@@ -14558,8 +14801,8 @@ function readCloudflareBootstrapFinalizeConfig(path) {
|
|
|
14558
14801
|
}
|
|
14559
14802
|
}
|
|
14560
14803
|
return {
|
|
14561
|
-
checkpointPath:
|
|
14562
|
-
acceptancePath:
|
|
14804
|
+
checkpointPath: resolve7(baseDirectory, input.checkpointPath),
|
|
14805
|
+
acceptancePath: resolve7(baseDirectory, input.acceptancePath)
|
|
14563
14806
|
};
|
|
14564
14807
|
}
|
|
14565
14808
|
async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies = {}) {
|
|
@@ -14571,31 +14814,31 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
14571
14814
|
}
|
|
14572
14815
|
|
|
14573
14816
|
// src/metal-bootstrap.ts
|
|
14574
|
-
import { createHash as
|
|
14817
|
+
import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
|
|
14575
14818
|
import {
|
|
14576
|
-
chmodSync as
|
|
14819
|
+
chmodSync as chmodSync5,
|
|
14577
14820
|
chownSync,
|
|
14578
14821
|
copyFileSync as copyFileSync2,
|
|
14579
|
-
existsSync as
|
|
14580
|
-
lstatSync as
|
|
14581
|
-
mkdirSync as
|
|
14582
|
-
readFileSync as
|
|
14822
|
+
existsSync as existsSync10,
|
|
14823
|
+
lstatSync as lstatSync6,
|
|
14824
|
+
mkdirSync as mkdirSync11,
|
|
14825
|
+
readFileSync as readFileSync11,
|
|
14583
14826
|
realpathSync as realpathSync4,
|
|
14584
|
-
renameSync as
|
|
14827
|
+
renameSync as renameSync9,
|
|
14585
14828
|
statSync,
|
|
14586
14829
|
symlinkSync as symlinkSync2,
|
|
14587
14830
|
unlinkSync as unlinkSync2,
|
|
14588
|
-
writeFileSync as
|
|
14831
|
+
writeFileSync as writeFileSync11
|
|
14589
14832
|
} from "fs";
|
|
14590
|
-
import { dirname as
|
|
14833
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, join as join9, resolve as resolve8 } from "path";
|
|
14591
14834
|
import { isIP as isIP5 } from "net";
|
|
14592
14835
|
|
|
14593
14836
|
// src/metal-isolation.ts
|
|
14594
|
-
import { mkdirSync as
|
|
14837
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
14595
14838
|
import { join as join8 } from "path";
|
|
14596
14839
|
|
|
14597
14840
|
// src/metal-provision.ts
|
|
14598
|
-
import { dirname as
|
|
14841
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join7 } from "path";
|
|
14599
14842
|
import { isIP as isIP4 } from "net";
|
|
14600
14843
|
|
|
14601
14844
|
// src/ubuntu.ts
|
|
@@ -14644,7 +14887,7 @@ function validateMetalProfile(profile) {
|
|
|
14644
14887
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
14645
14888
|
throw new MetalProvisionError("invalid guest address range");
|
|
14646
14889
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
14647
|
-
if (!
|
|
14890
|
+
if (!isAbsolute3(path))
|
|
14648
14891
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
14649
14892
|
}
|
|
14650
14893
|
new URL(profile.apiUrl);
|
|
@@ -14752,14 +14995,14 @@ var defaultExec = async (argv2) => {
|
|
|
14752
14995
|
]);
|
|
14753
14996
|
return { exitCode, stdout, stderr };
|
|
14754
14997
|
};
|
|
14755
|
-
var
|
|
14998
|
+
var checked4 = async (exec, argv2) => {
|
|
14756
14999
|
const result = await exec(argv2);
|
|
14757
15000
|
if (result.exitCode !== 0)
|
|
14758
15001
|
throw new Error(`${argv2[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
14759
15002
|
return result;
|
|
14760
15003
|
};
|
|
14761
15004
|
var requireGuestsInSlice = async (exec) => {
|
|
14762
|
-
const active = await
|
|
15005
|
+
const active = await checked4(exec, [
|
|
14763
15006
|
"systemctl",
|
|
14764
15007
|
"list-units",
|
|
14765
15008
|
"--type=service",
|
|
@@ -14773,7 +15016,7 @@ var requireGuestsInSlice = async (exec) => {
|
|
|
14773
15016
|
const service = line.trim().split(/\s+/)[0];
|
|
14774
15017
|
if (!service)
|
|
14775
15018
|
continue;
|
|
14776
|
-
const cgroup = await
|
|
15019
|
+
const cgroup = await checked4(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
|
|
14777
15020
|
if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
|
|
14778
15021
|
throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
|
|
14779
15022
|
}
|
|
@@ -14783,23 +15026,23 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
14783
15026
|
validateMetalProfile(profile);
|
|
14784
15027
|
await requireGuestsInSlice(exec);
|
|
14785
15028
|
const unitDir = profile.unitDir;
|
|
14786
|
-
|
|
14787
|
-
|
|
15029
|
+
mkdirSync10(unitDir, { recursive: true });
|
|
15030
|
+
writeFileSync10(join8(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
14788
15031
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
14789
15032
|
const directory = join8(unitDir, `${unit}.d`);
|
|
14790
|
-
|
|
14791
|
-
|
|
15033
|
+
mkdirSync10(directory, { recursive: true });
|
|
15034
|
+
writeFileSync10(join8(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
14792
15035
|
}
|
|
14793
15036
|
const initDirectory = join8(unitDir, "init.scope.d");
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
await
|
|
15037
|
+
mkdirSync10(initDirectory, { recursive: true });
|
|
15038
|
+
writeFileSync10(join8(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
15039
|
+
await checked4(exec, ["systemctl", "daemon-reload"]);
|
|
14797
15040
|
await requireGuestsInSlice(exec);
|
|
14798
15041
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
14799
15042
|
if (profile.housekeepingMemoryNodes)
|
|
14800
15043
|
properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
|
|
14801
15044
|
for (const unit of ["system.slice", "user.slice", "init.scope"]) {
|
|
14802
|
-
await
|
|
15045
|
+
await checked4(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
|
|
14803
15046
|
}
|
|
14804
15047
|
}
|
|
14805
15048
|
|
|
@@ -14911,7 +15154,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
14911
15154
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
14912
15155
|
}
|
|
14913
15156
|
const image = config.profile.images[Object.keys(config.profile.images)[0]];
|
|
14914
|
-
if (!image.path.startsWith("/var/lib/forgezero/images/") ||
|
|
15157
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve8(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
14915
15158
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
14916
15159
|
}
|
|
14917
15160
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
|
|
@@ -14946,10 +15189,10 @@ function validateMetalBootstrapConfig(config) {
|
|
|
14946
15189
|
return config;
|
|
14947
15190
|
}
|
|
14948
15191
|
function validateOwnerOnlyPath(path, requireRootOwner) {
|
|
14949
|
-
if (!
|
|
15192
|
+
if (!isAbsolute4(path) || resolve8(path) !== path || path.includes("/../")) {
|
|
14950
15193
|
throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
|
|
14951
15194
|
}
|
|
14952
|
-
const metadata =
|
|
15195
|
+
const metadata = lstatSync6(path);
|
|
14953
15196
|
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
|
|
14954
15197
|
throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
|
|
14955
15198
|
}
|
|
@@ -14967,7 +15210,7 @@ function readMetalBootstrapConfig(path) {
|
|
|
14967
15210
|
throw new MetalBootstrapError("metal bootstrap config size is invalid");
|
|
14968
15211
|
let parsed;
|
|
14969
15212
|
try {
|
|
14970
|
-
parsed = JSON.parse(
|
|
15213
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
14971
15214
|
} catch {
|
|
14972
15215
|
throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
|
|
14973
15216
|
}
|
|
@@ -15000,15 +15243,15 @@ function planMetalBootstrap(config) {
|
|
|
15000
15243
|
};
|
|
15001
15244
|
}
|
|
15002
15245
|
var atomicWrite2 = (path, body, mode) => {
|
|
15003
|
-
|
|
15246
|
+
mkdirSync11(dirname12(path), { recursive: true, mode: 493 });
|
|
15004
15247
|
const temporary = `${path}.next-${process.pid}`;
|
|
15005
|
-
|
|
15006
|
-
|
|
15248
|
+
writeFileSync11(temporary, body, { mode, flag: "wx" });
|
|
15249
|
+
chmodSync5(temporary, mode);
|
|
15007
15250
|
chownSync(temporary, 0, 0);
|
|
15008
|
-
|
|
15251
|
+
renameSync9(temporary, path);
|
|
15009
15252
|
};
|
|
15010
15253
|
var validateAgentSourcePath = (source) => {
|
|
15011
|
-
if (!
|
|
15254
|
+
if (!isAbsolute4(source) || !lstatSync6(source).isFile() || lstatSync6(source).isSymbolicLink()) {
|
|
15012
15255
|
throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
|
|
15013
15256
|
}
|
|
15014
15257
|
};
|
|
@@ -15172,11 +15415,11 @@ WantedBy=multi-user.target
|
|
|
15172
15415
|
var installAgentBinary = (source, version) => {
|
|
15173
15416
|
validateAgentSourcePath(source);
|
|
15174
15417
|
const release = `/opt/forgezero/agent/versions/${version}/dist`;
|
|
15175
|
-
|
|
15418
|
+
mkdirSync11(release, { recursive: true, mode: 493 });
|
|
15176
15419
|
copyFileSync2(source, join9(release, "fz-agent.js"));
|
|
15177
|
-
|
|
15420
|
+
chmodSync5(join9(release, "fz-agent.js"), 493);
|
|
15178
15421
|
chownSync(join9(release, "fz-agent.js"), 0, 0);
|
|
15179
|
-
|
|
15422
|
+
mkdirSync11("/opt/forgezero/agent", { recursive: true, mode: 493 });
|
|
15180
15423
|
for (const [link, target] of [
|
|
15181
15424
|
["/opt/forgezero/agent/current.next", `versions/${version}`],
|
|
15182
15425
|
[AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
|
|
@@ -15186,7 +15429,7 @@ var installAgentBinary = (source, version) => {
|
|
|
15186
15429
|
} catch {}
|
|
15187
15430
|
symlinkSync2(target, link);
|
|
15188
15431
|
if (link.endsWith("current.next"))
|
|
15189
|
-
|
|
15432
|
+
renameSync9(link, "/opt/forgezero/agent/current");
|
|
15190
15433
|
}
|
|
15191
15434
|
};
|
|
15192
15435
|
var preflight = async (config, exec) => {
|
|
@@ -15208,10 +15451,10 @@ var preflight = async (config, exec) => {
|
|
|
15208
15451
|
]);
|
|
15209
15452
|
await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
|
|
15210
15453
|
await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
|
|
15211
|
-
if (!
|
|
15454
|
+
if (!existsSync10("/dev/kvm"))
|
|
15212
15455
|
throw new MetalBootstrapError("/dev/kvm is required");
|
|
15213
15456
|
if (config.profile.confidential) {
|
|
15214
|
-
if (!
|
|
15457
|
+
if (!existsSync10("/dev/sev"))
|
|
15215
15458
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
15216
15459
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
15217
15460
|
}
|
|
@@ -15223,16 +15466,16 @@ var assertSupportedMetalHost = () => {
|
|
|
15223
15466
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
15224
15467
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
15225
15468
|
}
|
|
15226
|
-
const release =
|
|
15469
|
+
const release = readFileSync11("/etc/os-release", "utf8");
|
|
15227
15470
|
if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
|
|
15228
15471
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
15229
15472
|
}
|
|
15230
15473
|
};
|
|
15231
15474
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
15232
15475
|
const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
|
|
15233
|
-
if (
|
|
15476
|
+
if (existsSync10(image.path))
|
|
15234
15477
|
return;
|
|
15235
|
-
|
|
15478
|
+
mkdirSync11(dirname12(image.path), { recursive: true, mode: 493 });
|
|
15236
15479
|
const temporary = `${image.path}.next-${process.pid}`;
|
|
15237
15480
|
try {
|
|
15238
15481
|
await runChecked(exec, [
|
|
@@ -15249,9 +15492,9 @@ var ensurePinnedGuestImage = async (config, exec) => {
|
|
|
15249
15492
|
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
15250
15493
|
if (digest !== image.sha256)
|
|
15251
15494
|
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
15252
|
-
|
|
15495
|
+
chmodSync5(temporary, 292);
|
|
15253
15496
|
chownSync(temporary, 0, 0);
|
|
15254
|
-
|
|
15497
|
+
renameSync9(temporary, image.path);
|
|
15255
15498
|
} catch (cause) {
|
|
15256
15499
|
try {
|
|
15257
15500
|
unlinkSync2(temporary);
|
|
@@ -15264,11 +15507,11 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15264
15507
|
if ((options.getuid ?? process.getuid)?.() !== 0)
|
|
15265
15508
|
throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
|
|
15266
15509
|
assertSupportedMetalHost();
|
|
15267
|
-
if (
|
|
15510
|
+
if (existsSync10(STATE_PATH2) && !options.repair)
|
|
15268
15511
|
throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
|
|
15269
15512
|
if (config.agentSeedFile)
|
|
15270
15513
|
validateOwnerOnlyPath(config.agentSeedFile, true);
|
|
15271
|
-
if (config.agentSeedFile &&
|
|
15514
|
+
if (config.agentSeedFile && existsSync10(SEED_CREDENTIAL_PATH)) {
|
|
15272
15515
|
throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
|
|
15273
15516
|
}
|
|
15274
15517
|
validateAgentSourcePath(options.agentSourcePath);
|
|
@@ -15295,9 +15538,9 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15295
15538
|
await preflight(config, exec);
|
|
15296
15539
|
await ensureAccount(exec);
|
|
15297
15540
|
installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15541
|
+
mkdirSync11("/etc/forgezero/creds", { recursive: true, mode: 448 });
|
|
15542
|
+
mkdirSync11(config.profile.stateDir, { recursive: true, mode: 448 });
|
|
15543
|
+
mkdirSync11(config.profile.seedDir, { recursive: true, mode: 448 });
|
|
15301
15544
|
const persistedProfile = {
|
|
15302
15545
|
...config.profile,
|
|
15303
15546
|
metalHostname: config.metalHostname,
|
|
@@ -15306,13 +15549,13 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15306
15549
|
};
|
|
15307
15550
|
atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
15308
15551
|
`, 384);
|
|
15309
|
-
if (!
|
|
15310
|
-
const seed = config.agentSeedFile ?
|
|
15552
|
+
if (!existsSync10(SEED_CREDENTIAL_PATH)) {
|
|
15553
|
+
const seed = config.agentSeedFile ? readFileSync11(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
|
|
15311
15554
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
15312
15555
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
15313
15556
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
15314
15557
|
`);
|
|
15315
|
-
|
|
15558
|
+
chmodSync5(SEED_CREDENTIAL_PATH, 256);
|
|
15316
15559
|
chownSync(SEED_CREDENTIAL_PATH, 0, 0);
|
|
15317
15560
|
if (config.agentSeedFile)
|
|
15318
15561
|
unlinkSync2(config.agentSeedFile);
|
|
@@ -15361,7 +15604,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15361
15604
|
initializedAt: new Date().toISOString(),
|
|
15362
15605
|
role: "metal",
|
|
15363
15606
|
metalHostname: config.metalHostname,
|
|
15364
|
-
profileSha256:
|
|
15607
|
+
profileSha256: createHash5("sha256").update(JSON.stringify(config.profile)).digest("hex")
|
|
15365
15608
|
};
|
|
15366
15609
|
atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
|
|
15367
15610
|
`, 384);
|
|
@@ -15369,7 +15612,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15369
15612
|
}
|
|
15370
15613
|
var socketReady = (path) => {
|
|
15371
15614
|
try {
|
|
15372
|
-
return
|
|
15615
|
+
return lstatSync6(path).isSocket();
|
|
15373
15616
|
} catch {
|
|
15374
15617
|
return false;
|
|
15375
15618
|
}
|
|
@@ -15378,17 +15621,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15378
15621
|
const problems = [];
|
|
15379
15622
|
let profileValid = false, imageVerified = false, metalHostname, profileSha256;
|
|
15380
15623
|
let profileMode = null;
|
|
15381
|
-
if (
|
|
15624
|
+
if (existsSync10(PROFILE_PATH)) {
|
|
15382
15625
|
try {
|
|
15383
15626
|
const metadata = statSync(PROFILE_PATH);
|
|
15384
15627
|
profileMode = metadata.mode & 511;
|
|
15385
15628
|
if (profileMode !== 384 || metadata.uid !== 0)
|
|
15386
15629
|
problems.push("metal profile is not root-owned mode 0600");
|
|
15387
|
-
const persisted = JSON.parse(
|
|
15630
|
+
const persisted = JSON.parse(readFileSync11(PROFILE_PATH, "utf8"));
|
|
15388
15631
|
const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
|
|
15389
15632
|
validateMetalProfile(profile);
|
|
15390
15633
|
profileValid = true;
|
|
15391
|
-
profileSha256 =
|
|
15634
|
+
profileSha256 = createHash5("sha256").update(JSON.stringify(profile)).digest("hex");
|
|
15392
15635
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
|
|
15393
15636
|
problems.push("persisted metal host coordinates are invalid");
|
|
15394
15637
|
}
|
|
@@ -15417,7 +15660,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15417
15660
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
15418
15661
|
}
|
|
15419
15662
|
const image = profile.images[Object.keys(profile.images)[0]];
|
|
15420
|
-
if (
|
|
15663
|
+
if (existsSync10(image.path)) {
|
|
15421
15664
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
15422
15665
|
imageVerified = digest === image.sha256;
|
|
15423
15666
|
}
|
|
@@ -15428,9 +15671,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15428
15671
|
}
|
|
15429
15672
|
} else
|
|
15430
15673
|
problems.push("metal profile is missing");
|
|
15431
|
-
if (
|
|
15674
|
+
if (existsSync10(STATE_PATH2)) {
|
|
15432
15675
|
try {
|
|
15433
|
-
const state = JSON.parse(
|
|
15676
|
+
const state = JSON.parse(readFileSync11(STATE_PATH2, "utf8"));
|
|
15434
15677
|
metalHostname = state.metalHostname;
|
|
15435
15678
|
if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
|
|
15436
15679
|
problems.push("metal initialized state does not bind the current profile");
|
|
@@ -15446,7 +15689,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15446
15689
|
"forgezero-metal-agent-egress.service",
|
|
15447
15690
|
"forgezero-metal-agent.service"
|
|
15448
15691
|
]) {
|
|
15449
|
-
if (!
|
|
15692
|
+
if (!existsSync10(join9(UNIT_DIRECTORY, unit)))
|
|
15450
15693
|
units[unit] = "missing";
|
|
15451
15694
|
else
|
|
15452
15695
|
units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
|
|
@@ -15460,7 +15703,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15460
15703
|
if (!updateSocketReady)
|
|
15461
15704
|
problems.push("Agent update helper socket is not ready");
|
|
15462
15705
|
return {
|
|
15463
|
-
initialized:
|
|
15706
|
+
initialized: existsSync10(STATE_PATH2),
|
|
15464
15707
|
profileValid,
|
|
15465
15708
|
profileMode,
|
|
15466
15709
|
imageVerified,
|
|
@@ -15473,11 +15716,11 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15473
15716
|
}
|
|
15474
15717
|
|
|
15475
15718
|
// src/operator-bootstrap.ts
|
|
15476
|
-
import { createHash as
|
|
15477
|
-
import { lstatSync as
|
|
15719
|
+
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15720
|
+
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15478
15721
|
import { isIP as isIP6 } from "net";
|
|
15479
15722
|
import { tmpdir } from "os";
|
|
15480
|
-
import { basename as basename2, join as join10 } from "path";
|
|
15723
|
+
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15481
15724
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15482
15725
|
|
|
15483
15726
|
// src/platform-genesis.ts
|
|
@@ -15612,25 +15855,44 @@ var exactKeys5 = (value, keys, label) => {
|
|
|
15612
15855
|
throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
15613
15856
|
return record3;
|
|
15614
15857
|
};
|
|
15858
|
+
var canonicalOutputPath = (path, label) => {
|
|
15859
|
+
if (!isAbsolute5(path) || resolve9(path) !== path || /[\r\n\0]/.test(path)) {
|
|
15860
|
+
throw new Error(`${label} must be a canonical absolute path`);
|
|
15861
|
+
}
|
|
15862
|
+
return path;
|
|
15863
|
+
};
|
|
15864
|
+
var writeOwnerJson2 = (path, value, label) => {
|
|
15865
|
+
canonicalOutputPath(path, label);
|
|
15866
|
+
mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
|
|
15867
|
+
const parent = lstatSync7(dirname13(path));
|
|
15868
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
15869
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
15870
|
+
throw new Error(`${label} parent must be an owner-only directory`);
|
|
15871
|
+
}
|
|
15872
|
+
writeFileSync12(path, `${JSON.stringify(value, null, 2)}
|
|
15873
|
+
`, { mode: 384, flag: "wx" });
|
|
15874
|
+
chmodSync6(path, 384);
|
|
15875
|
+
return path;
|
|
15876
|
+
};
|
|
15615
15877
|
var ownerFile = (path, limit, label) => {
|
|
15616
15878
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15617
15879
|
throw new Error(`${label} path must be absolute`);
|
|
15618
|
-
const metadata =
|
|
15880
|
+
const metadata = lstatSync7(path);
|
|
15619
15881
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15620
15882
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
|
|
15621
15883
|
throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
|
|
15622
15884
|
}
|
|
15623
|
-
return
|
|
15885
|
+
return readFileSync12(path);
|
|
15624
15886
|
};
|
|
15625
15887
|
var publicIdentity = (path) => {
|
|
15626
15888
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15627
15889
|
throw new Error("SSH public-key path must be absolute");
|
|
15628
|
-
const metadata =
|
|
15890
|
+
const metadata = lstatSync7(path);
|
|
15629
15891
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15630
15892
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
|
|
15631
15893
|
throw new Error("SSH public key must be a caller-owned regular file with one link");
|
|
15632
15894
|
}
|
|
15633
|
-
const value =
|
|
15895
|
+
const value = readFileSync12(path, "utf8").trim();
|
|
15634
15896
|
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
|
|
15635
15897
|
throw new Error("SSH public key is malformed");
|
|
15636
15898
|
}
|
|
@@ -15639,7 +15901,7 @@ var publicIdentity = (path) => {
|
|
|
15639
15901
|
var socketPath = (path) => {
|
|
15640
15902
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15641
15903
|
throw new Error("SSH agent socket path must be absolute");
|
|
15642
|
-
const metadata =
|
|
15904
|
+
const metadata = lstatSync7(path);
|
|
15643
15905
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15644
15906
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
|
|
15645
15907
|
throw new Error("SSH agent socket must be a caller-owned Unix socket");
|
|
@@ -15653,7 +15915,7 @@ var fingerprint = (key) => {
|
|
|
15653
15915
|
if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
15654
15916
|
throw new Error("SSH host key is malformed");
|
|
15655
15917
|
}
|
|
15656
|
-
return `SHA256:${
|
|
15918
|
+
return `SHA256:${createHash6("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
|
|
15657
15919
|
};
|
|
15658
15920
|
var validateHop = (value, label) => {
|
|
15659
15921
|
const hop = exactKeys5(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
|
|
@@ -15670,8 +15932,10 @@ var validateHop = (value, label) => {
|
|
|
15670
15932
|
}
|
|
15671
15933
|
return hop;
|
|
15672
15934
|
};
|
|
15673
|
-
function
|
|
15674
|
-
|
|
15935
|
+
function createOperatorSshHop(input) {
|
|
15936
|
+
return validateHop({ ...input, hostKeySha256: fingerprint(input.hostKey) }, "operator SSH hop");
|
|
15937
|
+
}
|
|
15938
|
+
var validatePlatformRequestValue = (value) => {
|
|
15675
15939
|
const root = exactKeys5(value, ["kind", "target", "platformConfigFile"], "operator bootstrap request");
|
|
15676
15940
|
if (root.kind !== "platform-remote")
|
|
15677
15941
|
throw new Error("operator bootstrap kind must be platform-remote");
|
|
@@ -15690,6 +15954,7 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15690
15954
|
socketPath(targetValue.agentSocket);
|
|
15691
15955
|
if (typeof root.platformConfigFile !== "string")
|
|
15692
15956
|
throw new Error("platformConfigFile must be an absolute path");
|
|
15957
|
+
canonicalOutputPath(root.platformConfigFile, "platformConfigFile");
|
|
15693
15958
|
const config = readBootstrapConfig(root.platformConfigFile);
|
|
15694
15959
|
if (config.kind !== "platform")
|
|
15695
15960
|
throw new Error("operator bootstrap requires a platform config");
|
|
@@ -15703,9 +15968,39 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15703
15968
|
...targetValue.jump === undefined ? {} : { jump: validateHop(targetValue.jump, "operator bootstrap jump") }
|
|
15704
15969
|
}
|
|
15705
15970
|
};
|
|
15706
|
-
}
|
|
15707
|
-
function
|
|
15708
|
-
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator
|
|
15971
|
+
};
|
|
15972
|
+
function readOperatorPlatformBootstrapRequest(path) {
|
|
15973
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
|
|
15974
|
+
return validatePlatformRequestValue(value);
|
|
15975
|
+
}
|
|
15976
|
+
function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
15977
|
+
validatePlatformRequestValue(request);
|
|
15978
|
+
return writeOwnerJson2(path, request, "operator bootstrap request");
|
|
15979
|
+
}
|
|
15980
|
+
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
15981
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
15982
|
+
const root = exactKeys5(value, ["kind", "requests"], "operator fleet request");
|
|
15983
|
+
if (root.kind !== "platform-fleet-remote")
|
|
15984
|
+
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
15985
|
+
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
15986
|
+
throw new Error("operator fleet requires exactly three request files");
|
|
15987
|
+
}
|
|
15988
|
+
const requests = root.requests.map((entry) => canonicalOutputPath(entry, "operator fleet request file"));
|
|
15989
|
+
if (new Set(requests).size !== 3)
|
|
15990
|
+
throw new Error("operator fleet request files must be unique");
|
|
15991
|
+
for (const request of requests)
|
|
15992
|
+
readOperatorPlatformBootstrapRequest(request);
|
|
15993
|
+
return { kind: "platform-fleet-remote", requests };
|
|
15994
|
+
}
|
|
15995
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
15996
|
+
const request = { kind: "platform-fleet-remote", requests: requestFiles };
|
|
15997
|
+
if (new Set(requestFiles).size !== 3)
|
|
15998
|
+
throw new Error("operator fleet request files must be unique");
|
|
15999
|
+
for (const file of requestFiles)
|
|
16000
|
+
readOperatorPlatformBootstrapRequest(file);
|
|
16001
|
+
return writeOwnerJson2(path, request, "operator fleet request");
|
|
16002
|
+
}
|
|
16003
|
+
var validateMetalRequestValue = (value) => {
|
|
15709
16004
|
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
15710
16005
|
if (root.kind !== "metal-remote")
|
|
15711
16006
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
@@ -15724,6 +16019,7 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15724
16019
|
socketPath(targetValue.agentSocket);
|
|
15725
16020
|
if (typeof root.metalConfigFile !== "string")
|
|
15726
16021
|
throw new Error("metalConfigFile must be an absolute path");
|
|
16022
|
+
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
15727
16023
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
15728
16024
|
const genesis = exactKeys5(root.genesis, ["environment", "sshPublicKeyFiles"], "operator metal genesis");
|
|
15729
16025
|
if (typeof genesis.environment !== "string" || !PLATFORM_GENESIS_ENVIRONMENTS.includes(genesis.environment)) {
|
|
@@ -15747,6 +16043,14 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15747
16043
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
15748
16044
|
}
|
|
15749
16045
|
};
|
|
16046
|
+
};
|
|
16047
|
+
function readOperatorMetalBootstrapRequest(path) {
|
|
16048
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
|
|
16049
|
+
return validateMetalRequestValue(value);
|
|
16050
|
+
}
|
|
16051
|
+
function writeOperatorMetalBootstrapRequest(path, request) {
|
|
16052
|
+
validateMetalRequestValue(request);
|
|
16053
|
+
return writeOwnerJson2(path, request, "operator metal request");
|
|
15750
16054
|
}
|
|
15751
16055
|
function planOperatorMetalBootstrap(request, mode) {
|
|
15752
16056
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
@@ -15798,13 +16102,95 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
15798
16102
|
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
15799
16103
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
15800
16104
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
16105
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
15801
16106
|
"stage the rewritten owner-only platform config and its named credential handoffs",
|
|
15802
|
-
|
|
16107
|
+
"run typed fz bootstrap platform --apply from the local bundle",
|
|
15803
16108
|
"remove transient local and remote staging data"
|
|
15804
16109
|
],
|
|
15805
16110
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
15806
16111
|
};
|
|
15807
16112
|
}
|
|
16113
|
+
var fleetConfigIdentity = (config) => {
|
|
16114
|
+
const normalized = structuredClone(config);
|
|
16115
|
+
delete normalized.computeReference;
|
|
16116
|
+
delete normalized.nodeHostname;
|
|
16117
|
+
delete normalized.cloudflareHandoff;
|
|
16118
|
+
delete normalized.database.role;
|
|
16119
|
+
delete normalized.database.address;
|
|
16120
|
+
delete normalized.database.master;
|
|
16121
|
+
const environment = normalized.runtime.environment;
|
|
16122
|
+
delete environment.databaseRole;
|
|
16123
|
+
delete environment.databaseAddress;
|
|
16124
|
+
delete environment.databaseMaster;
|
|
16125
|
+
delete environment.nodeHostname;
|
|
16126
|
+
delete environment.seedSyncPeers;
|
|
16127
|
+
delete environment.custodianEmail;
|
|
16128
|
+
return JSON.stringify(normalized);
|
|
16129
|
+
};
|
|
16130
|
+
function validatedPlatformFleet(fleet) {
|
|
16131
|
+
const requests = fleet.requests.map(readOperatorPlatformBootstrapRequest);
|
|
16132
|
+
const configs = requests.map((request) => {
|
|
16133
|
+
const config = readBootstrapConfig(request.platformConfigFile);
|
|
16134
|
+
if (config.kind !== "platform")
|
|
16135
|
+
throw new Error("operator fleet requires platform configs");
|
|
16136
|
+
return config;
|
|
16137
|
+
});
|
|
16138
|
+
const environment = configs[0].environment;
|
|
16139
|
+
const expected = PLATFORM_GENESIS_FLEETS[environment];
|
|
16140
|
+
if (!expected || expected.length !== 3)
|
|
16141
|
+
throw new Error("operator fleet environment is invalid");
|
|
16142
|
+
const identity = fleetConfigIdentity(configs[0]);
|
|
16143
|
+
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
16144
|
+
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
16145
|
+
const coordinators = expected.map((guest) => `http://${guest.address}:8529`);
|
|
16146
|
+
const nodeHostnames = new Set;
|
|
16147
|
+
for (let index = 0;index < 3; index += 1) {
|
|
16148
|
+
const config = configs[index];
|
|
16149
|
+
const request = requests[index];
|
|
16150
|
+
const guest = expected[index];
|
|
16151
|
+
const role = index === 0 ? "master" : "joiner";
|
|
16152
|
+
if (config.environment !== environment || fleetConfigIdentity(config) !== identity) {
|
|
16153
|
+
throw new Error("operator fleet platform configs do not share one immutable identity");
|
|
16154
|
+
}
|
|
16155
|
+
if (config.computeReference !== guest.name || config.database.address !== guest.address || config.database.role !== role || config.database.agency !== "member" || (role === "master" ? config.database.master !== undefined : config.database.master !== expected[0].address) || config.database.coordinators.join(",") !== coordinators.join(",") || config.enrolment.source !== "genesis-derived" || config.cloudflareHandoff?.nodeName !== guest.name) {
|
|
16156
|
+
throw new Error(`operator fleet ${guest.name} does not match the exact genesis topology`);
|
|
16157
|
+
}
|
|
16158
|
+
if (request.target.address !== guest.address || request.target.port !== 22 || request.target.user !== "forgezero") {
|
|
16159
|
+
throw new Error(`operator fleet ${guest.name} SSH target must be forgezero@${guest.address}:22`);
|
|
16160
|
+
}
|
|
16161
|
+
if (`${request.target.identityPublicKeyFile}\x00${request.target.agentSocket}` !== operatorIdentity) {
|
|
16162
|
+
throw new Error("operator fleet must use one caller-approved SSH identity and agent socket");
|
|
16163
|
+
}
|
|
16164
|
+
if (JSON.stringify(request.target.jump ?? null) !== jumpIdentity) {
|
|
16165
|
+
throw new Error("operator fleet must use one pinned Metal jump or no jump on every node");
|
|
16166
|
+
}
|
|
16167
|
+
if (nodeHostnames.has(config.nodeHostname))
|
|
16168
|
+
throw new Error("operator fleet node hostnames must be unique");
|
|
16169
|
+
nodeHostnames.add(config.nodeHostname);
|
|
16170
|
+
}
|
|
16171
|
+
return { requests, configs, environment };
|
|
16172
|
+
}
|
|
16173
|
+
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
16174
|
+
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
16175
|
+
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
16176
|
+
for (const config of configs.slice(1)) {
|
|
16177
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
16178
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
16179
|
+
}
|
|
16180
|
+
}
|
|
16181
|
+
return {
|
|
16182
|
+
kind: "platform-fleet-remote",
|
|
16183
|
+
mode,
|
|
16184
|
+
mutation: mode !== "status",
|
|
16185
|
+
environment,
|
|
16186
|
+
nodes: requests.map((request, index) => ({
|
|
16187
|
+
computeReference: configs[index].computeReference,
|
|
16188
|
+
address: request.target.address,
|
|
16189
|
+
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
16190
|
+
})),
|
|
16191
|
+
secretInputs
|
|
16192
|
+
};
|
|
16193
|
+
}
|
|
15808
16194
|
var defaultExec3 = async (argv2, options = {}) => {
|
|
15809
16195
|
const child = Bun.spawn([...argv2], {
|
|
15810
16196
|
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
@@ -15828,7 +16214,7 @@ var defaultExec3 = async (argv2, options = {}) => {
|
|
|
15828
16214
|
]);
|
|
15829
16215
|
return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
|
|
15830
16216
|
};
|
|
15831
|
-
var
|
|
16217
|
+
var checked5 = async (exec, argv2, label, options) => {
|
|
15832
16218
|
const result = await exec(argv2, options);
|
|
15833
16219
|
if (result.exitCode !== 0)
|
|
15834
16220
|
throw new Error(`${label} failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
@@ -15841,7 +16227,7 @@ function writeKnownHosts(request, directory) {
|
|
|
15841
16227
|
if (request.target.jump)
|
|
15842
16228
|
lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
|
|
15843
16229
|
const path = join10(directory, "known_hosts");
|
|
15844
|
-
|
|
16230
|
+
writeFileSync12(path, `${lines.join(`
|
|
15845
16231
|
`)}
|
|
15846
16232
|
`, { mode: 384, flag: "wx" });
|
|
15847
16233
|
return path;
|
|
@@ -15882,7 +16268,7 @@ var safeRemoteArg = (value) => {
|
|
|
15882
16268
|
};
|
|
15883
16269
|
var remote = async (exec, request, knownHosts, argv2, label, secret = false, stdin) => {
|
|
15884
16270
|
argv2.forEach(safeRemoteArg);
|
|
15885
|
-
return
|
|
16271
|
+
return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv2], label, { secret, stdin });
|
|
15886
16272
|
};
|
|
15887
16273
|
var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
15888
16274
|
safeRemoteArg(path);
|
|
@@ -15903,36 +16289,45 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
15903
16289
|
};
|
|
15904
16290
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
15905
16291
|
safeRemoteArg(remotePath);
|
|
15906
|
-
await
|
|
16292
|
+
await checked5(exec, [
|
|
15907
16293
|
"scp",
|
|
15908
16294
|
...sshOptions(request, knownHosts, true),
|
|
15909
16295
|
local,
|
|
15910
16296
|
`${destination2(request.target)}:${remotePath}`
|
|
15911
16297
|
], "secure copy", { secret });
|
|
15912
16298
|
};
|
|
15913
|
-
function stageConfig(config, directory) {
|
|
16299
|
+
async function stageConfig(config, directory) {
|
|
15914
16300
|
const rewritten = structuredClone(config);
|
|
15915
16301
|
const staged = [];
|
|
15916
16302
|
for (const [name, source] of secretSources(config)) {
|
|
15917
16303
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
15918
16304
|
const local = join10(directory, name);
|
|
15919
|
-
|
|
16305
|
+
writeFileSync12(local, bytes, { mode: 384, flag: "wx" });
|
|
15920
16306
|
staged.push(name);
|
|
15921
16307
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
15922
16308
|
if (name === "cloudflare-handoff")
|
|
15923
16309
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
15924
16310
|
}
|
|
16311
|
+
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
16312
|
+
const bundleFiles = [
|
|
16313
|
+
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
16314
|
+
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
16315
|
+
];
|
|
16316
|
+
rewritten.bootstrapBundle = {
|
|
16317
|
+
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
16318
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`
|
|
16319
|
+
};
|
|
15925
16320
|
const path = join10(directory, "platform-config.json");
|
|
15926
|
-
|
|
16321
|
+
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
15927
16322
|
`, { mode: 384, flag: "wx" });
|
|
15928
|
-
return { path, files: staged };
|
|
16323
|
+
return { path, files: staged, bundleFiles };
|
|
15929
16324
|
}
|
|
15930
16325
|
function stageMetalConfig(config, directory) {
|
|
15931
16326
|
const rewritten = structuredClone(config);
|
|
15932
16327
|
const files = [];
|
|
15933
16328
|
if (config.agentSeedFile) {
|
|
15934
16329
|
const name = "metal-agent-seed";
|
|
15935
|
-
|
|
16330
|
+
writeFileSync12(join10(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
|
|
15936
16331
|
mode: 384,
|
|
15937
16332
|
flag: "wx"
|
|
15938
16333
|
});
|
|
@@ -15940,7 +16335,7 @@ function stageMetalConfig(config, directory) {
|
|
|
15940
16335
|
files.push(name);
|
|
15941
16336
|
}
|
|
15942
16337
|
const path = join10(directory, "metal-config.json");
|
|
15943
|
-
|
|
16338
|
+
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
15944
16339
|
`, { mode: 384, flag: "wx" });
|
|
15945
16340
|
return { path, files };
|
|
15946
16341
|
}
|
|
@@ -15949,10 +16344,10 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
15949
16344
|
if (!response.ok)
|
|
15950
16345
|
throw new Error("pinned Bun download failed");
|
|
15951
16346
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
15952
|
-
if (
|
|
16347
|
+
if (createHash6("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
|
|
15953
16348
|
throw new Error("pinned Bun checksum mismatch");
|
|
15954
16349
|
const path = join10(directory, "bun.zip");
|
|
15955
|
-
|
|
16350
|
+
writeFileSync12(path, bytes, { mode: 384, flag: "wx" });
|
|
15956
16351
|
return path;
|
|
15957
16352
|
}
|
|
15958
16353
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
@@ -15962,7 +16357,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
15962
16357
|
[options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
|
|
15963
16358
|
];
|
|
15964
16359
|
for (const [artifact] of artifacts) {
|
|
15965
|
-
if (!
|
|
16360
|
+
if (!readFileSync12(artifact).length)
|
|
15966
16361
|
throw new Error(`packaged artifact is empty: ${basename2(artifact)}`);
|
|
15967
16362
|
}
|
|
15968
16363
|
await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
|
|
@@ -16003,7 +16398,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16003
16398
|
socketPath(request.target.agentSocket);
|
|
16004
16399
|
const exec = options.exec ?? defaultExec3;
|
|
16005
16400
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
16006
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16401
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
|
|
16007
16402
|
let knownHosts = "";
|
|
16008
16403
|
try {
|
|
16009
16404
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16015,18 +16410,16 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16015
16410
|
if (config.kind !== "platform")
|
|
16016
16411
|
throw new Error("operator bootstrap requires a platform config");
|
|
16017
16412
|
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
16018
|
-
const staged = stageConfig(config, directory);
|
|
16413
|
+
const staged = await stageConfig(config, directory);
|
|
16019
16414
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
16020
16415
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
16021
16416
|
for (const name of staged.files)
|
|
16022
16417
|
await copy(exec, request, knownHosts, join10(directory, name), `${remoteTemp}/${name}`, true);
|
|
16023
|
-
for (const
|
|
16418
|
+
for (const bundle of staged.bundleFiles)
|
|
16419
|
+
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
16420
|
+
for (const name of ["platform-config.json", ...staged.files, ...staged.bundleFiles.map(({ name: name2 }) => name2)])
|
|
16024
16421
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
|
|
16025
|
-
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform"];
|
|
16026
|
-
if (mode === "prepare")
|
|
16027
|
-
command.push("prepare");
|
|
16028
|
-
else
|
|
16029
|
-
command.push("credentials-stdin");
|
|
16422
|
+
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
16030
16423
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
16031
16424
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
16032
16425
|
` : undefined);
|
|
@@ -16037,16 +16430,40 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16037
16430
|
return;
|
|
16038
16431
|
});
|
|
16039
16432
|
}
|
|
16040
|
-
|
|
16433
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16041
16434
|
}
|
|
16042
16435
|
}
|
|
16436
|
+
async function applyOperatorPlatformFleetBootstrap(fleet, mode, options = {}) {
|
|
16437
|
+
const plan = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
16438
|
+
const { requests, configs } = validatedPlatformFleet(fleet);
|
|
16439
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(configs[0], options.secrets) : undefined;
|
|
16440
|
+
if (secrets) {
|
|
16441
|
+
for (const config of configs.slice(1))
|
|
16442
|
+
validatePlatformBootstrapSecrets(config, secrets);
|
|
16443
|
+
}
|
|
16444
|
+
const settled = await Promise.allSettled(requests.map((request) => applyOperatorPlatformBootstrap(request, mode, { ...options, secrets })));
|
|
16445
|
+
const outcomes = settled.map((result, index) => {
|
|
16446
|
+
const config = configs[index];
|
|
16447
|
+
const address = requests[index].target.address;
|
|
16448
|
+
if (result.status === "fulfilled")
|
|
16449
|
+
return {
|
|
16450
|
+
computeReference: config.computeReference,
|
|
16451
|
+
address,
|
|
16452
|
+
ok: true,
|
|
16453
|
+
output: result.value.output.slice(0, 65536)
|
|
16454
|
+
};
|
|
16455
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
16456
|
+
return { computeReference: config.computeReference, address, ok: false, error: message.slice(0, 4096) };
|
|
16457
|
+
});
|
|
16458
|
+
return { plan, ok: outcomes.every(({ ok }) => ok), outcomes };
|
|
16459
|
+
}
|
|
16043
16460
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
16044
16461
|
const plan = planOperatorMetalBootstrap(request, mode);
|
|
16045
16462
|
publicIdentity(request.target.identityPublicKeyFile);
|
|
16046
16463
|
socketPath(request.target.agentSocket);
|
|
16047
16464
|
const exec = options.exec ?? defaultExec3;
|
|
16048
16465
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-metal-"));
|
|
16049
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16466
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
|
|
16050
16467
|
let knownHosts = "";
|
|
16051
16468
|
try {
|
|
16052
16469
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16090,7 +16507,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16090
16507
|
return;
|
|
16091
16508
|
});
|
|
16092
16509
|
}
|
|
16093
|
-
|
|
16510
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16094
16511
|
}
|
|
16095
16512
|
}
|
|
16096
16513
|
|
|
@@ -16103,9 +16520,6 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16103
16520
|
if (nodes.some((node) => !node.nodeHostname || !node.cloudflareHandoffFile)) {
|
|
16104
16521
|
throw new Error("every platform genesis node requires its public hostname and node-specific Cloudflare handoff");
|
|
16105
16522
|
}
|
|
16106
|
-
if (template.branch !== (environment === "production" ? "main" : "dev")) {
|
|
16107
|
-
throw new Error("platform genesis branch must match the selected environment");
|
|
16108
|
-
}
|
|
16109
16523
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
16110
16524
|
return fleet.map((guest, index) => {
|
|
16111
16525
|
const node = nodes[index];
|
|
@@ -16144,7 +16558,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16144
16558
|
}
|
|
16145
16559
|
|
|
16146
16560
|
// src/host-maintenance.ts
|
|
16147
|
-
import { lstatSync as
|
|
16561
|
+
import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
|
|
16148
16562
|
var ROOT = "/opt/forgezero";
|
|
16149
16563
|
var SLOT_FILE = `${ROOT}/.forge-slot`;
|
|
16150
16564
|
var SHARED_ENV = `${ROOT}/shared/.env`;
|
|
@@ -16152,9 +16566,9 @@ var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
|
|
|
16152
16566
|
var FZ = "/usr/local/bin/fz";
|
|
16153
16567
|
var localRuntime = () => ({
|
|
16154
16568
|
uid: () => process.getuid?.() ?? -1,
|
|
16155
|
-
read: (path) =>
|
|
16569
|
+
read: (path) => readFileSync13(path, "utf8"),
|
|
16156
16570
|
inspect(path) {
|
|
16157
|
-
const value =
|
|
16571
|
+
const value = lstatSync8(path);
|
|
16158
16572
|
return {
|
|
16159
16573
|
regular: value.isFile(),
|
|
16160
16574
|
symbolic: value.isSymbolicLink(),
|
|
@@ -16275,8 +16689,8 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
16275
16689
|
}
|
|
16276
16690
|
|
|
16277
16691
|
// src/cli/maintenance.ts
|
|
16278
|
-
import { existsSync as
|
|
16279
|
-
import { isAbsolute as
|
|
16692
|
+
import { existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
|
|
16693
|
+
import { isAbsolute as isAbsolute6, join as join11, relative as relative2, resolve as resolve10 } from "path";
|
|
16280
16694
|
var API_OPERATION_ENTRYPOINTS = {
|
|
16281
16695
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
16282
16696
|
"db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
|
|
@@ -16346,12 +16760,12 @@ function unsupportedRepositoryCliOption(argv2) {
|
|
|
16346
16760
|
}
|
|
16347
16761
|
function manifestName(root) {
|
|
16348
16762
|
const manifestPath = join11(root, "package.json");
|
|
16349
|
-
if (!
|
|
16763
|
+
if (!existsSync11(manifestPath)) {
|
|
16350
16764
|
throw new Error(`No package.json exists at repository root ${root}.`);
|
|
16351
16765
|
}
|
|
16352
16766
|
let parsed;
|
|
16353
16767
|
try {
|
|
16354
|
-
parsed = JSON.parse(
|
|
16768
|
+
parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
16355
16769
|
} catch (cause) {
|
|
16356
16770
|
throw new Error(`Cannot read ${manifestPath}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
16357
16771
|
}
|
|
@@ -16362,21 +16776,21 @@ function manifestName(root) {
|
|
|
16362
16776
|
}
|
|
16363
16777
|
function checkedEntrypoint(root, parts) {
|
|
16364
16778
|
const candidate = join11(root, ...parts);
|
|
16365
|
-
if (!
|
|
16779
|
+
if (!existsSync11(candidate) || !lstatSync9(candidate).isFile()) {
|
|
16366
16780
|
throw new Error(`The reviewed operation entrypoint is missing: ${candidate}`);
|
|
16367
16781
|
}
|
|
16368
|
-
if (
|
|
16782
|
+
if (lstatSync9(candidate).isSymbolicLink()) {
|
|
16369
16783
|
throw new Error(`The reviewed operation entrypoint must not be a symbolic link: ${candidate}`);
|
|
16370
16784
|
}
|
|
16371
16785
|
const actual = realpathSync5(candidate);
|
|
16372
16786
|
const within = relative2(root, actual);
|
|
16373
|
-
if (within.startsWith("..") ||
|
|
16787
|
+
if (within.startsWith("..") || isAbsolute6(within)) {
|
|
16374
16788
|
throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
|
|
16375
16789
|
}
|
|
16376
16790
|
return actual;
|
|
16377
16791
|
}
|
|
16378
16792
|
function resolveRepositoryOperation(operation, requestedRoot) {
|
|
16379
|
-
const root = realpathSync5(
|
|
16793
|
+
const root = realpathSync5(resolve10(requestedRoot));
|
|
16380
16794
|
const name = manifestName(root);
|
|
16381
16795
|
if (operation in API_OPERATION_ENTRYPOINTS) {
|
|
16382
16796
|
const parts = API_OPERATION_ENTRYPOINTS[operation];
|
|
@@ -16677,7 +17091,7 @@ async function api(options, path, init) {
|
|
|
16677
17091
|
return { status: response.status, body };
|
|
16678
17092
|
}
|
|
16679
17093
|
function sleep(ms) {
|
|
16680
|
-
return new Promise((
|
|
17094
|
+
return new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
16681
17095
|
}
|
|
16682
17096
|
function browserCommand(url) {
|
|
16683
17097
|
if (process.platform === "darwin")
|
|
@@ -16762,7 +17176,7 @@ function requireSession() {
|
|
|
16762
17176
|
function readOwnerOnlySecret(path, label) {
|
|
16763
17177
|
let metadata;
|
|
16764
17178
|
try {
|
|
16765
|
-
metadata =
|
|
17179
|
+
metadata = lstatSync10(path);
|
|
16766
17180
|
} catch {
|
|
16767
17181
|
throw new Error(`The ${label} file ${path} is unreadable.`);
|
|
16768
17182
|
}
|
|
@@ -16776,7 +17190,7 @@ function readOwnerOnlySecret(path, label) {
|
|
|
16776
17190
|
if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
16777
17191
|
throw new Error(`The ${label} file ${path} is not owned by the current user.`);
|
|
16778
17192
|
}
|
|
16779
|
-
const value =
|
|
17193
|
+
const value = readFileSync15(path, "utf8").trim();
|
|
16780
17194
|
if (!value)
|
|
16781
17195
|
throw new Error(`The ${label} file ${path} is empty.`);
|
|
16782
17196
|
return value;
|
|
@@ -16957,7 +17371,7 @@ async function cmdApi(options, args) {
|
|
|
16957
17371
|
let body = undefined;
|
|
16958
17372
|
if (options.data !== undefined && options.dataFile)
|
|
16959
17373
|
throw new Error("Use only one of --data or --data-file.");
|
|
16960
|
-
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ?
|
|
17374
|
+
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync15(options.dataFile, "utf8") : options.data;
|
|
16961
17375
|
if (encoded !== undefined)
|
|
16962
17376
|
body = JSON.parse(encoded);
|
|
16963
17377
|
const result = await api(options, `${url.pathname}${url.search}`, { method, body });
|
|
@@ -17051,7 +17465,7 @@ async function cmdAgent(options, args) {
|
|
|
17051
17465
|
seedCredentialPath: process.env.FZ_SEED_CREDENTIAL_PATH,
|
|
17052
17466
|
gitCredentialPath,
|
|
17053
17467
|
gitPublicKeyPath,
|
|
17054
|
-
generateGitIdentity: true,
|
|
17468
|
+
generateGitIdentity: process.env.FZ_GENERATE_GIT_DEPLOY_KEY === "true",
|
|
17055
17469
|
controlSocketPath: process.env.FZ_CONTROL_SOCKET,
|
|
17056
17470
|
repository: process.env.FZ_DEPLOY_REPO,
|
|
17057
17471
|
branch: process.env.FZ_DEPLOY_BRANCH,
|
|
@@ -17108,19 +17522,19 @@ async function cmdAgent(options, args) {
|
|
|
17108
17522
|
return 0;
|
|
17109
17523
|
}
|
|
17110
17524
|
try {
|
|
17111
|
-
|
|
17525
|
+
writeFileSync13(plan.unitPath, plan.unit, { mode: 420 });
|
|
17112
17526
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
17113
17527
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
17114
|
-
|
|
17115
|
-
|
|
17528
|
+
mkdirSync13(dirname14(auxiliary.path), { recursive: true, mode: 493 });
|
|
17529
|
+
writeFileSync13(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
17116
17530
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17117
17531
|
}
|
|
17118
17532
|
if (options.enrol) {
|
|
17119
|
-
if (!
|
|
17533
|
+
if (!existsSync12(enrolStatePath) && !existsSync12(enrolTokenCredentialPath)) {
|
|
17120
17534
|
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17121
17535
|
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17122
17536
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17123
|
-
|
|
17537
|
+
mkdirSync13(dirname14(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17124
17538
|
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17125
17539
|
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17126
17540
|
sealing.stdin.write(`${token}
|
|
@@ -17136,11 +17550,13 @@ async function cmdAgent(options, args) {
|
|
|
17136
17550
|
for (const step3 of transcript)
|
|
17137
17551
|
out.ok(step3.label);
|
|
17138
17552
|
out.ok("Agent service and socket verified");
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17553
|
+
if (existsSync12(gitPublicKeyPath)) {
|
|
17554
|
+
out.line();
|
|
17555
|
+
out.line(" Compatibility SSH deploy public key:");
|
|
17556
|
+
out.line();
|
|
17557
|
+
out.line(` ${readFileSync15(gitPublicKeyPath, "utf8").trim()}`);
|
|
17558
|
+
out.line();
|
|
17559
|
+
}
|
|
17144
17560
|
return 0;
|
|
17145
17561
|
} catch (cause) {
|
|
17146
17562
|
out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
|
|
@@ -17244,10 +17660,10 @@ function interactiveMetalBootstrap() {
|
|
|
17244
17660
|
});
|
|
17245
17661
|
}
|
|
17246
17662
|
function writeBootstrapConfig(path, config) {
|
|
17247
|
-
if (!
|
|
17663
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17248
17664
|
throw new Error("--output must be a canonical absolute path");
|
|
17249
|
-
|
|
17250
|
-
|
|
17665
|
+
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
17666
|
+
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
17251
17667
|
`, { mode: 384, flag: "wx" });
|
|
17252
17668
|
return path;
|
|
17253
17669
|
}
|
|
@@ -17308,17 +17724,48 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17308
17724
|
});
|
|
17309
17725
|
}
|
|
17310
17726
|
function genesisOutputDirectory(path) {
|
|
17311
|
-
if (!
|
|
17727
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17312
17728
|
throw new Error("--output must be a canonical absolute directory");
|
|
17313
|
-
if (!
|
|
17314
|
-
|
|
17315
|
-
const metadata =
|
|
17729
|
+
if (!existsSync12(path))
|
|
17730
|
+
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
17731
|
+
const metadata = lstatSync10(path);
|
|
17316
17732
|
const uid = process.getuid?.();
|
|
17317
17733
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
17318
17734
|
throw new Error("platform genesis output must be an owner-only directory owned by the current user");
|
|
17319
17735
|
}
|
|
17320
17736
|
return path;
|
|
17321
17737
|
}
|
|
17738
|
+
function interactiveOperatorHop(label, defaults) {
|
|
17739
|
+
return createOperatorSshHop({
|
|
17740
|
+
address: bootstrapAnswer(`${label} explicit SSH IP`, defaults.address),
|
|
17741
|
+
port: bootstrapNumber(`${label} SSH port`, "22"),
|
|
17742
|
+
user: bootstrapAnswer(`${label} SSH user`, defaults.user),
|
|
17743
|
+
hostKey: bootstrapAnswer(`${label} trusted ssh-ed25519 host public key`)
|
|
17744
|
+
});
|
|
17745
|
+
}
|
|
17746
|
+
function operatorIdentityCoordinates() {
|
|
17747
|
+
return {
|
|
17748
|
+
identityPublicKeyFile: bootstrapAnswer("Operator SSH public-key file (private key remains in Bitwarden SSH agent)"),
|
|
17749
|
+
agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
|
|
17750
|
+
};
|
|
17751
|
+
}
|
|
17752
|
+
function writeMetalOperatorFiles(directory) {
|
|
17753
|
+
const output = genesisOutputDirectory(directory);
|
|
17754
|
+
const config = interactiveMetalBootstrap();
|
|
17755
|
+
const metalConfig = writeBootstrapConfig(join12(output, "metal.json"), config);
|
|
17756
|
+
const identity = operatorIdentityCoordinates();
|
|
17757
|
+
const target = interactiveOperatorHop("Metal", { user: "root" });
|
|
17758
|
+
const remoteRequest = writeOperatorMetalBootstrapRequest(join12(output, "metal-remote.json"), {
|
|
17759
|
+
kind: "metal-remote",
|
|
17760
|
+
metalConfigFile: metalConfig,
|
|
17761
|
+
target: { ...target, ...identity },
|
|
17762
|
+
genesis: {
|
|
17763
|
+
environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
|
|
17764
|
+
sshPublicKeyFiles: [identity.identityPublicKeyFile]
|
|
17765
|
+
}
|
|
17766
|
+
});
|
|
17767
|
+
return { metalConfig, remoteRequest };
|
|
17768
|
+
}
|
|
17322
17769
|
function writePlatformGenesisFleet(directory) {
|
|
17323
17770
|
const output = genesisOutputDirectory(directory);
|
|
17324
17771
|
const template = interactiveBootstrap("platform", true);
|
|
@@ -17335,7 +17782,20 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17335
17782
|
writeBootstrapConfig(path, config);
|
|
17336
17783
|
return path;
|
|
17337
17784
|
});
|
|
17338
|
-
|
|
17785
|
+
const identity = operatorIdentityCoordinates();
|
|
17786
|
+
const useJump = bootstrapAnswer("Reach genesis computes through the Metal SSH jump? (yes/no)", "yes") === "yes";
|
|
17787
|
+
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
17788
|
+
const requests = configs.map((platformConfigFile, index) => {
|
|
17789
|
+
const guest = fleet[index];
|
|
17790
|
+
const target = interactiveOperatorHop(`${guest.name} compute`, { address: guest.address, user: "forgezero" });
|
|
17791
|
+
return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
|
|
17792
|
+
kind: "platform-remote",
|
|
17793
|
+
platformConfigFile,
|
|
17794
|
+
target: { ...target, ...identity, ...jump ? { jump } : {} }
|
|
17795
|
+
});
|
|
17796
|
+
});
|
|
17797
|
+
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
17798
|
+
return { configs, requests, fleetRequest };
|
|
17339
17799
|
}
|
|
17340
17800
|
function interactiveBootstrap(kind, genesis = false) {
|
|
17341
17801
|
if (!process.stdin.isTTY)
|
|
@@ -17356,8 +17816,8 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17356
17816
|
const nodeHostname = bootstrapAnswer(genesis ? `${computeReference} public node hostname` : "Public node hostname");
|
|
17357
17817
|
const apiUrl = bootstrapAnswer("Platform API URL");
|
|
17358
17818
|
const appOrigin = bootstrapAnswer("Public App origin");
|
|
17359
|
-
const
|
|
17360
|
-
const
|
|
17819
|
+
const bundleFile = bootstrapAnswer("Bootstrap API Git bundle (absolute path)");
|
|
17820
|
+
const manifestFile = bootstrapAnswer("Bootstrap bundle manifest (absolute path)", `${bundleFile}.json`);
|
|
17361
17821
|
const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
|
|
17362
17822
|
const collectorUnit = FORGEZERO_OTEL_COLLECTOR_UNIT;
|
|
17363
17823
|
const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
|
|
@@ -17386,8 +17846,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17386
17846
|
computeReference,
|
|
17387
17847
|
nodeHostname,
|
|
17388
17848
|
apiUrl,
|
|
17389
|
-
|
|
17390
|
-
branch,
|
|
17849
|
+
bootstrapBundle: { bundleFile, manifestFile },
|
|
17391
17850
|
telemetryEndpoint,
|
|
17392
17851
|
database: {
|
|
17393
17852
|
role,
|
|
@@ -17431,8 +17890,6 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17431
17890
|
...backupEndpoint && backupRegion && backupBucket && backupAccessKeyId ? {
|
|
17432
17891
|
backup: { endpoint: backupEndpoint, region: backupRegion, bucket: backupBucket, accessKeyId: backupAccessKeyId }
|
|
17433
17892
|
} : {},
|
|
17434
|
-
repository,
|
|
17435
|
-
branch,
|
|
17436
17893
|
deployProfile: environment
|
|
17437
17894
|
},
|
|
17438
17895
|
serviceUser: "forgezero-api",
|
|
@@ -17460,7 +17917,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17460
17917
|
if (operation === "config") {
|
|
17461
17918
|
const kind = args[1];
|
|
17462
17919
|
if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
|
|
17463
|
-
throw new Error("Usage: fz bootstrap config
|
|
17920
|
+
throw new Error("Usage: fz bootstrap config cloudflare --output <absolute-file> | fz bootstrap config metal|platform --output <absolute-owner-only-directory>");
|
|
17464
17921
|
}
|
|
17465
17922
|
if (kind === "platform") {
|
|
17466
17923
|
out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
|
|
@@ -17472,14 +17929,36 @@ async function cmdBootstrap(options, args) {
|
|
|
17472
17929
|
out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
|
|
17473
17930
|
return 0;
|
|
17474
17931
|
}
|
|
17475
|
-
|
|
17476
|
-
out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
|
|
17932
|
+
out.line(JSON.stringify({ kind, ...writeMetalOperatorFiles(options.outputPath), mode: "0600" }, null, 2));
|
|
17477
17933
|
return 0;
|
|
17478
17934
|
}
|
|
17935
|
+
if (operation === "platform" && args[1] === "fleet") {
|
|
17936
|
+
const mode = args[2];
|
|
17937
|
+
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
17938
|
+
throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
17939
|
+
}
|
|
17940
|
+
if (!options.bootstrapConfigPath)
|
|
17941
|
+
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
17942
|
+
const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
|
|
17943
|
+
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
17944
|
+
if (!options.apply) {
|
|
17945
|
+
out.line(JSON.stringify(plan2, null, 2));
|
|
17946
|
+
out.step("Review all three pinned targets and shared secret names, then repeat with --apply.");
|
|
17947
|
+
return 0;
|
|
17948
|
+
}
|
|
17949
|
+
const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
|
|
17950
|
+
const firstConfig = readBootstrapConfig(first.platformConfigFile);
|
|
17951
|
+
if (firstConfig.kind !== "platform")
|
|
17952
|
+
throw new Error("remote platform fleet requires platform configs");
|
|
17953
|
+
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
|
|
17954
|
+
const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode, { secrets: secrets2 });
|
|
17955
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
17956
|
+
return result2.ok ? 0 : 1;
|
|
17957
|
+
}
|
|
17479
17958
|
if (operation === "platform" && args[1] === "remote") {
|
|
17480
17959
|
const mode = args[2];
|
|
17481
|
-
if (!mode || !["
|
|
17482
|
-
throw new Error("Usage: fz bootstrap platform remote <
|
|
17960
|
+
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
17961
|
+
throw new Error("Usage: fz bootstrap platform remote <apply|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
17483
17962
|
}
|
|
17484
17963
|
if (!options.bootstrapConfigPath)
|
|
17485
17964
|
throw new Error("remote platform bootstrap requires --bootstrap-config");
|
|
@@ -17497,6 +17976,18 @@ async function cmdBootstrap(options, args) {
|
|
|
17497
17976
|
out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode, { secrets: secrets2 }), null, 2));
|
|
17498
17977
|
return 0;
|
|
17499
17978
|
}
|
|
17979
|
+
if (operation === "platform" && args[1] === "bundle") {
|
|
17980
|
+
if (args[2] !== undefined || !options.outputPath) {
|
|
17981
|
+
throw new Error("Usage: fz bootstrap platform bundle --root <clean-api-checkout> --branch <main|dev> --output <absolute.bundle>");
|
|
17982
|
+
}
|
|
17983
|
+
const result2 = await buildBootstrapBundle({
|
|
17984
|
+
repositoryRoot: options.projectRoot,
|
|
17985
|
+
outputPath: options.outputPath,
|
|
17986
|
+
branch: options.branch
|
|
17987
|
+
});
|
|
17988
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
17989
|
+
return 0;
|
|
17990
|
+
}
|
|
17500
17991
|
if (operation === "metal" && args[1] === "remote") {
|
|
17501
17992
|
const mode = args[2];
|
|
17502
17993
|
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "status"].includes(mode) || args[3] !== undefined) {
|
|
@@ -17553,31 +18044,12 @@ async function cmdBootstrap(options, args) {
|
|
|
17553
18044
|
await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply, tokens);
|
|
17554
18045
|
return 0;
|
|
17555
18046
|
}
|
|
17556
|
-
if (operation === "platform" && args[1] === "prepare") {
|
|
17557
|
-
if (args[2] !== undefined)
|
|
17558
|
-
throw new Error("platform prepare accepts no positional arguments");
|
|
17559
|
-
const config2 = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : interactiveBootstrap("platform");
|
|
17560
|
-
if (config2.kind !== "platform")
|
|
17561
|
-
throw new Error("platform prepare requires a platform bootstrap config");
|
|
17562
|
-
if (!options.apply) {
|
|
17563
|
-
out.line(JSON.stringify({
|
|
17564
|
-
kind: "platform",
|
|
17565
|
-
mode: "prepare",
|
|
17566
|
-
mutation: false,
|
|
17567
|
-
steps: ["bind immutable host intent", "install common Agent", "print machine public deploy key"]
|
|
17568
|
-
}, null, 2));
|
|
17569
|
-
out.step("Review the plan, then repeat with --apply as root. No database or API starts in prepare.");
|
|
17570
|
-
return 0;
|
|
17571
|
-
}
|
|
17572
|
-
out.line(JSON.stringify(await preparePlatformBootstrap(config2), null, 2));
|
|
17573
|
-
return 0;
|
|
17574
|
-
}
|
|
17575
18047
|
const credentialStdin = operation === "platform" && args[1] === "credentials-stdin";
|
|
17576
18048
|
if (credentialStdin && args[2] !== undefined)
|
|
17577
18049
|
throw new Error("platform credential stdin accepts no additional positional arguments");
|
|
17578
18050
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
17579
|
-
metal:
|
|
17580
|
-
compute:
|
|
18051
|
+
metal: existsSync12(METAL_BOOTSTRAP_STATE_PATH),
|
|
18052
|
+
compute: existsSync12(BOOTSTRAP_STATE_PATH)
|
|
17581
18053
|
});
|
|
17582
18054
|
if (operation === "status") {
|
|
17583
18055
|
if (installedBootstrapKind() === "metal") {
|
|
@@ -17618,7 +18090,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17618
18090
|
return 0;
|
|
17619
18091
|
}
|
|
17620
18092
|
if (!["platform", "repair"].includes(operation)) {
|
|
17621
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [
|
|
18093
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
17622
18094
|
}
|
|
17623
18095
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
17624
18096
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -17715,7 +18187,7 @@ async function cmdUnlock(options) {
|
|
|
17715
18187
|
if (options.key || options.userExplicit) {
|
|
17716
18188
|
throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
|
|
17717
18189
|
}
|
|
17718
|
-
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ?
|
|
18190
|
+
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync15(0, "utf8").trim() : "";
|
|
17719
18191
|
const phrase = phraseText ? phraseText.split(/\s+/) : [];
|
|
17720
18192
|
if (phrase.length !== 24) {
|
|
17721
18193
|
throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
|
|
@@ -17931,7 +18403,7 @@ function projectFromCheckout(options) {
|
|
|
17931
18403
|
try {
|
|
17932
18404
|
return loadConfig({
|
|
17933
18405
|
cwd: options.projectRoot,
|
|
17934
|
-
readFile: (path) =>
|
|
18406
|
+
readFile: (path) => existsSync12(path) ? readFileSync15(path, "utf8") : undefined
|
|
17935
18407
|
}).config.project;
|
|
17936
18408
|
} catch (cause) {
|
|
17937
18409
|
throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
@@ -17964,10 +18436,10 @@ async function waitForRuns(options, pipelineKey, keys) {
|
|
|
17964
18436
|
while (Date.now() < deadline) {
|
|
17965
18437
|
const payload = await deploymentRequest(options, `/runs?pipelineKey=${encodeURIComponent(pipelineKey)}`);
|
|
17966
18438
|
const runs = payload.runs ?? [];
|
|
17967
|
-
const selected = runs.filter((
|
|
17968
|
-
if (selected.length === keys.length && selected.every((
|
|
17969
|
-
out.line(JSON.stringify({ ok: selected.every((
|
|
17970
|
-
return selected.every((
|
|
18439
|
+
const selected = runs.filter((run2) => run2._key && keys.includes(run2._key));
|
|
18440
|
+
if (selected.length === keys.length && selected.every((run2) => terminal.has(run2.outcome ?? ""))) {
|
|
18441
|
+
out.line(JSON.stringify({ ok: selected.every((run2) => run2.outcome === "deployed"), runs: selected }, null, 2));
|
|
18442
|
+
return selected.every((run2) => run2.outcome === "deployed");
|
|
17971
18443
|
}
|
|
17972
18444
|
await sleep(2000);
|
|
17973
18445
|
}
|
|
@@ -17994,7 +18466,7 @@ async function cmdDeploy(options, args) {
|
|
|
17994
18466
|
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
17995
18467
|
return 0;
|
|
17996
18468
|
}
|
|
17997
|
-
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(
|
|
18469
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve11(options.projectRoot)), force: options.force });
|
|
17998
18470
|
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
17999
18471
|
if (options.json)
|
|
18000
18472
|
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
@@ -18019,7 +18491,7 @@ async function cmdDeploy(options, args) {
|
|
|
18019
18491
|
return problems.length === 0 ? 0 : 1;
|
|
18020
18492
|
}
|
|
18021
18493
|
if (operation === "check" || operation === "sync") {
|
|
18022
|
-
if (
|
|
18494
|
+
if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18023
18495
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18024
18496
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18025
18497
|
const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -18095,7 +18567,7 @@ async function cmdDeploy(options, args) {
|
|
|
18095
18567
|
branch: options.branch,
|
|
18096
18568
|
cloneUrl: required(options.cloneUrl, "--clone-url"),
|
|
18097
18569
|
sourceAuth,
|
|
18098
|
-
...options.knownHostsFile ? { knownHosts:
|
|
18570
|
+
...options.knownHostsFile ? { knownHosts: readFileSync15(options.knownHostsFile, "utf8") } : {},
|
|
18099
18571
|
projectKey
|
|
18100
18572
|
} });
|
|
18101
18573
|
const pipelineKey2 = String(created.pipelineKey);
|
|
@@ -18135,7 +18607,7 @@ async function cmdDeploy(options, args) {
|
|
|
18135
18607
|
out.line(JSON.stringify(released, null, 2));
|
|
18136
18608
|
return 0;
|
|
18137
18609
|
}
|
|
18138
|
-
const keys = (released.runs ?? []).flatMap((
|
|
18610
|
+
const keys = (released.runs ?? []).flatMap((run2) => run2._key ? [run2._key] : []);
|
|
18139
18611
|
if (keys.length === 0)
|
|
18140
18612
|
throw new Error("The API accepted the release but returned no deployment runs.");
|
|
18141
18613
|
return await waitForRuns(options, pipelineKey, keys) ? 0 : 1;
|
|
@@ -18191,15 +18663,18 @@ function usage() {
|
|
|
18191
18663
|
fz agent install Install the node agent as a systemd service, so
|
|
18192
18664
|
applications on this box read secrets through a
|
|
18193
18665
|
local socket instead of holding an API key
|
|
18194
|
-
fz bootstrap platform
|
|
18195
|
-
|
|
18196
|
-
fz bootstrap platform remote <
|
|
18666
|
+
fz bootstrap platform bundle
|
|
18667
|
+
Build one clean exact-revision API Git bundle for genesis
|
|
18668
|
+
fz bootstrap platform remote <apply|status>
|
|
18197
18669
|
Run typed bootstrap from the operator laptop through
|
|
18198
|
-
a pinned host and
|
|
18670
|
+
a pinned host; apply copies and verifies that bundle
|
|
18671
|
+
fz bootstrap platform fleet <apply|status>
|
|
18672
|
+
Validate the exact three-node genesis identity, prompt
|
|
18673
|
+
shared secrets once and run all pinned nodes concurrently
|
|
18199
18674
|
fz bootstrap platform Install/repair a typed elastic platform compute
|
|
18200
18675
|
fz bootstrap config <metal|platform|cloudflare>
|
|
18201
|
-
Write
|
|
18202
|
-
|
|
18676
|
+
Write validated owner-only metal config+remote request,
|
|
18677
|
+
Cloudflare file, or complete three-node platform request set
|
|
18203
18678
|
fz bootstrap platform cloudflare
|
|
18204
18679
|
Plan/apply attended Tunnel and DNS reconciliation
|
|
18205
18680
|
using hidden management and KV/Worker runtime token prompts
|