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,368 @@
1
+ """Install source-deployment runtime support from the committed workspace lock.
2
+
3
+ A signed kit carries a prebuilt runtime wheelhouse. A source deployment has no such artifact, so
4
+ the workstation builds the snapshot's pure-Python workspace wheels, exports the committed
5
+ ``uv.lock`` closure as hashed requirements, and transfers both as one digest-bound archive. The
6
+ managed host installs the third-party closure by hash and then the workspace wheels by hash
7
+ without any index or build step, so no unpinned build backend ever runs on the host.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import io
14
+ import json
15
+ import os
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import tarfile
21
+ import tempfile
22
+ import tomllib
23
+ from pathlib import Path, PurePosixPath
24
+
25
+ from fdai_deployment_cli.contracts import canonical_digest
26
+ from fdai_deployment_cli.private_output import (
27
+ read_private_bytes,
28
+ write_private_bytes,
29
+ write_private_output,
30
+ )
31
+ from fdai_deployment_cli.standalone_host_state import private_json as _private_json
32
+
33
+ # Kept equal to the runtime and support package sets of
34
+ # scripts/deployment/release/stage-runtime-wheelhouse.py by a consistency test.
35
+ WORKSPACE_PACKAGES = {
36
+ "fdai-service-contracts": "packages/service-contracts",
37
+ "fdai-core-control-plane": "services/core-control-plane",
38
+ "fdai-operator-service": "services/operator-service",
39
+ "fdai-document-ingestion-api": "services/document-ingestion-api",
40
+ "fdai-document-processing-worker": "services/document-processing-worker",
41
+ "fdai-isolated-executor-service": "services/isolated-executor",
42
+ "fdai-github-app-auth": "packages/github-app-auth",
43
+ "fdai-runtime-diagnostics": "packages/runtime-diagnostics",
44
+ }
45
+ INPUT_NAME = "source-runtime-support.tar"
46
+ _RECEIPT = "runtime-support-installation.json"
47
+ _SCHEMA = "fdai.source-runtime-support-installation.v2"
48
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
49
+ _MAX_REQUIREMENTS = 4 * 1024 * 1024
50
+ _MAX_ARCHIVE = 256 * 1024 * 1024
51
+ _WHEEL = re.compile(r"(?P<name>[A-Za-z0-9_.]+)-(?P<version>[^-]+)-py3-none-any\.whl")
52
+
53
+
54
+ def prepare_runtime_support_input(source_tree: Path, prepared_root: Path) -> str:
55
+ """Write the digest-bound runtime support archive and return its SHA-256 digest."""
56
+ uv = os.environ.get("UV") or shutil.which("uv")
57
+ if not uv:
58
+ raise ValueError("source deployment requires uv to prepare runtime support")
59
+ versions = _workspace_versions(source_tree)
60
+ with tempfile.TemporaryDirectory(prefix=".runtime-support-", dir=prepared_root) as scratch:
61
+ root = Path(scratch)
62
+ requirements = _export_requirements(uv, source_tree, root / "uv-cache")
63
+ # Build from a private copy so the verified snapshot is never written.
64
+ tree = root / "tree"
65
+ shutil.copytree(source_tree, tree, symlinks=True)
66
+ wheels = root / "wheels"
67
+ wheels.mkdir(mode=0o700)
68
+ members: dict[str, bytes] = {"requirements.txt": requirements}
69
+ pins = []
70
+ for name in WORKSPACE_PACKAGES:
71
+ _run(
72
+ (uv, "build", "--wheel", "--package", name, "--out-dir", str(wheels), "--quiet"),
73
+ tree,
74
+ 600,
75
+ "workspace wheel build",
76
+ )
77
+ for name, version in versions.items():
78
+ wheel = _single_wheel(wheels, name, version)
79
+ content = wheel.read_bytes()
80
+ members[f"wheels/{wheel.name}"] = content
81
+ pins.append(f"{name}=={version} --hash=sha256:{hashlib.sha256(content).hexdigest()}")
82
+ members["workspace.txt"] = ("\n".join(pins) + "\n").encode()
83
+ archive = _deterministic_tar(members)
84
+ destination = prepared_root / INPUT_NAME
85
+ destination.unlink(missing_ok=True)
86
+ write_private_bytes(destination, archive)
87
+ return hashlib.sha256(archive).hexdigest()
88
+
89
+
90
+ def _export_requirements(uv: str, source_tree: Path, cache: Path) -> bytes:
91
+ packages = tuple(argument for name in WORKSPACE_PACKAGES for argument in ("--package", name))
92
+ try:
93
+ result = subprocess.run(
94
+ (
95
+ uv,
96
+ "export",
97
+ "--directory",
98
+ str(source_tree),
99
+ "--locked",
100
+ "--offline",
101
+ *packages,
102
+ "--no-dev",
103
+ "--no-default-groups",
104
+ "--no-emit-workspace",
105
+ "--format",
106
+ "requirements-txt",
107
+ "--no-header",
108
+ "--no-annotate",
109
+ "--quiet",
110
+ ),
111
+ check=False,
112
+ capture_output=True,
113
+ timeout=120,
114
+ env={**os.environ, "UV_CACHE_DIR": str(cache), "UV_NO_PROGRESS": "1"},
115
+ )
116
+ except (OSError, subprocess.SubprocessError) as exc:
117
+ raise ValueError("locked runtime closure export failed") from exc
118
+ content = result.stdout
119
+ if result.returncode != 0 or not content or len(content) > _MAX_REQUIREMENTS:
120
+ raise ValueError("locked runtime closure export failed")
121
+ _validate_requirements(content)
122
+ return content
123
+
124
+
125
+ def _single_wheel(directory: Path, name: str, version: str) -> Path:
126
+ normalized = name.replace("-", "_")
127
+ matches = [
128
+ path
129
+ for path in directory.iterdir()
130
+ if (match := _WHEEL.fullmatch(path.name)) is not None
131
+ and match["name"] == normalized
132
+ and match["version"] == version
133
+ ]
134
+ if len(matches) != 1 or not matches[0].is_file() or matches[0].is_symlink():
135
+ raise ValueError(f"workspace package {name} did not build one pure-Python wheel")
136
+ return matches[0]
137
+
138
+
139
+ def _deterministic_tar(members: dict[str, bytes]) -> bytes:
140
+ buffer = io.BytesIO()
141
+ with tarfile.open(fileobj=buffer, mode="w", format=tarfile.PAX_FORMAT) as archive:
142
+ for name in sorted(members):
143
+ info = tarfile.TarInfo(name)
144
+ info.size = len(members[name])
145
+ info.mode = 0o600
146
+ archive.addfile(info, io.BytesIO(members[name]))
147
+ return buffer.getvalue()
148
+
149
+
150
+ def _read_input(archive: Path, digest: str) -> dict[str, bytes]:
151
+ content = read_private_bytes(archive, max_bytes=_MAX_ARCHIVE)
152
+ if hashlib.sha256(content).hexdigest() != digest:
153
+ raise ValueError("source runtime support input differs from the transferred digest")
154
+ members: dict[str, bytes] = {}
155
+ try:
156
+ with tarfile.open(fileobj=io.BytesIO(content), mode="r:") as bundle:
157
+ for member in bundle.getmembers():
158
+ path = PurePosixPath(member.name)
159
+ allowed = path.as_posix() in {"requirements.txt", "workspace.txt"} or (
160
+ len(path.parts) == 2
161
+ and path.parts[0] == "wheels"
162
+ and _WHEEL.fullmatch(path.parts[1]) is not None
163
+ )
164
+ if not member.isreg() or not allowed or member.name in members:
165
+ raise ValueError("source runtime support input member is invalid")
166
+ extracted = bundle.extractfile(member)
167
+ if extracted is None:
168
+ raise ValueError("source runtime support input member is invalid")
169
+ members[member.name] = extracted.read()
170
+ except tarfile.TarError as exc:
171
+ raise ValueError("source runtime support input is invalid") from exc
172
+ if "requirements.txt" not in members or "workspace.txt" not in members:
173
+ raise ValueError("source runtime support input is incomplete")
174
+ return members
175
+
176
+
177
+ def _validate_pins(content: bytes, members: dict[str, bytes], expected: dict[str, str]) -> None:
178
+ lines = content.decode("utf-8").splitlines()
179
+ pins = {}
180
+ for line in lines:
181
+ match = re.fullmatch(r"([a-z0-9-]+)==(\S+) --hash=sha256:([0-9a-f]{64})", line)
182
+ if match is None or match[1] in pins:
183
+ raise ValueError("source runtime workspace pins are invalid")
184
+ pins[match[1]] = (match[2], match[3])
185
+ wheel_digests = {
186
+ hashlib.sha256(value).hexdigest()
187
+ for name, value in members.items()
188
+ if name != "workspace.txt"
189
+ }
190
+ if {name: version for name, (version, _) in pins.items()} != expected or any(
191
+ digest not in wheel_digests for _, digest in pins.values()
192
+ ):
193
+ raise ValueError("source runtime workspace pins differ from the snapshot")
194
+
195
+
196
+ def install_source_runtime_support(
197
+ work_dir: Path,
198
+ *,
199
+ source_root: Path,
200
+ archive: Path,
201
+ archive_digest: str,
202
+ snapshot_digest: str,
203
+ ) -> None:
204
+ """Install the hashed lock closure, then the hashed workspace wheels, and verify them."""
205
+ if _DIGEST.fullmatch(archive_digest) is None or _DIGEST.fullmatch(snapshot_digest) is None:
206
+ raise ValueError("source runtime support binding is invalid")
207
+ members = _read_input(archive, archive_digest)
208
+ _validate_requirements(members["requirements.txt"])
209
+ expected = _workspace_versions(source_root)
210
+ _validate_pins(members["workspace.txt"], members, expected)
211
+ environment = work_dir / "runtime-venv"
212
+ receipt_path = work_dir / _RECEIPT
213
+ if _retained_matches(
214
+ environment,
215
+ receipt_path,
216
+ snapshot_digest=snapshot_digest,
217
+ input_digest=archive_digest,
218
+ expected=expected,
219
+ work_dir=work_dir,
220
+ ):
221
+ return
222
+ # The environment is disposable tooling; any partial or stale one is rebuilt.
223
+ receipt_path.unlink(missing_ok=True)
224
+ for stale in (environment, work_dir / "runtime-support-input"):
225
+ if stale.is_symlink() or stale.is_file():
226
+ stale.unlink()
227
+ elif stale.exists():
228
+ shutil.rmtree(stale)
229
+ unpacked = work_dir / "runtime-support-input"
230
+ (unpacked / "wheels").mkdir(mode=0o700, parents=True)
231
+ unpacked.chmod(0o700)
232
+ for name, content in members.items():
233
+ write_private_bytes(unpacked / name, content)
234
+ _run((sys.executable, "-m", "venv", str(environment)), work_dir, 120, "environment creation")
235
+ pip = str(environment / "bin/pip")
236
+ base = (pip, "install", "--no-cache-dir", "--disable-pip-version-check", "--require-hashes")
237
+ _run(
238
+ (*base, "--only-binary", ":all:", "--requirement", str(unpacked / "requirements.txt")),
239
+ work_dir,
240
+ 1200,
241
+ "locked dependency installation",
242
+ )
243
+ _run(
244
+ (
245
+ *base,
246
+ "--no-index",
247
+ "--no-deps",
248
+ "--only-binary",
249
+ ":all:",
250
+ "--find-links",
251
+ str(unpacked / "wheels"),
252
+ "--requirement",
253
+ str(unpacked / "workspace.txt"),
254
+ ),
255
+ work_dir,
256
+ 600,
257
+ "workspace package installation",
258
+ )
259
+ write_private_output(
260
+ receipt_path,
261
+ json.dumps(
262
+ {
263
+ "schema_version": _SCHEMA,
264
+ "snapshot_digest": snapshot_digest,
265
+ "input_digest": archive_digest,
266
+ "package_readback_digest": _readback(
267
+ environment, expected=expected, work_dir=work_dir
268
+ ),
269
+ "dependencies_verified": True,
270
+ },
271
+ sort_keys=True,
272
+ separators=(",", ":"),
273
+ )
274
+ + "\n",
275
+ )
276
+
277
+
278
+ def _retained_matches(
279
+ environment: Path,
280
+ receipt_path: Path,
281
+ *,
282
+ snapshot_digest: str,
283
+ input_digest: str,
284
+ expected: dict[str, str],
285
+ work_dir: Path,
286
+ ) -> bool:
287
+ if not receipt_path.is_file() or environment.is_symlink() or not environment.is_dir():
288
+ return False
289
+ try:
290
+ receipt = _private_json(receipt_path, "source runtime support receipt")
291
+ return (
292
+ receipt.get("schema_version") == _SCHEMA
293
+ and receipt.get("snapshot_digest") == snapshot_digest
294
+ and receipt.get("input_digest") == input_digest
295
+ and receipt.get("dependencies_verified") is True
296
+ and receipt.get("package_readback_digest")
297
+ == _readback(environment, expected=expected, work_dir=work_dir)
298
+ )
299
+ except (OSError, ValueError):
300
+ return False
301
+
302
+
303
+ def _validate_requirements(content: bytes) -> None:
304
+ try:
305
+ text = content.decode("utf-8")
306
+ except UnicodeDecodeError as exc:
307
+ raise ValueError("source runtime requirements are not UTF-8") from exc
308
+ entries = [line for line in text.splitlines() if line and not line.startswith((" ", "#"))]
309
+ if not entries or any(
310
+ line.startswith(("-", ".", "/")) or "://" in line or "==" not in line for line in entries
311
+ ):
312
+ raise ValueError("source runtime requirements must pin index packages only")
313
+ if "--hash=sha256:" not in text:
314
+ raise ValueError("source runtime requirements must carry hashes")
315
+
316
+
317
+ def _workspace_versions(source_root: Path) -> dict[str, str]:
318
+ versions = {}
319
+ for name, path in WORKSPACE_PACKAGES.items():
320
+ try:
321
+ project = tomllib.loads((source_root / path / "pyproject.toml").read_text("utf-8"))[
322
+ "project"
323
+ ]
324
+ except (OSError, KeyError, tomllib.TOMLDecodeError) as exc:
325
+ raise ValueError("source runtime workspace package is unavailable") from exc
326
+ if project.get("name") != name or not isinstance(project.get("version"), str):
327
+ raise ValueError("source runtime workspace package metadata is invalid")
328
+ versions[name] = project["version"]
329
+ return versions
330
+
331
+
332
+ def _readback(environment: Path, *, expected: dict[str, str], work_dir: Path) -> str:
333
+ pip = str(environment / "bin/pip")
334
+ _run((pip, "check"), work_dir, 120, "dependency check")
335
+ try:
336
+ result = subprocess.run(
337
+ (pip, "list", "--format=json", "--disable-pip-version-check"),
338
+ cwd=work_dir,
339
+ check=False,
340
+ capture_output=True,
341
+ timeout=120,
342
+ )
343
+ entries = json.loads(result.stdout) if result.returncode == 0 else None
344
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
345
+ entries = None
346
+ if not isinstance(entries, list):
347
+ raise ValueError("source runtime support package readback failed")
348
+ observed: dict[str, str] = {}
349
+ for entry in entries:
350
+ name = entry.get("name") if isinstance(entry, dict) else None
351
+ version = entry.get("version") if isinstance(entry, dict) else None
352
+ if not isinstance(name, str) or not isinstance(version, str):
353
+ raise ValueError("source runtime support package readback is invalid")
354
+ observed[re.sub(r"[-_.]+", "-", name.casefold())] = version
355
+ if any(observed.get(name) != version for name, version in expected.items()):
356
+ raise ValueError("source runtime workspace packages differ from the snapshot")
357
+ return canonical_digest(observed)
358
+
359
+
360
+ def _run(command: tuple[str, ...], work_dir: Path, timeout: int, step: str) -> None:
361
+ try:
362
+ result = subprocess.run(
363
+ command, cwd=work_dir, check=False, capture_output=True, timeout=timeout
364
+ )
365
+ except (OSError, subprocess.SubprocessError) as exc:
366
+ raise ValueError(f"source runtime support {step} failed") from exc
367
+ if result.returncode != 0:
368
+ raise ValueError(f"source runtime support {step} failed")