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,339 @@
1
+ """Signed authorization for one installed legacy-to-native workload scope transition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import binascii
7
+ import hashlib
8
+ from datetime import datetime, timedelta
9
+ from typing import Annotated, Any, Literal, Self
10
+ from uuid import UUID
11
+
12
+ from cryptography.exceptions import InvalidSignature
13
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
14
+ Ed25519PrivateKey,
15
+ Ed25519PublicKey,
16
+ )
17
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, StringConstraints, model_validator
18
+
19
+ from fdai_deployment_cli.contracts import canonical_bytes, canonical_digest
20
+ from fdai_deployment_cli.lifecycle_enrollment import installation_key_id
21
+ from fdai_deployment_cli.lifecycle_native import (
22
+ Digest,
23
+ NativeApproval,
24
+ PathId,
25
+ )
26
+
27
+ type HexDigest = Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{64}$")]
28
+ type InstallationKeyId = Annotated[str, StringConstraints(pattern=r"^installation-[0-9a-f]{16}$")]
29
+ type HubKeyId = Annotated[str, StringConstraints(pattern=r"^hub-[0-9a-f]{16}$")]
30
+ _MAX_REQUEST_LIFETIME = timedelta(minutes=10)
31
+
32
+
33
+ def existing_resources_digest(resources: object) -> str:
34
+ """Bind the canonical kind/name/UID inventory without exposing it to the Hub."""
35
+
36
+ return "sha256:" + canonical_digest(resources)
37
+
38
+
39
+ class NativeScopeTransitionRequest(BaseModel):
40
+ """Installation-signed request for a Hub decision, not permission to change local state."""
41
+
42
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
43
+
44
+ schema_version: Literal["fdai.lifecycle-native-transition-request.v1"]
45
+ request_id: UUID
46
+ installation_id: PathId
47
+ old_entity_id: PathId
48
+ new_entity_id: PathId
49
+ old_scope_digest: Digest
50
+ new_scope_digest: Digest
51
+ old_reported_state_digest: HexDigest
52
+ ownership_claim_digest: Digest
53
+ recovery_request_digest: Digest
54
+ recovered_identity_key_id: InstallationKeyId
55
+ existing_resources_digest: Digest
56
+ requested_at: AwareDatetime
57
+ expires_at: AwareDatetime
58
+ confirmation: Annotated[str, Field(min_length=1, max_length=512)]
59
+
60
+ @model_validator(mode="after")
61
+ def validate_transition(self) -> Self:
62
+ if (
63
+ self.old_entity_id == self.new_entity_id
64
+ or self.old_scope_digest == self.new_scope_digest
65
+ or not timedelta(0) < self.expires_at - self.requested_at <= _MAX_REQUEST_LIFETIME
66
+ or self.confirmation != self.required_confirmation
67
+ ):
68
+ raise ValueError("native transition request is invalid")
69
+ return self
70
+
71
+ @property
72
+ def required_confirmation(self) -> str:
73
+ return (
74
+ f"transition native scope {self.installation_id} "
75
+ f"{self.old_scope_digest} {self.new_scope_digest}"
76
+ )
77
+
78
+ @property
79
+ def payload(self) -> bytes:
80
+ return canonical_bytes(self.model_dump(mode="json"))
81
+
82
+ @property
83
+ def digest(self) -> str:
84
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
85
+
86
+ def require_current(self, now: datetime) -> None:
87
+ if now.utcoffset() is None or not self.requested_at <= now < self.expires_at:
88
+ raise ValueError("native transition request is not current")
89
+
90
+
91
+ class SignedNativeScopeTransitionRequest(BaseModel):
92
+ """Proof that the recovered installation identity requested the exact transition."""
93
+
94
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
95
+
96
+ request: NativeScopeTransitionRequest
97
+ installation_proof: Annotated[str, StringConstraints(min_length=88, max_length=88)]
98
+
99
+ @model_validator(mode="after")
100
+ def validate_proof_encoding(self) -> Self:
101
+ _signature_bytes(self.installation_proof, label="installation proof")
102
+ return self
103
+
104
+ def verify(self, public_key: bytes) -> bool:
105
+ if installation_key_id(public_key) != self.request.recovered_identity_key_id:
106
+ return False
107
+ try:
108
+ Ed25519PublicKey.from_public_bytes(public_key).verify(
109
+ _signature_bytes(self.installation_proof, label="installation proof"),
110
+ self.request.payload,
111
+ )
112
+ except (InvalidSignature, ValueError):
113
+ return False
114
+ return True
115
+
116
+
117
+ class NativeScopeTransitionApproval(BaseModel):
118
+ """Hub decision over recovery, accepted ownership, state, and the absence of an open Plan."""
119
+
120
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
121
+
122
+ schema_version: Literal["fdai.lifecycle-native-transition-approval.v1"]
123
+ request: SignedNativeScopeTransitionRequest
124
+ profile: Literal["full-authority-development"]
125
+ approved_by: Annotated[str, StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9@._ -]{0,127}$")]
126
+ approved_at: AwareDatetime
127
+ expires_at: AwareDatetime
128
+ recovery_audit_digest: Digest
129
+ ownership_audit_digest: Digest
130
+ hub_key_id: HubKeyId
131
+ hub_key_epoch: Annotated[int, Field(ge=1)]
132
+
133
+ @model_validator(mode="after")
134
+ def validate_approval(self) -> Self:
135
+ if (
136
+ self.approved_at < self.request.request.requested_at
137
+ or self.expires_at != self.request.request.expires_at
138
+ ):
139
+ raise ValueError("native transition approval interval is invalid")
140
+ return self
141
+
142
+ @property
143
+ def payload(self) -> bytes:
144
+ return canonical_bytes(self.model_dump(mode="json"))
145
+
146
+ @property
147
+ def digest(self) -> str:
148
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
149
+
150
+ def require_current(self, now: datetime) -> None:
151
+ if now.utcoffset() is None or not self.approved_at <= now < self.expires_at:
152
+ raise ValueError("native transition approval is not current")
153
+
154
+
155
+ class SignedNativeScopeTransitionApproval(BaseModel):
156
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
157
+
158
+ approval: NativeScopeTransitionApproval
159
+ hub_signature: Annotated[str, StringConstraints(min_length=88, max_length=88)]
160
+
161
+ @model_validator(mode="after")
162
+ def validate_signature_encoding(self) -> Self:
163
+ _signature_bytes(self.hub_signature, label="Hub signature")
164
+ return self
165
+
166
+ def verify(self, public_key: bytes, *, key_id: str, key_epoch: int) -> bool:
167
+ approval = self.approval
168
+ if (
169
+ approval.hub_key_id != key_id
170
+ or approval.hub_key_epoch != key_epoch
171
+ or hub_key_id(public_key) != key_id
172
+ ):
173
+ return False
174
+ try:
175
+ Ed25519PublicKey.from_public_bytes(public_key).verify(
176
+ _signature_bytes(self.hub_signature, label="Hub signature"),
177
+ approval.payload,
178
+ )
179
+ except (InvalidSignature, ValueError):
180
+ return False
181
+ return True
182
+
183
+
184
+ class NativeScopeTransitionReceipt(BaseModel):
185
+ """Agent-local commit proof for the crash-safe native scope replacement."""
186
+
187
+ model_config = ConfigDict(extra="forbid", frozen=True)
188
+
189
+ schema_version: Literal["fdai.lifecycle-native-transition-receipt.v1"]
190
+ request_digest: Digest
191
+ hub_approval_digest: Digest
192
+ installation_id: PathId
193
+ old_entity_id: PathId
194
+ new_entity_id: PathId
195
+ old_scope_digest: Digest
196
+ new_scope_digest: Digest
197
+ old_reported_state_digest: HexDigest
198
+ new_reported_state_digest: HexDigest
199
+ ownership_claim_digest: Digest
200
+ recovery_request_digest: Digest
201
+ existing_resources_digest: Digest
202
+ rollback_record_digest: Digest
203
+ completed_at: AwareDatetime
204
+ record_digest: Digest
205
+
206
+ @model_validator(mode="before")
207
+ @classmethod
208
+ def validate_record_digest(cls, value: Any) -> Any:
209
+ if not isinstance(value, dict):
210
+ return value
211
+ document = dict(value)
212
+ digest = document.pop("record_digest", None)
213
+ if digest != "sha256:" + canonical_digest(document):
214
+ raise ValueError("native transition receipt digest is invalid")
215
+ return value
216
+
217
+
218
+ class NativeScopeTransitionEntityState(BaseModel):
219
+ """The Entity portion of the state committed by the local transition."""
220
+
221
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
222
+
223
+ release_id: Annotated[str, StringConstraints(min_length=1, max_length=64)] | None
224
+ artifact_digests: tuple[Digest, ...]
225
+ health: Literal["healthy", "degraded", "unhealthy", "unknown"]
226
+
227
+ @model_validator(mode="after")
228
+ def validate_uninstalled_state(self) -> Self:
229
+ if self.release_id is None and (self.artifact_digests or self.health == "healthy"):
230
+ raise ValueError("an uninstalled transition Entity cannot report artifacts or health")
231
+ if tuple(sorted(set(self.artifact_digests))) != self.artifact_digests:
232
+ raise ValueError("transition artifact digests must be unique and sorted")
233
+ return self
234
+
235
+
236
+ class NativeScopeTransitionReportedState(BaseModel):
237
+ """Exact Hub state snapshot produced by the local transition."""
238
+
239
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
240
+
241
+ digest: HexDigest
242
+ schema_revision: Annotated[int, Field(ge=0, le=2**31 - 1)]
243
+ entities: dict[PathId, NativeScopeTransitionEntityState]
244
+ observed_at: AwareDatetime
245
+ available_artifact_digests: tuple[Digest, ...] | None = None
246
+
247
+ @model_validator(mode="after")
248
+ def validate_artifact_inventory(self) -> Self:
249
+ if self.available_artifact_digests is not None and (
250
+ tuple(sorted(set(self.available_artifact_digests))) != self.available_artifact_digests
251
+ ):
252
+ raise ValueError("available transition artifacts must be unique and sorted")
253
+ return self
254
+
255
+
256
+ class NativeScopeTransitionCompletion(BaseModel):
257
+ """Authenticated Agent claim that the approved local transition committed."""
258
+
259
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
260
+
261
+ schema_version: Literal["fdai.lifecycle-native-transition-completion.v1"]
262
+ approval: SignedNativeScopeTransitionApproval
263
+ transition_receipt: NativeScopeTransitionReceipt
264
+ native_approval: NativeApproval
265
+ reported_state: NativeScopeTransitionReportedState
266
+
267
+ @property
268
+ def digest(self) -> str:
269
+ return "sha256:" + canonical_digest(self.model_dump(mode="json"))
270
+
271
+
272
+ class NativeScopeTransitionCompletionReceipt(BaseModel):
273
+ """Durable Hub result returned unchanged for an exact completion retry."""
274
+
275
+ model_config = ConfigDict(extra="forbid", frozen=True)
276
+
277
+ schema_version: Literal["fdai.lifecycle-native-transition-completion-receipt.v1"]
278
+ completion_digest: Digest
279
+ request_digest: Digest
280
+ hub_approval_digest: Digest
281
+ transition_receipt_digest: Digest
282
+ installation_id: PathId
283
+ old_entity_id: PathId
284
+ new_entity_id: PathId
285
+ old_scope_digest: Digest
286
+ new_scope_digest: Digest
287
+ old_reported_state_digest: HexDigest
288
+ new_reported_state_digest: HexDigest
289
+ object_scope_digest: Digest
290
+ authoritative_uids_digest: Digest
291
+ source_settings_digest: HexDigest
292
+ agent_principal: Annotated[
293
+ str, StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9@._ -]{0,127}$")
294
+ ]
295
+ completed_at: AwareDatetime
296
+ receipt_digest: Digest
297
+
298
+ @model_validator(mode="before")
299
+ @classmethod
300
+ def validate_receipt_digest(cls, value: Any) -> Any:
301
+ if not isinstance(value, dict):
302
+ return value
303
+ document = dict(value)
304
+ digest = document.pop("receipt_digest", None)
305
+ if digest != "sha256:" + canonical_digest(document):
306
+ raise ValueError("native transition completion receipt digest is invalid")
307
+ return value
308
+
309
+
310
+ def sign_transition_request(
311
+ request: NativeScopeTransitionRequest, key: Ed25519PrivateKey
312
+ ) -> SignedNativeScopeTransitionRequest:
313
+ return SignedNativeScopeTransitionRequest(
314
+ request=request,
315
+ installation_proof=base64.b64encode(key.sign(request.payload)).decode("ascii"),
316
+ )
317
+
318
+
319
+ def sign_transition_approval(
320
+ approval: NativeScopeTransitionApproval, key: Ed25519PrivateKey
321
+ ) -> SignedNativeScopeTransitionApproval:
322
+ return SignedNativeScopeTransitionApproval(
323
+ approval=approval,
324
+ hub_signature=base64.b64encode(key.sign(approval.payload)).decode("ascii"),
325
+ )
326
+
327
+
328
+ def hub_key_id(public_key: bytes) -> str:
329
+ return "hub-" + hashlib.sha256(public_key).hexdigest()[:16]
330
+
331
+
332
+ def _signature_bytes(value: str, *, label: str) -> bytes:
333
+ try:
334
+ decoded = base64.b64decode(value, validate=True)
335
+ except (ValueError, binascii.Error) as error:
336
+ raise ValueError(f"{label} encoding is invalid") from error
337
+ if len(decoded) != 64 or base64.b64encode(decoded).decode("ascii") != value:
338
+ raise ValueError(f"{label} must contain canonical Ed25519 bytes")
339
+ return decoded
@@ -0,0 +1,78 @@
1
+ """Foundation ownership verification and operations-loop target exclusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass, field
8
+ from typing import Literal
9
+
10
+ from fdai_deployment_cli.contracts import canonical_bytes
11
+
12
+ _PREFIXED_SHA256 = re.compile(r"sha256:[0-9a-f]{64}\Z", re.ASCII)
13
+
14
+
15
+ @dataclass(frozen=True, slots=True, kw_only=True)
16
+ class SignedFoundationOwnershipReceipt:
17
+ """Signed Foundation creation receipt paired with Terraform state identity."""
18
+
19
+ resource_ref: str
20
+ foundation_receipt_digest: str
21
+ terraform_state_digest: str
22
+ signature: bytes
23
+
24
+ def __post_init__(self) -> None:
25
+ if not self.resource_ref.strip():
26
+ raise ValueError("resource_ref MUST NOT be empty")
27
+ if _PREFIXED_SHA256.fullmatch(self.foundation_receipt_digest) is None:
28
+ raise ValueError("foundation_receipt_digest MUST be sha256:<64 hex>")
29
+ if _PREFIXED_SHA256.fullmatch(self.terraform_state_digest) is None:
30
+ raise ValueError("terraform_state_digest MUST be sha256:<64 hex>")
31
+
32
+ def canonical_payload(self) -> bytes:
33
+ return canonical_bytes(
34
+ {
35
+ "schema_version": "fdai.foundation-ownership-receipt.v1",
36
+ "resource_ref": self.resource_ref,
37
+ "foundation_receipt_digest": self.foundation_receipt_digest,
38
+ "terraform_state_digest": self.terraform_state_digest,
39
+ }
40
+ )
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class ProvenFoundationOwnershipRegistry:
45
+ """Registry of resources with verified signed Foundation ownership receipts.
46
+
47
+ A manual ``fdai:managed=true`` tag without a verified signed Foundation receipt and
48
+ matching Terraform state digest is never admitted into this registry.
49
+ """
50
+
51
+ receipts_by_resource: Mapping[str, SignedFoundationOwnershipReceipt] = field(
52
+ default_factory=dict
53
+ )
54
+
55
+ def register_verified(
56
+ self,
57
+ receipt: SignedFoundationOwnershipReceipt,
58
+ *,
59
+ verify_signature: Callable[[bytes, bytes], bool],
60
+ ) -> ProvenFoundationOwnershipRegistry:
61
+ if not receipt.signature or not bool(
62
+ verify_signature(receipt.canonical_payload(), receipt.signature)
63
+ ):
64
+ raise ValueError("foundation_ownership_signature_invalid")
65
+ updated = dict(self.receipts_by_resource)
66
+ updated[receipt.resource_ref] = receipt
67
+ return ProvenFoundationOwnershipRegistry(receipts_by_resource=updated)
68
+
69
+ def is_proven_fdai_owned(self, resource_ref: str) -> bool:
70
+ return resource_ref in self.receipts_by_resource
71
+
72
+ def upstream_risk_gate_signal(self, target_resource_ref: str) -> Literal["deny"] | None:
73
+ """Return ``"deny"`` for RiskGate.evaluate when an operations-loop action targets a
74
+ proven FDAI-owned resource."""
75
+
76
+ if self.is_proven_fdai_owned(target_resource_ref):
77
+ return "deny"
78
+ return None