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,575 @@
1
+ """Reviewed deployment input for the optional operational evidence verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from fdai_deployment_cli.contracts import canonical_digest
13
+ from fdai_deployment_cli.private_output import read_private_bytes
14
+ from fdai_deployment_cli.standalone_host_state import replace_or_verify_private_json
15
+
16
+ _SCHEMA = "fdai.operational-evidence-verifier-deployment-input.v1"
17
+ _STAGED_SCHEMA = "fdai.operational-evidence-verifier-staged-input.v1"
18
+ _MAX_INPUT_BYTES = 256 * 1024
19
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
20
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
21
+ _GUID_FRAGMENT = r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"
22
+ _RESOURCE_GROUP_NAME = r"[A-Za-z0-9_.()-]{1,90}"
23
+ _RESOURCE_SCOPE = re.compile(
24
+ rf"/subscriptions/{_GUID_FRAGMENT}/resourceGroups/{_RESOURCE_GROUP_NAME}"
25
+ r"(?:/providers/[A-Za-z0-9.]+/[A-Za-z0-9_.()/:-]+)?"
26
+ )
27
+ _FIXED_VERIFIER_ROLE = "fdai_operational_evidence_verifier"
28
+ _TOP_FIELDS = frozenset(
29
+ {
30
+ "schema_version",
31
+ "trust_registry_path",
32
+ "trust_registry_pin",
33
+ "grant_registry_path",
34
+ "grant_registry_pin",
35
+ "anchors",
36
+ "caller_token_issuer",
37
+ "caller_token_audience",
38
+ "caller_token_jwks",
39
+ "role_readback_scopes",
40
+ "allowed_role_scopes",
41
+ "vertical_executor_principal_ids",
42
+ "writer_members",
43
+ "dev_gateway_executor_principal_id",
44
+ "verifier_database_role",
45
+ }
46
+ )
47
+ _ANCHORS_FIELDS = frozenset({"schema_version", "venue", "anchors"})
48
+ _ANCHOR_FIELDS = frozenset({"anchor_id", "principal_id", "evidence_class"})
49
+ _OPTIONAL_TOP_FIELDS = frozenset(
50
+ {
51
+ "grant_registry_json",
52
+ "observation_metric_workspace_id",
53
+ "observation_metric_queries_json",
54
+ "observation_scope_rows_json",
55
+ "forecast_history_sources_json",
56
+ }
57
+ )
58
+ _BINDING_FIELDS = frozenset(
59
+ {
60
+ "enabled",
61
+ "trust_registry_path",
62
+ "trust_registry_pin",
63
+ "grant_registry_path",
64
+ "grant_registry_pin",
65
+ "anchors_json",
66
+ "caller_token_issuer",
67
+ "caller_token_audience",
68
+ "caller_token_jwks_json",
69
+ "role_readback_scopes_json",
70
+ "allowed_role_scopes_json",
71
+ "vertical_executor_principals_json",
72
+ "writer_members_json",
73
+ "dev_gateway_executor_principal_id",
74
+ }
75
+ )
76
+ _SECRET_JWK_FIELDS = frozenset({"d", "p", "q", "dp", "dq", "qi", "k"})
77
+ _ALLOWED_ROLE_NAMES = frozenset(
78
+ {"AcrPull", "Key Vault Secrets User", "Reader", "Monitoring Reader"}
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class OperationalEvidenceVerifierDeploymentInput:
84
+ """Validated authority-neutral binding for the read-only verifier workload."""
85
+
86
+ binding: dict[str, object]
87
+ input_digest: str
88
+
89
+
90
+ def load_operational_evidence_verifier_input(
91
+ path: Path,
92
+ ) -> OperationalEvidenceVerifierDeploymentInput:
93
+ """Load one private reviewed verifier input from the operator-selected path."""
94
+
95
+ raw = _load_json_object(
96
+ path,
97
+ "operational evidence verifier deployment input",
98
+ )
99
+ return _from_mapping(raw, staged=False)
100
+
101
+
102
+ def staged_operational_evidence_verifier_input_from_path(
103
+ path: Path | None,
104
+ ) -> OperationalEvidenceVerifierDeploymentInput | None:
105
+ """Load a staged managed-host input, or report that the verifier was not selected."""
106
+
107
+ if path is None:
108
+ return None
109
+ raw = _load_json_object(
110
+ path,
111
+ "staged operational evidence verifier deployment input",
112
+ )
113
+ return _from_mapping(raw, staged=True)
114
+
115
+
116
+ def stage_operational_evidence_verifier_input(
117
+ value: OperationalEvidenceVerifierDeploymentInput | None,
118
+ *,
119
+ tunnel: Any,
120
+ prepared_root: Path,
121
+ remote_root: str,
122
+ ) -> tuple[str, ...]:
123
+ """Copy the reviewed non-secret input to the managed host and return CLI arguments."""
124
+
125
+ if value is None:
126
+ return ()
127
+ staged: dict[str, object] = {
128
+ "schema_version": _STAGED_SCHEMA,
129
+ "input_digest": value.input_digest,
130
+ "binding": value.binding,
131
+ }
132
+ local_path = prepared_root / "operational-evidence-verifier-input.json"
133
+ remote_path = f"{remote_root}/operational-evidence-verifier-input.json"
134
+ replace_or_verify_private_json(local_path, staged)
135
+ tunnel.copy_to(local_path, remote_path, timeout=120)
136
+ return ("--operational-evidence-verifier-input", remote_path)
137
+
138
+
139
+ def apply_operational_evidence_verifier_input(
140
+ values: dict[str, object],
141
+ verifier_input: OperationalEvidenceVerifierDeploymentInput | None,
142
+ ) -> None:
143
+ """Populate Terraform and application values only when the input is selected."""
144
+
145
+ if verifier_input is None:
146
+ return
147
+ values["enable_operational_evidence_verifier"] = True
148
+ values["operational_evidence_verifier"] = dict(verifier_input.binding)
149
+
150
+
151
+ def reconcile_retained_operational_evidence_verifier_input(
152
+ verifier_input: OperationalEvidenceVerifierDeploymentInput | None,
153
+ *,
154
+ retained: dict[str, object],
155
+ values: dict[str, object],
156
+ ) -> bool:
157
+ """Bind a newly selected verifier input onto a retained host context or fail on drift."""
158
+
159
+ previous = retained.get("oev_input_digest", "")
160
+ if not isinstance(previous, str):
161
+ raise ValueError("standalone host retained context differs")
162
+ current = verifier_input.input_digest if verifier_input is not None else ""
163
+ if previous and previous != current:
164
+ raise ValueError("standalone host retained context differs")
165
+ if verifier_input is None:
166
+ return False
167
+ apply_operational_evidence_verifier_input(values, verifier_input)
168
+ if previous == current:
169
+ return False
170
+ retained["oev_input_digest"] = current
171
+ return True
172
+
173
+
174
+ def _from_mapping(
175
+ value: Mapping[str, Any], *, staged: bool
176
+ ) -> OperationalEvidenceVerifierDeploymentInput:
177
+ if staged:
178
+ if set(value) != {"schema_version", "input_digest", "binding"}:
179
+ raise ValueError("operational evidence verifier input fields are invalid")
180
+ if value.get("schema_version") != _STAGED_SCHEMA:
181
+ raise ValueError("operational evidence verifier input schema is unsupported")
182
+ binding = _staged_binding(
183
+ _mapping(value["binding"], "operational evidence verifier binding")
184
+ )
185
+ material = {
186
+ "schema_version": _SCHEMA,
187
+ **_input_material_from_binding(binding),
188
+ }
189
+ digest = canonical_digest(material)
190
+ if value.get("input_digest") != digest:
191
+ raise ValueError("operational evidence verifier input digest differs")
192
+ return OperationalEvidenceVerifierDeploymentInput(binding=binding, input_digest=digest)
193
+ if not _TOP_FIELDS.issubset(value) or not set(value).issubset(
194
+ _TOP_FIELDS | _OPTIONAL_TOP_FIELDS
195
+ ):
196
+ raise ValueError("operational evidence verifier input fields are invalid")
197
+ if value.get("schema_version") != _SCHEMA:
198
+ raise ValueError("operational evidence verifier input schema is unsupported")
199
+ _reject_secret_material(value)
200
+ binding = _binding(value)
201
+ material = {
202
+ "schema_version": _SCHEMA,
203
+ **_input_material_from_binding(binding),
204
+ }
205
+ return OperationalEvidenceVerifierDeploymentInput(
206
+ binding=binding,
207
+ input_digest=canonical_digest(material),
208
+ )
209
+
210
+
211
+ def _staged_binding(value: Mapping[str, Any]) -> dict[str, object]:
212
+ if (
213
+ not _BINDING_FIELDS.issubset(value)
214
+ or not set(value).issubset(_BINDING_FIELDS | _OPTIONAL_TOP_FIELDS)
215
+ or value.get("enabled") is not True
216
+ ):
217
+ raise ValueError("operational evidence verifier binding fields are invalid")
218
+ anchors = _anchors(_json_field(value, "anchors_json"))
219
+ caller_jwks = _caller_jwks(_json_field(value, "caller_token_jwks_json"))
220
+ readback_scopes = _scopes(
221
+ _json_field(value, "role_readback_scopes_json"), "role_readback_scopes"
222
+ )
223
+ allowed_role_scopes = _allowed_role_scopes(_json_field(value, "allowed_role_scopes_json"))
224
+ vertical_principals = _guid_list(
225
+ _json_field(value, "vertical_executor_principals_json"),
226
+ "vertical_executor_principal_ids",
227
+ )
228
+ writer_members = _writer_members(_json_field(value, "writer_members_json"))
229
+ result: dict[str, object] = {
230
+ "enabled": True,
231
+ "trust_registry_path": _path_text(value, "trust_registry_path"),
232
+ "trust_registry_pin": _digest(value, "trust_registry_pin"),
233
+ "grant_registry_path": _path_text(value, "grant_registry_path"),
234
+ "grant_registry_pin": _digest(value, "grant_registry_pin"),
235
+ "anchors_json": _json_text(anchors),
236
+ "caller_token_issuer": _https_url(value, "caller_token_issuer"),
237
+ "caller_token_audience": _audience(value, "caller_token_audience"),
238
+ "caller_token_jwks_json": _json_text(caller_jwks),
239
+ "role_readback_scopes_json": _json_text(readback_scopes),
240
+ "allowed_role_scopes_json": _json_text(allowed_role_scopes),
241
+ "vertical_executor_principals_json": _json_text(vertical_principals),
242
+ "writer_members_json": _json_text(writer_members),
243
+ "dev_gateway_executor_principal_id": _guid(value, "dev_gateway_executor_principal_id"),
244
+ }
245
+ result.update(_optional_binding_fields(value))
246
+ return result
247
+
248
+
249
+ def _binding(value: Mapping[str, Any]) -> dict[str, object]:
250
+ trust_pin = _digest(value, "trust_registry_pin")
251
+ grant_pin = _digest(value, "grant_registry_pin")
252
+ anchors = _anchors(value["anchors"])
253
+ caller_jwks = _caller_jwks(value["caller_token_jwks"])
254
+ readback_scopes = _scopes(value["role_readback_scopes"], "role_readback_scopes")
255
+ allowed_role_scopes = _allowed_role_scopes(value["allowed_role_scopes"])
256
+ vertical_principals = _guid_list(
257
+ value["vertical_executor_principal_ids"],
258
+ "vertical_executor_principal_ids",
259
+ )
260
+ writer_members = _writer_members(value["writer_members"])
261
+ verifier_role = _text(value, "verifier_database_role")
262
+ if verifier_role != _FIXED_VERIFIER_ROLE:
263
+ raise ValueError("operational evidence verifier database role is fixed")
264
+ result: dict[str, object] = {
265
+ "enabled": True,
266
+ "trust_registry_path": _path_text(value, "trust_registry_path"),
267
+ "trust_registry_pin": trust_pin,
268
+ "grant_registry_path": _path_text(value, "grant_registry_path"),
269
+ "grant_registry_pin": grant_pin,
270
+ "anchors_json": _json_text(anchors),
271
+ "caller_token_issuer": _https_url(value, "caller_token_issuer"),
272
+ "caller_token_audience": _audience(value, "caller_token_audience"),
273
+ "caller_token_jwks_json": _json_text(caller_jwks),
274
+ "role_readback_scopes_json": _json_text(readback_scopes),
275
+ "allowed_role_scopes_json": _json_text(allowed_role_scopes),
276
+ "vertical_executor_principals_json": _json_text(vertical_principals),
277
+ "writer_members_json": _json_text(writer_members),
278
+ "dev_gateway_executor_principal_id": _guid(value, "dev_gateway_executor_principal_id"),
279
+ }
280
+ result.update(_optional_binding_fields(value))
281
+ return result
282
+
283
+
284
+ def _optional_binding_fields(value: Mapping[str, Any]) -> dict[str, object]:
285
+ optional: dict[str, object] = {}
286
+ if "observation_metric_workspace_id" in value:
287
+ optional["observation_metric_workspace_id"] = _guid(
288
+ value, "observation_metric_workspace_id"
289
+ )
290
+ for key in (
291
+ "grant_registry_json",
292
+ "observation_metric_queries_json",
293
+ "observation_scope_rows_json",
294
+ "forecast_history_sources_json",
295
+ ):
296
+ if key in value:
297
+ optional[key] = _json_text(_json_field(value, key))
298
+ return optional
299
+
300
+
301
+ def _input_material_from_binding(binding: Mapping[str, object]) -> dict[str, object]:
302
+ material: dict[str, object] = {
303
+ "trust_registry_path": binding["trust_registry_path"],
304
+ "trust_registry_pin": binding["trust_registry_pin"],
305
+ "grant_registry_path": binding["grant_registry_path"],
306
+ "grant_registry_pin": binding["grant_registry_pin"],
307
+ "anchors": json.loads(str(binding["anchors_json"])),
308
+ "caller_token_issuer": binding["caller_token_issuer"],
309
+ "caller_token_audience": binding["caller_token_audience"],
310
+ "caller_token_jwks": json.loads(str(binding["caller_token_jwks_json"])),
311
+ "role_readback_scopes": json.loads(str(binding["role_readback_scopes_json"])),
312
+ "allowed_role_scopes": json.loads(str(binding["allowed_role_scopes_json"])),
313
+ "vertical_executor_principal_ids": json.loads(
314
+ str(binding["vertical_executor_principals_json"])
315
+ ),
316
+ "writer_members": json.loads(str(binding["writer_members_json"])),
317
+ "dev_gateway_executor_principal_id": binding["dev_gateway_executor_principal_id"],
318
+ "verifier_database_role": _FIXED_VERIFIER_ROLE,
319
+ }
320
+ for key in _OPTIONAL_TOP_FIELDS:
321
+ if key in binding:
322
+ material[key] = binding[key]
323
+ return material
324
+
325
+
326
+ def _anchors(value: object) -> dict[str, object]:
327
+ data = _mapping(value, "operational evidence anchors")
328
+ if set(data) != _ANCHORS_FIELDS or data.get("schema_version") != "1.0.0":
329
+ raise ValueError("operational evidence anchors fields are invalid")
330
+ if data.get("venue") != "deployed":
331
+ raise ValueError("operational evidence anchors must target deployed venue")
332
+ items = data.get("anchors")
333
+ if not isinstance(items, list) or not items:
334
+ raise ValueError("operational evidence anchors must be a non-empty list")
335
+ seen: set[str] = set()
336
+ normalized: list[dict[str, str]] = []
337
+ for item in items:
338
+ anchor = _mapping(item, "operational evidence anchor")
339
+ if set(anchor) != _ANCHOR_FIELDS:
340
+ raise ValueError("operational evidence anchor fields are invalid")
341
+ anchor_id = _bounded_text(anchor, "anchor_id")
342
+ principal = _principal_text(anchor.get("principal_id"), "principal_id")
343
+ evidence_class = _bounded_text(anchor, "evidence_class")
344
+ if not anchor_id.startswith("anchor:"):
345
+ raise ValueError("operational evidence anchor id is invalid")
346
+ if evidence_class != "live":
347
+ raise ValueError("operational evidence anchors must use live evidence")
348
+ if anchor_id in seen:
349
+ raise ValueError("operational evidence anchor is duplicated")
350
+ seen.add(anchor_id)
351
+ normalized.append(
352
+ {
353
+ "anchor_id": anchor_id,
354
+ "principal_id": principal,
355
+ "evidence_class": evidence_class,
356
+ }
357
+ )
358
+ return {"schema_version": "1.0.0", "venue": "deployed", "anchors": normalized}
359
+
360
+
361
+ def _caller_jwks(value: object) -> dict[str, object]:
362
+ jwks = _mapping(value, "caller token JWKS")
363
+ if set(jwks) != {"keys"}:
364
+ raise ValueError("caller token JWKS fields are invalid")
365
+ keys = jwks["keys"]
366
+ if not isinstance(keys, list) or not keys:
367
+ raise ValueError("caller token JWKS must contain at least one public key")
368
+ normalized: list[dict[str, object]] = []
369
+ for item in keys:
370
+ key = _mapping(item, "caller token JWK")
371
+ if _SECRET_JWK_FIELDS & set(key):
372
+ raise ValueError("caller token JWKS contains secret key material")
373
+ if key.get("kty") in {"oct", "dir"}:
374
+ raise ValueError("caller token JWKS contains secret key material")
375
+ if not isinstance(key.get("kty"), str) or not str(key["kty"]).strip():
376
+ raise ValueError("caller token JWK is malformed")
377
+ normalized.append({str(name): member for name, member in key.items()})
378
+ return {"keys": normalized}
379
+
380
+
381
+ def _allowed_role_scopes(value: object) -> dict[str, list[str]]:
382
+ data = _mapping(value, "allowed role scopes")
383
+ if not data:
384
+ raise ValueError("allowed role scopes are required")
385
+ normalized: dict[str, list[str]] = {}
386
+ for role, scopes in data.items():
387
+ if role not in _ALLOWED_ROLE_NAMES:
388
+ raise ValueError("allowed role scopes include an unsupported role")
389
+ normalized[role] = _scopes(scopes, f"allowed scopes for {role}")
390
+ return normalized
391
+
392
+
393
+ def _writer_members(value: object) -> list[str]:
394
+ if value != [_FIXED_VERIFIER_ROLE]:
395
+ raise ValueError(
396
+ "operational evidence writer members must contain only the verifier writer"
397
+ )
398
+ return [_FIXED_VERIFIER_ROLE]
399
+
400
+
401
+ def _guid_list(value: object, label: str) -> list[str]:
402
+ if not isinstance(value, list) or not value:
403
+ raise ValueError(f"{label} must be a non-empty list of GUIDs")
404
+ return [_principal_text(item, label) for item in value]
405
+
406
+
407
+ def _scopes(value: object, label: str) -> list[str]:
408
+ if not isinstance(value, list) or not value:
409
+ raise ValueError(f"{label} must be a non-empty list")
410
+ scopes: list[str] = []
411
+ for item in value:
412
+ if not isinstance(item, str) or not item.strip():
413
+ raise ValueError(f"{label} entries must be strings")
414
+ scope = item.strip().rstrip("/")
415
+ match = _RESOURCE_SCOPE.fullmatch(scope)
416
+ if (
417
+ match is None
418
+ or "/resourceGroups/" not in scope
419
+ or _resource_group_ends_with_period(scope)
420
+ ):
421
+ raise ValueError(f"{label} entries must be exact Azure resource scopes")
422
+ _reject_placeholder(scope, label)
423
+ scopes.append(scope)
424
+ if len(scopes) != len(set(scopes)):
425
+ raise ValueError(f"{label} entries must be unique")
426
+ return scopes
427
+
428
+
429
+ def _digest(value: Mapping[str, Any], field: str) -> str:
430
+ item = _text(value, field)
431
+ if _DIGEST.fullmatch(item) is None:
432
+ raise ValueError(f"{field} must be a lowercase SHA-256 digest")
433
+ _reject_placeholder(item, field)
434
+ return item
435
+
436
+
437
+ def _guid(value: Mapping[str, Any], field: str) -> str:
438
+ return _principal_text(value.get(field), field)
439
+
440
+
441
+ def _principal_text(value: object, field: str) -> str:
442
+ if not isinstance(value, str) or _GUID.fullmatch(value) is None:
443
+ raise ValueError(f"{field} must be a GUID")
444
+ _reject_placeholder(value, field)
445
+ return value
446
+
447
+
448
+ def _path_text(value: Mapping[str, Any], field: str) -> str:
449
+ item = _text(value, field)
450
+ if not item or "\x00" in item or ".." in Path(item).parts or item in {"/", ".", "config"}:
451
+ raise ValueError(f"{field} is invalid")
452
+ _reject_placeholder(item, field)
453
+ return item
454
+
455
+
456
+ def _https_url(value: Mapping[str, Any], field: str) -> str:
457
+ item = _text(value, field)
458
+ if not item.startswith("https://") or " " in item or item.endswith("example.com"):
459
+ raise ValueError(f"{field} must be an HTTPS URL")
460
+ _reject_placeholder(item, field)
461
+ return item
462
+
463
+
464
+ def _audience(value: Mapping[str, Any], field: str) -> str:
465
+ item = _text(value, field)
466
+ if not item.startswith("api://") or len(item) > 256:
467
+ raise ValueError(f"{field} is invalid")
468
+ _reject_placeholder(item, field)
469
+ return item
470
+
471
+
472
+ def _text(value: Mapping[str, Any], field: str) -> str:
473
+ item = value.get(field)
474
+ if not isinstance(item, str) or not item.strip():
475
+ raise ValueError(f"{field} is required")
476
+ return item.strip()
477
+
478
+
479
+ def _bounded_text(value: Mapping[str, Any], field: str) -> str:
480
+ item = _text(value, field)
481
+ if len(item) > 256:
482
+ raise ValueError(f"{field} is too long")
483
+ _reject_placeholder(item, field)
484
+ return item
485
+
486
+
487
+ def _mapping(value: object, label: str) -> Mapping[str, Any]:
488
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
489
+ raise ValueError(f"{label} is invalid")
490
+ return value
491
+
492
+
493
+ def _load_json_object(path: Path, label: str) -> dict[str, Any]:
494
+ data = read_private_bytes(path, max_bytes=_MAX_INPUT_BYTES)
495
+ if not data:
496
+ raise ValueError(f"{label} is empty")
497
+ try:
498
+ value = json.loads(
499
+ data.decode("utf-8"),
500
+ object_pairs_hook=_strict_json_object,
501
+ )
502
+ except UnicodeDecodeError as exc:
503
+ raise ValueError(f"{label} is not valid UTF-8") from exc
504
+ except json.JSONDecodeError as exc:
505
+ raise ValueError(f"{label} is not valid JSON") from exc
506
+ if not isinstance(value, dict):
507
+ raise ValueError( # noqa: TRY004 - normalize untrusted JSON into a stable CLI error
508
+ f"{label} MUST be a JSON object"
509
+ )
510
+ return value
511
+
512
+
513
+ def _strict_json_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
514
+ value: dict[str, object] = {}
515
+ for key, item in pairs:
516
+ if key in value:
517
+ raise ValueError(f"duplicate JSON key: {key}")
518
+ value[key] = item
519
+ return value
520
+
521
+
522
+ def _json_text(value: object) -> str:
523
+ return json.dumps(value, sort_keys=True, separators=(",", ":"))
524
+
525
+
526
+ def _json_field(value: Mapping[str, Any], field: str) -> object:
527
+ raw = _text(value, field)
528
+ try:
529
+ return json.loads(raw, object_pairs_hook=_strict_json_object)
530
+ except json.JSONDecodeError as exc:
531
+ raise ValueError(f"{field} must be valid JSON") from exc
532
+
533
+
534
+ def _resource_group_ends_with_period(scope: str) -> bool:
535
+ marker = "/resourceGroups/"
536
+ group = scope.split(marker, 1)[1].split("/", 1)[0]
537
+ return group.endswith(".")
538
+
539
+
540
+ def _reject_placeholder(value: str, field: str) -> None:
541
+ lowered = value.casefold()
542
+ if (
543
+ set(value) <= {"0", "-"}
544
+ or "replace" in lowered
545
+ or "placeholder" in lowered
546
+ or "set-me" in lowered
547
+ or "<" in value
548
+ or ">" in value
549
+ ):
550
+ raise ValueError(f"{field} contains a placeholder value")
551
+
552
+
553
+ def _reject_secret_material(value: object) -> None:
554
+ if isinstance(value, Mapping):
555
+ if any(key.casefold() in {"password", "client_secret", "private_key"} for key in value):
556
+ raise ValueError("operational evidence verifier input contains secret material")
557
+ for item in value.values():
558
+ _reject_secret_material(item)
559
+ elif isinstance(value, list):
560
+ for item in value:
561
+ _reject_secret_material(item)
562
+ elif isinstance(value, str):
563
+ lowered = value.casefold()
564
+ if "private key" in lowered or "client_secret" in lowered or "password=" in lowered:
565
+ raise ValueError("operational evidence verifier input contains secret material")
566
+
567
+
568
+ __all__ = [
569
+ "OperationalEvidenceVerifierDeploymentInput",
570
+ "apply_operational_evidence_verifier_input",
571
+ "load_operational_evidence_verifier_input",
572
+ "reconcile_retained_operational_evidence_verifier_input",
573
+ "stage_operational_evidence_verifier_input",
574
+ "staged_operational_evidence_verifier_input_from_path",
575
+ ]
@@ -0,0 +1,101 @@
1
+ """Private, non-secret Terraform plan input snapshot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import stat
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from fdai_deployment_cli.contracts import canonical_bytes, load_json_object
12
+ from fdai_deployment_cli.target import compute_target_binding
13
+
14
+ PLAN_ONLY_PASSWORD = "FDAI-PLAN-ONLY-NOT-A-SECRET"
15
+ _REQUIRED = frozenset(
16
+ {
17
+ "region",
18
+ "tenant_id",
19
+ "subscription_id",
20
+ "target_binding",
21
+ "postgres_admin_login",
22
+ "postgres_admin_password",
23
+ "core_image",
24
+ }
25
+ )
26
+ _KEY = re.compile(r"^[a-z][a-z0-9_]{0,127}$")
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class PlanInputContext:
31
+ """Validated provider context removed from Terraform variable content."""
32
+
33
+ subscription_id: str
34
+ tenant_id: str
35
+
36
+
37
+ def snapshot_plan_input(
38
+ source: Path,
39
+ destination: Path,
40
+ *,
41
+ expected_target_binding: str,
42
+ expected_region: str,
43
+ expected_environment: str,
44
+ ) -> PlanInputContext:
45
+ """Validate and copy a mode-0600 JSON plan input without real secret values."""
46
+
47
+ values = read_plan_input(source)
48
+ if set(values) != _REQUIRED:
49
+ raise ValueError("Terraform plan input fields do not match the secret-free schema")
50
+ if any(_KEY.fullmatch(key) is None for key in values):
51
+ raise ValueError("Terraform plan input contains an invalid key")
52
+ if values["postgres_admin_password"] != PLAN_ONLY_PASSWORD:
53
+ raise ValueError("Terraform plan input MUST use the plan-only password placeholder")
54
+ tenant_id = values["tenant_id"]
55
+ subscription_id = values["subscription_id"]
56
+ if not isinstance(tenant_id, str) or not isinstance(subscription_id, str):
57
+ raise ValueError("Terraform plan input Azure target values MUST be strings")
58
+ derived_binding = compute_target_binding(
59
+ tenant_id=tenant_id,
60
+ subscription_id=subscription_id,
61
+ )
62
+ if values["target_binding"] != derived_binding or derived_binding != expected_target_binding:
63
+ raise ValueError("Terraform plan input target binding does not match the profile")
64
+ if values["region"] != expected_region:
65
+ raise ValueError("Terraform plan input region does not match the profile")
66
+ if expected_environment not in {"dev", "staging", "prod"}:
67
+ raise ValueError("Terraform plan input environment is unsupported")
68
+ terraform_values = {
69
+ key: value
70
+ for key, value in values.items()
71
+ if key not in {"target_binding", "subscription_id"}
72
+ }
73
+ terraform_values["env"] = expected_environment
74
+ write_plan_input(destination, terraform_values)
75
+ return PlanInputContext(subscription_id=subscription_id, tenant_id=tenant_id)
76
+
77
+
78
+ def read_plan_input(source: Path) -> dict[str, object]:
79
+ """Read bounded private JSON without following links or blocking on a FIFO."""
80
+
81
+ descriptor = os.open(source, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
82
+ with os.fdopen(descriptor, "rb") as stream:
83
+ details = os.fstat(stream.fileno())
84
+ if not stat.S_ISREG(details.st_mode) or stat.S_IMODE(details.st_mode) != 0o600:
85
+ raise PermissionError("Terraform plan input MUST be a mode-0600 regular file")
86
+ payload = stream.read(1_048_577)
87
+ return load_json_object(payload, label="Terraform plan input")
88
+
89
+
90
+ def write_plan_input(destination: Path, values: dict[str, object]) -> None:
91
+ """Persist validated plan variables exclusively with private permissions."""
92
+
93
+ output = os.open(
94
+ destination,
95
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
96
+ 0o600,
97
+ )
98
+ with os.fdopen(output, "wb") as stream:
99
+ stream.write(canonical_bytes(values) + b"\n")
100
+ stream.flush()
101
+ os.fsync(stream.fileno())