@forgezero/agent 0.1.73 → 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 (44) hide show
  1. package/README.md +565 -61
  2. package/dist/agent-heartbeat.d.ts +25 -1
  3. package/dist/agent-heartbeat.js +45 -5
  4. package/dist/bootstrap.d.ts +2 -0
  5. package/dist/bootstrap.js +22 -4
  6. package/dist/community-rehearsal-host.js +208 -9
  7. package/dist/credential-schema.d.ts +1 -1
  8. package/dist/credential-schema.js +1 -1
  9. package/dist/definition.js +206 -7
  10. package/dist/deploy-actions.d.ts +7 -0
  11. package/dist/deploy-compiler.js +191 -10
  12. package/dist/deploy-file.js +206 -7
  13. package/dist/deploy-plan-runner.d.ts +3 -0
  14. package/dist/deploy-plan-runner.js +190 -9
  15. package/dist/deploy-plan.js +187 -8
  16. package/dist/deploy-providers.d.ts +19 -0
  17. package/dist/deploy.d.ts +89 -3
  18. package/dist/deploy.js +49 -2
  19. package/dist/deployment-connectivity.d.ts +63 -0
  20. package/dist/deployment-connectivity.js +148 -0
  21. package/dist/deployment-pull.d.ts +2 -0
  22. package/dist/deployment-targets.d.ts +9 -0
  23. package/dist/deployment-targets.js +141 -0
  24. package/dist/deployment.d.ts +69 -0
  25. package/dist/fz-agent.js +1112 -302
  26. package/dist/fz.js +246 -18
  27. package/dist/index.d.ts +1 -0
  28. package/dist/metal-bootstrap.js +1 -1
  29. package/dist/metal-helper-socket.js +210 -11
  30. package/dist/metal-provision.js +210 -11
  31. package/dist/operator-bootstrap.js +22 -4
  32. package/dist/platform-bootstrap-runtime.d.ts +1 -0
  33. package/dist/platform-bootstrap-runtime.js +216 -10
  34. package/dist/platform-fleet-verification.js +523 -150
  35. package/dist/platform-genesis.js +206 -7
  36. package/dist/provision.js +465 -99
  37. package/dist/service-supervisor.d.ts +6 -0
  38. package/dist/software-helper.d.ts +3 -0
  39. package/dist/software-helper.js +465 -98
  40. package/dist/software.d.ts +4 -1
  41. package/dist/software.js +210 -8
  42. package/dist/ubuntu.js +206 -7
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +12 -4
@@ -48,12 +48,15 @@ interface EvaluationState {
48
48
  inputs: Readonly<Record<string, JsonValue>>;
49
49
  steps: Map<string, StepExecutionRecord>;
50
50
  plan: DeploymentPlan;
51
+ execution?: Readonly<Record<string, JsonValue>>;
51
52
  }
52
53
  export declare function evaluateCondition(value: Condition, state: EvaluationState): boolean;
53
54
  export interface RunDeploymentPlanOptions {
54
55
  plan: DeploymentPlan | unknown;
55
56
  workflow: string;
56
57
  inputs?: Readonly<Record<string, JsonValue>>;
58
+ /** Secret-free exact placement context supplied by the authenticated control plane. */
59
+ execution?: Readonly<Record<string, JsonValue>>;
57
60
  actions: Readonly<Record<string, ActionHandler>>;
58
61
  now?: () => number;
59
62
  sleep?: (ms: number) => Promise<void>;
@@ -38,6 +38,13 @@ function validateBuiltInAction(definition, context, where) {
38
38
  throw new Error(`${where}.with.requirements must name existing requirements`);
39
39
  return;
40
40
  }
41
+ if (definition.uses === "forgezero.network/ensure@1") {
42
+ exact(withValue, ["target"], `${where}.with`);
43
+ if (typeof withValue.target !== "string" || !Object.values(context.components).some((entry) => entry.target === withValue.target)) {
44
+ throw new Error(`${where}.with.target must name a target used by a component`);
45
+ }
46
+ return;
47
+ }
41
48
  if (definition.uses === "forgezero.exec/argv@1") {
42
49
  const allowed = ["component", "argv", "credentials"];
43
50
  const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
@@ -93,6 +100,8 @@ function validateBuiltInProvider(requirement2, where) {
93
100
  const expected = {
94
101
  "forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
95
102
  "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
103
+ "forgezero.containerd": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "runtimeClass", "namespace", "snapshotter"] },
104
+ "forgezero.kata": { capability: "runtime.container", versions: ["4.0.0"], keys: ["installScope", "runtimeClass", "namespace", "hypervisor", "confidentialGuest"] },
96
105
  "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
97
106
  "forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
98
107
  "forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
@@ -113,6 +122,10 @@ function validateBuiltInProvider(requirement2, where) {
113
122
  if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
114
123
  throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
115
124
  }
125
+ if (requirement2.provider === "forgezero.containerd" && (row2.runtimeClass !== "runc" || row2.namespace !== "forgezero" || row2.snapshotter !== "overlayfs"))
126
+ throw new Error(`${where} containerd config is unsupported by the fixed host strategy`);
127
+ if (requirement2.provider === "forgezero.kata" && (row2.runtimeClass !== "kata-qemu-snp" || row2.namespace !== "forgezero" || row2.hypervisor !== "qemu" || row2.confidentialGuest !== "sev-snp"))
128
+ throw new Error(`${where} Kata config is unsupported by the fixed host strategy`);
116
129
  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)))
117
130
  throw new Error(`${where} Nginx config is invalid`);
118
131
  if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
@@ -190,7 +203,7 @@ function reference(value, where) {
190
203
  const row2 = object(value, where);
191
204
  exact3(row2, ["$ref"], where);
192
205
  const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
193
- 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)) {
206
+ 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)) {
194
207
  fail(where, "contains an unsupported reference.");
195
208
  }
196
209
  return { $ref: coordinate };
@@ -430,10 +443,9 @@ function validateNetwork(value, where) {
430
443
  named(container.network, `${where}.container.network`);
431
444
  }
432
445
  }
433
- function validateComponent(value, where, targetNames, requirements) {
446
+ function validateComponent(value, where, targets, requirements) {
434
447
  const row2 = object(value, where);
435
- if (!targetNames.has(String(row2.target)))
436
- fail(`${where}.target`, "must name an existing target.");
448
+ const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
437
449
  if (row2.kind === "application") {
438
450
  exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
439
451
  const runtime = object(row2.runtime, `${where}.runtime`);
@@ -444,7 +456,9 @@ function validateComponent(value, where, targetNames, requirements) {
444
456
  exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
445
457
  validateArgv(runtime.argv, `${where}.runtime.argv`);
446
458
  } else if (runtime.kind === "container") {
447
- exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
459
+ exact3(runtime, ["kind", "provider", "requirement", "runtimeClass", "image", "entrypoint", "security"], `${where}.runtime`);
460
+ if (runtime.runtimeClass !== undefined && !["runc", "kata-qemu-snp"].includes(String(runtime.runtimeClass)))
461
+ fail(`${where}.runtime.runtimeClass`, "is unsupported.");
448
462
  const image = object(runtime.image, `${where}.runtime.image`);
449
463
  exact3(image, ["source"], `${where}.runtime.image`);
450
464
  const source = object(image.source, `${where}.runtime.image.source`);
@@ -478,6 +492,23 @@ function validateComponent(value, where, targetNames, requirements) {
478
492
  fail(`${where}.runtime.requirement`, "must provide runtime.container.");
479
493
  if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
480
494
  fail(`${where}.runtime.requirement`, "must provide a runtime capability.");
495
+ const execution = selectedTarget.allocation.execution;
496
+ if (execution === "native" && runtime.kind !== "native")
497
+ fail(`${where}.runtime.kind`, "must be native for the selected target.");
498
+ if (execution !== "native" && runtime.kind !== "container")
499
+ fail(`${where}.runtime.kind`, "must be container for the selected target.");
500
+ if (runtime.kind === "container" && execution === "oci-runc") {
501
+ if (!["forgezero.containerd", "forgezero.docker"].includes(String(runtime.provider)))
502
+ fail(`${where}.runtime.provider`, "must be forgezero.containerd for an OCI runc target.");
503
+ if (runtime.runtimeClass !== undefined && runtime.runtimeClass !== "runc")
504
+ fail(`${where}.runtime.runtimeClass`, "must be runc for the selected target.");
505
+ }
506
+ if (runtime.kind === "container" && execution === "oci-kata-qemu-snp") {
507
+ if (runtime.provider !== "forgezero.kata")
508
+ fail(`${where}.runtime.provider`, "must be forgezero.kata for a Kata QEMU SNP target.");
509
+ if (runtime.runtimeClass !== "kata-qemu-snp")
510
+ fail(`${where}.runtime.runtimeClass`, "must be kata-qemu-snp for the selected target.");
511
+ }
481
512
  const service = object(row2.service, `${where}.service`);
482
513
  exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
483
514
  if (service.protocol !== "http")
@@ -509,6 +540,18 @@ function validateComponent(value, where, targetNames, requirements) {
509
540
  const network = object(row2.network, `${where}.network`);
510
541
  if (network.container === undefined)
511
542
  fail(`${where}.network.container`, "must select bridge or reviewed host networking.");
543
+ if (selectedTarget.allocation.sharing === "shared") {
544
+ if (object(runtime.security, `${where}.runtime.security`).root !== "read-only")
545
+ fail(`${where}.runtime.security.root`, "must be read-only on a shared compute.");
546
+ if (network.container.mode !== "bridge")
547
+ fail(`${where}.network.container.mode`, "must be bridge on a shared compute.");
548
+ if (Array.isArray(row2.storage) && row2.storage.some((mount) => mount.class !== "ephemeral")) {
549
+ fail(`${where}.storage`, "may contain only size-bounded tmpfs mounts on a shared compute.");
550
+ }
551
+ }
552
+ }
553
+ if (runtime.kind === "native" && selectedTarget.allocation.sharing !== "exclusive") {
554
+ fail(`${where}.target`, "native applications require an exclusive compute.");
512
555
  }
513
556
  if (row2.storage !== undefined) {
514
557
  if (!Array.isArray(row2.storage) || row2.storage.length > 32)
@@ -545,6 +588,8 @@ function validateComponent(value, where, targetNames, requirements) {
545
588
  fail(`${where}.runtime.mode`, "must match the provider runtime mode.");
546
589
  if (runtime.mode !== "native")
547
590
  fail(`${where}.runtime.mode`, "must be native in deployment plan v1.");
591
+ if (selectedTarget.allocation.execution !== "native")
592
+ fail(`${where}.target`, "must select a native execution target for database components.");
548
593
  const topology = object(row2.topology, `${where}.topology`);
549
594
  if (topology.mode === "standalone")
550
595
  exact3(topology, ["mode"], `${where}.topology`);
@@ -741,15 +786,20 @@ function compileDeployment(sourceValue) {
741
786
  if (targetEntries.length < 1 || targetEntries.length > 64)
742
787
  fail("deployment.spec.targets", "must contain 1 to 64 targets.");
743
788
  const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
789
+ const targetDefinitions = new Map;
744
790
  for (const [targetName, value] of targetEntries) {
745
791
  const target = object(value, `deployment.spec.targets.${targetName}`);
746
- exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
792
+ exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
747
793
  if (target.kind !== "compute")
748
794
  fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
749
795
  const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
750
- exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
796
+ exact3(selector, ["profiles", "operatingSystem", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
751
797
  if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
752
798
  fail(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
799
+ const operatingSystem = object(selector.operatingSystem, `deployment.spec.targets.${targetName}.selector.operatingSystem`);
800
+ exact3(operatingSystem, ["id", "version", "architecture"], `deployment.spec.targets.${targetName}.selector.operatingSystem`);
801
+ if (operatingSystem.id !== "ubuntu" || !["24.04", "26.04"].includes(String(operatingSystem.version)) || operatingSystem.architecture !== "x64")
802
+ fail(`deployment.spec.targets.${targetName}.selector.operatingSystem`, "is unsupported.");
753
803
  if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
754
804
  fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
755
805
  if (selector.labels !== undefined)
@@ -762,8 +812,108 @@ function compileDeployment(sourceValue) {
762
812
  integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
763
813
  else
764
814
  reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
815
+ const allocation = object(target.allocation, `deployment.spec.targets.${targetName}.allocation`);
816
+ exact3(allocation, ["execution", "sharing", "resources", "existing"], `deployment.spec.targets.${targetName}.allocation`);
817
+ if (!["native", "oci-runc", "oci-kata-qemu-snp"].includes(String(allocation.execution)))
818
+ fail(`deployment.spec.targets.${targetName}.allocation.execution`, "is unsupported.");
819
+ if (!["exclusive", "shared"].includes(String(allocation.sharing)))
820
+ fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "is unsupported.");
821
+ if (!["prefer", "require"].includes(String(allocation.existing)))
822
+ fail(`deployment.spec.targets.${targetName}.allocation.existing`, "is unsupported.");
823
+ const capacity = object(allocation.resources, `deployment.spec.targets.${targetName}.allocation.resources`);
824
+ exact3(capacity, ["cpuCores", "memoryMiB", "storageGiB"], `deployment.spec.targets.${targetName}.allocation.resources`);
825
+ const capacityValue = (value2, where, minimumValue, maximumValue) => {
826
+ if (typeof value2 === "number")
827
+ integer(value2, where, minimumValue, maximumValue);
828
+ else
829
+ reference(value2, where);
830
+ };
831
+ capacityValue(capacity.cpuCores, `deployment.spec.targets.${targetName}.allocation.resources.cpuCores`, 1, 1024);
832
+ capacityValue(capacity.memoryMiB, `deployment.spec.targets.${targetName}.allocation.resources.memoryMiB`, 128, 4194304);
833
+ capacityValue(capacity.storageGiB, `deployment.spec.targets.${targetName}.allocation.resources.storageGiB`, 1, 1048576);
834
+ if (allocation.execution === "oci-kata-qemu-snp" && selector.confidentialCompute !== "required")
835
+ fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "must be required for Kata QEMU SNP.");
836
+ if ((allocation.execution === "oci-kata-qemu-snp" || selector.confidentialCompute === "required") && operatingSystem.version !== "26.04")
837
+ fail(`deployment.spec.targets.${targetName}.selector.operatingSystem.version`, "must be Ubuntu 26.04 for SEV-SNP.");
838
+ if (allocation.execution !== "oci-kata-qemu-snp" && selector.confidentialCompute === "required" && allocation.execution !== "native")
839
+ fail(`deployment.spec.targets.${targetName}.allocation.execution`, "cannot satisfy required confidential compute.");
840
+ if (target.provisioning !== undefined) {
841
+ const provisioning = object(target.provisioning, `deployment.spec.targets.${targetName}.provisioning`);
842
+ exact3(provisioning, ["planKey", "regionKey", "imageKey", "environmentKey", "ownership", "metalHostname", "maxMonthlySpendMinor"], `deployment.spec.targets.${targetName}.provisioning`);
843
+ for (const key of ["planKey", "regionKey", "imageKey", "environmentKey"])
844
+ named(provisioning[key], `deployment.spec.targets.${targetName}.provisioning.${key}`);
845
+ if (!["platform", "tenant-metal"].includes(String(provisioning.ownership)) || provisioning.ownership === "tenant-metal" !== Boolean(provisioning.metalHostname))
846
+ fail(`deployment.spec.targets.${targetName}.provisioning.ownership`, "must bind platform capacity or one tenant metal hostname.");
847
+ if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
848
+ fail(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
849
+ if (typeof provisioning.maxMonthlySpendMinor !== "string" || !/^\d{1,18}$/.test(provisioning.maxMonthlySpendMinor))
850
+ fail(`deployment.spec.targets.${targetName}.provisioning.maxMonthlySpendMinor`, "must be a bounded decimal minor-unit amount.");
851
+ }
852
+ if (allocation.existing === "prefer" && target.provisioning === undefined)
853
+ fail(`deployment.spec.targets.${targetName}.provisioning`, "is required when placement may provision after reuse.");
854
+ if (allocation.existing === "require" && target.provisioning !== undefined)
855
+ fail(`deployment.spec.targets.${targetName}.provisioning`, "must be omitted for reuse-only placement.");
856
+ if (target.connectivity !== undefined) {
857
+ const connectivity = object(target.connectivity, `deployment.spec.targets.${targetName}.connectivity`);
858
+ exact3(connectivity, ["private", "public"], `deployment.spec.targets.${targetName}.connectivity`);
859
+ const declaredPrivate = connectivity.private === undefined ? undefined : object(connectivity.private, `deployment.spec.targets.${targetName}.connectivity.private`);
860
+ if (declaredPrivate?.mode === "cloudflare-warp" && allocation.sharing !== "exclusive")
861
+ fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a Cloudflare WARP connector.");
862
+ if (connectivity.private !== undefined) {
863
+ const privateNetwork = declaredPrivate;
864
+ if (privateNetwork.mode === "cloudflare-warp") {
865
+ exact3(privateNetwork, ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.connectivity.private`);
866
+ if (!credentials[named(privateNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.private.credential`)])
867
+ fail(`deployment.spec.targets.${targetName}.connectivity.private.credential`, "must name a declared credential.");
868
+ if (typeof privateNetwork.network === "string")
869
+ named(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
870
+ else
871
+ reference(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
872
+ } else {
873
+ exact3(privateNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.private`);
874
+ if (!["disabled", "private-lan"].includes(String(privateNetwork.mode)))
875
+ fail(`deployment.spec.targets.${targetName}.connectivity.private.mode`, "is unsupported.");
876
+ }
877
+ }
878
+ if (connectivity.public !== undefined) {
879
+ const publicNetwork = object(connectivity.public, `deployment.spec.targets.${targetName}.connectivity.public`);
880
+ if (publicNetwork.mode === "cloudflare-tunnel") {
881
+ exact3(publicNetwork, ["mode", "credential", "zone", "hostname"], `deployment.spec.targets.${targetName}.connectivity.public`);
882
+ if (!credentials[named(publicNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.public.credential`)])
883
+ fail(`deployment.spec.targets.${targetName}.connectivity.public.credential`, "must name a declared credential.");
884
+ if (typeof publicNetwork.zone === "string") {
885
+ 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))
886
+ fail(`deployment.spec.targets.${targetName}.connectivity.public.zone`, "is invalid.");
887
+ } else
888
+ reference(publicNetwork.zone, `deployment.spec.targets.${targetName}.connectivity.public.zone`);
889
+ const hostname = object(publicNetwork.hostname, `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
890
+ const value2 = (coordinate, where, pattern) => {
891
+ if (typeof coordinate === "string") {
892
+ if (!pattern.test(coordinate))
893
+ fail(where, "is invalid.");
894
+ } else
895
+ reference(coordinate, where);
896
+ };
897
+ if (hostname.mode === "static") {
898
+ exact3(hostname, ["mode", "label"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
899
+ value2(hostname.label, `deployment.spec.targets.${targetName}.connectivity.public.hostname.label`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
900
+ } else if (hostname.mode === "indexed") {
901
+ exact3(hostname, ["mode", "prefix", "startAt"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
902
+ value2(hostname.prefix, `deployment.spec.targets.${targetName}.connectivity.public.hostname.prefix`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
903
+ if (hostname.startAt !== undefined)
904
+ integer(hostname.startAt, `deployment.spec.targets.${targetName}.connectivity.public.hostname.startAt`, 0, 1e6);
905
+ } else
906
+ fail(`deployment.spec.targets.${targetName}.connectivity.public.hostname.mode`, "is unsupported.");
907
+ } else {
908
+ exact3(publicNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.public`);
909
+ if (publicNetwork.mode !== "disabled")
910
+ fail(`deployment.spec.targets.${targetName}.connectivity.public.mode`, "is unsupported.");
911
+ }
912
+ }
913
+ }
765
914
  if (target.placement !== undefined)
766
915
  jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
916
+ targetDefinitions.set(targetName, target);
767
917
  }
768
918
  const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
769
919
  if (Object.keys(requirements).length > 64)
@@ -777,13 +927,34 @@ function compileDeployment(sourceValue) {
777
927
  const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
778
928
  requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
779
929
  }
930
+ for (const [targetName, target] of targetDefinitions) {
931
+ if (target.connectivity?.public?.mode === "cloudflare-tunnel" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.public-tunnel")) {
932
+ fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires one network.public-tunnel provider requirement.");
933
+ }
934
+ if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
935
+ fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
936
+ }
937
+ }
780
938
  const components = object(spec.components, "deployment.spec.components");
781
939
  const componentEntries = Object.entries(components);
782
940
  if (componentEntries.length < 1 || componentEntries.length > 128)
783
941
  fail("deployment.spec.components", "must contain 1 to 128 components.");
784
942
  for (const [componentName, value] of componentEntries) {
785
943
  named(componentName, "deployment.spec.components key");
786
- validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
944
+ validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
945
+ }
946
+ for (const [targetName, target] of targetDefinitions) {
947
+ if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
948
+ continue;
949
+ const publicApplications = componentEntries.filter(([, component2]) => {
950
+ const candidate = component2;
951
+ return candidate.kind === "application" && candidate.target === targetName && candidate.network?.public?.mode === "cloudflare-tunnel";
952
+ });
953
+ if (publicApplications.length !== 1)
954
+ fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires exactly one application on the target with network.public cloudflare-tunnel.");
955
+ const application = publicApplications[0][1];
956
+ if (application.kind !== "application" || !application.network?.ingress?.stablePort)
957
+ fail(`deployment.spec.components.${publicApplications[0][0]}.network.ingress`, "requires one stablePort for Tunnel ingress.");
787
958
  }
788
959
  const actionContext = {
789
960
  components,
@@ -825,6 +996,14 @@ function compileDeployment(sourceValue) {
825
996
  });
826
997
  plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
827
998
  }
999
+ const deployWorkflow = plannedWorkflows.find(({ id }) => id === "deploy");
1000
+ for (const [targetName, target] of targetDefinitions) {
1001
+ if (!target.connectivity || target.connectivity.public?.mode !== "cloudflare-tunnel" && target.connectivity.private?.mode !== "cloudflare-warp")
1002
+ continue;
1003
+ const ensures = deployWorkflow?.stages.flatMap(({ steps }) => steps).filter((step2) => step2.uses === "forgezero.network/ensure@1" && step2.with.target === targetName && (step2.scope.kind === "each-target" || step2.scope.kind === "target-batches") && step2.scope.target === targetName) ?? [];
1004
+ if (ensures.length !== 1)
1005
+ fail(`deployment.spec.workflows.deploy`, `must contain exactly one network.ensure step scoped to connected target ${targetName}.`);
1006
+ }
828
1007
  const typedSource = source;
829
1008
  return {
830
1009
  format: DEPLOY_PLAN_FORMAT,
@@ -949,6 +1128,8 @@ function resolveReference(reference2, state) {
949
1128
  return nested(state.plan.spec.targets[parts[1]], parts.slice(2));
950
1129
  if (parts[0] === "deployment")
951
1130
  return nested(state.plan, parts.slice(1));
1131
+ if (parts[0] === "execution")
1132
+ return nested(state.execution, parts.slice(1));
952
1133
  return;
953
1134
  }
954
1135
  function resolveOperand(value, state) {
@@ -1151,7 +1332,7 @@ async function runDeploymentPlan(options) {
1151
1332
  for (const [name, definition] of Object.entries(inputDefinitions))
1152
1333
  inputValues[name] = validateInputValue(definition, supplied[name], name);
1153
1334
  const execute = async () => {
1154
- const state = { inputs: inputValues, steps: new Map, plan };
1335
+ const state = { inputs: inputValues, steps: new Map, plan, execution: options.execution };
1155
1336
  const records = [];
1156
1337
  const successful = [];
1157
1338
  for (const stage of workflow.stages) {