rootsign 0.2.0__tar.gz → 0.3.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 (246) hide show
  1. {rootsign-0.2.0 → rootsign-0.3.0}/.env.example +23 -3
  2. rootsign-0.3.0/.github/dependabot.yml +31 -0
  3. {rootsign-0.2.0 → rootsign-0.3.0}/.github/workflows/ci.yml +140 -16
  4. {rootsign-0.2.0 → rootsign-0.3.0}/.github/workflows/release.yml +9 -3
  5. {rootsign-0.2.0 → rootsign-0.3.0}/CONTRIBUTING.md +32 -0
  6. {rootsign-0.2.0 → rootsign-0.3.0}/PKG-INFO +120 -7
  7. {rootsign-0.2.0 → rootsign-0.3.0}/README.md +116 -5
  8. rootsign-0.3.0/docs/adr/ADR-013-http-ingest-client-spool.md +283 -0
  9. rootsign-0.3.0/docs/adr/ADR-014-export-evidence-bundle.md +182 -0
  10. rootsign-0.3.0/docs/demo.gif +0 -0
  11. {rootsign-0.2.0 → rootsign-0.3.0}/docs/demo.tape +12 -2
  12. rootsign-0.3.0/docs/evidence-report-approval.png +0 -0
  13. rootsign-0.3.0/docs/evidence-report.png +0 -0
  14. {rootsign-0.2.0 → rootsign-0.3.0}/docs/framework-support.md +15 -1
  15. rootsign-0.3.0/docs/ingest-spec-v1.md +731 -0
  16. rootsign-0.3.0/docs/releases/v0.3.0.md +113 -0
  17. {rootsign-0.2.0 → rootsign-0.3.0}/examples/quickstart-jsonl/README.md +45 -2
  18. rootsign-0.3.0/examples/quickstart-jsonl/quickstart.py +72 -0
  19. {rootsign-0.2.0 → rootsign-0.3.0}/pyproject.toml +12 -5
  20. rootsign-0.3.0/rootsign/chain_state.py +173 -0
  21. rootsign-0.3.0/rootsign/cli.py +484 -0
  22. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/action.py +79 -44
  23. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/errors.py +104 -0
  24. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/handler.py +13 -0
  25. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/schemas.py +20 -0
  26. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/server.py +14 -7
  27. rootsign-0.3.0/rootsign/replay.py +148 -0
  28. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/__init__.py +1 -1
  29. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/buffered_client.py +103 -11
  30. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/chain.py +106 -53
  31. rootsign-0.3.0/rootsign/sdk/cli.py +645 -0
  32. rootsign-0.3.0/rootsign/sdk/client.py +850 -0
  33. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/config.py +58 -5
  34. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/decorator.py +82 -6
  35. rootsign-0.3.0/rootsign/sdk/export.py +870 -0
  36. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/jsonl_client.py +52 -40
  37. rootsign-0.3.0/rootsign/sdk/loss_ledger.py +110 -0
  38. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/registration.py +9 -6
  39. rootsign-0.3.0/rootsign/sdk/report.py +594 -0
  40. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/session.py +3 -2
  41. rootsign-0.3.0/rootsign/sdk/spool.py +282 -0
  42. rootsign-0.3.0/rootsign/verdict.py +140 -0
  43. {rootsign-0.2.0 → rootsign-0.3.0}/scripts/demo_for_gif.py +5 -1
  44. {rootsign-0.2.0 → rootsign-0.3.0}/tests/conftest.py +42 -0
  45. rootsign-0.3.0/tests/contract/cloud/test_http_transport.py +701 -0
  46. rootsign-0.3.0/tests/contract/no_extras/test_packaging_isolation.py +538 -0
  47. rootsign-0.3.0/tests/fixtures/evidence_bundle_v1.json +186 -0
  48. rootsign-0.3.0/tests/integration/test_cross_backend_hash.py +536 -0
  49. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_crud.py +78 -0
  50. rootsign-0.3.0/tests/integration/test_dead_spool.py +372 -0
  51. rootsign-0.3.0/tests/integration/test_export_cli_postgres.py +180 -0
  52. rootsign-0.3.0/tests/integration/test_export_sources.py +205 -0
  53. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_ingest.py +6 -32
  54. rootsign-0.3.0/tests/integration/test_kill_network.py +287 -0
  55. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_show_hn_quickstart.py +51 -21
  56. rootsign-0.3.0/tests/integration/test_verdict_parity.py +341 -0
  57. rootsign-0.3.0/tests/performance/_bench.py +55 -0
  58. rootsign-0.3.0/tests/performance/test_benchmarks.py +178 -0
  59. {rootsign-0.2.0 → rootsign-0.3.0}/tests/performance/test_langgraph_benchmarks.py +8 -0
  60. rootsign-0.3.0/tests/performance/test_roundtrip_benchmark.py +142 -0
  61. {rootsign-0.2.0 → rootsign-0.3.0}/tests/performance/test_verify_benchmarks.py +19 -9
  62. rootsign-0.3.0/tests/support/__init__.py +0 -0
  63. rootsign-0.3.0/tests/support/mock_ingest_store.py +206 -0
  64. rootsign-0.3.0/tests/support/session_files.py +142 -0
  65. rootsign-0.3.0/tests/unit/__init__.py +0 -0
  66. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_admin_cli.py +11 -4
  67. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_buffered_client.py +16 -3
  68. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_chain.py +113 -2
  69. rootsign-0.3.0/tests/unit/test_chain_state.py +145 -0
  70. rootsign-0.3.0/tests/unit/test_export_bundle.py +391 -0
  71. rootsign-0.3.0/tests/unit/test_export_check.py +228 -0
  72. rootsign-0.3.0/tests/unit/test_export_cli.py +251 -0
  73. rootsign-0.3.0/tests/unit/test_export_golden.py +359 -0
  74. rootsign-0.3.0/tests/unit/test_export_report.py +328 -0
  75. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hitl_jsonl.py +1 -1
  76. rootsign-0.3.0/tests/unit/test_http_ingest_client.py +894 -0
  77. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_ingest_client.py +16 -10
  78. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_jsonl_client.py +9 -6
  79. rootsign-0.3.0/tests/unit/test_log_injection.py +94 -0
  80. rootsign-0.3.0/tests/unit/test_record_loss_ladder.py +475 -0
  81. rootsign-0.3.0/tests/unit/test_replay.py +268 -0
  82. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_sdk_config.py +18 -0
  83. rootsign-0.3.0/tests/unit/test_spool_sync.py +600 -0
  84. rootsign-0.3.0/tests/unit/test_verdict.py +133 -0
  85. rootsign-0.2.0/docs/demo.gif +0 -0
  86. rootsign-0.2.0/examples/quickstart-jsonl/quickstart.py +0 -51
  87. rootsign-0.2.0/rootsign/cli.py +0 -264
  88. rootsign-0.2.0/rootsign/sdk/cli.py +0 -313
  89. rootsign-0.2.0/rootsign/sdk/client.py +0 -237
  90. rootsign-0.2.0/tests/contract/no_extras/test_packaging_isolation.py +0 -113
  91. rootsign-0.2.0/tests/integration/test_cross_backend_hash.py +0 -133
  92. rootsign-0.2.0/tests/performance/test_benchmarks.py +0 -125
  93. {rootsign-0.2.0 → rootsign-0.3.0}/.github/CLA_SIGNED.md +0 -0
  94. {rootsign-0.2.0 → rootsign-0.3.0}/.github/CODEOWNERS +0 -0
  95. {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  96. {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  97. {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/framework_integration.yml +0 -0
  98. {rootsign-0.2.0 → rootsign-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  99. {rootsign-0.2.0 → rootsign-0.3.0}/.gitignore +0 -0
  100. {rootsign-0.2.0 → rootsign-0.3.0}/.vscode/settings.json +0 -0
  101. {rootsign-0.2.0 → rootsign-0.3.0}/CLA.md +0 -0
  102. {rootsign-0.2.0 → rootsign-0.3.0}/CONTRIBUTORS.md +0 -0
  103. {rootsign-0.2.0 → rootsign-0.3.0}/LICENSE +0 -0
  104. {rootsign-0.2.0 → rootsign-0.3.0}/NOTICE +0 -0
  105. {rootsign-0.2.0 → rootsign-0.3.0}/SECURITY.md +0 -0
  106. {rootsign-0.2.0 → rootsign-0.3.0}/alembic.ini +0 -0
  107. {rootsign-0.2.0 → rootsign-0.3.0}/docker-compose.yml +0 -0
  108. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-001-hash-canonical-spec.md +0 -0
  109. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-002-transport-agnostic-client.md +0 -0
  110. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-003-framework-contract-tests.md +0 -0
  111. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-004-langgraph-interception-strategy.md +0 -0
  112. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-005-crewai-interception-strategy.md +0 -0
  113. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-006-redaction-contract.md +0 -0
  114. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-007-hitl-checkpoint-design.md +0 -0
  115. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-008-decision-capture.md +0 -0
  116. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-009-buffered-ingest-client.md +0 -0
  117. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-010-mcp-interception-strategy.md +0 -0
  118. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-011-jsonl-local-backend.md +0 -0
  119. {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-012-init-facade-contextvar-session.md +0 -0
  120. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.0.md +0 -0
  121. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.1.md +0 -0
  122. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.2.md +0 -0
  123. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.3.md +0 -0
  124. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.4.md +0 -0
  125. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.5.md +0 -0
  126. {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.2.0.md +0 -0
  127. {rootsign-0.2.0 → rootsign-0.3.0}/docs/rootsign-logo.png +0 -0
  128. {rootsign-0.2.0 → rootsign-0.3.0}/examples/README.md +0 -0
  129. {rootsign-0.2.0 → rootsign-0.3.0}/examples/docker-compose.yml +0 -0
  130. {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/.env.example +0 -0
  131. {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/README.md +0 -0
  132. {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/agent.py +0 -0
  133. {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/pyproject.toml +0 -0
  134. {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/verify.sh +0 -0
  135. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/__init__.py +0 -0
  136. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/__init__.py +0 -0
  137. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/env.py +0 -0
  138. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/script.py.mako +0 -0
  139. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0001_initial_schema.py +0 -0
  140. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0002_approval_parent_id.py +0 -0
  141. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0003_action_timed_out.py +0 -0
  142. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0004_approval_action_unique.py +0 -0
  143. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0005_agents_name_env_unique.py +0 -0
  144. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_version.py +0 -0
  145. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/config.py +0 -0
  146. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/__init__.py +0 -0
  147. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/agent.py +0 -0
  148. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/approval.py +0 -0
  149. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/base.py +0 -0
  150. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/decision.py +0 -0
  151. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/incident.py +0 -0
  152. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/policy.py +0 -0
  153. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/session.py +0 -0
  154. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/database.py +0 -0
  155. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/hashing.py +0 -0
  156. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/__init__.py +0 -0
  157. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/idempotency.py +0 -0
  158. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/__init__.py +0 -0
  159. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/proxy.py +0 -0
  160. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/__init__.py +0 -0
  161. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/action.py +0 -0
  162. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/agent.py +0 -0
  163. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/approval.py +0 -0
  164. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/decision.py +0 -0
  165. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/incident.py +0 -0
  166. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/policy.py +0 -0
  167. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/session.py +0 -0
  168. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/__init__.py +0 -0
  169. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/action.py +0 -0
  170. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/agent.py +0 -0
  171. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/approval.py +0 -0
  172. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/decision.py +0 -0
  173. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/incident.py +0 -0
  174. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/policy.py +0 -0
  175. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/session.py +0 -0
  176. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/_async_bridge.py +0 -0
  177. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/context.py +0 -0
  178. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/facade.py +0 -0
  179. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/__init__.py +0 -0
  180. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/crewai.py +0 -0
  181. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/langgraph.py +0 -0
  182. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/hashing.py +0 -0
  183. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/hitl.py +0 -0
  184. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/jsonl_registry.py +0 -0
  185. {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/redaction.py +0 -0
  186. {rootsign-0.2.0 → rootsign-0.3.0}/scripts/demo_verify_chain.py +0 -0
  187. {rootsign-0.2.0 → rootsign-0.3.0}/scripts/generate_hash_vectors.py +0 -0
  188. {rootsign-0.2.0 → rootsign-0.3.0}/scripts/init_test_db.sql +0 -0
  189. {rootsign-0.2.0 → rootsign-0.3.0}/tests/__init__.py +0 -0
  190. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/__init__.py +0 -0
  191. {rootsign-0.2.0/tests/contract/crewai → rootsign-0.3.0/tests/contract/cloud}/__init__.py +0 -0
  192. {rootsign-0.2.0/tests/integration → rootsign-0.3.0/tests/contract/crewai}/__init__.py +0 -0
  193. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/crewai/test_tool_interception.py +0 -0
  194. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/langgraph/__init__.py +0 -0
  195. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/langgraph/test_tool_interception.py +0 -0
  196. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/__init__.py +0 -0
  197. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_hitl.py +0 -0
  198. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_interception.py +0 -0
  199. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_server.py +0 -0
  200. {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/no_extras/__init__.py +0 -0
  201. {rootsign-0.2.0 → rootsign-0.3.0}/tests/fixtures/hash_vectors.json +0 -0
  202. {rootsign-0.2.0 → rootsign-0.3.0}/tests/fixtures/redaction_vectors.json +0 -0
  203. {rootsign-0.2.0/tests/performance → rootsign-0.3.0/tests/integration}/__init__.py +0 -0
  204. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_approval_concurrency.py +0 -0
  205. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_approve_cli.py +0 -0
  206. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_buffered_client.py +0 -0
  207. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_crewai_integration.py +0 -0
  208. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_decision_capture.py +0 -0
  209. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_decision_ingest.py +0 -0
  210. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_facade_implicit_tracers.py +0 -0
  211. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_facade_postgres.py +0 -0
  212. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_jsonl_buffered.py +0 -0
  213. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_langgraph_integration.py +0 -0
  214. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_mcp_proxy.py +0 -0
  215. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_mcp_server.py +0 -0
  216. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_payload_binding.py +0 -0
  217. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_register_agent.py +0 -0
  218. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_relationships.py +0 -0
  219. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_sdk_smoke.py +0 -0
  220. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_session_context_manager.py +0 -0
  221. {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_verify_cli.py +0 -0
  222. {rootsign-0.2.0/tests/unit → rootsign-0.3.0/tests/performance}/__init__.py +0 -0
  223. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_action_record_decision_id.py +0 -0
  224. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_action_sequence_counter.py +0 -0
  225. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_config_guard.py +0 -0
  226. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_context.py +0 -0
  227. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_crewai_tracer.py +0 -0
  228. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_crud_approval.py +0 -0
  229. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_action_record_json_safe.py +0 -0
  230. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_approval_record.py +0 -0
  231. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_decision_record.py +0 -0
  232. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_errors.py +0 -0
  233. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_facade.py +0 -0
  234. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hashing.py +0 -0
  235. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hitl.py +0 -0
  236. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_idempotency_bounds.py +0 -0
  237. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_input_payload_override.py +0 -0
  238. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_jsonl_registry.py +0 -0
  239. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_langgraph_tracer.py +0 -0
  240. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_redaction.py +0 -0
  241. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_redaction_pii.py +0 -0
  242. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_schemas.py +0 -0
  243. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_sdk_hashing.py +0 -0
  244. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_session_buffered_flush.py +0 -0
  245. {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_trace_hitl.py +0 -0
  246. {rootsign-0.2.0 → rootsign-0.3.0}/uv.lock +0 -0
@@ -17,11 +17,30 @@ DB_POOL_TIMEOUT=30
17
17
  # ---------------------------------------------------------------------------
18
18
 
19
19
  # Transport: 'local' calls IngestHandler in-process (Phase 1 default).
20
- # 'cloud' would POST to the hosted backend, but Phase 1 stubs that path —
21
- # HttpIngestClient raises NotImplementedError until Phase 2.
20
+ # 'cloud' POSTs batched envelopes to the hosted backend via HttpIngestClient
21
+ # (ADR-013). Needs the cloud extra: pip install 'rootsign[cloud]'
22
22
  ROOTSIGN_BACKEND=local
23
+ # NOTE: CLOUD_URL already ends in /v1 — the request path is {CLOUD_URL}/ingest.
23
24
  # ROOTSIGN_CLOUD_URL=https://ingest.getprovidex.com/v1
24
25
  # ROOTSIGN_API_KEY=your-api-key-here
26
+ # Cloud transport tuning. HTTP_MAX_RETRIES counts TOTAL attempts (one send
27
+ # plus at most two retries at the default of 3), and the transport is the only
28
+ # layer that retries — the buffering client stands down when it wraps this one.
29
+ # ROOTSIGN_HTTP_TIMEOUT_SECONDS=10
30
+ # ROOTSIGN_HTTP_MAX_RETRIES=3
31
+ # Offline spool root. Defaults to $ROOTSIGN_DATA_DIR/spool, so moving DATA_DIR
32
+ # moves the spool with it; set this only to split them (e.g. a durable volume).
33
+ # When the endpoint is unreachable, records land in <spool>/sessions/<id>.jsonl
34
+ # — ordinary session files, so `rootsign verify --local` reads them while still
35
+ # offline. Upload them later with `rootsign-admin sync`.
36
+ # ROOTSIGN_SPOOL_DIR=~/.rootsign/spool
37
+ # What to do when a record cannot be persisted at all — endpoint unreachable
38
+ # AND the spool write failing (disk full, read-only mount). 'warn' (default)
39
+ # drops telemetry with accounting: one CRITICAL, a loss tally at session close,
40
+ # and a hash-evident gap so verify reports INCOMPLETE rather than VALID.
41
+ # 'fail' raises into the caller instead — a halted agent over an incomplete
42
+ # record. HiTL/approval records fail closed either way.
43
+ # ROOTSIGN_ON_RECORD_LOSS=warn
25
44
 
26
45
  # Capture per-step Decision records (the agent's reasoning). Off by default
27
46
  # because Decision payloads are the largest and most PII-sensitive surface.
@@ -32,7 +51,8 @@ ROOTSIGN_CAPTURE_DECISIONS=false
32
51
  # The decorator drains this on the next successful call. Default: ~/.rootsign/wal
33
52
  ROOTSIGN_WAL_PATH=~/.rootsign/wal
34
53
 
35
- # Retry policy for HttpIngestClient (Phase 2 — unused in Phase 1).
54
+ # LEGACY retry knobs — read by no code path. The cloud transport honors
55
+ # ROOTSIGN_HTTP_TIMEOUT_SECONDS / ROOTSIGN_HTTP_MAX_RETRIES instead (above).
36
56
  ROOTSIGN_MAX_RETRIES=3
37
57
  ROOTSIGN_RETRY_BASE_DELAY=0.1
38
58
  ROOTSIGN_RETRY_MAX_DELAY=5.0
@@ -0,0 +1,31 @@
1
+ # Dependabot keeps the SHA-pinned GitHub Actions fresh.
2
+ #
3
+ # Why this file exists: `.github/workflows/release.yml` pins
4
+ # pypa/gh-action-pypi-publish to an immutable commit SHA rather than the
5
+ # mutable `release/v1` branch (CodeQL actions/unpinned-tag). That closes the
6
+ # supply-chain hole on the step that mints our PyPI Trusted Publishing OIDC
7
+ # token — but a hand-pinned SHA rots silently, which is its own risk. Dependabot
8
+ # bumps the pin and rewrites the trailing `# vX.Y.Z` comment for us, so the pin
9
+ # stays both immutable and current.
10
+ #
11
+ # Scope note: only `github-actions` is enabled. The Python dependency surface is
12
+ # deliberately left to security alerts rather than routine version-update PRs —
13
+ # the framework-contract CI jobs pin langgraph/crewai to specific minor ranges
14
+ # on purpose, and version-bump PRs against those would be noise.
15
+ version: 2
16
+ updates:
17
+ - package-ecosystem: github-actions
18
+ directory: /
19
+ schedule:
20
+ interval: weekly
21
+ commit-message:
22
+ # Matches the repo convention: type(scope): short description
23
+ prefix: "chore(ci)"
24
+ labels:
25
+ - dependencies
26
+ groups:
27
+ # One PR per week for routine action bumps instead of one per action.
28
+ # Security-relevant bumps still arrive on their own via alerts.
29
+ github-actions:
30
+ patterns:
31
+ - "*"
@@ -6,6 +6,13 @@ on:
6
6
  pull_request:
7
7
  branches: [main]
8
8
 
9
+ # Least privilege for every job below. CI only ever reads the repo — no job
10
+ # publishes, comments, or writes checks — so the read-only default holds for
11
+ # all of them and no job needs an override. Without this block the workflow
12
+ # inherits the repo-wide default token scopes (CodeQL actions/missing-workflow-permissions).
13
+ permissions:
14
+ contents: read
15
+
9
16
  jobs:
10
17
  unit-tests:
11
18
  name: Unit tests (Python ${{ matrix.python-version }})
@@ -38,8 +45,8 @@ jobs:
38
45
  TEST_DATABASE_URL: postgresql+asyncpg://rootsign:rootsign@localhost:5432/rootsign_test
39
46
  TEST_DATABASE_URL_SYNC: postgresql+psycopg2://rootsign:rootsign@localhost:5432/rootsign_test
40
47
  steps:
41
- - uses: actions/checkout@v5
42
- - uses: actions/setup-python@v6
48
+ - uses: actions/checkout@v7
49
+ - uses: actions/setup-python@v7
43
50
  with:
44
51
  python-version: ${{ matrix.python-version }}
45
52
  - name: Install
@@ -81,8 +88,8 @@ jobs:
81
88
  TEST_DATABASE_URL: postgresql+asyncpg://rootsign:rootsign@localhost:5432/rootsign_test
82
89
  TEST_DATABASE_URL_SYNC: postgresql+psycopg2://rootsign:rootsign@localhost:5432/rootsign_test
83
90
  steps:
84
- - uses: actions/checkout@v5
85
- - uses: actions/setup-python@v6
91
+ - uses: actions/checkout@v7
92
+ - uses: actions/setup-python@v7
86
93
  with:
87
94
  python-version: '3.12'
88
95
  # mcp extra so the DB-backed MCP proxy integration test
@@ -118,8 +125,8 @@ jobs:
118
125
  env:
119
126
  ROOTSIGN_SKIP_DB_BOOTSTRAP: '1'
120
127
  steps:
121
- - uses: actions/checkout@v5
122
- - uses: actions/setup-python@v6
128
+ - uses: actions/checkout@v7
129
+ - uses: actions/setup-python@v7
123
130
  with:
124
131
  python-version: '3.12'
125
132
  - name: Install with langgraph extra
@@ -146,8 +153,8 @@ jobs:
146
153
  env:
147
154
  ROOTSIGN_SKIP_DB_BOOTSTRAP: '1'
148
155
  steps:
149
- - uses: actions/checkout@v5
150
- - uses: actions/setup-python@v6
156
+ - uses: actions/checkout@v7
157
+ - uses: actions/setup-python@v7
151
158
  with:
152
159
  python-version: '3.12'
153
160
  - name: Install with crewai extra
@@ -170,8 +177,8 @@ jobs:
170
177
  env:
171
178
  ROOTSIGN_SKIP_DB_BOOTSTRAP: '1'
172
179
  steps:
173
- - uses: actions/checkout@v5
174
- - uses: actions/setup-python@v6
180
+ - uses: actions/checkout@v7
181
+ - uses: actions/setup-python@v7
175
182
  with:
176
183
  python-version: '3.12'
177
184
  - name: Install with mcp extra
@@ -188,20 +195,23 @@ jobs:
188
195
  name: No-extras install (DB-free core)
189
196
  runs-on: ubuntu-latest
190
197
  steps:
191
- - uses: actions/checkout@v5
192
- - uses: actions/setup-python@v6
198
+ - uses: actions/checkout@v7
199
+ - uses: actions/setup-python@v7
193
200
  with:
194
201
  python-version: '3.12'
195
202
  - name: Install core only (no extras)
196
203
  run: pip install .
197
- - name: Zero database packages resolved
204
+ - name: Zero database or cloud packages resolved
205
+ # httpx is in the `test`/`dev`/`mcp` extras, so a developer env always
206
+ # has it — this job is the only place its absence can be asserted
207
+ # (ADR-013 Decision 2).
198
208
  run: |
199
209
  python - <<'PY'
200
210
  import importlib.util as u
201
- bad = [m for m in ("sqlalchemy","asyncpg","psycopg2","greenlet","alembic")
211
+ bad = [m for m in ("sqlalchemy","asyncpg","psycopg2","greenlet","alembic","httpx")
202
212
  if u.find_spec(m) is not None]
203
- assert not bad, f"core install pulled DB packages: {bad}"
204
- print("no DB packages installed — OK")
213
+ assert not bad, f"core install pulled optional-extra packages: {bad}"
214
+ print("no DB or cloud packages installed — OK")
205
215
  PY
206
216
  - name: import rootsign + facade surface works DB-free
207
217
  run: |
@@ -231,6 +241,120 @@ jobs:
231
241
  assert "rootsign[postgres]" in str(e), e
232
242
  print("RootSignPostgresExtraRequired raised OK")
233
243
  PY
244
+ - name: cloud backend without extra raises actionable error
245
+ # The `cloud` extra's twin of the postgres check above (ADR-013 D2 /
246
+ # Sprint B T2.1). HttpIngestClient must stay importable without httpx
247
+ # and fail with the install command when it is actually selected.
248
+ env:
249
+ ROOTSIGN_BACKEND: cloud
250
+ run: |
251
+ python - <<'PY'
252
+ from rootsign.errors import RootSignCloudExtraRequired
253
+ from rootsign.sdk.client import HttpIngestClient, get_ingest_client
254
+ assert HttpIngestClient.owns_retry is True # importable without the extra
255
+ try:
256
+ get_ingest_client()
257
+ raise SystemExit("expected RootSignCloudExtraRequired")
258
+ except RootSignCloudExtraRequired as e:
259
+ assert "rootsign[cloud]" in str(e), e
260
+ print("RootSignCloudExtraRequired raised OK")
261
+ PY
262
+ - name: Console scripts run DB-free (CLI tripwire)
263
+ # `import rootsign` passing is NOT sufficient. The console script entry
264
+ # point imports `rootsign.sdk.cli`, a different import graph that the
265
+ # checks above never touch — a module-level `from rootsign.database
266
+ # import AsyncSessionLocal` there crashed EVERY `rootsign ...`
267
+ # invocation on a bare install (even `version` and `verify --local`,
268
+ # neither of which needs a DB) while this job stayed green. This step
269
+ # exercises the script itself so that class of regression fails here.
270
+ run: |
271
+ set -euo pipefail
272
+ rootsign version
273
+ rootsign --help > /dev/null
274
+ # The operator CLI is a separate entry point (rootsign.cli:app) with
275
+ # its own import graph. --help and the docker-only subcommands must
276
+ # work without the postgres extra.
277
+ rootsign-admin --help > /dev/null
278
+ rootsign-admin start-db --help > /dev/null
279
+ rootsign-admin stop-db --help > /dev/null
280
+ # `sync` is the cloud-extra command (ADR-013 D4 / T2.5). Its --help
281
+ # and its --dry-run must work on a bare install: reading the spool
282
+ # needs no extra, and --dry-run is how an operator finds out what is
283
+ # waiting before deciding whether to install anything.
284
+ rootsign-admin sync --help > /dev/null
285
+ rootsign-admin sync --dry-run > /dev/null
286
+ # Store-exported fixture, not a checked-in file: let the JSONL backend
287
+ # write a real session so the chain is produced by the same code path
288
+ # that verifies it. Never hand-construct a self_hash (ADR-001).
289
+ SESSION_ID="$(python - <<'PY'
290
+ import asyncio, rootsign
291
+
292
+ rootsign.init(agent="ci-tripwire")
293
+
294
+ @rootsign.trace()
295
+ async def add(a: int, b: int) -> int:
296
+ return a + b
297
+
298
+ async def main():
299
+ async with rootsign.session() as ctx:
300
+ await add(2, 3)
301
+ return ctx.session_id
302
+
303
+ print(asyncio.run(main()))
304
+ PY
305
+ )"
306
+ rootsign verify --local "$HOME/.rootsign/sessions/$SESSION_ID.jsonl"
307
+ # Evidence export is a bare-install path too (ADR-014 / T3.5): it is
308
+ # how a cloud-mode user turns a spooled session into something they
309
+ # can hand to a compliance officer, and it must not need a database.
310
+ rootsign export --local "$HOME/.rootsign/sessions/$SESSION_ID.jsonl" \
311
+ --out "$HOME/bundles" | tee /tmp/export.out
312
+ grep -q "manifest.json SHA-256" /tmp/export.out || {
313
+ echo "export did not print the manifest anchor"; exit 1; }
314
+ rootsign export --check "$HOME/bundles/evidence-$SESSION_ID" | grep -q "INTACT" || {
315
+ echo "a freshly written bundle failed its own --check"; exit 1; }
316
+ # Verifying a *spooled* copy of that session must print the replay
317
+ # breadcrumb — `sync` lives on the operator CLI, so the developer CLI
318
+ # is where it has to be discoverable (ADR-013 Decision 4).
319
+ mkdir -p "$HOME/.rootsign/spool/sessions"
320
+ cp "$HOME/.rootsign/sessions/$SESSION_ID.jsonl" \
321
+ "$HOME/.rootsign/spool/sessions/$SESSION_ID.jsonl"
322
+ rootsign verify --local "$HOME/.rootsign/spool/sessions/$SESSION_ID.jsonl" \
323
+ | grep -q "rootsign-admin sync" || {
324
+ echo "verify --local on a spool file lost the sync breadcrumb"; exit 1; }
325
+ # And syncing it on a bare install must name the cloud extra rather
326
+ # than dying inside httpx's import graph.
327
+ out="$(ROOTSIGN_API_KEY=sk-ci rootsign-admin sync 2>&1)" && {
328
+ echo "expected sync to fail without the cloud extra"; exit 1; }
329
+ echo "$out" | grep -q "rootsign\[cloud\]" || {
330
+ echo "missing cloud install hint:"; echo "$out"; exit 1; }
331
+ # `if`, not `&& { }`: this block runs under `set -e`, where a
332
+ # grep that finds nothing would end the whole step with status 1.
333
+ if echo "$out" | grep -qi "Traceback"; then
334
+ echo "leaked traceback from sync"; echo "$out"; exit 1
335
+ fi
336
+ rm -rf "$HOME/.rootsign/spool"
337
+ echo "console scripts OK, DB-free"
338
+ - name: DB-backed subcommands fail with the install hint, not a traceback
339
+ # The other half of the same bug: these commands legitimately need the
340
+ # extra, but must say so in one line and exit 1 rather than dumping a
341
+ # ModuleNotFoundError traceback at the user.
342
+ run: |
343
+ set -uo pipefail
344
+ # Every DB-backed subcommand across BOTH console scripts.
345
+ for cmd in "rootsign verify 550e8400-e29b-41d4-a716-446655440001" \
346
+ "rootsign export 550e8400-e29b-41d4-a716-446655440001" \
347
+ "rootsign approve --list" \
348
+ "rootsign approve 550e8400-e29b-41d4-a716-446655440001" \
349
+ "rootsign-admin status" \
350
+ "rootsign-admin init"; do
351
+ out="$($cmd 2>&1)" && { echo "expected failure: $cmd"; exit 1; }
352
+ echo "$out" | grep -q "rootsign\[postgres\]" || {
353
+ echo "missing install hint for: $cmd"; echo "$out"; exit 1; }
354
+ echo "$out" | grep -qi "Traceback" && {
355
+ echo "leaked traceback for: $cmd"; echo "$out"; exit 1; }
356
+ echo "$cmd -> actionable error OK"
357
+ done
234
358
  # NOTE: we deliberately do NOT run `pytest tests/contract/no_extras/`
235
359
  # here — the root tests/conftest.py imports sqlalchemy at module level
236
360
  # (for the DB fixtures), so pytest cannot even collect in a DB-free env.
@@ -22,8 +22,8 @@ jobs:
22
22
  permissions:
23
23
  id-token: write # required for Trusted Publishing (OIDC)
24
24
  steps:
25
- - uses: actions/checkout@v5
26
- - uses: actions/setup-python@v6
25
+ - uses: actions/checkout@v7
26
+ - uses: actions/setup-python@v7
27
27
  with:
28
28
  python-version: '3.12'
29
29
 
@@ -48,4 +48,10 @@ jobs:
48
48
  twine check dist/*
49
49
 
50
50
  - name: Publish to PyPI
51
- uses: pypa/gh-action-pypi-publish@release/v1
51
+ # Pinned to an immutable commit SHA, not the mutable `release/v1`
52
+ # branch. This step runs with `id-token: write` and mints the OIDC
53
+ # token that PyPI Trusted Publishing exchanges for upload rights to
54
+ # the `rootsign` project — anyone able to move that branch could
55
+ # publish a release under our name. SHA below == tag v1.14.2.
56
+ # Bump deliberately; verify the new SHA against the release tag.
57
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -20,6 +20,7 @@ docker-compose up -d db # PostgreSQL + TimescaleDB
20
20
  rootsign-admin init # alembic upgrade head
21
21
  python -m pytest tests/unit/ -v # unit tests (no DB needed at runtime — see note)
22
22
  python -m pytest tests/integration/ -v # integration tests (needs DB)
23
+ python -m pytest -m benchmark -v # performance budgets (opt-in — see note)
23
24
  ```
24
25
 
25
26
  > **Python:** RootSign requires Python 3.11 or 3.12. We do not support 3.10 or below.
@@ -29,6 +30,12 @@ python -m pytest tests/integration/ -v # integration tests (needs DB)
29
30
  > **Always invoke pytest as `python -m pytest`.** If you `brew install`-ed pytest, the system binary will resolve ahead of the venv's pytest on PATH and run under the system Python — which does not see your venv's site-packages and will fail with confusing `ModuleNotFoundError` (typically on `sqlalchemy` first). `python -m pytest` always uses the venv's interpreter.
30
31
  >
31
32
  > **Note on unit tests:** The session-scoped `_bootstrap_test_db` fixture in `tests/conftest.py` runs alembic against the test DB before any test (including unit tests). Bring `docker-compose up -d db` up first.
33
+ >
34
+ > **Performance benchmarks are opt-in.** Everything under `tests/performance/` is marked `benchmark` and is **skipped** by a default `python -m pytest` run; pass `-m benchmark` to execute it. These tests assert wall-clock budgets, so they are the most hardware-sensitive part of the suite — `test_1000_actions_under_2_seconds` has only ~1.36x headroom on a developer laptop. They are deliberately kept out of every CI gating job: a timing budget that reddens `main` for reasons unrelated to the diff teaches people to ignore red. Run them locally (or on purpose in CI) when touching the ingest, hashing, or verify paths.
35
+ >
36
+ > **The thresholds are ACs and do not move.** To keep them honest without making them flaky, each budget is sampled several times and asserted on the **median** (`tests/performance/_bench.py`). A single stalled run barely shifts a median, while a real regression shifts every sample — so the AC stays exactly as specified and the measurement stops reacting to background load, cold caches, or a noisy neighbour. Failures print every sample, not just the median, so you can see the spread. `test_p99_overhead_under_5ms` is the exception: it already asserts on the p99 of 1,000 calls, so it is a distribution by construction.
37
+ >
38
+ > These numbers are still **hardware- and environment-dependent** — they measure your disk, your Postgres tuning, and whatever else the machine is doing. Treat a failure as a prompt to investigate on an idle machine, not as proof of a regression. For scale: a trial run of `test_1000_actions_under_2_seconds` produced samples of 1.6s / 2.4s / 1.4s on an idle laptop; the 2.4s sample alone exceeds the 2.0s budget, which is exactly why the median exists.
32
39
 
33
40
  ## Branch naming
34
41
 
@@ -62,12 +69,37 @@ docs(adr): add ADR-004 for retry strategy
62
69
  - Framework integrations must pass contract tests on all supported framework versions (see CI matrix)
63
70
  - Run `ruff check .` and `ruff format .` before pushing
64
71
 
72
+ ### Golden fixtures
73
+
74
+ Three fixtures in `tests/fixtures/` freeze contracts that other people's code
75
+ depends on. A failure against one is a decision, not a bug:
76
+
77
+ | Fixture | Freezes | If it fails |
78
+ | --- | --- | --- |
79
+ | `hash_vectors.json` | the canonical Action hash (ADR-001) | stop — a new ADR is required, see below |
80
+ | `redaction_vectors.json` | the PII rule-set mappings (ADR-006) | confirm the mapping change is intended |
81
+ | `evidence_bundle_v1.json` | the `rootsign export` bundle schema (ADR-014) | see below |
82
+
83
+ The bundle schema is read by Phase 2 tooling and by anything a partner builds
84
+ on an exported bundle. **Additive** changes (a new optional field, content in
85
+ the reserved `compliance` block) are fine — regenerate and commit the diff:
86
+
87
+ ```bash
88
+ ROOTSIGN_UPDATE_GOLDEN=1 python -m pytest tests/unit/test_export_golden.py
89
+ ```
90
+
91
+ Renaming or removing a field is a **bundle version bump**: change
92
+ `EVIDENCE_BUNDLE_VERSION` in `rootsign/sdk/export.py` in the same PR, and say
93
+ why in the description. Never regenerate a fixture to make a red test green
94
+ without first deciding which of the two you are doing.
95
+
65
96
  ## What we will NOT merge
66
97
 
67
98
  - Changes to `compute_action_self_hash` canonical spec (see [ADR-001](docs/adr/ADR-001-hash-canonical-spec.md)) without a new ADR approved by the maintainer
68
99
  - Synchronous SQLAlchemy in any non-Alembic code
69
100
  - Mock-based integration tests (real PostgreSQL + TimescaleDB only — see [the data model rationale](AGENTS.md))
70
101
  - Any PR that reduces test coverage below 85% overall
102
+ - A regenerated `evidence_bundle_v1.json` that renames or removes a field without a matching `EVIDENCE_BUNDLE_VERSION` bump (see [ADR-014](docs/adr/ADR-014-export-evidence-bundle.md))
71
103
  - Code that swallows ingest failures silently (RootSign's promise is that ingest never raises into the agent, but failures *must* be logged at WARNING level — see [ADR-002](docs/adr/ADR-002-transport-agnostic-client.md))
72
104
 
73
105
  ## Adding a new framework integration
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: rootsign
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: RootSign — tamper-evident provenance logging for AI agents (powered by Providex AI)
5
5
  Author: Providex AI Inc.
6
6
  License: Apache-2.0
@@ -20,6 +20,8 @@ Requires-Dist: python-dotenv<2.0,>=1.0
20
20
  Requires-Dist: typer<1.0,>=0.12
21
21
  Provides-Extra: autogen
22
22
  Requires-Dist: pyautogen>=0.2; extra == 'autogen'
23
+ Provides-Extra: cloud
24
+ Requires-Dist: httpx<1.0,>=0.27; extra == 'cloud'
23
25
  Provides-Extra: crewai
24
26
  Requires-Dist: crewai>=0.28; extra == 'crewai'
25
27
  Provides-Extra: dev
@@ -89,13 +91,14 @@ Compliance-grade audit trails. Zero changes to your agent code.
89
91
 
90
92
  ## Status
91
93
 
92
- **v0.2.0.** `pip install rootsign` → a verified hash chain in under a minute: no Docker, no database, no plumbing. LangGraph + CrewAI integrations, a framework-agnostic MCP proxy, `rootsign verify` CLI, PII redaction, human-in-the-loop checkpoints, opt-in decision capture (PRD-19 / ADR-008), and opt-in SDK micro-batching are all shipping.
94
+ **v0.3.0.** `pip install rootsign` → a verified hash chain in under a minute: no Docker, no database, no plumbing. LangGraph + CrewAI integrations, a framework-agnostic MCP proxy, `rootsign verify` CLI, PII redaction, human-in-the-loop checkpoints, opt-in decision capture (PRD-19 / ADR-008), and opt-in SDK micro-batching are all shipping — plus evidence bundles (`rootsign export`), the cloud transport with its offline spool, and a published wire spec.
93
95
 
94
96
  | Phase | Scope | Status |
95
97
  |---|---|---|
96
98
  | 0 | Data model + storage + ingest handler | ✅ Complete |
97
99
  | 1 | Python SDK — `@rootsign.trace`, LangGraph + CrewAI + MCP proxy, `rootsign verify` CLI, redaction, HiTL checkpoint, decision capture, micro-batching | ✅ v0.1.5 |
98
100
  | 1.5 | Zero-dependency onboarding — JSONL default backend, `rootsign.init()` facade | ✅ v0.2.0 |
101
+ | 1.6 | Evidence bundles (`rootsign export`), cloud transport + offline spool, published ingest spec | ✅ v0.3.0 |
99
102
  | 2 | Hosted ingest backend + compliance dashboard | Planned |
100
103
  | 3 | Policy enforcement + incident workflow | Planned |
101
104
  | 4 | Cross-platform governance | Planned |
@@ -138,7 +141,7 @@ VALID ✓ — 2 records, chain intact
138
141
  Session: f758a636-7bcd-4f96-8940-eff7d80e760a
139
142
  ```
140
143
 
141
- Exit code is `0` for VALID and `1` for TAMPERED, so this drops into CI or a cron audit. Change any character in any record and the verifier names the broken link:
144
+ Exit code is `0` for VALID, `1` for TAMPERED (a record was altered) and `2` for INCOMPLETE (a record is missing) — so this drops into CI or a cron audit, and a script can tell theft from loss. Change any character in any record and the verifier names the broken link:
142
145
 
143
146
  ```bash
144
147
  $ rootsign verify --local ~/.rootsign/sessions/f758a636-7bcd-4f96-8940-eff7d80e760a.jsonl
@@ -148,7 +151,27 @@ TAMPERED ✗ — chain broken at record #1
148
151
  WARNING: This session log may have been tampered with.
149
152
  ```
150
153
 
151
- That's the whole surface: **`init()` → `session()` → `@trace` / `wrap_tools()` → `verify`.** `init()` is synchronous and does no I/O, so it's safe at module scope and inside a running event loop (notebooks, FastAPI startup); the agent record is get-or-created on the first `session()` entry, keyed on `(name, environment)`. Re-running your script never re-registers.
154
+ When someone outside engineering needs to see it, turn the same session into a self-contained evidence bundle:
155
+
156
+ ```bash
157
+ $ rootsign export --local ~/.rootsign/sessions/f758a636-7bcd-4f96-8940-eff7d80e760a.jsonl
158
+ VALID — 2 records, chain intact
159
+ Bundle: ./evidence-f758a636-7bcd-4f96-8940-eff7d80e760a
160
+ redaction.json
161
+ report.html
162
+ report.md
163
+ timeline.json
164
+ verification.json
165
+ manifest.json
166
+
167
+ manifest.json SHA-256: a1d61338d0f793ea03ac472863fadea5660e1e94846913ce7613d8c117d67cc5
168
+ Record that hash outside the bundle — a ticket, an email, a chain-of-custody log.
169
+ It is what proves a bundle you receive later is the one that was generated.
170
+ ```
171
+
172
+ Open `report.html` in any browser — [see below](#evidence-bundles).
173
+
174
+ That's the whole surface: **`init()` → `session()` → `@trace` / `wrap_tools()` → `verify` → `export`.** `init()` is synchronous and does no I/O, so it's safe at module scope and inside a running event loop (notebooks, FastAPI startup); the agent record is get-or-created on the first `session()` entry, keyed on `(name, environment)`. Re-running your script never re-registers.
152
175
 
153
176
  A runnable version of the above is [`examples/quickstart-jsonl`](examples/quickstart-jsonl/).
154
177
 
@@ -384,6 +407,94 @@ tools = rootsign.wrap_tools([send_invoice], redaction_config=redaction)
384
407
 
385
408
  `FinancialPIIConfig` adds account / routing / IBAN patterns; `HealthcarePIIConfig` adds MRN / NPI / DOB. Each accepts `extra_rules={...}` for domain-specific patterns without subclassing. See [ADR-006](docs/adr/ADR-006-redaction-contract.md).
386
409
 
410
+ ## Evidence bundles
411
+
412
+ A verified hash chain answers a developer's question. A compliance officer asks a different one — *what did this agent do, who approved it, and can I trust this file?* — and cannot read JSONL to find out.
413
+
414
+ `rootsign export` turns one session into a directory that answers it:
415
+
416
+ ```bash
417
+ $ rootsign export --local ~/.rootsign/sessions/<session_id>.jsonl --out ./bundles
418
+ ```
419
+
420
+ <p align="center">
421
+ <img src="https://raw.githubusercontent.com/Providex-AI/rootsign/main/docs/evidence-report.png" alt="RootSign evidence report — a VALID verdict banner, the session and agent identity block, the per-record chain table with self_hash and prev_action_hash, and the start of the session narrative" width="700" />
422
+ </p>
423
+
424
+ <p align="center"><em><code>report.html</code> — the verdict first, then the chain, then what happened.</em></p>
425
+
426
+ <p align="center">
427
+ <img src="https://raw.githubusercontent.com/Providex-AI/rootsign/main/docs/evidence-report-approval.png" alt="Further down the same report: the wire_transfer action recorded as human_rejected with no output hash, directly above the approval that rejected it — approver, reason, latency, and the context they were shown" width="700" />
428
+ </p>
429
+
430
+ <p align="center"><em>Further down: a £250,000 transfer the agent proposed, the human who refused it, and the context they were shown.</em></p>
431
+
432
+ | File | What it holds |
433
+ |---|---|
434
+ | `manifest.json` | bundle version, generator, agent identity, source backend, a SHA-256 for every other file, and a reserved `compliance` block |
435
+ | `verification.json` | the verdict plus a per-record listing — sequence, `action_id`, `self_hash`, `prev_action_hash`, and whether that record was verified |
436
+ | `timeline.json` | the session narrative in order: open → decisions → actions → approvals → close, with every field each record carries |
437
+ | `redaction.json` | which field paths held the `[REDACTED]` sentinel — the "PII was never stored" evidence |
438
+ | `report.md` / `report.html` | human renderings of exactly the above; the verdict is the first thing on the page |
439
+
440
+ Three properties make the bundle worth handing over:
441
+
442
+ **It is rendered from the JSON, never assembled twice.** Markdown and HTML are pure functions of the three JSON documents, so a fact that is not in the machine-readable evidence cannot appear in the human-readable report. The HTML has no JavaScript and no external requests — it opens identically from a file share, an email attachment, or an air-gapped review machine.
443
+
444
+ **It says what it does not know.** A session that stored only hashes says *"payload previews not retained for this session"* rather than showing empty fields. Records after a chain break are marked `unverified`, not `verified` — the verifier stopped there and proved nothing about them. `redaction.json` reports the paths that were redacted and explicitly does not claim which rule fired, because RootSign does not record that.
445
+
446
+ **It is self-verifying, and honest about the limit.** `manifest.json` carries a SHA-256 for every file, and the manifest's own hash is printed at export. Record that hash somewhere outside the bundle:
447
+
448
+ ```bash
449
+ $ rootsign export --check ./bundles/evidence-<session_id>
450
+ ✓ INTACT — 5 file(s) match manifest.json
451
+
452
+ manifest.json SHA-256: a1d61338d0f793ea03ac472863fadea5660e1e94846913ce7613d8c117d67cc5
453
+ Compare this against the hash recorded when the bundle was exported.
454
+ The file checks above cannot detect an edit that also updated the manifest.
455
+ ```
456
+
457
+ Re-hashing files against the manifest proves the bundle is internally consistent — nothing more. Someone who edits a file *and* updates the manifest passes that check trivially. The out-of-band manifest hash is the only anchor they cannot forge, which is why `--check` prints it whether or not anything is wrong.
458
+
459
+ For bundles leaving the building, `--redact-previews` withholds every field that carries stored content — payload previews, the context a human was shown at a checkpoint, and captured decision summaries — and names what it withheld, so the recipient knows what to ask for rather than guessing whether a field was stripped or never existed. Hashes, identities and timings are unaffected: the chain still verifies.
460
+
461
+ `--format json|md|html` narrows the renderings; it never drops the machine truth. `rootsign export <session_id>` reads a Postgres-backed session (agent name, owner and risk tier come along, since those live in the registration row). See [ADR-014](docs/adr/ADR-014-export-evidence-bundle.md).
462
+
463
+ ## Offline & sync (cloud mode)
464
+
465
+ Set `ROOTSIGN_BACKEND=cloud` and records go to the hosted ingest endpoint (`pip install 'rootsign[cloud]'`). The interesting part is what happens when that endpoint is not there.
466
+
467
+ **Nothing is lost, and the agent never notices.** When retries are exhausted, the SDK fails over to the same append-only writer the local backend uses, rooted at `$ROOTSIGN_DATA_DIR/spool/`. One WARNING is logged — not one per record — and your tool calls keep returning their real results, because an audit layer that halts the business it observes has inverted its own value.
468
+
469
+ **Spooled records are ordinary session files**, so the evidence is usable before it is uploaded:
470
+
471
+ ```bash
472
+ $ rootsign verify --local ~/.rootsign/spool/sessions/<session_id>.jsonl
473
+ VALID ✓ — 2 records, chain intact
474
+ Session: 2d333ca5-7b28-4cd3-80b4-8f67350d19ba
475
+
476
+ This is a spooled session — recorded locally because the cloud endpoint was unreachable.
477
+ Upload it (and everything else waiting) with:
478
+
479
+ rootsign-admin sync
480
+ ```
481
+
482
+ > A file that spooled **mid-session** verifies as `INCOMPLETE`, not `VALID` — the records from before the outage went over the wire and are not in it. That is the honest answer: the verifier is reading one file, and "this file is the whole session" is not a claim anyone can make offline. Sync it and the store's copy verifies `VALID`.
483
+
484
+ **When connectivity returns, upload:**
485
+
486
+ ```bash
487
+ $ rootsign-admin sync
488
+ 1 spooled session(s), 4 record(s) under ~/.rootsign/spool
489
+ Uploading to https://ingest.getprovidex.com/v1/ingest
490
+ synced 2d333ca5-7b28-4cd3-80b4-8f67350d19ba 4 accepted, 0 already present -> synced/
491
+ All spooled sessions uploaded.
492
+ ```
493
+
494
+ Safe to re-run: idempotency is server-side by `event_id`, so a partially uploaded session resumes and records the store already has count as delivered. Fully-uploaded files move to `spool/synced/`; a partial one stays put and reports the first rejected sequence. `--dry-run` lists what is waiting without contacting anything, and works on a bare install.
495
+
496
+ **The chain spans the outage.** Records written to the spool continue the sequence the wire path started, because the chain is sealed before the SDK chooses a destination — so after sync the store holds one unbroken session, not two fragments. If the disk *also* fails, telemetry drops with accounting (one CRITICAL, a loss ledger, and a chain gap that makes the loss provable as `INCOMPLETE`) while anything gated by a human approval fails closed and the tool never runs. See [ADR-013](docs/adr/ADR-013-http-ingest-client-spool.md).
497
+
387
498
  ## Micro-batching (opt-in)
388
499
 
389
500
  `BufferedIngestClient` wraps any ingest client and buffers `ACTION_RECORD`s in memory, flushing asynchronously — so a long, tool-heavy pipeline doesn't pay a per-call ingest round-trip. Enable it with `ROOTSIGN_BUFFERED=true` (the factory wraps the transport for you), or wrap explicitly:
@@ -426,14 +537,16 @@ The `-m benchmark` marker keeps the performance suite opt-in. Run it **without**
426
537
  * **`@rootsign.trace`** wraps a tool callable and emits an `ACTION_RECORD` envelope per call. LangGraph `BaseTool` and CrewAI tools are detected automatically.
427
538
  * **MCP proxy** — `create_proxy_app` intercepts MCP `tools/call` at the protocol layer, so any MCP-compatible agent is instrumented without a framework adapter (ADR-010).
428
539
  * **`rootsign.init()`** stores config with no I/O; `rootsign.session()` resolves the backend lazily on first entry and publishes `(ctx, client)` in a `ContextVar`, which is how `wrap_tools` / `@trace` / the MCP proxy find them without arguments (ADR-012).
429
- * **`JsonlIngestClient`** is the default transport: append-only JSONL under `~/.rootsign`, no dependencies (ADR-011). **`LocalIngestClient`** is the Postgres in-process path; a `HttpIngestClient` for the hosted backend lands in Phase 2. **`BufferedIngestClient`** optionally wraps any of them for async micro-batching (ADR-009).
540
+ * **`JsonlIngestClient`** is the default transport: append-only JSONL under `~/.rootsign`, no dependencies (ADR-011). **`LocalIngestClient`** is the Postgres in-process path. **`HttpIngestClient`** is the hosted-backend transport behind the optional `cloud` extra — it seals the chain client-side, owns its retry budget, and fails over to an on-disk spool rather than losing records (ADR-013). **`BufferedIngestClient`** optionally wraps any of them for async micro-batching (ADR-009).
430
541
  * **Hash chain** is per-session: each `Action` carries `prev_action_hash` so reconstructing the chain detects any after-the-fact modification.
542
+ * **The wire format is published**, not internal: [docs/ingest-spec-v1.md](docs/ingest-spec-v1.md) specifies the envelope, the five event types, the full error-code registry, idempotency by `event_id`, batch semantics, and the client-side hashing contract for cloud mode — the server recomputes every hash and never trusts one. It is the contract the Phase 2 backend, the TypeScript SDK, and any community connector implement, and the document a design partner's security reviewer asks for.
431
543
  * **`HiTLCheckpoint`** is an async poll loop that opens its own DB session per cycle — see ADR-007 for the loop-binding rationale.
544
+ * **`rootsign export`** builds the evidence bundle: JSON documents first, Markdown and HTML rendered from those and nothing else, with a SHA-256 per file in `manifest.json` (ADR-014). `rootsign-admin sync` is its counterpart on the operator CLI — batch replay of anything the spool caught while the endpoint was down.
432
545
  * **Storage** is either the JSONL writer (default, zero-dependency) or PostgreSQL 16 + TimescaleDB 2.14, where the `actions` table is a hypertable and the chain stays intact across chunks. Both use the same frozen canonical hash formula (ADR-001) — `rootsign verify` gives the same verdict either way.
433
546
 
434
547
  ## What's next
435
548
 
436
- * **Phase 2 cloud backend** — `HttpIngestClient` + hosted compliance dashboard. Drop-in replacement for `LocalIngestClient`; `BufferedIngestClient` already removes the per-call round-trip latency it would otherwise add.
549
+ * **Phase 2 hosted backend** — the ingest service and compliance dashboard the client already speaks to. `HttpIngestClient` ships now against the published [wire spec](docs/ingest-spec-v1.md), so the SDK half is done and tested against a mock server; cloud-sourced `rootsign export` waits on the server read API.
437
550
  * **Web UI for HiTL** — approve/reject pending actions from a browser instead of the CLI.
438
551
  * **AutoGen integration** — same duck-typing shape as CrewAI.
439
552