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,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
|