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,418 @@
1
+ """Receive one digest-bound execution bundle inside an eligible WSL host."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import shutil
11
+ import stat
12
+ import sys
13
+ import tarfile
14
+ from collections.abc import Sequence
15
+ from pathlib import Path, PurePosixPath
16
+
17
+ from fdai_deployment_cli.contracts import canonical_bytes, canonical_digest
18
+ from fdai_deployment_cli.private_output import read_private_bytes, write_private_bytes
19
+
20
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
21
+ _OPERATION_ID = re.compile(r"[a-z0-9][a-z0-9-]{0,127}")
22
+ _ROOT = "fdai-execution"
23
+ _MANIFEST = f"{_ROOT}/manifest.json"
24
+ _RECEIPT = ".fdai-execution-receipt.json"
25
+ _RETAINED_MANIFEST = ".fdai-execution-manifest.json"
26
+ _MAX_ARCHIVE_BYTES = 512 * 1024 * 1024
27
+ _MAX_FILE_BYTES = 512 * 1024 * 1024
28
+ _MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024
29
+ _MAX_MEMBERS = 20_000
30
+
31
+
32
+ def receive_execution_bundle(
33
+ archive: Path | None,
34
+ destination: Path,
35
+ *,
36
+ expected_digest: str,
37
+ expected_operation_id: str,
38
+ claim_digest: str = "0" * 64,
39
+ verify_existing: bool = False,
40
+ ) -> dict[str, object]:
41
+ """Verify and atomically extract one fixed-inventory execution bundle."""
42
+
43
+ if _DIGEST.fullmatch(expected_digest) is None:
44
+ raise ValueError("execution bundle digest is invalid")
45
+ if _DIGEST.fullmatch(claim_digest) is None:
46
+ raise ValueError("execution bundle claim digest is invalid")
47
+ if _OPERATION_ID.fullmatch(expected_operation_id) is None:
48
+ raise ValueError("execution bundle operation id is invalid")
49
+ if destination.exists() or destination.is_symlink():
50
+ if not verify_existing:
51
+ raise ValueError("execution bundle destination already exists")
52
+ return _verify_existing(
53
+ destination,
54
+ bundle_digest=expected_digest,
55
+ operation_id=expected_operation_id,
56
+ claim_digest=claim_digest,
57
+ )
58
+ if verify_existing:
59
+ raise ValueError("execution bundle verification requires an existing destination")
60
+ if archive is None:
61
+ raise ValueError("execution bundle archive is required for fresh extraction")
62
+ observed_digest = _archive_digest(archive)
63
+ if observed_digest != expected_digest:
64
+ raise ValueError("execution bundle digest differs")
65
+
66
+ temporary = destination.with_name(f".{destination.name}.extract-{os.getpid()}")
67
+ if temporary.exists() or temporary.is_symlink():
68
+ raise ValueError("execution bundle temporary destination already exists")
69
+ temporary.mkdir(mode=0o700)
70
+ published = False
71
+ try:
72
+ manifest, files = _extract(archive, temporary)
73
+ _validate_manifest(manifest, expected_operation_id, files)
74
+ _apply_modes(temporary, manifest)
75
+ result = _receipt(
76
+ bundle_digest=observed_digest,
77
+ operation_id=expected_operation_id,
78
+ claim_digest=claim_digest,
79
+ files=files,
80
+ inventory_digest=_manifest_inventory_digest(manifest),
81
+ )
82
+ write_private_bytes(
83
+ temporary / _RETAINED_MANIFEST,
84
+ canonical_bytes(manifest),
85
+ )
86
+ write_private_bytes(temporary / _RECEIPT, canonical_bytes(result))
87
+ _sync_tree_directories(temporary)
88
+ os.rename(temporary, destination)
89
+ _sync_directory(destination.parent)
90
+ published = True
91
+ return result
92
+ finally:
93
+ if not published:
94
+ shutil.rmtree(temporary, ignore_errors=True)
95
+
96
+
97
+ def _sync_directory(path: Path) -> None:
98
+ descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
99
+ try:
100
+ os.fsync(descriptor)
101
+ finally:
102
+ os.close(descriptor)
103
+
104
+
105
+ def _sync_tree_directories(root: Path) -> None:
106
+ directories = sorted(
107
+ (path for path in root.rglob("*") if path.is_dir()),
108
+ key=lambda path: len(path.parts),
109
+ reverse=True,
110
+ )
111
+ for directory in (*directories, root):
112
+ if directory.is_symlink():
113
+ raise ValueError("execution bundle extracted directory is a link")
114
+ _sync_directory(directory)
115
+
116
+
117
+ def _extract(archive: Path, destination: Path) -> tuple[dict[str, object], dict[str, str]]:
118
+ manifest: dict[str, object] | None = None
119
+ files: dict[str, str] = {}
120
+ total_bytes = 0
121
+ try:
122
+ with tarfile.open(archive, mode="r:gz") as stream:
123
+ payload_count = 0
124
+ member_count = 0
125
+ for member in stream:
126
+ member_count += 1
127
+ if member_count > _MAX_MEMBERS + 1:
128
+ raise ValueError("execution bundle archive member count exceeds its limit")
129
+ relative = _member_path(member.name)
130
+ if member.isdir():
131
+ continue
132
+ if not member.isfile() or member.size > _MAX_FILE_BYTES:
133
+ raise ValueError("execution bundle member type or size is invalid")
134
+ source = stream.extractfile(member)
135
+ if source is None:
136
+ raise ValueError("execution bundle member is unreadable")
137
+ if relative == "manifest.json":
138
+ if manifest is not None or member.size > 4 * 1024 * 1024:
139
+ raise ValueError("execution bundle manifest is invalid")
140
+ payload = source.read(member.size + 1)
141
+ if len(payload) != member.size:
142
+ raise ValueError("execution bundle manifest size differs")
143
+ value = json.loads(payload)
144
+ if not isinstance(value, dict):
145
+ raise ValueError("execution bundle manifest is invalid")
146
+ manifest = value
147
+ continue
148
+ total_bytes += member.size
149
+ if total_bytes > _MAX_TOTAL_BYTES:
150
+ raise ValueError("execution bundle exceeds its total size limit")
151
+ payload_count += 1
152
+ if payload_count > _MAX_MEMBERS:
153
+ raise ValueError("execution bundle member count exceeds its limit")
154
+ if relative in files:
155
+ raise ValueError("execution bundle repeats a file")
156
+ target = destination.joinpath(*PurePosixPath(relative).parts)
157
+ target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
158
+ descriptor = os.open(
159
+ target,
160
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
161
+ 0o600,
162
+ )
163
+ digest = hashlib.sha256()
164
+ copied = 0
165
+ with source, os.fdopen(descriptor, "wb") as output:
166
+ while chunk := source.read(1024 * 1024):
167
+ copied += len(chunk)
168
+ if copied > member.size:
169
+ raise ValueError("execution bundle member exceeded its declared size")
170
+ output.write(chunk)
171
+ digest.update(chunk)
172
+ output.flush()
173
+ os.fsync(output.fileno())
174
+ if copied != member.size:
175
+ raise ValueError("execution bundle member size differs")
176
+ files[relative] = digest.hexdigest()
177
+ except (tarfile.TarError, EOFError, json.JSONDecodeError) as exc:
178
+ raise ValueError("execution bundle archive is invalid") from exc
179
+ if manifest is None or not files:
180
+ raise ValueError("execution bundle manifest or payload is missing")
181
+ return manifest, dict(sorted(files.items()))
182
+
183
+
184
+ def _member_path(value: str) -> str:
185
+ path = PurePosixPath(value)
186
+ if (
187
+ path.is_absolute()
188
+ or "\\" in value
189
+ or not path.parts
190
+ or path.parts[0] != _ROOT
191
+ or any(part in {"", ".", ".."} for part in path.parts)
192
+ ):
193
+ raise ValueError("execution bundle path is invalid")
194
+ relative = PurePosixPath(*path.parts[1:]).as_posix()
195
+ if not relative or relative == _RECEIPT:
196
+ raise ValueError("execution bundle path is invalid")
197
+ return relative
198
+
199
+
200
+ def _validate_manifest(
201
+ manifest: dict[str, object], operation_id: str, files: dict[str, str]
202
+ ) -> None:
203
+ declared = manifest.get("files")
204
+ if (
205
+ set(manifest) != {"schema_version", "operation_id", "files"}
206
+ or manifest.get("schema_version") != "fdai.execution-bundle-manifest.v1"
207
+ or manifest.get("operation_id") != operation_id
208
+ or not isinstance(declared, dict)
209
+ or set(declared) != set(files)
210
+ ):
211
+ raise ValueError("execution bundle manifest differs from extracted payload")
212
+ for relative, digest in files.items():
213
+ record = declared.get(relative)
214
+ if (
215
+ not isinstance(record, dict)
216
+ or set(record) != {"sha256", "mode"}
217
+ or record.get("sha256") != digest
218
+ or record.get("mode") not in {"0600", "0700"}
219
+ ):
220
+ raise ValueError("execution bundle manifest differs from extracted payload")
221
+
222
+
223
+ def _receipt(
224
+ *,
225
+ bundle_digest: str,
226
+ operation_id: str,
227
+ claim_digest: str,
228
+ files: dict[str, str],
229
+ inventory_digest: str,
230
+ ) -> dict[str, object]:
231
+ result: dict[str, object] = {
232
+ "schema_version": "fdai.run-command-bundle-host-result.v1",
233
+ "state": "verified",
234
+ "operation_id": operation_id,
235
+ "bundle_digest": bundle_digest,
236
+ "claim_digest": claim_digest,
237
+ "file_count": len(files),
238
+ "inventory_digest": inventory_digest,
239
+ "mutation_performed": False,
240
+ }
241
+ result["receipt_digest"] = canonical_digest(result)
242
+ return result
243
+
244
+
245
+ def _verify_existing(
246
+ destination: Path,
247
+ *,
248
+ bundle_digest: str,
249
+ operation_id: str,
250
+ claim_digest: str,
251
+ ) -> dict[str, object]:
252
+ receipt = json.loads(read_private_bytes(destination / _RECEIPT, max_bytes=16_384))
253
+ if not isinstance(receipt, dict):
254
+ raise ValueError("execution bundle retained receipt is invalid")
255
+ manifest = json.loads(
256
+ read_private_bytes(destination / _RETAINED_MANIFEST, max_bytes=4 * 1024 * 1024)
257
+ )
258
+ if not isinstance(manifest, dict):
259
+ raise ValueError("execution bundle retained manifest is invalid")
260
+ files = _destination_files(destination, manifest)
261
+ _validate_manifest(manifest, operation_id, files)
262
+ expected = _receipt(
263
+ bundle_digest=bundle_digest,
264
+ operation_id=operation_id,
265
+ claim_digest=claim_digest,
266
+ files=files,
267
+ inventory_digest=_manifest_inventory_digest(manifest),
268
+ )
269
+ if receipt != expected:
270
+ raise ValueError("execution bundle retained destination differs")
271
+ return expected
272
+
273
+
274
+ def _destination_files(destination: Path, manifest: dict[str, object]) -> dict[str, str]:
275
+ root_details = destination.lstat()
276
+ if (
277
+ destination.is_symlink()
278
+ or not stat.S_ISDIR(root_details.st_mode)
279
+ or stat.S_IMODE(root_details.st_mode) != 0o700
280
+ or root_details.st_uid != os.geteuid()
281
+ ):
282
+ raise ValueError("execution bundle retained destination is invalid")
283
+ declared = manifest.get("files")
284
+ if not isinstance(declared, dict):
285
+ raise ValueError("execution bundle manifest is invalid")
286
+ files: dict[str, str] = {}
287
+ total_bytes = 0
288
+ for directory, directories, names in os.walk(destination, followlinks=False):
289
+ base = Path(directory)
290
+ if any((base / name).is_symlink() for name in directories):
291
+ raise ValueError("execution bundle retained destination contains a link")
292
+ for name in names:
293
+ candidate = base / name
294
+ relative = candidate.relative_to(destination).as_posix()
295
+ if relative in {_RECEIPT, _RETAINED_MANIFEST}:
296
+ continue
297
+ if len(files) >= _MAX_MEMBERS:
298
+ raise ValueError("execution bundle retained destination exceeds its file limit")
299
+ details = candidate.lstat()
300
+ record = declared.get(relative)
301
+ if (
302
+ candidate.is_symlink()
303
+ or not stat.S_ISREG(details.st_mode)
304
+ or details.st_nlink != 1
305
+ or not isinstance(record, dict)
306
+ or record.get("mode") not in {"0600", "0700"}
307
+ or stat.S_IMODE(details.st_mode) != int(str(record["mode"]), 8)
308
+ or details.st_size > _MAX_FILE_BYTES
309
+ ):
310
+ raise ValueError("execution bundle retained destination is invalid")
311
+ total_bytes += details.st_size
312
+ if total_bytes > _MAX_TOTAL_BYTES:
313
+ raise ValueError("execution bundle retained destination exceeds its size limit")
314
+ descriptor = os.open(candidate, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
315
+ digest = hashlib.sha256()
316
+ with os.fdopen(descriptor, "rb") as stream:
317
+ opened = os.fstat(stream.fileno())
318
+ copied = 0
319
+ while chunk := stream.read(1024 * 1024):
320
+ copied += len(chunk)
321
+ if copied > details.st_size:
322
+ raise ValueError("execution bundle retained file grew")
323
+ digest.update(chunk)
324
+ after = os.fstat(stream.fileno())
325
+ if (
326
+ copied != details.st_size
327
+ or opened.st_ino != details.st_ino
328
+ or after.st_size != opened.st_size
329
+ or after.st_mtime_ns != opened.st_mtime_ns
330
+ or after.st_ctime_ns != opened.st_ctime_ns
331
+ ):
332
+ raise ValueError("execution bundle retained file changed")
333
+ files[relative] = digest.hexdigest()
334
+ return dict(sorted(files.items()))
335
+
336
+
337
+ def _apply_modes(destination: Path, manifest: dict[str, object]) -> None:
338
+ declared = manifest.get("files")
339
+ if not isinstance(declared, dict):
340
+ raise ValueError("execution bundle manifest is invalid")
341
+ for relative, value in declared.items():
342
+ if not isinstance(relative, str) or not isinstance(value, dict):
343
+ raise ValueError("execution bundle manifest is invalid")
344
+ mode = value.get("mode")
345
+ if mode not in {"0600", "0700"}:
346
+ raise ValueError("execution bundle manifest mode is invalid")
347
+ path = destination.joinpath(*PurePosixPath(relative).parts)
348
+ if path.is_symlink() or not path.is_file():
349
+ raise ValueError("execution bundle manifest file is invalid")
350
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
351
+ try:
352
+ os.fchmod(descriptor, int(mode, 8))
353
+ os.fsync(descriptor)
354
+ finally:
355
+ os.close(descriptor)
356
+
357
+
358
+ def _manifest_inventory_digest(manifest: dict[str, object]) -> str:
359
+ declared = manifest.get("files")
360
+ if not isinstance(declared, dict):
361
+ raise ValueError("execution bundle manifest is invalid")
362
+ return canonical_digest({"files": declared})
363
+
364
+
365
+ def _archive_digest(path: Path) -> str:
366
+ details = path.lstat()
367
+ if (
368
+ path.is_symlink()
369
+ or not stat.S_ISREG(details.st_mode)
370
+ or not 0 < details.st_size <= _MAX_ARCHIVE_BYTES
371
+ ):
372
+ raise ValueError("execution bundle archive type or size is invalid")
373
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
374
+ digest = hashlib.sha256()
375
+ with os.fdopen(descriptor, "rb") as stream:
376
+ opened = os.fstat(stream.fileno())
377
+ while chunk := stream.read(1024 * 1024):
378
+ digest.update(chunk)
379
+ after = os.fstat(stream.fileno())
380
+ if (
381
+ opened.st_ino != details.st_ino
382
+ or opened.st_size != details.st_size
383
+ or after.st_size != opened.st_size
384
+ or after.st_mtime_ns != opened.st_mtime_ns
385
+ ):
386
+ raise ValueError("execution bundle archive changed while being read")
387
+ return digest.hexdigest()
388
+
389
+
390
+ def main(arguments: Sequence[str] | None = None) -> int:
391
+ """Receive one fixed execution bundle and emit a bounded machine result."""
392
+
393
+ parser = argparse.ArgumentParser(description=__doc__)
394
+ parser.add_argument("--archive", type=Path)
395
+ parser.add_argument("--destination", type=Path, required=True)
396
+ parser.add_argument("--bundle-digest", required=True)
397
+ parser.add_argument("--operation-id", required=True)
398
+ parser.add_argument("--claim-digest", required=True)
399
+ parser.add_argument("--verify-existing", action="store_true")
400
+ args = parser.parse_args(arguments)
401
+ try:
402
+ result = receive_execution_bundle(
403
+ args.archive,
404
+ args.destination,
405
+ expected_digest=args.bundle_digest,
406
+ expected_operation_id=args.operation_id,
407
+ claim_digest=args.claim_digest,
408
+ verify_existing=args.verify_existing,
409
+ )
410
+ except (OSError, ValueError, json.JSONDecodeError):
411
+ print("run command receiver failed; preserve retained evidence", file=sys.stderr)
412
+ return 3
413
+ print(canonical_bytes(result).decode())
414
+ return 0
415
+
416
+
417
+ if __name__ == "__main__":
418
+ raise SystemExit(main())