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,82 @@
1
+ """Stage local runtime release bytes before the enclosing kit is signed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from tempfile import TemporaryDirectory
8
+
9
+ from fdai_deployment_cli.offline_kit import (
10
+ _MAX_FILE_BYTES,
11
+ MANIFEST_NAME,
12
+ SIGNATURE_NAME,
13
+ _copy_verified_file,
14
+ _scan_tree,
15
+ _sha256_nofollow,
16
+ )
17
+ from fdai_deployment_cli.private_output import _open_private_parent
18
+ from fdai_deployment_cli.runtime_release import load_runtime_release, validate_runtime_images
19
+
20
+
21
+ def stage_runtime_release(
22
+ source: Path,
23
+ kit: Path,
24
+ *,
25
+ deployment_bundle: Path,
26
+ source_commit: str,
27
+ platform_tag: str,
28
+ ) -> str:
29
+ """Copy a complete local inventory without registry access or image execution.
30
+
31
+ V2 inventories also require valid service and ClamAV OCI content. Legacy v1
32
+ remains stageable for inspection, not complete preparation. This is release
33
+ assembly, not a signature or production-eligibility check. The caller supplies
34
+ a private kit directory and signs it after staging.
35
+ """
36
+ directory = _open_private_parent(kit / "runtime")
37
+ os.close(directory)
38
+ if any(
39
+ (kit / name).exists() or (kit / name).is_symlink()
40
+ for name in (MANIFEST_NAME, SIGNATURE_NAME)
41
+ ):
42
+ raise ValueError("runtime staging requires an unsigned kit")
43
+ release = load_runtime_release(
44
+ source,
45
+ expected_source_commit=source_commit,
46
+ expected_platform_tag=platform_tag,
47
+ )
48
+ bundle_details = deployment_bundle.lstat()
49
+ if bundle_details.st_size > _MAX_FILE_BYTES:
50
+ raise ValueError("deployment bundle exceeds the offline kit file size limit")
51
+ if release.deployment_bundle_sha256 != _sha256_nofollow(
52
+ deployment_bundle, expected=bundle_details
53
+ ):
54
+ raise ValueError("runtime inventory does not match the deployment bundle")
55
+ destination = kit / "runtime"
56
+ if destination.exists() or destination.is_symlink():
57
+ raise ValueError("runtime release destination already exists")
58
+ digests, sizes, _total = _scan_tree(source / "runtime")
59
+ with TemporaryDirectory(prefix=".runtime-", dir=kit) as temporary:
60
+ staging = Path(temporary)
61
+ payload = staging / "runtime"
62
+ payload.mkdir(mode=0o700)
63
+ for relative, digest in digests.items():
64
+ target = payload / relative
65
+ target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
66
+ _copy_verified_file(
67
+ source / "runtime" / relative,
68
+ target,
69
+ expected_digest=digest,
70
+ expected_size=sizes[relative],
71
+ )
72
+ copied = load_runtime_release(
73
+ staging,
74
+ expected_source_commit=source_commit,
75
+ expected_platform_tag=platform_tag,
76
+ )
77
+ if copied.digest != release.digest:
78
+ raise ValueError("runtime inventory changed during staging")
79
+ if copied.schema_version in {"fdai.runtime-release.v2", "fdai.runtime-release.v3"}:
80
+ validate_runtime_images(staging, copied)
81
+ payload.rename(destination)
82
+ return copied.digest
@@ -0,0 +1,215 @@
1
+ """Install verified runtime support from one signed aggregate lock."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from fdai_deployment_cli.contracts import canonical_digest, load_json_object
14
+ from fdai_deployment_cli.offline_kit import _read_regular as _read_kit_regular
15
+ from fdai_deployment_cli.private_output import write_private_output
16
+ from fdai_deployment_cli.standalone_host_state import private_json as _private_json
17
+ from fdai_deployment_cli.support_install import (
18
+ _validate_requirements as _validate_support_requirements,
19
+ )
20
+
21
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
22
+
23
+
24
+ def install_runtime_support(
25
+ work_dir: Path, *, artifact_root: Path, kit_manifest_digest: str
26
+ ) -> None:
27
+ """Install the admitted hashed support lock and verify resumable completion."""
28
+
29
+ if _DIGEST.fullmatch(kit_manifest_digest) is None:
30
+ raise ValueError("runtime migration support kit binding is invalid")
31
+ support = artifact_root / "support/python"
32
+ inventory_path = support / "inventory.json"
33
+ requirements = support / "requirements/support.txt"
34
+ try:
35
+ inventory_bytes = _read_kit_regular(inventory_path, 4 * 1024 * 1024)
36
+ requirement_bytes = _read_kit_regular(requirements, 8 * 1024 * 1024)
37
+ except (OSError, UnicodeError, ValueError) as exc:
38
+ raise ValueError("runtime migration support contract is unavailable") from exc
39
+ inventory = load_json_object(
40
+ inventory_bytes,
41
+ label="runtime support inventory",
42
+ max_bytes=4 * 1024 * 1024,
43
+ )
44
+ _validate_support_requirements(requirement_bytes)
45
+ if (
46
+ inventory.get("schema_version") != "fdai.runtime-wheelhouse.v1"
47
+ or inventory.get("artifact_kind") != "local-runtime-wheelhouse"
48
+ or inventory.get("status") != "complete"
49
+ or inventory.get("support_requirements") != "requirements/support.txt"
50
+ ):
51
+ raise ValueError("runtime migration support inventory is incomplete")
52
+ files = _mapping(inventory.get("files"), "runtime support file inventory")
53
+ expected_versions: dict[str, str] = {}
54
+ requirement_lines = set(requirement_bytes.decode("utf-8").splitlines())
55
+ for field in ("packages", "support_packages"):
56
+ records = _mapping(inventory.get(field), f"runtime support {field}")
57
+ for name, item in records.items():
58
+ record = _mapping(item, f"runtime support package {name}")
59
+ version, wheel = record.get("version"), record.get("wheel")
60
+ digest = files.get(wheel) if isinstance(wheel, str) else None
61
+ normalized = re.sub(r"[-_.]+", "-", name.casefold())
62
+ if (
63
+ not normalized
64
+ or not isinstance(version, str)
65
+ or not isinstance(wheel, str)
66
+ or not isinstance(digest, str)
67
+ or _DIGEST.fullmatch(digest) is None
68
+ or f"{name}=={version} --hash=sha256:{digest}" not in requirement_lines
69
+ or normalized in expected_versions
70
+ ):
71
+ raise ValueError("runtime migration support package contract is invalid")
72
+ expected_versions[normalized] = version
73
+ wheels = sorted(support.rglob("*.whl"))
74
+ if not wheels or any(not wheel.is_file() or wheel.is_symlink() for wheel in wheels):
75
+ raise ValueError("runtime migration support wheelhouse is invalid")
76
+ locations = sorted({wheel.parent for wheel in wheels}, key=str)
77
+ if not 1 <= len(locations) <= 64:
78
+ raise ValueError("runtime migration support wheel locations are invalid")
79
+
80
+ environment = work_dir / "runtime-venv"
81
+ receipt_path = work_dir / "runtime-support-installation.json"
82
+ retained_environment = environment.exists() or environment.is_symlink()
83
+ retained_receipt = receipt_path.exists() or receipt_path.is_symlink()
84
+ if retained_environment or retained_receipt:
85
+ if not retained_environment or not retained_receipt:
86
+ raise ValueError("runtime migration support installation is incomplete")
87
+ receipt = _private_json(receipt_path, "runtime support installation receipt")
88
+ if (
89
+ receipt.get("schema_version") != "fdai.runtime-support-installation.v1"
90
+ or receipt.get("kit_manifest_digest") != kit_manifest_digest
91
+ or receipt.get("inventory_digest") != hashlib.sha256(inventory_bytes).hexdigest()
92
+ or receipt.get("requirements_digest") != hashlib.sha256(requirement_bytes).hexdigest()
93
+ or receipt.get("dependencies_verified") is not True
94
+ ):
95
+ raise ValueError("runtime migration support receipt differs")
96
+ readback_digest = _runtime_support_readback(
97
+ environment, expected_versions=expected_versions, work_dir=work_dir
98
+ )
99
+ if receipt.get("package_readback_digest") != readback_digest:
100
+ raise ValueError("runtime migration support package readback differs")
101
+ return
102
+
103
+ _run(
104
+ (sys.executable, "-m", "venv", str(environment)),
105
+ cwd=work_dir,
106
+ timeout=120,
107
+ reason="runtime migration environment creation failed",
108
+ )
109
+ links = tuple(
110
+ argument for location in locations for argument in ("--find-links", str(location))
111
+ )
112
+ _run(
113
+ (
114
+ str(environment / "bin/pip"),
115
+ "install",
116
+ "--no-index",
117
+ "--no-cache-dir",
118
+ "--only-binary",
119
+ ":all:",
120
+ "--require-hashes",
121
+ *links,
122
+ "--requirement",
123
+ str(requirements),
124
+ ),
125
+ cwd=work_dir,
126
+ timeout=900,
127
+ reason="runtime migration support installation failed",
128
+ )
129
+ readback_digest = _runtime_support_readback(
130
+ environment, expected_versions=expected_versions, work_dir=work_dir
131
+ )
132
+ write_private_output(
133
+ receipt_path,
134
+ json.dumps(
135
+ {
136
+ "schema_version": "fdai.runtime-support-installation.v1",
137
+ "kit_manifest_digest": kit_manifest_digest,
138
+ "inventory_digest": hashlib.sha256(inventory_bytes).hexdigest(),
139
+ "requirements_digest": hashlib.sha256(requirement_bytes).hexdigest(),
140
+ "package_readback_digest": readback_digest,
141
+ "dependencies_verified": True,
142
+ },
143
+ sort_keys=True,
144
+ separators=(",", ":"),
145
+ )
146
+ + "\n",
147
+ )
148
+
149
+
150
+ def _runtime_support_readback(
151
+ environment: Path, *, expected_versions: dict[str, str], work_dir: Path
152
+ ) -> str:
153
+ pip = str(environment / "bin/pip")
154
+ _run(
155
+ (pip, "check"),
156
+ cwd=work_dir,
157
+ timeout=120,
158
+ reason="runtime migration support dependency check failed",
159
+ )
160
+ try:
161
+ entries = json.loads(
162
+ _capture(
163
+ (pip, "list", "--format=json"),
164
+ cwd=work_dir,
165
+ timeout=120,
166
+ reason="runtime migration support package readback failed",
167
+ )
168
+ )
169
+ except json.JSONDecodeError as exc:
170
+ raise ValueError("runtime migration support package readback is invalid") from exc
171
+ if not isinstance(entries, list):
172
+ raise ValueError("runtime migration support package readback is invalid")
173
+ observed: dict[str, str] = {}
174
+ for entry in entries:
175
+ if not isinstance(entry, dict):
176
+ raise ValueError("runtime migration support package readback is invalid")
177
+ name, version = entry.get("name"), entry.get("version")
178
+ if not isinstance(name, str) or not isinstance(version, str):
179
+ raise ValueError("runtime migration support package readback is invalid")
180
+ normalized = re.sub(r"[-_.]+", "-", name.casefold())
181
+ if normalized in observed:
182
+ raise ValueError("runtime migration support package readback contains duplicates")
183
+ observed[normalized] = version
184
+ if any(observed.get(name) != version for name, version in expected_versions.items()):
185
+ raise ValueError("runtime migration support packages differ from the signed inventory")
186
+ return canonical_digest(observed)
187
+
188
+
189
+ def _run(command: tuple[str, ...], *, cwd: Path, timeout: int, reason: str) -> None:
190
+ result = subprocess.run(command, cwd=cwd, check=False, capture_output=True, timeout=timeout)
191
+ if result.returncode != 0:
192
+ raise ValueError(reason)
193
+
194
+
195
+ def _capture(command: tuple[str, ...], *, cwd: Path, timeout: int, reason: str) -> str:
196
+ result = subprocess.run(
197
+ command,
198
+ cwd=cwd,
199
+ check=False,
200
+ capture_output=True,
201
+ text=True,
202
+ timeout=timeout,
203
+ )
204
+ if result.returncode != 0:
205
+ raise ValueError(reason)
206
+ return result.stdout
207
+
208
+
209
+ def _mapping(value: object, label: str) -> dict[str, Any]:
210
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
211
+ raise ValueError(f"{label} is invalid")
212
+ return {str(key): item for key, item in value.items()}
213
+
214
+
215
+ __all__ = ["install_runtime_support"]
@@ -0,0 +1,80 @@
1
+ """Deterministic no-mutation rehearsal of the genesis state machine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+ from pathlib import Path
7
+
8
+ from fdai_deployment_cli.contracts import SubscriptionProvisioningManifest, canonical_digest
9
+ from fdai_deployment_cli.state import (
10
+ GENESIS_HASH,
11
+ ProvisionEvent,
12
+ RunState,
13
+ append_event,
14
+ read_journal,
15
+ )
16
+
17
+
18
+ def rehearse(
19
+ manifest: SubscriptionProvisioningManifest,
20
+ *,
21
+ run_id: str,
22
+ journal: Path,
23
+ interrupt_after: str | None = None,
24
+ started_at: datetime | None = None,
25
+ ) -> tuple[ProvisionEvent, ...]:
26
+ """Rehearse remaining stages and optionally stop after one completed stage."""
27
+
28
+ events = read_journal(journal) if journal.exists() else ()
29
+ if events and events[-1].run_id != run_id:
30
+ raise ValueError("simulation run_id does not match the existing journal")
31
+ if events and events[-1].context_digest != manifest.digest:
32
+ raise ValueError("simulation manifest does not match the existing journal")
33
+ if events and events[-1].state is RunState.READY:
34
+ return events
35
+ known_stages = {entry.entry_id for entry in manifest.entries}
36
+ if interrupt_after is not None and interrupt_after not in known_stages:
37
+ raise ValueError("simulation interrupt stage is not in the manifest")
38
+ completed = {event.stage for event in events if event.state is RunState.COMPLETED}
39
+ previous_digest = events[-1].digest if events else GENESIS_HASH
40
+ sequence = len(events)
41
+ base = started_at or datetime.now(UTC)
42
+ for entry in manifest.entries:
43
+ if entry.entry_id in completed:
44
+ continue
45
+ sequence += 1
46
+ event = ProvisionEvent(
47
+ run_id=run_id,
48
+ context_digest=manifest.digest,
49
+ sequence=sequence,
50
+ stage=entry.entry_id,
51
+ attempt=1,
52
+ state=RunState.COMPLETED,
53
+ occurred_at=(base + timedelta(microseconds=sequence)).isoformat(),
54
+ previous_digest=previous_digest,
55
+ reason_code="simulation-only",
56
+ evidence_digest=canonical_digest(
57
+ {"mode": "simulation", "stage": entry.entry_id, "status": "completed"}
58
+ ),
59
+ )
60
+ append_event(journal, event)
61
+ previous_digest = event.digest
62
+ if entry.entry_id == interrupt_after:
63
+ return read_journal(journal)
64
+ sequence += 1
65
+ ready = ProvisionEvent(
66
+ run_id=run_id,
67
+ context_digest=manifest.digest,
68
+ sequence=sequence,
69
+ stage="system-readiness",
70
+ attempt=1,
71
+ state=RunState.READY,
72
+ occurred_at=(base + timedelta(microseconds=sequence)).isoformat(),
73
+ previous_digest=previous_digest,
74
+ reason_code="simulation-only",
75
+ evidence_digest=canonical_digest(
76
+ {"mode": "simulation", "stage": "system-readiness", "status": "ready"}
77
+ ),
78
+ )
79
+ append_event(journal, ready)
80
+ return read_journal(journal)