@forgezero/agent 0.1.29 → 0.1.31
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 +52 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +19 -10
- package/dist/definition.js +77 -31
- package/dist/deploy-file.d.ts +40 -0
- package/dist/deploy-file.js +378 -0
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +9 -5
- package/dist/fz-agent.js +123 -58
- package/dist/fz.js +729 -19
- package/dist/index.d.ts +2 -2
- package/dist/project-context.d.ts +37 -0
- package/dist/project-context.js +266 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +19 -4
- package/dist/software-helper.js +17 -2
- package/dist/software.d.ts +24 -2
- package/dist/software.js +20 -3
- package/dist/version.d.ts +1 -1
- package/package.json +12 -2
- package/schema/deploy-v2.json +70 -0
package/dist/fz-agent.js
CHANGED
|
@@ -4886,6 +4886,16 @@ import { readFileSync } from "fs";
|
|
|
4886
4886
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4887
4887
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
4888
4888
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
4889
|
+
var OS_CATALOG = [
|
|
4890
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
4891
|
+
];
|
|
4892
|
+
var SOFTWARE_CATALOG = [
|
|
4893
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4894
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4895
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4896
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4897
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4898
|
+
];
|
|
4889
4899
|
var UBUNTU_2604_X64 = [
|
|
4890
4900
|
{
|
|
4891
4901
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -4924,7 +4934,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
4924
4934
|
architecture
|
|
4925
4935
|
};
|
|
4926
4936
|
}
|
|
4927
|
-
function validateSoftwareRequirements(value) {
|
|
4937
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
4928
4938
|
if (!Array.isArray(value) || value.length > 32)
|
|
4929
4939
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
4930
4940
|
const seen = new Set;
|
|
@@ -4942,13 +4952,18 @@ function validateSoftwareRequirements(value) {
|
|
|
4942
4952
|
if (seen.has(requirement.id))
|
|
4943
4953
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
4944
4954
|
seen.add(requirement.id);
|
|
4955
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
4956
|
+
if (!catalog || catalog.status !== "active") {
|
|
4957
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
4958
|
+
}
|
|
4945
4959
|
return requirement;
|
|
4946
4960
|
});
|
|
4947
4961
|
}
|
|
4948
4962
|
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
4949
4963
|
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
4950
4964
|
const observation = options.observation ?? observeSoftwareHost();
|
|
4951
|
-
|
|
4965
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
4966
|
+
if (!os || os.status !== "active") {
|
|
4952
4967
|
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
4953
4968
|
}
|
|
4954
4969
|
const results = [];
|
|
@@ -4973,7 +4988,8 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
|
4973
4988
|
}
|
|
4974
4989
|
|
|
4975
4990
|
// src/definition.ts
|
|
4976
|
-
var PIPELINE_VERSION =
|
|
4991
|
+
var PIPELINE_VERSION = 2;
|
|
4992
|
+
var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
|
|
4977
4993
|
|
|
4978
4994
|
class DefinitionError extends Error {
|
|
4979
4995
|
constructor(message) {
|
|
@@ -4998,6 +5014,7 @@ var exactKeys = (value, allowed, where) => {
|
|
|
4998
5014
|
if (unknown.length > 0)
|
|
4999
5015
|
throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
|
|
5000
5016
|
};
|
|
5017
|
+
var NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
5001
5018
|
var RESERVED_STEP_ENV = new Set([
|
|
5002
5019
|
"PATH",
|
|
5003
5020
|
"HOME",
|
|
@@ -5010,46 +5027,67 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
5010
5027
|
"GIT_SSH",
|
|
5011
5028
|
"GIT_SSH_COMMAND"
|
|
5012
5029
|
]);
|
|
5013
|
-
function parseDeployDefinition(value) {
|
|
5030
|
+
function parseDeployDefinition(value, options = {}) {
|
|
5014
5031
|
const root = record(value, "pipeline");
|
|
5015
|
-
exactKeys(root, ["version", "name", "requireAttestation", "
|
|
5032
|
+
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
5033
|
+
if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
|
|
5034
|
+
throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
|
|
5035
|
+
}
|
|
5016
5036
|
if (root.version !== PIPELINE_VERSION) {
|
|
5017
5037
|
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
5018
5038
|
}
|
|
5019
|
-
if (
|
|
5020
|
-
throw new DefinitionError("pipeline.
|
|
5039
|
+
if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
|
|
5040
|
+
throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
|
|
5041
|
+
}
|
|
5042
|
+
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
5043
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
5044
|
+
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
5045
|
+
throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
|
|
5021
5046
|
}
|
|
5022
5047
|
if (!Array.isArray(root.steps) || root.steps.length === 0) {
|
|
5023
5048
|
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
5024
5049
|
}
|
|
5025
|
-
const
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5050
|
+
const profiles = {};
|
|
5051
|
+
for (const [name2, raw] of profileEntries) {
|
|
5052
|
+
if (!NAME.test(name2))
|
|
5053
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
|
|
5054
|
+
const profile = record(raw, `profiles.${name2}`);
|
|
5055
|
+
exactKeys(profile, ["software"], `profiles.${name2}`);
|
|
5056
|
+
if (!Array.isArray(profile.software)) {
|
|
5057
|
+
throw new DefinitionError(`profiles.${name2}.software must be an array.`);
|
|
5030
5058
|
}
|
|
5031
|
-
|
|
5032
|
-
name: text(role.name, `roles[${index}].name`),
|
|
5033
|
-
software: validateSoftwareRequirements(role.software)
|
|
5034
|
-
};
|
|
5035
|
-
});
|
|
5036
|
-
if (new Set(roles.map((role) => role.name)).size !== roles.length) {
|
|
5037
|
-
throw new DefinitionError("pipeline.roles must have unique names.");
|
|
5059
|
+
profiles[name2] = { software: validateSoftwareRequirements(profile.software, options) };
|
|
5038
5060
|
}
|
|
5039
5061
|
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
5040
5062
|
const steps = root.steps.map((raw, index) => {
|
|
5041
5063
|
const step = record(raw, `steps[${index}]`);
|
|
5042
|
-
exactKeys(step, ["name", "run", "phase", "
|
|
5064
|
+
exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
|
|
5043
5065
|
const phase = text(step.phase, `steps[${index}].phase`);
|
|
5044
5066
|
if (!phases.has(phase))
|
|
5045
5067
|
throw new DefinitionError(`steps[${index}].phase is not supported.`);
|
|
5046
|
-
if (step.
|
|
5068
|
+
if (step.scope !== "target" && step.scope !== "release") {
|
|
5069
|
+
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
5070
|
+
}
|
|
5071
|
+
if (step.always !== undefined && typeof step.always !== "boolean") {
|
|
5072
|
+
throw new DefinitionError(`steps[${index}].always must be a boolean.`);
|
|
5073
|
+
}
|
|
5074
|
+
let selectedProfiles;
|
|
5075
|
+
if (step.profiles !== undefined) {
|
|
5076
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
|
|
5077
|
+
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
5078
|
+
}
|
|
5079
|
+
selectedProfiles = [...step.profiles];
|
|
5080
|
+
if (new Set(selectedProfiles).size !== selectedProfiles.length) {
|
|
5081
|
+
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
|
|
5047
5085
|
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
5048
5086
|
}
|
|
5049
5087
|
if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
|
|
5050
5088
|
throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
|
|
5051
5089
|
}
|
|
5052
|
-
if (Array.isArray(step.secrets) && step.secrets.some((
|
|
5090
|
+
if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
|
|
5053
5091
|
throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
|
|
5054
5092
|
}
|
|
5055
5093
|
const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
|
|
@@ -5060,11 +5098,11 @@ function parseDeployDefinition(value) {
|
|
|
5060
5098
|
if (step.when !== undefined) {
|
|
5061
5099
|
const conditions = record(step.when, `steps[${index}].when`);
|
|
5062
5100
|
when = {};
|
|
5063
|
-
for (const [
|
|
5064
|
-
if (!/^[A-Z_][A-Z0-9_]*$/.test(
|
|
5101
|
+
for (const [name2, expected] of Object.entries(conditions)) {
|
|
5102
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
|
|
5065
5103
|
throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
|
|
5066
5104
|
}
|
|
5067
|
-
when[
|
|
5105
|
+
when[name2] = expected;
|
|
5068
5106
|
}
|
|
5069
5107
|
if (Object.keys(when).length === 0)
|
|
5070
5108
|
throw new DefinitionError(`steps[${index}].when must not be empty.`);
|
|
@@ -5073,8 +5111,9 @@ function parseDeployDefinition(value) {
|
|
|
5073
5111
|
name: text(step.name, `steps[${index}].name`),
|
|
5074
5112
|
run: text(step.run, `steps[${index}].run`),
|
|
5075
5113
|
phase,
|
|
5114
|
+
scope: step.scope,
|
|
5115
|
+
profiles: selectedProfiles,
|
|
5076
5116
|
secrets: step.secrets,
|
|
5077
|
-
once: step.once === true,
|
|
5078
5117
|
always: step.always === true,
|
|
5079
5118
|
timeoutMs,
|
|
5080
5119
|
when
|
|
@@ -5083,15 +5122,32 @@ function parseDeployDefinition(value) {
|
|
|
5083
5122
|
if (new Set(steps.map((step) => step.name)).size !== steps.length) {
|
|
5084
5123
|
throw new DefinitionError("pipeline.steps must have unique names.");
|
|
5085
5124
|
}
|
|
5125
|
+
const name = text(root.name, "pipeline.name");
|
|
5126
|
+
if (name.length > 120)
|
|
5127
|
+
throw new DefinitionError("pipeline.name must be at most 120 characters.");
|
|
5086
5128
|
return {
|
|
5087
5129
|
version: PIPELINE_VERSION,
|
|
5088
|
-
name
|
|
5130
|
+
name,
|
|
5089
5131
|
requireAttestation: root.requireAttestation === true,
|
|
5090
|
-
|
|
5132
|
+
profiles,
|
|
5091
5133
|
steps
|
|
5092
5134
|
};
|
|
5093
5135
|
}
|
|
5094
5136
|
|
|
5137
|
+
// src/deploy-file.ts
|
|
5138
|
+
import { createHash } from "crypto";
|
|
5139
|
+
var stable = (value) => {
|
|
5140
|
+
if (Array.isArray(value))
|
|
5141
|
+
return `[${value.map(stable).join(",")}]`;
|
|
5142
|
+
if (value && typeof value === "object") {
|
|
5143
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
|
|
5144
|
+
}
|
|
5145
|
+
return JSON.stringify(value);
|
|
5146
|
+
};
|
|
5147
|
+
function deployDefinitionDigest(definition) {
|
|
5148
|
+
return `sha256:${createHash("sha256").update(stable(definition)).digest("hex")}`;
|
|
5149
|
+
}
|
|
5150
|
+
|
|
5095
5151
|
// src/pipeline.ts
|
|
5096
5152
|
class PipelineError extends Error {
|
|
5097
5153
|
code;
|
|
@@ -5217,7 +5273,7 @@ function createDeploymentManager(options) {
|
|
|
5217
5273
|
const exec = options.exec ?? shell;
|
|
5218
5274
|
const projectExec = options.projectExec ?? exec;
|
|
5219
5275
|
const now = options.now ?? Date.now;
|
|
5220
|
-
const readDefinition = options.readDefinition ?? ((path) =>
|
|
5276
|
+
const readDefinition = options.readDefinition ?? ((path) => JSON.parse(readFileSync2(path, "utf8")));
|
|
5221
5277
|
const credentialsDirectory = process.env.CREDENTIALS_DIRECTORY;
|
|
5222
5278
|
const gitCredentialPath = options.gitCredentialPath ?? (credentialsDirectory ? join(credentialsDirectory, "git-deploy-key") : undefined);
|
|
5223
5279
|
const knownHostsPath = options.knownHostsPath ?? "/etc/forgezero/git/known_hosts";
|
|
@@ -5332,22 +5388,23 @@ function createDeploymentManager(options) {
|
|
|
5332
5388
|
if (request.revision && head !== request.revision) {
|
|
5333
5389
|
throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
|
|
5334
5390
|
}
|
|
5335
|
-
const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.
|
|
5336
|
-
const
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5391
|
+
const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.json")));
|
|
5392
|
+
const definitionDigest = deployDefinitionDigest(definition);
|
|
5393
|
+
const selectedProfile = definition.profiles[options.profile];
|
|
5394
|
+
if (!selectedProfile)
|
|
5395
|
+
throw new DeploymentError("PIPELINE_FAILED", `pipeline profile does not exist: ${options.profile}.`);
|
|
5396
|
+
if (selectedProfile.software.length > 0) {
|
|
5340
5397
|
if (!options.ensureSoftware) {
|
|
5341
5398
|
throw new DeploymentError("PIPELINE_FAILED", "the supervised software helper is unavailable");
|
|
5342
5399
|
}
|
|
5343
|
-
await options.ensureSoftware(
|
|
5400
|
+
await options.ensureSoftware(selectedProfile.software);
|
|
5344
5401
|
}
|
|
5345
5402
|
const phaseEnvironment = {
|
|
5346
5403
|
...options.environment ?? {},
|
|
5347
5404
|
FZ_RELEASE: release,
|
|
5348
5405
|
FZ_DEPLOY_REVISION: head,
|
|
5349
5406
|
FZ_DEPLOY_BRANCH: options.branch,
|
|
5350
|
-
|
|
5407
|
+
FZ_DEPLOY_PROFILE: options.profile,
|
|
5351
5408
|
...options.publicApiUrl ? { PUBLIC_API_URL: options.publicApiUrl } : {}
|
|
5352
5409
|
};
|
|
5353
5410
|
const phaseExec = ({ command, env: secrets, timeoutMs }) => projectExec({ command, cwd: release, env: { ...phaseEnvironment, ...secrets }, timeoutMs });
|
|
@@ -5363,7 +5420,7 @@ function createDeploymentManager(options) {
|
|
|
5363
5420
|
return {
|
|
5364
5421
|
name: `${definition.name}:${phase}`,
|
|
5365
5422
|
requireAttestation: definition.requireAttestation,
|
|
5366
|
-
steps: definition.steps.filter((step) => step.phase === phase && (!step.
|
|
5423
|
+
steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(options.profile)) && (step.scope === "target" || request.releaseExecutor === true) && (!step.when || Object.entries(step.when).every(([name, expected]) => phaseEnvironment[name] === expected)))
|
|
5367
5424
|
};
|
|
5368
5425
|
})
|
|
5369
5426
|
].filter((pipeline) => pipeline.steps.length > 0);
|
|
@@ -5382,6 +5439,9 @@ function createDeploymentManager(options) {
|
|
|
5382
5439
|
repository: options.repository,
|
|
5383
5440
|
branch: options.branch,
|
|
5384
5441
|
revision: head,
|
|
5442
|
+
definitionDigest,
|
|
5443
|
+
definitionVersion: definition.version,
|
|
5444
|
+
profile: options.profile,
|
|
5385
5445
|
release,
|
|
5386
5446
|
ok: true,
|
|
5387
5447
|
phases
|
|
@@ -5400,7 +5460,7 @@ function createDeploymentManager(options) {
|
|
|
5400
5460
|
return revision.toLowerCase();
|
|
5401
5461
|
},
|
|
5402
5462
|
deploy(request = {}) {
|
|
5403
|
-
const revisionKey = request.revision ? `${request.revision.toLowerCase()}:${request.
|
|
5463
|
+
const revisionKey = request.revision ? `${request.revision.toLowerCase()}:${request.releaseExecutor === true ? "release" : "target"}` : undefined;
|
|
5404
5464
|
if (revisionKey) {
|
|
5405
5465
|
const existing = activeRevisions.get(revisionKey);
|
|
5406
5466
|
if (existing)
|
|
@@ -5592,7 +5652,7 @@ async function deployClaim(options, claim) {
|
|
|
5592
5652
|
schedule();
|
|
5593
5653
|
let result;
|
|
5594
5654
|
try {
|
|
5595
|
-
result = await manager.deploy({ revision: claim.revision,
|
|
5655
|
+
result = await manager.deploy({ revision: claim.revision, releaseExecutor: claim.releaseExecutor }).result;
|
|
5596
5656
|
} finally {
|
|
5597
5657
|
stopped = true;
|
|
5598
5658
|
clearTimer(timer);
|
|
@@ -5645,7 +5705,10 @@ async function pullDeploymentOnce(options) {
|
|
|
5645
5705
|
claimToken: claim.claimToken,
|
|
5646
5706
|
ok: true,
|
|
5647
5707
|
detail: `Deployed ${result.revision}.`,
|
|
5648
|
-
release: result.release
|
|
5708
|
+
release: result.release,
|
|
5709
|
+
definitionDigest: result.definitionDigest,
|
|
5710
|
+
definitionVersion: result.definitionVersion,
|
|
5711
|
+
profile: result.profile
|
|
5649
5712
|
});
|
|
5650
5713
|
return { status: "deployed", claim, result };
|
|
5651
5714
|
}
|
|
@@ -6064,7 +6127,7 @@ import { chmodSync as chmodSync5, existsSync as existsSync6, unlinkSync as unlin
|
|
|
6064
6127
|
import { connect as connect2, createServer as createServer3 } from "net";
|
|
6065
6128
|
|
|
6066
6129
|
// src/metal-provision.ts
|
|
6067
|
-
import { createHash } from "crypto";
|
|
6130
|
+
import { createHash as createHash2 } from "crypto";
|
|
6068
6131
|
import {
|
|
6069
6132
|
existsSync as existsSync5,
|
|
6070
6133
|
mkdirSync as mkdirSync3,
|
|
@@ -6217,8 +6280,8 @@ function membersOfLinuxList(value, label) {
|
|
|
6217
6280
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
6218
6281
|
return members;
|
|
6219
6282
|
}
|
|
6220
|
-
var guestNameFor = (computeKey) => `fzg-${
|
|
6221
|
-
var tapNameFor = (computeKey) => `fzt${
|
|
6283
|
+
var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
6284
|
+
var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
6222
6285
|
var macForAddress = (address) => {
|
|
6223
6286
|
const octets = address.split(".").map(Number);
|
|
6224
6287
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
@@ -6300,7 +6363,7 @@ function allocateAddress(profile, computeKey, rows) {
|
|
|
6300
6363
|
return existing.address;
|
|
6301
6364
|
const used = new Set(rows.map((row) => row.address));
|
|
6302
6365
|
const width = profile.addressEnd - profile.addressStart + 1;
|
|
6303
|
-
const start =
|
|
6366
|
+
const start = createHash2("sha256").update(computeKey).digest().readUInt16BE(0) % width;
|
|
6304
6367
|
for (let offset = 0;offset < width; offset += 1) {
|
|
6305
6368
|
const last = profile.addressStart + (start + offset) % width;
|
|
6306
6369
|
const address = `${profile.subnetPrefix}.${last}`;
|
|
@@ -7515,7 +7578,7 @@ function materializeWarpMdm(options) {
|
|
|
7515
7578
|
}
|
|
7516
7579
|
|
|
7517
7580
|
// src/agent-update.ts
|
|
7518
|
-
import { createHash as
|
|
7581
|
+
import { createHash as createHash3, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
|
|
7519
7582
|
import {
|
|
7520
7583
|
chmodSync as chmodSync9,
|
|
7521
7584
|
existsSync as existsSync10,
|
|
@@ -7633,7 +7696,7 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
7633
7696
|
throw new Error("agent update tarball is empty or exceeds the size limit");
|
|
7634
7697
|
}
|
|
7635
7698
|
const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
|
|
7636
|
-
const actual =
|
|
7699
|
+
const actual = createHash3("sha512").update(bytes).digest();
|
|
7637
7700
|
if (!timingSafeEqual(actual, expected))
|
|
7638
7701
|
throw new Error("agent update integrity mismatch");
|
|
7639
7702
|
writeFileSync6(archive, bytes, { mode: 384, flag: "wx" });
|
|
@@ -7875,7 +7938,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
7875
7938
|
import { readFileSync as readFileSync7 } from "fs";
|
|
7876
7939
|
|
|
7877
7940
|
// src/version.ts
|
|
7878
|
-
var VERSION2 = "0.1.
|
|
7941
|
+
var VERSION2 = "0.1.31";
|
|
7879
7942
|
|
|
7880
7943
|
// src/agent-heartbeat.ts
|
|
7881
7944
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
@@ -8164,7 +8227,7 @@ if (import.meta.main) {
|
|
|
8164
8227
|
" FZ_SEED_PATH legacy/dev seed file (default: " + DEFAULT_SEED_PATH + ")",
|
|
8165
8228
|
" FZ_NODE_KEY override the node key (default: derived from the seed)",
|
|
8166
8229
|
"",
|
|
8167
|
-
" deploy [--revision=<full-sha>] [--
|
|
8230
|
+
" deploy [--revision=<full-sha>] [--release-executor]",
|
|
8168
8231
|
" identity print this sealed seed's public identity",
|
|
8169
8232
|
" enrol consume a systemd-loaded compute capability",
|
|
8170
8233
|
" metal-helper --profile=/etc/forgezero/metal.json",
|
|
@@ -8221,9 +8284,9 @@ if (import.meta.main) {
|
|
|
8221
8284
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
8222
8285
|
if (!profilePath)
|
|
8223
8286
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
8224
|
-
const
|
|
8287
|
+
const profile2 = JSON.parse(readFileSync8(profilePath, "utf8"));
|
|
8225
8288
|
const helper = startMetalHelper({
|
|
8226
|
-
profile,
|
|
8289
|
+
profile: profile2,
|
|
8227
8290
|
socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
|
|
8228
8291
|
});
|
|
8229
8292
|
console.log(`[metal-helper] listening on ${process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET}`);
|
|
@@ -8339,8 +8402,8 @@ if (import.meta.main) {
|
|
|
8339
8402
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
8340
8403
|
if (!profilePath)
|
|
8341
8404
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
8342
|
-
const
|
|
8343
|
-
await applyMetalIsolation(
|
|
8405
|
+
const profile2 = JSON.parse(readFileSync8(profilePath, "utf8"));
|
|
8406
|
+
await applyMetalIsolation(profile2);
|
|
8344
8407
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
8345
8408
|
process.exit(0);
|
|
8346
8409
|
}
|
|
@@ -8468,7 +8531,7 @@ if (import.meta.main) {
|
|
|
8468
8531
|
op: "deploy",
|
|
8469
8532
|
request: {
|
|
8470
8533
|
revision: revisionArg?.slice("--revision=".length),
|
|
8471
|
-
|
|
8534
|
+
releaseExecutor: args.includes("--release-executor")
|
|
8472
8535
|
}
|
|
8473
8536
|
} : command2 === "cancel" ? { op: "cancel", id: idArg?.slice("--id=".length) ?? "" } : ["pause-key", "resume-key", "stop-key", "start-key"].includes(command2) ? {
|
|
8474
8537
|
op: command2,
|
|
@@ -8575,19 +8638,19 @@ if (import.meta.main) {
|
|
|
8575
8638
|
} : systemdDeploymentSecrets;
|
|
8576
8639
|
const repository = process.env.FZ_DEPLOY_REPO;
|
|
8577
8640
|
const branch = process.env.FZ_DEPLOY_BRANCH;
|
|
8578
|
-
const
|
|
8641
|
+
const profile = process.env.FZ_DEPLOY_PROFILE;
|
|
8579
8642
|
const root = process.env.FZ_DEPLOY_ROOT;
|
|
8580
8643
|
const pullEnabled = process.env.FZ_DEPLOY_PULL === "true" && Boolean(process.env.FZ_API);
|
|
8581
8644
|
if (pullEnabled && !binding) {
|
|
8582
8645
|
throw new Error("agent: outbound guest deployment requires a persisted tenant enrolment binding");
|
|
8583
8646
|
}
|
|
8584
|
-
if (root && (repository && branch &&
|
|
8647
|
+
if (root && (repository && branch && profile || pullEnabled)) {
|
|
8585
8648
|
const managers = new Map;
|
|
8586
8649
|
const managerOptions = (source, key) => ({
|
|
8587
8650
|
key,
|
|
8588
8651
|
repository: source.repository,
|
|
8589
8652
|
branch: source.branch,
|
|
8590
|
-
|
|
8653
|
+
profile: source.profile,
|
|
8591
8654
|
sourceAuth: source.auth,
|
|
8592
8655
|
root,
|
|
8593
8656
|
publicApiUrl: process.env.FZ_PUBLIC_API_URL,
|
|
@@ -8604,7 +8667,7 @@ if (import.meta.main) {
|
|
|
8604
8667
|
ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
8605
8668
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
8606
8669
|
});
|
|
8607
|
-
const staticManager = repository && branch &&
|
|
8670
|
+
const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : undefined;
|
|
8608
8671
|
if (staticManager)
|
|
8609
8672
|
managers.set("__static__", staticManager);
|
|
8610
8673
|
const control = staticManager ? startControlServer(staticManager, process.env.FZ_CONTROL_SOCKET ?? DEFAULT_CONTROL_SOCKET) : undefined;
|
|
@@ -8620,7 +8683,7 @@ if (import.meta.main) {
|
|
|
8620
8683
|
claim.pipelineKey,
|
|
8621
8684
|
claim.source.repository,
|
|
8622
8685
|
claim.source.branch,
|
|
8623
|
-
claim.source.
|
|
8686
|
+
claim.source.profile,
|
|
8624
8687
|
JSON.stringify(claim.source.auth ?? null)
|
|
8625
8688
|
].join("\x00");
|
|
8626
8689
|
const existing = managers.get(cacheKey);
|
|
@@ -8818,6 +8881,8 @@ export {
|
|
|
8818
8881
|
SUPPORTED_GUEST_IMAGE,
|
|
8819
8882
|
SOFTWARE_HELPER_UNIT_PATH,
|
|
8820
8883
|
SOFTWARE_HELPER_GROUP,
|
|
8884
|
+
SOFTWARE_CATALOG,
|
|
8885
|
+
OS_CATALOG,
|
|
8821
8886
|
MAX_AGENT_TARBALL_BYTES,
|
|
8822
8887
|
DeploymentError,
|
|
8823
8888
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|