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,563 @@
1
+ """Pure value validation and projection for standalone deployment checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from typing import Any
9
+
10
+ from fdai_deployment_cli.aks_workload_jobs import AKS_CANARY_TOPIC
11
+ from fdai_deployment_cli.contracts import canonical_digest
12
+
13
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
14
+
15
+
16
+ def aks_core_environment(
17
+ *,
18
+ context: dict[str, object],
19
+ application_values: dict[str, object],
20
+ substrate_outputs: dict[str, object],
21
+ resource_group: str,
22
+ core_identity: dict[str, object],
23
+ topics: list[object],
24
+ deployment_binding: str,
25
+ ) -> dict[str, object]:
26
+ """Project the baseline AKS Core environment from verified deployment values."""
27
+
28
+ environment: dict[str, object] = {
29
+ "AZURE_TENANT_ID": context["tenant_id"],
30
+ "AZURE_SUBSCRIPTION_ID": context["subscription_id"],
31
+ "AZURE_RESOURCE_GROUP": resource_group,
32
+ "AZURE_REGION": application_values["region"],
33
+ "AZURE_CLIENT_ID": core_identity["client_id"],
34
+ "KAFKA_BOOTSTRAP_SERVERS": substrate_outputs["kafka"],
35
+ "KAFKA_TOPIC_EVENTS": str(topics[0]),
36
+ "FDAI_CANARY_TOPIC": AKS_CANARY_TOPIC,
37
+ "POSTGRES_HOST": substrate_outputs["postgres_fqdn"],
38
+ "POSTGRES_DATABASE": substrate_outputs["postgres_database"],
39
+ "RUNTIME_ENV": application_values["env"],
40
+ "AUTONOMY_MODE_DEFAULT": "shadow",
41
+ "FDAI_MONITOR_WORKSPACE_ID": substrate_outputs["workspace"],
42
+ "FDAI_OPERATING_MODEL_TOPIC": substrate_outputs["operating_model_topic"],
43
+ "FDAI_AUXILIARY_KAFKA_BOOTSTRAP_SERVERS": substrate_outputs["operational_kafka"],
44
+ "FDAI_ISOLATED_EXECUTOR_AUTHORITY_CUTOVER": "1",
45
+ "FDAI_PRODUCT_PROFILE_JSON": str(application_values["product_profile_json"]),
46
+ "FDAI_START_CONSUMER": "1",
47
+ "FDAI_INSTALLATION_BINDING": str(substrate_outputs["installation_binding"]),
48
+ "FDAI_LICENSE_DEPLOYMENT_BINDING": deployment_binding,
49
+ }
50
+ profile = str(application_values.get("development_authority_profile_json", "")).strip()
51
+ executor = str(application_values.get("development_authority_executor_principal", "")).strip()
52
+ if bool(profile) != bool(executor):
53
+ raise ValueError("development authority profile and executor must be selected together")
54
+ if profile:
55
+ environment["FDAI_FULL_AUTHORITY_DEVELOPMENT_PROFILE_JSON"] = profile
56
+ environment["FDAI_FULL_AUTHORITY_DEVELOPMENT_EXECUTOR_PRINCIPAL"] = executor
57
+ if stewardship_governance_selected(application_values):
58
+ environment["FDAI_STEWARDSHIP_GOVERNANCE_ENABLED"] = "true"
59
+ environment.update(_stewardship_bindings_environment(application_values))
60
+ return environment
61
+
62
+
63
+ def stewardship_governance_selected(application_values: dict[str, object]) -> bool:
64
+ """Report whether the selected product surfaces already carry the handover lifecycle.
65
+
66
+ The Console, the Operator API with enterprise identity bindings, and document ingestion are each
67
+ explicit selections. Together they produce handover drafts and name the accountable stewards, so
68
+ Core then runs the Console-governed candidate worker. The worker stays inert: an Owner activates
69
+ a candidate in the Console, and no GitHub credential is involved.
70
+ """
71
+
72
+ return (
73
+ application_values.get("enable_operator_api") is True
74
+ and application_values.get("enable_document_ingestion") is True
75
+ and bool(str(application_values.get("stewardship_maintainers", "")).strip())
76
+ )
77
+
78
+
79
+ def _stewardship_bindings_environment(application_values: dict[str, object]) -> dict[str, object]:
80
+ bindings = application_values["stewardship_agent_bindings"]
81
+ if not isinstance(bindings, dict):
82
+ raise ValueError("stewardship bindings are invalid") # noqa: TRY004
83
+ environment: dict[str, object] = {
84
+ "FDAI_MAINTAINERS": application_values["stewardship_maintainers"]
85
+ }
86
+ environment.update(
87
+ {f"FDAI_STEWARD_{name.upper()}": binding for name, binding in bindings.items()}
88
+ )
89
+ return environment
90
+
91
+
92
+ def plan_summary(value: object) -> dict[str, object]:
93
+ """Project one bounded Terraform plan into review-safe action counts and addresses."""
94
+ changes = value.get("resource_changes") if isinstance(value, dict) else None
95
+ if not isinstance(changes, list) or len(changes) > 5000:
96
+ raise ValueError("Terraform plan resource change inventory is invalid")
97
+ counts = {name: 0 for name in ("create", "update", "delete", "replace", "read", "no-op")}
98
+ types: dict[str, int] = {}
99
+ projected: list[dict[str, object]] = []
100
+ for item in changes:
101
+ if (
102
+ not isinstance(item, dict)
103
+ or not isinstance(item.get("type"), str)
104
+ or not isinstance(item.get("address"), str)
105
+ or not item["address"]
106
+ ):
107
+ raise ValueError("Terraform plan resource change is invalid")
108
+ change = item.get("change")
109
+ actions = change.get("actions") if isinstance(change, dict) else None
110
+ if not isinstance(actions, list) or not all(isinstance(action, str) for action in actions):
111
+ raise ValueError("Terraform plan action is invalid")
112
+ action_set = set(actions)
113
+ if action_set == {"create", "delete"}:
114
+ counts["replace"] += 1
115
+ else:
116
+ for action in action_set:
117
+ if action in counts:
118
+ counts[action] += 1
119
+ resource_type = str(item["type"])
120
+ types[resource_type] = types.get(resource_type, 0) + 1
121
+ projected.append({"address": item["address"], "actions": actions})
122
+ return {
123
+ "action_counts": counts,
124
+ "resource_type_counts": dict(sorted(types.items())),
125
+ "resource_changes": projected,
126
+ }
127
+
128
+
129
+ def required_image_digest(records: dict[str, object], name: str) -> str:
130
+ """Return one exact OCI image digest from a validated runtime record."""
131
+ record = records.get(name)
132
+ if not isinstance(record, dict) or any(not isinstance(key, str) for key in record):
133
+ raise ValueError(f"runtime image {name} is invalid")
134
+ digest = record.get("image_digest")
135
+ if not isinstance(digest, str) or re.fullmatch(r"sha256:[0-9a-f]{64}", digest) is None:
136
+ raise ValueError("runtime image digest is invalid")
137
+ return digest
138
+
139
+
140
+ def foundation_binding_digest(
141
+ handoff: dict[str, object],
142
+ *,
143
+ runner: dict[str, object],
144
+ state: dict[str, object],
145
+ ops: dict[str, object],
146
+ app: dict[str, object],
147
+ ) -> str:
148
+ """Bind the selected Foundation resources to the exact source handoff."""
149
+ return canonical_digest(
150
+ {
151
+ "source_commit": handoff.get("source_commit"),
152
+ "run_digest": handoff.get("run_digest"),
153
+ "region": handoff.get("region"),
154
+ "region_short": handoff.get("region_short"),
155
+ "runner": runner,
156
+ "state": state,
157
+ "ops": ops,
158
+ "app_resource_group": app,
159
+ }
160
+ )
161
+
162
+
163
+ def foundation_application_workload(
164
+ app: dict[str, object], *, environment: str, region_short: str
165
+ ) -> str:
166
+ """Recover the bounded workload token from one reviewed resource-group name."""
167
+ name = app.get("name")
168
+ suffix = f"-{environment}-{region_short}"
169
+ if not isinstance(name, str) or not name.startswith("rg-") or not name.endswith(suffix):
170
+ raise ValueError("Foundation application resource-group name is invalid")
171
+ workload = name[3 : -len(suffix)]
172
+ if re.fullmatch(r"[a-z][a-z0-9]{1,11}", workload) is None:
173
+ raise ValueError("Foundation application workload is invalid")
174
+ return workload
175
+
176
+
177
+ def vault_name(uri: str) -> str:
178
+ """Extract one bounded public-cloud Key Vault name from its exact HTTPS URI."""
179
+ match = re.fullmatch(r"https://([a-z0-9-]{3,24})[.]vault[.]azure[.]net/?", uri)
180
+ if match is None:
181
+ raise ValueError("Terraform Key Vault URI is invalid")
182
+ return match.group(1)
183
+
184
+
185
+ def registry_selector(registry_id: str, expected_name: str) -> tuple[str, ...]:
186
+ """Return ``az acr show`` selectors for a state-owned registry ID of the expected name."""
187
+
188
+ match = re.fullmatch(
189
+ r"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/"
190
+ r"Microsoft\.ContainerRegistry/registries/([^/]+)",
191
+ registry_id,
192
+ flags=re.IGNORECASE,
193
+ )
194
+ if match is None or match.group(3) != expected_name:
195
+ raise ValueError("Terraform registry differs from the planned image registry")
196
+ subscription, resource_group, name = match.groups()
197
+ return ("--name", name, "--resource-group", resource_group, "--subscription", subscription)
198
+
199
+
200
+ def planned_container_registry_name(
201
+ *, workload: str, environment: str, region_short: str, resource_suffix: str
202
+ ) -> str:
203
+ """Return the exact container registry name rendered by the verified Terraform root."""
204
+
205
+ if (
206
+ re.fullmatch(r"[a-z][a-z0-9]{1,11}", workload) is None
207
+ or re.fullmatch(r"[a-z][a-z0-9-]{1,15}", environment) is None
208
+ or re.fullmatch(r"[a-z][a-z0-9]{1,7}", region_short) is None
209
+ or re.fullmatch(r"[0-9a-f]{6}", resource_suffix) is None
210
+ ):
211
+ raise ValueError("planned container registry name inputs are invalid")
212
+ return f"cr{workload}{environment.replace('-', '')}{region_short}{resource_suffix}"
213
+
214
+
215
+ def planned_key_vault_name(
216
+ *, workload: str, environment: str, region_short: str, resource_suffix: str
217
+ ) -> str:
218
+ """Return the exact Key Vault name rendered by the verified Terraform root."""
219
+
220
+ if (
221
+ re.fullmatch(r"[a-z][a-z0-9]{1,11}", workload) is None
222
+ or re.fullmatch(r"[a-z][a-z0-9-]{1,15}", environment) is None
223
+ or re.fullmatch(r"[a-z][a-z0-9]{1,7}", region_short) is None
224
+ or re.fullmatch(r"[0-9a-f]{6}", resource_suffix) is None
225
+ ):
226
+ raise ValueError("planned Key Vault name inputs are invalid")
227
+ full_name = f"kv-{workload}-{environment}-{region_short}-{resource_suffix}"
228
+ return (
229
+ full_name
230
+ if len(full_name) <= 24
231
+ else f"kv-aip-{hashlib.sha256(full_name.encode()).hexdigest()[:8]}"
232
+ )
233
+
234
+
235
+ def planned_document_storage_name(
236
+ *, subscription_id: str, workload: str, environment: str, region_short: str
237
+ ) -> str:
238
+ """Return the exact document-storage name rendered by the verified Terraform root."""
239
+
240
+ if (
241
+ re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", subscription_id)
242
+ is None
243
+ or re.fullmatch(r"[a-z][a-z0-9]{1,11}", workload) is None
244
+ or re.fullmatch(r"[a-z][a-z0-9-]{1,15}", environment) is None
245
+ or re.fullmatch(r"[a-z][a-z0-9]{1,7}", region_short) is None
246
+ ):
247
+ raise ValueError("planned document storage name inputs are invalid")
248
+ unique = hashlib.md5(
249
+ f"{subscription_id.casefold()}:{environment}".encode(), usedforsecurity=False
250
+ ).hexdigest()[:6]
251
+ return f"st{workload}doc{environment}{region_short}{unique}"[:24]
252
+
253
+
254
+ def console_origin(hostname: str) -> str:
255
+ """Return the HTTPS origin for one deployed Static Web App hostname."""
256
+ if (
257
+ re.fullmatch(
258
+ r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:[.][0-9]+)?[.]azurestaticapps[.]net",
259
+ hostname,
260
+ )
261
+ is None
262
+ ):
263
+ raise ValueError("Console Static Web App hostname is invalid")
264
+ return f"https://{hostname}"
265
+
266
+
267
+ def aks_operator_environment(
268
+ *,
269
+ application_values: dict[str, Any],
270
+ core_environment: dict[str, Any],
271
+ substrate_outputs: dict[str, Any],
272
+ semantic_topics: list[Any],
273
+ operator_identity: dict[str, Any],
274
+ command_identity: dict[str, Any],
275
+ tenant_id: str,
276
+ console_origin: str,
277
+ ) -> dict[str, object]:
278
+ """Bind the Operator API pod environment selected by the enterprise identity surface."""
279
+
280
+ bindings = application_values["stewardship_agent_bindings"]
281
+ if not isinstance(bindings, dict):
282
+ raise ValueError("stewardship bindings are invalid") # noqa: TRY004
283
+ environment: dict[str, object] = {
284
+ "AZURE_CLIENT_ID": operator_identity["client_id"],
285
+ "FDAI_COMMAND_MI_CLIENT_ID": command_identity["client_id"],
286
+ "FDAI_ENTRA_TENANT_ID": tenant_id,
287
+ "FDAI_API_AUDIENCE": application_values["operator_api_audience"],
288
+ "FDAI_RBAC_READERS_GROUP_ID": application_values["rbac_readers_group_id"],
289
+ "FDAI_RBAC_CONTRIBUTORS_GROUP_ID": application_values["rbac_contributors_group_id"],
290
+ "FDAI_RBAC_APPROVERS_GROUP_ID": application_values["rbac_approvers_group_id"],
291
+ "FDAI_RBAC_OWNERS_GROUP_ID": application_values["rbac_owners_group_id"],
292
+ "FDAI_RBAC_BREAK_GLASS_GROUP_ID": application_values["rbac_break_glass_group_id"],
293
+ "FDAI_STEWARDSHIP_REQUIRE_BINDINGS": "1",
294
+ "FDAI_MAINTAINERS": application_values["stewardship_maintainers"],
295
+ "FDAI_KAFKA_BOOTSTRAP_SERVERS": core_environment["KAFKA_BOOTSTRAP_SERVERS"],
296
+ "KAFKA_TOPIC_EVENTS": core_environment["KAFKA_TOPIC_EVENTS"],
297
+ "FDAI_SEMANTIC_TURN_REQUEST_TOPIC": str(semantic_topics[0]),
298
+ "FDAI_SEMANTIC_TURN_PROJECTION_TOPIC": str(semantic_topics[1]),
299
+ "FDAI_SEMANTIC_TURN_PHYSICAL_TOPIC": substrate_outputs["semantic_physical"],
300
+ "FDAI_READ_INVESTIGATION_REQUEST_TOPIC": str(semantic_topics[2]),
301
+ "FDAI_OPERATOR_API_CORS_ALLOW_ORIGINS": console_origin,
302
+ }
303
+ environment.update(
304
+ {f"FDAI_STEWARD_{name.upper()}": binding for name, binding in bindings.items()}
305
+ )
306
+ return environment
307
+
308
+
309
+ # Shared-root Event Hubs startup readiness defaults (`infra/variables.tf`); the AKS
310
+ # renderer must supply them because Core's code defaults are too short for a consumer join.
311
+ AKS_CORE_STARTUP_READINESS: dict[str, str] = {
312
+ "FDAI_STARTUP_KAFKA_SETTLE_SECONDS": "12",
313
+ "FDAI_STARTUP_PROBE_TIMEOUT_SECONDS": "30",
314
+ "FDAI_STARTUP_PHASE_TIMEOUT_SECONDS": "60",
315
+ }
316
+
317
+ _KEY_VAULT_SECRET_ID = re.compile(
318
+ r"/subscriptions/[^/]+/resourceGroups/[^/]+/providers/"
319
+ r"Microsoft[.]KeyVault/vaults/[A-Za-z0-9-]{3,24}/secrets/([A-Za-z0-9-]{1,127})",
320
+ re.IGNORECASE,
321
+ )
322
+ _KEY_VAULT_SECRET_URL = re.compile(
323
+ r"https://[A-Za-z0-9-]{3,24}[.]vault[.]azure[.]net/"
324
+ r"secrets/([A-Za-z0-9-]{1,127})",
325
+ re.IGNORECASE,
326
+ )
327
+ _OPERATOR_REQUEST_SEED_NAMES = {
328
+ "core_signing_seed_secret_id": "fdai-operator-request-core-signing-seed",
329
+ "operator_signing_seed_secret_id": "fdai-operator-request-operator-signing-seed",
330
+ }
331
+
332
+
333
+ def aks_operator_request_receipts(
334
+ binding: object,
335
+ ) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]]:
336
+ """Bind Core and Operator to the deployment-owned operator_request receipt seeds.
337
+
338
+ Returns Core plain and secret environments, then Operator plain and secret environments.
339
+ Secret environments map variable names to Key Vault secret names.
340
+ """
341
+
342
+ if not isinstance(binding, dict):
343
+ raise ValueError("operator_request receipt binding is invalid") # noqa: TRY004
344
+ names: dict[str, str] = {}
345
+ for field, expected_name in _OPERATOR_REQUEST_SEED_NAMES.items():
346
+ reference = str(binding.get(field, ""))
347
+ match = _KEY_VAULT_SECRET_ID.fullmatch(reference) or _KEY_VAULT_SECRET_URL.fullmatch(
348
+ reference
349
+ )
350
+ if match is None or match.group(1) != expected_name:
351
+ raise ValueError("operator_request receipt seed reference is invalid")
352
+ names[field] = match.group(1)
353
+ core_producer = binding.get("core_producer_id")
354
+ operator_producer = binding.get("operator_producer_id")
355
+ if (
356
+ core_producer != "core-control-plane"
357
+ or not isinstance(operator_producer, str)
358
+ or not operator_producer
359
+ or operator_producer == core_producer
360
+ or names["core_signing_seed_secret_id"] == names["operator_signing_seed_secret_id"]
361
+ ):
362
+ raise ValueError("operator_request receipt producers are invalid")
363
+ return (
364
+ {
365
+ "FDAI_OPERATOR_REQUEST_CORE_PRODUCER_ID": core_producer,
366
+ "FDAI_OPERATOR_REQUEST_OPERATOR_PRODUCER_ID": operator_producer,
367
+ },
368
+ {
369
+ "FDAI_OPERATOR_REQUEST_CORE_SIGNING_SEED": names["core_signing_seed_secret_id"],
370
+ "FDAI_OPERATOR_REQUEST_OPERATOR_TRUST_SEED": names["operator_signing_seed_secret_id"],
371
+ },
372
+ {"FDAI_OPERATOR_REQUEST_RECEIPT_PRODUCER_ID": operator_producer},
373
+ {"FDAI_OPERATOR_REQUEST_OPERATOR_SIGNING_SEED": names["operator_signing_seed_secret_id"]},
374
+ )
375
+
376
+
377
+ def aks_notification_environment(
378
+ *,
379
+ application_values: dict[str, object],
380
+ email_binding: object,
381
+ identity: dict[str, Any],
382
+ ) -> dict[str, object]:
383
+ """Bind ACS Email delivery to the Core workload for the selected notifications add-on."""
384
+
385
+ binding = mapping(email_binding, "notification email binding")
386
+ recipients = application_values.get("notification_email_recipients")
387
+ if (
388
+ not isinstance(recipients, list)
389
+ or not recipients
390
+ or not all(isinstance(address, str) and address for address in recipients)
391
+ ):
392
+ raise ValueError("notifications add-on requires its private recipient list")
393
+ endpoint = binding.get("endpoint")
394
+ sender = binding.get("sender_address")
395
+ client_id = identity.get("client_id")
396
+ if not all(isinstance(value, str) and value for value in (endpoint, sender, client_id)):
397
+ raise ValueError("notification email binding is incomplete")
398
+ return {
399
+ "FDAI_EMAIL_ENDPOINT": endpoint,
400
+ "FDAI_EMAIL_SENDER_ADDRESS": sender,
401
+ "FDAI_EMAIL_RECIPIENT_ADDRESSES_JSON": json.dumps(recipients, separators=(",", ":")),
402
+ "FDAI_NOTIFICATION_MI_CLIENT_ID": client_id,
403
+ }
404
+
405
+
406
+ def aks_core_conversation_environment(
407
+ *,
408
+ application_values: dict[str, Any],
409
+ substrate_outputs: dict[str, object],
410
+ semantic_topics: list[object],
411
+ ) -> dict[str, str]:
412
+ """Build a complete Core semantic binding or reject incomplete model outputs."""
413
+
414
+ if len(semantic_topics) < 3:
415
+ raise ValueError("AKS semantic topic output contract is incomplete")
416
+
417
+ def required_text(value: object, label: str) -> str:
418
+ if not isinstance(value, str) or not value.strip():
419
+ raise ValueError(f"AKS {label} output is unavailable")
420
+ return value.strip()
421
+
422
+ environment = {
423
+ "FDAI_SEMANTIC_TURN_REQUEST_TOPIC": required_text(
424
+ semantic_topics[0], "semantic request topic"
425
+ ),
426
+ "FDAI_SEMANTIC_TURN_PROJECTION_TOPIC": required_text(
427
+ semantic_topics[1], "semantic projection topic"
428
+ ),
429
+ "FDAI_READ_INVESTIGATION_REQUEST_TOPIC": required_text(
430
+ semantic_topics[2], "read investigation topic"
431
+ ),
432
+ "FDAI_SEMANTIC_TURN_PHYSICAL_TOPIC": required_text(
433
+ substrate_outputs.get("semantic_physical"), "semantic physical topic"
434
+ ),
435
+ }
436
+ enable_llm = application_values.get("enable_llm")
437
+ if not isinstance(enable_llm, bool):
438
+ raise TypeError("AKS enable_llm setting MUST be a boolean")
439
+ if not enable_llm:
440
+ return environment
441
+
442
+ endpoint = required_text(substrate_outputs.get("llm_endpoint"), "LLM endpoint")
443
+ digest = required_text(substrate_outputs.get("resolved_models_sha256"), "resolved-model digest")
444
+ if re.fullmatch(r"[0-9a-f]{64}", digest) is None:
445
+ raise ValueError("AKS resolved-model digest MUST be a lowercase SHA-256 digest")
446
+ model_endpoints = substrate_outputs.get("llm_model_endpoints")
447
+ if (
448
+ not isinstance(model_endpoints, dict)
449
+ or not model_endpoints
450
+ or any(
451
+ not isinstance(reference, str)
452
+ or not reference.strip()
453
+ or not isinstance(model_endpoint, str)
454
+ or not model_endpoint.strip()
455
+ for reference, model_endpoint in model_endpoints.items()
456
+ )
457
+ ):
458
+ raise ValueError("AKS LLM model endpoint outputs are incomplete")
459
+ environment.update(
460
+ {
461
+ "LLM_MODE": "azure",
462
+ "LLM_RESOLVED_MODELS_PATH": "/app/resolved-models.json",
463
+ "LLM_RESOLVED_MODELS_SHA256": digest,
464
+ "FDAI_LLM_ENDPOINT": endpoint,
465
+ "FDAI_MODEL_ENDPOINTS_JSON": json.dumps(
466
+ model_endpoints,
467
+ separators=(",", ":"),
468
+ sort_keys=True,
469
+ ),
470
+ }
471
+ )
472
+ return environment
473
+
474
+
475
+ def selected_identity(identities: dict[str, Any], name: str, selected: bool) -> dict[str, Any]:
476
+ """Return one selected runtime identity or an empty optional binding."""
477
+
478
+ if not selected:
479
+ return {}
480
+ return mapping(identities.get(name), f"{name.replace('_', ' ')} runtime identity")
481
+
482
+
483
+ def mapping(value: object, label: str) -> dict[str, Any]:
484
+ """Require one string-keyed mapping."""
485
+
486
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
487
+ raise ValueError(f"{label} is invalid")
488
+ return {str(key): item for key, item in value.items()}
489
+
490
+
491
+ def required_guid(value: dict[str, Any], field: str) -> str:
492
+ """Return one required GUID field."""
493
+
494
+ item = value.get(field)
495
+ if not isinstance(item, str) or _GUID.fullmatch(item) is None:
496
+ raise ValueError(f"{field} is invalid")
497
+ return item
498
+
499
+
500
+ def require_browser_gateway_backend_nsg(*, enabled: bool, backend_nsg_id: str) -> str:
501
+ """Require an attached AKS subnet NSG whenever the browser gateway is enabled."""
502
+
503
+ if enabled and not backend_nsg_id.strip():
504
+ raise ValueError(
505
+ "AKS subnet network security group is required when the browser gateway is enabled"
506
+ )
507
+ return backend_nsg_id
508
+
509
+
510
+ def verify_browser_gateway_backend_nsg(
511
+ workloads_values: dict[str, object],
512
+ *,
513
+ observed_nsg_id: str,
514
+ ) -> None:
515
+ """Verify that the live AKS subnet NSG still matches the prepared gateway binding."""
516
+
517
+ gateway = mapping(workloads_values.get("browser_gateway"), "AKS browser gateway")
518
+ expected_nsg_id = require_browser_gateway_backend_nsg(
519
+ enabled=gateway.get("enabled") is True,
520
+ backend_nsg_id=str(gateway.get("backend_nsg_id", "")),
521
+ )
522
+ if expected_nsg_id and observed_nsg_id.strip().casefold() != expected_nsg_id.strip().casefold():
523
+ raise ValueError(
524
+ "AKS subnet network security group differs from the browser gateway binding"
525
+ )
526
+
527
+
528
+ def validate_browser_console_binding(
529
+ *,
530
+ subscription_id: str,
531
+ hostname: str,
532
+ console_origin: str,
533
+ resource_id: str,
534
+ operator_url: str,
535
+ ingestion_url: str,
536
+ ) -> dict[str, str]:
537
+ """Validate cross-state SWA and APIM outputs for the AKS browser Console."""
538
+
539
+ static_site_match = re.fullmatch(
540
+ r"/subscriptions/([^/]+)/resourceGroups/[^/]+/providers/"
541
+ r"Microsoft[.]Web/staticSites/[^/]+",
542
+ resource_id,
543
+ flags=re.IGNORECASE,
544
+ )
545
+ if static_site_match is None:
546
+ raise ValueError("Console Static Web App resource ID is invalid")
547
+ if static_site_match.group(1).casefold() != subscription_id.casefold():
548
+ raise ValueError("Console Static Web App belongs to a different subscription")
549
+ operator_match = re.fullmatch(
550
+ r"https://([a-z0-9](?:[a-z0-9-]*[a-z0-9])?[.]azure-api[.]net)",
551
+ operator_url,
552
+ )
553
+ if operator_match is None:
554
+ raise ValueError("browser gateway Operator URL is invalid")
555
+ if ingestion_url != f"{operator_url}/ingestion":
556
+ raise ValueError("browser gateway ingestion URL is not bound to the Operator gateway")
557
+ return {
558
+ "console_hostname": hostname,
559
+ "console_origin": console_origin,
560
+ "console_static_web_app_id": resource_id,
561
+ "operator_api_base_url": operator_url,
562
+ "ingestion_api_base_url": ingestion_url,
563
+ }
@@ -0,0 +1,100 @@
1
+ """Verified managed-host Kubernetes tool execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from collections.abc import Callable, Mapping
8
+ from pathlib import Path
9
+ from urllib.parse import urlencode
10
+
11
+
12
+ def tools(context: Mapping[str, object]) -> tuple[str, str, dict[str, str]]:
13
+ """Return the sealed kubectl pair and an environment that resolves its exec plugin."""
14
+
15
+ root = Path(str(context.get("kit_bin", "")))
16
+ selected: list[str] = []
17
+ for name in ("kubectl", "kubelogin"):
18
+ path = root / name
19
+ try:
20
+ details = path.lstat()
21
+ except OSError as exc:
22
+ raise ValueError(f"managed-host {name} is unavailable") from exc
23
+ if (
24
+ not stat.S_ISREG(details.st_mode)
25
+ or details.st_uid != os.geteuid()
26
+ or not details.st_mode & stat.S_IXUSR
27
+ ):
28
+ raise ValueError(f"managed-host {name} is unavailable")
29
+ selected.append(str(path))
30
+ environment = dict(os.environ)
31
+ current_path = environment.get("PATH", "")
32
+ environment["PATH"] = str(root) if not current_path else f"{root}{os.pathsep}{current_path}"
33
+ return selected[0], selected[1], environment
34
+
35
+
36
+ def capture_deployments(
37
+ context: Mapping[str, object],
38
+ *,
39
+ namespace: str,
40
+ service: str | None,
41
+ capture: Callable[..., str],
42
+ ) -> str:
43
+ """Read the typed Deployment collection with the sealed Kubernetes tools."""
44
+
45
+ kubeconfig = Path(str(context.get("kubeconfig", "")))
46
+ if not kubeconfig.is_file():
47
+ raise ValueError("AKS kubeconfig is unavailable for service update readback")
48
+ kubectl, _kubelogin, environment = tools(context)
49
+ endpoint = f"/apis/apps/v1/namespaces/{namespace}/deployments"
50
+ if service is not None:
51
+ endpoint = f"{endpoint}?{urlencode({'labelSelector': f'app.kubernetes.io/name={service}'})}"
52
+ return capture(
53
+ (
54
+ kubectl,
55
+ "get",
56
+ f"--raw={endpoint}",
57
+ "--request-timeout=60s",
58
+ f"--kubeconfig={kubeconfig}",
59
+ ),
60
+ cwd=kubeconfig.parent,
61
+ timeout=90,
62
+ reason="AKS Deployment observation failed",
63
+ env=environment,
64
+ )
65
+
66
+
67
+ def capture_pods(
68
+ context: Mapping[str, object],
69
+ *,
70
+ namespace: str,
71
+ service: str,
72
+ capture: Callable[..., str],
73
+ ) -> str:
74
+ """Read one service's Pod collection with the sealed Kubernetes tools."""
75
+
76
+ kubeconfig = Path(str(context.get("kubeconfig", "")))
77
+ if not kubeconfig.is_file():
78
+ raise ValueError("AKS kubeconfig is unavailable for service update readback")
79
+ kubectl, _kubelogin, environment = tools(context)
80
+ return capture(
81
+ (
82
+ kubectl,
83
+ "get",
84
+ "pods",
85
+ "--namespace",
86
+ namespace,
87
+ "--output",
88
+ "json",
89
+ "--request-timeout=60s",
90
+ f"--selector=app.kubernetes.io/name={service}",
91
+ f"--kubeconfig={kubeconfig}",
92
+ ),
93
+ cwd=kubeconfig.parent,
94
+ timeout=90,
95
+ reason="AKS Pod observation failed",
96
+ env=environment,
97
+ )
98
+
99
+
100
+ __all__ = ["capture_deployments", "capture_pods", "tools"]