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,540 @@
1
+ """Govern one separately approved residual apply after a partial Terraform effect."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import stat
9
+ import subprocess
10
+ from collections.abc import Callable
11
+ from datetime import UTC, datetime, timedelta
12
+ from pathlib import Path
13
+
14
+ from fdai_deployment_cli.contracts import canonical_digest
15
+ from fdai_deployment_cli.private_output import write_private_output
16
+ from fdai_deployment_cli.standalone_host_state import (
17
+ file_digest,
18
+ moment,
19
+ private_json,
20
+ replace_private_json,
21
+ )
22
+ from fdai_deployment_cli.standalone_host_values import plan_summary
23
+ from fdai_deployment_cli.standalone_review import validate_plan_review
24
+
25
+ _STAGES = frozenset({"access", "substrate", "runtime", "database", "application"})
26
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
27
+ _SOURCE_COMMIT = re.compile(r"[0-9a-f]{40}")
28
+
29
+
30
+ def seal_terraform_plan(path: Path) -> None:
31
+ """Validate and seal one Terraform-created binary plan before trust binding."""
32
+
33
+ try:
34
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
35
+ except OSError as exc:
36
+ raise ValueError("Terraform plan output is unavailable") from exc
37
+ with os.fdopen(descriptor, "rb") as stream:
38
+ before = os.fstat(stream.fileno())
39
+ if (
40
+ not stat.S_ISREG(before.st_mode)
41
+ or before.st_uid != os.geteuid()
42
+ or before.st_nlink != 1
43
+ or before.st_size <= 0
44
+ ):
45
+ raise ValueError("Terraform plan output is invalid")
46
+ os.fchmod(stream.fileno(), 0o600)
47
+ after = os.fstat(stream.fileno())
48
+ if (
49
+ stat.S_IMODE(after.st_mode) != 0o600
50
+ or after.st_dev != before.st_dev
51
+ or after.st_ino != before.st_ino
52
+ or after.st_size != before.st_size
53
+ or after.st_mtime_ns != before.st_mtime_ns
54
+ ):
55
+ raise ValueError("Terraform plan output changed while being sealed")
56
+
57
+
58
+ def prepare_residual_review(
59
+ *,
60
+ work_dir: Path,
61
+ stage: str,
62
+ context: dict[str, object],
63
+ infra: Path,
64
+ variables: Path,
65
+ targets: tuple[str, ...],
66
+ original_review: dict[str, object],
67
+ original_claim: dict[str, object],
68
+ ) -> dict[str, object] | None:
69
+ """Return a separately bound residual review, or None when the original effect converged."""
70
+
71
+ _validate_original_claim(stage, context, original_review, original_claim)
72
+ operation = _operation(stage)
73
+ plan_path = work_dir / f"{operation}.tfplan"
74
+ review_path = work_dir / f"{operation}-review.json"
75
+ plan_path.unlink(missing_ok=True)
76
+ review_path.unlink(missing_ok=True)
77
+ command = [
78
+ "terraform",
79
+ "plan",
80
+ "-detailed-exitcode",
81
+ "-input=false",
82
+ "-no-color",
83
+ f"-var-file={variables}",
84
+ f"-out={plan_path}",
85
+ *(f"-target={target}" for target in targets),
86
+ ]
87
+ completed = subprocess.run(
88
+ command,
89
+ cwd=infra,
90
+ check=False,
91
+ capture_output=True,
92
+ timeout=1800,
93
+ umask=0o077,
94
+ )
95
+ if completed.returncode == 0:
96
+ plan_path.unlink(missing_ok=True)
97
+ return None
98
+ if completed.returncode != 2:
99
+ plan_path.unlink(missing_ok=True)
100
+ raise ValueError("standalone residual plan failed")
101
+ seal_terraform_plan(plan_path)
102
+ shown = subprocess.run(
103
+ ("terraform", "show", "-json", str(plan_path)),
104
+ cwd=infra,
105
+ check=False,
106
+ capture_output=True,
107
+ text=True,
108
+ timeout=300,
109
+ umask=0o077,
110
+ )
111
+ if shown.returncode != 0:
112
+ raise ValueError("standalone residual plan projection failed")
113
+ try:
114
+ summary = plan_summary(json.loads(shown.stdout))
115
+ except json.JSONDecodeError as exc:
116
+ raise ValueError("standalone residual plan projection is invalid") from exc
117
+ review: dict[str, object] = {
118
+ "schema_version": "fdai.standalone-application-plan.v1",
119
+ "stage": stage,
120
+ "plan_digest": file_digest(plan_path),
121
+ "target_binding": _required_digest(context, "target_binding"),
122
+ "source_commit": _required_source_commit(context),
123
+ "runtime_profile_digest": _runtime_profile_digest(context),
124
+ "runtime_platform": _runtime_platform(context),
125
+ "summary": summary,
126
+ "residual_recovery": {
127
+ "operation": operation,
128
+ "original_plan_digest": original_review["plan_digest"],
129
+ "original_claim_digest": canonical_digest(original_claim),
130
+ },
131
+ "expires_at": moment(datetime.now(UTC) + timedelta(hours=1)),
132
+ "mutation_performed": False,
133
+ "subscription_ready": False,
134
+ }
135
+ review["review_digest"] = canonical_digest(review)
136
+ replace_private_json(review_path, review)
137
+ return review
138
+
139
+
140
+ def apply_residual_plan(
141
+ *,
142
+ work_dir: Path,
143
+ stage: str,
144
+ context: dict[str, object],
145
+ infra: Path,
146
+ variables: Path,
147
+ targets: tuple[str, ...],
148
+ original_review: dict[str, object],
149
+ original_claim: dict[str, object],
150
+ residual_review: dict[str, object],
151
+ approval: dict[str, object],
152
+ effect_readback: Callable[[], bool],
153
+ ) -> dict[str, object]:
154
+ """Claim and apply one approved residual plan, then require readback and zero change."""
155
+
156
+ _validate_residual_review(
157
+ stage=stage,
158
+ context=context,
159
+ original_review=original_review,
160
+ original_claim=original_claim,
161
+ residual_review=residual_review,
162
+ )
163
+ operation = _operation(stage)
164
+ plan_path = work_dir / f"{operation}.tfplan"
165
+ if file_digest(plan_path) != residual_review["plan_digest"]:
166
+ raise ValueError("standalone residual plan changed before apply")
167
+ claim_path = work_dir / f"{operation}-claim.json"
168
+ receipt_path = work_dir / f"{stage}-receipt.json"
169
+ if receipt_path.exists():
170
+ return private_json(receipt_path, "standalone residual apply receipt")
171
+ claim = _residual_claim(
172
+ stage=stage,
173
+ context=context,
174
+ original_claim=original_claim,
175
+ residual_review=residual_review,
176
+ approval=approval,
177
+ )
178
+ try:
179
+ write_private_output(
180
+ claim_path,
181
+ json.dumps(claim, sort_keys=True, separators=(",", ":")) + "\n",
182
+ )
183
+ except FileExistsError as exc:
184
+ raise ValueError(
185
+ "standalone residual apply claim already exists; automatic retry is blocked"
186
+ ) from exc
187
+ completed = subprocess.run(
188
+ ("terraform", "apply", "-input=false", "-no-color", str(plan_path)),
189
+ cwd=infra,
190
+ check=False,
191
+ capture_output=True,
192
+ timeout=7200,
193
+ umask=0o077,
194
+ )
195
+ if completed.returncode != 0:
196
+ raise ValueError(
197
+ f"{stage} residual exact apply failed; verification-only recovery is required"
198
+ )
199
+ if not effect_readback():
200
+ raise ValueError(f"{stage} residual apply effect readback is incomplete")
201
+ if not _zero_change(
202
+ infra=infra,
203
+ variables=variables,
204
+ targets=targets,
205
+ ):
206
+ raise ValueError(f"{stage} residual apply did not converge to zero change")
207
+ return _write_receipt(
208
+ receipt_path=receipt_path,
209
+ stage=stage,
210
+ context=context,
211
+ original_review=original_review,
212
+ original_claim=original_claim,
213
+ residual_review=residual_review,
214
+ residual_claim=claim,
215
+ verification_only=False,
216
+ )
217
+
218
+
219
+ def recover_residual_apply(
220
+ *,
221
+ work_dir: Path,
222
+ stage: str,
223
+ context: dict[str, object],
224
+ infra: Path,
225
+ variables: Path,
226
+ targets: tuple[str, ...],
227
+ original_review: dict[str, object],
228
+ original_claim: dict[str, object],
229
+ effect_readback: Callable[[], bool],
230
+ successor_prerequisite_verified: bool = False,
231
+ ) -> dict[str, object]:
232
+ """Verify a claimed residual effect without issuing another apply."""
233
+
234
+ operation = _operation(stage)
235
+ review = private_json(work_dir / f"{operation}-review.json", "standalone residual plan review")
236
+ claim = private_json(work_dir / f"{operation}-claim.json", "standalone residual apply claim")
237
+ _validate_residual_claim(
238
+ stage=stage,
239
+ context=context,
240
+ original_review=original_review,
241
+ original_claim=original_claim,
242
+ residual_review=review,
243
+ residual_claim=claim,
244
+ )
245
+ successor_operation = _successor_operation(stage)
246
+ successor_claim_path = work_dir / f"{successor_operation}-claim.json"
247
+ if successor_claim_path.exists() or successor_claim_path.is_symlink():
248
+ from fdai_deployment_cli.standalone_residual_successor import (
249
+ recover_residual_successor,
250
+ )
251
+
252
+ return recover_residual_successor(
253
+ work_dir=work_dir,
254
+ stage=stage,
255
+ context=context,
256
+ infra=infra,
257
+ variables=variables,
258
+ targets=targets,
259
+ original_review=original_review,
260
+ original_claim=original_claim,
261
+ residual_review=review,
262
+ residual_claim=claim,
263
+ effect_readback=effect_readback,
264
+ )
265
+ if not _zero_change(infra=infra, variables=variables, targets=targets):
266
+ from fdai_deployment_cli.standalone_residual_successor import (
267
+ prepare_residual_successor_review,
268
+ )
269
+
270
+ return prepare_residual_successor_review(
271
+ work_dir=work_dir,
272
+ stage=stage,
273
+ context=context,
274
+ infra=infra,
275
+ variables=variables,
276
+ targets=targets,
277
+ original_review=original_review,
278
+ original_claim=original_claim,
279
+ residual_review=review,
280
+ residual_claim=claim,
281
+ successor_prerequisite_verified=successor_prerequisite_verified,
282
+ )
283
+ if not effect_readback():
284
+ raise ValueError("standalone residual apply effect readback is incomplete")
285
+ return _write_receipt(
286
+ receipt_path=work_dir / f"{stage}-receipt.json",
287
+ stage=stage,
288
+ context=context,
289
+ original_review=original_review,
290
+ original_claim=original_claim,
291
+ residual_review=review,
292
+ residual_claim=claim,
293
+ verification_only=True,
294
+ )
295
+
296
+
297
+ def _validate_residual_claim(
298
+ *,
299
+ stage: str,
300
+ context: dict[str, object],
301
+ original_review: dict[str, object],
302
+ original_claim: dict[str, object],
303
+ residual_review: dict[str, object],
304
+ residual_claim: dict[str, object],
305
+ ) -> None:
306
+ _validate_residual_review(
307
+ stage=stage,
308
+ context=context,
309
+ original_review=original_review,
310
+ original_claim=original_claim,
311
+ residual_review=residual_review,
312
+ allow_expired=True,
313
+ )
314
+ expected_claim = _residual_claim_binding(
315
+ stage=stage,
316
+ context=context,
317
+ original_claim=original_claim,
318
+ residual_review=residual_review,
319
+ )
320
+ if (
321
+ residual_claim.get("schema_version") != "fdai.standalone-residual-claim.v1"
322
+ or any(residual_claim.get(key) != value for key, value in expected_claim.items())
323
+ or not isinstance(residual_claim.get("approval_digest"), str)
324
+ or _DIGEST.fullmatch(str(residual_claim["approval_digest"])) is None
325
+ or not isinstance(residual_claim.get("claimed_at"), str)
326
+ or residual_claim.get("mutation_performed") is not False
327
+ ):
328
+ raise ValueError("standalone residual apply recovery claim is invalid")
329
+
330
+
331
+ def residual_claim_exists(work_dir: Path, stage: str) -> bool:
332
+ path = work_dir / f"{_operation(stage)}-claim.json"
333
+ return path.exists() or path.is_symlink()
334
+
335
+
336
+ def _write_receipt(
337
+ *,
338
+ receipt_path: Path,
339
+ stage: str,
340
+ context: dict[str, object],
341
+ original_review: dict[str, object],
342
+ original_claim: dict[str, object],
343
+ residual_review: dict[str, object],
344
+ residual_claim: dict[str, object],
345
+ verification_only: bool,
346
+ ) -> dict[str, object]:
347
+ receipt: dict[str, object] = {
348
+ "schema_version": "fdai.standalone-residual-apply-receipt.v1",
349
+ "state": "applied",
350
+ "stage": stage,
351
+ "plan_digest": residual_review["plan_digest"],
352
+ "original_plan_digest": original_review["plan_digest"],
353
+ "runtime_profile_digest": _runtime_profile_digest(context),
354
+ "original_claim_digest": canonical_digest(original_claim),
355
+ "residual_claim_digest": canonical_digest(residual_claim),
356
+ "control_plane_readback_verified": True,
357
+ "terraform_zero_change_verified": True,
358
+ "residual_recovery": True,
359
+ "verification_only_recovery": verification_only,
360
+ "mutation_performed": True,
361
+ "subscription_ready": False,
362
+ }
363
+ receipt["receipt_digest"] = canonical_digest(receipt)
364
+ replace_private_json(receipt_path, receipt)
365
+ return receipt
366
+
367
+
368
+ def _residual_claim(
369
+ *,
370
+ stage: str,
371
+ context: dict[str, object],
372
+ original_claim: dict[str, object],
373
+ residual_review: dict[str, object],
374
+ approval: dict[str, object],
375
+ ) -> dict[str, object]:
376
+ claim: dict[str, object] = {
377
+ "schema_version": "fdai.standalone-residual-claim.v1",
378
+ **_residual_claim_binding(
379
+ stage=stage,
380
+ context=context,
381
+ original_claim=original_claim,
382
+ residual_review=residual_review,
383
+ ),
384
+ "approval_digest": canonical_digest(approval),
385
+ "claimed_at": moment(datetime.now(UTC)),
386
+ "mutation_performed": False,
387
+ }
388
+ return claim
389
+
390
+
391
+ def _residual_claim_binding(
392
+ *,
393
+ stage: str,
394
+ context: dict[str, object],
395
+ original_claim: dict[str, object],
396
+ residual_review: dict[str, object],
397
+ ) -> dict[str, object]:
398
+ original_claim_digest = canonical_digest(original_claim)
399
+ plan_digest = residual_review["plan_digest"]
400
+ return {
401
+ "stage": stage,
402
+ "operation": _operation(stage),
403
+ "original_claim_digest": original_claim_digest,
404
+ "plan_digest": plan_digest,
405
+ "idempotency_key": canonical_digest(
406
+ {
407
+ "target_binding": _required_digest(context, "target_binding"),
408
+ "original_claim_digest": original_claim_digest,
409
+ "plan_digest": plan_digest,
410
+ }
411
+ ),
412
+ }
413
+
414
+
415
+ def _validate_original_claim(
416
+ stage: str,
417
+ context: dict[str, object],
418
+ review: dict[str, object],
419
+ claim: dict[str, object],
420
+ ) -> None:
421
+ plan_digest = review.get("plan_digest")
422
+ if (
423
+ claim.get("schema_version") != "fdai.standalone-application-claim.v1"
424
+ or claim.get("stage") != stage
425
+ or not isinstance(plan_digest, str)
426
+ or _DIGEST.fullmatch(plan_digest) is None
427
+ or claim.get("plan_digest") != plan_digest
428
+ or claim.get("mutation_performed") is not False
429
+ or claim.get("idempotency_key")
430
+ != canonical_digest(
431
+ {
432
+ "target_binding": _required_digest(context, "target_binding"),
433
+ "plan_digest": plan_digest,
434
+ }
435
+ )
436
+ ):
437
+ raise ValueError("standalone apply recovery claim is invalid")
438
+
439
+
440
+ def _validate_residual_review(
441
+ *,
442
+ stage: str,
443
+ context: dict[str, object],
444
+ original_review: dict[str, object],
445
+ original_claim: dict[str, object],
446
+ residual_review: dict[str, object],
447
+ allow_expired: bool = False,
448
+ ) -> None:
449
+ _validate_original_claim(stage, context, original_review, original_claim)
450
+ validated_stage, _destructive = validate_plan_review(
451
+ residual_review, allow_expired=allow_expired
452
+ )
453
+ binding = residual_review.get("residual_recovery")
454
+ if (
455
+ validated_stage != stage
456
+ or residual_review.get("target_binding") != context.get("target_binding")
457
+ or residual_review.get("source_commit") != context.get("source_commit")
458
+ or _runtime_profile_digest(residual_review) != _runtime_profile_digest(context)
459
+ or not isinstance(binding, dict)
460
+ or binding
461
+ != {
462
+ "operation": _operation(stage),
463
+ "original_plan_digest": original_review.get("plan_digest"),
464
+ "original_claim_digest": canonical_digest(original_claim),
465
+ }
466
+ ):
467
+ raise ValueError("standalone residual plan binding is invalid")
468
+
469
+
470
+ def _zero_change(*, infra: Path, variables: Path, targets: tuple[str, ...]) -> bool:
471
+ command = [
472
+ "terraform",
473
+ "plan",
474
+ "-detailed-exitcode",
475
+ "-input=false",
476
+ "-no-color",
477
+ f"-var-file={variables}",
478
+ *(f"-target={target}" for target in targets),
479
+ ]
480
+ completed = subprocess.run(
481
+ command,
482
+ cwd=infra,
483
+ check=False,
484
+ capture_output=True,
485
+ timeout=1800,
486
+ umask=0o077,
487
+ )
488
+ if completed.returncode not in {0, 2}:
489
+ raise ValueError("standalone residual zero-change verification failed")
490
+ return completed.returncode == 0
491
+
492
+
493
+ def _operation(stage: str) -> str:
494
+ if stage not in _STAGES:
495
+ raise ValueError("standalone residual stage is invalid")
496
+ return f"{stage}-residual"
497
+
498
+
499
+ def _successor_operation(stage: str) -> str:
500
+ if stage not in _STAGES:
501
+ raise ValueError("standalone residual successor stage is invalid")
502
+ return f"{stage}-residual-successor"
503
+
504
+
505
+ def _runtime_profile_digest(value: dict[str, object]) -> str:
506
+ digest = value.get("runtime_profile_digest")
507
+ if not isinstance(digest, str) or _DIGEST.fullmatch(digest) is None:
508
+ raise ValueError("standalone residual runtime profile binding is invalid")
509
+ return digest
510
+
511
+
512
+ def _runtime_platform(context: dict[str, object]) -> str:
513
+ profile = context.get("runtime_profile")
514
+ platform = profile.get("runtime_platform") if isinstance(profile, dict) else None
515
+ if platform not in {"aks", "container-apps"}:
516
+ raise ValueError("standalone residual runtime platform is invalid")
517
+ return str(platform)
518
+
519
+
520
+ def _required_digest(value: dict[str, object], field: str) -> str:
521
+ item = value.get(field)
522
+ if not isinstance(item, str) or _DIGEST.fullmatch(item) is None:
523
+ raise ValueError(f"standalone residual {field} binding is invalid")
524
+ return item
525
+
526
+
527
+ def _required_source_commit(context: dict[str, object]) -> str:
528
+ value = context.get("source_commit")
529
+ if not isinstance(value, str) or _SOURCE_COMMIT.fullmatch(value) is None:
530
+ raise ValueError("standalone residual source binding is invalid")
531
+ return value
532
+
533
+
534
+ __all__ = [
535
+ "apply_residual_plan",
536
+ "prepare_residual_review",
537
+ "recover_residual_apply",
538
+ "residual_claim_exists",
539
+ "seal_terraform_plan",
540
+ ]