rabbit-brain 0.6.0__tar.gz

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 (266) hide show
  1. rabbit_brain-0.6.0/.cargo/config.toml +10 -0
  2. rabbit_brain-0.6.0/.github/scripts/check-attribution.py +152 -0
  3. rabbit_brain-0.6.0/.github/scripts/check-public-content.py +312 -0
  4. rabbit_brain-0.6.0/.github/scripts/smoke-wheel.py +133 -0
  5. rabbit_brain-0.6.0/.github/scripts/test-attribution.py +247 -0
  6. rabbit_brain-0.6.0/.github/scripts/test-public-content.py +426 -0
  7. rabbit_brain-0.6.0/.github/workflows/attribution.yml +26 -0
  8. rabbit_brain-0.6.0/.github/workflows/build-wheels.yml +96 -0
  9. rabbit_brain-0.6.0/.github/workflows/public-content.yml +46 -0
  10. rabbit_brain-0.6.0/.github/workflows/publish.yml +168 -0
  11. rabbit_brain-0.6.0/.github/workflows/tests.yml +142 -0
  12. rabbit_brain-0.6.0/AGENTS.md +699 -0
  13. rabbit_brain-0.6.0/CHANGELOG.md +74 -0
  14. rabbit_brain-0.6.0/CONTRIBUTING.md +49 -0
  15. rabbit_brain-0.6.0/LICENSE +202 -0
  16. rabbit_brain-0.6.0/PKG-INFO +197 -0
  17. rabbit_brain-0.6.0/PRIVACY.md +72 -0
  18. rabbit_brain-0.6.0/README.md +154 -0
  19. rabbit_brain-0.6.0/RELEASING.md +64 -0
  20. rabbit_brain-0.6.0/SECURITY.md +7 -0
  21. rabbit_brain-0.6.0/SKILL.md +60 -0
  22. rabbit_brain-0.6.0/THIRD-PARTY-NOTICES +2958 -0
  23. rabbit_brain-0.6.0/bench/__init__.py +0 -0
  24. rabbit_brain-0.6.0/bench/agent/SPEC.md +62 -0
  25. rabbit_brain-0.6.0/bench/agent/__init__.py +0 -0
  26. rabbit_brain-0.6.0/bench/agent/cases.py +316 -0
  27. rabbit_brain-0.6.0/bench/agent/checks.py +487 -0
  28. rabbit_brain-0.6.0/bench/agent/drive.py +59 -0
  29. rabbit_brain-0.6.0/bench/agent/run.py +170 -0
  30. rabbit_brain-0.6.0/bench/agent/scripted.py +502 -0
  31. rabbit_brain-0.6.0/docs/README.md +19 -0
  32. rabbit_brain-0.6.0/llms.txt +25 -0
  33. rabbit_brain-0.6.0/pyproject.toml +77 -0
  34. rabbit_brain-0.6.0/rust/ARCHITECTURE.md +200 -0
  35. rabbit_brain-0.6.0/rust/Cargo.lock +1291 -0
  36. rabbit_brain-0.6.0/rust/Cargo.toml +79 -0
  37. rabbit_brain-0.6.0/rust/src/acquisition.rs +150 -0
  38. rabbit_brain-0.6.0/rust/src/actor.rs +457 -0
  39. rabbit_brain-0.6.0/rust/src/argparse.rs +1321 -0
  40. rabbit_brain-0.6.0/rust/src/assess.rs +1105 -0
  41. rabbit_brain-0.6.0/rust/src/assets/export_readme.txt +55 -0
  42. rabbit_brain-0.6.0/rust/src/assets/mcp.json +1 -0
  43. rabbit_brain-0.6.0/rust/src/assets/start.md +149 -0
  44. rabbit_brain-0.6.0/rust/src/bin/rabbit-brain.rs +43 -0
  45. rabbit_brain-0.6.0/rust/src/bin/rbdiff.rs +159 -0
  46. rabbit_brain-0.6.0/rust/src/bridge.rs +1113 -0
  47. rabbit_brain-0.6.0/rust/src/cli.rs +776 -0
  48. rabbit_brain-0.6.0/rust/src/cli_parser.py +579 -0
  49. rabbit_brain-0.6.0/rust/src/cli_spec.json +7106 -0
  50. rabbit_brain-0.6.0/rust/src/clone.rs +751 -0
  51. rabbit_brain-0.6.0/rust/src/cmds/info.rs +198 -0
  52. rabbit_brain-0.6.0/rust/src/cmds/mod.rs +530 -0
  53. rabbit_brain-0.6.0/rust/src/cmds/reading.rs +2957 -0
  54. rabbit_brain-0.6.0/rust/src/cmds/record.rs +2615 -0
  55. rabbit_brain-0.6.0/rust/src/cmds/share.rs +208 -0
  56. rabbit_brain-0.6.0/rust/src/cmds/tour.rs +219 -0
  57. rabbit_brain-0.6.0/rust/src/cmds/upkeep.rs +1011 -0
  58. rabbit_brain-0.6.0/rust/src/commands.rs +138 -0
  59. rabbit_brain-0.6.0/rust/src/compiler.rs +736 -0
  60. rabbit_brain-0.6.0/rust/src/delta.rs +485 -0
  61. rabbit_brain-0.6.0/rust/src/dry.rs +463 -0
  62. rabbit_brain-0.6.0/rust/src/elsewhere.rs +563 -0
  63. rabbit_brain-0.6.0/rust/src/environment.rs +121 -0
  64. rabbit_brain-0.6.0/rust/src/errors.rs +128 -0
  65. rabbit_brain-0.6.0/rust/src/errors_table.rs +27 -0
  66. rabbit_brain-0.6.0/rust/src/export.rs +1937 -0
  67. rabbit_brain-0.6.0/rust/src/graph.rs +467 -0
  68. rabbit_brain-0.6.0/rust/src/history.rs +2053 -0
  69. rabbit_brain-0.6.0/rust/src/http.rs +462 -0
  70. rabbit_brain-0.6.0/rust/src/hub.rs +832 -0
  71. rabbit_brain-0.6.0/rust/src/ir.py +101 -0
  72. rabbit_brain-0.6.0/rust/src/journal.rs +3636 -0
  73. rabbit_brain-0.6.0/rust/src/ledger/authority.rs +425 -0
  74. rabbit_brain-0.6.0/rust/src/ledger/cite.rs +456 -0
  75. rabbit_brain-0.6.0/rust/src/ledger/design.rs +2948 -0
  76. rabbit_brain-0.6.0/rust/src/ledger/findings.rs +2581 -0
  77. rabbit_brain-0.6.0/rust/src/ledger/helpers.rs +546 -0
  78. rabbit_brain-0.6.0/rust/src/ledger/inquiries.rs +805 -0
  79. rabbit_brain-0.6.0/rust/src/ledger/mod.rs +4525 -0
  80. rabbit_brain-0.6.0/rust/src/ledger/relations.rs +334 -0
  81. rabbit_brain-0.6.0/rust/src/ledger/report_link.rs +315 -0
  82. rabbit_brain-0.6.0/rust/src/ledger/resync.rs +198 -0
  83. rabbit_brain-0.6.0/rust/src/ledger/support.rs +819 -0
  84. rabbit_brain-0.6.0/rust/src/ledger/updates.rs +625 -0
  85. rabbit_brain-0.6.0/rust/src/ledger/views.rs +2212 -0
  86. rabbit_brain-0.6.0/rust/src/ledger/writes.rs +3519 -0
  87. rabbit_brain-0.6.0/rust/src/lib.rs +66 -0
  88. rabbit_brain-0.6.0/rust/src/main.rs +7 -0
  89. rabbit_brain-0.6.0/rust/src/mcp.rs +501 -0
  90. rabbit_brain-0.6.0/rust/src/mcp_tools.py +304 -0
  91. rabbit_brain-0.6.0/rust/src/model.rs +795 -0
  92. rabbit_brain-0.6.0/rust/src/next_step.rs +284 -0
  93. rabbit_brain-0.6.0/rust/src/owned.rs +384 -0
  94. rabbit_brain-0.6.0/rust/src/page_assets.rs +3 -0
  95. rabbit_brain-0.6.0/rust/src/places.rs +1086 -0
  96. rabbit_brain-0.6.0/rust/src/plain.rs +1022 -0
  97. rabbit_brain-0.6.0/rust/src/pyfloat.rs +674 -0
  98. rabbit_brain-0.6.0/rust/src/pyjson.rs +575 -0
  99. rabbit_brain-0.6.0/rust/src/pytime.rs +228 -0
  100. rabbit_brain-0.6.0/rust/src/pyyaml.rs +464 -0
  101. rabbit_brain-0.6.0/rust/src/rbyaml.rs +1794 -0
  102. rabbit_brain-0.6.0/rust/src/record.rs +1019 -0
  103. rabbit_brain-0.6.0/rust/src/record_gen.rs +3683 -0
  104. rabbit_brain-0.6.0/rust/src/remove.rs +1038 -0
  105. rabbit_brain-0.6.0/rust/src/repo.rs +1782 -0
  106. rabbit_brain-0.6.0/rust/src/reproduce.rs +3240 -0
  107. rabbit_brain-0.6.0/rust/src/rng.rs +66 -0
  108. rabbit_brain-0.6.0/rust/src/schemas.json +1 -0
  109. rabbit_brain-0.6.0/rust/src/shell/assets/guide.css +36 -0
  110. rabbit_brain-0.6.0/rust/src/shell/assets/live.js +177 -0
  111. rabbit_brain-0.6.0/rust/src/shell/assets/open.css +91 -0
  112. rabbit_brain-0.6.0/rust/src/shell/assets/print.css +25 -0
  113. rabbit_brain-0.6.0/rust/src/shell/assets/script.js +1576 -0
  114. rabbit_brain-0.6.0/rust/src/shell/assets/shell.css +990 -0
  115. rabbit_brain-0.6.0/rust/src/shell/figure.rs +423 -0
  116. rabbit_brain-0.6.0/rust/src/shell/history_view.rs +747 -0
  117. rabbit_brain-0.6.0/rust/src/shell/mod.rs +197 -0
  118. rabbit_brain-0.6.0/rust/src/shell/model.rs +3138 -0
  119. rabbit_brain-0.6.0/rust/src/shell/nodes.rs +5572 -0
  120. rabbit_brain-0.6.0/rust/src/shell/open.rs +488 -0
  121. rabbit_brain-0.6.0/rust/src/shell/strings.rs +710 -0
  122. rabbit_brain-0.6.0/rust/src/shell/tier.rs +167 -0
  123. rabbit_brain-0.6.0/rust/src/shell/views.rs +5533 -0
  124. rabbit_brain-0.6.0/rust/src/skill.rs +881 -0
  125. rabbit_brain-0.6.0/rust/src/sources.rs +2395 -0
  126. rabbit_brain-0.6.0/rust/src/splice.rs +575 -0
  127. rabbit_brain-0.6.0/rust/src/sqlite.rs +624 -0
  128. rabbit_brain-0.6.0/rust/src/surface.rs +307 -0
  129. rabbit_brain-0.6.0/rust/src/sys.rs +1098 -0
  130. rabbit_brain-0.6.0/rust/src/telemetry.rs +1312 -0
  131. rabbit_brain-0.6.0/rust/src/usage.rs +115 -0
  132. rabbit_brain-0.6.0/rust/src/value.rs +354 -0
  133. rabbit_brain-0.6.0/rust/src/view.rs +4406 -0
  134. rabbit_brain-0.6.0/rust/src/webui.rs +1203 -0
  135. rabbit_brain-0.6.0/rust/src/zipfile.rs +445 -0
  136. rabbit_brain-0.6.0/schema/rb-v1.schema.json +3886 -0
  137. rabbit_brain-0.6.0/src/rabbit_brain/__init__.py +33 -0
  138. rabbit_brain-0.6.0/src/rabbit_brain/_rb.py +180 -0
  139. rabbit_brain-0.6.0/src/rabbit_brain/errors.py +71 -0
  140. rabbit_brain-0.6.0/src/rabbit_brain/models.py +33 -0
  141. rabbit_brain-0.6.0/src/rabbit_brain/record.py +1321 -0
  142. rabbit_brain-0.6.0/src/rabbit_brain/sdk.py +626 -0
  143. rabbit_brain-0.6.0/src/rabbit_brain/tui.py +1315 -0
  144. rabbit_brain-0.6.0/tests/app/app.mjs +1336 -0
  145. rabbit_brain-0.6.0/tests/app/cdp.mjs +480 -0
  146. rabbit_brain-0.6.0/tests/app/hub.mjs +68 -0
  147. rabbit_brain-0.6.0/tests/app/overview-v2.html +931 -0
  148. rabbit_brain-0.6.0/tests/app/p12.mjs +91 -0
  149. rabbit_brain-0.6.0/tests/app/rail.mjs +56 -0
  150. rabbit_brain-0.6.0/tests/app/rerun.mjs +363 -0
  151. rabbit_brain-0.6.0/tests/app/shoot.mjs +202 -0
  152. rabbit_brain-0.6.0/tests/app/smoke.mjs +32 -0
  153. rabbit_brain-0.6.0/tests/app/terms.mjs +82 -0
  154. rabbit_brain-0.6.0/tests/app/test_app.py +1081 -0
  155. rabbit_brain-0.6.0/tests/app/v2.mjs +194 -0
  156. rabbit_brain-0.6.0/tests/app/viewer.mjs +284 -0
  157. rabbit_brain-0.6.0/tests/conformance.py +65 -0
  158. rabbit_brain-0.6.0/tests/conftest.py +30 -0
  159. rabbit_brain-0.6.0/tests/fixtures/inquiry-before-design.json +70 -0
  160. rabbit_brain-0.6.0/tests/fixtures/question-call-before-links.json +102 -0
  161. rabbit_brain-0.6.0/tests/fixtures/repo_study.py +133 -0
  162. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/.gitattributes +1 -0
  163. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/.gitignore +4 -0
  164. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/experiments/e1/criteria.yaml +28 -0
  165. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/experiments/e1/experiment.yaml +64 -0
  166. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/experiments/e1/runs/runnnqi.yaml +42 -0
  167. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/experiments/e1/runs/runxpwd.yaml +42 -0
  168. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/experiments/e1/runs/runydns.yaml +42 -0
  169. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/log.jsonl +15 -0
  170. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/project.yaml +13 -0
  171. rabbit_brain-0.6.0/tests/fixtures/shared-files-record/.rb/questions/q1.yaml +7 -0
  172. rabbit_brain-0.6.0/tests/fixtures/shell/OWNERS.tsv +102 -0
  173. rabbit_brain-0.6.0/tests/fixtures/shell/dom-contract.json +377 -0
  174. rabbit_brain-0.6.0/tests/fixtures/shell/dom-fixture.html +958 -0
  175. rabbit_brain-0.6.0/tests/fixtures/shell/int8-demo.away.view.json +5948 -0
  176. rabbit_brain-0.6.0/tests/fixtures/shell/int8-demo.live.view.json +1953 -0
  177. rabbit_brain-0.6.0/tests/fixtures/shell/int8-demo.owner.view.json +5765 -0
  178. rabbit_brain-0.6.0/tests/fixtures/shell/int8-demo.past.view.json +5678 -0
  179. rabbit_brain-0.6.0/tests/fixtures/shell/merged-phases.txt +42 -0
  180. rabbit_brain-0.6.0/tests/fixtures/shell/owner_of.py +58 -0
  181. rabbit_brain-0.6.0/tests/fixtures/shell/place_of.py +78 -0
  182. rabbit_brain-0.6.0/tests/fixtures/shell/shape.py +205 -0
  183. rabbit_brain-0.6.0/tests/fixtures/shell/view-1.shape.tsv +847 -0
  184. rabbit_brain-0.6.0/tests/rbtest.py +1202 -0
  185. rabbit_brain-0.6.0/tests/rbyaml.py +1173 -0
  186. rabbit_brain-0.6.0/tests/shell_baselines.py +103 -0
  187. rabbit_brain-0.6.0/tests/test_adversarial.py +466 -0
  188. rabbit_brain-0.6.0/tests/test_agent_contract.py +117 -0
  189. rabbit_brain-0.6.0/tests/test_authority.py +516 -0
  190. rabbit_brain-0.6.0/tests/test_boundary.py +133 -0
  191. rabbit_brain-0.6.0/tests/test_call_authority.py +1039 -0
  192. rabbit_brain-0.6.0/tests/test_call_currency.py +287 -0
  193. rabbit_brain-0.6.0/tests/test_cli.py +50 -0
  194. rabbit_brain-0.6.0/tests/test_clone.py +667 -0
  195. rabbit_brain-0.6.0/tests/test_compile.py +199 -0
  196. rabbit_brain-0.6.0/tests/test_delta.py +298 -0
  197. rabbit_brain-0.6.0/tests/test_demo_record.py +2600 -0
  198. rabbit_brain-0.6.0/tests/test_demo_records.py +175 -0
  199. rabbit_brain-0.6.0/tests/test_design_standing.py +2928 -0
  200. rabbit_brain-0.6.0/tests/test_docs.py +489 -0
  201. rabbit_brain-0.6.0/tests/test_dry_run.py +656 -0
  202. rabbit_brain-0.6.0/tests/test_env.py +115 -0
  203. rabbit_brain-0.6.0/tests/test_export.py +1371 -0
  204. rabbit_brain-0.6.0/tests/test_failures.py +378 -0
  205. rabbit_brain-0.6.0/tests/test_finding_relations.py +343 -0
  206. rabbit_brain-0.6.0/tests/test_format_forward.py +534 -0
  207. rabbit_brain-0.6.0/tests/test_found_and_next.py +1309 -0
  208. rabbit_brain-0.6.0/tests/test_generated.py +31 -0
  209. rabbit_brain-0.6.0/tests/test_golden_harness.py +106 -0
  210. rabbit_brain-0.6.0/tests/test_graph.py +411 -0
  211. rabbit_brain-0.6.0/tests/test_history.py +508 -0
  212. rabbit_brain-0.6.0/tests/test_inquiries.py +963 -0
  213. rabbit_brain-0.6.0/tests/test_journal.py +564 -0
  214. rabbit_brain-0.6.0/tests/test_journal_rerun.py +205 -0
  215. rabbit_brain-0.6.0/tests/test_ledger.py +2184 -0
  216. rabbit_brain-0.6.0/tests/test_ledger_integrity.py +2190 -0
  217. rabbit_brain-0.6.0/tests/test_line_endings.py +141 -0
  218. rabbit_brain-0.6.0/tests/test_live.py +803 -0
  219. rabbit_brain-0.6.0/tests/test_looked_at.py +1146 -0
  220. rabbit_brain-0.6.0/tests/test_newer_write_atomicity.py +103 -0
  221. rabbit_brain-0.6.0/tests/test_next_step.py +54 -0
  222. rabbit_brain-0.6.0/tests/test_no_writer_in_text.py +358 -0
  223. rabbit_brain-0.6.0/tests/test_no_writer_on_screens.py +358 -0
  224. rabbit_brain-0.6.0/tests/test_notes_and_gates.py +656 -0
  225. rabbit_brain-0.6.0/tests/test_open.py +1845 -0
  226. rabbit_brain-0.6.0/tests/test_page.py +573 -0
  227. rabbit_brain-0.6.0/tests/test_parallel.py +1577 -0
  228. rabbit_brain-0.6.0/tests/test_places.py +173 -0
  229. rabbit_brain-0.6.0/tests/test_plain_text.py +216 -0
  230. rabbit_brain-0.6.0/tests/test_project_title.py +196 -0
  231. rabbit_brain-0.6.0/tests/test_question_standing.py +275 -0
  232. rabbit_brain-0.6.0/tests/test_rbyaml.py +298 -0
  233. rabbit_brain-0.6.0/tests/test_read_only_git.py +245 -0
  234. rabbit_brain-0.6.0/tests/test_readings.py +248 -0
  235. rabbit_brain-0.6.0/tests/test_relation_scope.py +90 -0
  236. rabbit_brain-0.6.0/tests/test_release_setup.py +119 -0
  237. rabbit_brain-0.6.0/tests/test_release_workflows.py +93 -0
  238. rabbit_brain-0.6.0/tests/test_removal.py +417 -0
  239. rabbit_brain-0.6.0/tests/test_remove.py +827 -0
  240. rabbit_brain-0.6.0/tests/test_repo.py +455 -0
  241. rabbit_brain-0.6.0/tests/test_reproduce.py +439 -0
  242. rabbit_brain-0.6.0/tests/test_rerun.py +408 -0
  243. rabbit_brain-0.6.0/tests/test_resolution.py +133 -0
  244. rabbit_brain-0.6.0/tests/test_rule_labels.py +97 -0
  245. rabbit_brain-0.6.0/tests/test_run_scope.py +730 -0
  246. rabbit_brain-0.6.0/tests/test_schema_file.py +27 -0
  247. rabbit_brain-0.6.0/tests/test_screen_reads.py +150 -0
  248. rabbit_brain-0.6.0/tests/test_sdk_mcp.py +587 -0
  249. rabbit_brain-0.6.0/tests/test_setting_reads.py +302 -0
  250. rabbit_brain-0.6.0/tests/test_shell.py +4369 -0
  251. rabbit_brain-0.6.0/tests/test_shell_cards.py +785 -0
  252. rabbit_brain-0.6.0/tests/test_shell_contract.py +596 -0
  253. rabbit_brain-0.6.0/tests/test_shell_model.py +2938 -0
  254. rabbit_brain-0.6.0/tests/test_skill.py +491 -0
  255. rabbit_brain-0.6.0/tests/test_snapshot.py +135 -0
  256. rabbit_brain-0.6.0/tests/test_sqlite.py +962 -0
  257. rabbit_brain-0.6.0/tests/test_start.py +1058 -0
  258. rabbit_brain-0.6.0/tests/test_surface.py +796 -0
  259. rabbit_brain-0.6.0/tests/test_sync.py +859 -0
  260. rabbit_brain-0.6.0/tests/test_telemetry.py +557 -0
  261. rabbit_brain-0.6.0/tests/test_telemetry_surfaces.py +221 -0
  262. rabbit_brain-0.6.0/tests/test_tour.py +137 -0
  263. rabbit_brain-0.6.0/tests/test_tui.py +1779 -0
  264. rabbit_brain-0.6.0/tests/test_type_guards.py +651 -0
  265. rabbit_brain-0.6.0/tests/test_worktrees.py +1150 -0
  266. rabbit_brain-0.6.0/tests/test_writers.py +639 -0
@@ -0,0 +1,10 @@
1
+ # rb.exe and rabbit-brain.exe carry the C runtime in themselves, so they start where the Visual C++ Redistributable is
2
+ # not installed (a venv does not bring it); cargo reads this file when it runs from the repository or below it
3
+ [target.x86_64-pc-windows-msvc]
4
+ rustflags = ["-C", "target-feature=+crt-static"]
5
+
6
+ # rb reads the ordinary tables of a SQLite database (rb run add --from, rb compile): the parts of the SQLite built into it
7
+ # that serve other uses (full-text search, R*Tree, dbstat, stat4, soundex, column metadata, loading extensions) are left
8
+ # out, about 0.5 MB of the binary; the variable set in the environment wins
9
+ [env]
10
+ LIBSQLITE3_FLAGS = "-USQLITE_ENABLE_FTS3 -USQLITE_ENABLE_FTS3_PARENTHESIS -USQLITE_ENABLE_FTS5 -USQLITE_ENABLE_RTREE -USQLITE_ENABLE_DBSTAT_VTAB -USQLITE_ENABLE_STAT4 -USQLITE_SOUNDEX -USQLITE_ENABLE_COLUMN_METADATA -USQLITE_ENABLE_LOAD_EXTENSION -DSQLITE_OMIT_LOAD_EXTENSION"
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ """Fail when a commit or a pull request credits a coding agent.
3
+
4
+ A commit fails when its author or committer is an agent identity, or when its message carries a co-author trailer
5
+ that names an agent, an agent session trailer, a generated-with footer or an agent session link. A pull request fails
6
+ when its title or body carries any of these. Names and addresses are matched whole: an identity is an agent's when its
7
+ name is an agent's name or its address is an agent's, a co-author trailer when its name starts with an agent's name or
8
+ its address is an agent's, and a footer only as a line of its own. A contributor whose name or address only contains
9
+ an agent's name passes. With no revisions, the GitHub Actions event names what to check: the commits a push adds, or a
10
+ pull request's commits over its base, with its title and body. Revisions name commits as git rev-list takes them, for
11
+ a check before pushing:
12
+
13
+ python .github/scripts/check-attribution.py origin/main..HEAD
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ import re
21
+ import subprocess
22
+ import sys
23
+
24
+ # Matched whole: the names agents commit under, and the addresses (Anthropic's, and an agent's GitHub app).
25
+ AGENT_NAME = r"(?:claude(?: code)?|codex|chatgpt|cursor agent)(?: ?\[bot\])?"
26
+ AGENT_EMAIL = (r"(?:noreply@anthropic\.com"
27
+ r"|(?:\d+\+)?(?:claude|codex|chatgpt|copilot|cursor|gemini)[a-z-]*\[bot\]@users\.noreply\.github\.com)")
28
+ AGENT_IDENTITY = re.compile(rf"{AGENT_NAME}[ \t]*<[^<>]*>|[^<>]*<{AGENT_EMAIL}>", re.IGNORECASE)
29
+ AGENT_TEXT = (
30
+ ("a co-author trailer that names an agent", re.compile(
31
+ rf"^[ \t]*co-authored-by:[ \t]*(?:(?:claude|codex|chatgpt|copilot|cursor|gemini)\b|[^<\n]*<{AGENT_EMAIL}>)",
32
+ re.IGNORECASE | re.MULTILINE)),
33
+ ("an agent session trailer", re.compile(r"^[ \t]*(?:claude|codex)-session:", re.IGNORECASE | re.MULTILINE)),
34
+ ("a generated-with footer", re.compile(
35
+ r"^[ \t]*[^\w\n]*[ \t]*generated[ \t]+(?:with|by)[ \t]+\[?(?:claude(?: code)?|codex|chatgpt)\]?"
36
+ r"(?:\([^)\n]*\))?\.?[ \t]*$", re.IGNORECASE | re.MULTILINE)),
37
+ ("an agent session link", re.compile(r"claude\.ai/code/session", re.IGNORECASE)),
38
+ )
39
+ COMMIT_ID = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}")
40
+ DELETED = re.compile(r"0+")
41
+
42
+
43
+ class CheckError(Exception):
44
+ pass
45
+
46
+
47
+ def has_commit(repo, rev):
48
+ return subprocess.run(["git", "-C", str(repo), "cat-file", "-e", f"{rev}^{{commit}}"],
49
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False).returncode == 0
50
+
51
+
52
+ def commits(repo, revisions):
53
+ """(id, author, committer, message) for every commit the revisions name, with identities as recorded."""
54
+ result = subprocess.run(
55
+ ["git", "-C", str(repo), "-c", "log.showSignature=false", "log", "--no-use-mailmap", "-z",
56
+ "--format=%H%x00%an <%ae>%x00%cn <%ce>%x00%B", *revisions, "--"],
57
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
58
+ if result.returncode:
59
+ raise CheckError("git could not list the commits to check")
60
+ fields = result.stdout.decode("utf-8", "replace").split("\0")
61
+ if fields[-1] == "":
62
+ fields.pop()
63
+ # Four fields per commit, each led by its id; anything else is output this check does not understand.
64
+ if len(fields) % 4 or not all(COMMIT_ID.fullmatch(field) for field in fields[::4]):
65
+ raise CheckError("git log output could not be read")
66
+ return [tuple(fields[n:n + 4]) for n in range(0, len(fields), 4)]
67
+
68
+
69
+ def event_revisions(repo, name, event, ref):
70
+ """The revisions of the commits the event adds, or None when it adds none."""
71
+ if name == "pull_request":
72
+ base, head = event["pull_request"]["base"]["sha"], event["pull_request"]["head"]["sha"]
73
+ if has_commit(repo, base) and has_commit(repo, head):
74
+ return [head, "--not", base]
75
+ # The checkout of a pull request is its merge commit: the base tip, then the head.
76
+ if has_commit(repo, "HEAD^2"):
77
+ return ["HEAD^2", "--not", "HEAD^1"]
78
+ raise CheckError("the pull request's commits are not in the checkout")
79
+ if name == "push":
80
+ before, after = event.get("before") or "", event.get("after") or ""
81
+ if not after or DELETED.fullmatch(after):
82
+ return None
83
+ if before and not DELETED.fullmatch(before) and has_commit(repo, before):
84
+ return [after, "--not", before]
85
+ # A new branch or tag, or a forced push whose old tip is gone: what the default branch does not hold yet.
86
+ default = (event.get("repository") or {}).get("default_branch") or ""
87
+ known = f"refs/remotes/origin/{default}"
88
+ if default and ref != f"refs/heads/{default}" and has_commit(repo, known):
89
+ return [after, "--not", known]
90
+ return [after]
91
+ raise CheckError(f"no rule for the {name or 'unnamed'} event")
92
+
93
+
94
+ def normalized(text):
95
+ """The text with every line break as a newline, so a line of its own reads the same from any platform."""
96
+ return text.replace("\r\n", "\n").replace("\r", "\n")
97
+
98
+
99
+ def findings(found, text):
100
+ """What credits an agent. Nothing from a commit or a pull request is quoted, only commit ids."""
101
+ out = []
102
+ for commit, author, committer, message in found:
103
+ if AGENT_IDENTITY.fullmatch(author) or AGENT_IDENTITY.fullmatch(committer):
104
+ out.append(f"commit {commit[:12]}: the author or committer is an agent identity")
105
+ out += [f"commit {commit[:12]}: the message carries {what}"
106
+ for what, rule in AGENT_TEXT if rule.search(normalized(message))]
107
+ out += [f"the pull request's title or body carries {what}"
108
+ for what, rule in AGENT_TEXT if rule.search(normalized(text))]
109
+ return out
110
+
111
+
112
+ def main(argv=None):
113
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
114
+ parser.add_argument("revisions", nargs="*",
115
+ help="commits to check, as git rev-list takes them (default: the event's)")
116
+ parser.add_argument("--repo", type=Path, default=Path("."),
117
+ help="the git checkout (default: the current directory)")
118
+ args = parser.parse_args(argv)
119
+ try:
120
+ text = None
121
+ revisions = args.revisions
122
+ if not revisions:
123
+ name, path = os.environ.get("GITHUB_EVENT_NAME", ""), os.environ.get("GITHUB_EVENT_PATH", "")
124
+ if not path:
125
+ raise CheckError("no revisions given and no GitHub Actions event to read")
126
+ with open(path, encoding="utf-8") as source:
127
+ event = json.load(source)
128
+ revisions = event_revisions(args.repo, name, event, os.environ.get("GITHUB_REF", ""))
129
+ if name == "pull_request":
130
+ pull = event["pull_request"]
131
+ text = "\n".join(part for part in (pull.get("title"), pull.get("body")) if isinstance(part, str))
132
+ found = commits(args.repo, revisions) if revisions else []
133
+ problems = findings(found, text or "")
134
+ except CheckError as error:
135
+ print(f"Attribution check failed: {error}.", file=sys.stderr)
136
+ return 2
137
+ except (OSError, ValueError, LookupError, TypeError, AttributeError):
138
+ print("Attribution check failed: the event or the commits could not be read.", file=sys.stderr)
139
+ return 2
140
+ if problems:
141
+ for problem in problems:
142
+ print(f"Attribution check failed: {problem}.", file=sys.stderr)
143
+ print("Commit under your own name, remove what is named above, and push again.", file=sys.stderr)
144
+ return 1
145
+ checked = f"{len(found)} commit{'' if len(found) == 1 else 's'}"
146
+ print(f"Attribution check passed: {checked}"
147
+ + (", and the pull request's title and body." if text is not None else "."))
148
+ return 0
149
+
150
+
151
+ if __name__ == "__main__":
152
+ sys.exit(main())
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env python3
2
+ """Check committed files and unpacked distributions against the release denylist.
3
+
4
+ The denylist comes from RB_RELEASE_DENYLIST, one literal phrase per line. Nothing
5
+ from that list, matching content, or matching filenames is printed. Missing
6
+ configuration and unreadable inputs are failures, not successful scans.
7
+
8
+ --source names the top level of a git checkout; the files committed at HEAD are
9
+ checked as git stores them, whatever the working tree holds.
10
+
11
+ --history also checks what a push or a pull request adds: every commit message,
12
+ tag message, file and file name in its history, and a pull request's title and
13
+ body. With no revisions it reads the GitHub Actions event from the step's
14
+ environment (GITHUB_EVENT_NAME, GITHUB_REF and the SCAN_* variables the
15
+ workflow sets); with revisions, as git rev-list takes them, it checks those:
16
+
17
+ python .github/scripts/check-public-content.py --source . --history origin/main..HEAD
18
+ """
19
+
20
+ import argparse
21
+ import codecs
22
+ import gzip
23
+ import os
24
+ from pathlib import Path
25
+ import re
26
+ import stat
27
+ import subprocess
28
+ import sys
29
+ import tarfile
30
+ import zipfile
31
+ import zlib
32
+
33
+ try:
34
+ import lzma
35
+ LZMA_ERRORS = (lzma.LZMAError,)
36
+ except ImportError: # a Python built without lzma: zipfile refuses an LZMA member with RuntimeError instead
37
+ LZMA_ERRORS = ()
38
+
39
+ OBJECT_ID = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}")
40
+ NO_OBJECT = re.compile(r"0+") # the id an event gives a ref that did not exist before, or no longer does
41
+
42
+
43
+ class ScanError(Exception):
44
+ pass
45
+
46
+
47
+ class ContentMatch(Exception):
48
+ pass
49
+
50
+
51
+ def git(root: Path, *args: str) -> bytes:
52
+ """What git prints for these arguments in the checkout at root; a failure is a scan error that names nothing."""
53
+ result = subprocess.run(["git", "-C", str(root), *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
54
+ if result.returncode:
55
+ raise ScanError("git could not read the checkout")
56
+ return result.stdout
57
+
58
+
59
+ class Exactly:
60
+ """The next `size` bytes of a stream, read as a file of their own."""
61
+
62
+ def __init__(self, stream, size: int):
63
+ self.stream, self.left = stream, size
64
+
65
+ def read(self, n: int = -1) -> bytes:
66
+ n = self.left if n < 0 else min(n, self.left)
67
+ data = self.stream.read(n) if n else b""
68
+ if len(data) != n:
69
+ raise ScanError("git stopped before an object was read whole")
70
+ self.left -= n
71
+ return data
72
+
73
+
74
+ def objects(root: Path, ids):
75
+ """(id, type, contents as a file) for each object id, as git cat-file --batch gives them, one at a time."""
76
+ with subprocess.Popen(["git", "-C", str(root), "cat-file", "--batch"], stdin=subprocess.PIPE,
77
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) as batch:
78
+ for oid in ids:
79
+ batch.stdin.write(oid + b"\n")
80
+ batch.stdin.flush()
81
+ header = batch.stdout.readline().split()
82
+ if len(header) != 3 or header[0] != oid or not header[2].isdigit():
83
+ raise ScanError("git could not read a committed object")
84
+ contents = Exactly(batch.stdout, int(header[2]))
85
+ yield oid, header[1].decode("ascii"), contents
86
+ while contents.read(65536):
87
+ pass
88
+ if batch.stdout.read(1) != b"\n":
89
+ raise ScanError("git could not read a committed object")
90
+ batch.stdin.close()
91
+ if batch.wait():
92
+ raise ScanError("git could not read a committed object")
93
+
94
+
95
+ def has_object(root: Path, rev: str) -> bool:
96
+ return subprocess.run(["git", "-C", str(root), "cat-file", "-e", rev], stdout=subprocess.DEVNULL,
97
+ stderr=subprocess.DEVNULL, check=False).returncode == 0
98
+
99
+
100
+ def event_scope(root: Path, environ) -> tuple:
101
+ """What a GitHub Actions event adds: (revisions as git rev-list takes them, text it carries, what that text is).
102
+ A push adds its commits, with a pushed tag's own object, and its ref's name; a pull request adds its commits,
103
+ its title and its body."""
104
+ def object_id(key):
105
+ value = environ.get(key, "")
106
+ if value and not OBJECT_ID.fullmatch(value):
107
+ raise ScanError(f"{key} is not an object id")
108
+ return value
109
+
110
+ name = environ.get("GITHUB_EVENT_NAME", "")
111
+ if name == "pull_request":
112
+ base, head = object_id("SCAN_PR_BASE"), object_id("SCAN_PR_HEAD")
113
+ text = "\n".join(environ.get(key, "") for key in ("SCAN_PR_TITLE", "SCAN_PR_BODY"))
114
+ said = "the pull request's title and body"
115
+ if base and head and has_object(root, base) and has_object(root, head):
116
+ return [head, "--not", base], text, said
117
+ # The checkout of a pull request is its merge commit: the base tip, then the head.
118
+ if has_object(root, "HEAD^2"):
119
+ return ["HEAD^2", "--not", "HEAD^1"], text, said
120
+ raise ScanError("the pull request's commits are not in the checkout")
121
+ if name == "push":
122
+ ref = environ.get("GITHUB_REF", "")
123
+ before, after = object_id("SCAN_BEFORE"), object_id("SCAN_AFTER")
124
+ if not after or NO_OBJECT.fullmatch(after):
125
+ return [], ref, "the ref's name" # a deleted branch or tag adds nothing
126
+ if not has_object(root, after):
127
+ raise ScanError("the pushed commits are not in the checkout")
128
+ if before and not NO_OBJECT.fullmatch(before) and has_object(root, before):
129
+ return [after, "--not", before], ref, "the ref's name"
130
+ # A new branch or tag, or a forced push whose old tip is gone: what the default branch does not hold. Never
131
+ # --not --remotes: the checkout's remote branches hold the pushed commits already.
132
+ default = environ.get("SCAN_DEFAULT_BRANCH", "")
133
+ known = f"refs/remotes/origin/{default}"
134
+ if default and ref != f"refs/heads/{default}" and has_object(root, known):
135
+ return [after, "--not", known], ref, "the ref's name"
136
+ return [after], ref, "the ref's name"
137
+ raise ScanError(f"no rule for the {name or 'unnamed'} event")
138
+
139
+
140
+ class Scanner:
141
+ def __init__(self, words: str):
142
+ self.phrases = tuple(line.strip().casefold() for line in words.splitlines() if line.strip())
143
+ if not self.phrases:
144
+ raise ScanError("RB_RELEASE_DENYLIST is missing or empty; content was not checked")
145
+ self.overlap = max(map(len, self.phrases)) - 1
146
+ self.files = 0
147
+ self.commits = self.tags = self.blobs = 0
148
+
149
+ def text(self, value: str):
150
+ folded = value.casefold()
151
+ if any(phrase in folded for phrase in self.phrases):
152
+ raise ContentMatch("blocked content found; matching text and path omitted")
153
+
154
+ def stream(self, source, *, count=True):
155
+ # Compiled Rust strings and the source documents are UTF-8. Incremental
156
+ # decoding preserves text and phrases that cross a read boundary; binary
157
+ # bytes outside UTF-8 are ignored without skipping the rest of the file.
158
+ decoder = codecs.getincrementaldecoder("utf-8")(errors="ignore")
159
+ tail = ""
160
+ while True:
161
+ block = source.read(65536)
162
+ value = tail + decoder.decode(block, final=not block).casefold()
163
+ self.text(value)
164
+ tail = value[-self.overlap:] if self.overlap else ""
165
+ if not block:
166
+ break
167
+ if count:
168
+ self.files += 1
169
+
170
+ def source(self, root: Path):
171
+ # A subdirectory would leave the rest of the checkout unchecked.
172
+ top = os.fsdecode(git(root, "rev-parse", "--show-toplevel").strip())
173
+ if not top or not os.path.samefile(top, root):
174
+ raise ScanError("--source is not the top level of a git checkout; the files outside it were not checked")
175
+ listing = git(root, "ls-tree", "-r", "-z", "--full-tree", "HEAD")
176
+ blobs = []
177
+ for entry in filter(None, listing.split(b"\0")):
178
+ info, _, name = entry.partition(b"\t")
179
+ mode, kind, oid = info.split(b" ")
180
+ self.text(os.fsdecode(name))
181
+ # A link's target or a submodule's commit is not a file this check reads; it is reviewed by hand.
182
+ if kind != b"blob" or mode not in (b"100644", b"100755"):
183
+ raise ScanError("tracked source contains a non-regular file; review it explicitly")
184
+ blobs.append(oid)
185
+ if not blobs:
186
+ raise ScanError("could not enumerate tracked source files")
187
+ for _, _, contents in objects(root, blobs):
188
+ self.stream(contents)
189
+
190
+ def history(self, root: Path, revisions):
191
+ """Every commit message, tag message, file and file name in the history the revisions add."""
192
+ if git(root, "rev-parse", "--is-shallow-repository").strip() != b"false":
193
+ raise ScanError("the checkout is shallow, so its history was not checked; fetch all of it (fetch-depth: 0)")
194
+ ids = []
195
+ for line in git(root, "rev-list", "--objects", *revisions, "--").splitlines():
196
+ oid, _, name = line.partition(b" ")
197
+ if not OBJECT_ID.fullmatch(oid.decode("ascii", "replace")):
198
+ raise ScanError("git rev-list output could not be read")
199
+ self.text(os.fsdecode(name)) # the path a file or folder was first found at
200
+ ids.append(oid)
201
+ for oid, kind, contents in objects(root, ids):
202
+ if kind == "blob":
203
+ self.stream(contents, count=False)
204
+ self.blobs += 1
205
+ elif kind == "tree":
206
+ # '<mode> <name>\0<id>' for each entry: every name, where rev-list stops a path at a line break
207
+ tree, at = contents.read(), 0
208
+ while at < len(tree):
209
+ space = tree.index(b" ", at)
210
+ end = tree.index(b"\0", space)
211
+ self.text(os.fsdecode(tree[space + 1:end]))
212
+ at = end + 1 + len(oid) // 2
213
+ elif kind == "commit":
214
+ self.text(contents.read().partition(b"\n\n")[2].decode("utf-8", "ignore")) # its message, as %B
215
+ self.commits += 1
216
+ elif kind == "tag":
217
+ headers, _, message = contents.read().partition(b"\n\n")
218
+ named = [line for line in headers.split(b"\n") if line.startswith(b"tag ")]
219
+ self.text(b"\n".join([*named, message]).decode("utf-8", "ignore")) # its name and message
220
+ self.tags += 1
221
+
222
+ def artifacts(self, root: Path):
223
+ paths = sorted(root.iterdir())
224
+ if not paths:
225
+ raise ScanError("the artifact directory is empty; distributions were not checked")
226
+ for path in paths:
227
+ self.text(path.name)
228
+ if not stat.S_ISREG(path.lstat().st_mode):
229
+ raise ScanError("the artifact directory contains a non-regular file")
230
+ before = self.files
231
+ # Also check archive metadata, including ZIP comments, which is not
232
+ # exposed as a member. Compressed payloads are checked below.
233
+ with path.open("rb") as source:
234
+ self.stream(source, count=False)
235
+ if path.suffix == ".whl":
236
+ with zipfile.ZipFile(path) as archive:
237
+ for member in archive.infolist():
238
+ self.text(member.filename)
239
+ if member.is_dir():
240
+ continue
241
+ if stat.S_ISLNK(member.external_attr >> 16):
242
+ raise ScanError("a wheel contains a symbolic link")
243
+ with archive.open(member) as source:
244
+ self.stream(source)
245
+ elif path.name.endswith(".tar.gz"):
246
+ # Read through the gzip trailer too: tar readers may stop at the
247
+ # end-of-archive marker before verifying its CRC or truncation.
248
+ with gzip.open(path, "rb") as source:
249
+ self.stream(source, count=False)
250
+ with tarfile.open(path, "r:gz") as archive:
251
+ for member in archive:
252
+ self.text(member.name)
253
+ if member.isdir():
254
+ continue
255
+ if not member.isfile():
256
+ raise ScanError("a source distribution contains a non-regular file")
257
+ source = archive.extractfile(member)
258
+ if source is None:
259
+ raise ScanError("a source distribution member could not be read")
260
+ with source:
261
+ self.stream(source)
262
+ else:
263
+ raise ScanError("unexpected artifact; only wheels and .tar.gz source distributions are accepted")
264
+ if self.files == before:
265
+ raise ScanError("a distribution contains no files")
266
+
267
+
268
+ def main(argv=None):
269
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
270
+ parser.add_argument("--source", type=Path, required=True,
271
+ help="top level of the git checkout whose committed files are checked")
272
+ parser.add_argument("--artifacts", type=Path, help="directory containing the wheels and sdist to check")
273
+ parser.add_argument("--history", nargs="*", metavar="REVISION",
274
+ help="also check the history these revisions add (default: what the GitHub Actions event adds)")
275
+ args = parser.parse_args(argv)
276
+ try:
277
+ scanner = Scanner(os.environ.get("RB_RELEASE_DENYLIST", ""))
278
+ scanner.source(args.source)
279
+ source_count = scanner.files
280
+ if args.artifacts is not None:
281
+ scanner.artifacts(args.artifacts)
282
+ checked = f"{source_count} tracked files, {scanner.files - source_count} distribution members"
283
+ if args.history is not None:
284
+ revisions, text, said = (args.history, "", "") if args.history else event_scope(args.source, os.environ)
285
+ if revisions:
286
+ scanner.history(args.source, revisions)
287
+ scanner.text(text)
288
+ checked += (f", and in the history {scanner.commits} commit messages, {scanner.tags} tag messages and "
289
+ f"{scanner.blobs} files" + (f", with {said}" if said else ""))
290
+ print(f"Public-content check passed: {checked}.")
291
+ return 0
292
+ except ContentMatch as error:
293
+ print(f"Public-content check failed: {error}.", file=sys.stderr)
294
+ return 1
295
+ except ScanError as error:
296
+ print(f"Public-content check failed: {error}.", file=sys.stderr)
297
+ return 2
298
+ except (OSError, EOFError, ValueError, RuntimeError, tarfile.TarError, zipfile.BadZipFile, zlib.error,
299
+ *LZMA_ERRORS):
300
+ # Exception messages can contain a forbidden path or archive member.
301
+ print("Public-content check failed: an input could not be read completely; "
302
+ "no successful audit was produced.", file=sys.stderr)
303
+ return 2
304
+ except Exception:
305
+ # Anything else is a failed check, never a match (exit 1) and never its message.
306
+ print("Public-content check failed: the check stopped on an unexpected error; "
307
+ "no successful audit was produced.", file=sys.stderr)
308
+ return 2
309
+
310
+
311
+ if __name__ == "__main__":
312
+ sys.exit(main())
@@ -0,0 +1,133 @@
1
+ """Install one native wheel in a clean environment, then use only that installation.
2
+
3
+ Run with a wheel path or a directory containing exactly one wheel. No source build is
4
+ allowed, and the SDK, executables and their embedded assets are checked outside the repo.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import struct
12
+ import subprocess
13
+ import sys
14
+ import sysconfig
15
+ import tempfile
16
+ import venv
17
+
18
+
19
+ def pe_imports(path: Path) -> list[str]:
20
+ """The DLLs a Windows executable names in its import and delay-load tables, read from its PE headers."""
21
+ data = path.read_bytes()
22
+ pe = struct.unpack_from("<I", data, 0x3C)[0]
23
+ if data[:2] != b"MZ" or data[pe:pe + 4] != b"PE\0\0":
24
+ raise ValueError(f"{path} is not a Windows executable")
25
+ sections, optional_size = struct.unpack_from("<H", data, pe + 6)[0], struct.unpack_from("<H", data, pe + 20)[0]
26
+ optional = pe + 24
27
+ directories = optional + (96 if struct.unpack_from("<H", data, optional)[0] == 0x10B else 112) # PE32, else PE32+
28
+ count = struct.unpack_from("<I", data, directories - 4)[0]
29
+ # each section: its size and address in memory, then its size and offset in the file
30
+ table = [struct.unpack_from("<IIII", data, optional + optional_size + 40 * i + 8) for i in range(sections)]
31
+
32
+ def offset(rva: int) -> int:
33
+ for size, address, raw_size, raw in table:
34
+ if address <= rva < address + max(size, raw_size):
35
+ return rva - address + raw
36
+ raise ValueError(f"{path}: address {rva:#x} is in no section")
37
+
38
+ names = []
39
+ for index, width, at_name in ((1, 20, 12), (13, 32, 4)): # the import table, then the delay-load table
40
+ rva = struct.unpack_from("<I", data, directories + 8 * index)[0] if index < count else 0
41
+ at = offset(rva) if rva else None
42
+ while at is not None and any(data[at:at + width]):
43
+ start = offset(struct.unpack_from("<I", data, at + at_name)[0])
44
+ names.append(data[start:data.index(b"\0", start)].decode("ascii"))
45
+ at += width
46
+ return names
47
+
48
+
49
+ def vc_runtime(executable: Path) -> list[str]:
50
+ """The Visual C++ runtime DLLs a Windows executable imports, which a venv does not bring."""
51
+ imported = pe_imports(executable)
52
+ assert "kernel32.dll" in {dll.lower() for dll in imported}, f"{executable.name}: its import table was not read: {imported}"
53
+ return [dll for dll in imported if dll.lower().startswith("vcruntime")]
54
+
55
+
56
+ def installed_smoke() -> None:
57
+ import rabbit_brain as rb
58
+ from rabbit_brain import _rb
59
+ import rabbit_brain.tui # noqa: F401 - the terminal UI's dependencies must be installed too
60
+
61
+ prefix = Path(sys.prefix).resolve()
62
+ assert Path(rb.__file__).resolve().is_relative_to(prefix), rb.__file__
63
+ scripts = Path(sysconfig.get_path("scripts"))
64
+ suffix = ".exe" if os.name == "nt" else ""
65
+ binary = scripts / f"rb{suffix}"
66
+ assert Path(_rb.found()).resolve() == binary.resolve()
67
+
68
+ def command(executable: Path, *args: str, input: str | None = None) -> str:
69
+ return subprocess.run([str(executable), *args], input=input, text=True, encoding="utf-8",
70
+ capture_output=True, check=True, timeout=30).stdout
71
+
72
+ for name in ("rb", "rabbit-brain"):
73
+ assert rb.__version__ in command(scripts / f"{name}{suffix}", "version")
74
+ if os.name == "nt": # the executables carry their own C runtime (.cargo/config.toml)
75
+ for name in ("rb", "rabbit-brain"):
76
+ runtime = vc_runtime(scripts / f"{name}.exe")
77
+ assert not runtime, f"{name}.exe imports {', '.join(runtime)}: it would not start without the Visual C++ Redistributable"
78
+ assert "Rabbit Brain" in command(binary, "docs")
79
+ assert "name: rabbit-brain" in command(binary, "skill")
80
+
81
+ state = rb.init("Installed wheel smoke check", path=Path.cwd())
82
+ exp = state.add_experiment("Packaging check", id="package")
83
+ artifact = Path("outputs") / "result.txt"
84
+ artifact.parent.mkdir()
85
+ artifact.write_text("ready\n", encoding="utf-8")
86
+ run = exp.record({"score": 1.0}, files=[artifact], command="wheel smoke check")
87
+ assert run is not None and run.metrics == {"score": 1.0}
88
+ assert run.files[0].path == "outputs/result.txt"
89
+ assert rb.open(Path.cwd()).experiment("package").id == exp.id
90
+
91
+ messages = [
92
+ {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
93
+ "protocolVersion": "2025-06-18", "capabilities": {},
94
+ "clientInfo": {"name": "wheel-smoke", "version": "1"}}},
95
+ {"jsonrpc": "2.0", "method": "notifications/initialized"},
96
+ {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
97
+ ]
98
+ responses = [json.loads(line) for line in command(
99
+ binary, "mcp", input="".join(json.dumps(message) + "\n" for message in messages)).splitlines()]
100
+ assert [message.get("id") for message in responses] == [1, 2], responses
101
+ assert responses[0]["result"]["serverInfo"]["version"] == rb.__version__
102
+ assert "context" in {tool["name"] for tool in responses[1]["result"]["tools"]}
103
+ print(f"Installed wheel OK: rb {rb.__version__}, CLI aliases, SDK, terminal UI, docs, skill and MCP")
104
+
105
+
106
+ def main() -> None:
107
+ if sys.argv[1:] == ["--installed"]:
108
+ installed_smoke()
109
+ return
110
+ if len(sys.argv) != 2:
111
+ raise SystemExit("usage: python smoke-wheel.py <wheel-or-directory>")
112
+ supplied = Path(sys.argv[1]).resolve()
113
+ wheels = list(supplied.glob("*.whl")) if supplied.is_dir() else [supplied]
114
+ if len(wheels) != 1 or not wheels[0].is_file() or wheels[0].suffix != ".whl":
115
+ raise SystemExit("expected exactly one native wheel")
116
+ script = Path(__file__).resolve()
117
+ with tempfile.TemporaryDirectory(prefix="rb-wheel-smoke-") as directory:
118
+ root = Path(directory)
119
+ environment = root / "venv"
120
+ venv.create(environment, with_pip=True)
121
+ python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
122
+ env = {**os.environ, "DO_NOT_TRACK": "1", "PYTHONUTF8": "1"}
123
+ env.pop("RB_BIN", None) # a development override must not bypass the installed wheel
124
+ subprocess.run([str(python), "-m", "pip", "install", "--only-binary=:all:", str(wheels[0])],
125
+ cwd=root, env=env, check=True, timeout=300)
126
+ project = root / "project"
127
+ project.mkdir()
128
+ subprocess.run([str(python), "-I", str(script), "--installed"], cwd=project, env=env,
129
+ check=True, timeout=120)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()