fdai-deployment-cli 0.1.2__py3-none-any.whl

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 (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,722 @@
1
+ """Pure validators for Hub lifecycle configuration packages.
2
+
3
+ The functions in this module validate and resolve candidate configuration bytes before
4
+ signing or import. They do not read files, contact Azure, sign packages, decrypt sealed
5
+ values, grant authority, or establish deployability beyond the returned validation result.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import copy
11
+ import re
12
+ import unicodedata
13
+ from collections.abc import Container, Mapping
14
+ from collections.abc import Sequence as AbstractSequence
15
+ from dataclasses import dataclass
16
+ from typing import cast
17
+ from urllib.parse import urlsplit
18
+
19
+ _ANNOTATION_AXIS = "x-fdai-axis"
20
+ _ANNOTATION_OWNER = "x-fdai-owner"
21
+ _SCHEMA_METADATA = frozenset({_ANNOTATION_AXIS, _ANNOTATION_OWNER, "default"})
22
+ _AUTHORITY_KEYS = frozenset(
23
+ {
24
+ "action-lifecycle",
25
+ "approval-profile",
26
+ "approval-policy",
27
+ "authorization-policy",
28
+ "standing-authority",
29
+ "standing-authorizations",
30
+ "promotion-kind",
31
+ "promotion-state",
32
+ "executor-identity",
33
+ "human-identity",
34
+ "kill-switch",
35
+ }
36
+ )
37
+ _CONFIGURATION_AXES = frozenset(
38
+ {
39
+ "execution-venue",
40
+ "deployment-environment",
41
+ "product-surface-profile",
42
+ "evidence-profile",
43
+ "optional-package-preference",
44
+ "kinetic-evidence-availability",
45
+ "evidence-conflict-state",
46
+ "distribution",
47
+ "operational-safety-profile",
48
+ "installation-path",
49
+ "release-channel-subscription",
50
+ "model-diversity-policy",
51
+ }
52
+ )
53
+ _SEMVER = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
54
+ _RANGE_PART = re.compile(r"^(>=|>|<=|<|=)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
55
+ _ACRONYM_BOUNDARY = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])")
56
+ _CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
57
+ _DIGIT_BOUNDARY = re.compile(r"(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])")
58
+ _TOKEN_SPLIT = re.compile(r"[^A-Za-z0-9]+")
59
+ _KEY_VAULT_REF = re.compile(
60
+ r"^kv://[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])/[A-Za-z0-9-]{1,127}$"
61
+ )
62
+ _KEY_VAULT_SECRET_REF = re.compile(
63
+ r"^[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])/[A-Za-z0-9-]{1,127}$"
64
+ )
65
+ _KEY_VAULT_SECRET_NAME = re.compile(r"^[A-Za-z0-9-]{1,127}$")
66
+ _GUID = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
67
+ _AZURE_RESOURCE_ID = re.compile(
68
+ r"^/subscriptions/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
69
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/resourceGroups/[A-Za-z0-9._()-]{1,90}"
70
+ r"(?:/providers/[A-Za-z0-9.]+(?:/[A-Za-z0-9._()-]+/[A-Za-z0-9._()-]+)+)?$"
71
+ )
72
+ _KEY_VAULT_SECRET_ID = re.compile(
73
+ r"^https://[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])"
74
+ r"\.vault\.azure\.net/secrets/[A-Za-z0-9-]{1,127}(?:/[0-9a-f]{32})?$"
75
+ )
76
+ _ENUM_VALUE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
77
+ _LOWER_ENUM_VALUE = re.compile(r"^[a-z][a-z0-9-]{0,31}$")
78
+ _VERSION_VALUE = re.compile(r"^(?:[0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]+\.[0-9]+(?:\.[0-9]+)?)$")
79
+ _DIRECT_SECRET_TOKENS = frozenset(
80
+ {
81
+ "authorization",
82
+ "credential",
83
+ "credentials",
84
+ "pass",
85
+ "passphrase",
86
+ "passphrases",
87
+ "passwd",
88
+ "password",
89
+ "passwords",
90
+ "pwd",
91
+ "sas",
92
+ "secret",
93
+ "secrets",
94
+ "token",
95
+ "tokens",
96
+ }
97
+ )
98
+ _DIRECT_SECRET_SUBSTRINGS = (
99
+ "apikey",
100
+ "connectionstring",
101
+ "credential",
102
+ "passphrase",
103
+ "passwd",
104
+ "password",
105
+ "privatekey",
106
+ "secret",
107
+ "token",
108
+ )
109
+ _KEY_CONTEXT_TOKENS = frozenset(
110
+ {
111
+ "access",
112
+ "account",
113
+ "api",
114
+ "client",
115
+ "encryption",
116
+ "master",
117
+ "primary",
118
+ "private",
119
+ "secondary",
120
+ "shared",
121
+ "signing",
122
+ "storage",
123
+ "subscription",
124
+ }
125
+ )
126
+ _REFERENCE_OR_METADATA_FINAL_TOKENS = frozenset(
127
+ {"column", "endpoint", "id", "kind", "name", "ref", "type", "uri", "url", "version"}
128
+ )
129
+ _NUMERIC_COUNTER_TOKENS = frozenset({"count", "limit", "max", "min", "minute", "per", "second"})
130
+ _SENSITIVE_ENUM_VALUES = frozenset(
131
+ {
132
+ "api-key",
133
+ "basic",
134
+ "bearer",
135
+ "certificate",
136
+ "client-credentials",
137
+ "managed-identity",
138
+ "none",
139
+ "oauth2",
140
+ "service-principal",
141
+ "workload-identity",
142
+ }
143
+ )
144
+
145
+ Version = tuple[int, int, int]
146
+
147
+
148
+ class ConfigurationValidationError(ValueError):
149
+ """Configuration validation failure with a stable machine-readable code."""
150
+
151
+ code: str
152
+ path: tuple[str, ...]
153
+
154
+ def __init__(self, code: str, path: AbstractSequence[str], message: str) -> None:
155
+ self.code = code
156
+ self.path = tuple(path)
157
+ super().__init__(message)
158
+
159
+
160
+ @dataclass(frozen=True, slots=True)
161
+ class LayerResolution:
162
+ """Resolved configuration for one Release and Entity."""
163
+
164
+ values: dict[str, object]
165
+ version_range: str
166
+
167
+
168
+ @dataclass(frozen=True, slots=True)
169
+ class _Constraint:
170
+ operator: str
171
+ version: Version
172
+
173
+
174
+ @dataclass(frozen=True, slots=True)
175
+ class _VersionRange:
176
+ source: str
177
+ constraints: tuple[_Constraint, ...]
178
+ lower: Version | None
179
+ lower_inclusive: bool
180
+ upper: Version | None
181
+ upper_inclusive: bool
182
+
183
+ def contains(self, version: Version) -> bool:
184
+ for constraint in self.constraints:
185
+ if constraint.operator == ">=" and version < constraint.version:
186
+ return False
187
+ if constraint.operator == ">" and version <= constraint.version:
188
+ return False
189
+ if constraint.operator == "<=" and version > constraint.version:
190
+ return False
191
+ if constraint.operator == "<" and version >= constraint.version:
192
+ return False
193
+ if constraint.operator == "=" and version != constraint.version:
194
+ return False
195
+ return True
196
+
197
+ @property
198
+ def specificity(self) -> tuple[Version, int, int, Version, int, int]:
199
+ """Order overlapping ranges by the most constrained matching interval."""
200
+
201
+ lower = self.lower if self.lower is not None else (-1, -1, -1)
202
+ upper = self.upper if self.upper is not None else (0, 0, 0)
203
+ return (
204
+ lower,
205
+ 0 if self.lower_inclusive else 1,
206
+ 1 if self.upper is not None else 0,
207
+ (-upper[0], -upper[1], -upper[2]),
208
+ 1 if not self.upper_inclusive else 0,
209
+ len(self.constraints),
210
+ )
211
+
212
+
213
+ def validate_release_configuration_schema(configuration_schema: Mapping[str, object]) -> None:
214
+ """Validate Release configuration key annotations without granting authority."""
215
+
216
+ for key, raw_entry in configuration_schema.items():
217
+ _validate_printable_configuration_key(key, (key,))
218
+ entry = _mapping(raw_entry, ("configuration_schema", key))
219
+ axis = entry.get(_ANNOTATION_AXIS)
220
+ owner = entry.get(_ANNOTATION_OWNER)
221
+ if (
222
+ not isinstance(axis, str)
223
+ or not axis.strip()
224
+ or not isinstance(owner, str)
225
+ or not owner.strip()
226
+ ):
227
+ raise ConfigurationValidationError(
228
+ "unannotated_configuration_key",
229
+ (key,),
230
+ f"configuration key {key!r} MUST declare x-fdai-axis and x-fdai-owner",
231
+ )
232
+ if _normal_token(axis) not in _CONFIGURATION_AXES:
233
+ raise ConfigurationValidationError(
234
+ "unsupported_configuration_axis",
235
+ (key,),
236
+ f"configuration key {key!r} uses an unsupported or authority axis",
237
+ )
238
+ if owner != "customer":
239
+ raise ConfigurationValidationError(
240
+ "unsupported_configuration_owner",
241
+ (key,),
242
+ "lifecycle configuration keys belong to the customer",
243
+ )
244
+ if set(entry) - _SCHEMA_METADATA:
245
+ raise ConfigurationValidationError(
246
+ "unknown_configuration_metadata",
247
+ (key,),
248
+ "configuration schema metadata is unsupported",
249
+ )
250
+ _reject_authority_keys({key: entry.get("default")}, (key,))
251
+
252
+
253
+ def _reject_authority_keys(value: object, path: tuple[str, ...]) -> None:
254
+ if isinstance(value, Mapping):
255
+ for key, item in value.items():
256
+ if isinstance(key, str) and _normal_token(key) in _AUTHORITY_KEYS:
257
+ raise ConfigurationValidationError(
258
+ "authority_configuration_key", path, "authority state is not configuration"
259
+ )
260
+ _reject_authority_keys(item, (*path, str(key)))
261
+ elif isinstance(value, AbstractSequence) and not isinstance(value, str | bytes | bytearray):
262
+ for index, item in enumerate(value):
263
+ _reject_authority_keys(item, (*path, str(index)))
264
+
265
+
266
+ def resolve_configuration_layers(
267
+ *,
268
+ release_version: str,
269
+ configuration_schema: Mapping[str, object],
270
+ environment_config: Mapping[str, object],
271
+ entity_overrides: AbstractSequence[Mapping[str, object]],
272
+ sealed_keys: Container[str] = frozenset(),
273
+ ) -> LayerResolution:
274
+ """Resolve defaults, Environment Config, and the most-specific matching override block."""
275
+
276
+ validate_release_configuration_schema(configuration_schema)
277
+ _reject_authority_keys(environment_config, ("environment_config",))
278
+ version = _parse_version(release_version, ("release_version",))
279
+ values = _defaults(configuration_schema)
280
+ _merge_values(
281
+ values,
282
+ environment_config,
283
+ schema=configuration_schema,
284
+ sealed_keys=sealed_keys,
285
+ path=("environment_config",),
286
+ )
287
+ matching: list[tuple[_VersionRange, Mapping[str, object], int]] = []
288
+ ranges_seen: set[tuple[Version | None, bool, Version | None, bool]] = set()
289
+ for index, raw_block in enumerate(entity_overrides):
290
+ block_path = ("entity_overrides", str(index))
291
+ block = _mapping(raw_block, block_path)
292
+ source = _text(block.get("versions"), (*block_path, "versions"))
293
+ values_raw = _mapping(block.get("values"), (*block_path, "values"))
294
+ _reject_authority_keys(values_raw, (*block_path, "values"))
295
+ version_range = _parse_range(source, (*block_path, "versions"))
296
+ interval = (
297
+ version_range.lower,
298
+ version_range.lower_inclusive,
299
+ version_range.upper,
300
+ version_range.upper_inclusive,
301
+ )
302
+ if interval in ranges_seen:
303
+ raise ConfigurationValidationError(
304
+ "duplicate_override_range",
305
+ (*block_path, "versions"),
306
+ f"Entity override range {source!r} is duplicated",
307
+ )
308
+ ranges_seen.add(interval)
309
+ _validate_values(
310
+ values_raw,
311
+ schema=configuration_schema,
312
+ sealed_keys=sealed_keys,
313
+ path=(*block_path, "values"),
314
+ )
315
+ if version_range.contains(version):
316
+ matching.append((version_range, values_raw, index))
317
+ if not matching:
318
+ raise ConfigurationValidationError(
319
+ "missing_matching_override_block",
320
+ ("entity_overrides",),
321
+ f"Release {release_version!r} has no matching Entity override block",
322
+ )
323
+ selected_range, selected_values, _index = max(
324
+ matching, key=lambda item: (item[0].specificity, -item[2])
325
+ )
326
+ if any(not _contains_range(other, selected_range) for other, _values, _i in matching):
327
+ raise ConfigurationValidationError(
328
+ "ambiguous_override_range",
329
+ ("entity_overrides",),
330
+ "matching override ranges have no unique most-specific interval",
331
+ )
332
+ _validate_values(
333
+ values, schema=configuration_schema, sealed_keys=sealed_keys, path=("defaults",)
334
+ )
335
+ _merge_values(
336
+ values,
337
+ selected_values,
338
+ schema=configuration_schema,
339
+ sealed_keys=sealed_keys,
340
+ path=("entity_overrides", selected_range.source, "values"),
341
+ )
342
+ return LayerResolution(values=values, version_range=selected_range.source)
343
+
344
+
345
+ def _contains_range(outer: _VersionRange, inner: _VersionRange) -> bool:
346
+ if outer.lower is not None and (
347
+ inner.lower is None
348
+ or inner.lower < outer.lower
349
+ or (inner.lower == outer.lower and inner.lower_inclusive and not outer.lower_inclusive)
350
+ ):
351
+ return False
352
+ return outer.upper is None or (
353
+ inner.upper is not None
354
+ and (
355
+ inner.upper < outer.upper
356
+ or (inner.upper == outer.upper and (outer.upper_inclusive or not inner.upper_inclusive))
357
+ )
358
+ )
359
+
360
+
361
+ def validate_configuration_package_for_signing(package: Mapping[str, object]) -> None:
362
+ """Reject literal secret values before any package signing step can run.
363
+
364
+ Key Vault references are data-plane references and are allowed. Literal values under
365
+ secret-bearing fields fail closed so they cannot be signed into a package.
366
+ """
367
+
368
+ _scan_secret_values(package, ())
369
+
370
+
371
+ def _defaults(configuration_schema: Mapping[str, object]) -> dict[str, object]:
372
+ values: dict[str, object] = {}
373
+ for key, raw_entry in configuration_schema.items():
374
+ entry = _mapping(raw_entry, ("configuration_schema", key))
375
+ if "default" in entry:
376
+ values[key] = copy.deepcopy(entry["default"])
377
+ return values
378
+
379
+
380
+ def _merge_values(
381
+ target: dict[str, object],
382
+ update: Mapping[str, object],
383
+ *,
384
+ schema: Mapping[str, object],
385
+ sealed_keys: Container[str],
386
+ path: tuple[str, ...],
387
+ ) -> None:
388
+ for key, value in update.items():
389
+ key_path = (*path, key)
390
+ _validate_configuration_key(key, schema=schema, sealed_keys=sealed_keys, path=key_path)
391
+ target[key] = copy.deepcopy(value)
392
+
393
+
394
+ def _validate_values(
395
+ values: Mapping[str, object],
396
+ *,
397
+ schema: Mapping[str, object],
398
+ sealed_keys: Container[str],
399
+ path: tuple[str, ...],
400
+ ) -> None:
401
+ for key in values:
402
+ _validate_configuration_key(key, schema=schema, sealed_keys=sealed_keys, path=(*path, key))
403
+
404
+
405
+ def _validate_configuration_key(
406
+ key: str,
407
+ *,
408
+ schema: Mapping[str, object],
409
+ sealed_keys: Container[str],
410
+ path: tuple[str, ...],
411
+ ) -> None:
412
+ _validate_printable_configuration_key(key, path)
413
+ if key not in schema:
414
+ raise ConfigurationValidationError(
415
+ "unknown_configuration_key",
416
+ path,
417
+ f"configuration key {key!r} is not declared by the Release schema",
418
+ )
419
+ if key in sealed_keys:
420
+ raise ConfigurationValidationError(
421
+ "sealed_configuration_key",
422
+ path,
423
+ f"configuration key {key!r} belongs in the sealed section",
424
+ )
425
+
426
+
427
+ def _scan_secret_values(value: object, path: tuple[str, ...]) -> None:
428
+ if isinstance(value, Mapping):
429
+ _scan_name_value_secret(value, path)
430
+ for raw_key, raw_item in value.items():
431
+ key = str(raw_key)
432
+ item_path = (*path, key)
433
+ _validate_printable_configuration_key(key, item_path)
434
+ if _is_sensitive_key(key):
435
+ _validate_secret_reference(raw_item, item_path, key)
436
+ continue
437
+ _scan_secret_values(raw_item, item_path)
438
+ return
439
+ if isinstance(value, AbstractSequence) and not isinstance(value, str | bytes | bytearray):
440
+ for index, item in enumerate(value):
441
+ _scan_secret_values(item, (*path, str(index)))
442
+
443
+
444
+ def _scan_name_value_secret(value: Mapping[object, object], path: tuple[str, ...]) -> None:
445
+ name_keys = [*_find_casefold_keys(value, "name"), *_find_casefold_keys(value, "key")]
446
+ value_keys = _find_casefold_keys(value, "value")
447
+ if not name_keys or not value_keys:
448
+ return
449
+ for name_key in name_keys:
450
+ name = value[name_key]
451
+ if not isinstance(name, str) or not _is_sensitive_key(name):
452
+ continue
453
+ for value_key in value_keys:
454
+ _validate_secret_reference(value[value_key], (*path, str(value_key)), name)
455
+
456
+
457
+ def _validate_secret_reference(value: object, path: tuple[str, ...], key: str) -> None:
458
+ tokens = _tokens(key)
459
+ if isinstance(value, bool):
460
+ return
461
+ if isinstance(value, str) and _is_reference_or_metadata_key(tokens):
462
+ _validate_reference_or_metadata_value(tokens, value, path, key)
463
+ return
464
+ if _is_numeric_token_counter(tokens, value):
465
+ return
466
+ if _is_ref_key(tokens) and isinstance(value, str) and _KEY_VAULT_SECRET_NAME.fullmatch(value):
467
+ return
468
+ if _is_key_vault_reference(value):
469
+ return
470
+ _raise_literal_secret(path, key)
471
+
472
+
473
+ def _is_valid_ref_value(value: str) -> bool:
474
+ return _KEY_VAULT_SECRET_NAME.fullmatch(value) is not None or _is_key_vault_reference(value)
475
+
476
+
477
+ def _validate_reference_or_metadata_value(
478
+ tokens: tuple[str, ...], value: str, path: tuple[str, ...], key: str
479
+ ) -> None:
480
+ final = tokens[-1]
481
+ prefix_sensitive = _metadata_prefix_is_sensitive(tokens)
482
+ if final == "ref":
483
+ if _is_valid_ref_value(value):
484
+ return
485
+ _raise_literal_secret(path, key)
486
+ if final == "name":
487
+ # The lifecycle design uses Key Vault secret names as references for secret_name and
488
+ # key_vault_secret_name. Other sensitive *_name fields can hide real literal secrets.
489
+ if (
490
+ tokens[:-1]
491
+ and (not prefix_sensitive or _is_allowed_name_reference_key(tokens))
492
+ and _KEY_VAULT_SECRET_NAME.fullmatch(value) is not None
493
+ ):
494
+ return
495
+ _raise_literal_secret(path, key)
496
+ if final in {"url", "uri", "endpoint"}:
497
+ if _is_safe_https_url(value):
498
+ return
499
+ _raise_literal_secret(path, key)
500
+ if final == "id":
501
+ if _KEY_VAULT_SECRET_ID.fullmatch(value) is not None:
502
+ return
503
+ if not prefix_sensitive and (
504
+ _GUID.fullmatch(value) is not None or _AZURE_RESOURCE_ID.fullmatch(value) is not None
505
+ ):
506
+ return
507
+ _raise_literal_secret(path, key)
508
+ if final in {"kind", "type", "version"}:
509
+ if _is_valid_enum_metadata(tokens, value):
510
+ return
511
+ _raise_literal_secret(path, key)
512
+ if final == "column":
513
+ if not prefix_sensitive and _ENUM_VALUE.fullmatch(value) is not None:
514
+ return
515
+ _raise_literal_secret(path, key)
516
+ _raise_literal_secret(path, key)
517
+
518
+
519
+ def _is_allowed_name_reference_key(tokens: tuple[str, ...]) -> bool:
520
+ return tokens in {("secret", "name"), ("key", "vault", "secret", "name")}
521
+
522
+
523
+ def _metadata_prefix_is_sensitive(tokens: tuple[str, ...]) -> bool:
524
+ if len(tokens) <= 1:
525
+ return False
526
+ prefix = tokens[:-1]
527
+ return _is_sensitive_key("_".join(prefix)) or _is_sensitive_key("".join(prefix))
528
+
529
+
530
+ def _is_contextual_key_secret(tokens: tuple[str, ...]) -> bool:
531
+ token_set = set(tokens)
532
+ return bool(token_set & {"key", "keys"} and token_set & _KEY_CONTEXT_TOKENS)
533
+
534
+
535
+ def _is_valid_enum_metadata(tokens: tuple[str, ...], value: str) -> bool:
536
+ if _metadata_prefix_is_sensitive(tokens) or _is_contextual_key_secret(tokens):
537
+ if tokens[-1] == "version" and _VERSION_VALUE.fullmatch(value) is not None:
538
+ return True
539
+ normalized = value.casefold()
540
+ return (
541
+ normalized in _SENSITIVE_ENUM_VALUES
542
+ and _LOWER_ENUM_VALUE.fullmatch(normalized) is not None
543
+ )
544
+ if tokens[-1] == "version" and _VERSION_VALUE.fullmatch(value) is not None:
545
+ return True
546
+ return _ENUM_VALUE.fullmatch(value) is not None and not _looks_random_enum_value(value)
547
+
548
+
549
+ def _looks_random_enum_value(value: str) -> bool:
550
+ if len(value) < 16:
551
+ return False
552
+ digit_ratio = sum(char.isdigit() for char in value) / len(value)
553
+ if digit_ratio > 0.35:
554
+ return True
555
+ unique_ratio = len(set(value.casefold())) / len(value)
556
+ return unique_ratio > 0.70
557
+
558
+
559
+ def _is_safe_https_url(value: str) -> bool:
560
+ parsed = urlsplit(value)
561
+ if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
562
+ return False
563
+ return not (parsed.query or parsed.fragment)
564
+
565
+
566
+ def _is_key_vault_reference(value: object) -> bool:
567
+ if isinstance(value, str) and _KEY_VAULT_REF.fullmatch(value) is not None:
568
+ return True
569
+ return (
570
+ isinstance(value, Mapping)
571
+ and set(value) == {"key_vault_secret"}
572
+ and isinstance(value["key_vault_secret"], str)
573
+ and _KEY_VAULT_SECRET_REF.fullmatch(value["key_vault_secret"]) is not None
574
+ )
575
+
576
+
577
+ def _raise_literal_secret(path: tuple[str, ...], key: str) -> None:
578
+ raise ConfigurationValidationError(
579
+ "literal_secret_value",
580
+ path,
581
+ f"field {key!r} contains a literal secret value or malformed secret reference",
582
+ )
583
+
584
+
585
+ def _find_casefold_keys(value: Mapping[object, object], target: str) -> tuple[object, ...]:
586
+ matches: list[object] = []
587
+ for key in value:
588
+ if isinstance(key, str) and key.casefold() == target:
589
+ matches.append(key)
590
+ return tuple(matches)
591
+
592
+
593
+ def _mapping(value: object, path: tuple[str, ...]) -> Mapping[str, object]:
594
+ if not isinstance(value, Mapping):
595
+ raise ConfigurationValidationError(
596
+ "invalid_mapping",
597
+ path,
598
+ f"{'.'.join(path) or 'configuration'} MUST be a mapping",
599
+ )
600
+ return cast(Mapping[str, object], value)
601
+
602
+
603
+ def _text(value: object, path: tuple[str, ...]) -> str:
604
+ if not isinstance(value, str) or not value:
605
+ raise ConfigurationValidationError(
606
+ "invalid_string",
607
+ path,
608
+ f"{'.'.join(path)} MUST be a non-empty string",
609
+ )
610
+ return value
611
+
612
+
613
+ def _normal_token(value: str) -> str:
614
+ tokens = _tokens(value)
615
+ return "-".join(tokens)
616
+
617
+
618
+ def _is_sensitive_key(value: str) -> bool:
619
+ tokens_tuple = _tokens(value)
620
+ tokens = set(tokens_tuple)
621
+ if tokens & _DIRECT_SECRET_TOKENS:
622
+ return True
623
+ if any(marker in token for token in tokens_tuple for marker in _DIRECT_SECRET_SUBSTRINGS):
624
+ return True
625
+ if "connection" in tokens and "string" in tokens:
626
+ return True
627
+ return bool(tokens & {"key", "keys"} and tokens & _KEY_CONTEXT_TOKENS)
628
+
629
+
630
+ def _is_ref_key(tokens: tuple[str, ...]) -> bool:
631
+ return bool(tokens) and tokens[-1] == "ref"
632
+
633
+
634
+ def _is_reference_or_metadata_key(tokens: tuple[str, ...]) -> bool:
635
+ return bool(tokens) and tokens[-1] in _REFERENCE_OR_METADATA_FINAL_TOKENS
636
+
637
+
638
+ def _is_numeric_token_counter(tokens: tuple[str, ...], value: object) -> bool:
639
+ if isinstance(value, bool) or not isinstance(value, int | float):
640
+ return False
641
+ token_set = set(tokens)
642
+ return bool(token_set & {"token", "tokens"} and token_set & _NUMERIC_COUNTER_TOKENS)
643
+
644
+
645
+ def _tokens(value: str) -> tuple[str, ...]:
646
+ separated = _ACRONYM_BOUNDARY.sub("-", value)
647
+ separated = _CAMEL_BOUNDARY.sub("-", separated)
648
+ separated = _DIGIT_BOUNDARY.sub("-", separated)
649
+ return tuple(token.lower() for token in _TOKEN_SPLIT.split(separated) if token)
650
+
651
+
652
+ def _validate_printable_configuration_key(key: str, path: tuple[str, ...]) -> None:
653
+ normalized = unicodedata.normalize("NFKC", key)
654
+ if normalized != key or any(unicodedata.category(char) == "Cf" for char in key):
655
+ raise ConfigurationValidationError(
656
+ "invalid_configuration_key",
657
+ path,
658
+ "configuration keys MUST be printable ASCII",
659
+ )
660
+ if any(ord(char) < 0x20 or ord(char) > 0x7E for char in normalized):
661
+ raise ConfigurationValidationError(
662
+ "invalid_configuration_key",
663
+ path,
664
+ "configuration keys MUST be printable ASCII",
665
+ )
666
+
667
+
668
+ def _parse_version(value: str, path: tuple[str, ...]) -> Version:
669
+ match = _SEMVER.fullmatch(value)
670
+ if match is None:
671
+ raise ConfigurationValidationError(
672
+ "invalid_release_version",
673
+ path,
674
+ f"{'.'.join(path)} MUST be a semantic version",
675
+ )
676
+ return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
677
+
678
+
679
+ def _parse_range(value: str, path: tuple[str, ...]) -> _VersionRange:
680
+ constraints: list[_Constraint] = []
681
+ lower: Version | None = None
682
+ lower_inclusive = True
683
+ upper: Version | None = None
684
+ upper_inclusive = True
685
+ for part in value.split():
686
+ match = _RANGE_PART.fullmatch(part)
687
+ if match is None:
688
+ raise ConfigurationValidationError(
689
+ "invalid_version_range",
690
+ path,
691
+ f"version range {value!r} is invalid",
692
+ )
693
+ operator = match.group(1) or "="
694
+ version = (int(match.group(2)), int(match.group(3)), int(match.group(4)))
695
+ constraints.append(_Constraint(operator=operator, version=version))
696
+ if operator in {">=", ">"}:
697
+ if lower is None or version > lower or (version == lower and operator == ">"):
698
+ lower = version
699
+ lower_inclusive = operator == ">="
700
+ elif operator in {"<=", "<"}:
701
+ if upper is None or version < upper or (version == upper and operator == "<"):
702
+ upper = version
703
+ upper_inclusive = operator == "<="
704
+ else:
705
+ lower = version
706
+ upper = version
707
+ lower_inclusive = True
708
+ upper_inclusive = True
709
+ if not constraints:
710
+ raise ConfigurationValidationError(
711
+ "invalid_version_range",
712
+ path,
713
+ "version range MUST contain at least one constraint",
714
+ )
715
+ return _VersionRange(
716
+ source=value,
717
+ constraints=tuple(constraints),
718
+ lower=lower,
719
+ lower_inclusive=lower_inclusive,
720
+ upper=upper,
721
+ upper_inclusive=upper_inclusive,
722
+ )