@forgezero/agent 0.1.53 → 0.1.56

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/dist/fz-agent.js CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { randomBytes as randomBytes6 } from "crypto";
6
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync18, mkdirSync as mkdirSync15, chmodSync as chmodSync17, lstatSync as lstatSync8, statfsSync } from "fs";
6
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, existsSync as existsSync20, mkdirSync as mkdirSync16, chmodSync as chmodSync17, lstatSync as lstatSync8, statfsSync as statfsSync2 } from "fs";
7
7
  import { cpus, totalmem } from "os";
8
- import { dirname as dirname11, join as join12 } from "path";
8
+ import { dirname as dirname12, join as join12 } from "path";
9
9
 
10
10
  // ../access/dist/security.js
11
11
  var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
@@ -4641,8 +4641,8 @@ function safeOp(line) {
4641
4641
  }
4642
4642
 
4643
4643
  // src/deployment.ts
4644
- import { chmodSync as chmodSync3, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
4645
- import { createHash as createHash3, randomUUID } from "crypto";
4644
+ import { chmodSync as chmodSync3, existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statfsSync, writeFileSync as writeFileSync2 } from "fs";
4645
+ import { createHash as createHash4, randomUUID } from "crypto";
4646
4646
  import { dirname, isAbsolute as isAbsolute2, join as join2 } from "path";
4647
4647
 
4648
4648
  // ../runtime/dist/queue.js
@@ -4953,6 +4953,7 @@ import {
4953
4953
  accessSync,
4954
4954
  chmodSync as chmodSync2,
4955
4955
  copyFileSync,
4956
+ existsSync as existsSync2,
4956
4957
  mkdtempSync,
4957
4958
  mkdirSync,
4958
4959
  readFileSync,
@@ -4973,6 +4974,7 @@ var OS_CATALOG = [
4973
4974
  ];
4974
4975
  var SOFTWARE_CATALOG = [
4975
4976
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4977
+ { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4976
4978
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4977
4979
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4978
4980
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -4982,6 +4984,7 @@ var SOFTWARE_CATALOG = [
4982
4984
  ];
4983
4985
  var UBUNTU_2604_X64 = [
4984
4986
  { requirement: { id: "bun", version: "1.3.14" } },
4987
+ { requirement: { id: "docker", version: "ubuntu-26.04" } },
4985
4988
  { requirement: { id: "nginx", version: "ubuntu-26.04" } },
4986
4989
  { requirement: { id: "arangodb", version: "3.11.14" } },
4987
4990
  { requirement: { id: "cloudflared", version: "2026.7.3" } },
@@ -4990,6 +4993,15 @@ var UBUNTU_2604_X64 = [
4990
4993
  { requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
4991
4994
  ];
4992
4995
  var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
4996
+ var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
4997
+ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
4998
+ "data-root": "/var/lib/docker",
4999
+ "storage-driver": "overlay2",
5000
+ "live-restore": true,
5001
+ "log-driver": "local",
5002
+ "log-opts": { "max-size": "10m", "max-file": "3" }
5003
+ }, null, 2)}
5004
+ `;
4993
5005
  var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
4994
5006
  var runSoftwareCommand = async (argv, env = {}) => {
4995
5007
  let child;
@@ -5032,6 +5044,15 @@ async function executeSoftwareOperation(operation) {
5032
5044
  if (operation.kind === "check") {
5033
5045
  if (software === "bun")
5034
5046
  return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
5047
+ if (software === "docker") {
5048
+ const binary = await run(["/usr/bin/docker", "--version"]);
5049
+ if (!successful(binary, /Docker version/))
5050
+ return { ...binary, exitCode: 1 };
5051
+ if (!existsSync2(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
5052
+ return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
5053
+ const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
5054
+ return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
5055
+ }
5035
5056
  if (software === "nginx") {
5036
5057
  const binary = await run(["/usr/sbin/nginx", "-v"]);
5037
5058
  return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
@@ -5064,11 +5085,22 @@ async function executeSoftwareOperation(operation) {
5064
5085
  return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
5065
5086
  }
5066
5087
  }
5067
- if (software === "nginx" || software === "ufw" || software === "openssh-client") {
5068
- const installed = await aptInstall(software === "openssh-client" ? "openssh-client" : software);
5069
- if (installed.exitCode !== 0 || software !== "nginx")
5088
+ if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
5089
+ const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
5090
+ const installed = await aptInstall(packageName);
5091
+ if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
5070
5092
  return installed;
5071
- return run(["/usr/bin/systemctl", "enable", "--now", "nginx.service"]);
5093
+ if (software === "docker") {
5094
+ mkdirSync("/etc/docker", { recursive: true, mode: 493 });
5095
+ if (existsSync2(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
5096
+ return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
5097
+ }
5098
+ if (!existsSync2(DOCKER_DAEMON_PATH))
5099
+ writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
5100
+ const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
5101
+ return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
5102
+ }
5103
+ return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
5072
5104
  }
5073
5105
  const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
5074
5106
  try {
@@ -5153,7 +5185,7 @@ function validateSoftwareRequirements(value, _options = {}) {
5153
5185
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
5154
5186
  throw new Error("software requirement contains an unknown field");
5155
5187
  }
5156
- if (!["bun", "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)) {
5188
+ 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)) {
5157
5189
  throw new Error("software requirement coordinate is invalid");
5158
5190
  }
5159
5191
  const requirement = { id: row.id, version: row.version };
@@ -5888,6 +5920,1206 @@ async function runPipeline(options) {
5888
5920
  return { pipeline: pipeline.name, ok: !failed, assurance, steps };
5889
5921
  }
5890
5922
 
5923
+ // src/deploy-plan.ts
5924
+ import { createHash as createHash3 } from "crypto";
5925
+
5926
+ // src/deploy-actions.ts
5927
+ var row = (value, where) => {
5928
+ if (!value || typeof value !== "object" || Array.isArray(value))
5929
+ throw new Error(`${where} must be an object`);
5930
+ return value;
5931
+ };
5932
+ var exact = (value, keys, where) => {
5933
+ const unknown = Object.keys(value).filter((key) => !keys.includes(key));
5934
+ if (unknown.length)
5935
+ throw new Error(`${where} contains unknown field(s): ${unknown.join(", ")}`);
5936
+ for (const key of keys)
5937
+ if (value[key] === undefined)
5938
+ throw new Error(`${where} requires ${key}`);
5939
+ };
5940
+ var component = (value, components, where) => {
5941
+ if (typeof value !== "string" || !components[value])
5942
+ throw new Error(`${where}.component must name an existing component`);
5943
+ return components[value];
5944
+ };
5945
+ var exactArgv = (value, where) => {
5946
+ if (!Array.isArray(value) || value.length < 1 || value.length > 256 || value.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 16384 || entry.includes("\x00")))
5947
+ throw new Error(`${where} requires a bounded exact argv array`);
5948
+ const executable = value[0].split("/").at(-1).toLowerCase();
5949
+ if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
5950
+ throw new Error(`${where} cannot invoke a command dispatcher`);
5951
+ };
5952
+ function validateBuiltInAction(definition, context, where) {
5953
+ if (!definition.uses.startsWith("forgezero."))
5954
+ return;
5955
+ const withValue = row(definition.with, `${where}.with`);
5956
+ if (definition.uses === "forgezero.software/ensure@1") {
5957
+ exact(withValue, ["requirements"], `${where}.with`);
5958
+ if (!Array.isArray(withValue.requirements) || withValue.requirements.length < 1 || withValue.requirements.length > 64 || withValue.requirements.some((name) => typeof name !== "string" || !context.requirements[name]))
5959
+ throw new Error(`${where}.with.requirements must name existing requirements`);
5960
+ return;
5961
+ }
5962
+ if (definition.uses === "forgezero.exec/argv@1") {
5963
+ const allowed = ["component", "argv", "credentials"];
5964
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
5965
+ if (unknown.length)
5966
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
5967
+ component(withValue.component, context.components, `${where}.with`);
5968
+ exactArgv(withValue.argv, `${where}.with.argv`);
5969
+ if (withValue.credentials !== undefined) {
5970
+ const bindings = row(withValue.credentials, `${where}.with.credentials`);
5971
+ for (const [environment, credentialName] of Object.entries(bindings))
5972
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(environment) || typeof credentialName !== "string" || !context.credentials[credentialName])
5973
+ throw new Error(`${where}.with.credentials contains an invalid binding`);
5974
+ }
5975
+ return;
5976
+ }
5977
+ if (["forgezero.oci/build@1", "forgezero.oci/pull@1"].includes(definition.uses)) {
5978
+ const allowed = definition.uses.endsWith("/build@1") ? ["component", "noCache"] : ["component"];
5979
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
5980
+ if (unknown.length)
5981
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
5982
+ const selected = component(withValue.component, context.components, `${where}.with`);
5983
+ if (selected.kind !== "application" || selected.runtime.kind !== "container")
5984
+ throw new Error(`${where}.with.component must use a container runtime`);
5985
+ if (withValue.noCache !== undefined && typeof withValue.noCache !== "boolean")
5986
+ throw new Error(`${where}.with.noCache must be a boolean`);
5987
+ return;
5988
+ }
5989
+ if (["forgezero.service/deploy@1", "forgezero.service/promote@1", "forgezero.health/http@1", "forgezero.storage/verify@1"].includes(definition.uses)) {
5990
+ const allowed = definition.uses === "forgezero.service/deploy@1" || definition.uses === "forgezero.service/promote@1" ? ["component", "imageDigest"] : ["component"];
5991
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
5992
+ if (unknown.length)
5993
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
5994
+ component(withValue.component, context.components, `${where}.with`);
5995
+ return;
5996
+ }
5997
+ throw new Error(`${where}.uses is an unknown ForgeZero action`);
5998
+ }
5999
+ // src/deploy-providers.ts
6000
+ var plain = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6001
+ var exact2 = (row2, keys, provider) => {
6002
+ const unknown = Object.keys(row2).filter((key) => !keys.includes(key));
6003
+ if (unknown.length)
6004
+ throw new Error(`${provider} config contains unknown field(s): ${unknown.join(", ")}`);
6005
+ for (const key of keys)
6006
+ if (row2[key] === undefined)
6007
+ throw new Error(`${provider} config requires ${key}`);
6008
+ };
6009
+ var absolute = (value) => typeof value === "string" && /^\/(?:[A-Za-z0-9._-]+\/?)*$/.test(value) && !value.includes("..");
6010
+ function validateBuiltInProvider(requirement2, where) {
6011
+ const row2 = plain(requirement2.config);
6012
+ if (!requirement2.provider.startsWith("forgezero."))
6013
+ return;
6014
+ const expected = {
6015
+ "forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
6016
+ "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
6017
+ "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
6018
+ "forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
6019
+ "forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
6020
+ "forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
6021
+ "forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
6022
+ "forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
6023
+ "forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
6024
+ };
6025
+ const contract = expected[requirement2.provider];
6026
+ if (!contract)
6027
+ throw new Error(`${where} uses unknown built-in provider ${requirement2.provider}`);
6028
+ if (requirement2.contract !== 1 || requirement2.capability !== contract.capability || !contract.versions.includes(requirement2.version))
6029
+ throw new Error(`${where} provider coordinate is unsupported`);
6030
+ exact2(row2, contract.keys, requirement2.provider);
6031
+ if (row2.installScope !== "host")
6032
+ throw new Error(`${where}.config.installScope must be host`);
6033
+ if (requirement2.provider === "forgezero.docker") {
6034
+ if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
6035
+ throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
6036
+ }
6037
+ if (requirement2.provider === "forgezero.nginx" && (typeof row2.websocket !== "boolean" || !Number.isInteger(row2.maximumBodyMiB) || Number(row2.maximumBodyMiB) < 1 || Number(row2.maximumBodyMiB) > 1024 || !(row2.workerProcesses === "auto" || Number.isInteger(row2.workerProcesses)) || !Number.isInteger(row2.workerConnections)))
6038
+ throw new Error(`${where} Nginx config is invalid`);
6039
+ if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
6040
+ throw new Error(`${where} ArangoDB config is invalid`);
6041
+ }
6042
+
6043
+ // src/deploy-plan.ts
6044
+ var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
6045
+ var DEPLOY_PLAN_VERSION = 1;
6046
+
6047
+ class DeploymentPlanError extends Error {
6048
+ constructor(message) {
6049
+ super(message);
6050
+ this.name = "DeploymentPlanError";
6051
+ }
6052
+ }
6053
+ var NAME2 = /^[a-z][A-Za-z0-9-]{0,62}$/;
6054
+ var COORDINATE = /^[a-z][a-z0-9.-]{0,127}$/;
6055
+ var ACTION = /^[a-z][a-z0-9.-]{0,127}\/[a-z][a-z0-9.-]{0,127}@[1-9][0-9]{0,5}$/;
6056
+ var VERSION = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
6057
+ var ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)*$/;
6058
+ var HEALTH_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
6059
+ var fail = (where, message) => {
6060
+ throw new DeploymentPlanError(`${where} ${message}`);
6061
+ };
6062
+ var object = (value, where) => {
6063
+ if (!value || typeof value !== "object" || Array.isArray(value))
6064
+ fail(where, "must be an object.");
6065
+ return value;
6066
+ };
6067
+ var exact3 = (value, allowed, where) => {
6068
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
6069
+ if (unknown.length)
6070
+ fail(where, `contains unknown field(s): ${unknown.join(", ")}.`);
6071
+ };
6072
+ var named = (value, where) => {
6073
+ if (typeof value !== "string" || !NAME2.test(value))
6074
+ fail(where, "must be a lowercase typed name.");
6075
+ return value;
6076
+ };
6077
+ var boundedString = (value, where, maximum = 256) => {
6078
+ if (typeof value !== "string" || value.length < 1 || value.length > maximum || value.includes("\x00"))
6079
+ fail(where, `must be a non-empty string of at most ${maximum} characters.`);
6080
+ return value;
6081
+ };
6082
+ var integer = (value, where, minimum, maximum) => {
6083
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
6084
+ fail(where, `must be an integer from ${minimum} to ${maximum}.`);
6085
+ return value;
6086
+ };
6087
+ var number = (value, where, minimum, maximum) => {
6088
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum)
6089
+ fail(where, `must be a finite number from ${minimum} to ${maximum}.`);
6090
+ return value;
6091
+ };
6092
+ function canonicalJson(value) {
6093
+ if (value === null || typeof value === "boolean" || typeof value === "string")
6094
+ return JSON.stringify(value);
6095
+ if (typeof value === "number") {
6096
+ if (!Number.isFinite(value))
6097
+ throw new DeploymentPlanError("deployment data contains a non-finite number.");
6098
+ return JSON.stringify(value);
6099
+ }
6100
+ if (Array.isArray(value))
6101
+ return `[${value.map(canonicalJson).join(",")}]`;
6102
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
6103
+ return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
6104
+ }
6105
+ throw new DeploymentPlanError("deployment data must contain only plain JSON values.");
6106
+ }
6107
+ function deploymentSourceDigest(source) {
6108
+ return `sha256:${createHash3("sha256").update(canonicalJson(source)).digest("hex")}`;
6109
+ }
6110
+ function reference(value, where) {
6111
+ const row2 = object(value, where);
6112
+ exact3(row2, ["$ref"], where);
6113
+ const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
6114
+ if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62})$/.test(coordinate)) {
6115
+ fail(where, "contains an unsupported reference.");
6116
+ }
6117
+ return { $ref: coordinate };
6118
+ }
6119
+ function operand(value, where) {
6120
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
6121
+ if (typeof value === "number" && !Number.isFinite(value))
6122
+ fail(where, "must be finite.");
6123
+ return value;
6124
+ }
6125
+ return reference(value, where);
6126
+ }
6127
+ function condition(value, where, depth = 0) {
6128
+ if (depth > 16)
6129
+ fail(where, "is nested too deeply.");
6130
+ const row2 = object(value, where);
6131
+ if (Object.keys(row2).length !== 1)
6132
+ fail(where, "must contain exactly one operator.");
6133
+ const [operator, body] = Object.entries(row2)[0];
6134
+ if (operator === "all" || operator === "any") {
6135
+ if (!Array.isArray(body) || body.length < 1 || body.length > 32)
6136
+ fail(where, `${operator} must contain 1 to 32 conditions.`);
6137
+ const entries = body;
6138
+ return { [operator]: entries.map((entry, index) => condition(entry, `${where}.${operator}[${index}]`, depth + 1)) };
6139
+ }
6140
+ if (operator === "not")
6141
+ return { not: condition(body, `${where}.not`, depth + 1) };
6142
+ if (["equals", "notEquals", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "contains", "startsWith"].includes(operator)) {
6143
+ if (!Array.isArray(body) || body.length !== 2)
6144
+ fail(where, `${operator} must contain two operands.`);
6145
+ const entries = body;
6146
+ return { [operator]: [operand(entries[0], `${where}.${operator}[0]`), operand(entries[1], `${where}.${operator}[1]`)] };
6147
+ }
6148
+ if (operator === "exists")
6149
+ return { exists: reference(body, `${where}.exists`) };
6150
+ if (["succeeded", "failed", "changed"].includes(operator))
6151
+ return { [operator]: named(body, `${where}.${operator}`) };
6152
+ return fail(where, `uses unsupported operator ${operator}.`);
6153
+ }
6154
+ function jsonValue(value, where, depth = 0) {
6155
+ if (depth > 16)
6156
+ fail(where, "is nested too deeply.");
6157
+ if (value === null || typeof value === "string" || typeof value === "boolean")
6158
+ return value;
6159
+ if (typeof value === "number") {
6160
+ if (!Number.isFinite(value))
6161
+ fail(where, "must be finite.");
6162
+ return value;
6163
+ }
6164
+ if (Array.isArray(value)) {
6165
+ if (value.length > 1000)
6166
+ fail(where, "contains more than 1000 entries.");
6167
+ return value.map((entry, index) => jsonValue(entry, `${where}[${index}]`, depth + 1));
6168
+ }
6169
+ const row2 = object(value, where);
6170
+ if (Object.keys(row2).length > 1000)
6171
+ fail(where, "contains more than 1000 fields.");
6172
+ return Object.fromEntries(Object.entries(row2).map(([key, entry]) => [key, jsonValue(entry, `${where}.${key}`, depth + 1)]));
6173
+ }
6174
+ function validateInput(value, where) {
6175
+ const row2 = object(value, where);
6176
+ const type = row2.type;
6177
+ const common = ["type", "required", "default"];
6178
+ if (row2.required !== undefined && typeof row2.required !== "boolean")
6179
+ fail(`${where}.required`, "must be a boolean.");
6180
+ if (type === "string") {
6181
+ exact3(row2, [...common, "minimumLength", "maximumLength", "pattern"], where);
6182
+ if (row2.default !== undefined)
6183
+ boundedString(row2.default, `${where}.default`, 16384);
6184
+ if (row2.minimumLength !== undefined)
6185
+ integer(row2.minimumLength, `${where}.minimumLength`, 0, 16384);
6186
+ if (row2.maximumLength !== undefined)
6187
+ integer(row2.maximumLength, `${where}.maximumLength`, 1, 16384);
6188
+ if (row2.pattern !== undefined) {
6189
+ const pattern = boundedString(row2.pattern, `${where}.pattern`, 512);
6190
+ try {
6191
+ new RegExp(pattern);
6192
+ } catch {
6193
+ fail(`${where}.pattern`, "must be a valid regular expression.");
6194
+ }
6195
+ }
6196
+ } else if (type === "integer" || type === "number") {
6197
+ exact3(row2, [...common, "minimum", "maximum"], where);
6198
+ if (row2.default !== undefined)
6199
+ number(row2.default, `${where}.default`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
6200
+ if (row2.minimum !== undefined)
6201
+ number(row2.minimum, `${where}.minimum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
6202
+ if (row2.maximum !== undefined)
6203
+ number(row2.maximum, `${where}.maximum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
6204
+ if (type === "integer" && row2.default !== undefined && !Number.isInteger(row2.default))
6205
+ fail(`${where}.default`, "must be an integer.");
6206
+ } else if (type === "boolean") {
6207
+ exact3(row2, common, where);
6208
+ if (row2.default !== undefined && typeof row2.default !== "boolean")
6209
+ fail(`${where}.default`, "must be a boolean.");
6210
+ } else if (type === "enum") {
6211
+ exact3(row2, [...common, "values"], where);
6212
+ if (!Array.isArray(row2.values) || row2.values.length < 1 || row2.values.length > 64)
6213
+ fail(`${where}.values`, "must contain 1 to 64 values.");
6214
+ const values = row2.values.map((entry, index) => boundedString(entry, `${where}.values[${index}]`, 128));
6215
+ if (new Set(values).size !== values.length)
6216
+ fail(`${where}.values`, "must be unique.");
6217
+ if (row2.default !== undefined && !values.includes(row2.default))
6218
+ fail(`${where}.default`, "must be an enum value.");
6219
+ } else if (type === "hostname") {
6220
+ exact3(row2, common, where);
6221
+ if (row2.default !== undefined && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(String(row2.default)))
6222
+ fail(`${where}.default`, "must be a hostname.");
6223
+ } else
6224
+ fail(`${where}.type`, "is unsupported.");
6225
+ return row2;
6226
+ }
6227
+ function validateRequirement(value, where) {
6228
+ const row2 = object(value, where);
6229
+ exact3(row2, ["capability", "provider", "contract", "version", "config", "when"], where);
6230
+ if (!COORDINATE.test(String(row2.capability)))
6231
+ fail(`${where}.capability`, "is invalid.");
6232
+ if (!COORDINATE.test(String(row2.provider)))
6233
+ fail(`${where}.provider`, "is invalid.");
6234
+ integer(row2.contract, `${where}.contract`, 1, 65535);
6235
+ if (typeof row2.version !== "string" || !VERSION.test(row2.version))
6236
+ fail(`${where}.version`, "is invalid.");
6237
+ jsonValue(row2.config, `${where}.config`);
6238
+ if (row2.when !== undefined)
6239
+ condition(row2.when, `${where}.when`);
6240
+ const requirement2 = row2;
6241
+ validateBuiltInProvider(requirement2, where);
6242
+ return requirement2;
6243
+ }
6244
+ function validateArgv(value, where) {
6245
+ if (!Array.isArray(value) || value.length < 1 || value.length > 256)
6246
+ fail(where, "must contain 1 to 256 arguments.");
6247
+ let bytes = 0;
6248
+ const values = value.map((entry, index) => {
6249
+ const result = boundedString(entry, `${where}[${index}]`, 16384);
6250
+ bytes += Buffer.byteLength(result);
6251
+ return result;
6252
+ });
6253
+ if (bytes > 64 * 1024)
6254
+ fail(where, "is larger than 64 KiB.");
6255
+ const executable = values[0].split("/").at(-1).toLowerCase();
6256
+ if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
6257
+ fail(`${where}[0]`, "may not invoke a command dispatcher.");
6258
+ return values;
6259
+ }
6260
+ function validateResources(value, where) {
6261
+ const row2 = object(value, where);
6262
+ exact3(row2, ["cpu", "memory", "pids", "io"], where);
6263
+ if (row2.cpu !== undefined) {
6264
+ const cpu = object(row2.cpu, `${where}.cpu`);
6265
+ exact3(cpu, ["limit", "weight", "pinning"], `${where}.cpu`);
6266
+ number(cpu.limit, `${where}.cpu.limit`, 0.01, 1024);
6267
+ if (cpu.weight !== undefined)
6268
+ integer(cpu.weight, `${where}.cpu.weight`, 1, 1e4);
6269
+ if (cpu.pinning !== undefined && !["automatic", "dedicated"].includes(String(cpu.pinning)))
6270
+ fail(`${where}.cpu.pinning`, "is unsupported.");
6271
+ }
6272
+ if (row2.memory !== undefined) {
6273
+ const memory = object(row2.memory, `${where}.memory`);
6274
+ exact3(memory, ["limitMiB", "reservationMiB", "swap"], `${where}.memory`);
6275
+ integer(memory.limitMiB, `${where}.memory.limitMiB`, 16, 16777216);
6276
+ if (memory.reservationMiB !== undefined && integer(memory.reservationMiB, `${where}.memory.reservationMiB`, 1, memory.limitMiB) > memory.limitMiB)
6277
+ fail(`${where}.memory.reservationMiB`, "must not exceed limitMiB.");
6278
+ if (memory.swap !== undefined && !["disabled", "bounded"].includes(String(memory.swap)))
6279
+ fail(`${where}.memory.swap`, "is unsupported.");
6280
+ }
6281
+ if (row2.pids !== undefined) {
6282
+ const pids = object(row2.pids, `${where}.pids`);
6283
+ exact3(pids, ["limit"], `${where}.pids`);
6284
+ integer(pids.limit, `${where}.pids.limit`, 1, 1048576);
6285
+ }
6286
+ if (row2.io !== undefined) {
6287
+ const io = object(row2.io, `${where}.io`);
6288
+ exact3(io, ["weight", "readBps", "writeBps", "readIops", "writeIops"], `${where}.io`);
6289
+ for (const key of Object.keys(io))
6290
+ integer(io[key], `${where}.io.${key}`, 1, Number.MAX_SAFE_INTEGER);
6291
+ }
6292
+ }
6293
+ function validateStorage(value, where) {
6294
+ const row2 = object(value, where);
6295
+ if (row2.class === "ephemeral") {
6296
+ exact3(row2, ["class", "path", "type", "sizeMiB"], where);
6297
+ if (row2.type !== "tmpfs")
6298
+ fail(`${where}.type`, "must be tmpfs.");
6299
+ integer(row2.sizeMiB, `${where}.sizeMiB`, 1, 1048576);
6300
+ } else if (row2.class === "persistent") {
6301
+ exact3(row2, ["class", "name", "path", "minimumFreePercent"], where);
6302
+ named(row2.name, `${where}.name`);
6303
+ if (row2.minimumFreePercent !== undefined)
6304
+ integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
6305
+ } else if (row2.class === "database") {
6306
+ exact3(row2, ["class", "name", "path", "minimumFreePercent", "minimumIops", "latencyTargetMs"], where);
6307
+ named(row2.name, `${where}.name`);
6308
+ integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
6309
+ if (row2.minimumIops !== undefined)
6310
+ integer(row2.minimumIops, `${where}.minimumIops`, 1, 1e8);
6311
+ if (row2.latencyTargetMs !== undefined)
6312
+ number(row2.latencyTargetMs, `${where}.latencyTargetMs`, 0.01, 60000);
6313
+ } else
6314
+ fail(`${where}.class`, "is unsupported.");
6315
+ if (typeof row2.path !== "string" || !ABSOLUTE_PATH.test(row2.path) || row2.path.includes(".."))
6316
+ fail(`${where}.path`, "must be a normalized absolute path.");
6317
+ }
6318
+ function validateNetwork(value, where) {
6319
+ const row2 = object(value, where);
6320
+ exact3(row2, ["ingress", "private", "public", "container"], where);
6321
+ if (row2.ingress !== undefined) {
6322
+ const ingress = object(row2.ingress, `${where}.ingress`);
6323
+ exact3(ingress, ["exposure", "stablePort"], `${where}.ingress`);
6324
+ if (!["loopback", "private", "public"].includes(String(ingress.exposure)))
6325
+ fail(`${where}.ingress.exposure`, "is unsupported.");
6326
+ if (ingress.stablePort !== undefined)
6327
+ integer(ingress.stablePort, `${where}.ingress.stablePort`, 1, 65535);
6328
+ }
6329
+ if (row2.private !== undefined) {
6330
+ const privateNetwork = object(row2.private, `${where}.private`);
6331
+ exact3(privateNetwork, ["mode", "preferences"], `${where}.private`);
6332
+ if (!["private-lan", "cloudflare-warp", "auto"].includes(String(privateNetwork.mode)))
6333
+ fail(`${where}.private.mode`, "is unsupported.");
6334
+ if (privateNetwork.preferences !== undefined && (!Array.isArray(privateNetwork.preferences) || privateNetwork.preferences.length < 1 || privateNetwork.preferences.length > 2 || privateNetwork.preferences.some((entry) => !["private-lan", "cloudflare-warp"].includes(String(entry))) || new Set(privateNetwork.preferences).size !== privateNetwork.preferences.length))
6335
+ fail(`${where}.private.preferences`, "is invalid.");
6336
+ }
6337
+ if (row2.public !== undefined) {
6338
+ const publicNetwork = object(row2.public, `${where}.public`);
6339
+ exact3(publicNetwork, ["mode", "optional"], `${where}.public`);
6340
+ if (!["disabled", "cloudflare-tunnel"].includes(String(publicNetwork.mode)))
6341
+ fail(`${where}.public.mode`, "is unsupported.");
6342
+ if (publicNetwork.optional !== undefined && typeof publicNetwork.optional !== "boolean")
6343
+ fail(`${where}.public.optional`, "must be a boolean.");
6344
+ }
6345
+ if (row2.container !== undefined) {
6346
+ const container = object(row2.container, `${where}.container`);
6347
+ exact3(container, ["mode", "network"], `${where}.container`);
6348
+ if (!["bridge", "host"].includes(String(container.mode)))
6349
+ fail(`${where}.container.mode`, "is unsupported.");
6350
+ if (container.network !== undefined)
6351
+ named(container.network, `${where}.container.network`);
6352
+ }
6353
+ }
6354
+ function validateComponent(value, where, targetNames, requirements) {
6355
+ const row2 = object(value, where);
6356
+ if (!targetNames.has(String(row2.target)))
6357
+ fail(`${where}.target`, "must name an existing target.");
6358
+ if (row2.kind === "application") {
6359
+ exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
6360
+ const runtime = object(row2.runtime, `${where}.runtime`);
6361
+ const runtimeRequirement = requirements.get(String(runtime.requirement));
6362
+ if (!runtimeRequirement)
6363
+ fail(`${where}.runtime.requirement`, "must name an existing requirement.");
6364
+ if (runtime.kind === "native") {
6365
+ exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
6366
+ validateArgv(runtime.argv, `${where}.runtime.argv`);
6367
+ } else if (runtime.kind === "container") {
6368
+ exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
6369
+ const image = object(runtime.image, `${where}.runtime.image`);
6370
+ exact3(image, ["source"], `${where}.runtime.image`);
6371
+ const source = object(image.source, `${where}.runtime.image.source`);
6372
+ if (source.kind === "build") {
6373
+ exact3(source, ["kind", "context", "dockerfile"], `${where}.runtime.image.source`);
6374
+ boundedString(source.context, `${where}.runtime.image.source.context`, 512);
6375
+ boundedString(source.dockerfile, `${where}.runtime.image.source.dockerfile`, 512);
6376
+ } else if (source.kind === "registry") {
6377
+ exact3(source, ["kind", "reference"], `${where}.runtime.image.source`);
6378
+ if (!/^[-a-zA-Z0-9./:_@]+$/.test(boundedString(source.reference, `${where}.runtime.image.source.reference`, 512)))
6379
+ fail(`${where}.runtime.image.source.reference`, "is invalid.");
6380
+ } else
6381
+ fail(`${where}.runtime.image.source.kind`, "is unsupported.");
6382
+ if (runtime.entrypoint !== undefined)
6383
+ validateArgv(runtime.entrypoint, `${where}.runtime.entrypoint`);
6384
+ const security = object(runtime.security, `${where}.runtime.security`);
6385
+ exact3(security, ["privileged", "noNewPrivileges", "root", "dropCapabilities"], `${where}.runtime.security`);
6386
+ if (security.privileged !== false || security.noNewPrivileges !== true)
6387
+ fail(`${where}.runtime.security`, "must disable privileged mode and enable noNewPrivileges.");
6388
+ if (!["read-only", "writable"].includes(String(security.root)))
6389
+ fail(`${where}.runtime.security.root`, "is unsupported.");
6390
+ if (!Array.isArray(security.dropCapabilities) || !security.dropCapabilities.includes("ALL"))
6391
+ fail(`${where}.runtime.security.dropCapabilities`, "must include ALL.");
6392
+ } else
6393
+ fail(`${where}.runtime.kind`, "is unsupported.");
6394
+ if (!COORDINATE.test(String(runtime.provider)))
6395
+ fail(`${where}.runtime.provider`, "is invalid.");
6396
+ if (runtimeRequirement && runtime.provider !== runtimeRequirement.provider)
6397
+ fail(`${where}.runtime.provider`, "must match its requirement provider.");
6398
+ if (runtimeRequirement && runtime.kind === "container" && runtimeRequirement.capability !== "runtime.container")
6399
+ fail(`${where}.runtime.requirement`, "must provide runtime.container.");
6400
+ if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
6401
+ fail(`${where}.runtime.requirement`, "must provide a runtime capability.");
6402
+ const service = object(row2.service, `${where}.service`);
6403
+ exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
6404
+ if (service.protocol !== "http")
6405
+ fail(`${where}.service.protocol`, "must be http.");
6406
+ integer(service.port, `${where}.service.port`, 1, 65535);
6407
+ const health = object(service.health, `${where}.service.health`);
6408
+ exact3(health, ["protocol", "method", "path", "expectedStatus", "timeoutMs", "intervalMs", "attempts"], `${where}.service.health`);
6409
+ if (health.protocol !== "http" || !["GET", "HEAD"].includes(String(health.method)) || typeof health.path !== "string" || !HEALTH_PATH.test(health.path) || health.path.includes("..") || health.path.includes("//"))
6410
+ fail(`${where}.service.health`, "is invalid.");
6411
+ if (!Array.isArray(health.expectedStatus) || health.expectedStatus.length < 1 || health.expectedStatus.length > 16 || health.expectedStatus.some((status) => !Number.isInteger(status) || status < 100 || status > 599))
6412
+ fail(`${where}.service.health.expectedStatus`, "is invalid.");
6413
+ integer(health.timeoutMs, `${where}.service.health.timeoutMs`, 100, 300000);
6414
+ if (health.intervalMs !== undefined)
6415
+ integer(health.intervalMs, `${where}.service.health.intervalMs`, 100, 300000);
6416
+ if (health.attempts !== undefined)
6417
+ integer(health.attempts, `${where}.service.health.attempts`, 1, 1000);
6418
+ if (service.websocket !== undefined && typeof service.websocket !== "boolean")
6419
+ fail(`${where}.service.websocket`, "must be a boolean.");
6420
+ if (service.maximumConnections !== undefined)
6421
+ integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
6422
+ validateResources(row2.resources, `${where}.resources`);
6423
+ if (runtime.kind === "container") {
6424
+ const resources = object(row2.resources, `${where}.resources`);
6425
+ for (const required of ["cpu", "memory", "pids"])
6426
+ if (resources[required] === undefined)
6427
+ fail(`${where}.resources.${required}`, "is required for a bounded container.");
6428
+ if (row2.network === undefined)
6429
+ fail(`${where}.network`, "is required for a container.");
6430
+ const network = object(row2.network, `${where}.network`);
6431
+ if (network.container === undefined)
6432
+ fail(`${where}.network.container`, "must select bridge or reviewed host networking.");
6433
+ }
6434
+ if (row2.storage !== undefined) {
6435
+ if (!Array.isArray(row2.storage) || row2.storage.length > 32)
6436
+ fail(`${where}.storage`, "must contain at most 32 mounts.");
6437
+ row2.storage.forEach((entry, index) => validateStorage(entry, `${where}.storage[${index}]`));
6438
+ }
6439
+ if (row2.network !== undefined)
6440
+ validateNetwork(row2.network, `${where}.network`);
6441
+ const rollout = object(row2.rollout, `${where}.rollout`);
6442
+ exact3(rollout, ["strategy", "proxy", "drainMs", "automaticRollback"], `${where}.rollout`);
6443
+ if (!["direct", "blue-green", "rolling", "canary"].includes(String(rollout.strategy)))
6444
+ fail(`${where}.rollout.strategy`, "is unsupported.");
6445
+ if (rollout.proxy !== undefined)
6446
+ named(rollout.proxy, `${where}.rollout.proxy`);
6447
+ if (rollout.strategy !== "direct") {
6448
+ const proxyRequirement = requirements.get(String(rollout.proxy));
6449
+ if (!proxyRequirement || proxyRequirement.capability !== "proxy.http")
6450
+ fail(`${where}.rollout.proxy`, "must name a proxy.http requirement for managed rollout.");
6451
+ }
6452
+ if (rollout.drainMs !== undefined)
6453
+ integer(rollout.drainMs, `${where}.rollout.drainMs`, 0, 600000);
6454
+ if (rollout.automaticRollback !== undefined && typeof rollout.automaticRollback !== "boolean")
6455
+ fail(`${where}.rollout.automaticRollback`, "must be a boolean.");
6456
+ } else if (row2.kind === "database") {
6457
+ exact3(row2, ["kind", "target", "runtime", "topology", "resources", "storage", "network"], where);
6458
+ const runtime = object(row2.runtime, `${where}.runtime`);
6459
+ exact3(runtime, ["provider", "requirement", "mode"], `${where}.runtime`);
6460
+ const databaseRequirement = requirements.get(String(runtime.requirement));
6461
+ if (!databaseRequirement)
6462
+ fail(`${where}.runtime.requirement`, "must name an existing requirement.");
6463
+ if (databaseRequirement && (runtime.provider !== databaseRequirement.provider || databaseRequirement.capability !== "database.arangodb"))
6464
+ fail(`${where}.runtime.requirement`, "must match a database.arangodb provider requirement.");
6465
+ if (databaseRequirement && databaseRequirement.config.runtime !== runtime.mode)
6466
+ fail(`${where}.runtime.mode`, "must match the provider runtime mode.");
6467
+ if (runtime.mode !== "native")
6468
+ fail(`${where}.runtime.mode`, "must be native in deployment plan v1.");
6469
+ const topology = object(row2.topology, `${where}.topology`);
6470
+ if (topology.mode === "standalone")
6471
+ exact3(topology, ["mode"], `${where}.topology`);
6472
+ else if (topology.mode === "cluster") {
6473
+ exact3(topology, ["mode", "bootstrapMembers", "replicationFactor", "writeConcern", "additionalJoiners"], `${where}.topology`);
6474
+ integer(topology.bootstrapMembers, `${where}.topology.bootstrapMembers`, 3, 9);
6475
+ integer(topology.replicationFactor, `${where}.topology.replicationFactor`, 1, 16);
6476
+ integer(topology.writeConcern, `${where}.topology.writeConcern`, 1, topology.replicationFactor);
6477
+ if (!["allowed", "disabled"].includes(String(topology.additionalJoiners)))
6478
+ fail(`${where}.topology.additionalJoiners`, "is unsupported.");
6479
+ } else
6480
+ fail(`${where}.topology.mode`, "is unsupported.");
6481
+ if (row2.resources !== undefined)
6482
+ validateResources(row2.resources, `${where}.resources`);
6483
+ validateStorage(row2.storage, `${where}.storage`);
6484
+ if (row2.storage.class !== "database")
6485
+ fail(`${where}.storage.class`, "must be database.");
6486
+ validateNetwork(row2.network, `${where}.network`);
6487
+ } else
6488
+ fail(`${where}.kind`, "is unsupported.");
6489
+ return row2;
6490
+ }
6491
+ function validateStep(value, where, targets, context) {
6492
+ const row2 = object(value, where);
6493
+ exact3(row2, ["uses", "with", "if", "scope", "timeoutMs", "retry", "failure", "compensate"], where);
6494
+ if (typeof row2.uses !== "string" || !ACTION.test(row2.uses))
6495
+ fail(`${where}.uses`, "must be a versioned action coordinate.");
6496
+ jsonValue(row2.with, `${where}.with`);
6497
+ if (row2.if !== undefined)
6498
+ condition(row2.if, `${where}.if`);
6499
+ const scope = object(row2.scope, `${where}.scope`);
6500
+ if (scope.kind === "release-executor")
6501
+ exact3(scope, ["kind"], `${where}.scope`);
6502
+ else if (scope.kind === "elected-one") {
6503
+ exact3(scope, ["kind", "group"], `${where}.scope`);
6504
+ named(scope.group, `${where}.scope.group`);
6505
+ } else if (scope.kind === "each-target") {
6506
+ exact3(scope, ["kind", "target"], `${where}.scope`);
6507
+ if (!targets.has(String(scope.target)))
6508
+ fail(`${where}.scope.target`, "must name an existing target.");
6509
+ } else if (scope.kind === "target-batches") {
6510
+ exact3(scope, ["kind", "target", "size"], `${where}.scope`);
6511
+ if (!targets.has(String(scope.target)))
6512
+ fail(`${where}.scope.target`, "must name an existing target.");
6513
+ integer(scope.size, `${where}.scope.size`, 1, 32);
6514
+ } else
6515
+ fail(`${where}.scope.kind`, "is unsupported.");
6516
+ if (row2.timeoutMs !== undefined)
6517
+ integer(row2.timeoutMs, `${where}.timeoutMs`, 1, 86400000);
6518
+ if (row2.retry !== undefined) {
6519
+ const retry = object(row2.retry, `${where}.retry`);
6520
+ exact3(retry, ["attempts", "backoff", "retryOn"], `${where}.retry`);
6521
+ integer(retry.attempts, `${where}.retry.attempts`, 1, 20);
6522
+ const backoff = object(retry.backoff, `${where}.retry.backoff`);
6523
+ exact3(backoff, ["kind", "initialMs", "maximumMs"], `${where}.retry.backoff`);
6524
+ if (!["fixed", "linear", "exponential"].includes(String(backoff.kind)))
6525
+ fail(`${where}.retry.backoff.kind`, "is unsupported.");
6526
+ integer(backoff.initialMs, `${where}.retry.backoff.initialMs`, 0, 300000);
6527
+ integer(backoff.maximumMs, `${where}.retry.backoff.maximumMs`, backoff.initialMs, 3600000);
6528
+ if (retry.retryOn !== undefined && (!Array.isArray(retry.retryOn) || retry.retryOn.length > 8 || retry.retryOn.some((entry) => !["network", "timeout", "provider-unavailable", "rate-limited", "conflict"].includes(String(entry))) || new Set(retry.retryOn).size !== retry.retryOn.length))
6529
+ fail(`${where}.retry.retryOn`, "is invalid.");
6530
+ }
6531
+ if (row2.failure !== undefined) {
6532
+ const failure = object(row2.failure, `${where}.failure`);
6533
+ exact3(failure, ["policy"], `${where}.failure`);
6534
+ if (!["continue", "stop", "rollback-stage", "rollback-deployment", "compensate", "manual-intervention"].includes(String(failure.policy)))
6535
+ fail(`${where}.failure.policy`, "is unsupported.");
6536
+ }
6537
+ if (row2.compensate !== undefined) {
6538
+ const compensate = object(row2.compensate, `${where}.compensate`);
6539
+ exact3(compensate, ["uses", "with"], `${where}.compensate`);
6540
+ if (typeof compensate.uses !== "string" || !ACTION.test(compensate.uses))
6541
+ fail(`${where}.compensate.uses`, "must be a versioned action coordinate.");
6542
+ jsonValue(compensate.with, `${where}.compensate.with`);
6543
+ if (row2.failure?.policy !== "compensate")
6544
+ fail(`${where}.compensate`, "requires failure.policy compensate.");
6545
+ }
6546
+ const definition = row2;
6547
+ validateBuiltInAction(definition, context, where);
6548
+ if (definition.compensate)
6549
+ validateBuiltInAction({ uses: definition.compensate.uses, with: definition.compensate.with, scope: definition.scope }, context, `${where}.compensate`);
6550
+ return definition;
6551
+ }
6552
+ function topologicalStages(stages, workflow) {
6553
+ const names = Object.keys(stages);
6554
+ const nameSet = new Set(names);
6555
+ for (const name of names)
6556
+ for (const dependency of stages[name].dependsOn ?? []) {
6557
+ if (!nameSet.has(dependency))
6558
+ fail(`workflows.${workflow}.stages.${name}.dependsOn`, `names missing stage ${dependency}.`);
6559
+ if (dependency === name)
6560
+ fail(`workflows.${workflow}.stages.${name}.dependsOn`, "cannot depend on itself.");
6561
+ }
6562
+ const visiting = new Set;
6563
+ const visited = new Set;
6564
+ const ordered = [];
6565
+ const visit = (name) => {
6566
+ if (visiting.has(name))
6567
+ fail(`workflows.${workflow}.stages`, "contains a dependency cycle.");
6568
+ if (visited.has(name))
6569
+ return;
6570
+ visiting.add(name);
6571
+ for (const dependency of stages[name].dependsOn ?? [])
6572
+ visit(dependency);
6573
+ visiting.delete(name);
6574
+ visited.add(name);
6575
+ ordered.push(name);
6576
+ };
6577
+ for (const name of names.sort())
6578
+ visit(name);
6579
+ return ordered;
6580
+ }
6581
+ function validateStage(value, where, targets, context) {
6582
+ const row2 = object(value, where);
6583
+ exact3(row2, ["dependsOn", "if", "strategy", "steps"], where);
6584
+ if (row2.dependsOn !== undefined && (!Array.isArray(row2.dependsOn) || row2.dependsOn.length > 64 || row2.dependsOn.some((entry) => typeof entry !== "string" || !NAME2.test(entry)) || new Set(row2.dependsOn).size !== row2.dependsOn.length))
6585
+ fail(`${where}.dependsOn`, "is invalid.");
6586
+ if (row2.if !== undefined)
6587
+ condition(row2.if, `${where}.if`);
6588
+ const strategy = object(row2.strategy, `${where}.strategy`);
6589
+ exact3(strategy, ["mode", "maximumConcurrency", "batchSize", "minimumHealthy", "increments", "observationMs"], `${where}.strategy`);
6590
+ if (!["sequential", "parallel", "rolling", "blue-green", "canary"].includes(String(strategy.mode)))
6591
+ fail(`${where}.strategy.mode`, "is unsupported.");
6592
+ if (strategy.maximumConcurrency !== undefined)
6593
+ integer(strategy.maximumConcurrency, `${where}.strategy.maximumConcurrency`, 1, 1024);
6594
+ if (strategy.batchSize !== undefined)
6595
+ integer(strategy.batchSize, `${where}.strategy.batchSize`, 1, 1024);
6596
+ if (strategy.minimumHealthy !== undefined)
6597
+ integer(strategy.minimumHealthy, `${where}.strategy.minimumHealthy`, 0, 1024);
6598
+ if (strategy.increments !== undefined && (!Array.isArray(strategy.increments) || strategy.increments.length < 1 || strategy.increments.length > 20 || strategy.increments.some((entry) => !Number.isInteger(entry) || entry < 1 || entry > 100) || strategy.increments.at(-1) !== 100))
6599
+ fail(`${where}.strategy.increments`, "must end at 100 and contain bounded percentages.");
6600
+ if (strategy.observationMs !== undefined)
6601
+ integer(strategy.observationMs, `${where}.strategy.observationMs`, 1000, 86400000);
6602
+ if (strategy.mode === "canary" && strategy.increments === undefined)
6603
+ fail(`${where}.strategy.increments`, "is required for canary mode.");
6604
+ if (strategy.mode === "rolling" && strategy.batchSize === undefined)
6605
+ fail(`${where}.strategy.batchSize`, "is required for rolling mode.");
6606
+ const steps = object(row2.steps, `${where}.steps`);
6607
+ const entries = Object.entries(steps);
6608
+ if (entries.length < 1 || entries.length > 256)
6609
+ fail(`${where}.steps`, "must contain 1 to 256 steps.");
6610
+ for (const [name, stepValue] of entries) {
6611
+ named(name, `${where}.steps key`);
6612
+ validateStep(stepValue, `${where}.steps.${name}`, targets, context);
6613
+ }
6614
+ return row2;
6615
+ }
6616
+ function compileDeployment(sourceValue) {
6617
+ const source = object(sourceValue, "deployment");
6618
+ exact3(source, ["apiVersion", "kind", "metadata", "spec"], "deployment");
6619
+ if (source.apiVersion !== "deploy.forgezero.net/v1")
6620
+ fail("deployment.apiVersion", "must be deploy.forgezero.net/v1.");
6621
+ if (source.kind !== "Deployment")
6622
+ fail("deployment.kind", "must be Deployment.");
6623
+ const metadata = object(source.metadata, "deployment.metadata");
6624
+ exact3(metadata, ["name", "description"], "deployment.metadata");
6625
+ const name = named(metadata.name, "deployment.metadata.name");
6626
+ if (metadata.description !== undefined)
6627
+ boundedString(metadata.description, "deployment.metadata.description", 1024);
6628
+ const spec = object(source.spec, "deployment.spec");
6629
+ exact3(spec, ["security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
6630
+ if (spec.security !== undefined) {
6631
+ const security = object(spec.security, "deployment.spec.security");
6632
+ exact3(security, ["attestation"], "deployment.spec.security");
6633
+ if (security.attestation !== undefined && !["required", "preferred", "disabled"].includes(String(security.attestation)))
6634
+ fail("deployment.spec.security.attestation", "is unsupported.");
6635
+ }
6636
+ const inputs = object(spec.inputs ?? {}, "deployment.spec.inputs");
6637
+ if (Object.keys(inputs).length > 128)
6638
+ fail("deployment.spec.inputs", "contains more than 128 inputs.");
6639
+ for (const [inputName, value] of Object.entries(inputs)) {
6640
+ named(inputName, "deployment.spec.inputs key");
6641
+ validateInput(value, `deployment.spec.inputs.${inputName}`);
6642
+ }
6643
+ const credentials = object(spec.credentials ?? {}, "deployment.spec.credentials");
6644
+ if (Object.keys(credentials).length > 128)
6645
+ fail("deployment.spec.credentials", "contains more than 128 credentials.");
6646
+ for (const [credentialName, value] of Object.entries(credentials)) {
6647
+ named(credentialName, "deployment.spec.credentials key");
6648
+ const credential = object(value, `deployment.spec.credentials.${credentialName}`);
6649
+ exact3(credential, ["schema", "source"], `deployment.spec.credentials.${credentialName}`);
6650
+ if (!ACTION.test(String(credential.schema)))
6651
+ fail(`deployment.spec.credentials.${credentialName}.schema`, "must be a versioned schema coordinate.");
6652
+ const credentialSource = object(credential.source, `deployment.spec.credentials.${credentialName}.source`);
6653
+ exact3(credentialSource, ["kind", "name", "fallback"], `deployment.spec.credentials.${credentialName}.source`);
6654
+ if (!["vault", "systemd"].includes(String(credentialSource.kind)))
6655
+ fail(`deployment.spec.credentials.${credentialName}.source.kind`, "is unsupported.");
6656
+ named(credentialSource.name, `deployment.spec.credentials.${credentialName}.source.name`);
6657
+ if (credentialSource.fallback !== undefined && (credentialSource.kind !== "vault" || credentialSource.fallback !== "systemd"))
6658
+ fail(`deployment.spec.credentials.${credentialName}.source.fallback`, "may only be systemd behind Vault.");
6659
+ }
6660
+ const targets = object(spec.targets, "deployment.spec.targets");
6661
+ const targetEntries = Object.entries(targets);
6662
+ if (targetEntries.length < 1 || targetEntries.length > 64)
6663
+ fail("deployment.spec.targets", "must contain 1 to 64 targets.");
6664
+ const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
6665
+ for (const [targetName, value] of targetEntries) {
6666
+ const target = object(value, `deployment.spec.targets.${targetName}`);
6667
+ exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
6668
+ if (target.kind !== "compute")
6669
+ fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
6670
+ const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
6671
+ exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
6672
+ if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME2.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
6673
+ fail(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
6674
+ if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
6675
+ fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
6676
+ if (selector.labels !== undefined)
6677
+ jsonValue(selector.labels, `deployment.spec.targets.${targetName}.selector.labels`);
6678
+ const cardinality = object(target.cardinality, `deployment.spec.targets.${targetName}.cardinality`);
6679
+ exact3(cardinality, ["minimum", "desired", "maximum"], `deployment.spec.targets.${targetName}.cardinality`);
6680
+ const minimum = integer(cardinality.minimum, `deployment.spec.targets.${targetName}.cardinality.minimum`, 1, 1024);
6681
+ const maximum = integer(cardinality.maximum, `deployment.spec.targets.${targetName}.cardinality.maximum`, minimum, 1024);
6682
+ if (typeof cardinality.desired === "number")
6683
+ integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
6684
+ else
6685
+ reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
6686
+ if (target.placement !== undefined)
6687
+ jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
6688
+ }
6689
+ const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
6690
+ if (Object.keys(requirements).length > 64)
6691
+ fail("deployment.spec.requirements", "contains more than 64 requirements.");
6692
+ const requirementDefinitions = new Map;
6693
+ const requiredProviders = new Map;
6694
+ for (const [requirementName, value] of Object.entries(requirements)) {
6695
+ named(requirementName, "deployment.spec.requirements key");
6696
+ const requirementValue = validateRequirement(value, `deployment.spec.requirements.${requirementName}`);
6697
+ requirementDefinitions.set(requirementName, requirementValue);
6698
+ const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
6699
+ requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
6700
+ }
6701
+ const components = object(spec.components, "deployment.spec.components");
6702
+ const componentEntries = Object.entries(components);
6703
+ if (componentEntries.length < 1 || componentEntries.length > 128)
6704
+ fail("deployment.spec.components", "must contain 1 to 128 components.");
6705
+ for (const [componentName, value] of componentEntries) {
6706
+ named(componentName, "deployment.spec.components key");
6707
+ validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
6708
+ }
6709
+ const actionContext = {
6710
+ components,
6711
+ requirements,
6712
+ credentials
6713
+ };
6714
+ const workflows = object(spec.workflows, "deployment.spec.workflows");
6715
+ const workflowEntries = Object.entries(workflows);
6716
+ if (workflowEntries.length < 1 || workflowEntries.length > 32)
6717
+ fail("deployment.spec.workflows", "must contain 1 to 32 workflows.");
6718
+ const plannedWorkflows = [];
6719
+ for (const [workflowName, value] of workflowEntries.sort(([left], [right]) => left.localeCompare(right))) {
6720
+ named(workflowName, "deployment.spec.workflows key");
6721
+ const workflowValue = object(value, `deployment.spec.workflows.${workflowName}`);
6722
+ exact3(workflowValue, ["concurrency", "stages"], `deployment.spec.workflows.${workflowName}`);
6723
+ if (workflowValue.concurrency !== undefined) {
6724
+ const concurrency = object(workflowValue.concurrency, `deployment.spec.workflows.${workflowName}.concurrency`);
6725
+ exact3(concurrency, ["group", "limit"], `deployment.spec.workflows.${workflowName}.concurrency`);
6726
+ boundedString(concurrency.group, `deployment.spec.workflows.${workflowName}.concurrency.group`, 256);
6727
+ integer(concurrency.limit, `deployment.spec.workflows.${workflowName}.concurrency.limit`, 1, 1024);
6728
+ }
6729
+ const stages = object(workflowValue.stages, `deployment.spec.workflows.${workflowName}.stages`);
6730
+ if (Object.keys(stages).length < 1 || Object.keys(stages).length > 64)
6731
+ fail(`deployment.spec.workflows.${workflowName}.stages`, "must contain 1 to 64 stages.");
6732
+ for (const [stageName, stageValue] of Object.entries(stages)) {
6733
+ named(stageName, `deployment.spec.workflows.${workflowName}.stages key`);
6734
+ validateStage(stageValue, `deployment.spec.workflows.${workflowName}.stages.${stageName}`, targetNames, actionContext);
6735
+ }
6736
+ const stepIds = new Set;
6737
+ const plannedStages = topologicalStages(stages, workflowName).map((stageName) => {
6738
+ const stageValue = stages[stageName];
6739
+ const plannedSteps = Object.entries(stageValue.steps).map(([stepName, stepValue]) => {
6740
+ if (stepIds.has(stepName))
6741
+ fail(`deployment.spec.workflows.${workflowName}`, `contains duplicate step id ${stepName}.`);
6742
+ stepIds.add(stepName);
6743
+ return { id: stepName, ...stepValue };
6744
+ });
6745
+ return { id: stageName, ...stageValue.dependsOn ? { dependsOn: [...stageValue.dependsOn].sort() } : {}, ...stageValue.if ? { if: stageValue.if } : {}, strategy: stageValue.strategy, steps: plannedSteps };
6746
+ });
6747
+ plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
6748
+ }
6749
+ const typedSource = source;
6750
+ return {
6751
+ format: DEPLOY_PLAN_FORMAT,
6752
+ version: DEPLOY_PLAN_VERSION,
6753
+ sourceDigest: deploymentSourceDigest(typedSource),
6754
+ name,
6755
+ ...metadata.description ? { description: metadata.description } : {},
6756
+ requiredProviders: [...requiredProviders.values()].sort((left, right) => `${left.provider}:${left.contract}:${left.capability}`.localeCompare(`${right.provider}:${right.contract}:${right.capability}`)),
6757
+ spec: {
6758
+ ...spec.security ? { security: spec.security } : {},
6759
+ inputs,
6760
+ credentials,
6761
+ targets,
6762
+ requirements,
6763
+ components,
6764
+ workflows: plannedWorkflows
6765
+ }
6766
+ };
6767
+ }
6768
+ function parseDeploymentPlan(value) {
6769
+ const row2 = object(value, "plan");
6770
+ exact3(row2, ["format", "version", "sourceDigest", "name", "description", "requiredProviders", "spec"], "plan");
6771
+ if (row2.format !== DEPLOY_PLAN_FORMAT || row2.version !== DEPLOY_PLAN_VERSION)
6772
+ fail("plan", "uses an unsupported format or version.");
6773
+ if (typeof row2.sourceDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(row2.sourceDigest))
6774
+ fail("plan.sourceDigest", "is invalid.");
6775
+ named(row2.name, "plan.name");
6776
+ if (row2.description !== undefined)
6777
+ boundedString(row2.description, "plan.description", 1024);
6778
+ if (!Array.isArray(row2.requiredProviders) || row2.requiredProviders.length > 64)
6779
+ fail("plan.requiredProviders", "must be a bounded array.");
6780
+ for (const [index, provider] of row2.requiredProviders.entries()) {
6781
+ const item = object(provider, `plan.requiredProviders[${index}]`);
6782
+ exact3(item, ["provider", "contract", "capability"], `plan.requiredProviders[${index}]`);
6783
+ if (!COORDINATE.test(String(item.provider)) || !COORDINATE.test(String(item.capability)))
6784
+ fail(`plan.requiredProviders[${index}]`, "contains an invalid coordinate.");
6785
+ integer(item.contract, `plan.requiredProviders[${index}].contract`, 1, 65535);
6786
+ }
6787
+ const spec = object(row2.spec, "plan.spec");
6788
+ const workflows = spec.workflows;
6789
+ if (!Array.isArray(workflows))
6790
+ fail("plan.spec.workflows", "must be an array.");
6791
+ const sourceWorkflows = {};
6792
+ for (const [workflowIndex, workflow] of workflows.entries()) {
6793
+ const item = object(workflow, `plan.spec.workflows[${workflowIndex}]`);
6794
+ exact3(item, ["id", "concurrency", "stages"], `plan.spec.workflows[${workflowIndex}]`);
6795
+ const workflowId = named(item.id, `plan.spec.workflows[${workflowIndex}].id`);
6796
+ if (sourceWorkflows[workflowId])
6797
+ fail("plan.spec.workflows", `contains duplicate workflow ${workflowId}.`);
6798
+ if (!Array.isArray(item.stages))
6799
+ fail(`plan.spec.workflows[${workflowIndex}].stages`, "must be an array.");
6800
+ const sourceStages = {};
6801
+ for (const [stageIndex, stage2] of item.stages.entries()) {
6802
+ const stageItem = object(stage2, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
6803
+ exact3(stageItem, ["id", "dependsOn", "if", "strategy", "steps"], `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
6804
+ const stageId = named(stageItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].id`);
6805
+ if (sourceStages[stageId])
6806
+ fail(`plan.spec.workflows[${workflowIndex}].stages`, `contains duplicate stage ${stageId}.`);
6807
+ if (!Array.isArray(stageItem.steps))
6808
+ fail(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, "must be an array.");
6809
+ const sourceSteps = {};
6810
+ for (const [stepIndex, step2] of stageItem.steps.entries()) {
6811
+ const stepItem = object(step2, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}]`);
6812
+ const stepId = named(stepItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}].id`);
6813
+ if (sourceSteps[stepId])
6814
+ fail(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, `contains duplicate step ${stepId}.`);
6815
+ const { id: _id3, ...stepWithoutId } = stepItem;
6816
+ sourceSteps[stepId] = stepWithoutId;
6817
+ }
6818
+ const { id: _id2, steps: _steps, ...stageWithoutId } = stageItem;
6819
+ sourceStages[stageId] = { ...stageWithoutId, steps: sourceSteps };
6820
+ }
6821
+ const { id: _id, stages: _stages, ...workflowWithoutId } = item;
6822
+ sourceWorkflows[workflowId] = { ...workflowWithoutId, stages: sourceStages };
6823
+ }
6824
+ const { workflows: _workflows, ...specWithoutWorkflows } = spec;
6825
+ const reconstructed = {
6826
+ apiVersion: "deploy.forgezero.net/v1",
6827
+ kind: "Deployment",
6828
+ metadata: { name: row2.name, ...row2.description ? { description: row2.description } : {} },
6829
+ spec: { ...specWithoutWorkflows, workflows: sourceWorkflows }
6830
+ };
6831
+ const compiled = compileDeployment(reconstructed);
6832
+ return { ...compiled, sourceDigest: row2.sourceDigest };
6833
+ }
6834
+
6835
+ // src/deploy-plan-runner.ts
6836
+ class DeploymentActionError extends Error {
6837
+ kind;
6838
+ constructor(kind, message) {
6839
+ super(message);
6840
+ this.kind = kind;
6841
+ this.name = "DeploymentActionError";
6842
+ }
6843
+ }
6844
+ function nested(value, path2) {
6845
+ let current = value;
6846
+ for (const part of path2) {
6847
+ if (!current || typeof current !== "object" || Array.isArray(current))
6848
+ return;
6849
+ current = current[part];
6850
+ }
6851
+ return current;
6852
+ }
6853
+ function resolveReference(reference2, state) {
6854
+ const parts = reference2.$ref.split(".");
6855
+ if (parts[0] === "inputs")
6856
+ return state.inputs[parts[1]];
6857
+ if (parts[0] === "steps") {
6858
+ const step2 = state.steps.get(parts[1]);
6859
+ if (parts[2] === "status")
6860
+ return step2?.status;
6861
+ if (parts[2] === "outputs")
6862
+ return nested(step2?.outputs, parts.slice(3));
6863
+ }
6864
+ if (parts[0] === "components")
6865
+ return nested(state.plan.spec.components[parts[1]], parts.slice(2));
6866
+ if (parts[0] === "targets")
6867
+ return nested(state.plan.spec.targets[parts[1]], parts.slice(2));
6868
+ if (parts[0] === "deployment")
6869
+ return nested(state.plan, parts.slice(1));
6870
+ return;
6871
+ }
6872
+ function resolveOperand(value, state) {
6873
+ return value && typeof value === "object" && "$ref" in value ? resolveReference(value, state) : value;
6874
+ }
6875
+ function stepMatches(id2, status, state) {
6876
+ return state.steps.get(id2)?.status === status;
6877
+ }
6878
+ function evaluateCondition(value, state) {
6879
+ if ("all" in value)
6880
+ return value.all.every((entry) => evaluateCondition(entry, state));
6881
+ if ("any" in value)
6882
+ return value.any.some((entry) => evaluateCondition(entry, state));
6883
+ if ("not" in value)
6884
+ return !evaluateCondition(value.not, state);
6885
+ if ("equals" in value)
6886
+ return resolveOperand(value.equals[0], state) === resolveOperand(value.equals[1], state);
6887
+ if ("notEquals" in value)
6888
+ return resolveOperand(value.notEquals[0], state) !== resolveOperand(value.notEquals[1], state);
6889
+ if ("greaterThan" in value)
6890
+ return Number(resolveOperand(value.greaterThan[0], state)) > Number(resolveOperand(value.greaterThan[1], state));
6891
+ if ("greaterThanOrEqual" in value)
6892
+ return Number(resolveOperand(value.greaterThanOrEqual[0], state)) >= Number(resolveOperand(value.greaterThanOrEqual[1], state));
6893
+ if ("lessThan" in value)
6894
+ return Number(resolveOperand(value.lessThan[0], state)) < Number(resolveOperand(value.lessThan[1], state));
6895
+ if ("lessThanOrEqual" in value)
6896
+ return Number(resolveOperand(value.lessThanOrEqual[0], state)) <= Number(resolveOperand(value.lessThanOrEqual[1], state));
6897
+ if ("contains" in value) {
6898
+ const [left, right] = [resolveOperand(value.contains[0], state), resolveOperand(value.contains[1], state)];
6899
+ return typeof left === "string" ? left.includes(String(right)) : Array.isArray(left) && left.includes(right);
6900
+ }
6901
+ if ("startsWith" in value)
6902
+ return String(resolveOperand(value.startsWith[0], state) ?? "").startsWith(String(resolveOperand(value.startsWith[1], state) ?? ""));
6903
+ if ("exists" in value)
6904
+ return resolveReference(value.exists, state) !== undefined;
6905
+ if ("succeeded" in value)
6906
+ return stepMatches(value.succeeded, "succeeded", state);
6907
+ if ("failed" in value)
6908
+ return stepMatches(value.failed, "failed", state);
6909
+ return state.steps.get(value.changed)?.changed === true;
6910
+ }
6911
+ function validateInputValue(definition, value, name) {
6912
+ const selected = value === undefined ? definition.default : value;
6913
+ if (selected === undefined) {
6914
+ if (definition.required)
6915
+ throw new DeploymentActionError("refused", `deployment input ${name} is required`);
6916
+ return null;
6917
+ }
6918
+ if (definition.type === "string" || definition.type === "hostname" || definition.type === "enum") {
6919
+ if (typeof selected !== "string")
6920
+ throw new DeploymentActionError("refused", `deployment input ${name} must be a string`);
6921
+ if (definition.type === "enum" && !definition.values.includes(selected))
6922
+ throw new DeploymentActionError("refused", `deployment input ${name} is not an allowed value`);
6923
+ if (definition.type === "hostname" && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(selected))
6924
+ throw new DeploymentActionError("refused", `deployment input ${name} must be a hostname`);
6925
+ if (definition.type === "string") {
6926
+ if (definition.minimumLength !== undefined && selected.length < definition.minimumLength)
6927
+ throw new DeploymentActionError("refused", `deployment input ${name} is too short`);
6928
+ if (definition.maximumLength !== undefined && selected.length > definition.maximumLength)
6929
+ throw new DeploymentActionError("refused", `deployment input ${name} is too long`);
6930
+ if (definition.pattern !== undefined && !new RegExp(definition.pattern).test(selected))
6931
+ throw new DeploymentActionError("refused", `deployment input ${name} does not match its pattern`);
6932
+ }
6933
+ return selected;
6934
+ }
6935
+ if (definition.type === "boolean") {
6936
+ if (typeof selected !== "boolean")
6937
+ throw new DeploymentActionError("refused", `deployment input ${name} must be a boolean`);
6938
+ return selected;
6939
+ }
6940
+ if (typeof selected !== "number" || !Number.isFinite(selected) || definition.type === "integer" && !Number.isInteger(selected))
6941
+ throw new DeploymentActionError("refused", `deployment input ${name} must be a ${definition.type}`);
6942
+ if (definition.minimum !== undefined && selected < definition.minimum)
6943
+ throw new DeploymentActionError("refused", `deployment input ${name} is below its minimum`);
6944
+ if (definition.maximum !== undefined && selected > definition.maximum)
6945
+ throw new DeploymentActionError("refused", `deployment input ${name} is above its maximum`);
6946
+ return selected;
6947
+ }
6948
+ function resolveJson(value, state) {
6949
+ if (Array.isArray(value))
6950
+ return value.map((entry) => resolveJson(entry, state));
6951
+ if (value && typeof value === "object") {
6952
+ if (Object.keys(value).length === 1 && "$ref" in value && typeof value.$ref === "string") {
6953
+ const resolved = resolveReference(value, state);
6954
+ if (resolved === undefined)
6955
+ throw new DeploymentActionError("refused", `deployment reference ${value.$ref} is unavailable`);
6956
+ return resolved;
6957
+ }
6958
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveJson(entry, state)]));
6959
+ }
6960
+ return value;
6961
+ }
6962
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6963
+ function backoff(step2, attempt) {
6964
+ const retry = step2.retry;
6965
+ if (!retry)
6966
+ return 0;
6967
+ const factor = retry.backoff.kind === "fixed" ? 1 : retry.backoff.kind === "linear" ? attempt : 2 ** (attempt - 1);
6968
+ return Math.min(retry.backoff.maximumMs, retry.backoff.initialMs * factor);
6969
+ }
6970
+ async function bounded(work, timeoutMs) {
6971
+ const controller = new AbortController;
6972
+ let timer;
6973
+ try {
6974
+ return await Promise.race([
6975
+ work(controller.signal),
6976
+ new Promise((_, reject) => {
6977
+ timer = setTimeout(() => {
6978
+ controller.abort();
6979
+ reject(new DeploymentActionError("timeout", `action exceeded ${timeoutMs}ms`));
6980
+ }, timeoutMs);
6981
+ })
6982
+ ]);
6983
+ } finally {
6984
+ if (timer)
6985
+ clearTimeout(timer);
6986
+ }
6987
+ }
6988
+ function actionError(cause) {
6989
+ return cause instanceof DeploymentActionError ? cause : new DeploymentActionError("failed", cause instanceof Error ? cause.message : "action failed");
6990
+ }
6991
+ async function compensate(record2, step2, stage2, options, state) {
6992
+ if (!step2.compensate)
6993
+ return;
6994
+ const handler = options.actions[step2.compensate.uses];
6995
+ if (!handler)
6996
+ throw new DeploymentActionError("refused", `compensation action ${step2.compensate.uses} is unsupported`);
6997
+ const compensation = { ...step2, uses: step2.compensate.uses, with: step2.compensate.with };
6998
+ await bounded((signal) => handler({ plan: state.plan, workflow: options.workflow, stage: stage2, step: compensation, inputs: state.inputs, resolved: resolveJson(compensation.with, state), attempt: 1, signal }), step2.timeoutMs ?? 600000);
6999
+ record2.status = "compensated";
7000
+ }
7001
+ async function runStep(step2, stage2, options, state) {
7002
+ const record2 = { id: step2.id, stage: stage2.id, action: step2.uses, status: "pending", attempts: 0, outputs: {}, changed: false };
7003
+ state.steps.set(step2.id, record2);
7004
+ if (options.shouldRun && !options.shouldRun(step2)) {
7005
+ record2.status = "skipped";
7006
+ return record2;
7007
+ }
7008
+ if (step2.if && !evaluateCondition(step2.if, state)) {
7009
+ record2.status = "skipped";
7010
+ return record2;
7011
+ }
7012
+ const handler = options.actions[step2.uses];
7013
+ if (!handler) {
7014
+ record2.status = "failed";
7015
+ record2.error = { kind: "refused", message: `action ${step2.uses} is unsupported` };
7016
+ return record2;
7017
+ }
7018
+ record2.startedAtTs = (options.now ?? Date.now)();
7019
+ record2.status = "running";
7020
+ const attempts = step2.retry?.attempts ?? 1;
7021
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
7022
+ record2.attempts = attempt;
7023
+ try {
7024
+ const result = await bounded((signal) => handler({ plan: state.plan, workflow: options.workflow, stage: stage2, step: step2, inputs: state.inputs, resolved: resolveJson(step2.with, state), attempt, signal }), step2.timeoutMs ?? 600000);
7025
+ record2.status = "succeeded";
7026
+ record2.outputs = result.outputs ?? {};
7027
+ record2.changed = result.changed === true;
7028
+ record2.evidence = result.evidence;
7029
+ record2.finishedAtTs = (options.now ?? Date.now)();
7030
+ return record2;
7031
+ } catch (cause) {
7032
+ const error = actionError(cause);
7033
+ const retryable = attempt < attempts && (step2.retry?.retryOn?.includes(error.kind) ?? error.kind !== "refused");
7034
+ if (!retryable) {
7035
+ record2.status = step2.failure?.policy === "manual-intervention" ? "manual-intervention" : "failed";
7036
+ record2.error = { kind: error.kind, message: error.message };
7037
+ record2.finishedAtTs = (options.now ?? Date.now)();
7038
+ return record2;
7039
+ }
7040
+ await (options.sleep ?? sleep)(backoff(step2, attempt));
7041
+ }
7042
+ }
7043
+ return record2;
7044
+ }
7045
+ async function mapLimit(values, limit, run2) {
7046
+ const results = new Array(values.length);
7047
+ let next = 0;
7048
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, async () => {
7049
+ for (;; ) {
7050
+ const index = next++;
7051
+ if (index >= values.length)
7052
+ return;
7053
+ results[index] = await run2(values[index]);
7054
+ }
7055
+ }));
7056
+ return results;
7057
+ }
7058
+ async function runDeploymentPlan(options) {
7059
+ const plan = parseDeploymentPlan(options.plan);
7060
+ const workflow = plan.spec.workflows.find((candidate) => candidate.id === options.workflow);
7061
+ if (!workflow)
7062
+ throw new DeploymentActionError("refused", `workflow ${options.workflow} does not exist`);
7063
+ const supplied = options.inputs ?? {};
7064
+ const inputValues = {};
7065
+ const inputDefinitions = plan.spec.inputs ?? {};
7066
+ for (const key of Object.keys(supplied))
7067
+ if (!Object.hasOwn(inputDefinitions, key))
7068
+ throw new DeploymentActionError("refused", `unknown deployment input ${key}`);
7069
+ for (const [name, definition] of Object.entries(inputDefinitions))
7070
+ inputValues[name] = validateInputValue(definition, supplied[name], name);
7071
+ const execute = async () => {
7072
+ const state = { inputs: inputValues, steps: new Map, plan };
7073
+ const records = [];
7074
+ const successful2 = [];
7075
+ for (const stage2 of workflow.stages) {
7076
+ if (stage2.if && !evaluateCondition(stage2.if, state)) {
7077
+ for (const step2 of stage2.steps) {
7078
+ const record2 = { id: step2.id, stage: stage2.id, action: step2.uses, status: "skipped", attempts: 0, outputs: {}, changed: false };
7079
+ state.steps.set(step2.id, record2);
7080
+ records.push(record2);
7081
+ }
7082
+ continue;
7083
+ }
7084
+ const parallel = stage2.strategy.mode === "parallel";
7085
+ const stageRecords = parallel ? await mapLimit(stage2.steps, stage2.strategy.maximumConcurrency ?? stage2.steps.length, (step2) => runStep(step2, stage2, { ...options, plan, workflow: workflow.id }, state)) : await (async () => {
7086
+ const rows = [];
7087
+ for (const step2 of stage2.steps) {
7088
+ const record2 = await runStep(step2, stage2, { ...options, plan, workflow: workflow.id }, state);
7089
+ rows.push(record2);
7090
+ if (["failed", "manual-intervention"].includes(record2.status) && step2.failure?.policy !== "continue")
7091
+ break;
7092
+ }
7093
+ return rows;
7094
+ })();
7095
+ records.push(...stageRecords);
7096
+ for (const record2 of stageRecords) {
7097
+ const step2 = stage2.steps.find((candidate) => candidate.id === record2.id);
7098
+ if (record2.status === "succeeded")
7099
+ successful2.push({ record: record2, step: step2, stage: stage2 });
7100
+ if (record2.status !== "failed" && record2.status !== "manual-intervention")
7101
+ continue;
7102
+ const policy = step2.failure?.policy ?? "stop";
7103
+ if (policy === "continue")
7104
+ continue;
7105
+ if (policy === "compensate")
7106
+ await compensate(record2, step2, stage2, { ...options, plan, workflow: workflow.id }, state);
7107
+ if (policy === "rollback-stage" || policy === "rollback-deployment") {
7108
+ const candidates = successful2.filter((entry) => policy === "rollback-deployment" || entry.stage.id === stage2.id).reverse();
7109
+ for (const entry of candidates)
7110
+ await compensate(entry.record, entry.step, entry.stage, { ...options, plan, workflow: workflow.id }, state);
7111
+ }
7112
+ return { workflow: workflow.id, ok: false, status: record2.status === "manual-intervention" ? "manual-intervention" : "failed", steps: records };
7113
+ }
7114
+ }
7115
+ return { workflow: workflow.id, ok: true, status: "succeeded", steps: records };
7116
+ };
7117
+ return workflow.concurrency && options.lock ? options.lock(workflow.concurrency.group, workflow.concurrency.limit, execute) : execute();
7118
+ }
7119
+
7120
+ // src/deploy-compiler.ts
7121
+ var DEPLOY_PLAN_FILE = ".fz/deploy.plan.json";
7122
+
5891
7123
  // src/deployment.ts
5892
7124
  class DeploymentError extends Error {
5893
7125
  code;
@@ -5933,7 +7165,7 @@ function persistCapacityCalibration(args) {
5933
7165
  if (!isAbsolute2(args.directory)) {
5934
7166
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be an absolute Agent-owned path");
5935
7167
  }
5936
- if (!existsSync2(args.directory)) {
7168
+ if (!existsSync3(args.directory)) {
5937
7169
  const parent = lstatSync(dirname(args.directory));
5938
7170
  if (!parent.isDirectory() || parent.isSymbolicLink()) {
5939
7171
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence parent must be a real Agent-owned directory");
@@ -5944,7 +7176,7 @@ function persistCapacityCalibration(args) {
5944
7176
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
5945
7177
  throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be a real Agent-owned directory");
5946
7178
  }
5947
- const identity = createHash3("sha256").update(`${args.deploymentKey}\x00${args.profile}\x00${args.revision}`).digest("hex").slice(0, 24);
7179
+ const identity = createHash4("sha256").update(`${args.deploymentKey}\x00${args.profile}\x00${args.revision}`).digest("hex").slice(0, 24);
5948
7180
  const path2 = join2(args.directory, `${identity}-${args.measuredAtTs}-${randomUUID().slice(0, 8)}.json`);
5949
7181
  const next = `${path2}.next`;
5950
7182
  const recommendedCoordinate = {
@@ -5989,7 +7221,7 @@ function createDeploymentManager(options) {
5989
7221
  if (content.length > 16385 || /\r|\0/.test(content)) {
5990
7222
  throw new DeploymentError("SOURCE_FAILED", "The pinned Git host keys are malformed.");
5991
7223
  }
5992
- if (existsSync2(knownHostsPath) && readFileSync2(knownHostsPath, "utf8") === content)
7224
+ if (existsSync3(knownHostsPath) && readFileSync2(knownHostsPath, "utf8") === content)
5993
7225
  return;
5994
7226
  mkdirSync2(dirname(knownHostsPath), { recursive: true, mode: 448 });
5995
7227
  const next = `${knownHostsPath}.${process.pid}.${randomUUID()}.next`;
@@ -6148,6 +7380,212 @@ function createDeploymentManager(options) {
6148
7380
  if (request.revision && head !== request.revision) {
6149
7381
  throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
6150
7382
  }
7383
+ const planPath = join2(release, DEPLOY_PLAN_FILE);
7384
+ if (existsSync3(planPath)) {
7385
+ const plan = parseDeploymentPlan(readDefinition(planPath));
7386
+ for (const workflow of Object.values(plan.spec.workflows))
7387
+ for (const stage2 of Object.values(workflow.stages)) {
7388
+ if (stage2.strategy.mode === "rolling" || stage2.strategy.mode === "canary" || Object.values(stage2.steps).some((step2) => step2.scope.kind === "target-batches")) {
7389
+ throw new DeploymentError("PIPELINE_FAILED", "rolling/canary plans require a fenced multi-target rollout lease that this control protocol does not yet supply");
7390
+ }
7391
+ }
7392
+ const targetNames = new Set(Object.entries(plan.spec.targets).filter(([, target]) => target.selector.profiles.includes(options.profile)).map(([name]) => name));
7393
+ if (targetNames.size === 0)
7394
+ throw new DeploymentError("PIPELINE_FAILED", `deployment plan has no target for profile ${options.profile}`);
7395
+ const assurance = plan.spec.security?.attestation ?? "disabled";
7396
+ if (assurance !== "disabled") {
7397
+ if (!options.attestation && assurance === "required")
7398
+ throw new DeploymentError("PIPELINE_FAILED", "deployment plan requires attestation but this Agent has no source");
7399
+ if (options.attestation) {
7400
+ try {
7401
+ await options.attestation.report(head);
7402
+ } catch (cause) {
7403
+ if (assurance === "required")
7404
+ throw new DeploymentError("PIPELINE_FAILED", `deployment attestation failed: ${cause.message}`);
7405
+ }
7406
+ }
7407
+ }
7408
+ const phaseEnvironment2 = {
7409
+ ...options.environment ?? {},
7410
+ FZ_RELEASE: release,
7411
+ FZ_DEPLOY_REVISION: head,
7412
+ FZ_DEPLOY_BRANCH: options.branch,
7413
+ FZ_DEPLOY_PROFILE: options.profile,
7414
+ ...options.publicApiUrl ? { PUBLIC_API_URL: options.publicApiUrl } : {}
7415
+ };
7416
+ const inputValues = {};
7417
+ for (const [name, definition2] of Object.entries(plan.spec.inputs ?? {})) {
7418
+ const raw = options.environment?.[name];
7419
+ if (raw === undefined)
7420
+ continue;
7421
+ inputValues[name] = definition2.type === "boolean" ? raw === "true" : definition2.type === "integer" || definition2.type === "number" ? Number(raw) : raw;
7422
+ }
7423
+ const projectExec2 = options.projectExec ?? spawnExact;
7424
+ const materializePlanArgv = (argv2) => argv2.map((argument) => {
7425
+ if (argument === "${FZ_RELEASE}")
7426
+ return release;
7427
+ if (argument.includes("${")) {
7428
+ throw new DeploymentActionError("refused", `unsupported deployment argv coordinate ${argument}`);
7429
+ }
7430
+ return argument;
7431
+ });
7432
+ const providerSoftware = {
7433
+ "forgezero.bun": "bun",
7434
+ "forgezero.docker": "docker",
7435
+ "forgezero.nginx": "nginx",
7436
+ "forgezero.arangodb": "arangodb",
7437
+ "forgezero.cloudflared": "cloudflared",
7438
+ "forgezero.warp": "cloudflare-warp",
7439
+ "forgezero.ufw": "ufw",
7440
+ "forgezero.openssh": "openssh-client"
7441
+ };
7442
+ const actions2 = {
7443
+ "forgezero.software/ensure@1": async ({ resolved }) => {
7444
+ if (!options.ensureSoftware)
7445
+ throw new DeploymentActionError("provider-unavailable", "the supervised software helper is unavailable");
7446
+ if (!Array.isArray(resolved.requirements))
7447
+ throw new DeploymentActionError("refused", "software.ensure requires a requirements array");
7448
+ const requirements = resolved.requirements.map((name) => {
7449
+ if (typeof name !== "string")
7450
+ throw new DeploymentActionError("refused", "software requirement name is invalid");
7451
+ const requirement2 = plan.spec.requirements?.[name];
7452
+ const id2 = requirement2 && providerSoftware[requirement2.provider];
7453
+ if (!requirement2 || !id2)
7454
+ throw new DeploymentActionError("refused", `software requirement ${name} is unsupported on this Agent`);
7455
+ return { id: id2, version: requirement2.version };
7456
+ });
7457
+ await options.ensureSoftware(requirements);
7458
+ return { changed: true };
7459
+ },
7460
+ "forgezero.exec/argv@1": async ({ resolved, step: step2 }) => {
7461
+ if (!Array.isArray(resolved.argv) || resolved.argv.some((value) => typeof value !== "string"))
7462
+ throw new DeploymentActionError("refused", "exec.argv requires an exact argv array");
7463
+ const environment = { ...phaseEnvironment2 };
7464
+ const bindings = resolved.credentials;
7465
+ if (bindings !== undefined) {
7466
+ if (!bindings || typeof bindings !== "object" || Array.isArray(bindings))
7467
+ throw new DeploymentActionError("refused", "exec.argv credentials must map environment names to credential declarations");
7468
+ for (const [environmentName, credentialName] of Object.entries(bindings)) {
7469
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(environmentName) || typeof credentialName !== "string")
7470
+ throw new DeploymentActionError("refused", "exec.argv credential binding is invalid");
7471
+ const credential = plan.spec.credentials?.[credentialName];
7472
+ if (!credential || !options.cache)
7473
+ throw new DeploymentActionError("refused", `credential ${credentialName} is unavailable`);
7474
+ environment[environmentName] = await options.cache.get(credential.source.name);
7475
+ }
7476
+ }
7477
+ const result = await projectExec2({ argv: materializePlanArgv(resolved.argv), cwd: release, env: environment, timeoutMs: step2.timeoutMs });
7478
+ if (result.exitCode !== 0)
7479
+ throw new DeploymentActionError("failed", result.output.slice(0, 2000) || `command exited ${result.exitCode}`);
7480
+ return { outputs: { exitCode: result.exitCode } };
7481
+ },
7482
+ "forgezero.health/http@1": async ({ resolved }) => {
7483
+ const componentName = String(resolved.component ?? "");
7484
+ const component2 = plan.spec.components[componentName];
7485
+ if (!component2 || component2.kind !== "application")
7486
+ throw new DeploymentActionError("refused", `health component ${componentName} is invalid`);
7487
+ const health = component2.service.health;
7488
+ try {
7489
+ const response = await fetch(`http://127.0.0.1:${component2.service.port}${health.path}`, { method: health.method, redirect: "manual", signal: AbortSignal.timeout(health.timeoutMs) });
7490
+ if (!health.expectedStatus.includes(response.status))
7491
+ throw new DeploymentActionError("failed", `health returned HTTP ${response.status}`);
7492
+ return { evidence: { status: response.status } };
7493
+ } catch (cause) {
7494
+ throw cause instanceof DeploymentActionError ? cause : new DeploymentActionError("network", cause.message);
7495
+ }
7496
+ },
7497
+ "forgezero.storage/verify@1": async ({ resolved }) => {
7498
+ const component2 = plan.spec.components[String(resolved.component ?? "")];
7499
+ const mounts = component2?.kind === "database" ? [component2.storage] : component2?.storage ?? [];
7500
+ for (const mount of mounts) {
7501
+ const candidate = existsSync3(mount.path) ? mount.path : dirname(mount.path);
7502
+ const stats = statfsSync(candidate);
7503
+ const freePercent = Number(stats.bavail) / Number(stats.blocks) * 100;
7504
+ const minimum = mount.class === "ephemeral" ? 0 : mount.minimumFreePercent ?? 0;
7505
+ if (!Number.isFinite(freePercent) || freePercent < minimum)
7506
+ throw new DeploymentActionError("refused", `storage ${mount.path} has ${freePercent.toFixed(1)}% free; ${minimum}% required`);
7507
+ }
7508
+ return { evidence: { mounts: mounts.length } };
7509
+ },
7510
+ "forgezero.oci/build@1": async ({ resolved }) => {
7511
+ if (!options.buildContainer)
7512
+ throw new DeploymentActionError("provider-unavailable", "the supervised container builder is unavailable");
7513
+ const componentName = String(resolved.component ?? "");
7514
+ const application = plan.spec.components[componentName];
7515
+ if (!application || application.kind !== "application" || application.runtime.kind !== "container")
7516
+ throw new DeploymentActionError("refused", `container component ${componentName} is invalid`);
7517
+ const result = await options.buildContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, noCache: resolved.noCache === true });
7518
+ return { changed: true, outputs: { image: result.image, digest: result.digest } };
7519
+ },
7520
+ "forgezero.oci/pull@1": async ({ resolved }) => {
7521
+ if (!options.buildContainer)
7522
+ throw new DeploymentActionError("provider-unavailable", "the supervised container puller is unavailable");
7523
+ const componentName = String(resolved.component ?? "");
7524
+ const application = plan.spec.components[componentName];
7525
+ if (!application || application.kind !== "application" || application.runtime.kind !== "container")
7526
+ throw new DeploymentActionError("refused", `container component ${componentName} is invalid`);
7527
+ const result = await options.buildContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application });
7528
+ return { changed: true, outputs: { image: result.image, digest: result.digest } };
7529
+ },
7530
+ "forgezero.service/deploy@1": async ({ resolved }) => {
7531
+ if (!options.activateContainer)
7532
+ throw new DeploymentActionError("provider-unavailable", "the supervised container activator is unavailable");
7533
+ const componentName = String(resolved.component ?? "");
7534
+ const application = plan.spec.components[componentName];
7535
+ const image = String(resolved.imageDigest ?? "");
7536
+ if (!application || application.kind !== "application" || application.runtime.kind !== "container" || !image)
7537
+ throw new DeploymentActionError("refused", `container deployment ${componentName} is invalid`);
7538
+ const state = await options.activateContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, image });
7539
+ return { changed: true, outputs: { activeSlot: state.activeSlot, publicPort: state.publicPort } };
7540
+ },
7541
+ "forgezero.service/promote@1": async ({ resolved }) => {
7542
+ if (!options.activateContainer)
7543
+ throw new DeploymentActionError("provider-unavailable", "the supervised container activator is unavailable");
7544
+ const componentName = String(resolved.component ?? "");
7545
+ const application = plan.spec.components[componentName];
7546
+ const image = String(resolved.imageDigest ?? "");
7547
+ if (!application || application.kind !== "application" || application.runtime.kind !== "container" || !image)
7548
+ throw new DeploymentActionError("refused", `container promotion ${componentName} is invalid`);
7549
+ const state = await options.activateContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, image });
7550
+ return { changed: true, outputs: { activeSlot: state.activeSlot, publicPort: state.publicPort } };
7551
+ }
7552
+ };
7553
+ const run2 = await runDeploymentPlan({
7554
+ plan,
7555
+ workflow: "deploy",
7556
+ inputs: inputValues,
7557
+ actions: actions2,
7558
+ shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : targetNames.has(step2.scope.target)
7559
+ });
7560
+ const phase = {
7561
+ pipeline: `${plan.name}:deploy`,
7562
+ ok: run2.ok,
7563
+ assurance: assurance === "disabled" ? "enrolled" : options.attestation ? "attested" : "enrolled",
7564
+ steps: run2.steps.map((step2) => ({
7565
+ name: step2.id,
7566
+ outcome: step2.status === "succeeded" || step2.status === "compensated" ? "ok" : step2.status === "skipped" ? "skipped" : "failed",
7567
+ exitCode: step2.status === "succeeded" || step2.status === "compensated" ? 0 : step2.status === "skipped" ? null : 1,
7568
+ log: step2.error?.message ?? "",
7569
+ durationMs: (step2.finishedAtTs ?? step2.startedAtTs ?? 0) - (step2.startedAtTs ?? 0)
7570
+ }))
7571
+ };
7572
+ if (!run2.ok) {
7573
+ const failed = run2.steps.find((step2) => step2.status === "failed" || step2.status === "manual-intervention");
7574
+ throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}`);
7575
+ }
7576
+ return {
7577
+ key: options.key,
7578
+ repository: options.repository,
7579
+ branch: options.branch,
7580
+ revision: head,
7581
+ definitionDigest: plan.sourceDigest,
7582
+ definitionVersion: plan.version,
7583
+ profile: options.profile,
7584
+ release,
7585
+ ok: true,
7586
+ phases: [phase]
7587
+ };
7588
+ }
6151
7589
  const definition = parseDeployDefinition(readDefinition(join2(release, ".fz", "deploy.json")));
6152
7590
  const definitionDigest = deployDefinitionDigest(definition);
6153
7591
  const selectedProfile = definition.profiles[options.profile];
@@ -6193,7 +7631,7 @@ function createDeploymentManager(options) {
6193
7631
  phase,
6194
7632
  name: `${definition.name}:${phase}`,
6195
7633
  requireAttestation: definition.requireAttestation,
6196
- 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)))
7634
+ steps: definition.steps.filter((step2) => step2.phase === phase && (!step2.profiles || step2.profiles.includes(options.profile)) && (step2.scope === "target" || request.releaseExecutor === true) && (!step2.when || Object.entries(step2.when).every(([name, expected]) => phaseEnvironment[name] === expected)))
6197
7635
  };
6198
7636
  })
6199
7637
  ].filter((pipeline) => pipeline.steps.length > 0);
@@ -6220,7 +7658,7 @@ function createDeploymentManager(options) {
6220
7658
  const result = await runPipeline({ pipeline, secret, exec: phaseExec, attest });
6221
7659
  phases.push(result);
6222
7660
  if (!result.ok) {
6223
- const failedStep = result.steps.find((step) => step.outcome === "failed");
7661
+ const failedStep = result.steps.find((step2) => step2.outcome === "failed");
6224
7662
  const detail = failedStep?.log.trim().slice(0, 2000);
6225
7663
  throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`));
6226
7664
  }
@@ -6312,7 +7750,7 @@ function createDeploymentManager(options) {
6312
7750
  }
6313
7751
 
6314
7752
  // src/control.ts
6315
- import { chmodSync as chmodSync4, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
7753
+ import { chmodSync as chmodSync4, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
6316
7754
  import { connect, createServer as createServer2 } from "net";
6317
7755
  var DEFAULT_CONTROL_SOCKET = "/run/forgezero/control.sock";
6318
7756
  var MAX_REQUEST_BYTES = 16 * 1024;
@@ -6369,7 +7807,7 @@ async function handleControl(manager, request) {
6369
7807
  }
6370
7808
  }
6371
7809
  function startControlServer(manager, socketPath = DEFAULT_CONTROL_SOCKET) {
6372
- if (existsSync3(socketPath))
7810
+ if (existsSync4(socketPath))
6373
7811
  unlinkSync3(socketPath);
6374
7812
  const server = createServer2((socket) => {
6375
7813
  let buffer = "";
@@ -6487,7 +7925,7 @@ async function deployClaim(options, claim) {
6487
7925
  }
6488
7926
  async function completeSigned(options, body) {
6489
7927
  const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
6490
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
7928
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
6491
7929
  let last;
6492
7930
  for (let attempt = 1;attempt <= attempts; attempt += 1) {
6493
7931
  try {
@@ -6498,7 +7936,7 @@ async function completeSigned(options, body) {
6498
7936
  if (cause instanceof SignedNodeHttpError && cause.status < 500)
6499
7937
  throw cause;
6500
7938
  if (attempt < attempts)
6501
- await sleep(Math.min(2000, 250 * 2 ** (attempt - 1)));
7939
+ await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
6502
7940
  }
6503
7941
  }
6504
7942
  throw last;
@@ -6589,7 +8027,7 @@ function startDeploymentPull(options) {
6589
8027
  // src/guest-enrolment.ts
6590
8028
  import {
6591
8029
  chmodSync as chmodSync5,
6592
- existsSync as existsSync4,
8030
+ existsSync as existsSync5,
6593
8031
  mkdirSync as mkdirSync3,
6594
8032
  readFileSync as readFileSync3,
6595
8033
  renameSync as renameSync3,
@@ -6621,11 +8059,11 @@ function privateNetworkAttachmentFromEnvironment(env = process.env) {
6621
8059
  var validBinding = (value, expectedNodeKey) => {
6622
8060
  if (!value || typeof value !== "object")
6623
8061
  return false;
6624
- const row = value;
6625
- return ["nodeKey", "computeReference", "projectKey", "environmentKey"].every((key) => typeof row[key] === "string" && row[key].length > 0) && (row.realm === "platform" || row.realm === "tenant" && typeof row.tenantSlug === "string" && row.tenantSlug.length > 0) && (!expectedNodeKey || row.nodeKey === expectedNodeKey);
8062
+ const row2 = value;
8063
+ return ["nodeKey", "computeReference", "projectKey", "environmentKey"].every((key) => typeof row2[key] === "string" && row2[key].length > 0) && (row2.realm === "platform" || row2.realm === "tenant" && typeof row2.tenantSlug === "string" && row2.tenantSlug.length > 0) && (!expectedNodeKey || row2.nodeKey === expectedNodeKey);
6626
8064
  };
6627
8065
  function loadGuestBinding(path2, expectedNodeKey) {
6628
- if (!existsSync4(path2))
8066
+ if (!existsSync5(path2))
6629
8067
  return null;
6630
8068
  const mode = statSync(path2).mode & 511;
6631
8069
  if ((mode & 63) !== 0) {
@@ -6751,7 +8189,7 @@ async function runClaim(options, claim) {
6751
8189
  }
6752
8190
  async function complete(options, body) {
6753
8191
  const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
6754
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8192
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
6755
8193
  let last;
6756
8194
  for (let attempt = 1;attempt <= attempts; attempt += 1) {
6757
8195
  try {
@@ -6762,7 +8200,7 @@ async function complete(options, body) {
6762
8200
  if (cause instanceof SignedNodeHttpError && cause.status < 500)
6763
8201
  throw cause;
6764
8202
  if (attempt < attempts)
6765
- await sleep(Math.min(2000, 250 * 2 ** (attempt - 1)));
8203
+ await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
6766
8204
  }
6767
8205
  }
6768
8206
  throw last;
@@ -6860,7 +8298,7 @@ var remoteOutcome3 = (cause) => cause instanceof SignedNodeHttpError && cause.st
6860
8298
  var post2 = (options, operation, body) => postSignedNode(options, `v1/node/migrations/${operation}`, body);
6861
8299
  async function complete2(options, body) {
6862
8300
  const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
6863
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8301
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
6864
8302
  let last;
6865
8303
  for (let attempt = 1;attempt <= attempts; attempt += 1) {
6866
8304
  try {
@@ -6871,7 +8309,7 @@ async function complete2(options, body) {
6871
8309
  if (cause instanceof SignedNodeHttpError && cause.status < 500)
6872
8310
  throw cause;
6873
8311
  if (attempt < attempts)
6874
- await sleep(Math.min(2000, 250 * 2 ** (attempt - 1)));
8312
+ await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
6875
8313
  }
6876
8314
  }
6877
8315
  throw last;
@@ -6988,7 +8426,7 @@ function startMigrationPull(options) {
6988
8426
  }
6989
8427
 
6990
8428
  // src/ssh-bootstrap.ts
6991
- import { createHash as createHash4 } from "crypto";
8429
+ import { createHash as createHash5 } from "crypto";
6992
8430
  import { chmodSync as chmodSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
6993
8431
  import { tmpdir as tmpdir2 } from "os";
6994
8432
  import { join as join3 } from "path";
@@ -7010,7 +8448,7 @@ function assertSupportedGuestImage(imageKey) {
7010
8448
  }
7011
8449
 
7012
8450
  // src/version.ts
7013
- var VERSION = "0.1.53";
8451
+ var VERSION2 = "0.1.56";
7014
8452
 
7015
8453
  // src/ssh-bootstrap.ts
7016
8454
  class SshBootstrapError extends Error {
@@ -7138,7 +8576,7 @@ var verifiedBunArchive = async (directory) => {
7138
8576
  if (!response.ok)
7139
8577
  throw new SshBootstrapError("BUN_DOWNLOAD_FAILED");
7140
8578
  const bytes = new Uint8Array(await response.arrayBuffer());
7141
- if (createHash4("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256) {
8579
+ if (createHash5("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256) {
7142
8580
  throw new SshBootstrapError("BUN_CHECKSUM_MISMATCH");
7143
8581
  }
7144
8582
  const archive = join3(directory, "bun.zip");
@@ -7522,7 +8960,7 @@ async function executeMetalAdmission(claim, options) {
7522
8960
  } },
7523
8961
  bunVersion: PINNED_BUN_VERSION,
7524
8962
  bunReleaseSha256: BUN_RELEASE_SHA256,
7525
- agentVersion: VERSION
8963
+ agentVersion: VERSION2
7526
8964
  }
7527
8965
  };
7528
8966
  const configPath = join3(directory, "metal.json");
@@ -7713,13 +9151,13 @@ function startMetalAdmissionPull(options) {
7713
9151
  }
7714
9152
 
7715
9153
  // src/metal-helper-socket.ts
7716
- import { chmodSync as chmodSync7, existsSync as existsSync6, unlinkSync as unlinkSync6 } from "fs";
9154
+ import { chmodSync as chmodSync7, existsSync as existsSync7, unlinkSync as unlinkSync6 } from "fs";
7717
9155
  import { connect as connect2, createServer as createServer3 } from "net";
7718
9156
 
7719
9157
  // src/metal-provision.ts
7720
- import { createHash as createHash5 } from "crypto";
9158
+ import { createHash as createHash6 } from "crypto";
7721
9159
  import {
7722
- existsSync as existsSync5,
9160
+ existsSync as existsSync6,
7723
9161
  mkdirSync as mkdirSync4,
7724
9162
  readFileSync as readFileSync5,
7725
9163
  readdirSync,
@@ -7856,8 +9294,8 @@ function membersOfLinuxList(value, label) {
7856
9294
  throw new MetalProvisionError(`${label} list overlaps itself`);
7857
9295
  return members;
7858
9296
  }
7859
- var guestNameFor = (computeKey) => `fzg-${createHash5("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
7860
- var tapNameFor = (computeKey) => `fzt${createHash5("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
9297
+ var guestNameFor = (computeKey) => `fzg-${createHash6("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
9298
+ var tapNameFor = (computeKey) => `fzt${createHash6("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
7861
9299
  var macForAddress = (address) => {
7862
9300
  const octets = address.split(".").map(Number);
7863
9301
  if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
@@ -7937,17 +9375,17 @@ function validateMetalProfile(profile) {
7937
9375
  }
7938
9376
  }
7939
9377
  var readManifests = (stateDir) => {
7940
- if (!existsSync5(stateDir))
9378
+ if (!existsSync6(stateDir))
7941
9379
  return [];
7942
9380
  return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync5(join4(stateDir, name), "utf8")));
7943
9381
  };
7944
9382
  function allocateAddress(profile, computeKey, rows) {
7945
- const existing = rows.find((row) => row.computeKey === computeKey);
9383
+ const existing = rows.find((row2) => row2.computeKey === computeKey);
7946
9384
  if (existing)
7947
9385
  return existing.address;
7948
- const used = new Set(rows.map((row) => row.address));
9386
+ const used = new Set(rows.map((row2) => row2.address));
7949
9387
  const width = profile.addressEnd - profile.addressStart + 1;
7950
- const start = createHash5("sha256").update(computeKey).digest().readUInt16BE(0) % width;
9388
+ const start = createHash6("sha256").update(computeKey).digest().readUInt16BE(0) % width;
7951
9389
  for (let offset = 0;offset < width; offset += 1) {
7952
9390
  const last = profile.addressStart + (start + offset) % width;
7953
9391
  const address = `${profile.subnetPrefix}.${last}`;
@@ -7966,13 +9404,13 @@ function requestedAddress(profile, claim, rows) {
7966
9404
  if (!Number.isInteger(last) || last < profile.addressStart || last > profile.addressEnd) {
7967
9405
  throw new MetalProvisionError("requested guest address is outside the allocatable range");
7968
9406
  }
7969
- const occupied = rows.find((row) => row.address === claim.spec.guestAddress && row.computeKey !== claim.computeKey);
9407
+ const occupied = rows.find((row2) => row2.address === claim.spec.guestAddress && row2.computeKey !== claim.computeKey);
7970
9408
  if (occupied)
7971
9409
  throw new MetalProvisionError("requested guest address is already allocated");
7972
9410
  return claim.spec.guestAddress;
7973
9411
  }
7974
9412
  function allocateCpuPool(profile, claim, rows) {
7975
- const prior = rows.find((row) => row.computeKey === claim.computeKey);
9413
+ const prior = rows.find((row2) => row2.computeKey === claim.computeKey);
7976
9414
  if (prior) {
7977
9415
  const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
7978
9416
  if (!retained || retained.cpus !== prior.allowedCpus || retained.memoryNodes !== prior.allowedMemoryNodes) {
@@ -7980,7 +9418,7 @@ function allocateCpuPool(profile, claim, rows) {
7980
9418
  }
7981
9419
  return retained;
7982
9420
  }
7983
- const used = new Set(rows.map((row) => row.cpuPoolKey));
9421
+ const used = new Set(rows.map((row2) => row2.cpuPoolKey));
7984
9422
  if (claim.spec.cpuPoolKey) {
7985
9423
  const requested = profile.cpuPools.find((pool) => pool.key === claim.spec.cpuPoolKey);
7986
9424
  if (!requested || used.has(requested.key)) {
@@ -8134,11 +9572,11 @@ async function provisionMetalGuest(profile, claim, exec) {
8134
9572
  throw new MetalProvisionError("invalid SSH public key list");
8135
9573
  }
8136
9574
  const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
8137
- const conflictingName = manifests.find((row) => row.name === name && row.computeKey !== claim.computeKey);
9575
+ const conflictingName = manifests.find((row2) => row2.name === name && row2.computeKey !== claim.computeKey);
8138
9576
  if (conflictingName)
8139
9577
  throw new MetalProvisionError("requested guest name is already allocated");
8140
9578
  const manifestPath = join4(profile.stateDir, `${name}.json`);
8141
- const prior = manifests.find((row) => row.computeKey === claim.computeKey);
9579
+ const prior = manifests.find((row2) => row2.computeKey === claim.computeKey);
8142
9580
  if (prior && prior.reference !== claim.spec.reference)
8143
9581
  throw new MetalProvisionError("compute identity conflicts with host inventory");
8144
9582
  const address = requestedAddress(profile, claim, manifests);
@@ -8221,7 +9659,7 @@ function legacyPlatformManifest(profile, claim, name) {
8221
9659
  return;
8222
9660
  const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
8223
9661
  const legacyPath = join4(legacyDir, `${name}.conf`);
8224
- if (!existsSync5(legacyPath))
9662
+ if (!existsSync6(legacyPath))
8225
9663
  return;
8226
9664
  const values = new Map;
8227
9665
  for (const line of readFileSync5(legacyPath, "utf8").split(/\r?\n/)) {
@@ -8258,7 +9696,7 @@ async function removeMetalGuest(profile, claim, exec) {
8258
9696
  const service = `forgezero-guest@${name}.service`;
8259
9697
  const unitPath = join4(profile.unitDir, service);
8260
9698
  const lv = `/dev/${profile.volumeGroup}/${name}`;
8261
- const manifest = existsSync5(manifestPath) ? JSON.parse(readFileSync5(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
9699
+ const manifest = existsSync6(manifestPath) ? JSON.parse(readFileSync5(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
8262
9700
  if (!manifest) {
8263
9701
  const seedBase = join4(profile.seedDir, name);
8264
9702
  const localArtifacts = [
@@ -8268,7 +9706,7 @@ async function removeMetalGuest(profile, claim, exec) {
8268
9706
  `${seedBase}-meta-data`,
8269
9707
  `${seedBase}-network-config`
8270
9708
  ];
8271
- if (localArtifacts.some(existsSync5)) {
9709
+ if (localArtifacts.some(existsSync6)) {
8272
9710
  throw new MetalProvisionError("guest artifacts exist without owned inventory");
8273
9711
  }
8274
9712
  const [volume, active] = await Promise.all([
@@ -8285,7 +9723,7 @@ async function removeMetalGuest(profile, claim, exec) {
8285
9723
  if (!currentIdentity && !legacyPlatformIdentity) {
8286
9724
  throw new MetalProvisionError("compute identity conflicts with host inventory");
8287
9725
  }
8288
- if (existsSync5(unitPath))
9726
+ if (existsSync6(unitPath))
8289
9727
  await checked2(exec, ["systemctl", "disable", "--now", service]);
8290
9728
  else if (legacyPlatformIdentity)
8291
9729
  await checked2(exec, ["systemctl", "disable", "--now", service]);
@@ -8302,14 +9740,14 @@ async function removeMetalGuest(profile, claim, exec) {
8302
9740
  join4(profile.seedDir, `${name}-network-config`),
8303
9741
  manifestPath
8304
9742
  ])
8305
- if (existsSync5(path2))
9743
+ if (existsSync6(path2))
8306
9744
  unlinkSync5(path2);
8307
9745
  if (legacyPlatformIdentity) {
8308
9746
  const legacyConfig = join4(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
8309
9747
  const legacyDropIn = join4(profile.unitDir, `${service}.d`);
8310
- if (existsSync5(legacyConfig))
9748
+ if (existsSync6(legacyConfig))
8311
9749
  unlinkSync5(legacyConfig);
8312
- if (existsSync5(legacyDropIn))
9750
+ if (existsSync6(legacyDropIn))
8313
9751
  rmSync3(legacyDropIn, { recursive: true });
8314
9752
  }
8315
9753
  await checked2(exec, ["systemctl", "daemon-reload"]);
@@ -8348,7 +9786,7 @@ command exceeded ${COMMAND_TIMEOUT_MS}ms`.trim() : stderr
8348
9786
  function startMetalHelper(options) {
8349
9787
  validateMetalProfile(options.profile);
8350
9788
  const socketPath = options.socketPath ?? DEFAULT_METAL_HELPER_SOCKET;
8351
- if (existsSync6(socketPath))
9789
+ if (existsSync7(socketPath))
8352
9790
  unlinkSync6(socketPath);
8353
9791
  let tail = Promise.resolve();
8354
9792
  const server = createServer3((socket) => {
@@ -8435,7 +9873,7 @@ function requestMetalProvision(claim, socketPath = DEFAULT_METAL_HELPER_SOCKET)
8435
9873
  }
8436
9874
 
8437
9875
  // src/deployment-runner.ts
8438
- import { chmodSync as chmodSync8, existsSync as existsSync7, realpathSync, unlinkSync as unlinkSync7 } from "fs";
9876
+ import { chmodSync as chmodSync8, existsSync as existsSync8, realpathSync, unlinkSync as unlinkSync7 } from "fs";
8439
9877
  import { isAbsolute as isAbsolute4, resolve, sep } from "path";
8440
9878
  import { connect as connect3, createServer as createServer4 } from "net";
8441
9879
  var DEFAULT_DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
@@ -8552,7 +9990,7 @@ function startDeploymentRunner(options) {
8552
9990
  const root = resolve(options.root);
8553
9991
  const home = resolve(options.home);
8554
9992
  const socketPath = options.socketPath ?? DEFAULT_DEPLOYMENT_RUNNER_SOCKET;
8555
- if (existsSync7(socketPath))
9993
+ if (existsSync8(socketPath))
8556
9994
  unlinkSync7(socketPath);
8557
9995
  const active = new Set;
8558
9996
  const server = createServer4((socket) => {
@@ -8648,7 +10086,7 @@ function requestDeploymentCommand(input, socketPath = DEFAULT_DEPLOYMENT_RUNNER_
8648
10086
  }
8649
10087
 
8650
10088
  // src/snp-attestation.ts
8651
- import { existsSync as existsSync8 } from "fs";
10089
+ import { existsSync as existsSync9 } from "fs";
8652
10090
 
8653
10091
  // ../runtime/dist/snp.js
8654
10092
  var REPORT_BYTES = 1184;
@@ -8741,7 +10179,7 @@ function createSnpAttestationSource(options = {}) {
8741
10179
  const python = options.python ?? "python3";
8742
10180
  const timeoutMs = options.timeoutMs ?? 1e4;
8743
10181
  const spawn = options.spawn ?? Bun.spawn;
8744
- const exists = options.exists ?? existsSync8;
10182
+ const exists = options.exists ?? existsSync9;
8745
10183
  return {
8746
10184
  name: "linux-sev-guest",
8747
10185
  async report(nonce) {
@@ -8949,7 +10387,7 @@ async function closeServerWithin(server, timeoutMs) {
8949
10387
  }
8950
10388
 
8951
10389
  // src/lifecycle-helper.ts
8952
- import { chmodSync as chmodSync9, existsSync as existsSync9, readFileSync as readFileSync6, unlinkSync as unlinkSync8 } from "fs";
10390
+ import { chmodSync as chmodSync9, existsSync as existsSync10, readFileSync as readFileSync6, unlinkSync as unlinkSync8 } from "fs";
8953
10391
  import { connect as connect4, createConnection, createServer as createServer5, isIP as isIP3 } from "net";
8954
10392
  var DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
8955
10393
  var MAX_REQUEST_BYTES4 = 16 * 1024;
@@ -9090,7 +10528,7 @@ async function executeLifecycleAction(profile, claim, exec = spawnLifecycleComma
9090
10528
  function startLifecycleHelper(options) {
9091
10529
  validateLifecycleProfile(options.profile);
9092
10530
  const socketPath = options.socketPath ?? DEFAULT_LIFECYCLE_HELPER_SOCKET;
9093
- if (existsSync9(socketPath))
10531
+ if (existsSync10(socketPath))
9094
10532
  unlinkSync8(socketPath);
9095
10533
  let tail = Promise.resolve();
9096
10534
  const server = createServer5((socket) => {
@@ -9252,11 +10690,11 @@ async function configureMeshConnector(input) {
9252
10690
  }
9253
10691
 
9254
10692
  // src/agent-update.ts
9255
- import { createHash as createHash6, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
10693
+ import { createHash as createHash7, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
9256
10694
  import {
9257
10695
  chmodSync as chmodSync11,
9258
10696
  closeSync as closeSync2,
9259
- existsSync as existsSync10,
10697
+ existsSync as existsSync11,
9260
10698
  fsyncSync,
9261
10699
  mkdirSync as mkdirSync7,
9262
10700
  openSync as openSync2,
@@ -9271,7 +10709,7 @@ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
9271
10709
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
9272
10710
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
9273
10711
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
9274
- var VERSION2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
10712
+ var VERSION3 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
9275
10713
  var REGISTRY = "registry.npmjs.org";
9276
10714
  var syncPath = (path2) => {
9277
10715
  const descriptor = openSync2(path2, "r");
@@ -9296,7 +10734,7 @@ var syncReleaseDirectory = (directory) => {
9296
10734
  function validateAgentRelease(release) {
9297
10735
  if (release?.package !== "@forgezero/agent")
9298
10736
  throw new Error("agent update package is fixed");
9299
- if (!VERSION2.test(release.version))
10737
+ if (!VERSION3.test(release.version))
9300
10738
  throw new Error("agent update version must be exact semver");
9301
10739
  const expectedTarball = `/@forgezero/agent/-/agent-${release.version}.tgz`;
9302
10740
  let url;
@@ -9314,7 +10752,7 @@ function validateAgentRelease(release) {
9314
10752
  return release;
9315
10753
  }
9316
10754
  function compareVersions(left, right) {
9317
- if (!VERSION2.test(left) || !VERSION2.test(right))
10755
+ if (!VERSION3.test(left) || !VERSION3.test(right))
9318
10756
  throw new Error("agent version must be exact semver");
9319
10757
  const a = left.split(".").map(Number);
9320
10758
  const b = right.split(".").map(Number);
@@ -9396,7 +10834,7 @@ async function stageAgentRelease(releaseInput, options) {
9396
10834
  throw new Error("agent update tarball is empty or exceeds the size limit");
9397
10835
  }
9398
10836
  const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
9399
- const actual = createHash6("sha512").update(bytes).digest();
10837
+ const actual = createHash7("sha512").update(bytes).digest();
9400
10838
  if (!timingSafeEqual(actual, expected))
9401
10839
  throw new Error("agent update integrity mismatch");
9402
10840
  writeFileSync8(archive, bytes, { mode: 384, flag: "wx" });
@@ -9415,19 +10853,19 @@ async function stageAgentRelease(releaseInput, options) {
9415
10853
  writeFileSync8(destination, extracted.output, { mode: 384, flag: "wx" });
9416
10854
  }
9417
10855
  await validateReleaseDirectory(unpacked, release, run2);
9418
- if (!existsSync10(finalDirectory)) {
10856
+ if (!existsSync11(finalDirectory)) {
9419
10857
  renameSync5(unpacked, finalDirectory);
9420
10858
  syncReleaseDirectory(finalDirectory);
9421
10859
  } else
9422
10860
  await validateReleaseDirectory(finalDirectory, release, run2);
9423
- if (!existsSync10(currentLink)) {
10861
+ if (!existsSync11(currentLink)) {
9424
10862
  throw new Error("agent update requires an active immutable release to roll back to");
9425
10863
  }
9426
10864
  const previousTarget = readlinkSync(currentLink);
9427
10865
  if (previousTarget !== join7("versions", options.currentVersion)) {
9428
10866
  throw new Error("agent update current release does not match the running version");
9429
10867
  }
9430
- if (!existsSync10(join7(root, previousTarget))) {
10868
+ if (!existsSync11(join7(root, previousTarget))) {
9431
10869
  throw new Error("agent update rollback release is missing");
9432
10870
  }
9433
10871
  return {
@@ -9468,7 +10906,7 @@ import { randomUUID as randomUUID3 } from "crypto";
9468
10906
  import {
9469
10907
  chmodSync as chmodSync12,
9470
10908
  closeSync as closeSync3,
9471
- existsSync as existsSync11,
10909
+ existsSync as existsSync12,
9472
10910
  fsyncSync as fsyncSync2,
9473
10911
  mkdirSync as mkdirSync8,
9474
10912
  openSync as openSync3,
@@ -9491,7 +10929,7 @@ var COMPUTE_HELPER_UNITS = [
9491
10929
  "forgezero-lifecycle-helper.service",
9492
10930
  "forgezero-software-helper.service"
9493
10931
  ];
9494
- var VERSION3 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
10932
+ var VERSION4 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
9495
10933
  var ATTEMPT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
9496
10934
  var REASON_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
9497
10935
  var MAX_REASON_BYTES = 512;
@@ -9514,9 +10952,9 @@ function validateReceipt(value) {
9514
10952
  const receipt = value;
9515
10953
  if (!receipt.attemptId || !ATTEMPT_ID.test(receipt.attemptId))
9516
10954
  throw new Error("Agent update attempt ID is invalid");
9517
- if (!receipt.fromVersion || !VERSION3.test(receipt.fromVersion))
10955
+ if (!receipt.fromVersion || !VERSION4.test(receipt.fromVersion))
9518
10956
  throw new Error("Agent update source version is invalid");
9519
- if (!receipt.targetVersion || !VERSION3.test(receipt.targetVersion))
10957
+ if (!receipt.targetVersion || !VERSION4.test(receipt.targetVersion))
9520
10958
  throw new Error("Agent update target version is invalid");
9521
10959
  if (!["activating", "active", "rolled-back", "failed"].includes(receipt.outcome ?? "")) {
9522
10960
  throw new Error("Agent update outcome is invalid");
@@ -9549,7 +10987,7 @@ function validateJournal(value, root) {
9549
10987
  throw new Error("Agent update journal is malformed");
9550
10988
  const legacy = value;
9551
10989
  if (legacy.schemaVersion === undefined) {
9552
- if (typeof legacy.version === "string" && VERSION3.test(legacy.version) && legacy.outcome === "active" && validTime(legacy.updatedAtTs) && Object.keys(value).every((key) => ["version", "outcome", "updatedAtTs"].includes(key)))
10990
+ if (typeof legacy.version === "string" && VERSION4.test(legacy.version) && legacy.outcome === "active" && validTime(legacy.updatedAtTs) && Object.keys(value).every((key) => ["version", "outcome", "updatedAtTs"].includes(key)))
9553
10991
  return;
9554
10992
  throw new Error("Agent update legacy receipt is malformed");
9555
10993
  }
@@ -9574,7 +11012,7 @@ function validateJournal(value, root) {
9574
11012
  return journal;
9575
11013
  }
9576
11014
  function readJournal(path2, root) {
9577
- if (!existsSync11(path2))
11015
+ if (!existsSync12(path2))
9578
11016
  return;
9579
11017
  return validateJournal(JSON.parse(readFileSync9(path2, "utf8")), root);
9580
11018
  }
@@ -9632,7 +11070,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
9632
11070
  }
9633
11071
  function readAgentUpdateReceipt(path2 = AGENT_UPDATE_RECEIPT) {
9634
11072
  try {
9635
- if (!existsSync11(path2))
11073
+ if (!existsSync12(path2))
9636
11074
  return;
9637
11075
  return validateReceipt(JSON.parse(readFileSync9(path2, "utf8")));
9638
11076
  } catch {
@@ -9788,7 +11226,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
9788
11226
  return publicReceipt(journal);
9789
11227
  }
9790
11228
  const staged = stagedFromJournal(journal, root);
9791
- if (!existsSync11(join8(root, journal.previousTarget))) {
11229
+ if (!existsSync12(join8(root, journal.previousTarget))) {
9792
11230
  throw new Error("Agent update rollback release is missing");
9793
11231
  }
9794
11232
  const run2 = options.run ?? runCommand;
@@ -9818,7 +11256,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
9818
11256
  }
9819
11257
  function startAgentUpdateHelper(options = {}) {
9820
11258
  const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
9821
- if (existsSync11(socketPath))
11259
+ if (existsSync12(socketPath))
9822
11260
  unlinkSync10(socketPath);
9823
11261
  mkdirSync8(dirname5(socketPath), { recursive: true, mode: 488 });
9824
11262
  const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
@@ -9935,7 +11373,7 @@ var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.st
9935
11373
 
9936
11374
  class AgentUpdateRefusedError extends Error {
9937
11375
  }
9938
- function observeAgentHost(version = VERSION, mode = "enrolled", osRelease = readFileSync10("/etc/os-release", "utf8"), architecture = process.arch) {
11376
+ function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync10("/etc/os-release", "utf8"), architecture = process.arch) {
9939
11377
  const values = Object.fromEntries(osRelease.split(`
9940
11378
  `).flatMap((line) => {
9941
11379
  const separator = line.indexOf("=");
@@ -9949,7 +11387,7 @@ function observeAgentHost(version = VERSION, mode = "enrolled", osRelease = read
9949
11387
  };
9950
11388
  }
9951
11389
  async function heartbeatAgentOnce(options) {
9952
- let observation = (options.observation ?? (() => observeAgentHost(options.version ?? VERSION, options.mode)))();
11390
+ let observation = (options.observation ?? (() => observeAgentHost(options.version ?? VERSION2, options.mode)))();
9953
11391
  const update = observation.update ?? readAgentUpdateReceipt(options.receiptPath ?? AGENT_UPDATE_RECEIPT);
9954
11392
  if (update)
9955
11393
  observation = { ...observation, update };
@@ -10053,19 +11491,19 @@ function startAgentHeartbeat(options) {
10053
11491
  }
10054
11492
 
10055
11493
  // src/software-helper.ts
10056
- import { chmodSync as chmodSync13, existsSync as existsSync13, mkdirSync as mkdirSync10, unlinkSync as unlinkSync11 } from "fs";
11494
+ import { chmodSync as chmodSync13, existsSync as existsSync15, mkdirSync as mkdirSync11, unlinkSync as unlinkSync11 } from "fs";
10057
11495
  import { connect as connect6, createServer as createServer7 } from "net";
10058
- import { dirname as dirname7 } from "path";
11496
+ import { dirname as dirname8 } from "path";
10059
11497
 
10060
11498
  // src/service-supervisor.ts
10061
- import { createHash as createHash7 } from "crypto";
10062
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync11, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
11499
+ import { createHash as createHash8 } from "crypto";
11500
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync11, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
10063
11501
  import { dirname as dirname6, join as join9, resolve as resolve4, sep as sep2 } from "path";
10064
11502
  var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
10065
11503
  var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
10066
11504
  var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
10067
11505
  var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
10068
- var idFor = (key) => createHash7("sha256").update(key).digest("hex").slice(0, 24);
11506
+ var idFor = (key) => createHash8("sha256").update(key).digest("hex").slice(0, 24);
10069
11507
  var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
10070
11508
  var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
10071
11509
  var defaultHost = {
@@ -10076,8 +11514,8 @@ var defaultHost = {
10076
11514
  renameSync7(next, path2);
10077
11515
  },
10078
11516
  read: (path2) => readFileSync11(path2, "utf8"),
10079
- exists: existsSync12,
10080
- list: (path2) => existsSync12(path2) ? readdirSync2(path2) : [],
11517
+ exists: existsSync13,
11518
+ list: (path2) => existsSync13(path2) ? readdirSync2(path2) : [],
10081
11519
  realpath: realpathSync2,
10082
11520
  mkdir: (path2, mode) => mkdirSync9(path2, { recursive: true, mode }),
10083
11521
  remove: (path2) => rmSync6(path2, { force: true }),
@@ -10138,7 +11576,7 @@ function allocatedPorts(request, id2, previous, states) {
10138
11576
  if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
10139
11577
  return [...previous.applicationPorts];
10140
11578
  const width = allocation.to - allocation.from + 1;
10141
- const start = Number.parseInt(createHash7("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
11579
+ const start = Number.parseInt(createHash8("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
10142
11580
  for (let offset = 0;offset < width; offset += 1) {
10143
11581
  const first = allocation.from + (start + offset) % width;
10144
11582
  const candidate = Array.from({ length: required }, (_, index) => first + index);
@@ -10274,6 +11712,297 @@ async function activateSupervisedService(request, host = defaultHost) {
10274
11712
  return state;
10275
11713
  }
10276
11714
 
11715
+ // src/container-supervisor.ts
11716
+ import { createHash as createHash9 } from "crypto";
11717
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync8, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
11718
+ import { dirname as dirname7, resolve as resolve5, sep as sep3 } from "path";
11719
+ var defaultHost2 = {
11720
+ realpath: realpathSync3,
11721
+ exists: existsSync14,
11722
+ read: (path2) => readFileSync12(path2, "utf8"),
11723
+ write(path2, content, mode) {
11724
+ mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
11725
+ const next = `${path2}.next`;
11726
+ writeFileSync11(next, content, { mode });
11727
+ renameSync8(next, path2);
11728
+ },
11729
+ remove: (path2) => rmSync7(path2, { force: true }),
11730
+ list: (path2) => existsSync14(path2) ? readdirSync3(path2) : [],
11731
+ mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
11732
+ async exec(argv2) {
11733
+ const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
11734
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
11735
+ return { exitCode, output: `${stdout}${stderr}` };
11736
+ },
11737
+ async health(port, path2, timeoutMs, method, expectedStatus) {
11738
+ try {
11739
+ const response = await fetch(`http://127.0.0.1:${port}${path2}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
11740
+ return expectedStatus.includes(response.status);
11741
+ } catch {
11742
+ return false;
11743
+ }
11744
+ },
11745
+ sleep: (ms) => Bun.sleep(ms),
11746
+ now: Date.now
11747
+ };
11748
+ var within3 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep3}`);
11749
+ var idFor2 = (key) => createHash9("sha256").update(key).digest("hex").slice(0, 24);
11750
+ var statePath2 = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
11751
+ var nameFor = (id2, slot) => `forgezero-${id2}-slot${slot}`;
11752
+ function validateRequest(request, host) {
11753
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
11754
+ throw new Error("CONTAINER_IDENTITY_INVALID");
11755
+ const root = host.realpath(resolve5(request.root));
11756
+ const release = host.realpath(resolve5(request.release));
11757
+ if (!within3(root, release))
11758
+ throw new Error("CONTAINER_RELEASE_OUTSIDE_ROOT");
11759
+ const application = request.application;
11760
+ if (application.kind !== "application" || application.runtime.kind !== "container")
11761
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
11762
+ if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
11763
+ throw new Error("CONTAINER_LIMITS_REQUIRED");
11764
+ if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
11765
+ throw new Error("CONTAINER_SECURITY_INVALID");
11766
+ if (!application.network?.container || !["bridge", "host"].includes(application.network.container.mode))
11767
+ throw new Error("CONTAINER_NETWORK_INVALID");
11768
+ if (!application.network.ingress?.stablePort)
11769
+ throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
11770
+ return { ...request, root, release };
11771
+ }
11772
+ async function checked6(host, argv2, code) {
11773
+ const result = await host.exec(argv2);
11774
+ if (result.exitCode !== 0)
11775
+ throw new Error(`${code}: ${result.output.slice(0, 512)}`);
11776
+ return result.output.trim();
11777
+ }
11778
+ async function buildContainerImage(requestValue, host = defaultHost2) {
11779
+ const request = validateRequest(requestValue, host);
11780
+ const runtime = request.application.runtime;
11781
+ if (runtime.kind !== "container")
11782
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
11783
+ const source = runtime.image.source;
11784
+ if (source.kind === "registry") {
11785
+ if (!source.reference.includes("@sha256:"))
11786
+ throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
11787
+ await checked6(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
11788
+ const digest2 = await checked6(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
11789
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
11790
+ throw new Error("CONTAINER_DIGEST_INVALID");
11791
+ return { image: source.reference, digest: digest2 };
11792
+ }
11793
+ const context = host.realpath(resolve5(request.release, source.context));
11794
+ const dockerfile = host.realpath(resolve5(request.release, source.dockerfile));
11795
+ if (!within3(request.release, context) || !within3(context, dockerfile))
11796
+ throw new Error("CONTAINER_BUILD_PATH_INVALID");
11797
+ const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
11798
+ await checked6(host, [
11799
+ "/usr/bin/docker",
11800
+ "build",
11801
+ "--pull",
11802
+ ...request.noCache ? ["--no-cache"] : [],
11803
+ "--file",
11804
+ dockerfile,
11805
+ "--tag",
11806
+ tag,
11807
+ "--label",
11808
+ `net.forgezero.revision=${request.revision}`,
11809
+ "--label",
11810
+ `net.forgezero.component=${request.component}`,
11811
+ context
11812
+ ], "CONTAINER_BUILD_FAILED");
11813
+ const digest = await checked6(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
11814
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
11815
+ throw new Error("CONTAINER_DIGEST_INVALID");
11816
+ return { image: tag, digest };
11817
+ }
11818
+ function readState2(host, id2) {
11819
+ if (!host.exists(statePath2(id2)))
11820
+ return null;
11821
+ try {
11822
+ const value = JSON.parse(host.read(statePath2(id2)));
11823
+ return value.format === 1 ? value : null;
11824
+ } catch {
11825
+ return null;
11826
+ }
11827
+ }
11828
+ function usedPorts(host, exceptId) {
11829
+ const ports = new Set;
11830
+ for (const name of host.list(SERVICE_STATE_DIRECTORY).filter((entry) => /^[a-f0-9]{24}\.json$/.test(entry))) {
11831
+ try {
11832
+ const state = JSON.parse(host.read(`${SERVICE_STATE_DIRECTORY}/${name}`));
11833
+ if (state.id !== exceptId) {
11834
+ ports.add(state.publicPort);
11835
+ state.applicationPorts.forEach((port) => ports.add(port));
11836
+ }
11837
+ } catch {}
11838
+ }
11839
+ return ports;
11840
+ }
11841
+ function allocatePorts(host, id2, count, stablePort, previous) {
11842
+ const used = usedPorts(host, id2);
11843
+ if (used.has(stablePort))
11844
+ throw new Error("CONTAINER_PUBLIC_PORT_CONFLICT");
11845
+ if (previous?.applicationPorts.length === count && previous.applicationPorts.every((port) => !used.has(port)))
11846
+ return [...previous.applicationPorts];
11847
+ const from = 20000;
11848
+ const to = 49999;
11849
+ const width = to - from + 1;
11850
+ const start = Number.parseInt(id2.slice(0, 8), 16) % width;
11851
+ for (let offset = 0;offset < width; offset += 1) {
11852
+ const first = from + (start + offset) % width;
11853
+ const values = Array.from({ length: count }, (_, index) => first + index);
11854
+ if (values.at(-1) <= to && values.every((port) => port !== stablePort && !used.has(port)))
11855
+ return values;
11856
+ }
11857
+ throw new Error("CONTAINER_PORTS_EXHAUSTED");
11858
+ }
11859
+ function nginx2(request, id2, port) {
11860
+ const service = request.application.service;
11861
+ const stablePort = request.application.network.ingress.stablePort;
11862
+ return `# ForgeZero container ${id2}
11863
+ ${service.maximumConnections ? `limit_conn_zone $server_name zone=fz_${id2}:64k;
11864
+ ` : ""}server {
11865
+ listen 127.0.0.1:${stablePort};
11866
+ server_name _;
11867
+ ${service.maximumConnections ? ` limit_conn fz_${id2} ${service.maximumConnections};
11868
+ limit_conn_status 503;
11869
+ ` : ""} location / {
11870
+ proxy_pass http://127.0.0.1:${port};
11871
+ proxy_http_version 1.1;
11872
+ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
11873
+ proxy_set_header Connection "upgrade";
11874
+ ` : ""} proxy_set_header Host $host;
11875
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
11876
+ proxy_set_header X-Forwarded-Proto $scheme;
11877
+ }
11878
+ }
11879
+ `;
11880
+ }
11881
+ function runArgv(request, id2, slot, port, networkName) {
11882
+ const { application } = request;
11883
+ const resources = application.resources;
11884
+ const runtime = application.runtime;
11885
+ if (runtime.kind !== "container")
11886
+ throw new Error("CONTAINER_RUNTIME_REQUIRED");
11887
+ const memory = resources.memory;
11888
+ const network = application.network.container;
11889
+ const argv2 = [
11890
+ "/usr/bin/docker",
11891
+ "run",
11892
+ "--detach",
11893
+ "--name",
11894
+ nameFor(id2, slot),
11895
+ "--restart",
11896
+ "unless-stopped",
11897
+ "--label",
11898
+ `net.forgezero.key=${request.key}`,
11899
+ "--label",
11900
+ `net.forgezero.revision=${request.revision}`,
11901
+ "--cpus",
11902
+ String(resources.cpu.limit),
11903
+ "--cpu-shares",
11904
+ String(resources.cpu.weight ?? 100),
11905
+ "--memory",
11906
+ `${memory.limitMiB}m`,
11907
+ ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
11908
+ ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
11909
+ "--pids-limit",
11910
+ String(resources.pids.limit),
11911
+ "--security-opt",
11912
+ "no-new-privileges=true",
11913
+ "--cap-drop",
11914
+ "ALL",
11915
+ ...runtime.security.root === "read-only" ? ["--read-only"] : [],
11916
+ ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
11917
+ ];
11918
+ for (const mount of application.storage ?? []) {
11919
+ if (mount.class === "ephemeral")
11920
+ argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
11921
+ else
11922
+ argv2.push("--volume", `forgezero-${id2}-${mount.name}:${mount.path}`);
11923
+ }
11924
+ argv2.push(request.image, ...runtime.entrypoint ?? []);
11925
+ return argv2;
11926
+ }
11927
+ async function activateContainer(requestValue, host = defaultHost2) {
11928
+ const request = validateRequest(requestValue, host);
11929
+ if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
11930
+ throw new Error("CONTAINER_IMAGE_INVALID");
11931
+ const id2 = idFor2(request.key);
11932
+ host.mkdir(SERVICE_STATE_DIRECTORY, 448);
11933
+ host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
11934
+ const previous = readState2(host, id2);
11935
+ const slots = request.application.rollout.strategy === "blue-green" ? 2 : 1;
11936
+ if (!["direct", "blue-green"].includes(request.application.rollout.strategy))
11937
+ throw new Error("CONTAINER_ROLLOUT_REQUIRES_CONTROL_PLANE");
11938
+ const containerNetwork = request.application.network.container;
11939
+ let networkName = "host";
11940
+ if (containerNetwork.mode === "bridge") {
11941
+ networkName = containerNetwork.network ? `forgezero-${id2}-${containerNetwork.network}` : "bridge";
11942
+ if (networkName !== "bridge") {
11943
+ const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
11944
+ if (inspected.exitCode !== 0) {
11945
+ await checked6(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id2}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
11946
+ } else if (inspected.output.trim() !== id2)
11947
+ throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
11948
+ }
11949
+ }
11950
+ const ports = allocatePorts(host, id2, slots, request.application.network.ingress.stablePort, previous);
11951
+ const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
11952
+ const port = ports[nextSlot];
11953
+ const name = nameFor(id2, nextSlot);
11954
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
11955
+ await checked6(host, runArgv(request, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
11956
+ let healthy = false;
11957
+ const health = request.application.service.health;
11958
+ for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
11959
+ if (await host.health(port, health.path, health.timeoutMs, health.method, health.expectedStatus)) {
11960
+ healthy = true;
11961
+ break;
11962
+ }
11963
+ await host.sleep(health.intervalMs ?? 1000);
11964
+ }
11965
+ if (!healthy) {
11966
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
11967
+ throw new Error("CONTAINER_HEALTH_FAILED");
11968
+ }
11969
+ const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id2}.conf`;
11970
+ const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
11971
+ host.write(nginxPath, nginx2(request, id2, port), 420);
11972
+ try {
11973
+ await checked6(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
11974
+ await checked6(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
11975
+ } catch (cause) {
11976
+ if (previousNginx === null)
11977
+ host.remove(nginxPath);
11978
+ else
11979
+ host.write(nginxPath, previousNginx, 420);
11980
+ await host.exec(["/usr/sbin/nginx", "-t"]);
11981
+ await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
11982
+ await host.exec(["/usr/bin/docker", "rm", "--force", name]);
11983
+ throw cause;
11984
+ }
11985
+ const state = {
11986
+ format: 1,
11987
+ id: id2,
11988
+ key: request.key,
11989
+ revision: request.revision,
11990
+ strategy: request.application.rollout.strategy,
11991
+ publicPort: request.application.network.ingress.stablePort,
11992
+ applicationPorts: ports,
11993
+ activeSlot: nextSlot,
11994
+ healthPath: health.path,
11995
+ updatedAtTs: host.now()
11996
+ };
11997
+ host.write(statePath2(id2), `${JSON.stringify(state, null, 2)}
11998
+ `, 384);
11999
+ if (previous && previous.activeSlot !== nextSlot) {
12000
+ await host.sleep(request.application.rollout.drainMs ?? 30000);
12001
+ await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id2, previous.activeSlot)]);
12002
+ }
12003
+ return state;
12004
+ }
12005
+
10277
12006
  // src/software-helper.ts
10278
12007
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
10279
12008
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
@@ -10282,11 +12011,13 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
10282
12011
  var MAX_PENDING_REQUESTS = 128;
10283
12012
  function startSoftwareHelper(options = {}) {
10284
12013
  const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
10285
- if (existsSync13(socketPath))
12014
+ if (existsSync15(socketPath))
10286
12015
  unlinkSync11(socketPath);
10287
- mkdirSync10(dirname7(socketPath), { recursive: true, mode: 488 });
12016
+ mkdirSync11(dirname8(socketPath), { recursive: true, mode: 488 });
10288
12017
  const ensure = options.ensure ?? ensureSoftwareRequirements;
10289
12018
  const activate = options.activate ?? activateSupervisedService;
12019
+ const buildContainer = options.buildContainer ?? buildContainerImage;
12020
+ const activateTypedContainer = options.activateContainer ?? activateContainer;
10290
12021
  let tail = Promise.resolve();
10291
12022
  let pending = 0;
10292
12023
  const server = createServer7((socket) => {
@@ -10319,10 +12050,29 @@ function startSoftwareHelper(options = {}) {
10319
12050
  return;
10320
12051
  }
10321
12052
  if (request.op === "activate-service" && request.request) {
10322
- if (!options.allowedRoot || request.request.root !== options.allowedRoot) {
12053
+ const serviceRequest = request.request;
12054
+ if (!options.allowedRoot || serviceRequest.root !== options.allowedRoot) {
10323
12055
  throw new Error("service deployment root is not owned by this helper");
10324
12056
  }
10325
- const state = await activate(request.request);
12057
+ const state = await activate(serviceRequest);
12058
+ socket.end(`${JSON.stringify({ ok: true, state })}
12059
+ `);
12060
+ return;
12061
+ }
12062
+ if (request.op === "build-container" && request.request) {
12063
+ const containerRequest = request.request;
12064
+ if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
12065
+ throw new Error("container deployment root is not owned by this helper");
12066
+ const result = await buildContainer(containerRequest);
12067
+ socket.end(`${JSON.stringify({ ok: true, result })}
12068
+ `);
12069
+ return;
12070
+ }
12071
+ if (request.op === "activate-container" && request.request) {
12072
+ const containerRequest = request.request;
12073
+ if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
12074
+ throw new Error("container deployment root is not owned by this helper");
12075
+ const state = await activateTypedContainer(containerRequest);
10326
12076
  socket.end(`${JSON.stringify({ ok: true, state })}
10327
12077
  `);
10328
12078
  return;
@@ -10346,8 +12096,43 @@ function startSoftwareHelper(options = {}) {
10346
12096
  server.listen(socketPath, () => chmodSync13(socketPath, 432));
10347
12097
  return server;
10348
12098
  }
12099
+ function requestContainer(op, request, socketPath, timeoutMs) {
12100
+ return new Promise((resolve6, reject) => {
12101
+ const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
12102
+ `));
12103
+ let buffer = "";
12104
+ socket.setTimeout(timeoutMs, () => {
12105
+ socket.destroy();
12106
+ reject(new Error("container helper timed out"));
12107
+ });
12108
+ socket.on("data", (chunk) => {
12109
+ buffer += chunk.toString("utf8");
12110
+ const newline = buffer.indexOf(`
12111
+ `);
12112
+ if (newline < 0)
12113
+ return;
12114
+ socket.end();
12115
+ try {
12116
+ const response = JSON.parse(buffer.slice(0, newline));
12117
+ const result = response.result ?? response.state;
12118
+ if (!response.ok || !result)
12119
+ throw new Error(response.error?.message ?? "container helper refused request");
12120
+ resolve6(result);
12121
+ } catch (cause) {
12122
+ reject(cause);
12123
+ }
12124
+ });
12125
+ socket.on("error", reject);
12126
+ });
12127
+ }
12128
+ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 30 * 60000) {
12129
+ return requestContainer("build-container", request, socketPath, timeoutMs);
12130
+ }
12131
+ function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
12132
+ return requestContainer("activate-container", request, socketPath, timeoutMs);
12133
+ }
10349
12134
  function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
10350
- return new Promise((resolve5, reject) => {
12135
+ return new Promise((resolve6, reject) => {
10351
12136
  const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
10352
12137
  `));
10353
12138
  let buffer = "";
@@ -10366,7 +12151,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
10366
12151
  const response = JSON.parse(buffer.slice(0, newline));
10367
12152
  if (!response.ok || !response.state)
10368
12153
  throw new Error(response.error?.message ?? "service helper refused activation");
10369
- resolve5(response.state);
12154
+ resolve6(response.state);
10370
12155
  } catch (cause) {
10371
12156
  reject(cause);
10372
12157
  }
@@ -10376,7 +12161,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
10376
12161
  }
10377
12162
  function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
10378
12163
  validateSoftwareRequirements(requirements);
10379
- return new Promise((resolve5, reject) => {
12164
+ return new Promise((resolve6, reject) => {
10380
12165
  const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
10381
12166
  `));
10382
12167
  let buffer = "";
@@ -10395,7 +12180,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
10395
12180
  const response = JSON.parse(buffer.slice(0, newline));
10396
12181
  if (!response.ok || !response.results)
10397
12182
  throw new Error(response.error?.message ?? "software helper refused the request");
10398
- resolve5(response.results);
12183
+ resolve6(response.results);
10399
12184
  } catch (cause) {
10400
12185
  reject(cause);
10401
12186
  }
@@ -10405,7 +12190,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
10405
12190
  }
10406
12191
 
10407
12192
  // src/egress-policy.ts
10408
- import { realpathSync as realpathSync3 } from "fs";
12193
+ import { realpathSync as realpathSync4 } from "fs";
10409
12194
  var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
10410
12195
  var SYSTEMD_RESOLVED_STUB = "/run/systemd/resolve/stub-resolv.conf";
10411
12196
  var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
@@ -10569,7 +12354,7 @@ function resolveServiceUid(user, command2 = systemCommand) {
10569
12354
  assertUid(uid);
10570
12355
  return uid;
10571
12356
  }
10572
- function assertResolvedStub(realpath = realpathSync3) {
12357
+ function assertResolvedStub(realpath = realpathSync4) {
10573
12358
  let source;
10574
12359
  try {
10575
12360
  source = realpath("/etc/resolv.conf");
@@ -10680,7 +12465,7 @@ async function superviseAgentEgressPolicy(options) {
10680
12465
  if (result.exitCode !== 0)
10681
12466
  throw new Error("Could not notify systemd that Agent egress is ready.");
10682
12467
  }))();
10683
- const wait = options.wait ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
12468
+ const wait = options.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
10684
12469
  const intervalMs = options.intervalMs ?? 1000;
10685
12470
  if (!Number.isSafeInteger(intervalMs) || intervalMs < 100 || intervalMs > 60000) {
10686
12471
  throw new Error("Agent egress monitor interval is invalid.");
@@ -10901,7 +12686,7 @@ function createAgentTelemetry(input, options = {}) {
10901
12686
  const warn = options.warn ?? ((message) => console.warn(`[telemetry] ${message}`));
10902
12687
  const now = options.now ?? Date.now;
10903
12688
  const random = options.random ?? Math.random;
10904
- const wait = options.wait ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
12689
+ const wait = options.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
10905
12690
  const processStartedAtMs = Math.max(0, Math.floor(now()));
10906
12691
  const processInstanceId = randomHex(16);
10907
12692
  const resource = {
@@ -10971,10 +12756,10 @@ function createAgentTelemetry(input, options = {}) {
10971
12756
  if (!transient || attempt + 1 >= AGENT_OTLP_MAX_ATTEMPTS)
10972
12757
  throw new PermanentExportError;
10973
12758
  const retryAfter2 = cause instanceof RetryableExportError ? cause.retryAfterMs : null;
10974
- const backoff = AGENT_OTLP_RETRY_BASE_MS * 2 ** attempt;
12759
+ const backoff2 = AGENT_OTLP_RETRY_BASE_MS * 2 ** attempt;
10975
12760
  const randomValue = random();
10976
- const jitter = Math.floor((Number.isFinite(randomValue) ? Math.max(0, Math.min(1, randomValue)) : 0) * backoff);
10977
- await wait(Math.min(AGENT_OTLP_MAX_RETRY_DELAY_MS, Math.max(retryAfter2 ?? 0, backoff + jitter)));
12761
+ const jitter = Math.floor((Number.isFinite(randomValue) ? Math.max(0, Math.min(1, randomValue)) : 0) * backoff2);
12762
+ await wait(Math.min(AGENT_OTLP_MAX_RETRY_DELAY_MS, Math.max(retryAfter2 ?? 0, backoff2 + jitter)));
10978
12763
  }
10979
12764
  }
10980
12765
  throw new PermanentExportError;
@@ -11109,8 +12894,8 @@ function createAgentTelemetry(input, options = {}) {
11109
12894
  } }, { name: "forgezero.agent.operations", sum: {
11110
12895
  aggregationTemporality: 2,
11111
12896
  isMonotonic: true,
11112
- dataPoints: metricRows.map((row, index) => ({
11113
- attributes: row.attributes,
12897
+ dataPoints: metricRows.map((row2, index) => ({
12898
+ attributes: row2.attributes,
11114
12899
  asInt: String(metricValues[index].count),
11115
12900
  startTimeUnixNano,
11116
12901
  timeUnixNano
@@ -11340,17 +13125,17 @@ import {
11340
13125
  chmodSync as chmodSync14,
11341
13126
  chownSync,
11342
13127
  copyFileSync as copyFileSync2,
11343
- existsSync as existsSync14,
13128
+ existsSync as existsSync16,
11344
13129
  lstatSync as lstatSync2,
11345
- mkdirSync as mkdirSync11,
11346
- readFileSync as readFileSync12,
11347
- readdirSync as readdirSync3,
11348
- realpathSync as realpathSync4,
11349
- renameSync as renameSync8,
11350
- rmSync as rmSync7,
13130
+ mkdirSync as mkdirSync12,
13131
+ readFileSync as readFileSync13,
13132
+ readdirSync as readdirSync4,
13133
+ realpathSync as realpathSync5,
13134
+ renameSync as renameSync9,
13135
+ rmSync as rmSync8,
11351
13136
  statSync as statSync3,
11352
13137
  symlinkSync as symlinkSync4,
11353
- writeFileSync as writeFileSync11
13138
+ writeFileSync as writeFileSync12
11354
13139
  } from "fs";
11355
13140
  import { join as join10 } from "path";
11356
13141
  var boundedInteger2 = (name, value, minimum, maximum) => {
@@ -11404,13 +13189,13 @@ var platformExec = async (argv2) => {
11404
13189
  };
11405
13190
  var replaceLink = (path2, target2) => {
11406
13191
  const pending = `${path2}.next`;
11407
- rmSync7(pending, { force: true });
13192
+ rmSync8(pending, { force: true });
11408
13193
  if (!target2) {
11409
- rmSync7(path2, { force: true });
13194
+ rmSync8(path2, { force: true });
11410
13195
  return;
11411
13196
  }
11412
13197
  symlinkSync4(target2, pending);
11413
- renameSync8(pending, path2);
13198
+ renameSync9(pending, path2);
11414
13199
  };
11415
13200
  var secureRelease = (path2, uid, gid) => {
11416
13201
  const visit = (current) => {
@@ -11420,7 +13205,7 @@ var secureRelease = (path2, uid, gid) => {
11420
13205
  chownSync(current, uid, gid);
11421
13206
  chmodSync14(current, metadata.isDirectory() ? 365 : 292);
11422
13207
  if (metadata.isDirectory())
11423
- for (const name of readdirSync3(current))
13208
+ for (const name of readdirSync4(current))
11424
13209
  visit(join10(current, name));
11425
13210
  };
11426
13211
  visit(path2);
@@ -11428,28 +13213,28 @@ var secureRelease = (path2, uid, gid) => {
11428
13213
  async function activatePlatformRelease(config, requestedRelease, options = {}) {
11429
13214
  const rendered = renderPlatformActivationFiles(config);
11430
13215
  const normalized = JSON.parse(rendered.helper);
11431
- const releases = realpathSync4(join10(normalized.root, "releases"));
11432
- const release = realpathSync4(requestedRelease);
13216
+ const releases = realpathSync5(join10(normalized.root, "releases"));
13217
+ const release = realpathSync5(requestedRelease);
11433
13218
  if (!release.startsWith(`${releases}/`) || release === releases)
11434
13219
  throw new Error("release is outside configured releases directory");
11435
- for (const required of [".fz/deploy.json", "src/index.ts", "bun.lock"]) {
13220
+ for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
11436
13221
  const metadata = statSync3(join10(release, required));
11437
13222
  if (!metadata.isFile() || metadata.size < 1)
11438
13223
  throw new Error(`release is incomplete: ${required}`);
11439
13224
  }
11440
13225
  const exec = options.exec ?? platformExec;
11441
13226
  const request = options.fetch ?? fetch;
11442
- const sleep = options.sleep ?? Bun.sleep;
13227
+ const sleep2 = options.sleep ?? Bun.sleep;
11443
13228
  const slots = join10(normalized.root, "slots");
11444
- mkdirSync11(slots, { recursive: true, mode: 493 });
13229
+ mkdirSync12(slots, { recursive: true, mode: 493 });
11445
13230
  const slotFile = join10(normalized.root, ".forge-slot");
11446
- const previousSlot = existsSync14(slotFile) ? readFileSync12(slotFile, "utf8").trim() : undefined;
13231
+ const previousSlot = existsSync16(slotFile) ? readFileSync13(slotFile, "utf8").trim() : undefined;
11447
13232
  const target2 = previousSlot === "blue" ? "green" : "blue";
11448
13233
  const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
11449
13234
  const targetLink = join10(slots, target2);
11450
13235
  let previousTarget;
11451
13236
  try {
11452
- previousTarget = realpathSync4(targetLink);
13237
+ previousTarget = realpathSync5(targetLink);
11453
13238
  } catch {}
11454
13239
  const user = await exec(["/usr/bin/id", "-u", normalized.serviceUser]);
11455
13240
  const group = await exec(["/usr/bin/id", "-g", normalized.serviceUser]);
@@ -11471,7 +13256,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
11471
13256
  } catch {}
11472
13257
  if (healthy)
11473
13258
  break;
11474
- await sleep(1000);
13259
+ await sleep2(1000);
11475
13260
  }
11476
13261
  const stopTarget = async () => {
11477
13262
  await exec(["/usr/bin/systemctl", "stop", service]);
@@ -11483,40 +13268,40 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
11483
13268
  }
11484
13269
  const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
11485
13270
  const backup = `${upstream}.forgezero-backup`;
11486
- if (existsSync14(upstream))
13271
+ if (existsSync16(upstream))
11487
13272
  copyFileSync2(upstream, backup);
11488
13273
  else
11489
- rmSync7(backup, { force: true });
11490
- writeFileSync11(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
13274
+ rmSync8(backup, { force: true });
13275
+ writeFileSync12(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
11491
13276
  `, { mode: 420 });
11492
13277
  const test = await exec(["/usr/sbin/nginx", "-t"]);
11493
13278
  const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
11494
13279
  if (reload.exitCode !== 0) {
11495
- if (existsSync14(backup))
11496
- renameSync8(backup, upstream);
13280
+ if (existsSync16(backup))
13281
+ renameSync9(backup, upstream);
11497
13282
  else
11498
- rmSync7(upstream, { force: true });
13283
+ rmSync8(upstream, { force: true });
11499
13284
  await exec(["/usr/sbin/nginx", "-t"]);
11500
13285
  await exec(["/usr/sbin/nginx", "-s", "reload"]);
11501
13286
  await stopTarget();
11502
13287
  throw new Error("nginx refused the promoted upstream");
11503
13288
  }
11504
- rmSync7(backup, { force: true });
11505
- writeFileSync11(slotFile, `${target2}
13289
+ rmSync8(backup, { force: true });
13290
+ writeFileSync12(slotFile, `${target2}
11506
13291
  `, { mode: 420 });
11507
13292
  if (previousSlot && previousSlot !== target2) {
11508
- await sleep(normalized.drainDeadlineMs);
13293
+ await sleep2(normalized.drainDeadlineMs);
11509
13294
  await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
11510
13295
  }
11511
- const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync3(right).mtimeMs - statSync3(left).mtimeMs).slice(normalized.keepReleases);
13296
+ const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync3(right).mtimeMs - statSync3(left).mtimeMs).slice(normalized.keepReleases);
11512
13297
  for (const path2 of old)
11513
13298
  if (path2 !== release)
11514
- rmSync7(path2, { recursive: true, force: true });
13299
+ rmSync8(path2, { recursive: true, force: true });
11515
13300
  return { release, slot: target2 };
11516
13301
  }
11517
13302
 
11518
13303
  // src/recovery-host.ts
11519
- import { existsSync as existsSync15, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
13304
+ import { existsSync as existsSync17, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
11520
13305
  import { hostname } from "os";
11521
13306
  import { join as join11 } from "path";
11522
13307
  var execute2 = async (argv2) => {
@@ -11524,25 +13309,25 @@ var execute2 = async (argv2) => {
11524
13309
  const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
11525
13310
  return { stdout, stderr, exitCode };
11526
13311
  };
11527
- var checked6 = async (run2, argv2, expected = 0) => {
13312
+ var checked7 = async (run2, argv2, expected = 0) => {
11528
13313
  const result = await run2(argv2);
11529
13314
  if (result.exitCode !== expected)
11530
13315
  throw new Error(`${argv2[0]} failed (${result.exitCode})`);
11531
13316
  return result;
11532
13317
  };
11533
13318
  var activeSlot = () => {
11534
- const slot = readFileSync13("/opt/forgezero/.forge-slot", "utf8").trim();
13319
+ const slot = readFileSync14("/opt/forgezero/.forge-slot", "utf8").trim();
11535
13320
  if (slot !== "blue" && slot !== "green")
11536
13321
  throw new Error("invalid active slot");
11537
13322
  return slot;
11538
13323
  };
11539
13324
  var exactState = (path2, expected) => {
11540
- if (!readFileSync13(path2, "utf8").split(`
13325
+ if (!readFileSync14(path2, "utf8").split(`
11541
13326
  `).includes(expected))
11542
13327
  throw new Error(`state mismatch: ${expected}`);
11543
13328
  };
11544
13329
  var nonEmpty = (path2) => {
11545
- if (!existsSync15(path2) || !statSync4(path2).isFile() || statSync4(path2).size < 1)
13330
+ if (!existsSync17(path2) || !statSync4(path2).isFile() || statSync4(path2).size < 1)
11546
13331
  throw new Error(`required file missing: ${path2}`);
11547
13332
  };
11548
13333
  var option = (args, name) => {
@@ -11586,29 +13371,29 @@ async function runRecoveryHost(args, run2 = execute2) {
11586
13371
  const slot = activeSlot();
11587
13372
  nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/restore-backup.ts"));
11588
13373
  nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/verify-recovery-cluster.ts"));
11589
- await checked6(run2, ["/usr/bin/test", "-x", "/usr/local/bin/fz"]);
11590
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], hasDatabase ? 0 : 3);
13374
+ await checked7(run2, ["/usr/bin/test", "-x", "/usr/local/bin/fz"]);
13375
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], hasDatabase ? 0 : 3);
11591
13376
  if (args.includes("--require-stopped")) {
11592
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
11593
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
13377
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
13378
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
11594
13379
  }
11595
13380
  return;
11596
13381
  }
11597
13382
  if (action === "fence") {
11598
- await checked6(run2, ["/usr/bin/systemctl", "stop", "forgezero@blue.service", "forgezero@green.service"]);
11599
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
11600
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
13383
+ await checked7(run2, ["/usr/bin/systemctl", "stop", "forgezero@blue.service", "forgezero@green.service"]);
13384
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
13385
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
11601
13386
  const role = option(args, "db-role");
11602
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], role === "none" ? 3 : 0);
13387
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], role === "none" ? 3 : 0);
11603
13388
  return;
11604
13389
  }
11605
13390
  if (action === "start-active") {
11606
13391
  const slot = activeSlot();
11607
13392
  const inactive = slot === "blue" ? "green" : "blue";
11608
- await checked6(run2, ["/usr/bin/systemctl", "stop", `forgezero@${inactive}.service`]);
11609
- await checked6(run2, ["/usr/bin/systemctl", "start", `forgezero@${slot}.service`]);
11610
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${slot}.service`]);
11611
- await checked6(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${inactive}.service`], 3);
13393
+ await checked7(run2, ["/usr/bin/systemctl", "stop", `forgezero@${inactive}.service`]);
13394
+ await checked7(run2, ["/usr/bin/systemctl", "start", `forgezero@${slot}.service`]);
13395
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${slot}.service`]);
13396
+ await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${inactive}.service`], 3);
11612
13397
  return;
11613
13398
  }
11614
13399
  if (action === "maintenance") {
@@ -11631,7 +13416,7 @@ async function runRecoveryHost(args, run2 = execute2) {
11631
13416
  "--property=LoadCredentialEncrypted=backup-recovery-root:/etc/forgezero/creds/backup-recovery-root.cred"
11632
13417
  ] : []
11633
13418
  ];
11634
- await checked6(run2, ["/usr/bin/systemd-run", "--quiet", "--wait", "--pipe", "--collect", `--unit=${unit2(option(args, "unit"))}`, ...properties, "/usr/local/bin/fz", ...fzArgs]);
13419
+ await checked7(run2, ["/usr/bin/systemd-run", "--quiet", "--wait", "--pipe", "--collect", `--unit=${unit2(option(args, "unit"))}`, ...properties, "/usr/local/bin/fz", ...fzArgs]);
11635
13420
  return;
11636
13421
  }
11637
13422
  throw new Error("unknown recovery-host action");
@@ -11643,15 +13428,15 @@ import { lstatSync as lstatSync5 } from "fs";
11643
13428
  // src/bootstrap.ts
11644
13429
  import {
11645
13430
  chmodSync as chmodSync16,
11646
- existsSync as existsSync17,
13431
+ existsSync as existsSync19,
11647
13432
  lstatSync as lstatSync4,
11648
- mkdirSync as mkdirSync13,
11649
- readFileSync as readFileSync15,
11650
- renameSync as renameSync10,
11651
- rmSync as rmSync9,
11652
- writeFileSync as writeFileSync13
13433
+ mkdirSync as mkdirSync14,
13434
+ readFileSync as readFileSync16,
13435
+ renameSync as renameSync11,
13436
+ rmSync as rmSync10,
13437
+ writeFileSync as writeFileSync14
11653
13438
  } from "fs";
11654
- import { dirname as dirname9 } from "path";
13439
+ import { dirname as dirname10 } from "path";
11655
13440
  import { fileURLToPath as fileURLToPath2 } from "url";
11656
13441
 
11657
13442
  // src/provision.ts
@@ -12354,7 +14139,7 @@ fz-agent egress-policy-check`;
12354
14139
  return "/usr/bin/apt-get install -y cloudflare-warp";
12355
14140
  return "/usr/bin/warp-cli --accept-tos status";
12356
14141
  };
12357
- var step = (label, operation, optional = false) => ({
14142
+ var step2 = (label, operation, optional = false) => ({
12358
14143
  label,
12359
14144
  operation,
12360
14145
  optional: optional || undefined,
@@ -12477,44 +14262,44 @@ function planProvision(options) {
12477
14262
  socketPath: options.socketPath,
12478
14263
  user,
12479
14264
  steps: [
12480
- ...options.enforceEgress ? [step("Ubuntu Agent egress prerequisites", { kind: "commands", commands: [
14265
+ ...options.enforceEgress ? [step2("Ubuntu Agent egress prerequisites", { kind: "commands", commands: [
12481
14266
  { argv: ["/usr/bin/apt-get", "update", "-qq"] },
12482
14267
  { argv: ["/usr/bin/apt-get", "install", "-y", "nftables"] },
12483
14268
  { argv: ["/usr/bin/systemctl", "enable", "--now", "systemd-resolved.service"] }
12484
- ] }), step("prove systemd-resolved stub ownership", { kind: "verify-resolved-stub" })] : [],
12485
- step("vault socket access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", VAULT_GROUP], acceptedExitCodes: [0, 9] }] }),
12486
- step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
12487
- ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION })] : [],
12488
- ...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
12489
- ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
14269
+ ] }), step2("prove systemd-resolved stub ownership", { kind: "verify-resolved-stub" })] : [],
14270
+ step2("vault socket access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", VAULT_GROUP], acceptedExitCodes: [0, 9] }] }),
14271
+ step2("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
14272
+ ...sourceBinPath && binPath ? [step2("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION2 })] : [],
14273
+ ...warpEnabled ? [step2("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
14274
+ ...deploymentEnabled ? [step2("deployment isolation group", { kind: "commands", commands: [
12490
14275
  { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
12491
14276
  { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
12492
14277
  ] })] : [],
12493
- ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
12494
- step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
12495
- step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
12496
- step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
12497
- ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
12498
- ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
14278
+ ...lifecycleEnabled ? [step2("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
14279
+ step2("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
14280
+ step2("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
14281
+ step2("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
14282
+ ...lifecycleEnabled ? [step2("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
14283
+ ...deploymentEnabled ? [step2("credential-free deployment account", { kind: "commands", commands: [
12499
14284
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
12500
14285
  { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
12501
14286
  { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
12502
14287
  { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
12503
14288
  ] })] : [],
12504
- step("credential and state directories", { kind: "directories", directories: [
14289
+ step2("credential and state directories", { kind: "directories", directories: [
12505
14290
  { path: credentialDir, mode: 448, owner: "root", group: "root" },
12506
14291
  { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
12507
14292
  ] }),
12508
- step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
14293
+ step2("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
12509
14294
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
12510
- step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
12511
- step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
14295
+ step2("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
14296
+ step2("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
12512
14297
  ] : [],
12513
14298
  ...bootstrapEnabled ? [
12514
- step("bootstrap SSH public identity directory", { kind: "directories", directories: [
14299
+ step2("bootstrap SSH public identity directory", { kind: "directories", directories: [
12515
14300
  { path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
12516
14301
  ] }),
12517
- step("unique encrypted bootstrap SSH identity", {
14302
+ step2("unique encrypted bootstrap SSH identity", {
12518
14303
  kind: "ensure-bootstrap-ssh-identity",
12519
14304
  credential: bootstrapSshCredentialPath,
12520
14305
  publicKey: bootstrapSshPublicKeyPath,
@@ -12522,10 +14307,10 @@ function planProvision(options) {
12522
14307
  })
12523
14308
  ] : [],
12524
14309
  ...enrolmentEnabled ? [
12525
- step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
12526
- step("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
14310
+ step2("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
14311
+ step2("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
12527
14312
  ] : [],
12528
- ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
14313
+ ...deploymentEnabled ? [step2("deployment directories", { kind: "directories", directories: [
12529
14314
  { path: deployRoot, mode: 493, owner: "root", group: "root" },
12530
14315
  { path: `${deployRoot}/releases`, mode: 2040, owner: "root", group: DEPLOYMENT_GROUP },
12531
14316
  { path: `${deployRoot}/cache`, mode: 488, owner: user, group: user },
@@ -12535,13 +14320,13 @@ function planProvision(options) {
12535
14320
  { path: `${deployRoot}/runner-home/cache`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
12536
14321
  { path: `${deployRoot}/app-home`, mode: 448, owner: APPLICATION_RUNTIME_USER, group: VAULT_GROUP }
12537
14322
  ] })] : [],
12538
- step("reload units", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "daemon-reload"] }] }),
12539
- ...deploymentEnabled ? [step("remove unsupported deployment socket activation", { kind: "commands", commands: [
14323
+ step2("reload units", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "daemon-reload"] }] }),
14324
+ ...deploymentEnabled ? [step2("remove unsupported deployment socket activation", { kind: "commands", commands: [
12540
14325
  { argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-deploy-runner.socket"], acceptedExitCodes: [0, 1, 5] },
12541
14326
  { argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
12542
14327
  { argv: ["/usr/bin/systemctl", "daemon-reload"] }
12543
14328
  ] })] : [],
12544
- step("enable and converge services", { kind: "commands", commands: [
14329
+ step2("enable and converge services", { kind: "commands", commands: [
12545
14330
  { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
12546
14331
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
12547
14332
  ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
@@ -12549,19 +14334,19 @@ function planProvision(options) {
12549
14334
  { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
12550
14335
  { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
12551
14336
  ] }),
12552
- ...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
12553
- ...options.enforceEgress ? [step("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
12554
- step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
12555
- step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
12556
- step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
12557
- step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
14337
+ ...enrolmentEnabled ? [step2("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
14338
+ ...options.enforceEgress ? [step2("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
14339
+ step2("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
14340
+ step2("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
14341
+ step2("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
14342
+ step2("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
12558
14343
  ...deploymentEnabled ? [
12559
- step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
12560
- step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
14344
+ step2("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
14345
+ step2("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
12561
14346
  ] : [],
12562
- ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
12563
- ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
12564
- ...options.repository ? [step("prove the deployment control socket exists", { kind: "wait-socket", path: options.controlSocketPath ?? "/run/forgezero/control.sock", attempts: 100, intervalMs: 100 })] : []
14347
+ ...lifecycleEnabled ? [step2("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
14348
+ ...warpEnabled ? [step2("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
14349
+ ...options.repository ? [step2("prove the deployment control socket exists", { kind: "wait-socket", path: options.controlSocketPath ?? "/run/forgezero/control.sock", attempts: 100, intervalMs: 100 })] : []
12565
14350
  ]
12566
14351
  };
12567
14352
  }
@@ -12571,17 +14356,17 @@ import { randomBytes as randomBytes5 } from "crypto";
12571
14356
  import {
12572
14357
  chmodSync as chmodSync15,
12573
14358
  copyFileSync as copyFileSync3,
12574
- existsSync as existsSync16,
14359
+ existsSync as existsSync18,
12575
14360
  lstatSync as lstatSync3,
12576
- mkdirSync as mkdirSync12,
12577
- readFileSync as readFileSync14,
12578
- realpathSync as realpathSync5,
12579
- renameSync as renameSync9,
12580
- rmSync as rmSync8,
14361
+ mkdirSync as mkdirSync13,
14362
+ readFileSync as readFileSync15,
14363
+ realpathSync as realpathSync6,
14364
+ renameSync as renameSync10,
14365
+ rmSync as rmSync9,
12581
14366
  symlinkSync as symlinkSync5,
12582
- writeFileSync as writeFileSync12
14367
+ writeFileSync as writeFileSync13
12583
14368
  } from "fs";
12584
- import { dirname as dirname8 } from "path";
14369
+ import { dirname as dirname9 } from "path";
12585
14370
  async function readCapabilities(run2) {
12586
14371
  const answers = {};
12587
14372
  const checks = Object.entries(CAPABILITY_CHECKS);
@@ -12640,28 +14425,28 @@ var runProvisionOperation = async (operation) => {
12640
14425
  }
12641
14426
  if (operation.kind === "install-runtime") {
12642
14427
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
12643
- mkdirSync12(`${release}/dist`, { recursive: true, mode: 493 });
12644
- mkdirSync12(dirname8(operation.binary), { recursive: true, mode: 493 });
14428
+ mkdirSync13(`${release}/dist`, { recursive: true, mode: 493 });
14429
+ mkdirSync13(dirname9(operation.binary), { recursive: true, mode: 493 });
12645
14430
  copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
12646
14431
  chmodSync15(`${release}/dist/fz-agent.js`, 493);
12647
- const gitSshSource = `${dirname8(operation.source)}/fz-git-ssh.js`;
12648
- if (!existsSync16(gitSshSource))
14432
+ const gitSshSource = `${dirname9(operation.source)}/fz-git-ssh.js`;
14433
+ if (!existsSync18(gitSshSource))
12649
14434
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
12650
14435
  copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
12651
14436
  chmodSync15(`${release}/dist/fz-git-ssh.js`, 493);
12652
14437
  const pending = "/opt/forgezero/agent/current.next";
12653
- rmSync8(pending, { force: true });
14438
+ rmSync9(pending, { force: true });
12654
14439
  symlinkSync5(`versions/${operation.version}`, pending);
12655
- renameSync9(pending, "/opt/forgezero/agent/current");
12656
- rmSync8(operation.binary, { force: true });
14440
+ renameSync10(pending, "/opt/forgezero/agent/current");
14441
+ rmSync9(operation.binary, { force: true });
12657
14442
  symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
12658
14443
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
12659
- rmSync8(gitSshBinary, { force: true });
14444
+ rmSync9(gitSshBinary, { force: true });
12660
14445
  symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
12661
14446
  return { stdout: "", exitCode: 0 };
12662
14447
  }
12663
14448
  if (operation.kind === "ensure-seed") {
12664
- if (existsSync16(operation.credential) && lstatSync3(operation.credential).size > 0)
14449
+ if (existsSync18(operation.credential) && lstatSync3(operation.credential).size > 0)
12665
14450
  return { stdout: "", exitCode: 0 };
12666
14451
  const seed = randomBytes5(32).toString("base64url");
12667
14452
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
@@ -12673,9 +14458,9 @@ var runProvisionOperation = async (operation) => {
12673
14458
  const key = "/run/forgezero-git-deploy-key";
12674
14459
  const publicKey = `${key}.pub`;
12675
14460
  try {
12676
- if (!existsSync16(operation.credential) || lstatSync3(operation.credential).size < 1) {
12677
- rmSync8(key, { force: true });
12678
- rmSync8(publicKey, { force: true });
14461
+ if (!existsSync18(operation.credential) || lstatSync3(operation.credential).size < 1) {
14462
+ rmSync9(key, { force: true });
14463
+ rmSync9(publicKey, { force: true });
12679
14464
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
12680
14465
  if (result.exitCode !== 0)
12681
14466
  return result;
@@ -12684,8 +14469,8 @@ var runProvisionOperation = async (operation) => {
12684
14469
  return result;
12685
14470
  chmodSync15(operation.credential, 256);
12686
14471
  }
12687
- if (!existsSync16(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
12688
- if (!existsSync16(key)) {
14472
+ if (!existsSync18(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
14473
+ if (!existsSync18(key)) {
12689
14474
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
12690
14475
  if (decrypted.exitCode !== 0)
12691
14476
  return decrypted;
@@ -12693,25 +14478,25 @@ var runProvisionOperation = async (operation) => {
12693
14478
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
12694
14479
  if (derived.exitCode !== 0)
12695
14480
  return derived;
12696
- writeFileSync12(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
14481
+ writeFileSync13(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
12697
14482
  `, { mode: 292 });
12698
14483
  }
12699
14484
  return { stdout: "", exitCode: 0 };
12700
14485
  } finally {
12701
- rmSync8(key, { force: true });
12702
- rmSync8(publicKey, { force: true });
14486
+ rmSync9(key, { force: true });
14487
+ rmSync9(publicKey, { force: true });
12703
14488
  }
12704
14489
  }
12705
14490
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
12706
14491
  const key = "/run/forgezero-bootstrap-ssh-key";
12707
14492
  const generatedPublicKey = `${key}.pub`;
12708
14493
  try {
12709
- if (!existsSync16(operation.credential) || lstatSync3(operation.credential).size < 1) {
12710
- rmSync8(key, { force: true });
12711
- rmSync8(generatedPublicKey, { force: true });
14494
+ if (!existsSync18(operation.credential) || lstatSync3(operation.credential).size < 1) {
14495
+ rmSync9(key, { force: true });
14496
+ rmSync9(generatedPublicKey, { force: true });
12712
14497
  let result;
12713
14498
  if (operation.source) {
12714
- const source = existsSync16(operation.source) ? lstatSync3(operation.source) : undefined;
14499
+ const source = existsSync18(operation.source) ? lstatSync3(operation.source) : undefined;
12715
14500
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
12716
14501
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
12717
14502
  }
@@ -12731,8 +14516,8 @@ var runProvisionOperation = async (operation) => {
12731
14516
  return result;
12732
14517
  chmodSync15(operation.credential, 256);
12733
14518
  }
12734
- if (!existsSync16(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
12735
- if (!existsSync16(key)) {
14519
+ if (!existsSync18(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
14520
+ if (!existsSync18(key)) {
12736
14521
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
12737
14522
  if (decrypted.exitCode !== 0)
12738
14523
  return decrypted;
@@ -12742,28 +14527,28 @@ var runProvisionOperation = async (operation) => {
12742
14527
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
12743
14528
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
12744
14529
  }
12745
- mkdirSync12(dirname8(operation.publicKey), { recursive: true, mode: 493 });
12746
- writeFileSync12(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
14530
+ mkdirSync13(dirname9(operation.publicKey), { recursive: true, mode: 493 });
14531
+ writeFileSync13(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
12747
14532
  `, { mode: 292 });
12748
14533
  chmodSync15(operation.publicKey, 292);
12749
14534
  }
12750
14535
  if (operation.source)
12751
- rmSync8(operation.source, { force: true });
14536
+ rmSync9(operation.source, { force: true });
12752
14537
  return { stdout: "", exitCode: 0 };
12753
14538
  } finally {
12754
- rmSync8(key, { force: true });
12755
- rmSync8(generatedPublicKey, { force: true });
14539
+ rmSync9(key, { force: true });
14540
+ rmSync9(generatedPublicKey, { force: true });
12756
14541
  }
12757
14542
  }
12758
14543
  if (operation.kind === "ensure-enrolment") {
12759
- if (existsSync16(operation.state) && lstatSync3(operation.state).size > 0 || existsSync16(operation.credential) && lstatSync3(operation.credential).size > 0)
14544
+ if (existsSync18(operation.state) && lstatSync3(operation.state).size > 0 || existsSync18(operation.credential) && lstatSync3(operation.credential).size > 0)
12760
14545
  return { stdout: "", exitCode: 0 };
12761
- if (!existsSync16(operation.source))
14546
+ if (!existsSync18(operation.source))
12762
14547
  return { stdout: "enrolment source is missing", exitCode: 1 };
12763
14548
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
12764
14549
  if (result.exitCode === 0) {
12765
14550
  chmodSync15(operation.credential, 256);
12766
- rmSync8(operation.source, { force: true });
14551
+ rmSync9(operation.source, { force: true });
12767
14552
  }
12768
14553
  return result;
12769
14554
  }
@@ -12778,7 +14563,7 @@ var runProvisionOperation = async (operation) => {
12778
14563
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
12779
14564
  }
12780
14565
  if (operation.kind === "verify-file")
12781
- return existsSync16(operation.path) && lstatSync3(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
14566
+ return existsSync18(operation.path) && lstatSync3(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
12782
14567
  if (operation.kind === "verify-egress") {
12783
14568
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
12784
14569
  if (active.exitCode !== 0)
@@ -12794,31 +14579,31 @@ var runProvisionOperation = async (operation) => {
12794
14579
  if (operation.kind === "verify-resolved-stub") {
12795
14580
  try {
12796
14581
  const expected = "/run/systemd/resolve/stub-resolv.conf";
12797
- return realpathSync5("/etc/resolv.conf") === expected && realpathSync5(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
14582
+ return realpathSync6("/etc/resolv.conf") === expected && realpathSync6(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
12798
14583
  } catch {
12799
14584
  return { stdout: "resolver stub missing", exitCode: 1 };
12800
14585
  }
12801
14586
  }
12802
14587
  if (operation.kind === "install-warp") {
12803
- const os = readFileSync14("/etc/os-release", "utf8");
14588
+ const os = readFileSync15("/etc/os-release", "utf8");
12804
14589
  if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
12805
14590
  return { stdout: "unsupported WARP host OS", exitCode: 1 };
12806
14591
  const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
12807
14592
  if (!response.ok)
12808
14593
  return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
12809
- mkdirSync12("/usr/share/keyrings", { recursive: true, mode: 493 });
12810
- mkdirSync12("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
12811
- mkdirSync12("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
14594
+ mkdirSync13("/usr/share/keyrings", { recursive: true, mode: 493 });
14595
+ mkdirSync13("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
14596
+ mkdirSync13("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
12812
14597
  const key = "/run/cloudflare-warp-key.gpg";
12813
- writeFileSync12(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
14598
+ writeFileSync13(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
12814
14599
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
12815
- rmSync8(key, { force: true });
14600
+ rmSync9(key, { force: true });
12816
14601
  if (result.exitCode !== 0)
12817
14602
  return result;
12818
14603
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
12819
14604
  if (!codename)
12820
14605
  return { stdout: "Ubuntu codename missing", exitCode: 1 };
12821
- writeFileSync12("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
14606
+ writeFileSync13("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
12822
14607
  `, { mode: 420 });
12823
14608
  result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
12824
14609
  return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
@@ -12849,11 +14634,11 @@ function planInstall(options) {
12849
14634
  }
12850
14635
  async function applyPlan(plan, run2) {
12851
14636
  const transcript = [];
12852
- for (const step2 of plan.steps) {
12853
- const result = await run2(step2.operation);
12854
- transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
12855
- if (result.exitCode !== 0 && !step2.optional) {
12856
- throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
14637
+ for (const step3 of plan.steps) {
14638
+ const result = await run2(step3.operation);
14639
+ transcript.push({ label: step3.label, command: step3.command, exitCode: result.exitCode });
14640
+ if (result.exitCode !== 0 && !step3.optional) {
14641
+ throw new Error(`${step3.label} failed (exit ${result.exitCode}): ${step3.command}`);
12857
14642
  }
12858
14643
  }
12859
14644
  return transcript;
@@ -13024,9 +14809,9 @@ async function bootstrapStatus(host = localBootstrapHost()) {
13024
14809
  if (health.exitCode !== 0)
13025
14810
  problems.push("platform API health check failed");
13026
14811
  }
13027
- const nginx2 = await host.exec(["nginx", "-t"]);
13028
- services["nginx-config"] = nginx2.exitCode === 0;
13029
- if (nginx2.exitCode !== 0)
14812
+ const nginx3 = await host.exec(["nginx", "-t"]);
14813
+ services["nginx-config"] = nginx3.exitCode === 0;
14814
+ if (nginx3.exitCode !== 0)
13030
14815
  problems.push("nginx configuration is invalid");
13031
14816
  if (state.databaseRole !== "none") {
13032
14817
  const unitPath = "/etc/systemd/system/forgezero-db.service";
@@ -13071,17 +14856,17 @@ function localBootstrapHost() {
13071
14856
  };
13072
14857
  return {
13073
14858
  uid: () => process.getuid?.() ?? -1,
13074
- exists: existsSync17,
13075
- read: (path2) => readFileSync15(path2, "utf8"),
14859
+ exists: existsSync19,
14860
+ read: (path2) => readFileSync16(path2, "utf8"),
13076
14861
  write(path2, content, mode) {
13077
- mkdirSync13(dirname9(path2), { recursive: true, mode: 493 });
14862
+ mkdirSync14(dirname10(path2), { recursive: true, mode: 493 });
13078
14863
  const temporary = `${path2}.next.${process.pid}`;
13079
- writeFileSync13(temporary, content, { mode });
14864
+ writeFileSync14(temporary, content, { mode });
13080
14865
  chmodSync16(temporary, mode);
13081
- renameSync10(temporary, path2);
14866
+ renameSync11(temporary, path2);
13082
14867
  },
13083
- mkdir: (path2, mode) => mkdirSync13(path2, { recursive: true, mode }),
13084
- remove: (path2) => rmSync9(path2, { force: true }),
14868
+ mkdir: (path2, mode) => mkdirSync14(path2, { recursive: true, mode }),
14869
+ remove: (path2) => rmSync10(path2, { force: true }),
13085
14870
  inspect(path2) {
13086
14871
  const value = lstatSync4(path2);
13087
14872
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
@@ -13105,7 +14890,7 @@ function localBootstrapHost() {
13105
14890
  async installAgent(config, enrolTokenSourcePath) {
13106
14891
  const capabilities = await readCapabilities(localRunner);
13107
14892
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
13108
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync17("/var/lib/forgezero/enrolment.json");
14893
+ const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync19("/var/lib/forgezero/enrolment.json");
13109
14894
  if (config.kind === "platform") {
13110
14895
  const lifecycle = config.database.role === "none" ? {
13111
14896
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -13117,8 +14902,8 @@ function localBootstrapHost() {
13117
14902
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
13118
14903
  databasePorts: [8529]
13119
14904
  };
13120
- mkdirSync13(dirname9(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
13121
- writeFileSync13(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
14905
+ mkdirSync14(dirname10(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
14906
+ writeFileSync14(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
13122
14907
  `, { mode: 256 });
13123
14908
  }
13124
14909
  const plan = planInstall({
@@ -13159,8 +14944,8 @@ function localBootstrapHost() {
13159
14944
  } : {}
13160
14945
  });
13161
14946
  for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
13162
- mkdirSync13(dirname9(unit3.path), { recursive: true, mode: 493 });
13163
- writeFileSync13(unit3.path, unit3.unit, { mode: 420 });
14947
+ mkdirSync14(dirname10(unit3.path), { recursive: true, mode: 493 });
14948
+ writeFileSync14(unit3.path, unit3.unit, { mode: 420 });
13164
14949
  }
13165
14950
  await applyPlan(plan, localRunner);
13166
14951
  return plan;
@@ -13194,8 +14979,8 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
13194
14979
  throw new Error("expected Agent version is invalid");
13195
14980
  if (runtime.uid() !== 0)
13196
14981
  throw new Error("platform-fleet-verify must run as root");
13197
- if (expectedVersion !== VERSION) {
13198
- throw new Error(`Agent version ${VERSION} does not match expected ${expectedVersion}`);
14982
+ if (expectedVersion !== VERSION2) {
14983
+ throw new Error(`Agent version ${VERSION2} does not match expected ${expectedVersion}`);
13199
14984
  }
13200
14985
  const status = await runtime.bootstrapStatus();
13201
14986
  if (!status.initialized || status.problems.length > 0) {
@@ -13208,16 +14993,17 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
13208
14993
  throw new Error("could not inspect failed systemd units");
13209
14994
  if (failed.output.trim())
13210
14995
  throw new Error(`failed systemd units are present: ${failed.output.trim()}`);
13211
- return { ok: true, agentVersion: VERSION, bootstrap: status, sevGuest: true, failedUnits: [] };
14996
+ return { ok: true, agentVersion: VERSION2, bootstrap: status, sevGuest: true, failedUnits: [] };
13212
14997
  }
13213
14998
 
13214
14999
  // src/community-rehearsal-host.ts
13215
- import { createHash as createHash8 } from "crypto";
13216
- import { lstatSync as lstatSync6, mkdirSync as mkdirSync14, readFileSync as readFileSync16, rmSync as rmSync10, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync14 } from "fs";
13217
- import { dirname as dirname10 } from "path";
15000
+ import { createHash as createHash10 } from "crypto";
15001
+ import { lstatSync as lstatSync6, mkdirSync as mkdirSync15, readFileSync as readFileSync17, rmSync as rmSync11, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync15 } from "fs";
15002
+ import { dirname as dirname11 } from "path";
13218
15003
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
13219
15004
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
13220
15005
  var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
15006
+ var COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
13221
15007
  var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
13222
15008
  var NODES = new Map([
13223
15009
  ["dev-fz-n1", { address: "10.42.0.21", agency: true }],
@@ -13227,11 +15013,11 @@ var NODES = new Map([
13227
15013
  ]);
13228
15014
  var RELEASE = /^[a-f0-9]{64}$/;
13229
15015
  async function ensureCommunityRehearsalArango(execute3 = executeSoftwareOperation) {
13230
- const requirement = { software: "arangodb", version: COMMUNITY_REHEARSAL_VERSION };
13231
- if ((await execute3({ kind: "check", ...requirement })).exitCode === 0)
15016
+ const requirement2 = { software: "arangodb", version: COMMUNITY_REHEARSAL_VERSION };
15017
+ if ((await execute3({ kind: "check", ...requirement2 })).exitCode === 0)
13232
15018
  return;
13233
- const installed = await execute3({ kind: "install", ...requirement });
13234
- if (installed.exitCode !== 0 || (await execute3({ kind: "check", ...requirement })).exitCode !== 0) {
15019
+ const installed = await execute3({ kind: "install", ...requirement2 });
15020
+ if (installed.exitCode !== 0 || (await execute3({ kind: "check", ...requirement2 })).exitCode !== 0) {
13235
15021
  throw new Error(`unable to install and verify ArangoDB ${COMMUNITY_REHEARSAL_VERSION}`);
13236
15022
  }
13237
15023
  }
@@ -13249,30 +15035,36 @@ var exactNode = (node) => {
13249
15035
  return { name: node, ...expected };
13250
15036
  };
13251
15037
  function parseCommunityRehearsalHostRequest(value) {
13252
- const row = exactObject(value);
13253
- const node = exactNode(row.node);
13254
- const action = row.action;
15038
+ const row2 = exactObject(value);
15039
+ const node = exactNode(row2.node);
15040
+ const action = row2.action;
13255
15041
  if (action === "prepare") {
13256
15042
  const allowed = ["action", "node", "address", "agency", "release", "archivePath", "archiveSha256", "jwt"];
13257
- if (Object.keys(row).some((key) => !allowed.includes(key)) || row.address !== node.address || row.agency !== node.agency || typeof row.release !== "string" || !RELEASE.test(row.release) || row.archiveSha256 !== row.release || row.archivePath !== `/tmp/forgezero-community-${row.release}.tar.gz` || typeof row.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row.jwt)) {
15043
+ if (Object.keys(row2).some((key) => !allowed.includes(key)) || row2.address !== node.address || row2.agency !== node.agency || typeof row2.release !== "string" || !RELEASE.test(row2.release) || row2.archiveSha256 !== row2.release || row2.archivePath !== `/tmp/forgezero-community-${row2.release}.tar.gz` || typeof row2.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row2.jwt)) {
13258
15044
  throw new Error("community rehearsal prepare request does not match the dedicated fleet contract");
13259
15045
  }
13260
- return row;
15046
+ return row2;
15047
+ }
15048
+ if (action === "cleanup") {
15049
+ if (Object.keys(row2).some((key) => !["action", "node"].includes(key))) {
15050
+ throw new Error("community rehearsal cleanup has unknown fields");
15051
+ }
15052
+ return { action, node: node.name };
13261
15053
  }
13262
15054
  if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
13263
- if (Object.keys(row).some((key) => !["action", "node"].includes(key)))
15055
+ if (Object.keys(row2).some((key) => !["action", "node"].includes(key)))
13264
15056
  throw new Error("community rehearsal host action has unknown fields");
13265
15057
  return { action, node: node.name };
13266
15058
  }
13267
15059
  if (action === "api") {
13268
- if (Object.keys(row).some((key) => !["action", "node", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row.operation)) || row.key !== undefined && (typeof row.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row.key)) || row.value !== undefined && (typeof row.value !== "string" || row.value.length > 4096)) {
15060
+ if (Object.keys(row2).some((key) => !["action", "node", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row2.operation)) || row2.key !== undefined && (typeof row2.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row2.key)) || row2.value !== undefined && (typeof row2.value !== "string" || row2.value.length > 4096)) {
13269
15061
  throw new Error("community rehearsal API request is invalid");
13270
15062
  }
13271
- if ((row.operation === "write" || row.operation === "read") && typeof row.key !== "string")
15063
+ if ((row2.operation === "write" || row2.operation === "read") && typeof row2.key !== "string")
13272
15064
  throw new Error("community rehearsal API key is required");
13273
- if ((row.operation === "write" || row.operation === "query") && typeof row.value !== "string")
15065
+ if ((row2.operation === "write" || row2.operation === "query") && typeof row2.value !== "string")
13274
15066
  throw new Error("community rehearsal API value is required");
13275
- return row;
15067
+ return row2;
13276
15068
  }
13277
15069
  throw new Error("unsupported community rehearsal host action");
13278
15070
  }
@@ -13374,6 +15166,19 @@ function planCommunityRehearsalPrepare(request) {
13374
15166
  { kind: "unlink", path: request.archivePath }
13375
15167
  ];
13376
15168
  }
15169
+ function planCommunityRehearsalCleanup() {
15170
+ return [
15171
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-community-api.service"], accepted: [0, 1, 5] },
15172
+ { kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] },
15173
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-community-api.service" },
15174
+ { kind: "unlink", path: "/etc/systemd/system/forgezero-rehearsal-db.service" },
15175
+ { kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
15176
+ { kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
15177
+ { kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
15178
+ { kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
15179
+ { kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
15180
+ ];
15181
+ }
13377
15182
  async function exec(argv2, stdin) {
13378
15183
  const child = Bun.spawn([...argv2], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
13379
15184
  if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
@@ -13390,10 +15195,10 @@ async function exec(argv2, stdin) {
13390
15195
  async function applyOperations(operations) {
13391
15196
  for (const operation of operations) {
13392
15197
  if (operation.kind === "remove-tree")
13393
- rmSync10(operation.path, { recursive: true, force: true });
15198
+ rmSync11(operation.path, { recursive: true, force: true });
13394
15199
  else if (operation.kind === "write") {
13395
- mkdirSync14(dirname10(operation.path), { recursive: true });
13396
- writeFileSync14(operation.path, operation.content, { mode: operation.mode });
15200
+ mkdirSync15(dirname11(operation.path), { recursive: true });
15201
+ writeFileSync15(operation.path, operation.content, { mode: operation.mode });
13397
15202
  } else if (operation.kind === "unlink") {
13398
15203
  try {
13399
15204
  unlinkSync12(operation.path);
@@ -13436,9 +15241,13 @@ async function runCommunityRehearsalHost(request) {
13436
15241
  if ((process.getuid?.() ?? -1) !== 0)
13437
15242
  throw new Error("community-rehearsal must run as root");
13438
15243
  const node = exactNode(request.node);
15244
+ if (request.action === "cleanup") {
15245
+ await applyOperations(planCommunityRehearsalCleanup());
15246
+ return { ok: true, action: request.action, node: node.name };
15247
+ }
13439
15248
  if (request.action === "prepare") {
13440
15249
  const metadata = lstatSync6(request.archivePath);
13441
- if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash8("sha256").update(readFileSync16(request.archivePath)).digest("hex") !== request.archiveSha256) {
15250
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash10("sha256").update(readFileSync17(request.archivePath)).digest("hex") !== request.archiveSha256) {
13442
15251
  throw new Error("community rehearsal archive is not the declared bounded release");
13443
15252
  }
13444
15253
  await ensureCommunityRehearsalArango();
@@ -13486,13 +15295,13 @@ async function runCommunityRehearsalHost(request) {
13486
15295
  }
13487
15296
 
13488
15297
  // src/supervised-app.ts
13489
- import { lstatSync as lstatSync7, readFileSync as readFileSync17 } from "fs";
15298
+ import { lstatSync as lstatSync7, readFileSync as readFileSync18 } from "fs";
13490
15299
  function readConfig(path2) {
13491
15300
  const stat = lstatSync7(path2);
13492
15301
  if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 18) !== 0 || stat.size > 128 * 1024) {
13493
15302
  throw new Error("supervised app config is unsafe");
13494
15303
  }
13495
- const value = JSON.parse(readFileSync17(path2, "utf8"));
15304
+ const value = JSON.parse(readFileSync18(path2, "utf8"));
13496
15305
  if (value.format !== 1 || typeof value.root !== "string" || !value.root.startsWith("/") || typeof value.release !== "string" || !value.release.startsWith(`${value.root}/releases/`) || typeof value.home !== "string" || value.home !== `${value.root}/app-home` || !Array.isArray(value.argv) || value.argv.length < 1 || value.argv.length > 256 || value.argv.some((argument) => typeof argument !== "string" || !argument || argument.includes("\x00")) || !value.environment || typeof value.environment !== "object" || Array.isArray(value.environment) || Object.entries(value.environment).some(([name, item]) => !/^FZ_(?:APP_PORT|RELEASE)$/.test(name) || typeof item !== "string" || /[\r\n\0]/.test(item))) {
13497
15306
  throw new Error("supervised app config is malformed");
13498
15307
  }
@@ -13538,16 +15347,16 @@ function agentCommandArgs(args, command2) {
13538
15347
  return args.slice(index + 1);
13539
15348
  }
13540
15349
  function loadOrCreateSeed(path2) {
13541
- if (existsSync18(path2)) {
13542
- const seed2 = new Uint8Array(Buffer.from(readFileSync18(path2, "utf8").trim(), "base64url"));
15350
+ if (existsSync20(path2)) {
15351
+ const seed2 = new Uint8Array(Buffer.from(readFileSync19(path2, "utf8").trim(), "base64url"));
13543
15352
  if (seed2.length < 32) {
13544
15353
  throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
13545
15354
  }
13546
15355
  return seed2;
13547
15356
  }
13548
- mkdirSync15(dirname11(path2), { recursive: true });
15357
+ mkdirSync16(dirname12(path2), { recursive: true });
13549
15358
  const seed = new Uint8Array(randomBytes6(32));
13550
- writeFileSync15(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
15359
+ writeFileSync16(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
13551
15360
  chmodSync17(path2, 384);
13552
15361
  return seed;
13553
15362
  }
@@ -13577,9 +15386,9 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
13577
15386
  if (!directory)
13578
15387
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
13579
15388
  const path2 = `${directory}/${name}`;
13580
- if (!existsSync18(path2))
15389
+ if (!existsSync20(path2))
13581
15390
  throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
13582
- const seed = new Uint8Array(Buffer.from(readFileSync18(path2, "utf8").trim(), "base64url"));
15391
+ const seed = new Uint8Array(Buffer.from(readFileSync19(path2, "utf8").trim(), "base64url"));
13583
15392
  if (seed.length < 32)
13584
15393
  throw new Error(`agent: the systemd credential ${name} is too short to derive a key from.`);
13585
15394
  return seed;
@@ -13589,7 +15398,7 @@ function loadTextCredential(name, directory = process.env.CREDENTIALS_DIRECTORY)
13589
15398
  throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the credential.");
13590
15399
  if (!/^[A-Za-z0-9_.-]+$/.test(name))
13591
15400
  throw new Error("agent: invalid systemd credential name.");
13592
- const value = readFileSync18(`${directory}/${name}`, "utf8").trim();
15401
+ const value = readFileSync19(`${directory}/${name}`, "utf8").trim();
13593
15402
  if (!value)
13594
15403
  throw new Error(`agent: systemd credential ${name} is empty.`);
13595
15404
  return value;
@@ -13669,7 +15478,7 @@ if (import.meta.main) {
13669
15478
  const args = process.argv.slice(2);
13670
15479
  if (args.includes("--help") || args.includes("-h")) {
13671
15480
  console.log([
13672
- `fz-agent ${VERSION}`,
15481
+ `fz-agent ${VERSION2}`,
13673
15482
  "",
13674
15483
  "Runs inside managed compute and answers secret requests over a local socket.",
13675
15484
  "It holds no configuration of its own \u2014 everything comes from the",
@@ -13702,7 +15511,7 @@ if (import.meta.main) {
13702
15511
  process.exit(0);
13703
15512
  }
13704
15513
  if (args.includes("--version") || args.includes("-v")) {
13705
- console.log(VERSION);
15514
+ console.log(VERSION2);
13706
15515
  process.exit(0);
13707
15516
  }
13708
15517
  const command2 = args.find((arg) => !arg.startsWith("-"));
@@ -13714,7 +15523,7 @@ if (import.meta.main) {
13714
15523
  if (configPath !== "/etc/forgezero/deploy-activation.json" || !release) {
13715
15524
  throw new Error("platform-activate requires the fixed config and one absolute release path");
13716
15525
  }
13717
- const config = JSON.parse(readFileSync18(configPath, "utf8"));
15526
+ const config = JSON.parse(readFileSync19(configPath, "utf8"));
13718
15527
  const result = await activatePlatformRelease(config, release);
13719
15528
  console.log(`promoted ${result.release} on ${result.slot}`);
13720
15529
  process.exit(0);
@@ -13745,7 +15554,7 @@ if (import.meta.main) {
13745
15554
  }
13746
15555
  }
13747
15556
  for (const relative of ["package.json", "bun.lock"]) {
13748
- const contents = readFileSync18(join12(process.cwd(), relative), "utf8");
15557
+ const contents = readFileSync19(join12(process.cwd(), relative), "utf8");
13749
15558
  if (/(?:workspace|file):/.test(contents)) {
13750
15559
  throw new Error(`project release ${relative} contains a local dependency`);
13751
15560
  }
@@ -13790,8 +15599,8 @@ if (import.meta.main) {
13790
15599
  keys: keys2,
13791
15600
  label: process.env.FZ_NODE_LABEL,
13792
15601
  edgeHostname: process.env.FZ_NODE_HOSTNAME,
13793
- gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync18(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
13794
- bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync18(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
15602
+ gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
15603
+ bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
13795
15604
  privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
13796
15605
  });
13797
15606
  console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
@@ -13833,7 +15642,7 @@ if (import.meta.main) {
13833
15642
  const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
13834
15643
  if (!profilePath)
13835
15644
  throw new Error("metal-helper requires --profile=/absolute/path.json");
13836
- const profile2 = JSON.parse(readFileSync18(profilePath, "utf8"));
15645
+ const profile2 = JSON.parse(readFileSync19(profilePath, "utf8"));
13837
15646
  const helper = startMetalHelper({
13838
15647
  profile: profile2,
13839
15648
  socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
@@ -13928,7 +15737,7 @@ if (import.meta.main) {
13928
15737
  if (stopping)
13929
15738
  return;
13930
15739
  stopping = true;
13931
- await new Promise((resolve5) => helper.close(() => resolve5()));
15740
+ await new Promise((resolve6) => helper.close(() => resolve6()));
13932
15741
  process.exit(0);
13933
15742
  };
13934
15743
  process.on("SIGTERM", () => void stop());
@@ -13950,7 +15759,7 @@ if (import.meta.main) {
13950
15759
  if (stopping)
13951
15760
  return;
13952
15761
  stopping = true;
13953
- await new Promise((resolve5) => helper.close(() => resolve5()));
15762
+ await new Promise((resolve6) => helper.close(() => resolve6()));
13954
15763
  process.exit(0);
13955
15764
  };
13956
15765
  process.on("SIGTERM", () => void stop());
@@ -13983,7 +15792,7 @@ if (import.meta.main) {
13983
15792
  const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
13984
15793
  if (!profilePath)
13985
15794
  throw new Error("metal-isolation requires --profile=/absolute/path.json");
13986
- const profile2 = JSON.parse(readFileSync18(profilePath, "utf8"));
15795
+ const profile2 = JSON.parse(readFileSync19(profilePath, "utf8"));
13987
15796
  await applyMetalIsolation(profile2);
13988
15797
  console.log("[metal-isolation] host and guest cgroup boundaries active");
13989
15798
  process.exit(0);
@@ -14028,7 +15837,7 @@ if (import.meta.main) {
14028
15837
  if (claimArg !== "-" && !claimArg.startsWith("/")) {
14029
15838
  throw new Error("metal-apply claim path must be absolute");
14030
15839
  }
14031
- const raw = readFileSync18(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
15840
+ const raw = readFileSync19(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
14032
15841
  if (Buffer.byteLength(raw) > 32 * 1024)
14033
15842
  throw new Error("metal-apply claim exceeds 32 KiB");
14034
15843
  const claim = JSON.parse(raw);
@@ -14053,7 +15862,7 @@ if (import.meta.main) {
14053
15862
  if (commandArgs.length !== 1 || commandArgs[0] !== "--request=-") {
14054
15863
  throw new Error("community-rehearsal requires exactly --request=-");
14055
15864
  }
14056
- const raw = readFileSync18("/dev/stdin", "utf8");
15865
+ const raw = readFileSync19("/dev/stdin", "utf8");
14057
15866
  if (Buffer.byteLength(raw) > 128 * 1024)
14058
15867
  throw new Error("community rehearsal request exceeds 128 KiB");
14059
15868
  const request = parseCommunityRehearsalHostRequest(JSON.parse(raw));
@@ -14064,7 +15873,7 @@ if (import.meta.main) {
14064
15873
  if (args.length !== 1 || args[0] !== "--request=-") {
14065
15874
  throw new Error("metal-admission-proof requires exactly --request=-");
14066
15875
  }
14067
- const raw = readFileSync18("/dev/stdin", "utf8");
15876
+ const raw = readFileSync19("/dev/stdin", "utf8");
14068
15877
  if (Buffer.byteLength(raw) > 32 * 1024)
14069
15878
  throw new Error("metal admission proof request exceeds 32 KiB");
14070
15879
  const request = JSON.parse(raw);
@@ -14074,7 +15883,7 @@ if (import.meta.main) {
14074
15883
  const seed = configuredAgentSeed({ credential: process.env.FZ_SEED_CREDENTIAL, allowFileSeed: false });
14075
15884
  const keys2 = deriveKeysFromSeed(seed);
14076
15885
  seed.fill(0);
14077
- const filesystem = statfsSync("/var/lib/forgezero");
15886
+ const filesystem = statfsSync2("/var/lib/forgezero");
14078
15887
  const body = {
14079
15888
  ...request,
14080
15889
  publicKeys: { ed25519: keys2.ed25519.publicKey, mlDsa: keys2.mlDsa.publicKey },
@@ -14082,9 +15891,9 @@ if (import.meta.main) {
14082
15891
  vcpu: cpus().length,
14083
15892
  memoryGib: Math.max(1, Math.floor(totalmem() / 1024 ** 3)),
14084
15893
  diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
14085
- kvm: existsSync18("/dev/kvm"),
14086
- snpHost: existsSync18("/dev/sev"),
14087
- helper: existsSync18(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
15894
+ kvm: existsSync20("/dev/kvm"),
15895
+ snpHost: existsSync20("/dev/sev"),
15896
+ helper: existsSync20(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
14088
15897
  }
14089
15898
  };
14090
15899
  const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
@@ -14119,9 +15928,9 @@ if (import.meta.main) {
14119
15928
  metalHostname: process.env.FZ_METAL_HOSTNAME,
14120
15929
  run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
14121
15930
  metalPreflight: () => ({
14122
- snpHost: existsSync18("/dev/sev"),
14123
- kvm: existsSync18("/dev/kvm"),
14124
- helper: existsSync18(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
15931
+ snpHost: existsSync20("/dev/sev"),
15932
+ kvm: existsSync20("/dev/kvm"),
15933
+ helper: existsSync20(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
14125
15934
  }),
14126
15935
  onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
14127
15936
  });
@@ -14204,7 +16013,7 @@ if (import.meta.main) {
14204
16013
  const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
14205
16014
  telemetry.event("agent.started");
14206
16015
  telemetry.setDraining(false);
14207
- const attestationSource = existsSync18("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
16016
+ const attestationSource = existsSync20("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
14208
16017
  const running = runAgent({
14209
16018
  socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
14210
16019
  seedCredential: process.env.FZ_SEED_CREDENTIAL,
@@ -14215,12 +16024,12 @@ if (import.meta.main) {
14215
16024
  record: (entry) => console.log(`[agent] ${entry.op} ${entry.outcome}${entry.detail ? ` ${entry.detail}` : ""}`)
14216
16025
  });
14217
16026
  const { nodeKey, keys, server } = running;
14218
- console.log(`[agent] ${VERSION} signing as ${nodeKey}`);
16027
+ console.log(`[agent] ${VERSION2} signing as ${nodeKey}`);
14219
16028
  const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
14220
16029
  let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
14221
16030
  const enrolmentCredential = process.env.FZ_ENROL_TOKEN_CREDENTIAL;
14222
- const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync18(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
14223
- const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync18(process.env.FZ_ENROL_TOKEN_FILE));
16031
+ const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync20(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
16032
+ const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync20(process.env.FZ_ENROL_TOKEN_FILE));
14224
16033
  if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
14225
16034
  binding = await enrolGuestIdentity({
14226
16035
  apiUrl: process.env.FZ_API,
@@ -14231,8 +16040,8 @@ if (import.meta.main) {
14231
16040
  keys,
14232
16041
  label: process.env.FZ_NODE_LABEL,
14233
16042
  edgeHostname: process.env.FZ_NODE_HOSTNAME,
14234
- gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync18(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
14235
- bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync18(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
16043
+ gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
16044
+ bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
14236
16045
  privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
14237
16046
  });
14238
16047
  console.log(`[agent] enrolled ${binding.computeReference} in project ${binding.projectKey}/${binding.environmentKey}`);
@@ -14298,7 +16107,7 @@ if (import.meta.main) {
14298
16107
  const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
14299
16108
  const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
14300
16109
  const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
14301
- if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync18(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
16110
+ if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync20(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
14302
16111
  throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
14303
16112
  }
14304
16113
  const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
@@ -14363,6 +16172,8 @@ if (import.meta.main) {
14363
16172
  capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ?? join12(root, "cache", "capacity"),
14364
16173
  ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
14365
16174
  activateService: (request) => requestServiceActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
16175
+ buildContainer: (request) => requestContainerBuild(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
16176
+ activateContainer: (request) => requestContainerActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
14366
16177
  projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
14367
16178
  });
14368
16179
  const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : undefined;
@@ -14454,7 +16265,7 @@ if (import.meta.main) {
14454
16265
  const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
14455
16266
  const deadline = Date.now() + deadlineMs;
14456
16267
  const remaining = () => Math.max(1, deadline - Date.now());
14457
- const controlClosed = control ? await settleWithin(new Promise((resolve5) => control.close(() => resolve5())), remaining()) : true;
16268
+ const controlClosed = control ? await settleWithin(new Promise((resolve6) => control.close(() => resolve6())), remaining()) : true;
14458
16269
  const pullDrain = pull?.stop() ?? Promise.resolve();
14459
16270
  const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
14460
16271
  const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
@@ -14639,7 +16450,7 @@ export {
14639
16450
  allocateAddress,
14640
16451
  agentCommandArgs,
14641
16452
  activateAgentRelease,
14642
- VERSION,
16453
+ VERSION2 as VERSION,
14643
16454
  SYSTEMD_RESOLVED_STUB,
14644
16455
  SYSTEMD_RESOLVED_ADDRESS,
14645
16456
  SUPPORTED_GUEST_IMAGE,