mostlyright-data 0.9.0__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 (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
@@ -0,0 +1,2533 @@
1
+ """Signed, exact-candidate local data-agent review envelopes.
2
+
3
+ One review envelope is the directory ``<run_dir>/review``. It holds
4
+ assignment.json, backlog.json, decision.json, findings.json, and
5
+ reports/<lens>.json for both required lenses. Every member is a sealed mode-0444
6
+ file under a sealed mode-0555 root.
7
+
8
+ Finalization is one transaction and runs in this order: bind every absolute
9
+ run-directory ancestor by descriptor, take the exclusive publication lock,
10
+ verify the candidate, stage the envelope in a private temporary directory,
11
+ install it with an atomic rename that refuses to replace an existing envelope,
12
+ then re-verify the installed bytes while the staging descriptors and snapshot
13
+ leases are still held. Each step revalidates the bound chain and the lock, so a
14
+ same-UID actor cannot substitute a pathname midway through.
15
+
16
+ Entry points, in call order: ``_sign_enrolled_report`` signs one reviewer
17
+ report, ``_finalize_enrolled_review`` writes the verifier-signed decision, and
18
+ ``verify_enrolled_review_decision`` re-verifies an installed envelope. Each
19
+ resolves its keys through an inherited coordinator session. The unenrolled forms
20
+ take their trust directly instead: ``_finalize_review`` takes the trusted
21
+ assignment and the verifier signer, and ``_verify_review_decision`` takes the
22
+ trusted verifier public key and, optionally, the trusted assignment.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import base64
28
+ import ctypes
29
+ import errno
30
+ import json
31
+ import os
32
+ import select
33
+ import stat
34
+ import struct
35
+ import sys
36
+ import uuid
37
+ from dataclasses import dataclass
38
+ from pathlib import Path, PurePosixPath
39
+ from typing import Any
40
+
41
+ try:
42
+ import fcntl
43
+ except ImportError: # pragma: no cover - unavailable on the already-unsupported Windows path
44
+ fcntl = None # type: ignore[assignment]
45
+
46
+ from cryptography.exceptions import InvalidSignature
47
+ from cryptography.hazmat.primitives import serialization
48
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
49
+
50
+ from mostlyright.data_harness.canonical import sha256_bytes
51
+ from mostlyright.data_harness.pipeline import (
52
+ canonical_json_line_bytes,
53
+ verify_candidate,
54
+ )
55
+ from mostlyright.data_harness.signing import (
56
+ SignerProviderError,
57
+ TrustedReviewAuthority,
58
+ TrustedReviewAuthorityProvider,
59
+ TrustedSigner,
60
+ resolve_review_authority,
61
+ )
62
+
63
+ REPORT_SCHEMA_VERSION = "agent-review.v2"
64
+ ASSIGNMENT_SCHEMA_VERSION = "review-assignment.v1"
65
+ DECISION_SCHEMA_VERSION = "review-decision.v2"
66
+ VERIFIER_VERSION = "local-review-verifier.v2"
67
+ REQUIRED_LENSES = frozenset({"question_source_fit", "transformation_data_quality"})
68
+ _SEVERITIES = frozenset({"blocker", "critical", "high", "medium", "low"})
69
+ _BLOCKING_SEVERITIES = frozenset({"blocker", "critical", "high"})
70
+ _REVIEW_ROOT_MEMBERS = frozenset(
71
+ {"assignment.json", "backlog.json", "decision.json", "findings.json", "reports"}
72
+ )
73
+ _REVIEW_REPORT_MEMBERS = frozenset(f"{lens}.json" for lens in REQUIRED_LENSES)
74
+ _REVIEW_FILE_PATHS = frozenset(
75
+ {
76
+ "assignment.json",
77
+ "backlog.json",
78
+ "decision.json",
79
+ "findings.json",
80
+ *(f"reports/{name}" for name in _REVIEW_REPORT_MEMBERS),
81
+ }
82
+ )
83
+ _REVIEW_PUBLICATION_LOCK = ".review.lock"
84
+ _MAX_REVIEW_MEMBER_BYTES = 10 * 1024 * 1024
85
+ # Linux inotify event bits, spelled here because ctypes gives no header constants. Values
86
+ # are from <sys/inotify.h> and are ABI-stable.
87
+ _IN_ATTRIB = 0x00000004
88
+ _IN_DELETE_SELF = 0x00000400
89
+ _IN_MOVE_SELF = 0x00000800
90
+ _IN_UNMOUNT = 0x00002000
91
+ # The mask retained review ancestors are watched under: any attribute change, self-delete,
92
+ # self-move, or unmount. IN_IGNORED and IN_Q_OVERFLOW arrive whatever the mask is set to,
93
+ # so any readable event at all fails the transaction closed.
94
+ _REVIEW_ANCESTOR_WATCH_MASK = _IN_ATTRIB | _IN_DELETE_SELF | _IN_MOVE_SELF | _IN_UNMOUNT
95
+ _SECURE_REVIEW_STAGING_SUPPORTED = (
96
+ os.name != "nt"
97
+ and fcntl is not None
98
+ and bool(getattr(os, "O_DIRECTORY", 0))
99
+ and bool(getattr(os, "O_NOFOLLOW", 0))
100
+ and hasattr(os, "fchmod")
101
+ and os.open in os.supports_dir_fd
102
+ and os.stat in os.supports_dir_fd
103
+ and os.mkdir in os.supports_dir_fd
104
+ and os.stat in os.supports_follow_symlinks
105
+ and os.listdir in os.supports_fd
106
+ )
107
+
108
+
109
+ class ReviewError(RuntimeError):
110
+ """A review envelope failed authentication or policy validation.
111
+
112
+ ``code`` carries no authority: nothing in the review path reads it, and it exists only so the
113
+ command line can look up the plain sentences that go with a refusal a person can actually hit.
114
+ It is what a person quotes and what an agent branches on, so every raise site in this module
115
+ passes one and ``tests/_typed_errors.py`` fails when one does not. It stays optional in the
116
+ signature because an exception is rebuilt from its message alone when it crosses a process
117
+ boundary; what is enforced is the raise site, which is where the code is decided.
118
+
119
+ The codes are grouped by the document the refusal is about -- ``ASSIGNMENT_``, ``REPORT_``,
120
+ ``DECISION_``, ``ENVELOPE_``, ``LOCK_``, ``KEY_`` -- and the path-safety refusals reuse the
121
+ ``CANDIDATE_ANCESTOR_`` codes the rest of the tree already raises for the same class of
122
+ failure, so one wording answers for both rather than a second one being invented here.
123
+ """
124
+
125
+ def __init__(self, message: str, *, code: str | None = None) -> None:
126
+ super().__init__(message)
127
+ self.code = code
128
+
129
+
130
+ def _secure_review_staging_supported() -> bool:
131
+ """Return whether exact descriptor-relative staging primitives are available."""
132
+
133
+ return _SECURE_REVIEW_STAGING_SUPPORTED
134
+
135
+
136
+ def _secure_review_verification_supported() -> bool:
137
+ """Return whether review verification can retain an exact directory snapshot."""
138
+
139
+ return _SECURE_REVIEW_STAGING_SUPPORTED
140
+
141
+
142
+ # inotify directory watches also receive named (len > 0) events for entries INSIDE the
143
+ # watched directory; only unnamed (len == 0) events — IN_ATTRIB/IN_DELETE_SELF/
144
+ # IN_MOVE_SELF/IN_UNMOUNT/IN_IGNORED and queue overflow — describe the watched inode
145
+ # itself, which is the kqueue EVFILT_VNODE semantic this monitor is defined against.
146
+ _INOTIFY_EVENT_HEADER = struct.Struct("iIII")
147
+
148
+
149
+ @dataclass
150
+ class _ReviewAncestorMonitor:
151
+ """Retain kernel move/delete notifications for every bound run ancestor."""
152
+
153
+ kind: str
154
+ resource: Any
155
+ event_capacity: int
156
+
157
+ def poll(self) -> bool:
158
+ try:
159
+ if self.kind == "kqueue":
160
+ return bool(self.resource.control(None, self.event_capacity, 0))
161
+ while True:
162
+ try:
163
+ payload = os.read(self.resource, 4096)
164
+ except BlockingIOError:
165
+ return False
166
+ offset = 0
167
+ while offset + _INOTIFY_EVENT_HEADER.size <= len(payload):
168
+ _wd, _mask, _cookie, name_length = _INOTIFY_EVENT_HEADER.unpack_from(
169
+ payload, offset
170
+ )
171
+ if name_length == 0:
172
+ return True
173
+ offset += _INOTIFY_EVENT_HEADER.size + name_length
174
+ except OSError as exc:
175
+ raise ReviewError(
176
+ "review run ancestor monitor became unavailable", code="CANDIDATE_ANCESTOR_RACE"
177
+ ) from exc
178
+
179
+ def close(self) -> None:
180
+ if self.kind == "kqueue":
181
+ self.resource.close()
182
+ else:
183
+ os.close(self.resource)
184
+
185
+
186
+ # One retained ancestor of the run directory: descriptor, parent_fd, name, entry identity
187
+ # (st_dev, st_ino, st_nlink), and a human label used in refusal messages. The
188
+ # filesystem-anchor record carries no parent_fd and no name, so revalidation checks it by
189
+ # descriptor only. pipeline.py retains the same shape on its own run handle; the two must
190
+ # change together.
191
+ _RetainedAncestorRecord = tuple[int, int | None, str | None, tuple[int, int, int], str]
192
+
193
+
194
+ @dataclass
195
+ class _ReviewRunHandle:
196
+ """One retained absolute run-directory chain for a complete review transaction."""
197
+
198
+ run_dir: Path
199
+ descriptors: list[int]
200
+ records: list[_RetainedAncestorRecord]
201
+ monitor: _ReviewAncestorMonitor
202
+
203
+ @property
204
+ def run_fd(self) -> int:
205
+ return self.descriptors[-1]
206
+
207
+ def close(self) -> None:
208
+ self.monitor.close()
209
+ for descriptor in reversed(self.descriptors):
210
+ os.close(descriptor)
211
+ self.descriptors.clear()
212
+
213
+
214
+ @dataclass
215
+ class _ReviewPublicationLock:
216
+ """One descriptor-bound exclusive publication lock retained through verification."""
217
+
218
+ handle: _ReviewRunHandle
219
+ descriptor: int
220
+ lock_stat: os.stat_result
221
+
222
+ def validate(self) -> None:
223
+ _validate_review_run_handle(self.handle)
224
+ opened = os.fstat(self.descriptor)
225
+ _require_stable_stat(self.lock_stat, opened, "review publication lock")
226
+ _require_review_publication_lock(opened)
227
+ named = os.stat(
228
+ _REVIEW_PUBLICATION_LOCK,
229
+ dir_fd=self.handle.run_fd,
230
+ follow_symlinks=False,
231
+ )
232
+ _require_stable_stat(self.lock_stat, named, "review publication lock")
233
+ _require_review_publication_lock(named)
234
+
235
+ def close(self) -> None:
236
+ if fcntl is not None:
237
+ fcntl.flock(self.descriptor, fcntl.LOCK_UN)
238
+ os.close(self.descriptor)
239
+
240
+
241
+ @dataclass
242
+ class _ReviewSnapshotLease:
243
+ """Retain the exact review child root through the result linearization point."""
244
+
245
+ handle: _ReviewRunHandle
246
+ review_fd: int
247
+ review_stat: os.stat_result
248
+ owns_review_fd: bool
249
+
250
+ def validate(self) -> None:
251
+ _validate_review_run_handle(self.handle)
252
+ _require_stable_stat(
253
+ self.review_stat,
254
+ os.fstat(self.review_fd),
255
+ "review root",
256
+ )
257
+ named = os.stat(
258
+ "review",
259
+ dir_fd=self.handle.run_fd,
260
+ follow_symlinks=False,
261
+ )
262
+ _require_stable_stat(self.review_stat, named, "review root")
263
+
264
+ def close(self) -> None:
265
+ if self.owns_review_fd:
266
+ os.close(self.review_fd)
267
+
268
+
269
+ @dataclass(frozen=True)
270
+ class ReviewResult:
271
+ """Authenticated outcome of one review envelope.
272
+
273
+ status is exactly one of two values. It is "fixes_required" when any finding
274
+ has severity blocker, critical, or high, and "reviewed_candidate" otherwise.
275
+ candidate_digest names the exact reviewed candidate. decision_digest is the
276
+ SHA-256 of the canonical decision payload; the verifier signs that payload
277
+ with this digest appended. review_dir is the installed envelope directory.
278
+ """
279
+
280
+ status: str
281
+ candidate_digest: str
282
+ decision_digest: str
283
+ review_dir: Path
284
+ decision_document: dict[str, Any]
285
+
286
+
287
+ def public_key_to_base64(public_key: Ed25519PublicKey) -> str:
288
+ raw = public_key.public_bytes(
289
+ serialization.Encoding.Raw,
290
+ serialization.PublicFormat.Raw,
291
+ )
292
+ return base64.b64encode(raw).decode("ascii")
293
+
294
+
295
+ def load_public_key_base64(value: str) -> Ed25519PublicKey:
296
+ try:
297
+ raw = base64.b64decode(value, validate=True)
298
+ return Ed25519PublicKey.from_public_bytes(raw)
299
+ except (ValueError, TypeError) as exc:
300
+ raise ReviewError("invalid Ed25519 public key", code="KEY_INVALID") from exc
301
+
302
+
303
+ def _trusted_authority_signer(value: Any, role: str) -> TrustedSigner:
304
+ if not callable(getattr(value, "sign", None)) or not callable(
305
+ getattr(value, "public_key", None)
306
+ ):
307
+ raise SignerProviderError(f"trusted review authority returned an invalid {role} signer")
308
+ return value
309
+
310
+
311
+ def _review_authority_material(
312
+ provider: TrustedReviewAuthorityProvider | None,
313
+ ) -> tuple[TrustedReviewAuthority, Any, TrustedSigner]:
314
+ authority = resolve_review_authority(provider)
315
+ try:
316
+ assignment = authority.assignment_value()
317
+ verifier = _trusted_authority_signer(authority.verifier_signer(), "verifier")
318
+ except (ReviewError, SignerProviderError):
319
+ raise
320
+ except Exception as exc:
321
+ raise SignerProviderError(
322
+ "trusted review authority could not provide its enrollment"
323
+ ) from exc
324
+ return authority, assignment, verifier
325
+
326
+
327
+ def _sign_report(payload: Any, private_key: TrustedSigner) -> dict[str, Any]:
328
+ """Internal primitive: validate and sign one already-authorized report payload."""
329
+
330
+ normalized = _parse_report_payload(payload)
331
+ signature = private_key.sign(canonical_json_line_bytes(normalized))
332
+ return {**normalized, "signature": base64.b64encode(signature).decode("ascii")}
333
+
334
+
335
+ def _sign_assigned_report(
336
+ payload: Any,
337
+ *,
338
+ trusted_assignment_value: Any,
339
+ reviewer_signer: TrustedSigner,
340
+ ) -> dict[str, Any]:
341
+ """Sign only when a trusted enrollment authorizes this exact report identity."""
342
+
343
+ assignment = _parse_assignment(trusted_assignment_value)
344
+ normalized = _parse_report_payload(payload)
345
+ reviewer = next(
346
+ (
347
+ item
348
+ for item in assignment["reviewers"]
349
+ if item["attempt_id"] == normalized["reviewer_attempt"]
350
+ ),
351
+ None,
352
+ )
353
+ if reviewer is None:
354
+ raise ReviewError(
355
+ "report reviewer attempt is not enrolled by the trusted authority",
356
+ code="ASSIGNMENT_ATTEMPT_NOT_ENROLLED",
357
+ )
358
+ expected = {
359
+ "assignment_digest": _normalized_assignment_digest(assignment),
360
+ "candidate_digest": assignment["candidate_digest"],
361
+ "validation_policy_digest": assignment["validation_policy_digest"],
362
+ "cycle_lineage": assignment["cycle_lineage"],
363
+ "failed_cycle_count": assignment["failed_cycle_count"],
364
+ "reviewer_principal": reviewer["principal_id"],
365
+ "reviewer_attempt": reviewer["attempt_id"],
366
+ "reviewer_key_id": reviewer["key_id"],
367
+ "lens": reviewer["lens"],
368
+ }
369
+ for field, value in expected.items():
370
+ if normalized[field] != value:
371
+ raise ReviewError(
372
+ f"report {field} is not authorized by the trusted assignment",
373
+ code="ASSIGNMENT_FIELD_UNAUTHORIZED",
374
+ )
375
+ if public_key_to_base64(reviewer_signer.public_key()) != reviewer["public_key"]:
376
+ raise ReviewError(
377
+ "reviewer signer does not match the trusted assignment enrollment",
378
+ code="ASSIGNMENT_SIGNER_MISMATCH",
379
+ )
380
+ return _sign_report(normalized, reviewer_signer)
381
+
382
+
383
+ def _sign_enrolled_report(
384
+ payload: Any,
385
+ *,
386
+ review_authority_provider: TrustedReviewAuthorityProvider | None,
387
+ ) -> dict[str, Any]:
388
+ """Sign one report only through an inherited coordinator session."""
389
+
390
+ authority = resolve_review_authority(review_authority_provider)
391
+ try:
392
+ assignment = authority.assignment_value()
393
+ except (ReviewError, SignerProviderError):
394
+ raise
395
+ except Exception as exc:
396
+ raise SignerProviderError(
397
+ "trusted review authority could not provide its enrollment"
398
+ ) from exc
399
+ normalized = _parse_report_payload(payload)
400
+ try:
401
+ signer = _trusted_authority_signer(
402
+ authority.reviewer_signer_for(normalized["reviewer_attempt"]),
403
+ "reviewer",
404
+ )
405
+ except (ReviewError, SignerProviderError):
406
+ raise
407
+ except Exception as exc:
408
+ raise SignerProviderError(
409
+ "trusted review authority could not resolve the reviewer enrollment"
410
+ ) from exc
411
+ return _sign_assigned_report(
412
+ normalized,
413
+ trusted_assignment_value=assignment,
414
+ reviewer_signer=signer,
415
+ )
416
+
417
+
418
+ def _finalize_enrolled_review(
419
+ *,
420
+ run_dir: Path,
421
+ report_values: list[Any],
422
+ review_authority_provider: TrustedReviewAuthorityProvider | None,
423
+ ) -> ReviewResult:
424
+ """Finalize one review only through an inherited coordinator session."""
425
+
426
+ _authority, assignment, verifier = _review_authority_material(review_authority_provider)
427
+ return _finalize_review(
428
+ run_dir=run_dir,
429
+ assignment_value=assignment,
430
+ report_values=report_values,
431
+ verifier_private_key=verifier,
432
+ )
433
+
434
+
435
+ def _finalize_review(
436
+ *,
437
+ run_dir: Path,
438
+ assignment_value: Any,
439
+ report_values: list[Any],
440
+ verifier_private_key: TrustedSigner,
441
+ _run_parent_fd: int | None = None,
442
+ _transaction_validator: Any | None = None,
443
+ ) -> ReviewResult:
444
+ """Authenticate two reports and write one immutable verifier-signed decision envelope."""
445
+
446
+ if not _secure_review_staging_supported():
447
+ raise ReviewError(
448
+ "secure descriptor-relative review staging is unavailable on this platform",
449
+ code="PLATFORM_REVIEW_STAGING_UNSUPPORTED",
450
+ )
451
+ handle = (
452
+ _open_review_run_handle(run_dir)
453
+ if _run_parent_fd is None
454
+ else _open_review_run_handle_from_parent(run_dir, _run_parent_fd)
455
+ )
456
+ publication_lock: _ReviewPublicationLock | None = None
457
+ staging_name = f".review.tmp-{uuid.uuid4().hex}"
458
+ staging_fd = -1
459
+ renamed = False
460
+ installed_identity_owned = False
461
+ installation_durability_confirmed = False
462
+ staging_identity: tuple[int, int, int] | None = None
463
+ verification_leases: list[Any] = []
464
+ try:
465
+ publication_lock = _acquire_review_publication_lock(handle)
466
+
467
+ def validate_transaction() -> None:
468
+ if publication_lock is None: # pragma: no cover - developer invariant
469
+ raise ReviewError("review publication lock is unavailable", code="LOCK_UNAVAILABLE")
470
+ if _transaction_validator is not None:
471
+ _transaction_validator()
472
+ publication_lock.validate()
473
+
474
+ verified_candidate = verify_candidate(
475
+ handle.run_dir,
476
+ _run_fd=handle.run_fd,
477
+ _ancestor_validator=validate_transaction,
478
+ )
479
+ validate_transaction()
480
+ assignment = _parse_assignment(assignment_value)
481
+ _validate_assignment_against_candidate(assignment, verified_candidate)
482
+ verifier_public = verifier_private_key.public_key()
483
+ if public_key_to_base64(verifier_public) != assignment["verifier"]["public_key"]:
484
+ raise ReviewError(
485
+ "verifier private key does not match the enrolled assignment key",
486
+ code="ASSIGNMENT_VERIFIER_KEY_MISMATCH",
487
+ )
488
+
489
+ reports = [_parse_signed_report(value) for value in report_values]
490
+ if len(reports) != 2:
491
+ raise ReviewError("exactly two signed review reports are required", code="REPORT_COUNT")
492
+ assignments_by_lens = {item["lens"]: item for item in assignment["reviewers"]}
493
+ reports_by_lens: dict[str, dict[str, Any]] = {}
494
+ for report in reports:
495
+ lens = report["lens"]
496
+ if lens in reports_by_lens:
497
+ raise ReviewError(f"duplicate report lens: {lens}", code="REPORT_LENS_DUPLICATE")
498
+ expected = assignments_by_lens.get(lens)
499
+ if expected is None:
500
+ raise ReviewError(
501
+ f"report lens is not assigned: {lens}", code="REPORT_LENS_UNASSIGNED"
502
+ )
503
+ _verify_report(report, expected, verified_candidate, assignment)
504
+ reports_by_lens[lens] = report
505
+ if set(reports_by_lens) != REQUIRED_LENSES:
506
+ raise ReviewError(
507
+ "both required review lenses must be present", code="REPORT_LENSES_INCOMPLETE"
508
+ )
509
+
510
+ findings = _normalize_findings(reports_by_lens)
511
+ backlog = [finding for finding in findings if finding["severity"] in {"medium", "low"}]
512
+ status = _derive_review_status(findings, assignment["failed_cycle_count"])
513
+ expected_decision_digest = _expected_review_decision_digest(
514
+ assignment=assignment,
515
+ reports_by_lens=reports_by_lens,
516
+ findings=findings,
517
+ backlog=backlog,
518
+ status=status,
519
+ verified_candidate=verified_candidate,
520
+ )
521
+
522
+ try:
523
+ os.stat("review", dir_fd=handle.run_fd, follow_symlinks=False)
524
+ except FileNotFoundError:
525
+ pass
526
+ else:
527
+ _ensure_installed_review_durable(handle)
528
+ verified = _verify_review_decision(
529
+ handle.run_dir,
530
+ trusted_verifier_public_key=verifier_public,
531
+ trusted_assignment_value=assignment,
532
+ _run_handle=handle,
533
+ _transaction_validator=validate_transaction,
534
+ )
535
+ if verified.decision_digest != expected_decision_digest or verified.status != status:
536
+ raise ReviewError(
537
+ "existing review envelope does not match this finalization request",
538
+ code="ENVELOPE_MISMATCH",
539
+ )
540
+ validate_transaction()
541
+ return verified
542
+ os.mkdir(staging_name, mode=0o700, dir_fd=handle.run_fd)
543
+ created_staging = os.stat(
544
+ staging_name,
545
+ dir_fd=handle.run_fd,
546
+ follow_symlinks=False,
547
+ )
548
+ if not stat.S_ISDIR(created_staging.st_mode):
549
+ raise ReviewError(
550
+ "review staging root is not a directory", code="ENVELOPE_STAGING_ROOT_INVALID"
551
+ )
552
+ staging_identity = _entry_identity(created_staging)
553
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
554
+ staging_fd = os.open(staging_name, directory_flags, dir_fd=handle.run_fd)
555
+ _require_same_identity(created_staging, os.fstat(staging_fd), "review staging root")
556
+ decision_digest = _stage_review_envelope(
557
+ staging_fd,
558
+ parent_fd=handle.run_fd,
559
+ staging_name=staging_name,
560
+ expected_root_identity=staging_identity,
561
+ assignment=assignment,
562
+ reports_by_lens=reports_by_lens,
563
+ findings=findings,
564
+ backlog=backlog,
565
+ status=status,
566
+ verified_candidate=verified_candidate,
567
+ verifier_private_key=verifier_private_key,
568
+ )
569
+ validate_transaction()
570
+ try:
571
+ _rename_no_replace(
572
+ handle.run_dir / staging_name,
573
+ handle.run_dir / "review",
574
+ expected_source_identity=staging_identity,
575
+ installed_mode=0o555,
576
+ parent_fd=handle.run_fd,
577
+ source_fd=staging_fd,
578
+ )
579
+ except FileExistsError as exc:
580
+ raise ReviewError(
581
+ "review envelope already exists and cannot be overwritten", code="ENVELOPE_EXISTS"
582
+ ) from exc
583
+ except Exception:
584
+ installed_identity_owned = _named_entry_has_identity(
585
+ handle.run_fd,
586
+ "review",
587
+ staging_identity,
588
+ )
589
+ raise
590
+ renamed = True
591
+ installed_identity_owned = True
592
+ _ensure_installed_review_durable(handle, retained_review_fd=staging_fd)
593
+ installation_durability_confirmed = True
594
+ validate_transaction()
595
+ verified = _verify_review_decision(
596
+ handle.run_dir,
597
+ trusted_verifier_public_key=verifier_public,
598
+ trusted_assignment_value=assignment,
599
+ _run_handle=handle,
600
+ _retained_review_fd=staging_fd,
601
+ _expected_review_identity=staging_identity,
602
+ _retained_snapshot_leases=verification_leases,
603
+ _transaction_validator=validate_transaction,
604
+ )
605
+ if (
606
+ decision_digest != expected_decision_digest
607
+ or verified.decision_digest != expected_decision_digest
608
+ or verified.status != status
609
+ ):
610
+ raise ReviewError(
611
+ "installed review did not verify as the finalized decision",
612
+ code="DECISION_NOT_VERIFIED",
613
+ )
614
+ for lease in verification_leases:
615
+ lease.validate()
616
+ validate_transaction()
617
+ result = verified
618
+ for lease in verification_leases:
619
+ lease.validate()
620
+ return result
621
+ except Exception:
622
+ if staging_identity is not None:
623
+ _remove_at(
624
+ handle.run_fd,
625
+ staging_name,
626
+ expected_identity=staging_identity,
627
+ )
628
+ if (renamed or installed_identity_owned) and installation_durability_confirmed:
629
+ _remove_at(
630
+ handle.run_fd,
631
+ "review",
632
+ expected_identity=staging_identity,
633
+ )
634
+ raise
635
+ finally:
636
+ for lease in reversed(verification_leases):
637
+ lease.close()
638
+ if staging_fd >= 0:
639
+ os.close(staging_fd)
640
+ if publication_lock is not None:
641
+ publication_lock.close()
642
+ handle.close()
643
+
644
+
645
+ def verify_enrolled_review_decision(
646
+ run_dir: Path,
647
+ *,
648
+ review_authority_provider: TrustedReviewAuthorityProvider | None,
649
+ ) -> ReviewResult:
650
+ """Verify one installed review only through an inherited coordinator session.
651
+
652
+ This is the read-only promotion gate for callers such as deployment planning. It deliberately
653
+ accepts a provider rather than raw keys or assignments, so an ordinary caller cannot downgrade
654
+ protected review authority into caller-authored trust material.
655
+ """
656
+
657
+ _authority, assignment, verifier = _review_authority_material(review_authority_provider)
658
+ return _verify_review_decision(
659
+ run_dir,
660
+ trusted_verifier_public_key=verifier.public_key(),
661
+ trusted_assignment_value=assignment,
662
+ )
663
+
664
+
665
+ def _verify_review_decision(
666
+ run_dir: Path,
667
+ *,
668
+ trusted_verifier_public_key: Ed25519PublicKey,
669
+ trusted_assignment_value: Any | None = None,
670
+ _run_parent_fd: int | None = None,
671
+ _ancestor_validator: Any | None = None,
672
+ _run_handle: _ReviewRunHandle | None = None,
673
+ _retained_review_fd: int | None = None,
674
+ _expected_review_identity: tuple[int, int, int] | None = None,
675
+ _retained_snapshot_leases: list[Any] | None = None,
676
+ _transaction_validator: Any | None = None,
677
+ ) -> ReviewResult:
678
+ """Return authoritative status/digest evidence at a bound-snapshot linearization.
679
+
680
+ The result remains the evidence if a same-UID actor mutates the pathnames after
681
+ the final no-write observation.
682
+
683
+ The two revalidation callbacks are not interchangeable. _transaction_validator
684
+ revalidates the whole enclosing transaction, including the publication lock a
685
+ finalizer holds. _ancestor_validator revalidates only the caller's outer
686
+ ancestors, so it is accepted alone and wrapped with this function's own
687
+ run-handle check.
688
+ """
689
+
690
+ if not _secure_review_verification_supported():
691
+ raise ReviewError(
692
+ "secure descriptor-relative review verification is unavailable on this platform",
693
+ code="PLATFORM_REVIEW_VERIFY_UNSUPPORTED",
694
+ )
695
+ owned_handle = _run_handle is None
696
+ handle = _run_handle or (
697
+ _open_review_run_handle(run_dir)
698
+ if _run_parent_fd is None
699
+ else _open_review_run_handle_from_parent(run_dir, _run_parent_fd)
700
+ )
701
+ transaction_validator = _transaction_validator
702
+ if transaction_validator is None and _ancestor_validator is not None:
703
+
704
+ def transaction_validator() -> None:
705
+ _ancestor_validator()
706
+ _validate_review_run_handle(handle)
707
+
708
+ try:
709
+ return _verify_review_decision_with_handle(
710
+ handle,
711
+ trusted_verifier_public_key=trusted_verifier_public_key,
712
+ trusted_assignment_value=trusted_assignment_value,
713
+ retained_review_fd=_retained_review_fd,
714
+ expected_review_identity=_expected_review_identity,
715
+ retained_snapshot_leases=_retained_snapshot_leases,
716
+ transaction_validator=transaction_validator,
717
+ )
718
+ finally:
719
+ if owned_handle:
720
+ handle.close()
721
+
722
+
723
+ def _verify_review_decision_with_handle(
724
+ handle: _ReviewRunHandle,
725
+ *,
726
+ trusted_verifier_public_key: Ed25519PublicKey,
727
+ trusted_assignment_value: Any | None,
728
+ retained_review_fd: int | None,
729
+ expected_review_identity: tuple[int, int, int] | None,
730
+ retained_snapshot_leases: list[Any] | None,
731
+ transaction_validator: Any | None,
732
+ ) -> ReviewResult:
733
+ """Verify one review from an already-bound absolute run-directory chain."""
734
+
735
+ validate_transaction = transaction_validator or (lambda: _validate_review_run_handle(handle))
736
+ validate_transaction()
737
+ candidate_leases: list[Any] = []
738
+ verified_candidate = verify_candidate(
739
+ handle.run_dir,
740
+ _run_fd=handle.run_fd,
741
+ _ancestor_validator=validate_transaction,
742
+ _retained_leases=candidate_leases,
743
+ )
744
+ if len(candidate_leases) != 1:
745
+ for lease in candidate_leases:
746
+ lease.close()
747
+ raise ReviewError(
748
+ "candidate verification did not retain its exact snapshot",
749
+ code="DECISION_SNAPSHOT_MISSING",
750
+ )
751
+ candidate_lease = candidate_leases[0]
752
+ review_lease: _ReviewSnapshotLease | None = None
753
+ try:
754
+ validate_transaction()
755
+ envelope, review_lease = _read_sealed_review_tree(
756
+ handle,
757
+ retained_review_fd=retained_review_fd,
758
+ expected_review_identity=expected_review_identity,
759
+ )
760
+ result = _verify_review_snapshot(
761
+ handle=handle,
762
+ verified_candidate=verified_candidate,
763
+ envelope=envelope,
764
+ trusted_verifier_public_key=trusted_verifier_public_key,
765
+ trusted_assignment_value=trusted_assignment_value,
766
+ )
767
+ candidate_lease.validate()
768
+ review_lease.validate()
769
+ validate_transaction()
770
+ if retained_snapshot_leases is not None:
771
+ retained_snapshot_leases.extend([candidate_lease, review_lease])
772
+ return result
773
+ review_lease.close()
774
+ candidate_lease.close()
775
+ return result
776
+ except Exception:
777
+ if review_lease is not None:
778
+ review_lease.close()
779
+ candidate_lease.close()
780
+ raise
781
+
782
+
783
+ def _verify_review_snapshot(
784
+ *,
785
+ handle: _ReviewRunHandle,
786
+ verified_candidate: Any,
787
+ envelope: dict[str, bytes],
788
+ trusted_verifier_public_key: Ed25519PublicKey,
789
+ trusted_assignment_value: Any | None,
790
+ ) -> ReviewResult:
791
+ """Authenticate one retained candidate/review byte snapshot."""
792
+
793
+ review_dir = handle.run_dir / "review"
794
+ assignment = _decode_json(envelope["assignment.json"], "assignment")
795
+ assignment = _parse_assignment(assignment)
796
+ if trusted_assignment_value is not None:
797
+ trusted_assignment = _parse_assignment(trusted_assignment_value)
798
+ if canonical_json_line_bytes(assignment) != canonical_json_line_bytes(trusted_assignment):
799
+ raise ReviewError(
800
+ "review assignment is not the trusted coordinator enrollment",
801
+ code="ASSIGNMENT_NOT_ENROLLED",
802
+ )
803
+ if assignment["verifier"]["public_key"] != public_key_to_base64(trusted_verifier_public_key):
804
+ raise ReviewError(
805
+ "review verifier is not the trusted enrolled key",
806
+ code="ASSIGNMENT_VERIFIER_NOT_ENROLLED",
807
+ )
808
+ _validate_assignment_against_candidate(assignment, verified_candidate)
809
+
810
+ reports_by_lens = {
811
+ lens: _parse_signed_report(_decode_json(envelope[f"reports/{lens}.json"], f"report {lens}"))
812
+ for lens in REQUIRED_LENSES
813
+ }
814
+ assignments_by_lens = {item["lens"]: item for item in assignment["reviewers"]}
815
+ for lens, report in reports_by_lens.items():
816
+ _verify_report(report, assignments_by_lens[lens], verified_candidate, assignment)
817
+ expected_findings = _normalize_findings(reports_by_lens)
818
+ findings = _decode_json(envelope["findings.json"], "findings")
819
+ if findings != expected_findings:
820
+ raise ReviewError("normalized findings were changed", code="DECISION_FINDINGS_CHANGED")
821
+ expected_backlog = [
822
+ finding for finding in expected_findings if finding["severity"] in {"medium", "low"}
823
+ ]
824
+ backlog = _decode_json(envelope["backlog.json"], "backlog")
825
+ if backlog != expected_backlog:
826
+ raise ReviewError("review backlog was changed", code="DECISION_BACKLOG_CHANGED")
827
+
828
+ decision = _decode_json(envelope["decision.json"], "decision")
829
+ if not isinstance(decision, dict) or set(decision) != {
830
+ "schema_version",
831
+ "candidate_digest",
832
+ "table_sha256",
833
+ "validation_policy_digest",
834
+ "assignment_sha256",
835
+ "report_sha256s",
836
+ "findings_sha256",
837
+ "backlog_sha256",
838
+ "cycle_lineage",
839
+ "failed_cycle_count",
840
+ "verifier_version",
841
+ "verifier_principal",
842
+ "status",
843
+ "decision_digest",
844
+ "signature",
845
+ }:
846
+ raise ReviewError("review decision fields are invalid", code="DECISION_FIELDS_INVALID")
847
+ signed_body = {key: value for key, value in decision.items() if key != "signature"}
848
+ payload = {key: value for key, value in signed_body.items() if key != "decision_digest"}
849
+ if decision["decision_digest"] != sha256_bytes(canonical_json_line_bytes(payload)):
850
+ raise ReviewError("review decision digest is invalid", code="DECISION_DIGEST_INVALID")
851
+ expected_hashes = {
852
+ "assignment_sha256": sha256_bytes(envelope["assignment.json"]),
853
+ "report_sha256s": {
854
+ lens: sha256_bytes(envelope[f"reports/{lens}.json"]) for lens in sorted(REQUIRED_LENSES)
855
+ },
856
+ "findings_sha256": sha256_bytes(envelope["findings.json"]),
857
+ "backlog_sha256": sha256_bytes(envelope["backlog.json"]),
858
+ }
859
+ for field, expected in expected_hashes.items():
860
+ if decision[field] != expected:
861
+ raise ReviewError(
862
+ f"review decision {field} does not match its envelope",
863
+ code="DECISION_FIELD_MISMATCH",
864
+ )
865
+ expected_status = _derive_review_status(findings, assignment["failed_cycle_count"])
866
+ expected_payload_values = {
867
+ "schema_version": DECISION_SCHEMA_VERSION,
868
+ "candidate_digest": verified_candidate.candidate_digest,
869
+ "table_sha256": verified_candidate.table_sha256,
870
+ "validation_policy_digest": verified_candidate.validation_policy_digest,
871
+ "cycle_lineage": assignment["cycle_lineage"],
872
+ "failed_cycle_count": assignment["failed_cycle_count"],
873
+ "verifier_version": VERIFIER_VERSION,
874
+ "verifier_principal": assignment["verifier"]["principal_id"],
875
+ "status": expected_status,
876
+ }
877
+ for field, expected in expected_payload_values.items():
878
+ if decision[field] != expected:
879
+ raise ReviewError(f"review decision {field} is invalid", code="DECISION_FIELD_INVALID")
880
+ try:
881
+ signature = base64.b64decode(decision["signature"], validate=True)
882
+ trusted_verifier_public_key.verify(signature, canonical_json_line_bytes(signed_body))
883
+ except (InvalidSignature, ValueError, TypeError) as exc:
884
+ raise ReviewError(
885
+ "review decision signature is invalid", code="DECISION_SIGNATURE_INVALID"
886
+ ) from exc
887
+ _validate_review_run_handle(handle)
888
+ return ReviewResult(
889
+ status=expected_status,
890
+ candidate_digest=verified_candidate.candidate_digest,
891
+ decision_digest=decision["decision_digest"],
892
+ review_dir=review_dir,
893
+ decision_document=dict(decision),
894
+ )
895
+
896
+
897
+ def _parse_assignment(value: Any) -> dict[str, Any]:
898
+ data = _object(value, "assignment")
899
+ _exact_fields(
900
+ data,
901
+ {
902
+ "schema_version",
903
+ "candidate_digest",
904
+ "validation_policy_digest",
905
+ "cycle_lineage",
906
+ "failed_cycle_count",
907
+ "producer",
908
+ "reviewers",
909
+ "verifier",
910
+ },
911
+ "assignment",
912
+ )
913
+ if data["schema_version"] != ASSIGNMENT_SCHEMA_VERSION:
914
+ raise ReviewError(
915
+ "assignment schema version is unsupported", code="ASSIGNMENT_VERSION_UNSUPPORTED"
916
+ )
917
+ normalized = {
918
+ "schema_version": ASSIGNMENT_SCHEMA_VERSION,
919
+ "candidate_digest": _digest(data["candidate_digest"], "candidate_digest"),
920
+ "validation_policy_digest": _digest(
921
+ data["validation_policy_digest"], "validation_policy_digest"
922
+ ),
923
+ "cycle_lineage": _identifier(data["cycle_lineage"], "cycle_lineage"),
924
+ "failed_cycle_count": data["failed_cycle_count"],
925
+ "producer": _parse_principal_attempt(data["producer"], "producer"),
926
+ "reviewers": _parse_reviewers(data["reviewers"]),
927
+ "verifier": _parse_verifier(data["verifier"]),
928
+ }
929
+ if type(normalized["failed_cycle_count"]) is not int or normalized["failed_cycle_count"] != 0:
930
+ raise ReviewError(
931
+ "failed_cycle_count must be 0 for the current initial-candidate review schema",
932
+ code="ASSIGNMENT_CYCLE_COUNT_INVALID",
933
+ )
934
+ reviewers = normalized["reviewers"]
935
+ principals = [
936
+ normalized["producer"]["principal_id"],
937
+ *(item["principal_id"] for item in reviewers),
938
+ normalized["verifier"]["principal_id"],
939
+ ]
940
+ if len(principals) != len(set(principals)):
941
+ raise ReviewError(
942
+ "producer, reviewers, and verifier must be distinct principals",
943
+ code="ASSIGNMENT_PRINCIPALS_NOT_DISTINCT",
944
+ )
945
+ attempts = [normalized["producer"]["attempt_id"], *(item["attempt_id"] for item in reviewers)]
946
+ if len(attempts) != len(set(attempts)):
947
+ raise ReviewError(
948
+ "producer and reviewer attempts must be unique", code="ASSIGNMENT_ATTEMPTS_NOT_DISTINCT"
949
+ )
950
+ keys = [item["public_key"] for item in reviewers] + [normalized["verifier"]["public_key"]]
951
+ if len(keys) != len(set(keys)):
952
+ raise ReviewError(
953
+ "reviewer and verifier signing keys must be distinct",
954
+ code="ASSIGNMENT_KEYS_NOT_DISTINCT",
955
+ )
956
+ key_ids = [item["key_id"] for item in reviewers]
957
+ if len(key_ids) != len(set(key_ids)):
958
+ raise ReviewError(
959
+ "reviewer signing key IDs must be distinct", code="ASSIGNMENT_KEY_IDS_NOT_DISTINCT"
960
+ )
961
+ return normalized
962
+
963
+
964
+ def _parse_reviewers(value: Any) -> list[dict[str, str]]:
965
+ if not isinstance(value, list) or len(value) != 2:
966
+ raise ReviewError(
967
+ "assignment must contain exactly two reviewers", code="ASSIGNMENT_REVIEWER_COUNT"
968
+ )
969
+ reviewers = []
970
+ for index, item in enumerate(value):
971
+ data = _object(item, f"reviewers[{index}]")
972
+ _exact_fields(
973
+ data,
974
+ {"principal_id", "attempt_id", "lens", "key_id", "public_key"},
975
+ f"reviewers[{index}]",
976
+ )
977
+ reviewer = {
978
+ **_parse_principal_attempt(
979
+ data,
980
+ f"reviewers[{index}]",
981
+ extra={"lens", "key_id", "public_key"},
982
+ ),
983
+ "lens": _choice(data["lens"], REQUIRED_LENSES, f"reviewers[{index}].lens"),
984
+ "key_id": _identifier(data["key_id"], f"reviewers[{index}].key_id"),
985
+ "public_key": _validated_public_key(data["public_key"]),
986
+ }
987
+ reviewers.append(reviewer)
988
+ if {reviewer["lens"] for reviewer in reviewers} != REQUIRED_LENSES:
989
+ raise ReviewError(
990
+ "assignment must cover both required lenses", code="ASSIGNMENT_LENSES_INCOMPLETE"
991
+ )
992
+ return sorted(reviewers, key=lambda item: item["lens"])
993
+
994
+
995
+ def _parse_verifier(value: Any) -> dict[str, str]:
996
+ data = _object(value, "verifier")
997
+ _exact_fields(data, {"principal_id", "public_key"}, "verifier")
998
+ return {
999
+ "principal_id": _identifier(data["principal_id"], "verifier.principal_id"),
1000
+ "public_key": _validated_public_key(data["public_key"]),
1001
+ }
1002
+
1003
+
1004
+ def _parse_principal_attempt(
1005
+ value: Any, name: str, *, extra: set[str] | None = None
1006
+ ) -> dict[str, str]:
1007
+ data = _object(value, name)
1008
+ fields = {"principal_id", "attempt_id", *(extra or set())}
1009
+ _exact_fields(data, fields, name)
1010
+ return {
1011
+ "principal_id": _identifier(data["principal_id"], f"{name}.principal_id"),
1012
+ "attempt_id": _identifier(data["attempt_id"], f"{name}.attempt_id"),
1013
+ }
1014
+
1015
+
1016
+ def _parse_report_payload(value: Any) -> dict[str, Any]:
1017
+ data = _object(value, "report")
1018
+ _exact_fields(
1019
+ data,
1020
+ {
1021
+ "schema_version",
1022
+ "assignment_digest",
1023
+ "candidate_digest",
1024
+ "validation_policy_digest",
1025
+ "cycle_lineage",
1026
+ "failed_cycle_count",
1027
+ "reviewer_principal",
1028
+ "reviewer_attempt",
1029
+ "reviewer_key_id",
1030
+ "lens",
1031
+ "reviewed_artifact_paths",
1032
+ "summary",
1033
+ "findings",
1034
+ },
1035
+ "report",
1036
+ )
1037
+ if data["schema_version"] != REPORT_SCHEMA_VERSION:
1038
+ raise ReviewError("report schema version is unsupported", code="REPORT_VERSION_UNSUPPORTED")
1039
+ findings = data["findings"]
1040
+ if not isinstance(findings, list):
1041
+ raise ReviewError("report findings must be an array", code="REPORT_FINDINGS_INVALID")
1042
+ failed_cycle_count = data["failed_cycle_count"]
1043
+ if type(failed_cycle_count) is not int or failed_cycle_count != 0:
1044
+ raise ReviewError(
1045
+ "report failed_cycle_count must be 0 for the current initial-candidate review schema",
1046
+ code="REPORT_CYCLE_COUNT_INVALID",
1047
+ )
1048
+ reviewed_artifact_paths = _parse_artifact_paths(data["reviewed_artifact_paths"])
1049
+ normalized_findings = [
1050
+ _parse_finding(item, index, data["reviewer_principal"])
1051
+ for index, item in enumerate(findings)
1052
+ ]
1053
+ ids = [finding["finding_id"] for finding in normalized_findings]
1054
+ if len(ids) != len(set(ids)):
1055
+ raise ReviewError(
1056
+ "finding IDs must be unique within a report", code="REPORT_FINDING_IDS_DUPLICATE"
1057
+ )
1058
+ return {
1059
+ "schema_version": REPORT_SCHEMA_VERSION,
1060
+ "assignment_digest": _digest(data["assignment_digest"], "assignment_digest"),
1061
+ "candidate_digest": _digest(data["candidate_digest"], "candidate_digest"),
1062
+ "validation_policy_digest": _digest(
1063
+ data["validation_policy_digest"], "validation_policy_digest"
1064
+ ),
1065
+ "cycle_lineage": _identifier(data["cycle_lineage"], "cycle_lineage"),
1066
+ "failed_cycle_count": failed_cycle_count,
1067
+ "reviewer_principal": _identifier(data["reviewer_principal"], "reviewer_principal"),
1068
+ "reviewer_attempt": _identifier(data["reviewer_attempt"], "reviewer_attempt"),
1069
+ "reviewer_key_id": _identifier(data["reviewer_key_id"], "reviewer_key_id"),
1070
+ "lens": _choice(data["lens"], REQUIRED_LENSES, "lens"),
1071
+ "reviewed_artifact_paths": reviewed_artifact_paths,
1072
+ "summary": _text(data["summary"], "summary"),
1073
+ "findings": normalized_findings,
1074
+ }
1075
+
1076
+
1077
+ def _parse_signed_report(value: Any) -> dict[str, Any]:
1078
+ data = _object(value, "signed report")
1079
+ if "signature" not in data:
1080
+ raise ReviewError("signed report is missing signature", code="REPORT_SIGNATURE_MISSING")
1081
+ payload = _parse_report_payload({key: val for key, val in data.items() if key != "signature"})
1082
+ signature = data["signature"]
1083
+ if not isinstance(signature, str):
1084
+ raise ReviewError("report signature must be base64 text", code="REPORT_SIGNATURE_INVALID")
1085
+ try:
1086
+ base64.b64decode(signature, validate=True)
1087
+ except (ValueError, TypeError) as exc:
1088
+ raise ReviewError(
1089
+ "report signature is invalid base64", code="REPORT_SIGNATURE_INVALID"
1090
+ ) from exc
1091
+ return {**payload, "signature": signature}
1092
+
1093
+
1094
+ def _derive_review_status(findings: list[dict[str, Any]], failed_cycle_count: int) -> str:
1095
+ """Derive one canonical review status for finalization and later verification."""
1096
+
1097
+ if type(failed_cycle_count) is not int or failed_cycle_count != 0:
1098
+ raise ReviewError(
1099
+ "failed_cycle_count must be 0 for the current initial-candidate review schema",
1100
+ code="REPORT_CYCLE_COUNT_INVALID",
1101
+ )
1102
+ has_blocking = any(finding["severity"] in _BLOCKING_SEVERITIES for finding in findings)
1103
+ if has_blocking:
1104
+ return "fixes_required"
1105
+ return "reviewed_candidate"
1106
+
1107
+
1108
+ def _parse_finding(value: Any, index: int, reviewer_principal: Any) -> dict[str, Any]:
1109
+ name = f"findings[{index}]"
1110
+ data = _object(value, name)
1111
+ _exact_fields(
1112
+ data,
1113
+ {
1114
+ "finding_id",
1115
+ "severity",
1116
+ "invariant",
1117
+ "evidence",
1118
+ "affected_artifact",
1119
+ "reviewer",
1120
+ "repair_owner",
1121
+ "acceptance_condition",
1122
+ "status",
1123
+ "resolution_evidence",
1124
+ },
1125
+ name,
1126
+ )
1127
+ reviewer = _identifier(data["reviewer"], f"{name}.reviewer")
1128
+ if reviewer != reviewer_principal:
1129
+ raise ReviewError(
1130
+ f"{name}.reviewer must match the report reviewer",
1131
+ code="REPORT_FINDING_REVIEWER_MISMATCH",
1132
+ )
1133
+ if data["status"] != "open" or data["resolution_evidence"] is not None:
1134
+ raise ReviewError(
1135
+ f"{name} must be an open finding without resolution evidence",
1136
+ code="REPORT_FINDING_NOT_OPEN",
1137
+ )
1138
+ return {
1139
+ "finding_id": _identifier(data["finding_id"], f"{name}.finding_id"),
1140
+ "severity": _choice(data["severity"], _SEVERITIES, f"{name}.severity"),
1141
+ "invariant": _text(data["invariant"], f"{name}.invariant"),
1142
+ "evidence": _text(data["evidence"], f"{name}.evidence"),
1143
+ "affected_artifact": _text(data["affected_artifact"], f"{name}.affected_artifact"),
1144
+ "reviewer": reviewer,
1145
+ "repair_owner": _identifier(data["repair_owner"], f"{name}.repair_owner"),
1146
+ "acceptance_condition": _text(data["acceptance_condition"], f"{name}.acceptance_condition"),
1147
+ "status": "open",
1148
+ "resolution_evidence": None,
1149
+ }
1150
+
1151
+
1152
+ def _verify_report(
1153
+ report: dict[str, Any],
1154
+ reviewer_assignment: dict[str, str],
1155
+ candidate: Any,
1156
+ assignment: dict[str, Any],
1157
+ ) -> None:
1158
+ expected = {
1159
+ "assignment_digest": _normalized_assignment_digest(assignment),
1160
+ "candidate_digest": candidate.candidate_digest,
1161
+ "validation_policy_digest": candidate.validation_policy_digest,
1162
+ "cycle_lineage": assignment["cycle_lineage"],
1163
+ "failed_cycle_count": assignment["failed_cycle_count"],
1164
+ "reviewer_principal": reviewer_assignment["principal_id"],
1165
+ "reviewer_attempt": reviewer_assignment["attempt_id"],
1166
+ "reviewer_key_id": reviewer_assignment["key_id"],
1167
+ "lens": reviewer_assignment["lens"],
1168
+ "reviewed_artifact_paths": _candidate_artifact_paths(candidate),
1169
+ }
1170
+ for field, value in expected.items():
1171
+ if report[field] != value:
1172
+ raise ReviewError(
1173
+ f"report {field} does not match its assignment/candidate",
1174
+ code="REPORT_FIELD_MISMATCH",
1175
+ )
1176
+ payload = {key: value for key, value in report.items() if key != "signature"}
1177
+ try:
1178
+ signature = base64.b64decode(report["signature"], validate=True)
1179
+ load_public_key_base64(reviewer_assignment["public_key"]).verify(
1180
+ signature, canonical_json_line_bytes(payload)
1181
+ )
1182
+ except (InvalidSignature, ValueError, TypeError) as exc:
1183
+ raise ReviewError(
1184
+ "review report signature is invalid", code="REPORT_SIGNATURE_INVALID"
1185
+ ) from exc
1186
+
1187
+
1188
+ def _normalize_findings(reports_by_lens: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
1189
+ findings = [
1190
+ finding for lens in sorted(reports_by_lens) for finding in reports_by_lens[lens]["findings"]
1191
+ ]
1192
+ ids = [finding["finding_id"] for finding in findings]
1193
+ if len(ids) != len(set(ids)):
1194
+ raise ReviewError(
1195
+ "finding IDs must be unique across reports", code="REPORT_FINDING_IDS_DUPLICATE"
1196
+ )
1197
+ return sorted(findings, key=lambda finding: finding["finding_id"])
1198
+
1199
+
1200
+ def _validate_assignment_against_candidate(assignment: dict[str, Any], candidate: Any) -> None:
1201
+ if assignment["candidate_digest"] != candidate.candidate_digest:
1202
+ raise ReviewError(
1203
+ "assignment candidate digest does not match", code="ASSIGNMENT_CANDIDATE_MISMATCH"
1204
+ )
1205
+ if assignment["validation_policy_digest"] != candidate.validation_policy_digest:
1206
+ raise ReviewError(
1207
+ "assignment validation policy digest does not match", code="ASSIGNMENT_POLICY_MISMATCH"
1208
+ )
1209
+ if candidate.producer_principal != assignment["producer"]["principal_id"]:
1210
+ raise ReviewError(
1211
+ "assignment producer principal does not match sealed execution",
1212
+ code="ASSIGNMENT_PRODUCER_MISMATCH",
1213
+ )
1214
+ if candidate.producer_attempt != assignment["producer"]["attempt_id"]:
1215
+ raise ReviewError(
1216
+ "assignment producer attempt does not match sealed execution",
1217
+ code="ASSIGNMENT_PRODUCER_ATTEMPT_MISMATCH",
1218
+ )
1219
+
1220
+
1221
+ def _normalized_assignment_digest(assignment: dict[str, Any]) -> str:
1222
+ return sha256_bytes(canonical_json_line_bytes(assignment))
1223
+
1224
+
1225
+ def _candidate_artifact_paths(candidate: Any) -> list[str]:
1226
+ members = candidate.manifest.get("members")
1227
+ if not isinstance(members, list): # verify_candidate should make this unreachable
1228
+ raise ReviewError(
1229
+ "verified candidate does not expose its artifact members",
1230
+ code="CANDIDATE_MEMBER_MISMATCH",
1231
+ )
1232
+ paths = ["candidate/manifest.json"]
1233
+ for member in members:
1234
+ if not isinstance(member, dict) or not isinstance(member.get("path"), str):
1235
+ raise ReviewError(
1236
+ "verified candidate contains an invalid artifact member",
1237
+ code="CANDIDATE_MEMBER_INVALID",
1238
+ )
1239
+ paths.append(f"candidate/{member['path']}")
1240
+ return sorted(paths)
1241
+
1242
+
1243
+ def _parse_artifact_paths(value: Any) -> list[str]:
1244
+ if not isinstance(value, list) or not value:
1245
+ raise ReviewError(
1246
+ "reviewed_artifact_paths must be a non-empty array",
1247
+ code="REPORT_ARTIFACT_PATHS_INVALID",
1248
+ )
1249
+ paths: list[str] = []
1250
+ for index, item in enumerate(value):
1251
+ if not isinstance(item, str) or "\\" in item:
1252
+ raise ReviewError(
1253
+ f"reviewed_artifact_paths[{index}] is invalid", code="REPORT_ARTIFACT_PATHS_INVALID"
1254
+ )
1255
+ path = PurePosixPath(item)
1256
+ if (
1257
+ path.is_absolute()
1258
+ or not path.parts
1259
+ or any(part in {"", ".", ".."} for part in path.parts)
1260
+ ):
1261
+ raise ReviewError(
1262
+ f"reviewed_artifact_paths[{index}] is not normalized and relative",
1263
+ code="REPORT_ARTIFACT_PATHS_INVALID",
1264
+ )
1265
+ paths.append(path.as_posix())
1266
+ if paths != sorted(set(paths)):
1267
+ raise ReviewError(
1268
+ "reviewed_artifact_paths must be unique and sorted",
1269
+ code="REPORT_ARTIFACT_PATHS_INVALID",
1270
+ )
1271
+ return paths
1272
+
1273
+
1274
+ def _validated_public_key(value: Any) -> str:
1275
+ if not isinstance(value, str):
1276
+ raise ReviewError("public key must be base64 text", code="KEY_ENCODING_INVALID")
1277
+ load_public_key_base64(value)
1278
+ return value
1279
+
1280
+
1281
+ def _object(value: Any, name: str) -> dict[str, Any]:
1282
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
1283
+ raise ReviewError(
1284
+ f"{name} must be an object with string keys", code="ENVELOPE_FIELD_NOT_AN_OBJECT"
1285
+ )
1286
+ return value
1287
+
1288
+
1289
+ def _exact_fields(data: dict[str, Any], expected: set[str], name: str) -> None:
1290
+ if set(data) != expected:
1291
+ raise ReviewError(f"{name} fields are invalid", code="ENVELOPE_FIELDS_INVALID")
1292
+
1293
+
1294
+ def _identifier(value: Any, name: str) -> str:
1295
+ if not isinstance(value, str) or not value:
1296
+ raise ReviewError(
1297
+ f"{name} must be a non-empty identifier", code="ENVELOPE_FIELD_NOT_AN_IDENTIFIER"
1298
+ )
1299
+ candidate = value.replace("-", "a").replace("_", "a")
1300
+ if not candidate.isalnum():
1301
+ raise ReviewError(
1302
+ f"{name} contains unsupported characters", code="ENVELOPE_FIELD_UNSUPPORTED_CHARACTERS"
1303
+ )
1304
+ return value
1305
+
1306
+
1307
+ def _text(value: Any, name: str) -> str:
1308
+ if not isinstance(value, str) or not value.strip():
1309
+ raise ReviewError(f"{name} must be non-empty text", code="ENVELOPE_FIELD_EMPTY")
1310
+ return value
1311
+
1312
+
1313
+ def _digest(value: Any, name: str) -> str:
1314
+ if (
1315
+ not isinstance(value, str)
1316
+ or len(value) != 64
1317
+ or any(char not in "0123456789abcdef" for char in value)
1318
+ ):
1319
+ raise ReviewError(
1320
+ f"{name} must be a lowercase SHA-256 digest", code="ENVELOPE_FIELD_NOT_A_DIGEST"
1321
+ )
1322
+ return value
1323
+
1324
+
1325
+ def _choice(value: Any, choices: frozenset[str], name: str) -> str:
1326
+ if not isinstance(value, str) or value not in choices:
1327
+ raise ReviewError(
1328
+ f"{name} must be one of {sorted(choices)}", code="ENVELOPE_FIELD_NOT_A_CHOICE"
1329
+ )
1330
+ return value
1331
+
1332
+
1333
+ def _decode_json(raw: bytes, name: str) -> Any:
1334
+ try:
1335
+ return json.loads(raw.decode("utf-8"))
1336
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
1337
+ raise ReviewError(f"{name} cannot be read", code="ENVELOPE_FILE_UNREADABLE") from exc
1338
+
1339
+
1340
+ def _absolute_lexical_path(path: Path) -> Path:
1341
+ """Return an absolute normalized path without resolving filesystem links."""
1342
+
1343
+ return Path(os.path.abspath(os.fspath(path)))
1344
+
1345
+
1346
+ def _open_review_run_handle(run_dir: Path) -> _ReviewRunHandle:
1347
+ """Bind every absolute run ancestor and start swap/restore monitoring."""
1348
+
1349
+ if not _secure_review_verification_supported():
1350
+ raise ReviewError(
1351
+ "secure descriptor-relative review verification is unavailable on this platform",
1352
+ code="PLATFORM_REVIEW_VERIFY_UNSUPPORTED",
1353
+ )
1354
+ run_dir = _absolute_lexical_path(run_dir)
1355
+ if run_dir == Path(run_dir.anchor) or not run_dir.name:
1356
+ raise ReviewError(
1357
+ "review run directory must name a non-root directory", code="CANDIDATE_ANCESTOR_INVALID"
1358
+ )
1359
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
1360
+ descriptors: list[int] = []
1361
+ records: list[_RetainedAncestorRecord] = []
1362
+ monitor: _ReviewAncestorMonitor | None = None
1363
+ try:
1364
+ anchor_fd = os.open(run_dir.anchor, directory_flags)
1365
+ descriptors.append(anchor_fd)
1366
+ anchor_info = os.fstat(anchor_fd)
1367
+ if not stat.S_ISDIR(anchor_info.st_mode):
1368
+ raise ReviewError(
1369
+ "review run filesystem anchor is not a directory", code="CANDIDATE_ANCESTOR_INVALID"
1370
+ )
1371
+ records.append(
1372
+ (
1373
+ anchor_fd,
1374
+ None,
1375
+ None,
1376
+ _entry_identity(anchor_info),
1377
+ "review run filesystem anchor",
1378
+ )
1379
+ )
1380
+
1381
+ parent_fd = anchor_fd
1382
+ traversed = Path(run_dir.anchor)
1383
+ for part in run_dir.parts[1:]:
1384
+ traversed /= part
1385
+ named = os.stat(part, dir_fd=parent_fd, follow_symlinks=False)
1386
+ if not stat.S_ISDIR(named.st_mode):
1387
+ raise ReviewError(
1388
+ f"review run ancestor is not a directory: {traversed}",
1389
+ code="CANDIDATE_ANCESTOR_INVALID",
1390
+ )
1391
+ descriptor = os.open(part, directory_flags, dir_fd=parent_fd)
1392
+ descriptors.append(descriptor)
1393
+ opened = os.fstat(descriptor)
1394
+ _require_same_identity(named, opened, f"review run ancestor {traversed}")
1395
+ records.append(
1396
+ (
1397
+ descriptor,
1398
+ parent_fd,
1399
+ part,
1400
+ _entry_identity(opened),
1401
+ f"review run ancestor {traversed}",
1402
+ )
1403
+ )
1404
+ parent_fd = descriptor
1405
+
1406
+ monitor = _create_review_ancestor_monitor(descriptors)
1407
+ handle = _ReviewRunHandle(
1408
+ run_dir=run_dir,
1409
+ descriptors=descriptors,
1410
+ records=records,
1411
+ monitor=monitor,
1412
+ )
1413
+ _validate_review_run_handle(handle)
1414
+ return handle
1415
+ except ReviewError:
1416
+ if monitor is not None:
1417
+ monitor.close()
1418
+ for descriptor in reversed(descriptors):
1419
+ os.close(descriptor)
1420
+ raise
1421
+ except OSError as exc:
1422
+ if monitor is not None:
1423
+ monitor.close()
1424
+ for descriptor in reversed(descriptors):
1425
+ os.close(descriptor)
1426
+ raise ReviewError(
1427
+ "review run ancestor chain cannot be opened safely", code="CANDIDATE_ANCESTOR_INVALID"
1428
+ ) from exc
1429
+
1430
+
1431
+ def _open_review_run_handle_from_parent(
1432
+ run_dir: Path,
1433
+ run_parent_fd: int,
1434
+ ) -> _ReviewRunHandle:
1435
+ """Bind a run child through an already-retained trusted parent descriptor."""
1436
+
1437
+ if not _secure_review_verification_supported():
1438
+ raise ReviewError(
1439
+ "secure descriptor-relative review verification is unavailable on this platform",
1440
+ code="PLATFORM_REVIEW_VERIFY_UNSUPPORTED",
1441
+ )
1442
+ run_dir = _absolute_lexical_path(run_dir)
1443
+ if not run_dir.name:
1444
+ raise ReviewError(
1445
+ "review run directory must name a non-root directory", code="CANDIDATE_ANCESTOR_INVALID"
1446
+ )
1447
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
1448
+ run_fd = -1
1449
+ monitor: _ReviewAncestorMonitor | None = None
1450
+ try:
1451
+ named = os.stat(run_dir.name, dir_fd=run_parent_fd, follow_symlinks=False)
1452
+ if not stat.S_ISDIR(named.st_mode):
1453
+ raise ReviewError(
1454
+ "review run child is not a directory", code="CANDIDATE_ANCESTOR_INVALID"
1455
+ )
1456
+ run_fd = os.open(run_dir.name, directory_flags, dir_fd=run_parent_fd)
1457
+ opened = os.fstat(run_fd)
1458
+ _require_same_identity(named, opened, "review run child")
1459
+ monitor = _create_review_ancestor_monitor([run_fd])
1460
+ handle = _ReviewRunHandle(
1461
+ run_dir=run_dir,
1462
+ descriptors=[run_fd],
1463
+ records=[
1464
+ (
1465
+ run_fd,
1466
+ run_parent_fd,
1467
+ run_dir.name,
1468
+ _entry_identity(opened),
1469
+ "review run child",
1470
+ )
1471
+ ],
1472
+ monitor=monitor,
1473
+ )
1474
+ _validate_review_run_handle(handle)
1475
+ return handle
1476
+ except ReviewError:
1477
+ if monitor is not None:
1478
+ monitor.close()
1479
+ if run_fd >= 0:
1480
+ os.close(run_fd)
1481
+ raise
1482
+ except OSError as exc:
1483
+ if monitor is not None:
1484
+ monitor.close()
1485
+ if run_fd >= 0:
1486
+ os.close(run_fd)
1487
+ raise ReviewError(
1488
+ "review run child cannot be opened safely", code="CANDIDATE_ANCESTOR_INVALID"
1489
+ ) from exc
1490
+
1491
+
1492
+ def _create_review_ancestor_monitor(descriptors: list[int]) -> _ReviewAncestorMonitor:
1493
+ """Watch retained ancestor inodes for move/delete/swap events."""
1494
+
1495
+ if hasattr(select, "kqueue"):
1496
+ queue = select.kqueue()
1497
+ try:
1498
+ flags = select.KQ_NOTE_DELETE | select.KQ_NOTE_RENAME | select.KQ_NOTE_REVOKE
1499
+ if hasattr(select, "KQ_NOTE_ATTRIB"):
1500
+ flags |= select.KQ_NOTE_ATTRIB
1501
+ changes = [
1502
+ select.kevent(
1503
+ descriptor,
1504
+ filter=select.KQ_FILTER_VNODE,
1505
+ flags=select.KQ_EV_ADD | select.KQ_EV_CLEAR,
1506
+ fflags=flags,
1507
+ )
1508
+ for descriptor in descriptors
1509
+ ]
1510
+ queue.control(changes, 0, 0)
1511
+ return _ReviewAncestorMonitor("kqueue", queue, max(1, len(descriptors)))
1512
+ except Exception:
1513
+ queue.close()
1514
+ raise
1515
+
1516
+ if sys.platform.startswith("linux"):
1517
+ monitor_fd = -1
1518
+ try:
1519
+ libc = ctypes.CDLL(None, use_errno=True)
1520
+ inotify_init1 = libc.inotify_init1
1521
+ inotify_init1.argtypes = [ctypes.c_int]
1522
+ inotify_init1.restype = ctypes.c_int
1523
+ inotify_add_watch = libc.inotify_add_watch
1524
+ inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
1525
+ inotify_add_watch.restype = ctypes.c_int
1526
+ monitor_fd = inotify_init1(os.O_CLOEXEC | os.O_NONBLOCK)
1527
+ if monitor_fd < 0:
1528
+ raise OSError(ctypes.get_errno(), "inotify_init1 failed")
1529
+ for descriptor in descriptors:
1530
+ descriptor_path = os.fsencode(f"/proc/self/fd/{descriptor}")
1531
+ if inotify_add_watch(monitor_fd, descriptor_path, _REVIEW_ANCESTOR_WATCH_MASK) < 0:
1532
+ raise OSError(ctypes.get_errno(), "inotify_add_watch failed")
1533
+ return _ReviewAncestorMonitor("inotify", monitor_fd, 1)
1534
+ except (AttributeError, OSError) as exc:
1535
+ if monitor_fd >= 0:
1536
+ os.close(monitor_fd)
1537
+ raise ReviewError(
1538
+ "secure review ancestor monitoring is unavailable on this platform",
1539
+ code="PLATFORM_REVIEW_MONITOR_UNSUPPORTED",
1540
+ ) from exc
1541
+
1542
+ raise ReviewError(
1543
+ "secure review ancestor monitoring is unavailable on this platform",
1544
+ code="PLATFORM_REVIEW_MONITOR_UNSUPPORTED",
1545
+ )
1546
+
1547
+
1548
+ def _validate_review_run_handle(handle: _ReviewRunHandle) -> None:
1549
+ """Reject ancestor substitution, including kernel-observed swap/restore."""
1550
+
1551
+ if handle.monitor.poll():
1552
+ raise ReviewError(
1553
+ "review run ancestor changed during the transaction", code="CANDIDATE_ANCESTOR_RACE"
1554
+ )
1555
+ try:
1556
+ for descriptor, parent_fd, name, expected, label in handle.records:
1557
+ opened = os.fstat(descriptor)
1558
+ if not stat.S_ISDIR(opened.st_mode) or _entry_identity(opened) != expected:
1559
+ raise ReviewError(
1560
+ f"{label} changed identity during the transaction",
1561
+ code="CANDIDATE_ANCESTOR_RACE",
1562
+ )
1563
+ if parent_fd is not None and name is not None:
1564
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1565
+ if not stat.S_ISDIR(named.st_mode) or _entry_identity(named) != expected:
1566
+ raise ReviewError(
1567
+ f"{label} path changed during the transaction",
1568
+ code="CANDIDATE_ANCESTOR_RACE",
1569
+ )
1570
+ except ReviewError:
1571
+ raise
1572
+ except OSError as exc:
1573
+ raise ReviewError(
1574
+ "review run ancestor became unavailable", code="CANDIDATE_ANCESTOR_RACE"
1575
+ ) from exc
1576
+ if handle.monitor.poll():
1577
+ raise ReviewError(
1578
+ "review run ancestor changed during the transaction", code="CANDIDATE_ANCESTOR_RACE"
1579
+ )
1580
+
1581
+
1582
+ def _acquire_review_publication_lock(
1583
+ handle: _ReviewRunHandle,
1584
+ ) -> _ReviewPublicationLock:
1585
+ """Serialize all finalizers for one run before candidate verification or staging."""
1586
+
1587
+ if fcntl is None: # pragma: no cover - rejected by the platform preflight
1588
+ raise ReviewError(
1589
+ "secure review publication locking is unavailable",
1590
+ code="PLATFORM_REVIEW_LOCK_UNSUPPORTED",
1591
+ )
1592
+ flags = (
1593
+ os.O_RDWR
1594
+ | os.O_CREAT
1595
+ | getattr(os, "O_NOFOLLOW", 0)
1596
+ | getattr(os, "O_CLOEXEC", 0)
1597
+ | getattr(os, "O_NONBLOCK", 0)
1598
+ )
1599
+ descriptor = -1
1600
+ try:
1601
+ descriptor = os.open(
1602
+ _REVIEW_PUBLICATION_LOCK,
1603
+ flags,
1604
+ 0o600,
1605
+ dir_fd=handle.run_fd,
1606
+ )
1607
+ opened = os.fstat(descriptor)
1608
+ _require_review_publication_lock(opened)
1609
+ named = os.stat(
1610
+ _REVIEW_PUBLICATION_LOCK,
1611
+ dir_fd=handle.run_fd,
1612
+ follow_symlinks=False,
1613
+ )
1614
+ _require_same_identity(opened, named, "review publication lock")
1615
+
1616
+ fcntl.flock(descriptor, fcntl.LOCK_EX)
1617
+ locked = os.fstat(descriptor)
1618
+ _require_same_identity(opened, locked, "review publication lock")
1619
+ _require_review_publication_lock(locked)
1620
+ os.fchmod(descriptor, 0o600)
1621
+ locked = os.fstat(descriptor)
1622
+ _require_review_publication_lock(locked)
1623
+ named = os.stat(
1624
+ _REVIEW_PUBLICATION_LOCK,
1625
+ dir_fd=handle.run_fd,
1626
+ follow_symlinks=False,
1627
+ )
1628
+ _require_same_identity(locked, named, "review publication lock")
1629
+ _require_review_publication_lock(named)
1630
+ publication_lock = _ReviewPublicationLock(
1631
+ handle=handle,
1632
+ descriptor=descriptor,
1633
+ lock_stat=locked,
1634
+ )
1635
+ publication_lock.validate()
1636
+ return publication_lock
1637
+ except ReviewError:
1638
+ if descriptor >= 0:
1639
+ os.close(descriptor)
1640
+ raise
1641
+ except OSError as exc:
1642
+ if descriptor >= 0:
1643
+ os.close(descriptor)
1644
+ raise ReviewError(
1645
+ "review publication lock cannot be acquired safely", code="LOCK_UNSAFE"
1646
+ ) from exc
1647
+
1648
+
1649
+ def _require_review_publication_lock(info: os.stat_result) -> None:
1650
+ expected_uid = os.geteuid() if hasattr(os, "geteuid") else info.st_uid
1651
+ if (
1652
+ not stat.S_ISREG(info.st_mode)
1653
+ or info.st_nlink != 1
1654
+ or info.st_size != 0
1655
+ or info.st_uid != expected_uid
1656
+ or stat.S_IMODE(info.st_mode) != 0o600
1657
+ ):
1658
+ raise ReviewError(
1659
+ "review publication lock is not an owned empty single-link mode-0600 regular file",
1660
+ code="LOCK_INVALID",
1661
+ )
1662
+
1663
+
1664
+ def _named_entry_has_identity(
1665
+ parent_fd: int,
1666
+ name: str,
1667
+ expected_identity: tuple[int, int, int] | None,
1668
+ ) -> bool:
1669
+ if expected_identity is None:
1670
+ return False
1671
+ try:
1672
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1673
+ except OSError:
1674
+ return False
1675
+ return _entry_identity(named) == expected_identity
1676
+
1677
+
1678
+ def _review_durability_barrier(descriptor: int, boundary: str) -> None:
1679
+ """Persist one review directory boundary or fail without deleting publication."""
1680
+
1681
+ try:
1682
+ os.fsync(descriptor)
1683
+ except OSError as exc:
1684
+ raise ReviewError(
1685
+ f"review durability barrier failed: {boundary}", code="ENVELOPE_DURABILITY_FAILED"
1686
+ ) from exc
1687
+
1688
+
1689
+ def _ensure_installed_review_durable(
1690
+ handle: _ReviewRunHandle,
1691
+ *,
1692
+ retained_review_fd: int | None = None,
1693
+ ) -> None:
1694
+ """Retryable post-rename durability barriers for an installed review."""
1695
+
1696
+ owned = retained_review_fd is None
1697
+ review_fd = retained_review_fd if retained_review_fd is not None else -1
1698
+ try:
1699
+ review_named = os.stat("review", dir_fd=handle.run_fd, follow_symlinks=False)
1700
+ _require_sealed_directory(review_named, "review root")
1701
+ if review_fd < 0:
1702
+ review_fd = os.open(
1703
+ "review",
1704
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
1705
+ dir_fd=handle.run_fd,
1706
+ )
1707
+ review_opened = os.fstat(review_fd)
1708
+ _require_same_identity(review_named, review_opened, "review root")
1709
+ _require_sealed_directory(review_opened, "review root")
1710
+ _review_durability_barrier(review_fd, "review-root")
1711
+ _review_durability_barrier(handle.run_fd, "review-run-parent")
1712
+ review_after = os.stat("review", dir_fd=handle.run_fd, follow_symlinks=False)
1713
+ _require_stable_stat(review_opened, review_after, "review root")
1714
+ except ReviewError:
1715
+ raise
1716
+ except OSError as exc:
1717
+ raise ReviewError(
1718
+ "installed review durability cannot be established safely",
1719
+ code="ENVELOPE_DURABILITY_FAILED",
1720
+ ) from exc
1721
+ finally:
1722
+ if owned and review_fd >= 0:
1723
+ os.close(review_fd)
1724
+
1725
+
1726
+ def _stage_review_envelope(
1727
+ root_fd: int,
1728
+ *,
1729
+ parent_fd: int,
1730
+ staging_name: str,
1731
+ expected_root_identity: tuple[int, int, int],
1732
+ assignment: dict[str, Any],
1733
+ reports_by_lens: dict[str, dict[str, Any]],
1734
+ findings: list[dict[str, Any]],
1735
+ backlog: list[dict[str, Any]],
1736
+ status: str,
1737
+ verified_candidate: Any,
1738
+ verifier_private_key: TrustedSigner,
1739
+ ) -> str:
1740
+ """Write, hash, and seal the exact retained staging descriptors."""
1741
+
1742
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
1743
+ reports_fd = -1
1744
+ # Member label -> (parent descriptor, name, open descriptor, sealed stat).
1745
+ retained: dict[str, tuple[int, str, int, os.stat_result]] = {}
1746
+ try:
1747
+ root_opened = os.fstat(root_fd)
1748
+ if _entry_identity(root_opened) != expected_root_identity:
1749
+ raise ReviewError(
1750
+ "review staging root changed identity before writing",
1751
+ code="ENVELOPE_STAGING_ROOT_CHANGED",
1752
+ )
1753
+ if set(os.listdir(root_fd)):
1754
+ raise ReviewError(
1755
+ "review staging root was not empty before writing",
1756
+ code="ENVELOPE_STAGING_ROOT_NOT_EMPTY",
1757
+ )
1758
+
1759
+ os.mkdir("reports", mode=0o700, dir_fd=root_fd)
1760
+ reports_named = os.stat("reports", dir_fd=root_fd, follow_symlinks=False)
1761
+ if not stat.S_ISDIR(reports_named.st_mode):
1762
+ raise ReviewError(
1763
+ "review reports member is not a directory", code="ENVELOPE_REPORTS_MEMBER_INVALID"
1764
+ )
1765
+ reports_fd = os.open("reports", directory_flags, dir_fd=root_fd)
1766
+ reports_opened = os.fstat(reports_fd)
1767
+ _require_same_identity(reports_named, reports_opened, "review reports directory")
1768
+
1769
+ assignment_fd, assignment_stat, assignment_sha256 = _create_hash_and_seal_member(
1770
+ root_fd,
1771
+ "assignment.json",
1772
+ "assignment.json",
1773
+ canonical_json_line_bytes(assignment),
1774
+ )
1775
+ retained["assignment.json"] = (
1776
+ root_fd,
1777
+ "assignment.json",
1778
+ assignment_fd,
1779
+ assignment_stat,
1780
+ )
1781
+ report_sha256s: dict[str, str] = {}
1782
+ for lens in sorted(reports_by_lens):
1783
+ name = f"{lens}.json"
1784
+ descriptor, sealed, digest = _create_hash_and_seal_member(
1785
+ reports_fd,
1786
+ name,
1787
+ f"reports/{name}",
1788
+ canonical_json_line_bytes(reports_by_lens[lens]),
1789
+ )
1790
+ retained[f"reports/{name}"] = (reports_fd, name, descriptor, sealed)
1791
+ report_sha256s[lens] = digest
1792
+
1793
+ findings_fd, findings_stat, findings_sha256 = _create_hash_and_seal_member(
1794
+ root_fd,
1795
+ "findings.json",
1796
+ "findings.json",
1797
+ canonical_json_line_bytes(findings),
1798
+ )
1799
+ retained["findings.json"] = (
1800
+ root_fd,
1801
+ "findings.json",
1802
+ findings_fd,
1803
+ findings_stat,
1804
+ )
1805
+ backlog_fd, backlog_stat, backlog_sha256 = _create_hash_and_seal_member(
1806
+ root_fd,
1807
+ "backlog.json",
1808
+ "backlog.json",
1809
+ canonical_json_line_bytes(backlog),
1810
+ )
1811
+ retained["backlog.json"] = (
1812
+ root_fd,
1813
+ "backlog.json",
1814
+ backlog_fd,
1815
+ backlog_stat,
1816
+ )
1817
+
1818
+ decision, decision_digest = _build_signed_decision(
1819
+ assignment=assignment,
1820
+ status=status,
1821
+ verified_candidate=verified_candidate,
1822
+ verifier_private_key=verifier_private_key,
1823
+ assignment_sha256=assignment_sha256,
1824
+ report_sha256s=report_sha256s,
1825
+ findings_sha256=findings_sha256,
1826
+ backlog_sha256=backlog_sha256,
1827
+ )
1828
+ decision_fd, decision_stat, _ = _create_hash_and_seal_member(
1829
+ root_fd,
1830
+ "decision.json",
1831
+ "decision.json",
1832
+ canonical_json_line_bytes(decision),
1833
+ )
1834
+ retained["decision.json"] = (
1835
+ root_fd,
1836
+ "decision.json",
1837
+ decision_fd,
1838
+ decision_stat,
1839
+ )
1840
+
1841
+ os.fchmod(reports_fd, 0o555)
1842
+ os.fsync(reports_fd)
1843
+ reports_sealed = os.fstat(reports_fd)
1844
+ _require_sealed_directory(reports_sealed, "review reports directory")
1845
+ _validate_staged_quiescence(
1846
+ parent_fd=parent_fd,
1847
+ staging_name=staging_name,
1848
+ expected_root_identity=expected_root_identity,
1849
+ root_fd=root_fd,
1850
+ reports_fd=reports_fd,
1851
+ reports_sealed=reports_sealed,
1852
+ retained=retained,
1853
+ )
1854
+ return decision_digest
1855
+ except ReviewError:
1856
+ raise
1857
+ except OSError as exc:
1858
+ raise ReviewError(
1859
+ "review staging envelope cannot be created safely", code="ENVELOPE_STAGING_UNSAFE"
1860
+ ) from exc
1861
+ finally:
1862
+ for _, _, descriptor, _ in retained.values():
1863
+ os.close(descriptor)
1864
+ if reports_fd >= 0:
1865
+ os.close(reports_fd)
1866
+
1867
+
1868
+ def _create_hash_and_seal_member(
1869
+ parent_fd: int,
1870
+ name: str,
1871
+ label: str,
1872
+ raw: bytes,
1873
+ ) -> tuple[int, os.stat_result, str]:
1874
+ if len(raw) > _MAX_REVIEW_MEMBER_BYTES:
1875
+ raise ReviewError(f"review member is too large: {label}", code="ENVELOPE_MEMBER_TOO_LARGE")
1876
+ flags = (
1877
+ os.O_RDWR
1878
+ | os.O_CREAT
1879
+ | os.O_EXCL
1880
+ | getattr(os, "O_NOFOLLOW", 0)
1881
+ | getattr(os, "O_CLOEXEC", 0)
1882
+ | getattr(os, "O_NONBLOCK", 0)
1883
+ )
1884
+ descriptor = os.open(name, flags, 0o600, dir_fd=parent_fd)
1885
+ try:
1886
+ opened = os.fstat(descriptor)
1887
+ if not stat.S_ISREG(opened.st_mode) or opened.st_nlink != 1:
1888
+ raise ReviewError(
1889
+ f"review staging member is not a single-link regular file: {label}",
1890
+ code="ENVELOPE_STAGING_MEMBER_INVALID",
1891
+ )
1892
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1893
+ _require_same_identity(opened, named, label)
1894
+
1895
+ offset = 0
1896
+ while offset < len(raw):
1897
+ written = os.write(descriptor, raw[offset:])
1898
+ if written <= 0:
1899
+ raise ReviewError(
1900
+ f"review staging member could not be written: {label}",
1901
+ code="ENVELOPE_STAGING_MEMBER_UNWRITABLE",
1902
+ )
1903
+ offset += written
1904
+ os.fsync(descriptor)
1905
+ written_stat = os.fstat(descriptor)
1906
+ if written_stat.st_size != len(raw):
1907
+ raise ReviewError(
1908
+ f"review staging member size changed while writing: {label}",
1909
+ code="ENVELOPE_STAGING_MEMBER_CHANGED",
1910
+ )
1911
+ os.lseek(descriptor, 0, os.SEEK_SET)
1912
+ observed = _read_bounded_descriptor(descriptor, label)
1913
+ _require_stable_stat(written_stat, os.fstat(descriptor), label)
1914
+ if observed != raw:
1915
+ raise ReviewError(
1916
+ f"review staging member changed before hashing: {label}",
1917
+ code="ENVELOPE_STAGING_MEMBER_CHANGED",
1918
+ )
1919
+ digest = sha256_bytes(observed)
1920
+
1921
+ os.fchmod(descriptor, 0o444)
1922
+ os.fsync(descriptor)
1923
+ sealed = os.fstat(descriptor)
1924
+ _require_sealed_file(sealed, label)
1925
+ named_after = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1926
+ _require_stable_stat(sealed, named_after, label)
1927
+ return descriptor, sealed, digest
1928
+ except Exception:
1929
+ os.close(descriptor)
1930
+ raise
1931
+
1932
+
1933
+ def _read_bounded_descriptor(descriptor: int, label: str) -> bytes:
1934
+ raw = bytearray()
1935
+ while True:
1936
+ remaining = _MAX_REVIEW_MEMBER_BYTES + 1 - len(raw)
1937
+ chunk = os.read(descriptor, min(1024 * 1024, remaining))
1938
+ if not chunk:
1939
+ return bytes(raw)
1940
+ raw.extend(chunk)
1941
+ if len(raw) > _MAX_REVIEW_MEMBER_BYTES:
1942
+ raise ReviewError(
1943
+ f"review member is too large: {label}", code="ENVELOPE_MEMBER_TOO_LARGE"
1944
+ )
1945
+
1946
+
1947
+ def _build_signed_decision(
1948
+ *,
1949
+ assignment: dict[str, Any],
1950
+ status: str,
1951
+ verified_candidate: Any,
1952
+ verifier_private_key: TrustedSigner,
1953
+ assignment_sha256: str,
1954
+ report_sha256s: dict[str, str],
1955
+ findings_sha256: str,
1956
+ backlog_sha256: str,
1957
+ ) -> tuple[dict[str, Any], str]:
1958
+ decision_payload = _decision_payload(
1959
+ assignment=assignment,
1960
+ status=status,
1961
+ verified_candidate=verified_candidate,
1962
+ assignment_sha256=assignment_sha256,
1963
+ report_sha256s=report_sha256s,
1964
+ findings_sha256=findings_sha256,
1965
+ backlog_sha256=backlog_sha256,
1966
+ )
1967
+ decision_digest = sha256_bytes(canonical_json_line_bytes(decision_payload))
1968
+ signed_body = {**decision_payload, "decision_digest": decision_digest}
1969
+ signature = verifier_private_key.sign(canonical_json_line_bytes(signed_body))
1970
+ return (
1971
+ {
1972
+ **signed_body,
1973
+ "signature": base64.b64encode(signature).decode("ascii"),
1974
+ },
1975
+ decision_digest,
1976
+ )
1977
+
1978
+
1979
+ def _decision_payload(
1980
+ *,
1981
+ assignment: dict[str, Any],
1982
+ status: str,
1983
+ verified_candidate: Any,
1984
+ assignment_sha256: str,
1985
+ report_sha256s: dict[str, str],
1986
+ findings_sha256: str,
1987
+ backlog_sha256: str,
1988
+ ) -> dict[str, Any]:
1989
+ return {
1990
+ "schema_version": DECISION_SCHEMA_VERSION,
1991
+ "candidate_digest": verified_candidate.candidate_digest,
1992
+ "table_sha256": verified_candidate.table_sha256,
1993
+ "validation_policy_digest": verified_candidate.validation_policy_digest,
1994
+ "assignment_sha256": assignment_sha256,
1995
+ "report_sha256s": report_sha256s,
1996
+ "findings_sha256": findings_sha256,
1997
+ "backlog_sha256": backlog_sha256,
1998
+ "cycle_lineage": assignment["cycle_lineage"],
1999
+ "failed_cycle_count": assignment["failed_cycle_count"],
2000
+ "verifier_version": VERIFIER_VERSION,
2001
+ "verifier_principal": assignment["verifier"]["principal_id"],
2002
+ "status": status,
2003
+ }
2004
+
2005
+
2006
+ def _expected_review_decision_digest(
2007
+ *,
2008
+ assignment: dict[str, Any],
2009
+ reports_by_lens: dict[str, dict[str, Any]],
2010
+ findings: list[dict[str, Any]],
2011
+ backlog: list[dict[str, Any]],
2012
+ status: str,
2013
+ verified_candidate: Any,
2014
+ ) -> str:
2015
+ payload = _decision_payload(
2016
+ assignment=assignment,
2017
+ status=status,
2018
+ verified_candidate=verified_candidate,
2019
+ assignment_sha256=sha256_bytes(canonical_json_line_bytes(assignment)),
2020
+ report_sha256s={
2021
+ lens: sha256_bytes(canonical_json_line_bytes(reports_by_lens[lens]))
2022
+ for lens in sorted(REQUIRED_LENSES)
2023
+ },
2024
+ findings_sha256=sha256_bytes(canonical_json_line_bytes(findings)),
2025
+ backlog_sha256=sha256_bytes(canonical_json_line_bytes(backlog)),
2026
+ )
2027
+ return sha256_bytes(canonical_json_line_bytes(payload))
2028
+
2029
+
2030
+ def _validate_staged_quiescence(
2031
+ *,
2032
+ parent_fd: int,
2033
+ staging_name: str,
2034
+ expected_root_identity: tuple[int, int, int],
2035
+ root_fd: int,
2036
+ reports_fd: int,
2037
+ reports_sealed: os.stat_result,
2038
+ retained: dict[str, tuple[int, str, int, os.stat_result]],
2039
+ ) -> None:
2040
+ root_before = os.fstat(root_fd)
2041
+ if _entry_identity(root_before) != expected_root_identity:
2042
+ raise ReviewError(
2043
+ "review staging root changed identity before install",
2044
+ code="ENVELOPE_STAGING_ROOT_CHANGED",
2045
+ )
2046
+ if not stat.S_ISDIR(root_before.st_mode) or stat.S_IMODE(root_before.st_mode) != 0o700:
2047
+ raise ReviewError(
2048
+ "review staging root is not mode-0700 before install", code="ENVELOPE_STAGING_ROOT_MODE"
2049
+ )
2050
+ if set(os.listdir(root_fd)) != _REVIEW_ROOT_MEMBERS:
2051
+ raise ReviewError(
2052
+ "review staging member set is invalid", code="ENVELOPE_STAGING_MEMBERS_INVALID"
2053
+ )
2054
+ if set(os.listdir(reports_fd)) != _REVIEW_REPORT_MEMBERS:
2055
+ raise ReviewError(
2056
+ "review staging report member set is invalid",
2057
+ code="ENVELOPE_STAGING_REPORT_MEMBERS_INVALID",
2058
+ )
2059
+ reports_named = os.stat("reports", dir_fd=root_fd, follow_symlinks=False)
2060
+ _require_stable_stat(reports_sealed, reports_named, "review reports directory")
2061
+
2062
+ for label in sorted(retained):
2063
+ member_parent_fd, name, retained_fd, sealed = retained[label]
2064
+ retained_now = os.fstat(retained_fd)
2065
+ _require_stable_stat(sealed, retained_now, label)
2066
+ _require_sealed_file(retained_now, label)
2067
+ named = os.stat(name, dir_fd=member_parent_fd, follow_symlinks=False)
2068
+ _require_stable_stat(sealed, named, label)
2069
+ _require_sealed_file(named, label)
2070
+ flags = (
2071
+ os.O_RDONLY
2072
+ | getattr(os, "O_NOFOLLOW", 0)
2073
+ | getattr(os, "O_CLOEXEC", 0)
2074
+ | getattr(os, "O_NONBLOCK", 0)
2075
+ )
2076
+ check_fd = os.open(name, flags, dir_fd=member_parent_fd)
2077
+ try:
2078
+ checked = os.fstat(check_fd)
2079
+ _require_stable_stat(sealed, checked, label)
2080
+ _require_sealed_file(checked, label)
2081
+ finally:
2082
+ os.close(check_fd)
2083
+ named_after = os.stat(name, dir_fd=member_parent_fd, follow_symlinks=False)
2084
+ _require_stable_stat(sealed, named_after, label)
2085
+
2086
+ if set(os.listdir(reports_fd)) != _REVIEW_REPORT_MEMBERS:
2087
+ raise ReviewError(
2088
+ "review staging report member set changed before install",
2089
+ code="ENVELOPE_STAGING_REPORT_MEMBERS_CHANGED",
2090
+ )
2091
+ _require_stable_stat(reports_sealed, os.fstat(reports_fd), "review reports directory")
2092
+ if set(os.listdir(root_fd)) != _REVIEW_ROOT_MEMBERS:
2093
+ raise ReviewError(
2094
+ "review staging member set changed before install",
2095
+ code="ENVELOPE_STAGING_MEMBERS_CHANGED",
2096
+ )
2097
+ _require_stable_stat(root_before, os.fstat(root_fd), "review staging root")
2098
+ root_named = os.stat(
2099
+ staging_name,
2100
+ dir_fd=parent_fd,
2101
+ follow_symlinks=False,
2102
+ )
2103
+ _require_same_identity(root_before, root_named, "review staging root")
2104
+
2105
+
2106
+ def _read_sealed_review_tree(
2107
+ handle: _ReviewRunHandle,
2108
+ *,
2109
+ retained_review_fd: int | None,
2110
+ expected_review_identity: tuple[int, int, int] | None,
2111
+ ) -> tuple[dict[str, bytes], _ReviewSnapshotLease]:
2112
+ """Read one exact review snapshot without following any member path."""
2113
+
2114
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
2115
+ review_fd = -1
2116
+ reports_fd = -1
2117
+ owned_review_fd = retained_review_fd is None
2118
+ success = False
2119
+ try:
2120
+ _validate_review_run_handle(handle)
2121
+ review_named = os.stat("review", dir_fd=handle.run_fd, follow_symlinks=False)
2122
+ _require_sealed_directory(review_named, "review root")
2123
+ if expected_review_identity is not None and (
2124
+ _entry_identity(review_named) != expected_review_identity
2125
+ ):
2126
+ raise ReviewError(
2127
+ "installed review root does not match the finalized staging root",
2128
+ code="ENVELOPE_INSTALL_ROOT_MISMATCH",
2129
+ )
2130
+ review_fd = (
2131
+ os.open("review", directory_flags, dir_fd=handle.run_fd)
2132
+ if retained_review_fd is None
2133
+ else retained_review_fd
2134
+ )
2135
+ review_opened = os.fstat(review_fd)
2136
+ _require_same_identity(review_named, review_opened, "review root")
2137
+ _require_sealed_directory(review_opened, "review root")
2138
+ if set(os.listdir(review_fd)) != _REVIEW_ROOT_MEMBERS:
2139
+ raise ReviewError(
2140
+ "review envelope member set has changed", code="ENVELOPE_MEMBERS_CHANGED"
2141
+ )
2142
+
2143
+ reports_named = os.stat("reports", dir_fd=review_fd, follow_symlinks=False)
2144
+ _require_sealed_directory(reports_named, "review reports directory")
2145
+ reports_fd = os.open("reports", directory_flags, dir_fd=review_fd)
2146
+ reports_opened = os.fstat(reports_fd)
2147
+ _require_same_identity(reports_named, reports_opened, "review reports directory")
2148
+ if set(os.listdir(reports_fd)) != _REVIEW_REPORT_MEMBERS:
2149
+ raise ReviewError(
2150
+ "review report member set has changed", code="ENVELOPE_REPORT_MEMBERS_CHANGED"
2151
+ )
2152
+
2153
+ envelope = {
2154
+ name: _read_sealed_member(review_fd, name, name)
2155
+ for name in sorted(_REVIEW_ROOT_MEMBERS - {"reports"})
2156
+ }
2157
+ envelope.update(
2158
+ {
2159
+ f"reports/{name}": _read_sealed_member(
2160
+ reports_fd,
2161
+ name,
2162
+ f"reports/{name}",
2163
+ )
2164
+ for name in sorted(_REVIEW_REPORT_MEMBERS)
2165
+ }
2166
+ )
2167
+ if set(envelope) != _REVIEW_FILE_PATHS:
2168
+ raise ReviewError(
2169
+ "review envelope member set has changed", code="ENVELOPE_MEMBERS_CHANGED"
2170
+ )
2171
+ if set(os.listdir(reports_fd)) != _REVIEW_REPORT_MEMBERS:
2172
+ raise ReviewError(
2173
+ "review report member set changed during verification",
2174
+ code="ENVELOPE_REPORT_MEMBERS_CHANGED",
2175
+ )
2176
+ _require_stable_stat(reports_opened, os.fstat(reports_fd), "review reports directory")
2177
+ reports_after = os.stat("reports", dir_fd=review_fd, follow_symlinks=False)
2178
+ _require_stable_stat(reports_named, reports_after, "review reports directory")
2179
+ if set(os.listdir(review_fd)) != _REVIEW_ROOT_MEMBERS:
2180
+ raise ReviewError(
2181
+ "review envelope member set changed during verification",
2182
+ code="ENVELOPE_MEMBERS_CHANGED",
2183
+ )
2184
+ _require_stable_stat(review_opened, os.fstat(review_fd), "review root")
2185
+ review_after = os.stat("review", dir_fd=handle.run_fd, follow_symlinks=False)
2186
+ _require_stable_stat(review_named, review_after, "review root")
2187
+ _validate_review_run_handle(handle)
2188
+ lease = _ReviewSnapshotLease(
2189
+ handle=handle,
2190
+ review_fd=review_fd,
2191
+ review_stat=review_named,
2192
+ owns_review_fd=owned_review_fd,
2193
+ )
2194
+ lease.validate()
2195
+ success = True
2196
+ return envelope, lease
2197
+ except ReviewError:
2198
+ raise
2199
+ except OSError as exc:
2200
+ raise ReviewError(
2201
+ "sealed review envelope cannot be read safely", code="ENVELOPE_UNREADABLE"
2202
+ ) from exc
2203
+ finally:
2204
+ if reports_fd >= 0:
2205
+ os.close(reports_fd)
2206
+ if not success and owned_review_fd and review_fd >= 0:
2207
+ os.close(review_fd)
2208
+
2209
+
2210
+ def _read_sealed_member(parent_fd: int, name: str, label: str) -> bytes:
2211
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
2212
+ _require_sealed_file(named, label)
2213
+ flags = (
2214
+ os.O_RDONLY
2215
+ | getattr(os, "O_NOFOLLOW", 0)
2216
+ | getattr(os, "O_CLOEXEC", 0)
2217
+ | getattr(os, "O_NONBLOCK", 0)
2218
+ )
2219
+ descriptor = os.open(name, flags, dir_fd=parent_fd)
2220
+ try:
2221
+ opened = os.fstat(descriptor)
2222
+ _require_sealed_file(opened, label)
2223
+ _require_same_identity(named, opened, label)
2224
+ raw = bytearray()
2225
+ while True:
2226
+ remaining = _MAX_REVIEW_MEMBER_BYTES + 1 - len(raw)
2227
+ chunk = os.read(descriptor, min(1024 * 1024, remaining))
2228
+ if not chunk:
2229
+ break
2230
+ raw.extend(chunk)
2231
+ if len(raw) > _MAX_REVIEW_MEMBER_BYTES:
2232
+ raise ReviewError(
2233
+ f"review member is too large: {label}", code="ENVELOPE_MEMBER_TOO_LARGE"
2234
+ )
2235
+ _require_stable_stat(opened, os.fstat(descriptor), label)
2236
+ finally:
2237
+ os.close(descriptor)
2238
+ after = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
2239
+ _require_stable_stat(named, after, label)
2240
+ return bytes(raw)
2241
+
2242
+
2243
+ def _require_sealed_directory(info: os.stat_result, label: str) -> None:
2244
+ if not stat.S_ISDIR(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o555:
2245
+ raise ReviewError(
2246
+ f"{label} is not a sealed mode-0555 directory", code="ENVELOPE_MEMBER_NOT_SEALED"
2247
+ )
2248
+
2249
+
2250
+ def _require_sealed_file(info: os.stat_result, label: str) -> None:
2251
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or stat.S_IMODE(info.st_mode) != 0o444:
2252
+ raise ReviewError(
2253
+ f"{label} is not a sealed single-link mode-0444 regular file",
2254
+ code="ENVELOPE_MEMBER_NOT_SEALED",
2255
+ )
2256
+
2257
+
2258
+ def _stat_signature(info: os.stat_result) -> tuple[int, ...]:
2259
+ return (
2260
+ info.st_dev,
2261
+ info.st_ino,
2262
+ info.st_mode,
2263
+ info.st_nlink,
2264
+ info.st_size,
2265
+ info.st_mtime_ns,
2266
+ info.st_ctime_ns,
2267
+ )
2268
+
2269
+
2270
+ def _entry_identity(info: os.stat_result) -> tuple[int, int, int]:
2271
+ return (info.st_dev, info.st_ino, stat.S_IFMT(info.st_mode))
2272
+
2273
+
2274
+ def _require_same_identity(before: os.stat_result, after: os.stat_result, label: str) -> None:
2275
+ if _entry_identity(before) != _entry_identity(after):
2276
+ raise ReviewError(
2277
+ f"{label} changed identity during verification", code="ENVELOPE_MEMBER_CHANGED"
2278
+ )
2279
+
2280
+
2281
+ def _require_stable_stat(before: os.stat_result, after: os.stat_result, label: str) -> None:
2282
+ if _stat_signature(before) != _stat_signature(after):
2283
+ raise ReviewError(f"{label} changed during verification", code="ENVELOPE_MEMBER_CHANGED")
2284
+
2285
+
2286
+ def _rename_no_replace(
2287
+ source: Path,
2288
+ destination: Path,
2289
+ *,
2290
+ expected_source_identity: tuple[int, int, int] | None = None,
2291
+ installed_mode: int | None = None,
2292
+ parent_fd: int | None = None,
2293
+ source_fd: int | None = None,
2294
+ ) -> None:
2295
+ """Atomically install a directory while refusing to replace any destination."""
2296
+
2297
+ if source.parent != destination.parent:
2298
+ raise OSError(errno.EXDEV, "review install must remain within one directory")
2299
+ if os.name == "nt":
2300
+ raise OSError(
2301
+ errno.ENOSYS,
2302
+ "secure descriptor-relative review install is unavailable on Windows",
2303
+ )
2304
+
2305
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
2306
+ active_parent_fd = parent_fd if parent_fd is not None else -1
2307
+ active_source_fd = source_fd if source_fd is not None else -1
2308
+ owned_parent_fd = parent_fd is None
2309
+ owned_source_fd = source_fd is None
2310
+ try:
2311
+ if active_parent_fd < 0:
2312
+ active_parent_fd = os.open(source.parent, flags)
2313
+ source_named = os.stat(
2314
+ source.name,
2315
+ dir_fd=active_parent_fd,
2316
+ follow_symlinks=False,
2317
+ )
2318
+ expected = expected_source_identity or _entry_identity(source_named)
2319
+ if _entry_identity(source_named) != expected or not stat.S_ISDIR(source_named.st_mode):
2320
+ raise OSError(errno.ESTALE, "review install source identity changed", source)
2321
+ if active_source_fd < 0:
2322
+ active_source_fd = os.open(source.name, flags, dir_fd=active_parent_fd)
2323
+ source_opened = os.fstat(active_source_fd)
2324
+ if _entry_identity(source_opened) != expected:
2325
+ raise OSError(errno.ESTALE, "review install source identity changed", source)
2326
+
2327
+ if sys.platform.startswith("linux"):
2328
+ libc = ctypes.CDLL(None, use_errno=True)
2329
+ renameat2 = getattr(libc, "renameat2", None)
2330
+ if renameat2 is None:
2331
+ raise OSError(errno.ENOSYS, "renameat2 is required for atomic no-replace install")
2332
+ renameat2.argtypes = [
2333
+ ctypes.c_int,
2334
+ ctypes.c_char_p,
2335
+ ctypes.c_int,
2336
+ ctypes.c_char_p,
2337
+ ctypes.c_uint,
2338
+ ]
2339
+ renameat2.restype = ctypes.c_int
2340
+ result = renameat2(
2341
+ active_parent_fd,
2342
+ os.fsencode(source.name),
2343
+ active_parent_fd,
2344
+ os.fsencode(destination.name),
2345
+ 1, # RENAME_NOREPLACE: report EEXIST instead of replacing the destination.
2346
+ )
2347
+ if result != 0:
2348
+ error = ctypes.get_errno()
2349
+ if error == errno.EEXIST:
2350
+ raise FileExistsError(error, os.strerror(error), destination)
2351
+ raise OSError(error, os.strerror(error), destination)
2352
+ elif sys.platform == "darwin":
2353
+ libc = ctypes.CDLL(None, use_errno=True)
2354
+ renameatx_np = libc.renameatx_np
2355
+ renameatx_np.argtypes = [
2356
+ ctypes.c_int,
2357
+ ctypes.c_char_p,
2358
+ ctypes.c_int,
2359
+ ctypes.c_char_p,
2360
+ ctypes.c_uint,
2361
+ ]
2362
+ renameatx_np.restype = ctypes.c_int
2363
+ result = renameatx_np(
2364
+ active_parent_fd,
2365
+ os.fsencode(source.name),
2366
+ active_parent_fd,
2367
+ os.fsencode(destination.name),
2368
+ # RENAME_EXCL | RENAME_NOFOLLOW_ANY: the same no-replace guarantee,
2369
+ # plus a refusal to traverse a symlink in either path.
2370
+ 0x00000004 | 0x00000010,
2371
+ )
2372
+ if result != 0:
2373
+ error = ctypes.get_errno()
2374
+ if error == errno.EEXIST:
2375
+ raise FileExistsError(error, os.strerror(error), destination)
2376
+ raise OSError(error, os.strerror(error), destination)
2377
+ else:
2378
+ raise OSError(errno.ENOSYS, "atomic no-replace directory install is unsupported")
2379
+
2380
+ destination_named = os.stat(
2381
+ destination.name,
2382
+ dir_fd=active_parent_fd,
2383
+ follow_symlinks=False,
2384
+ )
2385
+ if _entry_identity(destination_named) != expected:
2386
+ raise OSError(errno.ESTALE, "installed review identity changed", destination)
2387
+ if installed_mode is not None:
2388
+ os.fchmod(active_source_fd, installed_mode)
2389
+ os.fsync(active_source_fd)
2390
+ installed_opened = os.fstat(active_source_fd)
2391
+ if _entry_identity(installed_opened) != expected:
2392
+ raise OSError(errno.ESTALE, "installed review identity changed", destination)
2393
+ if installed_mode is not None and stat.S_IMODE(installed_opened.st_mode) != installed_mode:
2394
+ raise OSError(errno.EPERM, "installed review mode could not be sealed", destination)
2395
+ destination_after_seal = os.stat(
2396
+ destination.name,
2397
+ dir_fd=active_parent_fd,
2398
+ follow_symlinks=False,
2399
+ )
2400
+ if _entry_identity(destination_after_seal) != expected:
2401
+ raise OSError(errno.ESTALE, "installed review identity changed", destination)
2402
+ if (
2403
+ installed_mode is not None
2404
+ and stat.S_IMODE(destination_after_seal.st_mode) != installed_mode
2405
+ ):
2406
+ raise OSError(errno.EPERM, "installed review mode could not be sealed", destination)
2407
+ finally:
2408
+ if owned_source_fd and active_source_fd >= 0:
2409
+ os.close(active_source_fd)
2410
+ if owned_parent_fd and active_parent_fd >= 0:
2411
+ os.close(active_parent_fd)
2412
+
2413
+
2414
+ def _remove_tree(
2415
+ path: Path,
2416
+ *,
2417
+ expected_identity: tuple[int, int, int] | None = None,
2418
+ ) -> None:
2419
+ """Best-effort removal that never follows or chmods a symlink target."""
2420
+
2421
+ if os.name == "nt":
2422
+ _remove_tree_fallback(path, expected_identity=expected_identity)
2423
+ return
2424
+ parent_fd = -1
2425
+ try:
2426
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
2427
+ parent_fd = os.open(path.parent, flags)
2428
+ named = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)
2429
+ expected = expected_identity or _entry_identity(named)
2430
+ if _entry_identity(named) != expected:
2431
+ return
2432
+ _remove_at(parent_fd, path.name, expected_identity=expected)
2433
+ except (FileNotFoundError, NotADirectoryError):
2434
+ return
2435
+ except OSError:
2436
+ return
2437
+ finally:
2438
+ if parent_fd >= 0:
2439
+ os.close(parent_fd)
2440
+
2441
+
2442
+ def _remove_at(
2443
+ parent_fd: int,
2444
+ name: str,
2445
+ *,
2446
+ expected_identity: tuple[int, int, int] | None = None,
2447
+ ) -> None:
2448
+ try:
2449
+ info = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
2450
+ except FileNotFoundError:
2451
+ return
2452
+ expected = expected_identity or _entry_identity(info)
2453
+ if _entry_identity(info) != expected:
2454
+ return
2455
+ if not stat.S_ISDIR(info.st_mode):
2456
+ try:
2457
+ current = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
2458
+ if _entry_identity(current) != expected:
2459
+ return
2460
+ os.unlink(name, dir_fd=parent_fd)
2461
+ except FileNotFoundError:
2462
+ pass
2463
+ return
2464
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
2465
+ try:
2466
+ directory_fd = os.open(name, flags, dir_fd=parent_fd)
2467
+ except FileNotFoundError:
2468
+ return
2469
+ except OSError:
2470
+ return
2471
+ try:
2472
+ opened = os.fstat(directory_fd)
2473
+ if _entry_identity(opened) != expected:
2474
+ return
2475
+ os.fchmod(directory_fd, 0o700)
2476
+ for child in os.listdir(directory_fd):
2477
+ try:
2478
+ child_info = os.stat(child, dir_fd=directory_fd, follow_symlinks=False)
2479
+ _remove_at(
2480
+ directory_fd,
2481
+ child,
2482
+ expected_identity=_entry_identity(child_info),
2483
+ )
2484
+ except OSError:
2485
+ continue
2486
+ finally:
2487
+ os.close(directory_fd)
2488
+ try:
2489
+ current = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
2490
+ if _entry_identity(current) != expected:
2491
+ return
2492
+ os.rmdir(name, dir_fd=parent_fd)
2493
+ except FileNotFoundError:
2494
+ pass
2495
+
2496
+
2497
+ def _remove_tree_fallback(
2498
+ path: Path,
2499
+ *,
2500
+ expected_identity: tuple[int, int, int] | None = None,
2501
+ ) -> None:
2502
+ try:
2503
+ info = path.lstat()
2504
+ except FileNotFoundError:
2505
+ return
2506
+ expected = expected_identity or _entry_identity(info)
2507
+ if _entry_identity(info) != expected:
2508
+ return
2509
+ if not stat.S_ISDIR(info.st_mode):
2510
+ try:
2511
+ if _entry_identity(path.lstat()) != expected:
2512
+ return
2513
+ path.unlink()
2514
+ except OSError:
2515
+ pass
2516
+ return
2517
+ try:
2518
+ path.chmod(0o700)
2519
+ children = list(path.iterdir())
2520
+ except OSError:
2521
+ return
2522
+ for child in children:
2523
+ try:
2524
+ child_identity = _entry_identity(child.lstat())
2525
+ except FileNotFoundError:
2526
+ continue
2527
+ _remove_tree_fallback(child, expected_identity=child_identity)
2528
+ try:
2529
+ if _entry_identity(path.lstat()) != expected:
2530
+ return
2531
+ path.rmdir()
2532
+ except OSError:
2533
+ pass