@forgezero/agent 0.1.61 → 0.1.63
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 +38 -13
- 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 +7 -17
- package/dist/bootstrap.js +407 -214
- 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 +16 -0
- package/dist/fz-agent.js +486 -363
- package/dist/fz.js +499 -305
- 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 +1 -1
- package/dist/operator-bootstrap.js +509 -307
- package/dist/platform-bootstrap-runtime.d.ts +0 -2
- package/dist/platform-bootstrap-runtime.js +0 -4
- package/dist/platform-fleet-verification.js +110 -44
- package/dist/provision.d.ts +4 -1
- package/dist/provision.js +15 -7
- 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 mkdirSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
4815
|
+
import { basename as basename3, dirname as dirname13, isAbsolute as isAbsolute6, resolve as resolve10 } 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.63";
|
|
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) {
|
|
@@ -5897,6 +5902,8 @@ function agentUnit(options) {
|
|
|
5897
5902
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
5898
5903
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
5899
5904
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
5905
|
+
options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
|
|
5906
|
+
options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
|
|
5900
5907
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
5901
5908
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
5902
5909
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
@@ -11555,18 +11562,18 @@ function removeSession(api, realm, path = defaultSessionPath()) {
|
|
|
11555
11562
|
}
|
|
11556
11563
|
|
|
11557
11564
|
// src/bootstrap.ts
|
|
11558
|
-
import { createHash as
|
|
11565
|
+
import { createHash as createHash4, createHmac as createHmac2, randomBytes as randomBytes7 } from "crypto";
|
|
11559
11566
|
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
|
|
11567
|
+
chmodSync as chmodSync4,
|
|
11568
|
+
existsSync as existsSync9,
|
|
11569
|
+
lstatSync as lstatSync5,
|
|
11570
|
+
mkdirSync as mkdirSync9,
|
|
11571
|
+
readFileSync as readFileSync9,
|
|
11572
|
+
renameSync as renameSync8,
|
|
11573
|
+
rmSync as rmSync6,
|
|
11574
|
+
writeFileSync as writeFileSync9
|
|
11568
11575
|
} from "fs";
|
|
11569
|
-
import { dirname as
|
|
11576
|
+
import { dirname as dirname9 } from "path";
|
|
11570
11577
|
import { fileURLToPath } from "url";
|
|
11571
11578
|
|
|
11572
11579
|
// src/platform-bootstrap-runtime.ts
|
|
@@ -11654,8 +11661,6 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
11654
11661
|
databaseUser: input.databaseUser,
|
|
11655
11662
|
sharedDirectory: input.sharedDirectory,
|
|
11656
11663
|
seedSyncEpoch: input.seedSyncEpoch,
|
|
11657
|
-
repository: input.repository,
|
|
11658
|
-
branch: input.branch,
|
|
11659
11664
|
deployProfile: input.deployProfile
|
|
11660
11665
|
}))
|
|
11661
11666
|
safeAtom(name, value);
|
|
@@ -11738,8 +11743,6 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
11738
11743
|
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
11739
11744
|
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
11740
11745
|
FZ_PROFILE: value.deployProfile,
|
|
11741
|
-
FZ_REPO: value.repository,
|
|
11742
|
-
FZ_BRANCH: value.branch,
|
|
11743
11746
|
FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
|
|
11744
11747
|
FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
|
|
11745
11748
|
FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
|
|
@@ -13158,6 +13161,164 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
13158
13161
|
|
|
13159
13162
|
// src/bootstrap.ts
|
|
13160
13163
|
init_dist();
|
|
13164
|
+
|
|
13165
|
+
// src/bootstrap-bundle.ts
|
|
13166
|
+
import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
|
|
13167
|
+
import {
|
|
13168
|
+
chmodSync as chmodSync3,
|
|
13169
|
+
createReadStream,
|
|
13170
|
+
existsSync as existsSync8,
|
|
13171
|
+
lstatSync as lstatSync4,
|
|
13172
|
+
mkdirSync as mkdirSync8,
|
|
13173
|
+
readFileSync as readFileSync8,
|
|
13174
|
+
renameSync as renameSync7,
|
|
13175
|
+
rmSync as rmSync5,
|
|
13176
|
+
writeFileSync as writeFileSync8
|
|
13177
|
+
} from "fs";
|
|
13178
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve6 } from "path";
|
|
13179
|
+
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
13180
|
+
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
13181
|
+
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
13182
|
+
var run = async (argv2) => {
|
|
13183
|
+
const child = Bun.spawn([...argv2], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
13184
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
13185
|
+
new Response(child.stdout).text(),
|
|
13186
|
+
new Response(child.stderr).text(),
|
|
13187
|
+
child.exited
|
|
13188
|
+
]);
|
|
13189
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 65536) };
|
|
13190
|
+
};
|
|
13191
|
+
var checked2 = async (exec, argv2, label) => {
|
|
13192
|
+
const result = await exec(argv2);
|
|
13193
|
+
if (result.exitCode !== 0)
|
|
13194
|
+
throw new Error(`${label} failed${result.output.trim() ? `: ${result.output.trim()}` : ""}`);
|
|
13195
|
+
return result.output.trim();
|
|
13196
|
+
};
|
|
13197
|
+
async function sha256File(path) {
|
|
13198
|
+
const hash = createHash3("sha256");
|
|
13199
|
+
await new Promise((resolveDone, reject) => {
|
|
13200
|
+
const stream = createReadStream(path);
|
|
13201
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
13202
|
+
stream.on("error", reject);
|
|
13203
|
+
stream.on("end", resolveDone);
|
|
13204
|
+
});
|
|
13205
|
+
return hash.digest("hex");
|
|
13206
|
+
}
|
|
13207
|
+
var branchName = (value) => {
|
|
13208
|
+
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("-")) {
|
|
13209
|
+
throw new Error("bootstrap bundle branch is malformed");
|
|
13210
|
+
}
|
|
13211
|
+
return value;
|
|
13212
|
+
};
|
|
13213
|
+
function parseBootstrapBundleManifest(value) {
|
|
13214
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13215
|
+
throw new Error("bootstrap bundle manifest must be an object");
|
|
13216
|
+
const source = value;
|
|
13217
|
+
const allowed = ["format", "kind", "branch", "revision", "sha256", "bytes", "createdAt"];
|
|
13218
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
13219
|
+
if (unknown.length)
|
|
13220
|
+
throw new Error(`bootstrap bundle manifest contains unknown field ${unknown[0]}`);
|
|
13221
|
+
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
13222
|
+
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
13223
|
+
}
|
|
13224
|
+
branchName(source.branch);
|
|
13225
|
+
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
13226
|
+
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
13227
|
+
}
|
|
13228
|
+
if (typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256)) {
|
|
13229
|
+
throw new Error("bootstrap bundle digest is malformed");
|
|
13230
|
+
}
|
|
13231
|
+
if (!Number.isSafeInteger(source.bytes) || Number(source.bytes) < 1 || Number(source.bytes) > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
13232
|
+
throw new Error("bootstrap bundle size is outside the supported boundary");
|
|
13233
|
+
}
|
|
13234
|
+
if (typeof source.createdAt !== "string" || Number.isNaN(Date.parse(source.createdAt))) {
|
|
13235
|
+
throw new Error("bootstrap bundle creation time is malformed");
|
|
13236
|
+
}
|
|
13237
|
+
return source;
|
|
13238
|
+
}
|
|
13239
|
+
function ownerRegularFile(path, maximum, label) {
|
|
13240
|
+
if (!isAbsolute2(path) || resolve6(path) !== path || /[\r\n\0]/.test(path)) {
|
|
13241
|
+
throw new Error(`${label} path must be canonical and absolute`);
|
|
13242
|
+
}
|
|
13243
|
+
const value = lstatSync4(path);
|
|
13244
|
+
const uid = process.getuid?.() ?? value.uid;
|
|
13245
|
+
if (!value.isFile() || value.isSymbolicLink() || value.uid !== uid || value.nlink !== 1 || (value.mode & 63) !== 0 || value.size < 1 || value.size > maximum) {
|
|
13246
|
+
throw new Error(`${label} must be an owner-only regular file with one link and at most ${maximum} bytes`);
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
13249
|
+
async function readBootstrapBundle(bundlePath, manifestPath = `${bundlePath}.json`) {
|
|
13250
|
+
ownerRegularFile(bundlePath, MAX_BOOTSTRAP_BUNDLE_BYTES, "bootstrap bundle");
|
|
13251
|
+
ownerRegularFile(manifestPath, 16 * 1024, "bootstrap bundle manifest");
|
|
13252
|
+
let parsed;
|
|
13253
|
+
try {
|
|
13254
|
+
parsed = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
13255
|
+
} catch {
|
|
13256
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13257
|
+
}
|
|
13258
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13259
|
+
const size = lstatSync4(bundlePath).size;
|
|
13260
|
+
if (size !== manifest.bytes || await sha256File(bundlePath) !== manifest.sha256) {
|
|
13261
|
+
throw new Error("bootstrap bundle bytes do not match their manifest");
|
|
13262
|
+
}
|
|
13263
|
+
return { bundlePath, manifestPath, manifest };
|
|
13264
|
+
}
|
|
13265
|
+
async function buildBootstrapBundle(input, exec = run) {
|
|
13266
|
+
const repositoryRoot = resolve6(input.repositoryRoot);
|
|
13267
|
+
const outputPath = resolve6(input.outputPath);
|
|
13268
|
+
const manifestPath = `${outputPath}.json`;
|
|
13269
|
+
const branch = branchName(input.branch);
|
|
13270
|
+
if (!isAbsolute2(input.outputPath) || outputPath !== input.outputPath) {
|
|
13271
|
+
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
13272
|
+
}
|
|
13273
|
+
if (existsSync8(outputPath) || existsSync8(manifestPath)) {
|
|
13274
|
+
throw new Error("bootstrap bundle output already exists");
|
|
13275
|
+
}
|
|
13276
|
+
const root = lstatSync4(repositoryRoot);
|
|
13277
|
+
if (!root.isDirectory() || root.isSymbolicLink())
|
|
13278
|
+
throw new Error("bootstrap bundle source must be a real directory");
|
|
13279
|
+
mkdirSync8(dirname8(outputPath), { recursive: true, mode: 448 });
|
|
13280
|
+
const parent = lstatSync4(dirname8(outputPath));
|
|
13281
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
13282
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
13283
|
+
throw new Error("bootstrap bundle output directory must be caller-owned and owner-only");
|
|
13284
|
+
}
|
|
13285
|
+
const dirty = await checked2(exec, ["git", "-C", repositoryRoot, "status", "--porcelain=v1", "--untracked-files=no"], "Git worktree check");
|
|
13286
|
+
if (dirty)
|
|
13287
|
+
throw new Error("bootstrap bundle source has tracked changes; commit the reviewed API release first");
|
|
13288
|
+
const revision = (await checked2(exec, ["git", "-C", repositoryRoot, "rev-parse", "--verify", `refs/heads/${branch}^{commit}`], "Git branch resolution")).toLowerCase();
|
|
13289
|
+
if (!/^[a-f0-9]{40}$/.test(revision))
|
|
13290
|
+
throw new Error("bootstrap bundle branch did not resolve to one exact commit");
|
|
13291
|
+
const temporary = `${outputPath}.next.${process.pid}.${randomBytes6(6).toString("hex")}`;
|
|
13292
|
+
try {
|
|
13293
|
+
await checked2(exec, ["git", "-C", repositoryRoot, "bundle", "create", temporary, `refs/heads/${branch}`], "Git bundle creation");
|
|
13294
|
+
chmodSync3(temporary, 384);
|
|
13295
|
+
const file = lstatSync4(temporary);
|
|
13296
|
+
if (!file.isFile() || file.isSymbolicLink() || file.size < 1 || file.size > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
13297
|
+
throw new Error("created bootstrap bundle is outside the supported boundary");
|
|
13298
|
+
}
|
|
13299
|
+
await checked2(exec, ["git", "bundle", "verify", temporary], "Git bundle verification");
|
|
13300
|
+
const manifest = {
|
|
13301
|
+
format: BOOTSTRAP_BUNDLE_FORMAT,
|
|
13302
|
+
kind: BOOTSTRAP_BUNDLE_KIND,
|
|
13303
|
+
branch,
|
|
13304
|
+
revision,
|
|
13305
|
+
sha256: await sha256File(temporary),
|
|
13306
|
+
bytes: file.size,
|
|
13307
|
+
createdAt: new Date().toISOString()
|
|
13308
|
+
};
|
|
13309
|
+
const manifestTemporary = `${manifestPath}.next.${process.pid}.${randomBytes6(6).toString("hex")}`;
|
|
13310
|
+
writeFileSync8(manifestTemporary, `${JSON.stringify(manifest, null, 2)}
|
|
13311
|
+
`, { mode: 384, flag: "wx" });
|
|
13312
|
+
renameSync7(temporary, outputPath);
|
|
13313
|
+
renameSync7(manifestTemporary, manifestPath);
|
|
13314
|
+
return { bundlePath: outputPath, manifestPath, manifest };
|
|
13315
|
+
} catch (cause) {
|
|
13316
|
+
rmSync5(temporary, { force: true });
|
|
13317
|
+
throw cause;
|
|
13318
|
+
}
|
|
13319
|
+
}
|
|
13320
|
+
|
|
13321
|
+
// src/bootstrap.ts
|
|
13161
13322
|
var PLATFORM_BOOTSTRAP_PROFILES = [
|
|
13162
13323
|
"platform-db-api",
|
|
13163
13324
|
"platform-api"
|
|
@@ -13257,7 +13418,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
|
|
|
13257
13418
|
var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
13258
13419
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
13259
13420
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
13260
|
-
var
|
|
13421
|
+
var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
|
|
13261
13422
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
13262
13423
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
13263
13424
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -13330,6 +13491,9 @@ function validateBootstrapConfig(value) {
|
|
|
13330
13491
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
13331
13492
|
}
|
|
13332
13493
|
if (value.kind === "enrolled-compute") {
|
|
13494
|
+
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
13495
|
+
throw new Error("gitDeployKey must be boolean");
|
|
13496
|
+
}
|
|
13333
13497
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
|
|
13334
13498
|
throw new Error("tenant realm is malformed");
|
|
13335
13499
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
@@ -13355,6 +13519,9 @@ function validateBootstrapConfig(value) {
|
|
|
13355
13519
|
throw new Error("unsupported platform software profile");
|
|
13356
13520
|
if (!["production", "development"].includes(value.environment))
|
|
13357
13521
|
throw new Error("platform environment must be production or development");
|
|
13522
|
+
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
13523
|
+
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
13524
|
+
}
|
|
13358
13525
|
let api;
|
|
13359
13526
|
try {
|
|
13360
13527
|
api = new URL(value.apiUrl);
|
|
@@ -13387,7 +13554,7 @@ function validateBootstrapConfig(value) {
|
|
|
13387
13554
|
if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
13388
13555
|
throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
13389
13556
|
}
|
|
13390
|
-
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.
|
|
13557
|
+
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
13391
13558
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
13392
13559
|
}
|
|
13393
13560
|
if (runtime.deployProfile !== value.environment)
|
|
@@ -13417,6 +13584,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
13417
13584
|
] : [
|
|
13418
13585
|
...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
|
|
13419
13586
|
{ id: "bun", version: "1.3.14" },
|
|
13587
|
+
{ id: "git", version: "ubuntu-26.04" },
|
|
13420
13588
|
{ id: "nginx", version: "ubuntu-26.04" },
|
|
13421
13589
|
...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
|
|
13422
13590
|
...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
|
|
@@ -13445,7 +13613,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
13445
13613
|
]
|
|
13446
13614
|
};
|
|
13447
13615
|
}
|
|
13448
|
-
var
|
|
13616
|
+
var checked3 = async (host, argv2, label, options) => {
|
|
13449
13617
|
const result = await host.exec(argv2, options);
|
|
13450
13618
|
if (result.exitCode !== 0)
|
|
13451
13619
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
@@ -13626,6 +13794,37 @@ async function seal(host, name, destination2, value) {
|
|
|
13626
13794
|
if (result.exitCode !== 0)
|
|
13627
13795
|
throw new Error(`could not seal ${name}: ${result.output.trim()}`);
|
|
13628
13796
|
}
|
|
13797
|
+
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
13798
|
+
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
13799
|
+
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
13800
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
13801
|
+
throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
|
|
13802
|
+
}
|
|
13803
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
13804
|
+
throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
|
|
13805
|
+
}
|
|
13806
|
+
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
13807
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
13808
|
+
}
|
|
13809
|
+
let parsed;
|
|
13810
|
+
try {
|
|
13811
|
+
parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
|
|
13812
|
+
} catch {
|
|
13813
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
13814
|
+
}
|
|
13815
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
13816
|
+
const expectedBranch = config.environment === "production" ? "main" : "dev";
|
|
13817
|
+
if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
13818
|
+
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
13819
|
+
}
|
|
13820
|
+
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
13821
|
+
if (digest !== manifest.sha256)
|
|
13822
|
+
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
13823
|
+
if (verifyGit) {
|
|
13824
|
+
await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
|
|
13825
|
+
}
|
|
13826
|
+
return manifest;
|
|
13827
|
+
}
|
|
13629
13828
|
function bootstrapIdentity(config) {
|
|
13630
13829
|
if (config.kind === "enrolled-compute")
|
|
13631
13830
|
return {
|
|
@@ -13652,8 +13851,7 @@ function bootstrapIdentity(config) {
|
|
|
13652
13851
|
computeReference: config.computeReference,
|
|
13653
13852
|
nodeHostname: config.nodeHostname,
|
|
13654
13853
|
apiUrl: config.apiUrl,
|
|
13655
|
-
|
|
13656
|
-
branch: config.branch,
|
|
13854
|
+
bootstrapBundle: config.bootstrapBundle,
|
|
13657
13855
|
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
13658
13856
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
13659
13857
|
database: {
|
|
@@ -13683,7 +13881,7 @@ function bootstrapIdentity(config) {
|
|
|
13683
13881
|
};
|
|
13684
13882
|
}
|
|
13685
13883
|
function bootstrapIdentityDigest(config) {
|
|
13686
|
-
return
|
|
13884
|
+
return createHash4("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
13687
13885
|
}
|
|
13688
13886
|
function parseStoredState(raw) {
|
|
13689
13887
|
let value;
|
|
@@ -13733,34 +13931,6 @@ function bindBootstrapIntent(host, config) {
|
|
|
13733
13931
|
}
|
|
13734
13932
|
return identityDigest;
|
|
13735
13933
|
}
|
|
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
13934
|
function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
13765
13935
|
return `${JSON.stringify({
|
|
13766
13936
|
format: 2,
|
|
@@ -13914,6 +14084,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
13914
14084
|
let installed;
|
|
13915
14085
|
if (host.exists(STATE_PATH))
|
|
13916
14086
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
14087
|
+
const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
13917
14088
|
let cloudflare;
|
|
13918
14089
|
if (config.cloudflareHandoff) {
|
|
13919
14090
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -13971,14 +14142,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
13971
14142
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
13972
14143
|
if (!secrets)
|
|
13973
14144
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
13974
|
-
const
|
|
14145
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets);
|
|
13975
14146
|
return {
|
|
13976
|
-
root:
|
|
13977
|
-
email:
|
|
13978
|
-
enrolmentToken:
|
|
13979
|
-
backup:
|
|
13980
|
-
cloudflareTunnelToken:
|
|
13981
|
-
cloudflareApiToken:
|
|
14147
|
+
root: checked4.clusterBootstrapCode,
|
|
14148
|
+
email: checked4.emailSecret,
|
|
14149
|
+
enrolmentToken: checked4.enrolmentToken,
|
|
14150
|
+
backup: checked4.backupS3Secret,
|
|
14151
|
+
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
14152
|
+
cloudflareApiToken: checked4.cloudflareApiToken
|
|
13982
14153
|
};
|
|
13983
14154
|
})() : undefined;
|
|
13984
14155
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -14023,18 +14194,21 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14023
14194
|
} else
|
|
14024
14195
|
await host.ensureSoftware(plan.software);
|
|
14025
14196
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14026
|
-
await
|
|
14027
|
-
await
|
|
14028
|
-
await
|
|
14197
|
+
await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
|
|
14198
|
+
await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
|
|
14199
|
+
await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
|
|
14029
14200
|
for (const cidr of config.firewall.privateCidrs) {
|
|
14030
14201
|
if (config.database.role !== "none")
|
|
14031
|
-
await
|
|
14202
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
|
|
14032
14203
|
for (const port of [config.runtime.bluePort, config.runtime.greenPort])
|
|
14033
|
-
await
|
|
14204
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
|
|
14034
14205
|
}
|
|
14035
|
-
await
|
|
14206
|
+
await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
|
|
14036
14207
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 !== "ufw"));
|
|
14037
14208
|
}
|
|
14209
|
+
if (config.kind === "platform" && bootstrapManifest) {
|
|
14210
|
+
await verifyBootstrapBundleOnHost(host, config, true);
|
|
14211
|
+
}
|
|
14038
14212
|
if (config.kind === "platform") {
|
|
14039
14213
|
const root = platformPrivate.root;
|
|
14040
14214
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
@@ -14089,8 +14263,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14089
14263
|
keepReleases: runtime.keepReleases,
|
|
14090
14264
|
drainDeadlineMs: runtime.environment.drainDeadlineMs
|
|
14091
14265
|
});
|
|
14092
|
-
await
|
|
14093
|
-
await
|
|
14266
|
+
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
14267
|
+
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
14094
14268
|
});
|
|
14095
14269
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
14096
14270
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
@@ -14099,7 +14273,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14099
14273
|
host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
|
|
14100
14274
|
`, 420);
|
|
14101
14275
|
}
|
|
14102
|
-
await
|
|
14276
|
+
await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
|
|
14103
14277
|
host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
|
|
14104
14278
|
host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
|
|
14105
14279
|
host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
|
|
@@ -14108,30 +14282,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14108
14282
|
host.write("/etc/forgezero/deploy.env", activation.environment, 420);
|
|
14109
14283
|
host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
|
|
14110
14284
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
14111
|
-
await
|
|
14285
|
+
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
14112
14286
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
14113
|
-
await
|
|
14114
|
-
const otlpStatus = (await
|
|
14287
|
+
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
14288
|
+
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
14115
14289
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
14116
14290
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
14117
|
-
await
|
|
14118
|
-
await
|
|
14119
|
-
await
|
|
14291
|
+
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
14292
|
+
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
14293
|
+
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
14120
14294
|
if (config.database.role === "master") {
|
|
14121
14295
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
14122
14296
|
if (!host.exists(invite)) {
|
|
14123
|
-
host.write(invite, `plt_${
|
|
14297
|
+
host.write(invite, `plt_${randomBytes7(24).toString("hex")}
|
|
14124
14298
|
`, 384);
|
|
14125
|
-
await
|
|
14299
|
+
await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
14126
14300
|
}
|
|
14127
14301
|
}
|
|
14128
14302
|
if (config.database.role !== "none") {
|
|
14129
14303
|
host.mkdir("/var/lib/forgezero-cluster", 448);
|
|
14130
|
-
await
|
|
14304
|
+
await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
|
|
14131
14305
|
host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
|
|
14132
14306
|
host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
|
|
14133
|
-
await
|
|
14134
|
-
await
|
|
14307
|
+
await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
|
|
14308
|
+
await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
|
|
14135
14309
|
const evidence = {
|
|
14136
14310
|
expectedMode: "default",
|
|
14137
14311
|
role: "COORDINATOR",
|
|
@@ -14142,15 +14316,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14142
14316
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
14143
14317
|
`, 384);
|
|
14144
14318
|
}
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14319
|
+
if (bootstrapManifest) {
|
|
14320
|
+
await checked3(host, [
|
|
14321
|
+
"runuser",
|
|
14322
|
+
"-u",
|
|
14323
|
+
"forgezero-agent",
|
|
14324
|
+
"--",
|
|
14325
|
+
"/usr/local/bin/fz-agent",
|
|
14326
|
+
"deploy",
|
|
14327
|
+
`--revision=${bootstrapManifest.revision}`,
|
|
14328
|
+
...config.database.role === "master" ? ["--release-executor"] : []
|
|
14329
|
+
], "initial Agent deployment");
|
|
14330
|
+
host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
|
|
14331
|
+
format: 1,
|
|
14332
|
+
kind: "forgezero-bootstrap-release",
|
|
14333
|
+
revision: bootstrapManifest.revision,
|
|
14334
|
+
sha256: bootstrapManifest.sha256,
|
|
14335
|
+
branch: bootstrapManifest.branch,
|
|
14336
|
+
deployedAt: new Date().toISOString()
|
|
14337
|
+
}, null, 2)}
|
|
14338
|
+
`, 384);
|
|
14339
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
14340
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
14341
|
+
await host.installAgent(config);
|
|
14342
|
+
}
|
|
14154
14343
|
}
|
|
14155
14344
|
if (config.cloudflareHandoff) {
|
|
14156
14345
|
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
@@ -14159,8 +14348,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14159
14348
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
14160
14349
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
14161
14350
|
host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
|
|
14162
|
-
await
|
|
14163
|
-
await
|
|
14351
|
+
await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
|
|
14352
|
+
await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
|
|
14164
14353
|
const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
|
|
14165
14354
|
if (!tunnelId)
|
|
14166
14355
|
throw new Error("Cloudflare tunnel identity is missing after handoff validation");
|
|
@@ -14170,8 +14359,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14170
14359
|
if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
|
|
14171
14360
|
throw new Error("sealed Cloudflare Mesh connector credential is missing");
|
|
14172
14361
|
host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
|
|
14173
|
-
await
|
|
14174
|
-
await
|
|
14362
|
+
await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
|
|
14363
|
+
await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
|
|
14175
14364
|
}
|
|
14176
14365
|
host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
|
|
14177
14366
|
const status = await bootstrapStatus(host);
|
|
@@ -14216,8 +14405,7 @@ function strictBootstrapDocument(value) {
|
|
|
14216
14405
|
"computeReference",
|
|
14217
14406
|
"nodeHostname",
|
|
14218
14407
|
"apiUrl",
|
|
14219
|
-
"
|
|
14220
|
-
"branch",
|
|
14408
|
+
"bootstrapBundle",
|
|
14221
14409
|
"deployRoot",
|
|
14222
14410
|
"telemetryEndpoint",
|
|
14223
14411
|
"database",
|
|
@@ -14230,9 +14418,11 @@ function strictBootstrapDocument(value) {
|
|
|
14230
14418
|
"realm",
|
|
14231
14419
|
"software",
|
|
14232
14420
|
"deploymentCredentials",
|
|
14421
|
+
"gitDeployKey",
|
|
14233
14422
|
"bootstrapRunner"
|
|
14234
14423
|
], "bootstrap config");
|
|
14235
14424
|
if (root.kind === "platform") {
|
|
14425
|
+
exactKeys2(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
14236
14426
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14237
14427
|
if (root.cloudflareHandoff !== undefined)
|
|
14238
14428
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -14274,8 +14464,6 @@ function strictBootstrapDocument(value) {
|
|
|
14274
14464
|
"agentOtlpEndpoint",
|
|
14275
14465
|
"custodianEmail",
|
|
14276
14466
|
"email",
|
|
14277
|
-
"repository",
|
|
14278
|
-
"branch",
|
|
14279
14467
|
"deployProfile",
|
|
14280
14468
|
"otlpFlushIntervalMs",
|
|
14281
14469
|
"otlpTraceSampleRatio",
|
|
@@ -14313,11 +14501,11 @@ function strictBootstrapDocument(value) {
|
|
|
14313
14501
|
return value;
|
|
14314
14502
|
}
|
|
14315
14503
|
function readBootstrapConfig(path) {
|
|
14316
|
-
const metadata =
|
|
14504
|
+
const metadata = lstatSync5(path);
|
|
14317
14505
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
14318
14506
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
14319
14507
|
}
|
|
14320
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
14508
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync9(path, "utf8"))));
|
|
14321
14509
|
}
|
|
14322
14510
|
function localBootstrapHost() {
|
|
14323
14511
|
const execute = async (argv2, options = {}) => {
|
|
@@ -14335,19 +14523,19 @@ function localBootstrapHost() {
|
|
|
14335
14523
|
};
|
|
14336
14524
|
return {
|
|
14337
14525
|
uid: () => process.getuid?.() ?? -1,
|
|
14338
|
-
exists:
|
|
14339
|
-
read: (path) =>
|
|
14526
|
+
exists: existsSync9,
|
|
14527
|
+
read: (path) => readFileSync9(path, "utf8"),
|
|
14340
14528
|
write(path, content, mode) {
|
|
14341
|
-
|
|
14529
|
+
mkdirSync9(dirname9(path), { recursive: true, mode: 493 });
|
|
14342
14530
|
const temporary = `${path}.next.${process.pid}`;
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14531
|
+
writeFileSync9(temporary, content, { mode });
|
|
14532
|
+
chmodSync4(temporary, mode);
|
|
14533
|
+
renameSync8(temporary, path);
|
|
14346
14534
|
},
|
|
14347
|
-
mkdir: (path, mode) =>
|
|
14348
|
-
remove: (path) =>
|
|
14535
|
+
mkdir: (path, mode) => mkdirSync9(path, { recursive: true, mode }),
|
|
14536
|
+
remove: (path) => rmSync6(path, { force: true }),
|
|
14349
14537
|
inspect(path) {
|
|
14350
|
-
const value =
|
|
14538
|
+
const value = lstatSync5(path);
|
|
14351
14539
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
14352
14540
|
},
|
|
14353
14541
|
exec: execute,
|
|
@@ -14369,7 +14557,12 @@ function localBootstrapHost() {
|
|
|
14369
14557
|
async installAgent(config) {
|
|
14370
14558
|
const capabilities = await readCapabilities(localRunner);
|
|
14371
14559
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
14372
|
-
const hasBinding =
|
|
14560
|
+
const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
|
|
14561
|
+
const initialBundle = config.kind === "platform" && !existsSync9(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
14562
|
+
path: config.bootstrapBundle.bundleFile,
|
|
14563
|
+
manifestPath: config.bootstrapBundle.manifestFile,
|
|
14564
|
+
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
|
|
14565
|
+
} : undefined;
|
|
14373
14566
|
if (config.kind === "platform") {
|
|
14374
14567
|
const lifecycle = config.database.role === "none" ? {
|
|
14375
14568
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -14381,8 +14574,8 @@ function localBootstrapHost() {
|
|
|
14381
14574
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
14382
14575
|
databasePorts: [8529]
|
|
14383
14576
|
};
|
|
14384
|
-
|
|
14385
|
-
|
|
14577
|
+
mkdirSync9(dirname9(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
14578
|
+
writeFileSync9(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
14386
14579
|
`, { mode: 256 });
|
|
14387
14580
|
}
|
|
14388
14581
|
const plan = planInstall({
|
|
@@ -14390,15 +14583,17 @@ function localBootstrapHost() {
|
|
|
14390
14583
|
socketPath: DEFAULT_SOCKET,
|
|
14391
14584
|
seedPath: "/var/lib/forgezero/node.seed",
|
|
14392
14585
|
controlSocketPath: "/run/forgezero/control.sock",
|
|
14393
|
-
repository: config.repository,
|
|
14394
|
-
branch: config.branch,
|
|
14586
|
+
repository: initialBundle?.path ?? (config.kind === "enrolled-compute" ? config.repository : undefined),
|
|
14587
|
+
branch: initialBundle?.manifest.branch ?? (config.kind === "enrolled-compute" ? config.branch : undefined),
|
|
14588
|
+
bootstrapBundlePath: initialBundle?.path,
|
|
14589
|
+
bootstrapBundleManifestPath: initialBundle?.manifestPath,
|
|
14395
14590
|
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
14396
14591
|
deployRoot,
|
|
14397
14592
|
deploymentCredentials: config.deploymentCredentials,
|
|
14398
14593
|
publicApiUrl: config.apiUrl,
|
|
14399
|
-
gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
|
|
14400
|
-
gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
|
|
14401
|
-
generateGitIdentity: true,
|
|
14594
|
+
gitCredentialPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
14595
|
+
gitPublicKeyPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
14596
|
+
generateGitIdentity: config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
14402
14597
|
pullDeployments: hasBinding,
|
|
14403
14598
|
pullMigrations: config.kind === "platform" && hasBinding,
|
|
14404
14599
|
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
@@ -14412,9 +14607,11 @@ function localBootstrapHost() {
|
|
|
14412
14607
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
14413
14608
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14414
14609
|
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14610
|
+
...existsSync9(ENROL_CREDENTIAL) || hasBinding ? {
|
|
14611
|
+
enrolTokenCredentialPath: existsSync9(ENROL_CREDENTIAL) ? ENROL_CREDENTIAL : undefined,
|
|
14612
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
14613
|
+
} : {},
|
|
14415
14614
|
...hasBinding ? {
|
|
14416
|
-
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14417
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
14418
14615
|
apiUrl: config.apiUrl,
|
|
14419
14616
|
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14420
14617
|
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
@@ -14422,8 +14619,8 @@ function localBootstrapHost() {
|
|
|
14422
14619
|
} : {}
|
|
14423
14620
|
});
|
|
14424
14621
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
14425
|
-
|
|
14426
|
-
|
|
14622
|
+
mkdirSync9(dirname9(unit.path), { recursive: true, mode: 493 });
|
|
14623
|
+
writeFileSync9(unit.path, unit.unit, { mode: 420 });
|
|
14427
14624
|
}
|
|
14428
14625
|
await applyPlan(plan, localRunner);
|
|
14429
14626
|
return plan;
|
|
@@ -14432,8 +14629,8 @@ function localBootstrapHost() {
|
|
|
14432
14629
|
}
|
|
14433
14630
|
|
|
14434
14631
|
// src/cli/cloudflare-bootstrap.ts
|
|
14435
|
-
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as
|
|
14436
|
-
import { dirname as
|
|
14632
|
+
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync10 } from "fs";
|
|
14633
|
+
import { dirname as dirname10, resolve as resolve7 } from "path";
|
|
14437
14634
|
async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
|
|
14438
14635
|
return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
|
|
14439
14636
|
zoneName: input.zoneName,
|
|
@@ -14464,7 +14661,7 @@ function createCloudflareBootstrapCommandConfig(input) {
|
|
|
14464
14661
|
};
|
|
14465
14662
|
}
|
|
14466
14663
|
function readOwnerConfig(path) {
|
|
14467
|
-
const absolute2 =
|
|
14664
|
+
const absolute2 = resolve7(path);
|
|
14468
14665
|
let descriptor;
|
|
14469
14666
|
try {
|
|
14470
14667
|
descriptor = openSync(absolute2, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
@@ -14473,7 +14670,7 @@ function readOwnerConfig(path) {
|
|
|
14473
14670
|
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
|
|
14474
14671
|
throw new Error(`${absolute2} must be one operator-owned 0600 regular file`);
|
|
14475
14672
|
}
|
|
14476
|
-
return JSON.parse(
|
|
14673
|
+
return JSON.parse(readFileSync10(descriptor, "utf8"));
|
|
14477
14674
|
} catch (cause) {
|
|
14478
14675
|
if (cause instanceof SyntaxError)
|
|
14479
14676
|
throw new Error(`${absolute2} is not valid Cloudflare bootstrap JSON`);
|
|
@@ -14487,7 +14684,7 @@ function readOwnerConfig(path) {
|
|
|
14487
14684
|
}
|
|
14488
14685
|
function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
14489
14686
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
|
|
14490
|
-
const baseDirectory =
|
|
14687
|
+
const baseDirectory = dirname10(resolve7(path));
|
|
14491
14688
|
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"], "Cloudflare bootstrap config");
|
|
14492
14689
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
|
|
14493
14690
|
throw new Error("Cloudflare bootstrap config format/kind is invalid");
|
|
@@ -14527,7 +14724,7 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
14527
14724
|
return {
|
|
14528
14725
|
mode,
|
|
14529
14726
|
coordinates,
|
|
14530
|
-
checkpointPath:
|
|
14727
|
+
checkpointPath: resolve7(baseDirectory, input.checkpointPath)
|
|
14531
14728
|
};
|
|
14532
14729
|
}
|
|
14533
14730
|
async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
|
|
@@ -14540,14 +14737,14 @@ async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependen
|
|
|
14540
14737
|
return evidence;
|
|
14541
14738
|
}
|
|
14542
14739
|
async function runCloudflareBootstrapVerificationCommand(checkpointPath, dependencies = {}) {
|
|
14543
|
-
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(
|
|
14740
|
+
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(resolve7(checkpointPath));
|
|
14544
14741
|
(dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
|
|
14545
14742
|
`);
|
|
14546
14743
|
return evidence;
|
|
14547
14744
|
}
|
|
14548
14745
|
function readCloudflareBootstrapFinalizeConfig(path) {
|
|
14549
14746
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap finalize config");
|
|
14550
|
-
const baseDirectory =
|
|
14747
|
+
const baseDirectory = dirname10(resolve7(path));
|
|
14551
14748
|
exactKeys3(input, ["format", "kind", "checkpointPath", "acceptancePath"], "Cloudflare bootstrap finalize config");
|
|
14552
14749
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-finalize") {
|
|
14553
14750
|
throw new Error("Cloudflare bootstrap finalize config format/kind is invalid");
|
|
@@ -14558,8 +14755,8 @@ function readCloudflareBootstrapFinalizeConfig(path) {
|
|
|
14558
14755
|
}
|
|
14559
14756
|
}
|
|
14560
14757
|
return {
|
|
14561
|
-
checkpointPath:
|
|
14562
|
-
acceptancePath:
|
|
14758
|
+
checkpointPath: resolve7(baseDirectory, input.checkpointPath),
|
|
14759
|
+
acceptancePath: resolve7(baseDirectory, input.acceptancePath)
|
|
14563
14760
|
};
|
|
14564
14761
|
}
|
|
14565
14762
|
async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies = {}) {
|
|
@@ -14571,31 +14768,31 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
14571
14768
|
}
|
|
14572
14769
|
|
|
14573
14770
|
// src/metal-bootstrap.ts
|
|
14574
|
-
import { createHash as
|
|
14771
|
+
import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
|
|
14575
14772
|
import {
|
|
14576
|
-
chmodSync as
|
|
14773
|
+
chmodSync as chmodSync5,
|
|
14577
14774
|
chownSync,
|
|
14578
14775
|
copyFileSync as copyFileSync2,
|
|
14579
|
-
existsSync as
|
|
14580
|
-
lstatSync as
|
|
14581
|
-
mkdirSync as
|
|
14582
|
-
readFileSync as
|
|
14776
|
+
existsSync as existsSync10,
|
|
14777
|
+
lstatSync as lstatSync6,
|
|
14778
|
+
mkdirSync as mkdirSync11,
|
|
14779
|
+
readFileSync as readFileSync11,
|
|
14583
14780
|
realpathSync as realpathSync4,
|
|
14584
|
-
renameSync as
|
|
14781
|
+
renameSync as renameSync9,
|
|
14585
14782
|
statSync,
|
|
14586
14783
|
symlinkSync as symlinkSync2,
|
|
14587
14784
|
unlinkSync as unlinkSync2,
|
|
14588
|
-
writeFileSync as
|
|
14785
|
+
writeFileSync as writeFileSync11
|
|
14589
14786
|
} from "fs";
|
|
14590
|
-
import { dirname as
|
|
14787
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, join as join9, resolve as resolve8 } from "path";
|
|
14591
14788
|
import { isIP as isIP5 } from "net";
|
|
14592
14789
|
|
|
14593
14790
|
// src/metal-isolation.ts
|
|
14594
|
-
import { mkdirSync as
|
|
14791
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
14595
14792
|
import { join as join8 } from "path";
|
|
14596
14793
|
|
|
14597
14794
|
// src/metal-provision.ts
|
|
14598
|
-
import { dirname as
|
|
14795
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join7 } from "path";
|
|
14599
14796
|
import { isIP as isIP4 } from "net";
|
|
14600
14797
|
|
|
14601
14798
|
// src/ubuntu.ts
|
|
@@ -14644,7 +14841,7 @@ function validateMetalProfile(profile) {
|
|
|
14644
14841
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
14645
14842
|
throw new MetalProvisionError("invalid guest address range");
|
|
14646
14843
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
14647
|
-
if (!
|
|
14844
|
+
if (!isAbsolute3(path))
|
|
14648
14845
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
14649
14846
|
}
|
|
14650
14847
|
new URL(profile.apiUrl);
|
|
@@ -14752,14 +14949,14 @@ var defaultExec = async (argv2) => {
|
|
|
14752
14949
|
]);
|
|
14753
14950
|
return { exitCode, stdout, stderr };
|
|
14754
14951
|
};
|
|
14755
|
-
var
|
|
14952
|
+
var checked4 = async (exec, argv2) => {
|
|
14756
14953
|
const result = await exec(argv2);
|
|
14757
14954
|
if (result.exitCode !== 0)
|
|
14758
14955
|
throw new Error(`${argv2[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
14759
14956
|
return result;
|
|
14760
14957
|
};
|
|
14761
14958
|
var requireGuestsInSlice = async (exec) => {
|
|
14762
|
-
const active = await
|
|
14959
|
+
const active = await checked4(exec, [
|
|
14763
14960
|
"systemctl",
|
|
14764
14961
|
"list-units",
|
|
14765
14962
|
"--type=service",
|
|
@@ -14773,7 +14970,7 @@ var requireGuestsInSlice = async (exec) => {
|
|
|
14773
14970
|
const service = line.trim().split(/\s+/)[0];
|
|
14774
14971
|
if (!service)
|
|
14775
14972
|
continue;
|
|
14776
|
-
const cgroup = await
|
|
14973
|
+
const cgroup = await checked4(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
|
|
14777
14974
|
if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
|
|
14778
14975
|
throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
|
|
14779
14976
|
}
|
|
@@ -14783,23 +14980,23 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
14783
14980
|
validateMetalProfile(profile);
|
|
14784
14981
|
await requireGuestsInSlice(exec);
|
|
14785
14982
|
const unitDir = profile.unitDir;
|
|
14786
|
-
|
|
14787
|
-
|
|
14983
|
+
mkdirSync10(unitDir, { recursive: true });
|
|
14984
|
+
writeFileSync10(join8(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
14788
14985
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
14789
14986
|
const directory = join8(unitDir, `${unit}.d`);
|
|
14790
|
-
|
|
14791
|
-
|
|
14987
|
+
mkdirSync10(directory, { recursive: true });
|
|
14988
|
+
writeFileSync10(join8(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
14792
14989
|
}
|
|
14793
14990
|
const initDirectory = join8(unitDir, "init.scope.d");
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
await
|
|
14991
|
+
mkdirSync10(initDirectory, { recursive: true });
|
|
14992
|
+
writeFileSync10(join8(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
14993
|
+
await checked4(exec, ["systemctl", "daemon-reload"]);
|
|
14797
14994
|
await requireGuestsInSlice(exec);
|
|
14798
14995
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
14799
14996
|
if (profile.housekeepingMemoryNodes)
|
|
14800
14997
|
properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
|
|
14801
14998
|
for (const unit of ["system.slice", "user.slice", "init.scope"]) {
|
|
14802
|
-
await
|
|
14999
|
+
await checked4(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
|
|
14803
15000
|
}
|
|
14804
15001
|
}
|
|
14805
15002
|
|
|
@@ -14911,7 +15108,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
14911
15108
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
14912
15109
|
}
|
|
14913
15110
|
const image = config.profile.images[Object.keys(config.profile.images)[0]];
|
|
14914
|
-
if (!image.path.startsWith("/var/lib/forgezero/images/") ||
|
|
15111
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve8(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
14915
15112
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
14916
15113
|
}
|
|
14917
15114
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
|
|
@@ -14946,10 +15143,10 @@ function validateMetalBootstrapConfig(config) {
|
|
|
14946
15143
|
return config;
|
|
14947
15144
|
}
|
|
14948
15145
|
function validateOwnerOnlyPath(path, requireRootOwner) {
|
|
14949
|
-
if (!
|
|
15146
|
+
if (!isAbsolute4(path) || resolve8(path) !== path || path.includes("/../")) {
|
|
14950
15147
|
throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
|
|
14951
15148
|
}
|
|
14952
|
-
const metadata =
|
|
15149
|
+
const metadata = lstatSync6(path);
|
|
14953
15150
|
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
|
|
14954
15151
|
throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
|
|
14955
15152
|
}
|
|
@@ -14967,7 +15164,7 @@ function readMetalBootstrapConfig(path) {
|
|
|
14967
15164
|
throw new MetalBootstrapError("metal bootstrap config size is invalid");
|
|
14968
15165
|
let parsed;
|
|
14969
15166
|
try {
|
|
14970
|
-
parsed = JSON.parse(
|
|
15167
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
14971
15168
|
} catch {
|
|
14972
15169
|
throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
|
|
14973
15170
|
}
|
|
@@ -15000,15 +15197,15 @@ function planMetalBootstrap(config) {
|
|
|
15000
15197
|
};
|
|
15001
15198
|
}
|
|
15002
15199
|
var atomicWrite2 = (path, body, mode) => {
|
|
15003
|
-
|
|
15200
|
+
mkdirSync11(dirname12(path), { recursive: true, mode: 493 });
|
|
15004
15201
|
const temporary = `${path}.next-${process.pid}`;
|
|
15005
|
-
|
|
15006
|
-
|
|
15202
|
+
writeFileSync11(temporary, body, { mode, flag: "wx" });
|
|
15203
|
+
chmodSync5(temporary, mode);
|
|
15007
15204
|
chownSync(temporary, 0, 0);
|
|
15008
|
-
|
|
15205
|
+
renameSync9(temporary, path);
|
|
15009
15206
|
};
|
|
15010
15207
|
var validateAgentSourcePath = (source) => {
|
|
15011
|
-
if (!
|
|
15208
|
+
if (!isAbsolute4(source) || !lstatSync6(source).isFile() || lstatSync6(source).isSymbolicLink()) {
|
|
15012
15209
|
throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
|
|
15013
15210
|
}
|
|
15014
15211
|
};
|
|
@@ -15172,11 +15369,11 @@ WantedBy=multi-user.target
|
|
|
15172
15369
|
var installAgentBinary = (source, version) => {
|
|
15173
15370
|
validateAgentSourcePath(source);
|
|
15174
15371
|
const release = `/opt/forgezero/agent/versions/${version}/dist`;
|
|
15175
|
-
|
|
15372
|
+
mkdirSync11(release, { recursive: true, mode: 493 });
|
|
15176
15373
|
copyFileSync2(source, join9(release, "fz-agent.js"));
|
|
15177
|
-
|
|
15374
|
+
chmodSync5(join9(release, "fz-agent.js"), 493);
|
|
15178
15375
|
chownSync(join9(release, "fz-agent.js"), 0, 0);
|
|
15179
|
-
|
|
15376
|
+
mkdirSync11("/opt/forgezero/agent", { recursive: true, mode: 493 });
|
|
15180
15377
|
for (const [link, target] of [
|
|
15181
15378
|
["/opt/forgezero/agent/current.next", `versions/${version}`],
|
|
15182
15379
|
[AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
|
|
@@ -15186,7 +15383,7 @@ var installAgentBinary = (source, version) => {
|
|
|
15186
15383
|
} catch {}
|
|
15187
15384
|
symlinkSync2(target, link);
|
|
15188
15385
|
if (link.endsWith("current.next"))
|
|
15189
|
-
|
|
15386
|
+
renameSync9(link, "/opt/forgezero/agent/current");
|
|
15190
15387
|
}
|
|
15191
15388
|
};
|
|
15192
15389
|
var preflight = async (config, exec) => {
|
|
@@ -15208,10 +15405,10 @@ var preflight = async (config, exec) => {
|
|
|
15208
15405
|
]);
|
|
15209
15406
|
await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
|
|
15210
15407
|
await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
|
|
15211
|
-
if (!
|
|
15408
|
+
if (!existsSync10("/dev/kvm"))
|
|
15212
15409
|
throw new MetalBootstrapError("/dev/kvm is required");
|
|
15213
15410
|
if (config.profile.confidential) {
|
|
15214
|
-
if (!
|
|
15411
|
+
if (!existsSync10("/dev/sev"))
|
|
15215
15412
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
15216
15413
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
15217
15414
|
}
|
|
@@ -15223,16 +15420,16 @@ var assertSupportedMetalHost = () => {
|
|
|
15223
15420
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
15224
15421
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
15225
15422
|
}
|
|
15226
|
-
const release =
|
|
15423
|
+
const release = readFileSync11("/etc/os-release", "utf8");
|
|
15227
15424
|
if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
|
|
15228
15425
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
15229
15426
|
}
|
|
15230
15427
|
};
|
|
15231
15428
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
15232
15429
|
const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
|
|
15233
|
-
if (
|
|
15430
|
+
if (existsSync10(image.path))
|
|
15234
15431
|
return;
|
|
15235
|
-
|
|
15432
|
+
mkdirSync11(dirname12(image.path), { recursive: true, mode: 493 });
|
|
15236
15433
|
const temporary = `${image.path}.next-${process.pid}`;
|
|
15237
15434
|
try {
|
|
15238
15435
|
await runChecked(exec, [
|
|
@@ -15249,9 +15446,9 @@ var ensurePinnedGuestImage = async (config, exec) => {
|
|
|
15249
15446
|
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
15250
15447
|
if (digest !== image.sha256)
|
|
15251
15448
|
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
15252
|
-
|
|
15449
|
+
chmodSync5(temporary, 292);
|
|
15253
15450
|
chownSync(temporary, 0, 0);
|
|
15254
|
-
|
|
15451
|
+
renameSync9(temporary, image.path);
|
|
15255
15452
|
} catch (cause) {
|
|
15256
15453
|
try {
|
|
15257
15454
|
unlinkSync2(temporary);
|
|
@@ -15264,11 +15461,11 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15264
15461
|
if ((options.getuid ?? process.getuid)?.() !== 0)
|
|
15265
15462
|
throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
|
|
15266
15463
|
assertSupportedMetalHost();
|
|
15267
|
-
if (
|
|
15464
|
+
if (existsSync10(STATE_PATH2) && !options.repair)
|
|
15268
15465
|
throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
|
|
15269
15466
|
if (config.agentSeedFile)
|
|
15270
15467
|
validateOwnerOnlyPath(config.agentSeedFile, true);
|
|
15271
|
-
if (config.agentSeedFile &&
|
|
15468
|
+
if (config.agentSeedFile && existsSync10(SEED_CREDENTIAL_PATH)) {
|
|
15272
15469
|
throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
|
|
15273
15470
|
}
|
|
15274
15471
|
validateAgentSourcePath(options.agentSourcePath);
|
|
@@ -15295,9 +15492,9 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15295
15492
|
await preflight(config, exec);
|
|
15296
15493
|
await ensureAccount(exec);
|
|
15297
15494
|
installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15495
|
+
mkdirSync11("/etc/forgezero/creds", { recursive: true, mode: 448 });
|
|
15496
|
+
mkdirSync11(config.profile.stateDir, { recursive: true, mode: 448 });
|
|
15497
|
+
mkdirSync11(config.profile.seedDir, { recursive: true, mode: 448 });
|
|
15301
15498
|
const persistedProfile = {
|
|
15302
15499
|
...config.profile,
|
|
15303
15500
|
metalHostname: config.metalHostname,
|
|
@@ -15306,13 +15503,13 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15306
15503
|
};
|
|
15307
15504
|
atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
15308
15505
|
`, 384);
|
|
15309
|
-
if (!
|
|
15310
|
-
const seed = config.agentSeedFile ?
|
|
15506
|
+
if (!existsSync10(SEED_CREDENTIAL_PATH)) {
|
|
15507
|
+
const seed = config.agentSeedFile ? readFileSync11(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
|
|
15311
15508
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
15312
15509
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
15313
15510
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
15314
15511
|
`);
|
|
15315
|
-
|
|
15512
|
+
chmodSync5(SEED_CREDENTIAL_PATH, 256);
|
|
15316
15513
|
chownSync(SEED_CREDENTIAL_PATH, 0, 0);
|
|
15317
15514
|
if (config.agentSeedFile)
|
|
15318
15515
|
unlinkSync2(config.agentSeedFile);
|
|
@@ -15361,7 +15558,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15361
15558
|
initializedAt: new Date().toISOString(),
|
|
15362
15559
|
role: "metal",
|
|
15363
15560
|
metalHostname: config.metalHostname,
|
|
15364
|
-
profileSha256:
|
|
15561
|
+
profileSha256: createHash5("sha256").update(JSON.stringify(config.profile)).digest("hex")
|
|
15365
15562
|
};
|
|
15366
15563
|
atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
|
|
15367
15564
|
`, 384);
|
|
@@ -15369,7 +15566,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15369
15566
|
}
|
|
15370
15567
|
var socketReady = (path) => {
|
|
15371
15568
|
try {
|
|
15372
|
-
return
|
|
15569
|
+
return lstatSync6(path).isSocket();
|
|
15373
15570
|
} catch {
|
|
15374
15571
|
return false;
|
|
15375
15572
|
}
|
|
@@ -15378,17 +15575,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15378
15575
|
const problems = [];
|
|
15379
15576
|
let profileValid = false, imageVerified = false, metalHostname, profileSha256;
|
|
15380
15577
|
let profileMode = null;
|
|
15381
|
-
if (
|
|
15578
|
+
if (existsSync10(PROFILE_PATH)) {
|
|
15382
15579
|
try {
|
|
15383
15580
|
const metadata = statSync(PROFILE_PATH);
|
|
15384
15581
|
profileMode = metadata.mode & 511;
|
|
15385
15582
|
if (profileMode !== 384 || metadata.uid !== 0)
|
|
15386
15583
|
problems.push("metal profile is not root-owned mode 0600");
|
|
15387
|
-
const persisted = JSON.parse(
|
|
15584
|
+
const persisted = JSON.parse(readFileSync11(PROFILE_PATH, "utf8"));
|
|
15388
15585
|
const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
|
|
15389
15586
|
validateMetalProfile(profile);
|
|
15390
15587
|
profileValid = true;
|
|
15391
|
-
profileSha256 =
|
|
15588
|
+
profileSha256 = createHash5("sha256").update(JSON.stringify(profile)).digest("hex");
|
|
15392
15589
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
|
|
15393
15590
|
problems.push("persisted metal host coordinates are invalid");
|
|
15394
15591
|
}
|
|
@@ -15417,7 +15614,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15417
15614
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
15418
15615
|
}
|
|
15419
15616
|
const image = profile.images[Object.keys(profile.images)[0]];
|
|
15420
|
-
if (
|
|
15617
|
+
if (existsSync10(image.path)) {
|
|
15421
15618
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
15422
15619
|
imageVerified = digest === image.sha256;
|
|
15423
15620
|
}
|
|
@@ -15428,9 +15625,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15428
15625
|
}
|
|
15429
15626
|
} else
|
|
15430
15627
|
problems.push("metal profile is missing");
|
|
15431
|
-
if (
|
|
15628
|
+
if (existsSync10(STATE_PATH2)) {
|
|
15432
15629
|
try {
|
|
15433
|
-
const state = JSON.parse(
|
|
15630
|
+
const state = JSON.parse(readFileSync11(STATE_PATH2, "utf8"));
|
|
15434
15631
|
metalHostname = state.metalHostname;
|
|
15435
15632
|
if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
|
|
15436
15633
|
problems.push("metal initialized state does not bind the current profile");
|
|
@@ -15446,7 +15643,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15446
15643
|
"forgezero-metal-agent-egress.service",
|
|
15447
15644
|
"forgezero-metal-agent.service"
|
|
15448
15645
|
]) {
|
|
15449
|
-
if (!
|
|
15646
|
+
if (!existsSync10(join9(UNIT_DIRECTORY, unit)))
|
|
15450
15647
|
units[unit] = "missing";
|
|
15451
15648
|
else
|
|
15452
15649
|
units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
|
|
@@ -15460,7 +15657,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15460
15657
|
if (!updateSocketReady)
|
|
15461
15658
|
problems.push("Agent update helper socket is not ready");
|
|
15462
15659
|
return {
|
|
15463
|
-
initialized:
|
|
15660
|
+
initialized: existsSync10(STATE_PATH2),
|
|
15464
15661
|
profileValid,
|
|
15465
15662
|
profileMode,
|
|
15466
15663
|
imageVerified,
|
|
@@ -15473,8 +15670,8 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15473
15670
|
}
|
|
15474
15671
|
|
|
15475
15672
|
// src/operator-bootstrap.ts
|
|
15476
|
-
import { createHash as
|
|
15477
|
-
import { lstatSync as
|
|
15673
|
+
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15674
|
+
import { lstatSync as lstatSync7, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15478
15675
|
import { isIP as isIP6 } from "net";
|
|
15479
15676
|
import { tmpdir } from "os";
|
|
15480
15677
|
import { basename as basename2, join as join10 } from "path";
|
|
@@ -15615,22 +15812,22 @@ var exactKeys5 = (value, keys, label) => {
|
|
|
15615
15812
|
var ownerFile = (path, limit, label) => {
|
|
15616
15813
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15617
15814
|
throw new Error(`${label} path must be absolute`);
|
|
15618
|
-
const metadata =
|
|
15815
|
+
const metadata = lstatSync7(path);
|
|
15619
15816
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15620
15817
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
|
|
15621
15818
|
throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
|
|
15622
15819
|
}
|
|
15623
|
-
return
|
|
15820
|
+
return readFileSync12(path);
|
|
15624
15821
|
};
|
|
15625
15822
|
var publicIdentity = (path) => {
|
|
15626
15823
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15627
15824
|
throw new Error("SSH public-key path must be absolute");
|
|
15628
|
-
const metadata =
|
|
15825
|
+
const metadata = lstatSync7(path);
|
|
15629
15826
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15630
15827
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
|
|
15631
15828
|
throw new Error("SSH public key must be a caller-owned regular file with one link");
|
|
15632
15829
|
}
|
|
15633
|
-
const value =
|
|
15830
|
+
const value = readFileSync12(path, "utf8").trim();
|
|
15634
15831
|
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
|
|
15635
15832
|
throw new Error("SSH public key is malformed");
|
|
15636
15833
|
}
|
|
@@ -15639,7 +15836,7 @@ var publicIdentity = (path) => {
|
|
|
15639
15836
|
var socketPath = (path) => {
|
|
15640
15837
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15641
15838
|
throw new Error("SSH agent socket path must be absolute");
|
|
15642
|
-
const metadata =
|
|
15839
|
+
const metadata = lstatSync7(path);
|
|
15643
15840
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
15644
15841
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
|
|
15645
15842
|
throw new Error("SSH agent socket must be a caller-owned Unix socket");
|
|
@@ -15653,7 +15850,7 @@ var fingerprint = (key) => {
|
|
|
15653
15850
|
if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
15654
15851
|
throw new Error("SSH host key is malformed");
|
|
15655
15852
|
}
|
|
15656
|
-
return `SHA256:${
|
|
15853
|
+
return `SHA256:${createHash6("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
|
|
15657
15854
|
};
|
|
15658
15855
|
var validateHop = (value, label) => {
|
|
15659
15856
|
const hop = exactKeys5(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
|
|
@@ -15798,8 +15995,9 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
15798
15995
|
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
15799
15996
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
15800
15997
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
15998
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
15801
15999
|
"stage the rewritten owner-only platform config and its named credential handoffs",
|
|
15802
|
-
|
|
16000
|
+
"run typed fz bootstrap platform --apply from the local bundle",
|
|
15803
16001
|
"remove transient local and remote staging data"
|
|
15804
16002
|
],
|
|
15805
16003
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
@@ -15828,7 +16026,7 @@ var defaultExec3 = async (argv2, options = {}) => {
|
|
|
15828
16026
|
]);
|
|
15829
16027
|
return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
|
|
15830
16028
|
};
|
|
15831
|
-
var
|
|
16029
|
+
var checked5 = async (exec, argv2, label, options) => {
|
|
15832
16030
|
const result = await exec(argv2, options);
|
|
15833
16031
|
if (result.exitCode !== 0)
|
|
15834
16032
|
throw new Error(`${label} failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
@@ -15841,7 +16039,7 @@ function writeKnownHosts(request, directory) {
|
|
|
15841
16039
|
if (request.target.jump)
|
|
15842
16040
|
lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
|
|
15843
16041
|
const path = join10(directory, "known_hosts");
|
|
15844
|
-
|
|
16042
|
+
writeFileSync12(path, `${lines.join(`
|
|
15845
16043
|
`)}
|
|
15846
16044
|
`, { mode: 384, flag: "wx" });
|
|
15847
16045
|
return path;
|
|
@@ -15882,7 +16080,7 @@ var safeRemoteArg = (value) => {
|
|
|
15882
16080
|
};
|
|
15883
16081
|
var remote = async (exec, request, knownHosts, argv2, label, secret = false, stdin) => {
|
|
15884
16082
|
argv2.forEach(safeRemoteArg);
|
|
15885
|
-
return
|
|
16083
|
+
return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv2], label, { secret, stdin });
|
|
15886
16084
|
};
|
|
15887
16085
|
var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
15888
16086
|
safeRemoteArg(path);
|
|
@@ -15903,36 +16101,45 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
15903
16101
|
};
|
|
15904
16102
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
15905
16103
|
safeRemoteArg(remotePath);
|
|
15906
|
-
await
|
|
16104
|
+
await checked5(exec, [
|
|
15907
16105
|
"scp",
|
|
15908
16106
|
...sshOptions(request, knownHosts, true),
|
|
15909
16107
|
local,
|
|
15910
16108
|
`${destination2(request.target)}:${remotePath}`
|
|
15911
16109
|
], "secure copy", { secret });
|
|
15912
16110
|
};
|
|
15913
|
-
function stageConfig(config, directory) {
|
|
16111
|
+
async function stageConfig(config, directory) {
|
|
15914
16112
|
const rewritten = structuredClone(config);
|
|
15915
16113
|
const staged = [];
|
|
15916
16114
|
for (const [name, source] of secretSources(config)) {
|
|
15917
16115
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
15918
16116
|
const local = join10(directory, name);
|
|
15919
|
-
|
|
16117
|
+
writeFileSync12(local, bytes, { mode: 384, flag: "wx" });
|
|
15920
16118
|
staged.push(name);
|
|
15921
16119
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
15922
16120
|
if (name === "cloudflare-handoff")
|
|
15923
16121
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
15924
16122
|
}
|
|
16123
|
+
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
16124
|
+
const bundleFiles = [
|
|
16125
|
+
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
16126
|
+
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
16127
|
+
];
|
|
16128
|
+
rewritten.bootstrapBundle = {
|
|
16129
|
+
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
16130
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`
|
|
16131
|
+
};
|
|
15925
16132
|
const path = join10(directory, "platform-config.json");
|
|
15926
|
-
|
|
16133
|
+
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
15927
16134
|
`, { mode: 384, flag: "wx" });
|
|
15928
|
-
return { path, files: staged };
|
|
16135
|
+
return { path, files: staged, bundleFiles };
|
|
15929
16136
|
}
|
|
15930
16137
|
function stageMetalConfig(config, directory) {
|
|
15931
16138
|
const rewritten = structuredClone(config);
|
|
15932
16139
|
const files = [];
|
|
15933
16140
|
if (config.agentSeedFile) {
|
|
15934
16141
|
const name = "metal-agent-seed";
|
|
15935
|
-
|
|
16142
|
+
writeFileSync12(join10(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
|
|
15936
16143
|
mode: 384,
|
|
15937
16144
|
flag: "wx"
|
|
15938
16145
|
});
|
|
@@ -15940,7 +16147,7 @@ function stageMetalConfig(config, directory) {
|
|
|
15940
16147
|
files.push(name);
|
|
15941
16148
|
}
|
|
15942
16149
|
const path = join10(directory, "metal-config.json");
|
|
15943
|
-
|
|
16150
|
+
writeFileSync12(path, `${JSON.stringify(rewritten, null, 2)}
|
|
15944
16151
|
`, { mode: 384, flag: "wx" });
|
|
15945
16152
|
return { path, files };
|
|
15946
16153
|
}
|
|
@@ -15949,10 +16156,10 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
15949
16156
|
if (!response.ok)
|
|
15950
16157
|
throw new Error("pinned Bun download failed");
|
|
15951
16158
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
15952
|
-
if (
|
|
16159
|
+
if (createHash6("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
|
|
15953
16160
|
throw new Error("pinned Bun checksum mismatch");
|
|
15954
16161
|
const path = join10(directory, "bun.zip");
|
|
15955
|
-
|
|
16162
|
+
writeFileSync12(path, bytes, { mode: 384, flag: "wx" });
|
|
15956
16163
|
return path;
|
|
15957
16164
|
}
|
|
15958
16165
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
@@ -15962,7 +16169,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
15962
16169
|
[options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
|
|
15963
16170
|
];
|
|
15964
16171
|
for (const [artifact] of artifacts) {
|
|
15965
|
-
if (!
|
|
16172
|
+
if (!readFileSync12(artifact).length)
|
|
15966
16173
|
throw new Error(`packaged artifact is empty: ${basename2(artifact)}`);
|
|
15967
16174
|
}
|
|
15968
16175
|
await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
|
|
@@ -16003,7 +16210,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16003
16210
|
socketPath(request.target.agentSocket);
|
|
16004
16211
|
const exec = options.exec ?? defaultExec3;
|
|
16005
16212
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
16006
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16213
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
|
|
16007
16214
|
let knownHosts = "";
|
|
16008
16215
|
try {
|
|
16009
16216
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16015,18 +16222,16 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16015
16222
|
if (config.kind !== "platform")
|
|
16016
16223
|
throw new Error("operator bootstrap requires a platform config");
|
|
16017
16224
|
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
16018
|
-
const staged = stageConfig(config, directory);
|
|
16225
|
+
const staged = await stageConfig(config, directory);
|
|
16019
16226
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
16020
16227
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
16021
16228
|
for (const name of staged.files)
|
|
16022
16229
|
await copy(exec, request, knownHosts, join10(directory, name), `${remoteTemp}/${name}`, true);
|
|
16023
|
-
for (const
|
|
16230
|
+
for (const bundle of staged.bundleFiles)
|
|
16231
|
+
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
16232
|
+
for (const name of ["platform-config.json", ...staged.files, ...staged.bundleFiles.map(({ name: name2 }) => name2)])
|
|
16024
16233
|
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");
|
|
16234
|
+
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
16030
16235
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
16031
16236
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
16032
16237
|
` : undefined);
|
|
@@ -16037,7 +16242,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16037
16242
|
return;
|
|
16038
16243
|
});
|
|
16039
16244
|
}
|
|
16040
|
-
|
|
16245
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16041
16246
|
}
|
|
16042
16247
|
}
|
|
16043
16248
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
@@ -16046,7 +16251,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16046
16251
|
socketPath(request.target.agentSocket);
|
|
16047
16252
|
const exec = options.exec ?? defaultExec3;
|
|
16048
16253
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-metal-"));
|
|
16049
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16254
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
|
|
16050
16255
|
let knownHosts = "";
|
|
16051
16256
|
try {
|
|
16052
16257
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16090,7 +16295,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16090
16295
|
return;
|
|
16091
16296
|
});
|
|
16092
16297
|
}
|
|
16093
|
-
|
|
16298
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
16094
16299
|
}
|
|
16095
16300
|
}
|
|
16096
16301
|
|
|
@@ -16103,9 +16308,6 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16103
16308
|
if (nodes.some((node) => !node.nodeHostname || !node.cloudflareHandoffFile)) {
|
|
16104
16309
|
throw new Error("every platform genesis node requires its public hostname and node-specific Cloudflare handoff");
|
|
16105
16310
|
}
|
|
16106
|
-
if (template.branch !== (environment === "production" ? "main" : "dev")) {
|
|
16107
|
-
throw new Error("platform genesis branch must match the selected environment");
|
|
16108
|
-
}
|
|
16109
16311
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
16110
16312
|
return fleet.map((guest, index) => {
|
|
16111
16313
|
const node = nodes[index];
|
|
@@ -16144,7 +16346,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16144
16346
|
}
|
|
16145
16347
|
|
|
16146
16348
|
// src/host-maintenance.ts
|
|
16147
|
-
import { lstatSync as
|
|
16349
|
+
import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
|
|
16148
16350
|
var ROOT = "/opt/forgezero";
|
|
16149
16351
|
var SLOT_FILE = `${ROOT}/.forge-slot`;
|
|
16150
16352
|
var SHARED_ENV = `${ROOT}/shared/.env`;
|
|
@@ -16152,9 +16354,9 @@ var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
|
|
|
16152
16354
|
var FZ = "/usr/local/bin/fz";
|
|
16153
16355
|
var localRuntime = () => ({
|
|
16154
16356
|
uid: () => process.getuid?.() ?? -1,
|
|
16155
|
-
read: (path) =>
|
|
16357
|
+
read: (path) => readFileSync13(path, "utf8"),
|
|
16156
16358
|
inspect(path) {
|
|
16157
|
-
const value =
|
|
16359
|
+
const value = lstatSync8(path);
|
|
16158
16360
|
return {
|
|
16159
16361
|
regular: value.isFile(),
|
|
16160
16362
|
symbolic: value.isSymbolicLink(),
|
|
@@ -16275,8 +16477,8 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
16275
16477
|
}
|
|
16276
16478
|
|
|
16277
16479
|
// src/cli/maintenance.ts
|
|
16278
|
-
import { existsSync as
|
|
16279
|
-
import { isAbsolute as
|
|
16480
|
+
import { existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
|
|
16481
|
+
import { isAbsolute as isAbsolute5, join as join11, relative as relative2, resolve as resolve9 } from "path";
|
|
16280
16482
|
var API_OPERATION_ENTRYPOINTS = {
|
|
16281
16483
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
16282
16484
|
"db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
|
|
@@ -16346,12 +16548,12 @@ function unsupportedRepositoryCliOption(argv2) {
|
|
|
16346
16548
|
}
|
|
16347
16549
|
function manifestName(root) {
|
|
16348
16550
|
const manifestPath = join11(root, "package.json");
|
|
16349
|
-
if (!
|
|
16551
|
+
if (!existsSync11(manifestPath)) {
|
|
16350
16552
|
throw new Error(`No package.json exists at repository root ${root}.`);
|
|
16351
16553
|
}
|
|
16352
16554
|
let parsed;
|
|
16353
16555
|
try {
|
|
16354
|
-
parsed = JSON.parse(
|
|
16556
|
+
parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
16355
16557
|
} catch (cause) {
|
|
16356
16558
|
throw new Error(`Cannot read ${manifestPath}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
16357
16559
|
}
|
|
@@ -16362,21 +16564,21 @@ function manifestName(root) {
|
|
|
16362
16564
|
}
|
|
16363
16565
|
function checkedEntrypoint(root, parts) {
|
|
16364
16566
|
const candidate = join11(root, ...parts);
|
|
16365
|
-
if (!
|
|
16567
|
+
if (!existsSync11(candidate) || !lstatSync9(candidate).isFile()) {
|
|
16366
16568
|
throw new Error(`The reviewed operation entrypoint is missing: ${candidate}`);
|
|
16367
16569
|
}
|
|
16368
|
-
if (
|
|
16570
|
+
if (lstatSync9(candidate).isSymbolicLink()) {
|
|
16369
16571
|
throw new Error(`The reviewed operation entrypoint must not be a symbolic link: ${candidate}`);
|
|
16370
16572
|
}
|
|
16371
16573
|
const actual = realpathSync5(candidate);
|
|
16372
16574
|
const within = relative2(root, actual);
|
|
16373
|
-
if (within.startsWith("..") ||
|
|
16575
|
+
if (within.startsWith("..") || isAbsolute5(within)) {
|
|
16374
16576
|
throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
|
|
16375
16577
|
}
|
|
16376
16578
|
return actual;
|
|
16377
16579
|
}
|
|
16378
16580
|
function resolveRepositoryOperation(operation, requestedRoot) {
|
|
16379
|
-
const root = realpathSync5(
|
|
16581
|
+
const root = realpathSync5(resolve9(requestedRoot));
|
|
16380
16582
|
const name = manifestName(root);
|
|
16381
16583
|
if (operation in API_OPERATION_ENTRYPOINTS) {
|
|
16382
16584
|
const parts = API_OPERATION_ENTRYPOINTS[operation];
|
|
@@ -16677,7 +16879,7 @@ async function api(options, path, init) {
|
|
|
16677
16879
|
return { status: response.status, body };
|
|
16678
16880
|
}
|
|
16679
16881
|
function sleep(ms) {
|
|
16680
|
-
return new Promise((
|
|
16882
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
16681
16883
|
}
|
|
16682
16884
|
function browserCommand(url) {
|
|
16683
16885
|
if (process.platform === "darwin")
|
|
@@ -16762,7 +16964,7 @@ function requireSession() {
|
|
|
16762
16964
|
function readOwnerOnlySecret(path, label) {
|
|
16763
16965
|
let metadata;
|
|
16764
16966
|
try {
|
|
16765
|
-
metadata =
|
|
16967
|
+
metadata = lstatSync10(path);
|
|
16766
16968
|
} catch {
|
|
16767
16969
|
throw new Error(`The ${label} file ${path} is unreadable.`);
|
|
16768
16970
|
}
|
|
@@ -16776,7 +16978,7 @@ function readOwnerOnlySecret(path, label) {
|
|
|
16776
16978
|
if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
16777
16979
|
throw new Error(`The ${label} file ${path} is not owned by the current user.`);
|
|
16778
16980
|
}
|
|
16779
|
-
const value =
|
|
16981
|
+
const value = readFileSync15(path, "utf8").trim();
|
|
16780
16982
|
if (!value)
|
|
16781
16983
|
throw new Error(`The ${label} file ${path} is empty.`);
|
|
16782
16984
|
return value;
|
|
@@ -16957,7 +17159,7 @@ async function cmdApi(options, args) {
|
|
|
16957
17159
|
let body = undefined;
|
|
16958
17160
|
if (options.data !== undefined && options.dataFile)
|
|
16959
17161
|
throw new Error("Use only one of --data or --data-file.");
|
|
16960
|
-
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ?
|
|
17162
|
+
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync15(options.dataFile, "utf8") : options.data;
|
|
16961
17163
|
if (encoded !== undefined)
|
|
16962
17164
|
body = JSON.parse(encoded);
|
|
16963
17165
|
const result = await api(options, `${url.pathname}${url.search}`, { method, body });
|
|
@@ -17051,7 +17253,7 @@ async function cmdAgent(options, args) {
|
|
|
17051
17253
|
seedCredentialPath: process.env.FZ_SEED_CREDENTIAL_PATH,
|
|
17052
17254
|
gitCredentialPath,
|
|
17053
17255
|
gitPublicKeyPath,
|
|
17054
|
-
generateGitIdentity: true,
|
|
17256
|
+
generateGitIdentity: process.env.FZ_GENERATE_GIT_DEPLOY_KEY === "true",
|
|
17055
17257
|
controlSocketPath: process.env.FZ_CONTROL_SOCKET,
|
|
17056
17258
|
repository: process.env.FZ_DEPLOY_REPO,
|
|
17057
17259
|
branch: process.env.FZ_DEPLOY_BRANCH,
|
|
@@ -17108,19 +17310,19 @@ async function cmdAgent(options, args) {
|
|
|
17108
17310
|
return 0;
|
|
17109
17311
|
}
|
|
17110
17312
|
try {
|
|
17111
|
-
|
|
17313
|
+
writeFileSync13(plan.unitPath, plan.unit, { mode: 420 });
|
|
17112
17314
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
17113
17315
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
17114
|
-
|
|
17115
|
-
|
|
17316
|
+
mkdirSync12(dirname13(auxiliary.path), { recursive: true, mode: 493 });
|
|
17317
|
+
writeFileSync13(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
17116
17318
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17117
17319
|
}
|
|
17118
17320
|
if (options.enrol) {
|
|
17119
|
-
if (!
|
|
17321
|
+
if (!existsSync12(enrolStatePath) && !existsSync12(enrolTokenCredentialPath)) {
|
|
17120
17322
|
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17121
17323
|
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17122
17324
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17123
|
-
|
|
17325
|
+
mkdirSync12(dirname13(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17124
17326
|
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17125
17327
|
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17126
17328
|
sealing.stdin.write(`${token}
|
|
@@ -17136,11 +17338,13 @@ async function cmdAgent(options, args) {
|
|
|
17136
17338
|
for (const step3 of transcript)
|
|
17137
17339
|
out.ok(step3.label);
|
|
17138
17340
|
out.ok("Agent service and socket verified");
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17341
|
+
if (existsSync12(gitPublicKeyPath)) {
|
|
17342
|
+
out.line();
|
|
17343
|
+
out.line(" Compatibility SSH deploy public key:");
|
|
17344
|
+
out.line();
|
|
17345
|
+
out.line(` ${readFileSync15(gitPublicKeyPath, "utf8").trim()}`);
|
|
17346
|
+
out.line();
|
|
17347
|
+
}
|
|
17144
17348
|
return 0;
|
|
17145
17349
|
} catch (cause) {
|
|
17146
17350
|
out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
|
|
@@ -17244,10 +17448,10 @@ function interactiveMetalBootstrap() {
|
|
|
17244
17448
|
});
|
|
17245
17449
|
}
|
|
17246
17450
|
function writeBootstrapConfig(path, config) {
|
|
17247
|
-
if (!
|
|
17451
|
+
if (!isAbsolute6(path) || resolve10(path) !== path)
|
|
17248
17452
|
throw new Error("--output must be a canonical absolute path");
|
|
17249
|
-
|
|
17250
|
-
|
|
17453
|
+
mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
|
|
17454
|
+
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
17251
17455
|
`, { mode: 384, flag: "wx" });
|
|
17252
17456
|
return path;
|
|
17253
17457
|
}
|
|
@@ -17308,11 +17512,11 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17308
17512
|
});
|
|
17309
17513
|
}
|
|
17310
17514
|
function genesisOutputDirectory(path) {
|
|
17311
|
-
if (!
|
|
17515
|
+
if (!isAbsolute6(path) || resolve10(path) !== path)
|
|
17312
17516
|
throw new Error("--output must be a canonical absolute directory");
|
|
17313
|
-
if (!
|
|
17314
|
-
|
|
17315
|
-
const metadata =
|
|
17517
|
+
if (!existsSync12(path))
|
|
17518
|
+
mkdirSync12(path, { recursive: true, mode: 448 });
|
|
17519
|
+
const metadata = lstatSync10(path);
|
|
17316
17520
|
const uid = process.getuid?.();
|
|
17317
17521
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
17318
17522
|
throw new Error("platform genesis output must be an owner-only directory owned by the current user");
|
|
@@ -17356,8 +17560,8 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17356
17560
|
const nodeHostname = bootstrapAnswer(genesis ? `${computeReference} public node hostname` : "Public node hostname");
|
|
17357
17561
|
const apiUrl = bootstrapAnswer("Platform API URL");
|
|
17358
17562
|
const appOrigin = bootstrapAnswer("Public App origin");
|
|
17359
|
-
const
|
|
17360
|
-
const
|
|
17563
|
+
const bundleFile = bootstrapAnswer("Bootstrap API Git bundle (absolute path)");
|
|
17564
|
+
const manifestFile = bootstrapAnswer("Bootstrap bundle manifest (absolute path)", `${bundleFile}.json`);
|
|
17361
17565
|
const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
|
|
17362
17566
|
const collectorUnit = FORGEZERO_OTEL_COLLECTOR_UNIT;
|
|
17363
17567
|
const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
|
|
@@ -17386,8 +17590,7 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17386
17590
|
computeReference,
|
|
17387
17591
|
nodeHostname,
|
|
17388
17592
|
apiUrl,
|
|
17389
|
-
|
|
17390
|
-
branch,
|
|
17593
|
+
bootstrapBundle: { bundleFile, manifestFile },
|
|
17391
17594
|
telemetryEndpoint,
|
|
17392
17595
|
database: {
|
|
17393
17596
|
role,
|
|
@@ -17431,8 +17634,6 @@ function interactiveBootstrap(kind, genesis = false) {
|
|
|
17431
17634
|
...backupEndpoint && backupRegion && backupBucket && backupAccessKeyId ? {
|
|
17432
17635
|
backup: { endpoint: backupEndpoint, region: backupRegion, bucket: backupBucket, accessKeyId: backupAccessKeyId }
|
|
17433
17636
|
} : {},
|
|
17434
|
-
repository,
|
|
17435
|
-
branch,
|
|
17436
17637
|
deployProfile: environment
|
|
17437
17638
|
},
|
|
17438
17639
|
serviceUser: "forgezero-api",
|
|
@@ -17478,8 +17679,8 @@ async function cmdBootstrap(options, args) {
|
|
|
17478
17679
|
}
|
|
17479
17680
|
if (operation === "platform" && args[1] === "remote") {
|
|
17480
17681
|
const mode = args[2];
|
|
17481
|
-
if (!mode || !["
|
|
17482
|
-
throw new Error("Usage: fz bootstrap platform remote <
|
|
17682
|
+
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
17683
|
+
throw new Error("Usage: fz bootstrap platform remote <apply|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
17483
17684
|
}
|
|
17484
17685
|
if (!options.bootstrapConfigPath)
|
|
17485
17686
|
throw new Error("remote platform bootstrap requires --bootstrap-config");
|
|
@@ -17497,6 +17698,18 @@ async function cmdBootstrap(options, args) {
|
|
|
17497
17698
|
out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode, { secrets: secrets2 }), null, 2));
|
|
17498
17699
|
return 0;
|
|
17499
17700
|
}
|
|
17701
|
+
if (operation === "platform" && args[1] === "bundle") {
|
|
17702
|
+
if (args[2] !== undefined || !options.outputPath) {
|
|
17703
|
+
throw new Error("Usage: fz bootstrap platform bundle --root <clean-api-checkout> --branch <main|dev> --output <absolute.bundle>");
|
|
17704
|
+
}
|
|
17705
|
+
const result2 = await buildBootstrapBundle({
|
|
17706
|
+
repositoryRoot: options.projectRoot,
|
|
17707
|
+
outputPath: options.outputPath,
|
|
17708
|
+
branch: options.branch
|
|
17709
|
+
});
|
|
17710
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
17711
|
+
return 0;
|
|
17712
|
+
}
|
|
17500
17713
|
if (operation === "metal" && args[1] === "remote") {
|
|
17501
17714
|
const mode = args[2];
|
|
17502
17715
|
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "status"].includes(mode) || args[3] !== undefined) {
|
|
@@ -17553,31 +17766,12 @@ async function cmdBootstrap(options, args) {
|
|
|
17553
17766
|
await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply, tokens);
|
|
17554
17767
|
return 0;
|
|
17555
17768
|
}
|
|
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
17769
|
const credentialStdin = operation === "platform" && args[1] === "credentials-stdin";
|
|
17576
17770
|
if (credentialStdin && args[2] !== undefined)
|
|
17577
17771
|
throw new Error("platform credential stdin accepts no additional positional arguments");
|
|
17578
17772
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
17579
|
-
metal:
|
|
17580
|
-
compute:
|
|
17773
|
+
metal: existsSync12(METAL_BOOTSTRAP_STATE_PATH),
|
|
17774
|
+
compute: existsSync12(BOOTSTRAP_STATE_PATH)
|
|
17581
17775
|
});
|
|
17582
17776
|
if (operation === "status") {
|
|
17583
17777
|
if (installedBootstrapKind() === "metal") {
|
|
@@ -17618,7 +17812,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17618
17812
|
return 0;
|
|
17619
17813
|
}
|
|
17620
17814
|
if (!["platform", "repair"].includes(operation)) {
|
|
17621
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [
|
|
17815
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
17622
17816
|
}
|
|
17623
17817
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
17624
17818
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -17715,7 +17909,7 @@ async function cmdUnlock(options) {
|
|
|
17715
17909
|
if (options.key || options.userExplicit) {
|
|
17716
17910
|
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
17911
|
}
|
|
17718
|
-
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ?
|
|
17912
|
+
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync15(0, "utf8").trim() : "";
|
|
17719
17913
|
const phrase = phraseText ? phraseText.split(/\s+/) : [];
|
|
17720
17914
|
if (phrase.length !== 24) {
|
|
17721
17915
|
throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
|
|
@@ -17931,7 +18125,7 @@ function projectFromCheckout(options) {
|
|
|
17931
18125
|
try {
|
|
17932
18126
|
return loadConfig({
|
|
17933
18127
|
cwd: options.projectRoot,
|
|
17934
|
-
readFile: (path) =>
|
|
18128
|
+
readFile: (path) => existsSync12(path) ? readFileSync15(path, "utf8") : undefined
|
|
17935
18129
|
}).config.project;
|
|
17936
18130
|
} catch (cause) {
|
|
17937
18131
|
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 +18158,10 @@ async function waitForRuns(options, pipelineKey, keys) {
|
|
|
17964
18158
|
while (Date.now() < deadline) {
|
|
17965
18159
|
const payload = await deploymentRequest(options, `/runs?pipelineKey=${encodeURIComponent(pipelineKey)}`);
|
|
17966
18160
|
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((
|
|
18161
|
+
const selected = runs.filter((run2) => run2._key && keys.includes(run2._key));
|
|
18162
|
+
if (selected.length === keys.length && selected.every((run2) => terminal.has(run2.outcome ?? ""))) {
|
|
18163
|
+
out.line(JSON.stringify({ ok: selected.every((run2) => run2.outcome === "deployed"), runs: selected }, null, 2));
|
|
18164
|
+
return selected.every((run2) => run2.outcome === "deployed");
|
|
17971
18165
|
}
|
|
17972
18166
|
await sleep(2000);
|
|
17973
18167
|
}
|
|
@@ -17994,7 +18188,7 @@ async function cmdDeploy(options, args) {
|
|
|
17994
18188
|
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
17995
18189
|
return 0;
|
|
17996
18190
|
}
|
|
17997
|
-
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(
|
|
18191
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve10(options.projectRoot)), force: options.force });
|
|
17998
18192
|
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
17999
18193
|
if (options.json)
|
|
18000
18194
|
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
@@ -18019,7 +18213,7 @@ async function cmdDeploy(options, args) {
|
|
|
18019
18213
|
return problems.length === 0 ? 0 : 1;
|
|
18020
18214
|
}
|
|
18021
18215
|
if (operation === "check" || operation === "sync") {
|
|
18022
|
-
if (
|
|
18216
|
+
if (existsSync12(resolve10(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18023
18217
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18024
18218
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18025
18219
|
const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -18095,7 +18289,7 @@ async function cmdDeploy(options, args) {
|
|
|
18095
18289
|
branch: options.branch,
|
|
18096
18290
|
cloneUrl: required(options.cloneUrl, "--clone-url"),
|
|
18097
18291
|
sourceAuth,
|
|
18098
|
-
...options.knownHostsFile ? { knownHosts:
|
|
18292
|
+
...options.knownHostsFile ? { knownHosts: readFileSync15(options.knownHostsFile, "utf8") } : {},
|
|
18099
18293
|
projectKey
|
|
18100
18294
|
} });
|
|
18101
18295
|
const pipelineKey2 = String(created.pipelineKey);
|
|
@@ -18135,7 +18329,7 @@ async function cmdDeploy(options, args) {
|
|
|
18135
18329
|
out.line(JSON.stringify(released, null, 2));
|
|
18136
18330
|
return 0;
|
|
18137
18331
|
}
|
|
18138
|
-
const keys = (released.runs ?? []).flatMap((
|
|
18332
|
+
const keys = (released.runs ?? []).flatMap((run2) => run2._key ? [run2._key] : []);
|
|
18139
18333
|
if (keys.length === 0)
|
|
18140
18334
|
throw new Error("The API accepted the release but returned no deployment runs.");
|
|
18141
18335
|
return await waitForRuns(options, pipelineKey, keys) ? 0 : 1;
|
|
@@ -18191,11 +18385,11 @@ function usage() {
|
|
|
18191
18385
|
fz agent install Install the node agent as a systemd service, so
|
|
18192
18386
|
applications on this box read secrets through a
|
|
18193
18387
|
local socket instead of holding an API key
|
|
18194
|
-
fz bootstrap platform
|
|
18195
|
-
|
|
18196
|
-
fz bootstrap platform remote <
|
|
18388
|
+
fz bootstrap platform bundle
|
|
18389
|
+
Build one clean exact-revision API Git bundle for genesis
|
|
18390
|
+
fz bootstrap platform remote <apply|status>
|
|
18197
18391
|
Run typed bootstrap from the operator laptop through
|
|
18198
|
-
a pinned host and
|
|
18392
|
+
a pinned host; apply copies and verifies that bundle
|
|
18199
18393
|
fz bootstrap platform Install/repair a typed elastic platform compute
|
|
18200
18394
|
fz bootstrap config <metal|platform|cloudflare>
|
|
18201
18395
|
Write a validated owner-only metal/Cloudflare file or
|