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,438 @@
1
+ """Adopt one verified recovered Foundation without repeating its effects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import stat
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from fdai_deployment_cli.contracts import (
14
+ ProvisionProfile,
15
+ canonical_bytes,
16
+ canonical_digest,
17
+ load_json_object,
18
+ )
19
+ from fdai_deployment_cli.foundation_adoption_evidence import (
20
+ validate_foundation_adoption_receipt,
21
+ verify_foundation_chain,
22
+ )
23
+ from fdai_deployment_cli.private_output import read_private_bytes, write_private_bytes
24
+ from fdai_deployment_cli.target import compute_target_binding
25
+
26
+ _COMMIT = re.compile(r"[0-9a-f]{40}")
27
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
28
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
29
+ _PLAN_REF = "foundation-adoption"
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class AdoptedGenesisContext:
34
+ """Prepared fields consumed by the existing standalone application coordinator."""
35
+
36
+ root: Path
37
+ profile: Path
38
+ ssh_private_key: Path
39
+ source_commit: str
40
+ target_binding: str
41
+ run_binding: str
42
+ kit_manifest_digest: str
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class FoundationAdoption:
47
+ """Staged no-effect Foundation context and its immutable evidence."""
48
+
49
+ prepared: AdoptedGenesisContext
50
+ status: dict[str, object]
51
+ receipt: dict[str, object]
52
+
53
+
54
+ def stage_recovered_foundation(
55
+ *,
56
+ foundation_directory: Path,
57
+ recovery_directory: Path,
58
+ destination: Path,
59
+ application_source_commit: str,
60
+ kit_manifest_digest: str,
61
+ runtime_release_digest: str,
62
+ tenant_id: str,
63
+ subscription_id: str,
64
+ region: str,
65
+ monthly_cost_ceiling: int,
66
+ ) -> FoundationAdoption:
67
+ """Verify and stage a recovered Foundation for a separately approved application.
68
+
69
+ The source Foundation remains the only owner of its backend and resources. This
70
+ boundary copies only immutable handoff and access evidence into a new private
71
+ application context. It never plans, applies, migrates, enrolls, or reclaims state.
72
+ """
73
+
74
+ _validate_inputs(
75
+ foundation_directory=foundation_directory,
76
+ recovery_directory=recovery_directory,
77
+ destination=destination,
78
+ application_source_commit=application_source_commit,
79
+ kit_manifest_digest=kit_manifest_digest,
80
+ runtime_release_digest=runtime_release_digest,
81
+ tenant_id=tenant_id,
82
+ subscription_id=subscription_id,
83
+ region=region,
84
+ monthly_cost_ceiling=monthly_cost_ceiling,
85
+ )
86
+ _require_private_directory(foundation_directory)
87
+ _require_private_directory(recovery_directory)
88
+ _create_or_validate_private_directory(destination)
89
+ plan_directory = destination / _PLAN_REF
90
+ _create_or_validate_private_directory(plan_directory)
91
+ evidence_directory = plan_directory / "evidence"
92
+ _create_or_validate_private_directory(evidence_directory)
93
+
94
+ profile_bytes = read_private_bytes(foundation_directory / "profile.json", max_bytes=65_536)
95
+ profile = ProvisionProfile.from_mapping(
96
+ load_json_object(profile_bytes, label="adopted Foundation profile")
97
+ )
98
+ private_key = read_private_bytes(foundation_directory / "runner_ed25519", max_bytes=65_536)
99
+ handoff_path, receipt_path = _terminal_foundation_paths(recovery_directory)
100
+ handoff_bytes, handoff = _read_record(handoff_path, label="terminal Foundation handoff")
101
+ recovery_bytes, recovery = _read_receipt(
102
+ receipt_path,
103
+ label="terminal Foundation apply receipt",
104
+ digest_field="receipt_digest",
105
+ )
106
+ enrollment_bytes, enrollment = _read_receipt(
107
+ recovery_directory / "runner-enrollment-receipt.json",
108
+ label="Foundation enrollment receipt",
109
+ digest_field="receipt_digest",
110
+ )
111
+ state_bytes, state_receipt = _read_receipt(
112
+ recovery_directory / "foundation-state-handoff-receipt.json",
113
+ label="Foundation state handoff receipt",
114
+ digest_field="receipt_digest",
115
+ )
116
+ authority_bytes, authority = _read_receipt(
117
+ recovery_directory / "foundation-state-authority.json",
118
+ label="Foundation state authority",
119
+ digest_field="authority_digest",
120
+ )
121
+ known_hosts = read_private_bytes(recovery_directory / "runner-known-hosts", max_bytes=262_144)
122
+
123
+ receipt = build_foundation_adoption_receipt(
124
+ profile=profile,
125
+ handoff=handoff,
126
+ recovery=recovery,
127
+ enrollment=enrollment,
128
+ state_receipt=state_receipt,
129
+ authority=authority,
130
+ known_hosts=known_hosts,
131
+ application_source_commit=application_source_commit,
132
+ kit_manifest_digest=kit_manifest_digest,
133
+ runtime_release_digest=runtime_release_digest,
134
+ tenant_id=tenant_id,
135
+ subscription_id=subscription_id,
136
+ region=region,
137
+ monthly_cost_ceiling=monthly_cost_ceiling,
138
+ )
139
+ target_binding = str(receipt["target_binding"])
140
+ adopted_run_binding = str(receipt["adopted_run_binding"])
141
+
142
+ staged = {
143
+ destination / "profile.json": profile_bytes,
144
+ destination / "runner_ed25519": private_key,
145
+ plan_directory / "foundation-private-handoff.json": handoff_bytes,
146
+ plan_directory / "runner-known-hosts": known_hosts,
147
+ evidence_directory / "recovery-apply-receipt.json": recovery_bytes,
148
+ evidence_directory / "runner-enrollment-receipt.json": enrollment_bytes,
149
+ evidence_directory / "foundation-state-handoff-receipt.json": state_bytes,
150
+ evidence_directory / "foundation-state-authority.json": authority_bytes,
151
+ destination / "foundation-adoption-receipt.json": canonical_bytes(receipt),
152
+ }
153
+ for path, content in staged.items():
154
+ _write_or_verify(path, content)
155
+
156
+ prepared = AdoptedGenesisContext(
157
+ root=destination,
158
+ profile=destination / "profile.json",
159
+ ssh_private_key=destination / "runner_ed25519",
160
+ source_commit=application_source_commit,
161
+ target_binding=target_binding,
162
+ run_binding=adopted_run_binding,
163
+ kit_manifest_digest=kit_manifest_digest,
164
+ )
165
+ status: dict[str, object] = {
166
+ "schema_version": "fdai.foundation-adoption-status.v1",
167
+ "state": "adopted",
168
+ "source_commit": application_source_commit,
169
+ "run_binding": adopted_run_binding,
170
+ "target_binding": target_binding,
171
+ "foundation_report": {
172
+ "foundation_plan": {"plan_ref": _PLAN_REF},
173
+ "state_handoff": {"receipt_digest": state_receipt["receipt_digest"]},
174
+ },
175
+ "mutation_performed": False,
176
+ "deployment_ready": False,
177
+ "subscription_ready": False,
178
+ }
179
+ return FoundationAdoption(prepared=prepared, status=status, receipt=receipt)
180
+
181
+
182
+ def _terminal_foundation_paths(directory: Path) -> tuple[Path, Path]:
183
+ recovery = (
184
+ directory / "recovery-private-handoff.json",
185
+ directory / "recovery-apply-receipt.json",
186
+ )
187
+ applied = (
188
+ directory / "foundation-private-handoff.json",
189
+ directory / "foundation-apply-receipt.json",
190
+ )
191
+ available = [pair for pair in (recovery, applied) if all(path.is_file() for path in pair)]
192
+ if len(available) != 1:
193
+ raise ValueError("Foundation adoption requires one exact terminal evidence pair")
194
+ return available[0]
195
+
196
+
197
+ def build_foundation_adoption_receipt(
198
+ *,
199
+ profile: ProvisionProfile,
200
+ handoff: dict[str, Any],
201
+ recovery: dict[str, Any],
202
+ enrollment: dict[str, Any],
203
+ state_receipt: dict[str, Any],
204
+ authority: dict[str, Any],
205
+ known_hosts: bytes,
206
+ application_source_commit: str,
207
+ kit_manifest_digest: str,
208
+ runtime_release_digest: str,
209
+ tenant_id: str,
210
+ subscription_id: str,
211
+ region: str,
212
+ monthly_cost_ceiling: int,
213
+ ) -> dict[str, object]:
214
+ """Verify one terminal Foundation chain and bind it to an application kit revision."""
215
+
216
+ target_binding = compute_target_binding(
217
+ tenant_id=tenant_id,
218
+ subscription_id=subscription_id,
219
+ )
220
+ foundation_source_commit, foundation_run_binding = verify_foundation_chain(
221
+ profile=profile,
222
+ handoff=handoff,
223
+ recovery=recovery,
224
+ enrollment=enrollment,
225
+ state_receipt=state_receipt,
226
+ authority=authority,
227
+ known_hosts=known_hosts,
228
+ target_binding=target_binding,
229
+ tenant_id=tenant_id,
230
+ subscription_id=subscription_id,
231
+ region=region,
232
+ monthly_cost_ceiling=monthly_cost_ceiling,
233
+ )
234
+ adopted_run_binding = canonical_digest(
235
+ {
236
+ "foundation_run_binding": foundation_run_binding,
237
+ "application_source_commit": application_source_commit,
238
+ "kit_manifest_digest": kit_manifest_digest,
239
+ "runtime_release_digest": runtime_release_digest,
240
+ }
241
+ )
242
+ receipt: dict[str, object] = {
243
+ "schema_version": "fdai.foundation-adoption.v1",
244
+ "state": "adopted",
245
+ "foundation_source_commit": foundation_source_commit,
246
+ "application_source_commit": application_source_commit,
247
+ "foundation_run_binding": foundation_run_binding,
248
+ "adopted_run_binding": adopted_run_binding,
249
+ "target_binding": target_binding,
250
+ "kit_manifest_digest": kit_manifest_digest,
251
+ "runtime_release_digest": runtime_release_digest,
252
+ "profile_digest": canonical_digest(profile.to_mapping()),
253
+ "foundation_handoff_digest": canonical_digest(handoff),
254
+ "foundation_recovery_receipt_digest": recovery["receipt_digest"],
255
+ "foundation_enrollment_receipt_digest": enrollment["receipt_digest"],
256
+ "foundation_state_receipt_digest": state_receipt["receipt_digest"],
257
+ "foundation_state_authority_digest": authority["authority_digest"],
258
+ "known_hosts_digest": hashlib.sha256(known_hosts).hexdigest(),
259
+ "managed_resource_count": state_receipt["managed_resource_count"],
260
+ "remote_backend_authority_verified": True,
261
+ "zero_change_verified": True,
262
+ "local_state_deleted": True,
263
+ "no_effect_adoption": True,
264
+ "mutation_performed": False,
265
+ "deployment_ready": False,
266
+ "subscription_ready": False,
267
+ }
268
+ receipt["receipt_digest"] = canonical_digest(receipt)
269
+ validate_foundation_adoption_receipt(
270
+ receipt,
271
+ handoff=handoff,
272
+ target_binding=target_binding,
273
+ application_source_commit=application_source_commit,
274
+ kit_manifest_digest=kit_manifest_digest,
275
+ runtime_release_digest=runtime_release_digest,
276
+ )
277
+ return receipt
278
+
279
+
280
+ def write_lineage_adoption_receipt(
281
+ *,
282
+ run_root: Path,
283
+ plan_directory: Path,
284
+ application_source_commit: str,
285
+ kit_manifest_digest: str,
286
+ runtime_release_digest: str,
287
+ tenant_id: str,
288
+ subscription_id: str,
289
+ region: str,
290
+ monthly_cost_ceiling: int,
291
+ ) -> dict[str, object]:
292
+ """Bind a retained offline Foundation to a newer kit through no-effect adoption evidence.
293
+
294
+ An offline kit upgrade continues the Foundation in its original work directory. The
295
+ managed host accepts a kit from another revision only through this receipt, rebuilt
296
+ from the retained, independently verified Foundation chain. A receipt for another kit
297
+ is derived evidence and is superseded atomically.
298
+ """
299
+
300
+ _require_private_directory(run_root)
301
+ _require_private_directory(plan_directory)
302
+ profile = ProvisionProfile.from_mapping(
303
+ load_json_object(
304
+ read_private_bytes(run_root / "profile.json", max_bytes=65_536),
305
+ label="retained Foundation profile",
306
+ )
307
+ )
308
+ _handoff_bytes, handoff = _read_record(
309
+ plan_directory / "foundation-private-handoff.json",
310
+ label="retained Foundation handoff",
311
+ )
312
+ _apply_bytes, applied = _read_receipt(
313
+ plan_directory / "foundation-apply-receipt.json",
314
+ label="retained Foundation apply receipt",
315
+ digest_field="receipt_digest",
316
+ )
317
+ _enrollment_bytes, enrollment = _read_receipt(
318
+ plan_directory / "runner-enrollment-receipt.json",
319
+ label="retained Foundation enrollment receipt",
320
+ digest_field="receipt_digest",
321
+ )
322
+ _state_bytes, state_receipt = _read_receipt(
323
+ plan_directory / "foundation-state-handoff-receipt.json",
324
+ label="retained Foundation state handoff receipt",
325
+ digest_field="receipt_digest",
326
+ )
327
+ _authority_bytes, authority = _read_receipt(
328
+ plan_directory / "foundation-state-authority.json",
329
+ label="retained Foundation state authority",
330
+ digest_field="authority_digest",
331
+ )
332
+ known_hosts = read_private_bytes(plan_directory / "runner-known-hosts", max_bytes=262_144)
333
+ receipt = build_foundation_adoption_receipt(
334
+ profile=profile,
335
+ handoff=handoff,
336
+ recovery=applied,
337
+ enrollment=enrollment,
338
+ state_receipt=state_receipt,
339
+ authority=authority,
340
+ known_hosts=known_hosts,
341
+ application_source_commit=application_source_commit,
342
+ kit_manifest_digest=kit_manifest_digest,
343
+ runtime_release_digest=runtime_release_digest,
344
+ tenant_id=tenant_id,
345
+ subscription_id=subscription_id,
346
+ region=region,
347
+ monthly_cost_ceiling=monthly_cost_ceiling,
348
+ )
349
+ if receipt["foundation_source_commit"] == application_source_commit:
350
+ raise ValueError("a same-revision Foundation needs no lineage adoption")
351
+ path = run_root / "foundation-adoption-receipt.json"
352
+ content = canonical_bytes(receipt)
353
+ if path.exists() or path.is_symlink():
354
+ if read_private_bytes(path, max_bytes=1_048_576) == content:
355
+ return receipt
356
+ path.unlink()
357
+ write_private_bytes(path, content)
358
+ return receipt
359
+
360
+
361
+ def _validate_inputs(
362
+ *,
363
+ foundation_directory: Path,
364
+ recovery_directory: Path,
365
+ destination: Path,
366
+ application_source_commit: str,
367
+ kit_manifest_digest: str,
368
+ runtime_release_digest: str,
369
+ tenant_id: str,
370
+ subscription_id: str,
371
+ region: str,
372
+ monthly_cost_ceiling: int,
373
+ ) -> None:
374
+ if not all(
375
+ path.is_absolute() for path in (foundation_directory, recovery_directory, destination)
376
+ ):
377
+ raise ValueError("Foundation adoption paths must be absolute")
378
+ foundation_resolved = foundation_directory.resolve()
379
+ recovery_resolved = recovery_directory.resolve()
380
+ destination_resolved = destination.resolve()
381
+ if (
382
+ foundation_resolved == recovery_resolved
383
+ or destination_resolved.is_relative_to(foundation_resolved)
384
+ or destination_resolved.is_relative_to(recovery_resolved)
385
+ or foundation_resolved.is_relative_to(destination_resolved)
386
+ or recovery_resolved.is_relative_to(destination_resolved)
387
+ ):
388
+ raise ValueError("Foundation adoption destination must be separate")
389
+ if (
390
+ _COMMIT.fullmatch(application_source_commit) is None
391
+ or _DIGEST.fullmatch(kit_manifest_digest) is None
392
+ or _DIGEST.fullmatch(runtime_release_digest) is None
393
+ or _GUID.fullmatch(tenant_id) is None
394
+ or _GUID.fullmatch(subscription_id) is None
395
+ or re.fullmatch(r"[a-z][a-z0-9]+", region) is None
396
+ or type(monthly_cost_ceiling) is not int
397
+ or monthly_cost_ceiling <= 0
398
+ ):
399
+ raise ValueError("Foundation adoption context is invalid")
400
+
401
+
402
+ def _read_record(path: Path, *, label: str) -> tuple[bytes, dict[str, Any]]:
403
+ raw = read_private_bytes(path, max_bytes=1_048_576)
404
+ return raw, load_json_object(raw, label=label)
405
+
406
+
407
+ def _read_receipt(path: Path, *, label: str, digest_field: str) -> tuple[bytes, dict[str, Any]]:
408
+ raw, value = _read_record(path, label=label)
409
+ digest = value.get(digest_field)
410
+ unsigned = {key: item for key, item in value.items() if key != digest_field}
411
+ if not isinstance(digest, str) or canonical_digest(unsigned) != digest:
412
+ raise ValueError(f"{label} digest differs")
413
+ return raw, value
414
+
415
+
416
+ def _create_or_validate_private_directory(path: Path) -> None:
417
+ path.mkdir(parents=True, mode=0o700, exist_ok=True)
418
+ _require_private_directory(path)
419
+
420
+
421
+ def _require_private_directory(path: Path) -> None:
422
+ if not path.is_absolute():
423
+ raise ValueError("Foundation adoption directory must be absolute")
424
+ details = path.lstat()
425
+ if (
426
+ not stat.S_ISDIR(details.st_mode)
427
+ or stat.S_IMODE(details.st_mode) != 0o700
428
+ or details.st_uid != os.geteuid()
429
+ ):
430
+ raise PermissionError("Foundation adoption directory must be current-UID mode 0700")
431
+
432
+
433
+ def _write_or_verify(path: Path, content: bytes) -> None:
434
+ if path.exists() or path.is_symlink():
435
+ if read_private_bytes(path, max_bytes=max(1, len(content))) != content:
436
+ raise ValueError("retained Foundation adoption artifact differs")
437
+ return
438
+ write_private_bytes(path, content)
@@ -0,0 +1,245 @@
1
+ """Validate recovered Foundation evidence and no-effect adoption receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from typing import Any
8
+
9
+ from fdai_deployment_cli.contracts import ProvisionProfile, canonical_digest
10
+
11
+ _COMMIT = re.compile(r"[0-9a-f]{40}")
12
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
13
+ _ADOPTION_KEYS = {
14
+ "schema_version",
15
+ "state",
16
+ "foundation_source_commit",
17
+ "application_source_commit",
18
+ "foundation_run_binding",
19
+ "adopted_run_binding",
20
+ "target_binding",
21
+ "kit_manifest_digest",
22
+ "runtime_release_digest",
23
+ "profile_digest",
24
+ "foundation_handoff_digest",
25
+ "foundation_recovery_receipt_digest",
26
+ "foundation_enrollment_receipt_digest",
27
+ "foundation_state_receipt_digest",
28
+ "foundation_state_authority_digest",
29
+ "known_hosts_digest",
30
+ "managed_resource_count",
31
+ "remote_backend_authority_verified",
32
+ "zero_change_verified",
33
+ "local_state_deleted",
34
+ "no_effect_adoption",
35
+ "mutation_performed",
36
+ "deployment_ready",
37
+ "subscription_ready",
38
+ "receipt_digest",
39
+ }
40
+
41
+
42
+ def validate_foundation_adoption_receipt(
43
+ receipt: dict[str, Any],
44
+ *,
45
+ handoff: dict[str, Any],
46
+ target_binding: str,
47
+ application_source_commit: str,
48
+ kit_manifest_digest: str,
49
+ runtime_release_digest: str,
50
+ ) -> str:
51
+ """Validate a no-effect adoption against exact Foundation and artifact inputs."""
52
+
53
+ digest = receipt.get("receipt_digest")
54
+ unsigned = {key: value for key, value in receipt.items() if key != "receipt_digest"}
55
+ managed_resource_count = receipt.get("managed_resource_count")
56
+ foundation_run_binding = handoff.get("run_digest")
57
+ foundation_source_commit = handoff.get("source_commit")
58
+ if (
59
+ set(receipt) != _ADOPTION_KEYS
60
+ or not isinstance(digest, str)
61
+ or canonical_digest(unsigned) != digest
62
+ or receipt.get("schema_version") != "fdai.foundation-adoption.v1"
63
+ or receipt.get("state") != "adopted"
64
+ or not isinstance(foundation_source_commit, str)
65
+ or _COMMIT.fullmatch(foundation_source_commit) is None
66
+ or _COMMIT.fullmatch(application_source_commit) is None
67
+ or receipt.get("foundation_source_commit") != foundation_source_commit
68
+ or receipt.get("application_source_commit") != application_source_commit
69
+ or receipt.get("foundation_run_binding") != foundation_run_binding
70
+ or receipt.get("target_binding") != target_binding
71
+ or receipt.get("kit_manifest_digest") != kit_manifest_digest
72
+ or receipt.get("runtime_release_digest") != runtime_release_digest
73
+ or receipt.get("foundation_handoff_digest") != canonical_digest(handoff)
74
+ or type(managed_resource_count) is not int
75
+ or managed_resource_count <= 0
76
+ or any(
77
+ not isinstance(receipt.get(field), str)
78
+ or _DIGEST.fullmatch(str(receipt[field])) is None
79
+ for field in (
80
+ "foundation_run_binding",
81
+ "adopted_run_binding",
82
+ "target_binding",
83
+ "kit_manifest_digest",
84
+ "runtime_release_digest",
85
+ "profile_digest",
86
+ "foundation_handoff_digest",
87
+ "foundation_recovery_receipt_digest",
88
+ "foundation_enrollment_receipt_digest",
89
+ "foundation_state_receipt_digest",
90
+ "foundation_state_authority_digest",
91
+ "known_hosts_digest",
92
+ )
93
+ )
94
+ or any(
95
+ receipt.get(field) is not True
96
+ for field in (
97
+ "remote_backend_authority_verified",
98
+ "zero_change_verified",
99
+ "local_state_deleted",
100
+ "no_effect_adoption",
101
+ )
102
+ )
103
+ or any(
104
+ receipt.get(field) is not False
105
+ for field in (
106
+ "mutation_performed",
107
+ "deployment_ready",
108
+ "subscription_ready",
109
+ )
110
+ )
111
+ ):
112
+ raise ValueError("Foundation adoption receipt differs from its exact context")
113
+ expected_run_binding = canonical_digest(
114
+ {
115
+ "foundation_run_binding": foundation_run_binding,
116
+ "application_source_commit": application_source_commit,
117
+ "kit_manifest_digest": kit_manifest_digest,
118
+ "runtime_release_digest": runtime_release_digest,
119
+ }
120
+ )
121
+ if receipt.get("adopted_run_binding") != expected_run_binding:
122
+ raise ValueError("Foundation adoption run binding differs")
123
+ return digest
124
+
125
+
126
+ def verify_foundation_chain(
127
+ *,
128
+ profile: ProvisionProfile,
129
+ handoff: dict[str, Any],
130
+ recovery: dict[str, Any],
131
+ enrollment: dict[str, Any],
132
+ state_receipt: dict[str, Any],
133
+ authority: dict[str, Any],
134
+ known_hosts: bytes,
135
+ target_binding: str,
136
+ tenant_id: str,
137
+ subscription_id: str,
138
+ region: str,
139
+ monthly_cost_ceiling: int,
140
+ ) -> tuple[str, str]:
141
+ """Verify recovered Foundation lineage, target, cleanup, and backend authority."""
142
+
143
+ if (
144
+ profile.environment != "dev"
145
+ or profile.region != region
146
+ or profile.target_binding != target_binding
147
+ or profile.connectivity not in {"online", "offline"}
148
+ or profile.host != "managed-vm"
149
+ or profile.transport != "manual"
150
+ or profile.access_method != "bastion"
151
+ or not profile.shadow_only
152
+ or profile.approval_quorum != 1
153
+ or profile.monthly_cost_ceiling != monthly_cost_ceiling
154
+ ):
155
+ raise ValueError("adopted Foundation profile differs from the selected deployment")
156
+ foundation_source_commit = handoff.get("source_commit")
157
+ foundation_run_binding = handoff.get("run_digest")
158
+ runner = handoff.get("runner")
159
+ access = handoff.get("access")
160
+ if (
161
+ not isinstance(foundation_source_commit, str)
162
+ or _COMMIT.fullmatch(foundation_source_commit) is None
163
+ or not isinstance(foundation_run_binding, str)
164
+ or _DIGEST.fullmatch(foundation_run_binding) is None
165
+ or handoff.get("tenant_id") != tenant_id
166
+ or handoff.get("subscription_id") != subscription_id
167
+ or handoff.get("region") != region
168
+ or not isinstance(runner, dict)
169
+ or runner.get("execution_transport") != "manual"
170
+ or not isinstance(access, dict)
171
+ or access.get("method") != "bastion"
172
+ ):
173
+ raise ValueError("recovered Foundation handoff context is invalid")
174
+ expected_records = (
175
+ (enrollment, "fdai.genesis-runner-enrollment-receipt.v1", "attested"),
176
+ (
177
+ state_receipt,
178
+ "fdai.genesis-foundation-state-handoff-receipt.v1",
179
+ "verified",
180
+ ),
181
+ (authority, "fdai.genesis-foundation-state-authority.v1", "verified"),
182
+ )
183
+ if any(
184
+ record.get("schema_version") != schema or record.get("state") != state
185
+ for record, schema, state in expected_records
186
+ ):
187
+ raise ValueError("Foundation adoption evidence is not terminal")
188
+ # A Foundation reaches this point either by ordinary apply or by a governed recovery.
189
+ # Both produce a terminal receipt over the same chain, so accept each exact pair and
190
+ # nothing else; an in-progress or unknown receipt still stops adoption.
191
+ if (recovery.get("schema_version"), recovery.get("state")) not in {
192
+ ("fdai.foundation-recovery-receipt.v1", "verified"),
193
+ ("fdai.genesis-foundation-apply-receipt.v1", "applied"),
194
+ }:
195
+ raise ValueError("Foundation adoption evidence is not terminal")
196
+ if any(
197
+ record.get("source_commit") != foundation_source_commit
198
+ for record in (recovery, enrollment, state_receipt, authority)
199
+ ):
200
+ raise ValueError("Foundation adoption source lineage differs")
201
+ if canonical_digest(handoff) != recovery.get("handoff_digest"):
202
+ raise ValueError("Foundation recovery does not bind the private handoff")
203
+ if (
204
+ enrollment.get("foundation_receipt_digest") != recovery.get("receipt_digest")
205
+ or state_receipt.get("foundation_receipt_digest") != recovery.get("receipt_digest")
206
+ or state_receipt.get("enrollment_receipt_digest") != enrollment.get("receipt_digest")
207
+ ):
208
+ raise ValueError("Foundation adoption receipt chain differs")
209
+ if any(
210
+ record.get("target_binding") != target_binding
211
+ for record in (enrollment, state_receipt, authority)
212
+ ):
213
+ raise ValueError("Foundation adoption target binding differs")
214
+ if (
215
+ state_receipt.get("authority_digest") != authority.get("authority_digest")
216
+ or state_receipt.get("state_digest") != authority.get("state_digest")
217
+ or state_receipt.get("managed_resource_count") != authority.get("managed_resource_count")
218
+ ):
219
+ raise ValueError("Foundation state authority differs from its handoff receipt")
220
+ managed_resource_count = state_receipt.get("managed_resource_count")
221
+ if type(managed_resource_count) is not int or managed_resource_count <= 0:
222
+ raise ValueError("Foundation adoption managed resource count is invalid")
223
+ required_true = (
224
+ (recovery, "control_plane_readback_verified"),
225
+ (recovery, "zero_change_verified"),
226
+ (enrollment, "effect_verified"),
227
+ (enrollment, "identity_attested"),
228
+ (enrollment, "manual_host_readback_verified"),
229
+ (enrollment, "services_attested"),
230
+ (state_receipt, "effect_verified"),
231
+ (state_receipt, "runner_attested"),
232
+ (state_receipt, "remote_backend_authority_verified"),
233
+ (state_receipt, "zero_change_verified"),
234
+ (state_receipt, "local_state_deletion_authorized"),
235
+ (state_receipt, "local_state_deleted"),
236
+ (state_receipt, "remote_transient_deleted"),
237
+ (authority, "remote_backend_authority_verified"),
238
+ (authority, "zero_change_verified"),
239
+ (authority, "local_state_deletion_authorized"),
240
+ )
241
+ if any(record.get(field) is not True for record, field in required_true):
242
+ raise ValueError("Foundation adoption evidence is incomplete")
243
+ if hashlib.sha256(known_hosts).hexdigest() != enrollment.get("host_key_digest"):
244
+ raise ValueError("Foundation enrollment host key evidence differs")
245
+ return foundation_source_commit, foundation_run_binding