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,568 @@
1
+ """Contracts for one bounded legacy native-transition state reconciliation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import binascii
7
+ import re
8
+ from collections import Counter
9
+ from datetime import UTC, datetime, timedelta
10
+ from typing import Annotated, Any, Literal, Self
11
+ from uuid import UUID
12
+
13
+ from cryptography.exceptions import InvalidSignature
14
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
15
+ Ed25519PrivateKey,
16
+ Ed25519PublicKey,
17
+ )
18
+ from pydantic import (
19
+ AwareDatetime,
20
+ BaseModel,
21
+ BeforeValidator,
22
+ ConfigDict,
23
+ Field,
24
+ PlainSerializer,
25
+ StringConstraints,
26
+ model_validator,
27
+ )
28
+
29
+ from fdai_deployment_cli.contracts import canonical_bytes, canonical_digest
30
+ from fdai_deployment_cli.lifecycle_native import Digest, NativeApproval, PathId
31
+ from fdai_deployment_cli.lifecycle_native_transition import (
32
+ HexDigest,
33
+ NativeScopeTransitionCompletionReceipt,
34
+ NativeScopeTransitionEntityState,
35
+ NativeScopeTransitionReportedState,
36
+ hub_key_id,
37
+ )
38
+
39
+ Actor = Annotated[str, StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9@._ -]{0,127}$")]
40
+
41
+
42
+ def _serialize_reported_state_timestamp(value: datetime) -> str:
43
+ timespec = "milliseconds" if value.microsecond % 1000 == 0 else "microseconds"
44
+ return value.astimezone(UTC).isoformat(timespec=timespec).replace("+00:00", "Z")
45
+
46
+
47
+ LegacyReportedStateTimestamp = Annotated[
48
+ AwareDatetime,
49
+ PlainSerializer(
50
+ _serialize_reported_state_timestamp,
51
+ return_type=str,
52
+ ),
53
+ ]
54
+
55
+ _IMMUTABLE_RECEIPT_TIMESTAMP_PATTERN = re.compile(
56
+ (
57
+ r"(?P<instant>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:"
58
+ r"[0-9]{2}\.(?:[0-9]{3}|[0-9]{6}))(?P<zone>Z|\+00:00)"
59
+ ),
60
+ flags=re.ASCII,
61
+ )
62
+
63
+
64
+ class LegacyNativeScopeTransitionReportedState(BaseModel):
65
+ """Exact legacy state whose artifact inventory retains its stored sequence."""
66
+
67
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
68
+
69
+ digest: HexDigest
70
+ schema_revision: Annotated[int, Field(ge=0, le=2**31 - 1)]
71
+ entities: dict[PathId, NativeScopeTransitionEntityState]
72
+ observed_at: LegacyReportedStateTimestamp
73
+ available_artifact_digests: tuple[Digest, ...] | None = None
74
+
75
+ @model_validator(mode="after")
76
+ def validate_stored_digest(self) -> Self:
77
+ document = self.model_dump(mode="json", exclude={"digest"})
78
+ if self.available_artifact_digests is None:
79
+ document.pop("available_artifact_digests")
80
+ if self.digest != canonical_digest(document):
81
+ raise ValueError("legacy transition reported state digest is invalid")
82
+ return self
83
+
84
+
85
+ def _strict_reported_state(value: object) -> object:
86
+ if isinstance(value, dict):
87
+ return NativeScopeTransitionReportedState.model_validate_json(canonical_bytes(value))
88
+ return value
89
+
90
+
91
+ def _legacy_reported_state(value: object) -> object:
92
+ if isinstance(value, dict):
93
+ return LegacyNativeScopeTransitionReportedState.model_validate_json(canonical_bytes(value))
94
+ return value
95
+
96
+
97
+ def _native_approval(value: object) -> object:
98
+ if isinstance(value, dict):
99
+ return NativeApproval.model_validate_json(canonical_bytes(value))
100
+ return value
101
+
102
+
103
+ def _reconciliation_request(value: object) -> object:
104
+ if isinstance(value, dict):
105
+ model = (
106
+ NativeScopeTransitionStateReconciliationRenewalRequest
107
+ if value.get("schema_version")
108
+ == "fdai.lifecycle-native-transition-state-reconciliation-renewal-request.v1"
109
+ else NativeScopeTransitionStateReconciliationRequest
110
+ )
111
+ return model.model_validate_json(canonical_bytes(value))
112
+ return value
113
+
114
+
115
+ def _reconciliation_approval(value: object) -> object:
116
+ if isinstance(value, dict):
117
+ return NativeScopeTransitionStateReconciliationApproval.model_validate_json(
118
+ canonical_bytes(value)
119
+ )
120
+ return value
121
+
122
+
123
+ def _signed_reconciliation_approval(value: object) -> object:
124
+ if isinstance(value, dict):
125
+ return SignedNativeScopeTransitionStateReconciliationApproval.model_validate_json(
126
+ canonical_bytes(value)
127
+ )
128
+ return value
129
+
130
+
131
+ StrictReportedStateReadback = Annotated[
132
+ NativeScopeTransitionReportedState,
133
+ BeforeValidator(_strict_reported_state),
134
+ ]
135
+ LegacyReportedStateReadback = Annotated[
136
+ LegacyNativeScopeTransitionReportedState,
137
+ BeforeValidator(_legacy_reported_state),
138
+ ]
139
+
140
+
141
+ def _immutable_receipt_timestamp(value: object) -> object:
142
+ if isinstance(value, str):
143
+ if _IMMUTABLE_RECEIPT_TIMESTAMP_PATTERN.fullmatch(value) is None:
144
+ raise ValueError("immutable transition timestamp must use exact UTC precision")
145
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
146
+ if isinstance(value, datetime) and value.utcoffset() != timedelta(0):
147
+ raise ValueError("immutable transition timestamp must use UTC")
148
+ return value
149
+
150
+
151
+ ImmutableReceiptTimestamp = Annotated[
152
+ AwareDatetime,
153
+ BeforeValidator(_immutable_receipt_timestamp),
154
+ PlainSerializer(
155
+ _serialize_reported_state_timestamp,
156
+ return_type=str,
157
+ ),
158
+ ]
159
+
160
+
161
+ def _immutable_receipt_record_digests(
162
+ document: dict[str, Any],
163
+ ) -> tuple[str, str] | None:
164
+ """Digest the same exact UTC receipt with its legacy and Hub timestamp spellings."""
165
+
166
+ completed_at = document.get("completed_at")
167
+ if not isinstance(completed_at, str):
168
+ return None
169
+ match = _IMMUTABLE_RECEIPT_TIMESTAMP_PATTERN.fullmatch(completed_at)
170
+ if match is None:
171
+ return None
172
+ instant = datetime.fromisoformat(completed_at.replace("Z", "+00:00"))
173
+ if instant.utcoffset() != timedelta(0):
174
+ return None
175
+ timestamp = match.group("instant")
176
+ legacy_document = {**document, "completed_at": timestamp + "+00:00"}
177
+ hub_document = {**document, "completed_at": timestamp + "Z"}
178
+ return (
179
+ "sha256:" + canonical_digest(legacy_document),
180
+ "sha256:" + canonical_digest(hub_document),
181
+ )
182
+
183
+
184
+ def transition_reported_state_document(
185
+ state: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
186
+ *,
187
+ observed_at: datetime | None = None,
188
+ milliseconds: bool = False,
189
+ ) -> dict[str, object]:
190
+ """Return canonical state fields with an explicitly selected timestamp representation."""
191
+
192
+ timestamp = (observed_at or state.observed_at).astimezone(UTC)
193
+ document: dict[str, object] = {
194
+ "schema_revision": state.schema_revision,
195
+ "entities": {
196
+ entity_id: {
197
+ "release_id": entity.release_id,
198
+ "artifact_digests": list(entity.artifact_digests),
199
+ "health": entity.health,
200
+ }
201
+ for entity_id, entity in sorted(state.entities.items())
202
+ },
203
+ "observed_at": (
204
+ timestamp.isoformat(timespec="milliseconds") if milliseconds else timestamp.isoformat()
205
+ ).replace("+00:00", "Z"),
206
+ }
207
+ if state.available_artifact_digests is not None:
208
+ document["available_artifact_digests"] = list(state.available_artifact_digests)
209
+ return document
210
+
211
+
212
+ def transition_reported_state_digest(
213
+ state: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
214
+ *,
215
+ observed_at: datetime | None = None,
216
+ milliseconds: bool = False,
217
+ ) -> str:
218
+ return canonical_digest(
219
+ transition_reported_state_document(
220
+ state,
221
+ observed_at=observed_at,
222
+ milliseconds=milliseconds,
223
+ )
224
+ )
225
+
226
+
227
+ def same_transition_state_fields(
228
+ left: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
229
+ right: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
230
+ ) -> bool:
231
+ """Compare non-time fields while preserving exact artifact multiplicity."""
232
+
233
+ left_artifacts = left.available_artifact_digests
234
+ right_artifacts = right.available_artifact_digests
235
+ return (
236
+ left.schema_revision == right.schema_revision
237
+ and left.entities == right.entities
238
+ and (
239
+ left_artifacts is None
240
+ and right_artifacts is None
241
+ or left_artifacts is not None
242
+ and right_artifacts is not None
243
+ and Counter(left_artifacts) == Counter(right_artifacts)
244
+ )
245
+ )
246
+
247
+
248
+ def same_transition_state_semantic_inventory(
249
+ left: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
250
+ right: LegacyNativeScopeTransitionReportedState | NativeScopeTransitionReportedState,
251
+ ) -> bool:
252
+ """Compare a legacy source with its deduplicated authoritative target."""
253
+
254
+ left_artifacts = left.available_artifact_digests
255
+ right_artifacts = right.available_artifact_digests
256
+ return (
257
+ left.schema_revision == right.schema_revision
258
+ and left.entities == right.entities
259
+ and left.observed_at == right.observed_at
260
+ and (
261
+ left_artifacts is None
262
+ and right_artifacts is None
263
+ or left_artifacts is not None
264
+ and right_artifacts is not None
265
+ and set(left_artifacts) == set(right_artifacts)
266
+ )
267
+ )
268
+
269
+
270
+ def canonical_transition_reported_state(
271
+ source: LegacyNativeScopeTransitionReportedState,
272
+ ) -> NativeScopeTransitionReportedState:
273
+ """Build the strict target without mutating or normalizing the legacy source."""
274
+
275
+ inventory = source.available_artifact_digests
276
+ document = source.model_dump(mode="python")
277
+ document["digest"] = "0" * 64
278
+ document["observed_at"] = source.observed_at
279
+ document["available_artifact_digests"] = (
280
+ None if inventory is None else tuple(sorted(set(inventory)))
281
+ )
282
+ provisional = NativeScopeTransitionReportedState.model_validate(document)
283
+ document["digest"] = transition_reported_state_digest(provisional)
284
+ return NativeScopeTransitionReportedState.model_validate(document)
285
+
286
+
287
+ def _require_strict_target_inventory(state: NativeScopeTransitionReportedState) -> None:
288
+ inventory = state.available_artifact_digests
289
+ if inventory is not None and tuple(sorted(set(inventory))) != inventory:
290
+ raise ValueError("authoritative transition artifacts must be unique and sorted")
291
+
292
+
293
+ class LegacyNativeScopeTransitionReceipt(BaseModel):
294
+ """Immutable pre-fix receipt whose stored bytes retain their original precision."""
295
+
296
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
297
+
298
+ schema_version: Literal["fdai.lifecycle-native-transition-receipt.v1"]
299
+ request_digest: Digest
300
+ hub_approval_digest: Digest
301
+ installation_id: PathId
302
+ old_entity_id: PathId
303
+ new_entity_id: PathId
304
+ old_scope_digest: Digest
305
+ new_scope_digest: Digest
306
+ old_reported_state_digest: HexDigest
307
+ new_reported_state_digest: HexDigest
308
+ ownership_claim_digest: Digest
309
+ recovery_request_digest: Digest
310
+ existing_resources_digest: Digest
311
+ rollback_record_digest: Digest
312
+ completed_at: ImmutableReceiptTimestamp
313
+ record_digest: Digest
314
+
315
+ @model_validator(mode="before")
316
+ @classmethod
317
+ def validate_record_digest(cls, value: Any) -> Any:
318
+ if not isinstance(value, dict):
319
+ return value
320
+ document = dict(value)
321
+ digest = document.pop("record_digest", None)
322
+ expected = _immutable_receipt_record_digests(document)
323
+ if expected is None:
324
+ raise ValueError("legacy native transition receipt digest is invalid")
325
+ legacy_digest, hub_digest = expected
326
+ serialized_digest = (
327
+ hub_digest if str(document["completed_at"]).endswith("Z") else legacy_digest
328
+ )
329
+ if digest != legacy_digest or serialized_digest != "sha256:" + canonical_digest(document):
330
+ raise ValueError("legacy native transition receipt digest is invalid")
331
+ return value
332
+
333
+
334
+ def regenerated_transition_receipt_digest(
335
+ receipt: LegacyNativeScopeTransitionReceipt,
336
+ *,
337
+ new_reported_state_digest: str,
338
+ ) -> str:
339
+ """Reproduce the Hub transition receipt without changing the immutable receipt."""
340
+
341
+ document = receipt.model_dump(mode="json", exclude={"record_digest"})
342
+ document["new_reported_state_digest"] = new_reported_state_digest
343
+ return "sha256:" + canonical_digest(document)
344
+
345
+
346
+ class _NativeScopeTransitionStateReconciliationRequest(BaseModel):
347
+ """Common immutable evidence and bounded timing for approval requests."""
348
+
349
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
350
+
351
+ request_id: UUID
352
+ transition_receipt: LegacyNativeScopeTransitionReceipt
353
+ completion_receipt: NativeScopeTransitionCompletionReceipt
354
+ native_approval: Annotated[NativeApproval, BeforeValidator(_native_approval)]
355
+ legacy_reported_state: LegacyReportedStateReadback
356
+ requested_at: AwareDatetime
357
+ expires_at: AwareDatetime
358
+ confirmation: Annotated[str, StringConstraints(min_length=1, max_length=1024)]
359
+
360
+ @model_validator(mode="after")
361
+ def validate_window(self) -> Self:
362
+ if not self.requested_at < self.expires_at <= self.requested_at + timedelta(minutes=10):
363
+ raise ValueError("native transition reconciliation window is invalid")
364
+ return self
365
+
366
+ @property
367
+ def installation_id(self) -> str:
368
+ return self.transition_receipt.installation_id
369
+
370
+ @property
371
+ def digest(self) -> str:
372
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
373
+
374
+ def require_current(self, now: datetime) -> None:
375
+ if now.utcoffset() is None or not self.requested_at <= now < self.expires_at:
376
+ raise ValueError("native transition reconciliation request is not current")
377
+
378
+
379
+ class NativeScopeTransitionStateReconciliationRequest(
380
+ _NativeScopeTransitionStateReconciliationRequest
381
+ ):
382
+ """Operator request carrying all immutable legacy and Hub completion evidence."""
383
+
384
+ schema_version: Literal["fdai.lifecycle-native-transition-state-reconciliation-request.v1"]
385
+
386
+
387
+ class NativeScopeTransitionStateReconciliationRenewalRequest(
388
+ _NativeScopeTransitionStateReconciliationRequest
389
+ ):
390
+ """Fresh bounded request that supersedes one expired unconsumed approval."""
391
+
392
+ schema_version: Literal[
393
+ "fdai.lifecycle-native-transition-state-reconciliation-renewal-request.v1"
394
+ ]
395
+ supersedes_approval_digest: Digest
396
+
397
+
398
+ type NativeScopeTransitionStateReconciliationApprovalRequest = (
399
+ NativeScopeTransitionStateReconciliationRequest
400
+ | NativeScopeTransitionStateReconciliationRenewalRequest
401
+ )
402
+
403
+
404
+ def reconciliation_request_evidence_digest(
405
+ request: NativeScopeTransitionStateReconciliationApprovalRequest,
406
+ ) -> str:
407
+ """Digest every request field that renewal is not allowed to change."""
408
+
409
+ document = request.model_dump(mode="json")
410
+ for field in (
411
+ "schema_version",
412
+ "request_id",
413
+ "requested_at",
414
+ "expires_at",
415
+ "supersedes_approval_digest",
416
+ ):
417
+ document.pop(field, None)
418
+ return "sha256:" + canonical_digest(document)
419
+
420
+
421
+ class NativeScopeTransitionStateReconciliationApproval(BaseModel):
422
+ """Hub-authoritative state correction signed after completion-audit verification."""
423
+
424
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
425
+
426
+ schema_version: Literal["fdai.lifecycle-native-transition-state-reconciliation-approval.v1"]
427
+ request: Annotated[
428
+ NativeScopeTransitionStateReconciliationApprovalRequest,
429
+ BeforeValidator(_reconciliation_request),
430
+ ]
431
+ source_hub_reported_state: LegacyReportedStateReadback
432
+ authoritative_reported_state: StrictReportedStateReadback
433
+ approved_by: Actor
434
+ approved_at: AwareDatetime
435
+ expires_at: AwareDatetime
436
+ completion_audit_digest: Digest
437
+ hub_key_id: Annotated[str, StringConstraints(pattern=r"^hub-[0-9a-f]{16}$")]
438
+ hub_key_epoch: Annotated[int, Field(ge=1)]
439
+
440
+ @model_validator(mode="after")
441
+ def validate_window(self) -> Self:
442
+ _require_strict_target_inventory(self.authoritative_reported_state)
443
+ if (
444
+ self.expires_at != self.request.expires_at
445
+ or not self.request.requested_at <= self.approved_at < self.expires_at
446
+ ):
447
+ raise ValueError("native transition reconciliation approval window is invalid")
448
+ return self
449
+
450
+ @property
451
+ def digest(self) -> str:
452
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
453
+
454
+ @property
455
+ def payload(self) -> bytes:
456
+ return b"fdai.lifecycle-native-transition-state-reconciliation-approval.v1\n" + (
457
+ canonical_bytes(self.model_dump(mode="json"))
458
+ )
459
+
460
+ def require_current(self, now: datetime) -> None:
461
+ self.request.require_current(now)
462
+ if now.utcoffset() is None or not self.approved_at <= now < self.expires_at:
463
+ raise ValueError("native transition reconciliation approval is not current")
464
+
465
+
466
+ class SignedNativeScopeTransitionStateReconciliationApproval(BaseModel):
467
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
468
+
469
+ approval: Annotated[
470
+ NativeScopeTransitionStateReconciliationApproval,
471
+ BeforeValidator(_reconciliation_approval),
472
+ ]
473
+ hub_signature: Annotated[str, StringConstraints(min_length=88, max_length=88)]
474
+
475
+ @model_validator(mode="after")
476
+ def validate_signature_encoding(self) -> Self:
477
+ _signature_bytes(self.hub_signature)
478
+ return self
479
+
480
+ def verify(self, public_key: bytes, *, key_id: str, key_epoch: int) -> bool:
481
+ approval = self.approval
482
+ if (
483
+ approval.hub_key_id != key_id
484
+ or approval.hub_key_epoch != key_epoch
485
+ or hub_key_id(public_key) != key_id
486
+ ):
487
+ return False
488
+ try:
489
+ Ed25519PublicKey.from_public_bytes(public_key).verify(
490
+ _signature_bytes(self.hub_signature),
491
+ approval.payload,
492
+ )
493
+ except (InvalidSignature, ValueError):
494
+ return False
495
+ return True
496
+
497
+
498
+ class NativeScopeTransitionStateReconciliationCompletion(BaseModel):
499
+ """Agent-authenticated proof that only its durable current-state file was corrected."""
500
+
501
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
502
+
503
+ schema_version: Literal["fdai.lifecycle-native-transition-state-reconciliation-completion.v1"]
504
+ approval: Annotated[
505
+ SignedNativeScopeTransitionStateReconciliationApproval,
506
+ BeforeValidator(_signed_reconciliation_approval),
507
+ ]
508
+ reported_state: NativeScopeTransitionReportedState
509
+
510
+ @model_validator(mode="after")
511
+ def validate_target_inventory(self) -> Self:
512
+ _require_strict_target_inventory(self.reported_state)
513
+ return self
514
+
515
+ @property
516
+ def digest(self) -> str:
517
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
518
+
519
+
520
+ class NativeScopeTransitionStateReconciliationReceipt(BaseModel):
521
+ """Hub readback retained unchanged by both Hub and Agent for exact retries."""
522
+
523
+ model_config = ConfigDict(extra="forbid", frozen=True)
524
+
525
+ schema_version: Literal["fdai.lifecycle-native-transition-state-reconciliation-receipt.v1"]
526
+ completion_digest: Digest
527
+ request_digest: Digest
528
+ approval_digest: Digest
529
+ transition_receipt_digest: Digest
530
+ hub_completion_receipt_digest: Digest
531
+ installation_id: PathId
532
+ old_reported_state_digest: HexDigest
533
+ new_reported_state_digest: HexDigest
534
+ agent_principal: Actor
535
+ authoritative_reported_state: StrictReportedStateReadback
536
+ completed_at: AwareDatetime
537
+ receipt_digest: Digest
538
+
539
+ @model_validator(mode="before")
540
+ @classmethod
541
+ def validate_receipt_digest(cls, value: Any) -> Any:
542
+ if not isinstance(value, dict):
543
+ return value
544
+ document = dict(value)
545
+ digest = document.pop("receipt_digest", None)
546
+ if digest != "sha256:" + canonical_digest(document):
547
+ raise ValueError("native transition reconciliation receipt digest is invalid")
548
+ return value
549
+
550
+
551
+ def sign_transition_state_reconciliation_approval(
552
+ approval: NativeScopeTransitionStateReconciliationApproval,
553
+ key: Ed25519PrivateKey,
554
+ ) -> SignedNativeScopeTransitionStateReconciliationApproval:
555
+ return SignedNativeScopeTransitionStateReconciliationApproval(
556
+ approval=approval,
557
+ hub_signature=base64.b64encode(key.sign(approval.payload)).decode("ascii"),
558
+ )
559
+
560
+
561
+ def _signature_bytes(value: str) -> bytes:
562
+ try:
563
+ decoded = base64.b64decode(value, validate=True)
564
+ except (ValueError, binascii.Error) as error:
565
+ raise ValueError("Hub signature encoding is invalid") from error
566
+ if len(decoded) != 64 or base64.b64encode(decoded).decode("ascii") != value:
567
+ raise ValueError("Hub signature must contain canonical Ed25519 bytes")
568
+ return decoded