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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- 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
|