@forgezero/agent 0.1.74 → 0.1.75

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.
Files changed (42) hide show
  1. package/README.md +126 -17
  2. package/dist/agent-heartbeat.d.ts +25 -1
  3. package/dist/agent-heartbeat.js +45 -5
  4. package/dist/bootstrap.js +1 -1
  5. package/dist/community-rehearsal-host.js +208 -9
  6. package/dist/credential-schema.d.ts +1 -1
  7. package/dist/credential-schema.js +1 -1
  8. package/dist/definition.js +206 -7
  9. package/dist/deploy-actions.d.ts +7 -0
  10. package/dist/deploy-compiler.js +191 -10
  11. package/dist/deploy-file.js +206 -7
  12. package/dist/deploy-plan-runner.d.ts +3 -0
  13. package/dist/deploy-plan-runner.js +190 -9
  14. package/dist/deploy-plan.js +187 -8
  15. package/dist/deploy-providers.d.ts +19 -0
  16. package/dist/deploy.d.ts +89 -3
  17. package/dist/deploy.js +49 -2
  18. package/dist/deployment-connectivity.d.ts +63 -0
  19. package/dist/deployment-connectivity.js +148 -0
  20. package/dist/deployment-pull.d.ts +2 -0
  21. package/dist/deployment-targets.d.ts +9 -0
  22. package/dist/deployment-targets.js +141 -0
  23. package/dist/deployment.d.ts +69 -0
  24. package/dist/fz-agent.js +1111 -301
  25. package/dist/fz.js +220 -15
  26. package/dist/index.d.ts +1 -0
  27. package/dist/metal-bootstrap.js +1 -1
  28. package/dist/metal-helper-socket.js +210 -11
  29. package/dist/metal-provision.js +210 -11
  30. package/dist/operator-bootstrap.js +1 -1
  31. package/dist/platform-bootstrap-runtime.js +209 -10
  32. package/dist/platform-fleet-verification.js +515 -149
  33. package/dist/platform-genesis.js +206 -7
  34. package/dist/provision.js +465 -99
  35. package/dist/service-supervisor.d.ts +6 -0
  36. package/dist/software-helper.d.ts +3 -0
  37. package/dist/software-helper.js +465 -98
  38. package/dist/software.d.ts +4 -1
  39. package/dist/software.js +210 -8
  40. package/dist/ubuntu.js +206 -7
  41. package/dist/version.d.ts +1 -1
  42. package/package.json +12 -4
package/dist/fz.js CHANGED
@@ -4634,7 +4634,7 @@ var VaultError, runtimeEnvironment = () => typeof process !== "undefined" && pro
4634
4634
  } catch {
4635
4635
  return;
4636
4636
  }
4637
- }, VERSION = "0.1.14";
4637
+ }, VERSION = "0.1.15";
4638
4638
  var init_dist = __esm(() => {
4639
4639
  init_identity();
4640
4640
  VaultError = class VaultError extends Error {
@@ -4829,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4829
4829
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4830
4830
 
4831
4831
  // src/version.ts
4832
- var VERSION2 = "0.1.74";
4832
+ var VERSION2 = "0.1.75";
4833
4833
 
4834
4834
  // src/software.ts
4835
4835
  var PINNED_BUN_VERSION = "1.3.14";
@@ -4863,6 +4863,8 @@ var OS_CATALOG = [
4863
4863
  var SOFTWARE_CATALOG = [
4864
4864
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4865
4865
  { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4866
+ { id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4867
+ { id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4866
4868
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4867
4869
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4868
4870
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -4871,6 +4873,7 @@ var SOFTWARE_CATALOG = [
4871
4873
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4872
4874
  { id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4873
4875
  { id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4876
+ { id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4874
4877
  { id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4875
4878
  { id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4876
4879
  { id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -4895,7 +4898,7 @@ function validateSoftwareRequirements(value, _options = {}) {
4895
4898
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
4896
4899
  throw new Error("software requirement contains an unknown field");
4897
4900
  }
4898
- if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4901
+ if (!["bun", "docker", "containerd", "kata-containers", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4899
4902
  throw new Error("software requirement coordinate is invalid");
4900
4903
  }
4901
4904
  const requirement = { id: row.id, version: row.version };
@@ -10399,6 +10402,13 @@ function validateBuiltInAction(definition, context, where) {
10399
10402
  throw new Error(`${where}.with.requirements must name existing requirements`);
10400
10403
  return;
10401
10404
  }
10405
+ if (definition.uses === "forgezero.network/ensure@1") {
10406
+ exact(withValue, ["target"], `${where}.with`);
10407
+ if (typeof withValue.target !== "string" || !Object.values(context.components).some((entry) => entry.target === withValue.target)) {
10408
+ throw new Error(`${where}.with.target must name a target used by a component`);
10409
+ }
10410
+ return;
10411
+ }
10402
10412
  if (definition.uses === "forgezero.exec/argv@1") {
10403
10413
  const allowed = ["component", "argv", "credentials"];
10404
10414
  const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
@@ -10454,6 +10464,8 @@ function validateBuiltInProvider(requirement2, where) {
10454
10464
  const expected = {
10455
10465
  "forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
10456
10466
  "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
10467
+ "forgezero.containerd": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "runtimeClass", "namespace", "snapshotter"] },
10468
+ "forgezero.kata": { capability: "runtime.container", versions: ["4.0.0"], keys: ["installScope", "runtimeClass", "namespace", "hypervisor", "confidentialGuest"] },
10457
10469
  "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
10458
10470
  "forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
10459
10471
  "forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
@@ -10474,6 +10486,10 @@ function validateBuiltInProvider(requirement2, where) {
10474
10486
  if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
10475
10487
  throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
10476
10488
  }
10489
+ if (requirement2.provider === "forgezero.containerd" && (row2.runtimeClass !== "runc" || row2.namespace !== "forgezero" || row2.snapshotter !== "overlayfs"))
10490
+ throw new Error(`${where} containerd config is unsupported by the fixed host strategy`);
10491
+ if (requirement2.provider === "forgezero.kata" && (row2.runtimeClass !== "kata-qemu-snp" || row2.namespace !== "forgezero" || row2.hypervisor !== "qemu" || row2.confidentialGuest !== "sev-snp"))
10492
+ throw new Error(`${where} Kata config is unsupported by the fixed host strategy`);
10477
10493
  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)))
10478
10494
  throw new Error(`${where} Nginx config is invalid`);
10479
10495
  if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
@@ -10551,7 +10567,7 @@ function reference(value, where) {
10551
10567
  const row2 = object(value, where);
10552
10568
  exact3(row2, ["$ref"], where);
10553
10569
  const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
10554
- 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)) {
10570
+ 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}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB))$/.test(coordinate)) {
10555
10571
  fail2(where, "contains an unsupported reference.");
10556
10572
  }
10557
10573
  return { $ref: coordinate };
@@ -10791,10 +10807,9 @@ function validateNetwork(value, where) {
10791
10807
  named(container.network, `${where}.container.network`);
10792
10808
  }
10793
10809
  }
10794
- function validateComponent(value, where, targetNames, requirements) {
10810
+ function validateComponent(value, where, targets, requirements) {
10795
10811
  const row2 = object(value, where);
10796
- if (!targetNames.has(String(row2.target)))
10797
- fail2(`${where}.target`, "must name an existing target.");
10812
+ const selectedTarget = targets.get(String(row2.target)) ?? fail2(`${where}.target`, "must name an existing target.");
10798
10813
  if (row2.kind === "application") {
10799
10814
  exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
10800
10815
  const runtime = object(row2.runtime, `${where}.runtime`);
@@ -10805,7 +10820,9 @@ function validateComponent(value, where, targetNames, requirements) {
10805
10820
  exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
10806
10821
  validateArgv(runtime.argv, `${where}.runtime.argv`);
10807
10822
  } else if (runtime.kind === "container") {
10808
- exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
10823
+ exact3(runtime, ["kind", "provider", "requirement", "runtimeClass", "image", "entrypoint", "security"], `${where}.runtime`);
10824
+ if (runtime.runtimeClass !== undefined && !["runc", "kata-qemu-snp"].includes(String(runtime.runtimeClass)))
10825
+ fail2(`${where}.runtime.runtimeClass`, "is unsupported.");
10809
10826
  const image = object(runtime.image, `${where}.runtime.image`);
10810
10827
  exact3(image, ["source"], `${where}.runtime.image`);
10811
10828
  const source = object(image.source, `${where}.runtime.image.source`);
@@ -10839,6 +10856,23 @@ function validateComponent(value, where, targetNames, requirements) {
10839
10856
  fail2(`${where}.runtime.requirement`, "must provide runtime.container.");
10840
10857
  if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
10841
10858
  fail2(`${where}.runtime.requirement`, "must provide a runtime capability.");
10859
+ const execution = selectedTarget.allocation.execution;
10860
+ if (execution === "native" && runtime.kind !== "native")
10861
+ fail2(`${where}.runtime.kind`, "must be native for the selected target.");
10862
+ if (execution !== "native" && runtime.kind !== "container")
10863
+ fail2(`${where}.runtime.kind`, "must be container for the selected target.");
10864
+ if (runtime.kind === "container" && execution === "oci-runc") {
10865
+ if (!["forgezero.containerd", "forgezero.docker"].includes(String(runtime.provider)))
10866
+ fail2(`${where}.runtime.provider`, "must be forgezero.containerd for an OCI runc target.");
10867
+ if (runtime.runtimeClass !== undefined && runtime.runtimeClass !== "runc")
10868
+ fail2(`${where}.runtime.runtimeClass`, "must be runc for the selected target.");
10869
+ }
10870
+ if (runtime.kind === "container" && execution === "oci-kata-qemu-snp") {
10871
+ if (runtime.provider !== "forgezero.kata")
10872
+ fail2(`${where}.runtime.provider`, "must be forgezero.kata for a Kata QEMU SNP target.");
10873
+ if (runtime.runtimeClass !== "kata-qemu-snp")
10874
+ fail2(`${where}.runtime.runtimeClass`, "must be kata-qemu-snp for the selected target.");
10875
+ }
10842
10876
  const service = object(row2.service, `${where}.service`);
10843
10877
  exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
10844
10878
  if (service.protocol !== "http")
@@ -10870,6 +10904,18 @@ function validateComponent(value, where, targetNames, requirements) {
10870
10904
  const network = object(row2.network, `${where}.network`);
10871
10905
  if (network.container === undefined)
10872
10906
  fail2(`${where}.network.container`, "must select bridge or reviewed host networking.");
10907
+ if (selectedTarget.allocation.sharing === "shared") {
10908
+ if (object(runtime.security, `${where}.runtime.security`).root !== "read-only")
10909
+ fail2(`${where}.runtime.security.root`, "must be read-only on a shared compute.");
10910
+ if (network.container.mode !== "bridge")
10911
+ fail2(`${where}.network.container.mode`, "must be bridge on a shared compute.");
10912
+ if (Array.isArray(row2.storage) && row2.storage.some((mount) => mount.class !== "ephemeral")) {
10913
+ fail2(`${where}.storage`, "may contain only size-bounded tmpfs mounts on a shared compute.");
10914
+ }
10915
+ }
10916
+ }
10917
+ if (runtime.kind === "native" && selectedTarget.allocation.sharing !== "exclusive") {
10918
+ fail2(`${where}.target`, "native applications require an exclusive compute.");
10873
10919
  }
10874
10920
  if (row2.storage !== undefined) {
10875
10921
  if (!Array.isArray(row2.storage) || row2.storage.length > 32)
@@ -10906,6 +10952,8 @@ function validateComponent(value, where, targetNames, requirements) {
10906
10952
  fail2(`${where}.runtime.mode`, "must match the provider runtime mode.");
10907
10953
  if (runtime.mode !== "native")
10908
10954
  fail2(`${where}.runtime.mode`, "must be native in deployment plan v1.");
10955
+ if (selectedTarget.allocation.execution !== "native")
10956
+ fail2(`${where}.target`, "must select a native execution target for database components.");
10909
10957
  const topology = object(row2.topology, `${where}.topology`);
10910
10958
  if (topology.mode === "standalone")
10911
10959
  exact3(topology, ["mode"], `${where}.topology`);
@@ -11102,15 +11150,20 @@ function compileDeployment(sourceValue) {
11102
11150
  if (targetEntries.length < 1 || targetEntries.length > 64)
11103
11151
  fail2("deployment.spec.targets", "must contain 1 to 64 targets.");
11104
11152
  const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
11153
+ const targetDefinitions = new Map;
11105
11154
  for (const [targetName, value] of targetEntries) {
11106
11155
  const target = object(value, `deployment.spec.targets.${targetName}`);
11107
- exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
11156
+ exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
11108
11157
  if (target.kind !== "compute")
11109
11158
  fail2(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
11110
11159
  const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
11111
- exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
11160
+ exact3(selector, ["profiles", "operatingSystem", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
11112
11161
  if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME3.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
11113
11162
  fail2(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
11163
+ const operatingSystem = object(selector.operatingSystem, `deployment.spec.targets.${targetName}.selector.operatingSystem`);
11164
+ exact3(operatingSystem, ["id", "version", "architecture"], `deployment.spec.targets.${targetName}.selector.operatingSystem`);
11165
+ if (operatingSystem.id !== "ubuntu" || !["24.04", "26.04"].includes(String(operatingSystem.version)) || operatingSystem.architecture !== "x64")
11166
+ fail2(`deployment.spec.targets.${targetName}.selector.operatingSystem`, "is unsupported.");
11114
11167
  if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
11115
11168
  fail2(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
11116
11169
  if (selector.labels !== undefined)
@@ -11123,8 +11176,108 @@ function compileDeployment(sourceValue) {
11123
11176
  integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
11124
11177
  else
11125
11178
  reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
11179
+ const allocation = object(target.allocation, `deployment.spec.targets.${targetName}.allocation`);
11180
+ exact3(allocation, ["execution", "sharing", "resources", "existing"], `deployment.spec.targets.${targetName}.allocation`);
11181
+ if (!["native", "oci-runc", "oci-kata-qemu-snp"].includes(String(allocation.execution)))
11182
+ fail2(`deployment.spec.targets.${targetName}.allocation.execution`, "is unsupported.");
11183
+ if (!["exclusive", "shared"].includes(String(allocation.sharing)))
11184
+ fail2(`deployment.spec.targets.${targetName}.allocation.sharing`, "is unsupported.");
11185
+ if (!["prefer", "require"].includes(String(allocation.existing)))
11186
+ fail2(`deployment.spec.targets.${targetName}.allocation.existing`, "is unsupported.");
11187
+ const capacity = object(allocation.resources, `deployment.spec.targets.${targetName}.allocation.resources`);
11188
+ exact3(capacity, ["cpuCores", "memoryMiB", "storageGiB"], `deployment.spec.targets.${targetName}.allocation.resources`);
11189
+ const capacityValue = (value2, where, minimumValue, maximumValue) => {
11190
+ if (typeof value2 === "number")
11191
+ integer(value2, where, minimumValue, maximumValue);
11192
+ else
11193
+ reference(value2, where);
11194
+ };
11195
+ capacityValue(capacity.cpuCores, `deployment.spec.targets.${targetName}.allocation.resources.cpuCores`, 1, 1024);
11196
+ capacityValue(capacity.memoryMiB, `deployment.spec.targets.${targetName}.allocation.resources.memoryMiB`, 128, 4194304);
11197
+ capacityValue(capacity.storageGiB, `deployment.spec.targets.${targetName}.allocation.resources.storageGiB`, 1, 1048576);
11198
+ if (allocation.execution === "oci-kata-qemu-snp" && selector.confidentialCompute !== "required")
11199
+ fail2(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "must be required for Kata QEMU SNP.");
11200
+ if ((allocation.execution === "oci-kata-qemu-snp" || selector.confidentialCompute === "required") && operatingSystem.version !== "26.04")
11201
+ fail2(`deployment.spec.targets.${targetName}.selector.operatingSystem.version`, "must be Ubuntu 26.04 for SEV-SNP.");
11202
+ if (allocation.execution !== "oci-kata-qemu-snp" && selector.confidentialCompute === "required" && allocation.execution !== "native")
11203
+ fail2(`deployment.spec.targets.${targetName}.allocation.execution`, "cannot satisfy required confidential compute.");
11204
+ if (target.provisioning !== undefined) {
11205
+ const provisioning = object(target.provisioning, `deployment.spec.targets.${targetName}.provisioning`);
11206
+ exact3(provisioning, ["planKey", "regionKey", "imageKey", "environmentKey", "ownership", "metalHostname", "maxMonthlySpendMinor"], `deployment.spec.targets.${targetName}.provisioning`);
11207
+ for (const key of ["planKey", "regionKey", "imageKey", "environmentKey"])
11208
+ named(provisioning[key], `deployment.spec.targets.${targetName}.provisioning.${key}`);
11209
+ if (!["platform", "tenant-metal"].includes(String(provisioning.ownership)) || provisioning.ownership === "tenant-metal" !== Boolean(provisioning.metalHostname))
11210
+ fail2(`deployment.spec.targets.${targetName}.provisioning.ownership`, "must bind platform capacity or one tenant metal hostname.");
11211
+ if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
11212
+ fail2(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
11213
+ if (typeof provisioning.maxMonthlySpendMinor !== "string" || !/^\d{1,18}$/.test(provisioning.maxMonthlySpendMinor))
11214
+ fail2(`deployment.spec.targets.${targetName}.provisioning.maxMonthlySpendMinor`, "must be a bounded decimal minor-unit amount.");
11215
+ }
11216
+ if (allocation.existing === "prefer" && target.provisioning === undefined)
11217
+ fail2(`deployment.spec.targets.${targetName}.provisioning`, "is required when placement may provision after reuse.");
11218
+ if (allocation.existing === "require" && target.provisioning !== undefined)
11219
+ fail2(`deployment.spec.targets.${targetName}.provisioning`, "must be omitted for reuse-only placement.");
11220
+ if (target.connectivity !== undefined) {
11221
+ const connectivity = object(target.connectivity, `deployment.spec.targets.${targetName}.connectivity`);
11222
+ exact3(connectivity, ["private", "public"], `deployment.spec.targets.${targetName}.connectivity`);
11223
+ const declaredPrivate = connectivity.private === undefined ? undefined : object(connectivity.private, `deployment.spec.targets.${targetName}.connectivity.private`);
11224
+ if (declaredPrivate?.mode === "cloudflare-warp" && allocation.sharing !== "exclusive")
11225
+ fail2(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a Cloudflare WARP connector.");
11226
+ if (connectivity.private !== undefined) {
11227
+ const privateNetwork = declaredPrivate;
11228
+ if (privateNetwork.mode === "cloudflare-warp") {
11229
+ exact3(privateNetwork, ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.connectivity.private`);
11230
+ if (!credentials[named(privateNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.private.credential`)])
11231
+ fail2(`deployment.spec.targets.${targetName}.connectivity.private.credential`, "must name a declared credential.");
11232
+ if (typeof privateNetwork.network === "string")
11233
+ named(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
11234
+ else
11235
+ reference(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
11236
+ } else {
11237
+ exact3(privateNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.private`);
11238
+ if (!["disabled", "private-lan"].includes(String(privateNetwork.mode)))
11239
+ fail2(`deployment.spec.targets.${targetName}.connectivity.private.mode`, "is unsupported.");
11240
+ }
11241
+ }
11242
+ if (connectivity.public !== undefined) {
11243
+ const publicNetwork = object(connectivity.public, `deployment.spec.targets.${targetName}.connectivity.public`);
11244
+ if (publicNetwork.mode === "cloudflare-tunnel") {
11245
+ exact3(publicNetwork, ["mode", "credential", "zone", "hostname"], `deployment.spec.targets.${targetName}.connectivity.public`);
11246
+ if (!credentials[named(publicNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.public.credential`)])
11247
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public.credential`, "must name a declared credential.");
11248
+ if (typeof publicNetwork.zone === "string") {
11249
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(publicNetwork.zone))
11250
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public.zone`, "is invalid.");
11251
+ } else
11252
+ reference(publicNetwork.zone, `deployment.spec.targets.${targetName}.connectivity.public.zone`);
11253
+ const hostname = object(publicNetwork.hostname, `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
11254
+ const value2 = (coordinate, where, pattern) => {
11255
+ if (typeof coordinate === "string") {
11256
+ if (!pattern.test(coordinate))
11257
+ fail2(where, "is invalid.");
11258
+ } else
11259
+ reference(coordinate, where);
11260
+ };
11261
+ if (hostname.mode === "static") {
11262
+ exact3(hostname, ["mode", "label"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
11263
+ value2(hostname.label, `deployment.spec.targets.${targetName}.connectivity.public.hostname.label`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
11264
+ } else if (hostname.mode === "indexed") {
11265
+ exact3(hostname, ["mode", "prefix", "startAt"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
11266
+ value2(hostname.prefix, `deployment.spec.targets.${targetName}.connectivity.public.hostname.prefix`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
11267
+ if (hostname.startAt !== undefined)
11268
+ integer(hostname.startAt, `deployment.spec.targets.${targetName}.connectivity.public.hostname.startAt`, 0, 1e6);
11269
+ } else
11270
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public.hostname.mode`, "is unsupported.");
11271
+ } else {
11272
+ exact3(publicNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.public`);
11273
+ if (publicNetwork.mode !== "disabled")
11274
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public.mode`, "is unsupported.");
11275
+ }
11276
+ }
11277
+ }
11126
11278
  if (target.placement !== undefined)
11127
11279
  jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
11280
+ targetDefinitions.set(targetName, target);
11128
11281
  }
11129
11282
  const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
11130
11283
  if (Object.keys(requirements).length > 64)
@@ -11138,13 +11291,34 @@ function compileDeployment(sourceValue) {
11138
11291
  const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
11139
11292
  requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
11140
11293
  }
11294
+ for (const [targetName, target] of targetDefinitions) {
11295
+ if (target.connectivity?.public?.mode === "cloudflare-tunnel" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.public-tunnel")) {
11296
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public`, "requires one network.public-tunnel provider requirement.");
11297
+ }
11298
+ if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
11299
+ fail2(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
11300
+ }
11301
+ }
11141
11302
  const components = object(spec.components, "deployment.spec.components");
11142
11303
  const componentEntries = Object.entries(components);
11143
11304
  if (componentEntries.length < 1 || componentEntries.length > 128)
11144
11305
  fail2("deployment.spec.components", "must contain 1 to 128 components.");
11145
11306
  for (const [componentName, value] of componentEntries) {
11146
11307
  named(componentName, "deployment.spec.components key");
11147
- validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
11308
+ validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
11309
+ }
11310
+ for (const [targetName, target] of targetDefinitions) {
11311
+ if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
11312
+ continue;
11313
+ const publicApplications = componentEntries.filter(([, component2]) => {
11314
+ const candidate = component2;
11315
+ return candidate.kind === "application" && candidate.target === targetName && candidate.network?.public?.mode === "cloudflare-tunnel";
11316
+ });
11317
+ if (publicApplications.length !== 1)
11318
+ fail2(`deployment.spec.targets.${targetName}.connectivity.public`, "requires exactly one application on the target with network.public cloudflare-tunnel.");
11319
+ const application = publicApplications[0][1];
11320
+ if (application.kind !== "application" || !application.network?.ingress?.stablePort)
11321
+ fail2(`deployment.spec.components.${publicApplications[0][0]}.network.ingress`, "requires one stablePort for Tunnel ingress.");
11148
11322
  }
11149
11323
  const actionContext = {
11150
11324
  components,
@@ -11186,6 +11360,14 @@ function compileDeployment(sourceValue) {
11186
11360
  });
11187
11361
  plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
11188
11362
  }
11363
+ const deployWorkflow = plannedWorkflows.find(({ id: id2 }) => id2 === "deploy");
11364
+ for (const [targetName, target] of targetDefinitions) {
11365
+ if (!target.connectivity || target.connectivity.public?.mode !== "cloudflare-tunnel" && target.connectivity.private?.mode !== "cloudflare-warp")
11366
+ continue;
11367
+ const ensures = deployWorkflow?.stages.flatMap(({ steps }) => steps).filter((step3) => step3.uses === "forgezero.network/ensure@1" && step3.with.target === targetName && (step3.scope.kind === "each-target" || step3.scope.kind === "target-batches") && step3.scope.target === targetName) ?? [];
11368
+ if (ensures.length !== 1)
11369
+ fail2(`deployment.spec.workflows.deploy`, `must contain exactly one network.ensure step scoped to connected target ${targetName}.`);
11370
+ }
11189
11371
  const typedSource = source;
11190
11372
  return {
11191
11373
  format: DEPLOY_PLAN_FORMAT,
@@ -11294,8 +11476,10 @@ export default defineDeployment({
11294
11476
  inputs: { replicas: input.integer({ minimum: 1, maximum: 32, default: 1 }) },
11295
11477
  targets: {
11296
11478
  app: target.compute({
11297
- selector: { profiles: ['app'] },
11298
- cardinality: { minimum: 1, desired: input.ref('replicas'), maximum: 32 }
11479
+ profiles: ['app'],
11480
+ replicas: { minimum: 1, desired: input.ref('replicas'), maximum: 32 },
11481
+ resources: { cpuCores: 1, memoryMiB: 512, storageGiB: 8 },
11482
+ os: 'ubuntu-24.04', runtime: 'native', isolation: 'standard', reuse: 'require'
11299
11483
  })
11300
11484
  },
11301
11485
  requirements: { bun: providers.bun.require() },
@@ -19277,6 +19461,24 @@ async function cmdDeploy(options, args) {
19277
19461
  return 0;
19278
19462
  }
19279
19463
  const pipelineKey = required(options.pipelineKey, "--pipeline");
19464
+ if (operation === "plan") {
19465
+ const revision = required(options.revision, "--revision").toLowerCase();
19466
+ if (!/^[a-f0-9]{40}$/.test(revision))
19467
+ throw new Error("--revision must be a full 40-character Git commit.");
19468
+ const compiled = inspectCompiledDeployment(options.projectRoot);
19469
+ let inputs;
19470
+ if (options.data !== undefined) {
19471
+ const parsed = JSON.parse(options.data === "-" ? await Bun.stdin.text() : options.data);
19472
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
19473
+ throw new Error("--data must be one JSON object of typed deployment inputs.");
19474
+ inputs = parsed;
19475
+ }
19476
+ out.line(JSON.stringify(await deploymentRequest(options, "/plan", {
19477
+ method: "POST",
19478
+ body: { pipelineKey, revision, plan: compiled.plan, ...inputs ? { inputs } : {} }
19479
+ }), null, 2));
19480
+ return 0;
19481
+ }
19280
19482
  if (operation === "attach") {
19281
19483
  if (options.targets.length === 0)
19282
19484
  throw new Error("At least one --target <compute>@<profile> is required.");
@@ -19322,7 +19524,7 @@ async function cmdDeploy(options, args) {
19322
19524
  out.line(JSON.stringify(await deploymentRequest(options, "/rotate", { method: "POST", body: { pipelineKey } }), null, 2));
19323
19525
  return 0;
19324
19526
  }
19325
- out.fail("Usage: fz deploy init|compile|check|sync|catalog|list|connect|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
19527
+ out.fail("Usage: fz deploy init|compile|check|sync|catalog|list|connect|plan|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
19326
19528
  return 2;
19327
19529
  } catch (cause) {
19328
19530
  out.fail(cause instanceof Error ? cause.message : String(cause));
@@ -19399,7 +19601,10 @@ function usage() {
19399
19601
  fz deploy sync Prove readiness and print the Git synchronization rule
19400
19602
  fz deploy catalog List selectable tested OS/software coordinates
19401
19603
  fz deploy list List pipelines for .fz/config.json's project
19402
- fz deploy connect Create a repository pipeline and attach compute targets
19604
+ fz deploy connect Create a repository pipeline; optional --target values
19605
+ are manual anchors, not required for automatic placement
19606
+ fz deploy plan Approve the compiled plan at --revision and let the API
19607
+ reuse idle compute before bounded provisioning
19403
19608
  fz deploy attach Attach --target <compute>@<profile> to a pipeline
19404
19609
  fz deploy runs Show pipeline deployment runs
19405
19610
  fz deploy release Queue one exact --revision; --wait for completion
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { AGENT_CREDENTIAL_LOCATIONS, AGENT_CREDENTIAL_POLICY, METAL_SYSTEMD_CRED
13
13
  export type { AgentCredentialLocation, DeploymentCredentialBinding, DeploymentCredentialSchema } from './credential-schema';
14
14
  export { createDeploymentManager, DeploymentError } from './deployment';
15
15
  export type { DeploymentManager, DeploymentOptions, DeploymentRequest, DeploymentResult } from './deployment';
16
+ export { DeploymentInputError, resolveDeploymentInputs, resolveDeploymentTargets } from './deployment-targets';
16
17
  export { DEFAULT_CONTROL_SOCKET, requestControl, startControlServer } from './control';
17
18
  export { pullDeploymentOnce, startDeploymentPull } from './deployment-pull';
18
19
  export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from './deployment-pull';
@@ -354,7 +354,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
354
354
  }
355
355
 
356
356
  // src/version.ts
357
- var VERSION = "0.1.74";
357
+ var VERSION = "0.1.75";
358
358
 
359
359
  // src/otel-collector.ts
360
360
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";