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,1055 @@
1
+ """Crash-safe protected credential envelopes and local store coordination.
2
+
3
+ Raw device credentials are permitted only in :class:`ProtectedCredential` values and backend
4
+ payloads. Every representation and every error in this module is deliberately allowlisted; do
5
+ not replace the explicit encoders with generic dataclass serialization.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import os
12
+ import secrets
13
+ import stat
14
+ import time
15
+ from collections.abc import Iterator
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Protocol, Self
19
+
20
+ from mostlyright.data_harness.canonical import (
21
+ CanonicalJSONError,
22
+ canonical_json_bytes,
23
+ parse_canonical_json,
24
+ )
25
+
26
+ CREDENTIAL_ENVELOPE_SCHEMA = "mr-data-credentials.v2"
27
+ MAX_CREDENTIAL_ENVELOPE_BYTES = 64 * 1024
28
+ SECURE_FILE_NAME = "credentials.secure"
29
+ LOCK_FILE_NAME = ".credential.lock"
30
+
31
+ _STATES = frozenset(
32
+ {"active", "login_pending", "revoke_pending", "cleanup_pending", "logout_pending"}
33
+ )
34
+ _OPERATIONS = frozenset({"login", "rotate"})
35
+ _CREDENTIAL_FIELDS = frozenset(
36
+ {"cloud_url", "key_id", "prefix", "last_four", "device_name", "created_at", "raw_key"}
37
+ )
38
+ _LEASE_FIELDS = frozenset(
39
+ {"nonce", "base_generation", "operation", "requested_device_name", "created_at"}
40
+ )
41
+
42
+
43
+ class CredentialStoreError(RuntimeError):
44
+ """A typed, secret-free protected-store refusal."""
45
+
46
+ def __init__(self, code: str, detail: str) -> None:
47
+ self.code = code
48
+ self.detail = detail
49
+ super().__init__(f"{code}: {detail}")
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ProtectedCredential:
54
+ """One secret-bearing device credential with an always-redacted representation."""
55
+
56
+ cloud_url: str
57
+ key_id: str
58
+ prefix: str
59
+ last_four: str
60
+ device_name: str
61
+ created_at: str
62
+ raw_key: str
63
+
64
+ def __post_init__(self) -> None:
65
+ if any(
66
+ not isinstance(getattr(self, field), str) or not getattr(self, field)
67
+ for field in _CREDENTIAL_FIELDS
68
+ ):
69
+ raise CredentialStoreError(
70
+ "CREDENTIAL_STORE_CORRUPT", "the protected credential has an invalid field"
71
+ )
72
+
73
+ def __repr__(self) -> str:
74
+ return (
75
+ "ProtectedCredential("
76
+ f"cloud_url={self.cloud_url!r}, key_id={self.key_id!r}, prefix={self.prefix!r}, "
77
+ f"last_four={self.last_four!r}, device_name={self.device_name!r}, "
78
+ f"created_at={self.created_at!r}, raw_key='<redacted>')"
79
+ )
80
+
81
+ def _payload(self) -> dict[str, str]:
82
+ return {
83
+ "cloud_url": self.cloud_url,
84
+ "key_id": self.key_id,
85
+ "prefix": self.prefix,
86
+ "last_four": self.last_four,
87
+ "device_name": self.device_name,
88
+ "created_at": self.created_at,
89
+ "raw_key": self.raw_key,
90
+ }
91
+
92
+ @classmethod
93
+ def _from_payload(cls, value: object) -> Self:
94
+ if not isinstance(value, dict) or set(value) != _CREDENTIAL_FIELDS:
95
+ raise _corrupt()
96
+ if any(
97
+ not isinstance(value[field], str) or not value[field] for field in _CREDENTIAL_FIELDS
98
+ ):
99
+ raise _corrupt()
100
+ return cls(**{field: value[field] for field in _CREDENTIAL_FIELDS})
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class CredentialLease:
105
+ """The durable owner of one device authorization flow for one store generation."""
106
+
107
+ nonce: str
108
+ base_generation: int
109
+ operation: str
110
+ requested_device_name: str
111
+ created_at: str
112
+
113
+ def __post_init__(self) -> None:
114
+ if (
115
+ not isinstance(self.nonce, str)
116
+ or not self.nonce
117
+ or type(self.base_generation) is not int
118
+ or self.base_generation < 0
119
+ or self.operation not in _OPERATIONS
120
+ or not isinstance(self.requested_device_name, str)
121
+ or not self.requested_device_name
122
+ or not isinstance(self.created_at, str)
123
+ or not self.created_at
124
+ ):
125
+ raise CredentialStoreError(
126
+ "CREDENTIAL_STORE_CORRUPT", "the protected login lease is invalid"
127
+ )
128
+
129
+ def __repr__(self) -> str:
130
+ return (
131
+ "CredentialLease("
132
+ f"nonce={self.nonce!r}, base_generation={self.base_generation!r}, "
133
+ f"operation={self.operation!r}, requested_device_name={self.requested_device_name!r}, "
134
+ f"created_at={self.created_at!r})"
135
+ )
136
+
137
+ def _payload(self) -> dict[str, str | int]:
138
+ return {
139
+ "nonce": self.nonce,
140
+ "base_generation": self.base_generation,
141
+ "operation": self.operation,
142
+ "requested_device_name": self.requested_device_name,
143
+ "created_at": self.created_at,
144
+ }
145
+
146
+ @classmethod
147
+ def _from_payload(cls, value: object) -> Self:
148
+ if not isinstance(value, dict) or set(value) != _LEASE_FIELDS:
149
+ raise _corrupt()
150
+ try:
151
+ return cls(
152
+ nonce=value["nonce"],
153
+ base_generation=value["base_generation"],
154
+ operation=value["operation"],
155
+ requested_device_name=value["requested_device_name"],
156
+ created_at=value["created_at"],
157
+ )
158
+ except (CredentialStoreError, TypeError):
159
+ raise _corrupt() from None
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class CredentialEnvelope:
164
+ """One exact state in the closed credential transaction protocol."""
165
+
166
+ generation: int
167
+ state: str
168
+ active: ProtectedCredential | None = None
169
+ prior: ProtectedCredential | None = None
170
+ login_pending: CredentialLease | None = None
171
+
172
+ def __post_init__(self) -> None:
173
+ if type(self.generation) is not int or self.generation < 0 or self.state not in _STATES:
174
+ raise _corrupt()
175
+ if self.state == "active":
176
+ valid = self.active is not None and self.prior is None and self.login_pending is None
177
+ elif self.state == "login_pending":
178
+ lease = self.login_pending
179
+ valid = (
180
+ lease is not None
181
+ and self.prior is None
182
+ and self.generation == lease.base_generation + 1
183
+ and (
184
+ (lease.operation == "login" and self.active is None)
185
+ or (lease.operation == "rotate" and self.active is not None)
186
+ )
187
+ )
188
+ elif self.state in {"revoke_pending", "cleanup_pending"}:
189
+ valid = (
190
+ self.active is not None and self.prior is not None and self.login_pending is None
191
+ )
192
+ else: # logout_pending
193
+ valid = self.active is not None and self.prior is None and self.login_pending is None
194
+ if not valid:
195
+ raise _corrupt()
196
+
197
+ def __repr__(self) -> str:
198
+ return (
199
+ "CredentialEnvelope("
200
+ f"generation={self.generation!r}, state={self.state!r}, active={self.active!r}, "
201
+ f"prior={self.prior!r}, login_pending={self.login_pending!r})"
202
+ )
203
+
204
+ @classmethod
205
+ def active_envelope(cls, *, generation: int, active: ProtectedCredential) -> Self:
206
+ return cls(generation=generation, state="active", active=active)
207
+
208
+ @classmethod
209
+ def login_pending_envelope(
210
+ cls,
211
+ *,
212
+ generation: int,
213
+ active: ProtectedCredential | None,
214
+ lease: CredentialLease,
215
+ ) -> Self:
216
+ return cls(
217
+ generation=generation,
218
+ state="login_pending",
219
+ active=active,
220
+ login_pending=lease,
221
+ )
222
+
223
+ @classmethod
224
+ def pending_remote_revoke(
225
+ cls,
226
+ *,
227
+ generation: int,
228
+ active: ProtectedCredential,
229
+ prior: ProtectedCredential,
230
+ ) -> Self:
231
+ return cls(
232
+ generation=generation,
233
+ state="revoke_pending",
234
+ active=active,
235
+ prior=prior,
236
+ )
237
+
238
+ @classmethod
239
+ def pending_cleanup(
240
+ cls,
241
+ *,
242
+ generation: int,
243
+ active: ProtectedCredential,
244
+ prior: ProtectedCredential,
245
+ ) -> Self:
246
+ return cls(
247
+ generation=generation,
248
+ state="cleanup_pending",
249
+ active=active,
250
+ prior=prior,
251
+ )
252
+
253
+ @classmethod
254
+ def pending_logout(cls, *, generation: int, active: ProtectedCredential) -> Self:
255
+ return cls(generation=generation, state="logout_pending", active=active)
256
+
257
+ def to_bytes(self) -> bytes:
258
+ payload: dict[str, object] = {
259
+ "schema_version": CREDENTIAL_ENVELOPE_SCHEMA,
260
+ "generation": self.generation,
261
+ "state": self.state,
262
+ }
263
+ if self.active is not None:
264
+ payload["active"] = self.active._payload()
265
+ if self.prior is not None:
266
+ payload["prior"] = self.prior._payload()
267
+ if self.login_pending is not None:
268
+ payload["login_pending"] = self.login_pending._payload()
269
+ return canonical_json_bytes(payload)
270
+
271
+ @classmethod
272
+ def from_bytes(cls, value: bytes | bytearray | memoryview) -> Self:
273
+ raw = bytes(value)
274
+ if len(raw) > MAX_CREDENTIAL_ENVELOPE_BYTES:
275
+ raise CredentialStoreError(
276
+ "CREDENTIAL_STORE_CORRUPT", "the protected credential envelope exceeds its limit"
277
+ )
278
+ try:
279
+ parsed = parse_canonical_json(raw)
280
+ except (CanonicalJSONError, UnicodeError, ValueError):
281
+ raise _corrupt() from None
282
+ if not isinstance(parsed, dict):
283
+ raise _corrupt()
284
+ state = parsed.get("state")
285
+ required = {"schema_version", "generation", "state"}
286
+ if state == "active" or state == "logout_pending":
287
+ required.add("active")
288
+ elif state == "login_pending":
289
+ required.add("login_pending")
290
+ if "active" in parsed:
291
+ required.add("active")
292
+ elif state in {"revoke_pending", "cleanup_pending"}:
293
+ required.update({"active", "prior"})
294
+ if set(parsed) != required or parsed.get("schema_version") != CREDENTIAL_ENVELOPE_SCHEMA:
295
+ raise _corrupt()
296
+ try:
297
+ return cls(
298
+ generation=parsed["generation"],
299
+ state=state,
300
+ active=ProtectedCredential._from_payload(parsed["active"])
301
+ if "active" in parsed
302
+ else None,
303
+ prior=ProtectedCredential._from_payload(parsed["prior"])
304
+ if "prior" in parsed
305
+ else None,
306
+ login_pending=CredentialLease._from_payload(parsed["login_pending"])
307
+ if "login_pending" in parsed
308
+ else None,
309
+ )
310
+ except (CredentialStoreError, KeyError, TypeError):
311
+ raise _corrupt() from None
312
+
313
+
314
+ class CredentialBackend(Protocol):
315
+ """A backend that stores one opaque protected-envelope payload."""
316
+
317
+ name: str
318
+
319
+ def read(self) -> bytes | None: ...
320
+
321
+ def write(self, payload: bytes) -> None: ...
322
+
323
+ def delete(self) -> None: ...
324
+
325
+
326
+ class SecureFileBackend:
327
+ """Explicit, atomic 0600 fallback backend; never selected implicitly."""
328
+
329
+ name = "secure-file"
330
+
331
+ def __init__(
332
+ self,
333
+ config_dir: Path,
334
+ *,
335
+ explicit: bool,
336
+ filename: str = SECURE_FILE_NAME,
337
+ ) -> None:
338
+ if explicit is not True:
339
+ raise CredentialStoreError(
340
+ "CREDENTIAL_STORE_EXPLICIT_REQUIRED",
341
+ "secure-file storage requires explicit selection",
342
+ )
343
+ self.config_dir = Path(config_dir)
344
+ if not filename or filename in {".", ".."} or Path(filename).name != filename:
345
+ raise CredentialStoreError(
346
+ "CREDENTIAL_STORE_CORRUPT", "the protected credential filename is invalid"
347
+ )
348
+ self._filename = filename
349
+ self.path = self.config_dir / filename
350
+ _ensure_private_directory(self.config_dir)
351
+
352
+ def read(self) -> bytes | None:
353
+ _validate_private_directory(self.config_dir)
354
+ try:
355
+ descriptor = os.open(
356
+ self.path,
357
+ os.O_RDONLY
358
+ | getattr(os, "O_BINARY", 0)
359
+ | getattr(os, "O_NONBLOCK", 0)
360
+ | getattr(os, "O_NOFOLLOW", 0),
361
+ )
362
+ except FileNotFoundError:
363
+ if os.path.lexists(self.path):
364
+ raise _permission_refusal() from None
365
+ return None
366
+ except OSError:
367
+ raise _permission_refusal() from None
368
+ try:
369
+ info = os.fstat(descriptor)
370
+ _validate_private_file(info, path=self.path)
371
+ if info.st_size > MAX_CREDENTIAL_ENVELOPE_BYTES:
372
+ raise CredentialStoreError(
373
+ "CREDENTIAL_STORE_CORRUPT",
374
+ "the protected credential envelope exceeds its limit",
375
+ )
376
+ chunks: list[bytes] = []
377
+ remaining = MAX_CREDENTIAL_ENVELOPE_BYTES + 1
378
+ while remaining:
379
+ chunk = os.read(descriptor, min(remaining, 16 * 1024))
380
+ if not chunk:
381
+ break
382
+ chunks.append(chunk)
383
+ remaining -= len(chunk)
384
+ raw = b"".join(chunks)
385
+ if len(raw) > MAX_CREDENTIAL_ENVELOPE_BYTES:
386
+ raise CredentialStoreError(
387
+ "CREDENTIAL_STORE_CORRUPT",
388
+ "the protected credential envelope exceeds its limit",
389
+ )
390
+ return raw
391
+ except CredentialStoreError:
392
+ raise
393
+ except OSError:
394
+ raise CredentialStoreError(
395
+ "CREDENTIAL_STORE_UNAVAILABLE", "secure-file storage could not be read"
396
+ ) from None
397
+ finally:
398
+ os.close(descriptor)
399
+
400
+ def write(self, payload: bytes) -> None:
401
+ _validate_private_directory(self.config_dir)
402
+ if len(payload) > MAX_CREDENTIAL_ENVELOPE_BYTES:
403
+ raise CredentialStoreError(
404
+ "CREDENTIAL_STORE_CORRUPT", "the protected credential envelope exceeds its limit"
405
+ )
406
+ if os.path.lexists(self.path):
407
+ # Validate the current target before replacing it. Atomic replacement is not license
408
+ # to overwrite a link, hardlink, foreign owner, or permissive file.
409
+ self.read()
410
+ stage_name = f".{self._filename}.{secrets.token_hex(16)}.stage"
411
+ stage = self.config_dir / stage_name
412
+ descriptor: int | None = None
413
+ directory_fd: int | None = None
414
+ try:
415
+ descriptor = os.open(
416
+ stage,
417
+ os.O_WRONLY
418
+ | os.O_CREAT
419
+ | os.O_EXCL
420
+ | getattr(os, "O_BINARY", 0)
421
+ | getattr(os, "O_NOFOLLOW", 0),
422
+ 0o600,
423
+ )
424
+ if os.name == "posix":
425
+ os.fchmod(descriptor, 0o600)
426
+ offset = 0
427
+ while offset < len(payload):
428
+ written = os.write(descriptor, payload[offset:])
429
+ if written <= 0:
430
+ raise OSError("short credential write")
431
+ offset += written
432
+ os.fsync(descriptor)
433
+ _validate_private_file(os.fstat(descriptor), path=stage)
434
+ os.close(descriptor)
435
+ descriptor = None
436
+ os.replace(stage, self.path)
437
+ if os.name == "posix":
438
+ directory_fd = os.open(
439
+ self.config_dir,
440
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
441
+ )
442
+ os.fsync(directory_fd)
443
+ except CredentialStoreError:
444
+ raise
445
+ except OSError:
446
+ raise CredentialStoreError(
447
+ "CREDENTIAL_STORE_WRITE_FAILED", "secure-file storage could not be updated"
448
+ ) from None
449
+ finally:
450
+ if descriptor is not None:
451
+ os.close(descriptor)
452
+ if directory_fd is not None:
453
+ os.close(directory_fd)
454
+ with contextlib.suppress(OSError):
455
+ stage.unlink()
456
+
457
+ def delete(self) -> None:
458
+ if self.read() is None:
459
+ return
460
+ try:
461
+ self.path.unlink()
462
+ if os.name == "posix":
463
+ directory_fd = os.open(
464
+ self.config_dir,
465
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
466
+ )
467
+ try:
468
+ os.fsync(directory_fd)
469
+ finally:
470
+ os.close(directory_fd)
471
+ except OSError:
472
+ raise CredentialStoreError(
473
+ "CREDENTIAL_STORE_WRITE_FAILED", "secure-file storage could not be removed"
474
+ ) from None
475
+
476
+
477
+ @contextlib.contextmanager
478
+ def credential_lock(config_dir: Path, *, timeout_seconds: float = 5.0) -> Iterator[None]:
479
+ """Hold the per-config-root cross-process credential lock."""
480
+
481
+ root = Path(config_dir)
482
+ _ensure_private_directory(root)
483
+ _validate_private_directory(root)
484
+ lock_path = root / LOCK_FILE_NAME
485
+ descriptor: int | None = None
486
+ try:
487
+ open_flags = os.O_RDWR | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
488
+ try:
489
+ descriptor = os.open(lock_path, open_flags | os.O_CREAT | os.O_EXCL, 0o600)
490
+ created = True
491
+ except FileExistsError:
492
+ descriptor = os.open(lock_path, open_flags)
493
+ created = False
494
+ if os.name == "posix" and created:
495
+ os.fchmod(descriptor, 0o600)
496
+ _validate_private_file(os.fstat(descriptor), path=lock_path)
497
+ except (OSError, CredentialStoreError):
498
+ if descriptor is not None:
499
+ os.close(descriptor)
500
+ raise CredentialStoreError(
501
+ "CREDENTIAL_LOCK_UNAVAILABLE", "the credential lock could not be opened"
502
+ ) from None
503
+ acquired = False
504
+ deadline = time.monotonic() + max(0.0, timeout_seconds)
505
+ try:
506
+ if os.name == "nt":
507
+ import msvcrt
508
+
509
+ if os.fstat(descriptor).st_size == 0:
510
+ os.write(descriptor, b"\0")
511
+ os.fsync(descriptor)
512
+ while not acquired:
513
+ try:
514
+ os.lseek(descriptor, 0, os.SEEK_SET)
515
+ msvcrt.locking(descriptor, msvcrt.LK_NBLCK, 1)
516
+ acquired = True
517
+ except OSError:
518
+ if time.monotonic() >= deadline:
519
+ break
520
+ time.sleep(0.01)
521
+ else:
522
+ import fcntl
523
+
524
+ while not acquired:
525
+ try:
526
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
527
+ acquired = True
528
+ except BlockingIOError:
529
+ if time.monotonic() >= deadline:
530
+ break
531
+ time.sleep(0.01)
532
+ if not acquired:
533
+ raise CredentialStoreError(
534
+ "CREDENTIAL_LOCKED", "another credential operation holds the local lock"
535
+ )
536
+ yield
537
+ finally:
538
+ if acquired:
539
+ if os.name == "nt":
540
+ import msvcrt
541
+
542
+ os.lseek(descriptor, 0, os.SEEK_SET)
543
+ msvcrt.locking(descriptor, msvcrt.LK_UNLCK, 1)
544
+ else:
545
+ import fcntl
546
+
547
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
548
+ os.close(descriptor)
549
+
550
+
551
+ class CredentialStore:
552
+ """Locking generation/lease facade over one selected backend."""
553
+
554
+ def __init__(self, config_dir: Path, backend: CredentialBackend) -> None:
555
+ self.config_dir = Path(config_dir)
556
+ self.backend = backend
557
+
558
+ def _load_unlocked(self) -> CredentialEnvelope | None:
559
+ payload = self.backend.read()
560
+ return None if payload is None else CredentialEnvelope.from_bytes(payload)
561
+
562
+ def load(self) -> CredentialEnvelope | None:
563
+ with credential_lock(self.config_dir):
564
+ return self._load_unlocked()
565
+
566
+ def load_under_held_lock(self) -> CredentialEnvelope | None:
567
+ """Load while a caller coordinating multiple local files already holds this root's lock."""
568
+
569
+ return self._load_unlocked()
570
+
571
+ def write_under_held_lock(self, envelope: CredentialEnvelope) -> None:
572
+ """Write under an outer lock, resuming only a byte-identical interrupted install."""
573
+
574
+ incoming = envelope.to_bytes()
575
+ existing = self.backend.read()
576
+ if existing is not None and secrets.compare_digest(existing, incoming):
577
+ return
578
+ current = None if existing is None else CredentialEnvelope.from_bytes(existing)
579
+ _validate_saved_transition(
580
+ current,
581
+ envelope,
582
+ expected_generation=None if current is None else current.generation,
583
+ )
584
+ self.backend.write(incoming)
585
+
586
+ def replace_under_held_lock(
587
+ self,
588
+ envelope: CredentialEnvelope,
589
+ *,
590
+ expected_generation: int,
591
+ expected_states: frozenset[str],
592
+ ) -> CredentialEnvelope:
593
+ """Advance one known recovery transition while an outer credential lock is held.
594
+
595
+ Generic ``save`` intentionally permits only active-to-active replacement. Lifecycle
596
+ orchestration needs the same generation proof for its closed pending states without
597
+ creating a second, unchecked backend write path.
598
+ """
599
+
600
+ current = self._load_unlocked()
601
+ if (
602
+ current is None
603
+ or current.generation != expected_generation
604
+ or current.state not in expected_states
605
+ or envelope.generation != expected_generation + 1
606
+ ):
607
+ raise CredentialStoreError(
608
+ "CREDENTIAL_GENERATION_CONFLICT",
609
+ "the protected credential transaction changed before it could advance",
610
+ )
611
+ self.backend.write(envelope.to_bytes())
612
+ return envelope
613
+
614
+ def delete_under_held_lock(
615
+ self,
616
+ *,
617
+ expected_generation: int,
618
+ expected_states: frozenset[str],
619
+ ) -> None:
620
+ """Remove protected material only from a caller-proved terminal cleanup state."""
621
+
622
+ current = self._load_unlocked()
623
+ if (
624
+ current is None
625
+ or current.generation != expected_generation
626
+ or current.state not in expected_states
627
+ ):
628
+ raise CredentialStoreError(
629
+ "CREDENTIAL_GENERATION_CONFLICT",
630
+ "the protected credential transaction changed before cleanup",
631
+ )
632
+ self.backend.delete()
633
+
634
+ def save(
635
+ self, envelope: CredentialEnvelope, *, expected_generation: int | None
636
+ ) -> CredentialEnvelope:
637
+ with credential_lock(self.config_dir):
638
+ current = self._load_unlocked()
639
+ _validate_saved_transition(current, envelope, expected_generation=expected_generation)
640
+ self.backend.write(envelope.to_bytes())
641
+ return envelope
642
+
643
+ def acquire_login_lease(
644
+ self,
645
+ *,
646
+ operation: str,
647
+ requested_device_name: str,
648
+ created_at: str,
649
+ nonce: str | None = None,
650
+ ) -> CredentialEnvelope:
651
+ with credential_lock(self.config_dir):
652
+ current = self._load_unlocked()
653
+ if current is not None and current.state == "login_pending":
654
+ raise CredentialStoreError(
655
+ "CREDENTIAL_LOGIN_PENDING",
656
+ "another device flow is pending and its mint outcome is unproved",
657
+ )
658
+ if current is not None and current.state != "active":
659
+ raise CredentialStoreError(
660
+ "CREDENTIAL_RECOVERY_REQUIRED",
661
+ "the protected credential requires recovery before another mutation",
662
+ )
663
+ base_generation = 0 if current is None else current.generation
664
+ active = None if current is None else current.active
665
+ lease = CredentialLease(
666
+ nonce=nonce or secrets.token_urlsafe(32),
667
+ base_generation=base_generation,
668
+ operation=operation,
669
+ requested_device_name=requested_device_name,
670
+ created_at=created_at,
671
+ )
672
+ pending = CredentialEnvelope.login_pending_envelope(
673
+ generation=base_generation + 1,
674
+ active=active,
675
+ lease=lease,
676
+ )
677
+ self.backend.write(pending.to_bytes())
678
+ return pending
679
+
680
+ def complete_login_lease(
681
+ self, *, nonce: str, minted: ProtectedCredential
682
+ ) -> CredentialEnvelope:
683
+ with credential_lock(self.config_dir):
684
+ current = self._load_unlocked()
685
+ lease = None if current is None else current.login_pending
686
+ if (
687
+ current is None
688
+ or current.state != "login_pending"
689
+ or lease is None
690
+ or lease.nonce != nonce
691
+ or current.generation != lease.base_generation + 1
692
+ ):
693
+ raise CredentialStoreError(
694
+ "CREDENTIAL_LEASE_CONFLICT",
695
+ "the device flow no longer owns the protected credential generation",
696
+ )
697
+ if current.active is None:
698
+ installed = CredentialEnvelope.active_envelope(
699
+ generation=current.generation + 1,
700
+ active=minted,
701
+ )
702
+ else:
703
+ installed = CredentialEnvelope.pending_remote_revoke(
704
+ generation=current.generation + 1,
705
+ active=minted,
706
+ prior=current.active,
707
+ )
708
+ self.backend.write(installed.to_bytes())
709
+ # Durability proof belongs inside the same lock as the install.
710
+ # Once this method returns, another process may legitimately
711
+ # recover this generation or acquire the next lease; callers must
712
+ # never reinterpret that later drift as a failed mint persistence.
713
+ if self._load_unlocked() != installed:
714
+ raise CredentialStoreError(
715
+ "CREDENTIAL_STORE_READBACK_FAILED",
716
+ "the replacement could not be verified",
717
+ )
718
+ return installed
719
+
720
+ def recover_login_lease_local_only(self) -> dict[str, str]:
721
+ with credential_lock(self.config_dir):
722
+ current = self._load_unlocked()
723
+ if current is None or current.state != "login_pending":
724
+ raise CredentialStoreError(
725
+ "CREDENTIAL_RECOVERY_NOT_APPLICABLE",
726
+ "there is no pending device flow to recover",
727
+ )
728
+ if current.active is None:
729
+ self.backend.delete()
730
+ else:
731
+ self.backend.write(
732
+ CredentialEnvelope.active_envelope(
733
+ generation=current.generation + 1,
734
+ active=current.active,
735
+ ).to_bytes()
736
+ )
737
+ return {"mint_outcome": "unproved", "remote_cleanup": "unproved"}
738
+
739
+
740
+ def _corrupt() -> CredentialStoreError:
741
+ return CredentialStoreError(
742
+ "CREDENTIAL_STORE_CORRUPT", "the protected credential envelope is invalid"
743
+ )
744
+
745
+
746
+ def _permission_refusal() -> CredentialStoreError:
747
+ return CredentialStoreError(
748
+ "CREDENTIAL_STORE_PERMISSIONS",
749
+ "secure-file storage is not an owner-controlled regular single-link file",
750
+ )
751
+
752
+
753
+ def _validate_saved_transition(
754
+ current: CredentialEnvelope | None,
755
+ incoming: CredentialEnvelope,
756
+ *,
757
+ expected_generation: int | None,
758
+ ) -> None:
759
+ """Prove a generic save is exactly one active-to-active generation transition."""
760
+
761
+ actual_generation = None if current is None else current.generation
762
+ if actual_generation != expected_generation:
763
+ raise CredentialStoreError(
764
+ "CREDENTIAL_GENERATION_CONFLICT",
765
+ "the protected credential generation changed",
766
+ )
767
+ if current is not None and current.state != "active":
768
+ raise CredentialStoreError(
769
+ "CREDENTIAL_RECOVERY_REQUIRED",
770
+ "the protected credential requires recovery before another mutation",
771
+ )
772
+ next_generation = 1 if current is None else current.generation + 1
773
+ if incoming.generation != next_generation:
774
+ raise CredentialStoreError(
775
+ "CREDENTIAL_GENERATION_CONFLICT",
776
+ "the protected credential generation must advance exactly once",
777
+ )
778
+ if incoming.state != "active":
779
+ raise CredentialStoreError(
780
+ "CREDENTIAL_RECOVERY_REQUIRED",
781
+ "generic credential saves may only install an active generation",
782
+ )
783
+
784
+
785
+ def _ensure_private_directory(path: Path) -> None:
786
+ try:
787
+ try:
788
+ path.mkdir(mode=0o700, parents=True, exist_ok=False)
789
+ created = True
790
+ except FileExistsError:
791
+ created = False
792
+ if created and os.name == "posix":
793
+ os.chmod(path, 0o700, follow_symlinks=False)
794
+ _validate_private_directory(path)
795
+ except OSError:
796
+ raise CredentialStoreError(
797
+ "CREDENTIAL_STORE_PERMISSIONS", "the credential directory is not owner controlled"
798
+ ) from None
799
+
800
+
801
+ def _validate_private_directory(path: Path) -> None:
802
+ try:
803
+ info = path.stat(follow_symlinks=False)
804
+ except OSError:
805
+ raise CredentialStoreError(
806
+ "CREDENTIAL_STORE_PERMISSIONS", "the credential directory is not owner controlled"
807
+ ) from None
808
+ if not stat.S_ISDIR(info.st_mode):
809
+ raise CredentialStoreError(
810
+ "CREDENTIAL_STORE_PERMISSIONS", "the credential directory is not owner controlled"
811
+ )
812
+ if os.name == "posix" and (info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) != 0o700):
813
+ raise CredentialStoreError(
814
+ "CREDENTIAL_STORE_PERMISSIONS", "the credential directory is not owner controlled"
815
+ )
816
+ if os.name == "nt" and not _windows_acl_is_private(path):
817
+ raise CredentialStoreError(
818
+ "CREDENTIAL_STORE_PERMISSIONS", "the credential directory is not owner controlled"
819
+ )
820
+
821
+
822
+ def _validate_private_file(info: os.stat_result, *, path: Path | None = None) -> None:
823
+ valid = stat.S_ISREG(info.st_mode) and info.st_nlink == 1
824
+ if os.name == "posix":
825
+ valid = valid and info.st_uid == os.geteuid() and stat.S_IMODE(info.st_mode) == 0o600
826
+ elif os.name == "nt":
827
+ valid = path is not None and _windows_acl_is_private(path)
828
+ if not valid:
829
+ raise _permission_refusal()
830
+
831
+
832
+ def _windows_acl_is_private(path: Path) -> bool:
833
+ """Prove only the current user, SYSTEM, or Administrators can mutate a Windows path."""
834
+
835
+ if os.name != "nt":
836
+ return True
837
+ try:
838
+ import ctypes
839
+ from ctypes import wintypes
840
+
841
+ advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
842
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
843
+ _configure_windows_security_apis(advapi32, kernel32)
844
+ get_attributes = kernel32.GetFileAttributesW
845
+ get_attributes.argtypes = [wintypes.LPCWSTR]
846
+ get_attributes.restype = wintypes.DWORD
847
+ attributes = get_attributes(str(path))
848
+ if attributes == 0xFFFFFFFF or attributes & 0x400: # invalid or reparse point
849
+ return False
850
+
851
+ owner = ctypes.c_void_p()
852
+ dacl = ctypes.c_void_p()
853
+ descriptor = ctypes.c_void_p()
854
+ get_security = advapi32.GetNamedSecurityInfoW
855
+ result = get_security(
856
+ str(path),
857
+ 1, # SE_FILE_OBJECT
858
+ 0x1 | 0x4, # OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
859
+ ctypes.byref(owner),
860
+ None,
861
+ ctypes.byref(dacl),
862
+ None,
863
+ ctypes.byref(descriptor),
864
+ )
865
+ if result != 0 or not owner.value or not dacl.value or not descriptor.value:
866
+ return False
867
+ try:
868
+ owned_current_sid = _windows_current_user_sid(advapi32, kernel32)
869
+ if owned_current_sid is None:
870
+ return False
871
+ current_sid, _current_sid_backing = owned_current_sid
872
+ if not advapi32.EqualSid(owner, current_sid):
873
+ return False
874
+ allowed = [current_sid]
875
+ allocated: list[ctypes.c_void_p] = []
876
+ for text in ("S-1-5-18", "S-1-5-32-544"): # SYSTEM, Administrators
877
+ sid = ctypes.c_void_p()
878
+ if not advapi32.ConvertStringSidToSidW(text, ctypes.byref(sid)) or not sid.value:
879
+ return False
880
+ allocated.append(sid)
881
+ allowed.append(sid.value)
882
+ try:
883
+ return _windows_acl_has_only_allowed_writers(advapi32, dacl, allowed)
884
+ finally:
885
+ for sid in allocated:
886
+ kernel32.LocalFree(sid)
887
+ finally:
888
+ kernel32.LocalFree(descriptor)
889
+ except (AttributeError, OSError, TypeError, ValueError):
890
+ return False
891
+
892
+
893
+ def _configure_windows_security_apis(advapi32: object, kernel32: object) -> None:
894
+ """Bind every pointer-bearing ACL API before the first 64-bit Windows call."""
895
+
896
+ import ctypes
897
+ from ctypes import wintypes
898
+
899
+ void_pointer_pointer = ctypes.POINTER(ctypes.c_void_p)
900
+ advapi32.GetNamedSecurityInfoW.argtypes = [
901
+ wintypes.LPCWSTR,
902
+ wintypes.DWORD,
903
+ wintypes.DWORD,
904
+ void_pointer_pointer,
905
+ void_pointer_pointer,
906
+ void_pointer_pointer,
907
+ void_pointer_pointer,
908
+ void_pointer_pointer,
909
+ ]
910
+ advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD
911
+ advapi32.EqualSid.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
912
+ advapi32.EqualSid.restype = wintypes.BOOL
913
+ advapi32.ConvertStringSidToSidW.argtypes = [wintypes.LPCWSTR, void_pointer_pointer]
914
+ advapi32.ConvertStringSidToSidW.restype = wintypes.BOOL
915
+ advapi32.OpenProcessToken.argtypes = [
916
+ wintypes.HANDLE,
917
+ wintypes.DWORD,
918
+ ctypes.POINTER(wintypes.HANDLE),
919
+ ]
920
+ advapi32.OpenProcessToken.restype = wintypes.BOOL
921
+ advapi32.GetTokenInformation.argtypes = [
922
+ wintypes.HANDLE,
923
+ ctypes.c_int,
924
+ ctypes.c_void_p,
925
+ wintypes.DWORD,
926
+ ctypes.POINTER(wintypes.DWORD),
927
+ ]
928
+ advapi32.GetTokenInformation.restype = wintypes.BOOL
929
+ advapi32.GetAce.argtypes = [ctypes.c_void_p, wintypes.DWORD, void_pointer_pointer]
930
+ advapi32.GetAce.restype = wintypes.BOOL
931
+ kernel32.GetCurrentProcess.argtypes = []
932
+ kernel32.GetCurrentProcess.restype = wintypes.HANDLE
933
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
934
+ kernel32.CloseHandle.restype = wintypes.BOOL
935
+ kernel32.LocalFree.argtypes = [ctypes.c_void_p]
936
+ kernel32.LocalFree.restype = ctypes.c_void_p
937
+
938
+
939
+ def _windows_current_user_sid(advapi32: object, kernel32: object) -> tuple[int, object] | None:
940
+ import ctypes
941
+ from ctypes import wintypes
942
+
943
+ token = wintypes.HANDLE()
944
+ if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), 0x8, ctypes.byref(token)):
945
+ return None
946
+ try:
947
+ required = wintypes.DWORD()
948
+ advapi32.GetTokenInformation(token, 1, None, 0, ctypes.byref(required))
949
+ if required.value == 0:
950
+ return None
951
+ buffer = ctypes.create_string_buffer(required.value)
952
+ if not advapi32.GetTokenInformation(
953
+ token, 1, buffer, required.value, ctypes.byref(required)
954
+ ):
955
+ return None
956
+ sid = ctypes.cast(buffer, ctypes.POINTER(ctypes.c_void_p)).contents.value
957
+ if sid is None:
958
+ return None
959
+ return sid, buffer
960
+ finally:
961
+ kernel32.CloseHandle(token)
962
+
963
+
964
+ def _windows_acl_has_only_allowed_writers(
965
+ advapi32: object,
966
+ dacl: object,
967
+ allowed_sids: list[int],
968
+ ) -> bool:
969
+ import ctypes
970
+ from ctypes import wintypes
971
+
972
+ class ACL(ctypes.Structure):
973
+ _fields_ = [
974
+ ("revision", ctypes.c_ubyte),
975
+ ("reserved", ctypes.c_ubyte),
976
+ ("size", ctypes.c_ushort),
977
+ ("ace_count", ctypes.c_ushort),
978
+ ("reserved2", ctypes.c_ushort),
979
+ ]
980
+
981
+ class ACE_HEADER(ctypes.Structure):
982
+ _fields_ = [
983
+ ("ace_type", ctypes.c_ubyte),
984
+ ("ace_flags", ctypes.c_ubyte),
985
+ ("ace_size", ctypes.c_ushort),
986
+ ]
987
+
988
+ class ACCESS_ALLOWED_ACE(ctypes.Structure):
989
+ _fields_ = [
990
+ ("header", ACE_HEADER),
991
+ ("mask", wintypes.DWORD),
992
+ ("sid_start", wintypes.DWORD),
993
+ ]
994
+
995
+ write_mask = (
996
+ 0x2
997
+ | 0x4
998
+ | 0x10
999
+ | 0x40 # FILE_DELETE_CHILD grants directory-entry deletion.
1000
+ | 0x100
1001
+ | 0x10000
1002
+ | 0x40000
1003
+ | 0x80000
1004
+ | 0x10000000
1005
+ | 0x40000000
1006
+ )
1007
+ non_granting_ace_types = {
1008
+ 1, # ACCESS_DENIED_ACE_TYPE
1009
+ 2, # SYSTEM_AUDIT_ACE_TYPE
1010
+ 3, # SYSTEM_ALARM_ACE_TYPE
1011
+ 6, # ACCESS_DENIED_OBJECT_ACE_TYPE
1012
+ 7, # SYSTEM_AUDIT_OBJECT_ACE_TYPE
1013
+ 8, # SYSTEM_ALARM_OBJECT_ACE_TYPE
1014
+ 10, # ACCESS_DENIED_CALLBACK_ACE_TYPE
1015
+ 12, # ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE
1016
+ 13, # SYSTEM_AUDIT_CALLBACK_ACE_TYPE
1017
+ 14, # SYSTEM_ALARM_CALLBACK_ACE_TYPE
1018
+ 15, # SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE
1019
+ 16, # SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE
1020
+ 17, # SYSTEM_MANDATORY_LABEL_ACE_TYPE
1021
+ 18, # SYSTEM_RESOURCE_ATTRIBUTE_ACE_TYPE
1022
+ 19, # SYSTEM_SCOPED_POLICY_ID_ACE_TYPE
1023
+ 20, # SYSTEM_PROCESS_TRUST_LABEL_ACE_TYPE
1024
+ 21, # SYSTEM_ACCESS_FILTER_ACE_TYPE
1025
+ }
1026
+ acl = ctypes.cast(dacl, ctypes.POINTER(ACL)).contents
1027
+ for index in range(acl.ace_count):
1028
+ ace_pointer = ctypes.c_void_p()
1029
+ if not advapi32.GetAce(dacl, index, ctypes.byref(ace_pointer)):
1030
+ return False
1031
+ header = ctypes.cast(ace_pointer, ctypes.POINTER(ACE_HEADER)).contents
1032
+ if header.ace_type != 0:
1033
+ if header.ace_type in non_granting_ace_types:
1034
+ continue
1035
+ return False # Unsupported or unknown granting ACE: the writer proof is incomplete.
1036
+ ace = ctypes.cast(ace_pointer, ctypes.POINTER(ACCESS_ALLOWED_ACE)).contents
1037
+ if not ace.mask & write_mask:
1038
+ continue
1039
+ sid = ace_pointer.value + ACCESS_ALLOWED_ACE.sid_start.offset
1040
+ if not any(advapi32.EqualSid(sid, permitted) for permitted in allowed_sids):
1041
+ return False
1042
+ return True
1043
+
1044
+
1045
+ __all__ = [
1046
+ "CREDENTIAL_ENVELOPE_SCHEMA",
1047
+ "CredentialBackend",
1048
+ "CredentialEnvelope",
1049
+ "CredentialLease",
1050
+ "CredentialStore",
1051
+ "CredentialStoreError",
1052
+ "ProtectedCredential",
1053
+ "SecureFileBackend",
1054
+ "credential_lock",
1055
+ ]