rootsign 0.2.1__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 (242) hide show
  1. {rootsign-0.2.1 → rootsign-0.3.0}/.env.example +23 -3
  2. {rootsign-0.2.1 → rootsign-0.3.0}/.github/workflows/ci.yml +62 -4
  3. {rootsign-0.2.1 → rootsign-0.3.0}/CONTRIBUTING.md +25 -0
  4. {rootsign-0.2.1 → rootsign-0.3.0}/PKG-INFO +119 -6
  5. {rootsign-0.2.1 → rootsign-0.3.0}/README.md +116 -5
  6. rootsign-0.3.0/docs/adr/ADR-013-http-ingest-client-spool.md +283 -0
  7. rootsign-0.3.0/docs/adr/ADR-014-export-evidence-bundle.md +182 -0
  8. rootsign-0.3.0/docs/demo.gif +0 -0
  9. {rootsign-0.2.1 → rootsign-0.3.0}/docs/demo.tape +12 -2
  10. rootsign-0.3.0/docs/evidence-report-approval.png +0 -0
  11. rootsign-0.3.0/docs/evidence-report.png +0 -0
  12. {rootsign-0.2.1 → rootsign-0.3.0}/docs/framework-support.md +15 -1
  13. rootsign-0.3.0/docs/ingest-spec-v1.md +731 -0
  14. rootsign-0.3.0/docs/releases/v0.3.0.md +113 -0
  15. {rootsign-0.2.1 → rootsign-0.3.0}/examples/quickstart-jsonl/README.md +45 -2
  16. rootsign-0.3.0/examples/quickstart-jsonl/quickstart.py +72 -0
  17. {rootsign-0.2.1 → rootsign-0.3.0}/pyproject.toml +12 -5
  18. rootsign-0.3.0/rootsign/chain_state.py +173 -0
  19. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/cli.py +190 -1
  20. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/action.py +79 -44
  21. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/errors.py +73 -0
  22. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/ingest/handler.py +13 -0
  23. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/ingest/schemas.py +20 -0
  24. rootsign-0.3.0/rootsign/replay.py +148 -0
  25. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/__init__.py +1 -1
  26. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/buffered_client.py +103 -11
  27. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/chain.py +102 -52
  28. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/cli.py +289 -4
  29. rootsign-0.3.0/rootsign/sdk/client.py +850 -0
  30. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/config.py +58 -5
  31. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/decorator.py +29 -3
  32. rootsign-0.3.0/rootsign/sdk/export.py +870 -0
  33. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/jsonl_client.py +52 -40
  34. rootsign-0.3.0/rootsign/sdk/loss_ledger.py +110 -0
  35. rootsign-0.3.0/rootsign/sdk/report.py +594 -0
  36. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/session.py +3 -2
  37. rootsign-0.3.0/rootsign/sdk/spool.py +282 -0
  38. rootsign-0.3.0/rootsign/verdict.py +140 -0
  39. {rootsign-0.2.1 → rootsign-0.3.0}/scripts/demo_for_gif.py +5 -1
  40. {rootsign-0.2.1 → rootsign-0.3.0}/tests/conftest.py +18 -0
  41. rootsign-0.3.0/tests/contract/cloud/test_http_transport.py +701 -0
  42. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/no_extras/test_packaging_isolation.py +271 -6
  43. rootsign-0.3.0/tests/fixtures/evidence_bundle_v1.json +186 -0
  44. rootsign-0.3.0/tests/integration/test_cross_backend_hash.py +536 -0
  45. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_crud.py +78 -0
  46. rootsign-0.3.0/tests/integration/test_dead_spool.py +372 -0
  47. rootsign-0.3.0/tests/integration/test_export_cli_postgres.py +180 -0
  48. rootsign-0.3.0/tests/integration/test_export_sources.py +205 -0
  49. rootsign-0.3.0/tests/integration/test_kill_network.py +287 -0
  50. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_show_hn_quickstart.py +51 -21
  51. rootsign-0.3.0/tests/integration/test_verdict_parity.py +341 -0
  52. rootsign-0.3.0/tests/support/__init__.py +0 -0
  53. rootsign-0.3.0/tests/support/mock_ingest_store.py +206 -0
  54. rootsign-0.3.0/tests/support/session_files.py +142 -0
  55. rootsign-0.3.0/tests/unit/__init__.py +0 -0
  56. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_buffered_client.py +16 -3
  57. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_chain.py +113 -2
  58. rootsign-0.3.0/tests/unit/test_chain_state.py +145 -0
  59. rootsign-0.3.0/tests/unit/test_export_bundle.py +391 -0
  60. rootsign-0.3.0/tests/unit/test_export_check.py +228 -0
  61. rootsign-0.3.0/tests/unit/test_export_cli.py +251 -0
  62. rootsign-0.3.0/tests/unit/test_export_golden.py +359 -0
  63. rootsign-0.3.0/tests/unit/test_export_report.py +328 -0
  64. rootsign-0.3.0/tests/unit/test_http_ingest_client.py +894 -0
  65. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_ingest_client.py +16 -10
  66. rootsign-0.3.0/tests/unit/test_record_loss_ladder.py +475 -0
  67. rootsign-0.3.0/tests/unit/test_replay.py +268 -0
  68. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_sdk_config.py +18 -0
  69. rootsign-0.3.0/tests/unit/test_spool_sync.py +600 -0
  70. rootsign-0.3.0/tests/unit/test_verdict.py +133 -0
  71. rootsign-0.2.1/docs/demo.gif +0 -0
  72. rootsign-0.2.1/examples/quickstart-jsonl/quickstart.py +0 -51
  73. rootsign-0.2.1/rootsign/sdk/client.py +0 -235
  74. rootsign-0.2.1/tests/integration/test_cross_backend_hash.py +0 -321
  75. {rootsign-0.2.1 → rootsign-0.3.0}/.github/CLA_SIGNED.md +0 -0
  76. {rootsign-0.2.1 → rootsign-0.3.0}/.github/CODEOWNERS +0 -0
  77. {rootsign-0.2.1 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  78. {rootsign-0.2.1 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  79. {rootsign-0.2.1 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/framework_integration.yml +0 -0
  80. {rootsign-0.2.1 → rootsign-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  81. {rootsign-0.2.1 → rootsign-0.3.0}/.github/dependabot.yml +0 -0
  82. {rootsign-0.2.1 → rootsign-0.3.0}/.github/workflows/release.yml +0 -0
  83. {rootsign-0.2.1 → rootsign-0.3.0}/.gitignore +0 -0
  84. {rootsign-0.2.1 → rootsign-0.3.0}/.vscode/settings.json +0 -0
  85. {rootsign-0.2.1 → rootsign-0.3.0}/CLA.md +0 -0
  86. {rootsign-0.2.1 → rootsign-0.3.0}/CONTRIBUTORS.md +0 -0
  87. {rootsign-0.2.1 → rootsign-0.3.0}/LICENSE +0 -0
  88. {rootsign-0.2.1 → rootsign-0.3.0}/NOTICE +0 -0
  89. {rootsign-0.2.1 → rootsign-0.3.0}/SECURITY.md +0 -0
  90. {rootsign-0.2.1 → rootsign-0.3.0}/alembic.ini +0 -0
  91. {rootsign-0.2.1 → rootsign-0.3.0}/docker-compose.yml +0 -0
  92. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-001-hash-canonical-spec.md +0 -0
  93. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-002-transport-agnostic-client.md +0 -0
  94. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-003-framework-contract-tests.md +0 -0
  95. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-004-langgraph-interception-strategy.md +0 -0
  96. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-005-crewai-interception-strategy.md +0 -0
  97. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-006-redaction-contract.md +0 -0
  98. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-007-hitl-checkpoint-design.md +0 -0
  99. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-008-decision-capture.md +0 -0
  100. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-009-buffered-ingest-client.md +0 -0
  101. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-010-mcp-interception-strategy.md +0 -0
  102. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-011-jsonl-local-backend.md +0 -0
  103. {rootsign-0.2.1 → rootsign-0.3.0}/docs/adr/ADR-012-init-facade-contextvar-session.md +0 -0
  104. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.0.md +0 -0
  105. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.1.md +0 -0
  106. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.2.md +0 -0
  107. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.3.md +0 -0
  108. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.4.md +0 -0
  109. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.1.5.md +0 -0
  110. {rootsign-0.2.1 → rootsign-0.3.0}/docs/releases/v0.2.0.md +0 -0
  111. {rootsign-0.2.1 → rootsign-0.3.0}/docs/rootsign-logo.png +0 -0
  112. {rootsign-0.2.1 → rootsign-0.3.0}/examples/README.md +0 -0
  113. {rootsign-0.2.1 → rootsign-0.3.0}/examples/docker-compose.yml +0 -0
  114. {rootsign-0.2.1 → rootsign-0.3.0}/examples/langgraph-invoice-agent/.env.example +0 -0
  115. {rootsign-0.2.1 → rootsign-0.3.0}/examples/langgraph-invoice-agent/README.md +0 -0
  116. {rootsign-0.2.1 → rootsign-0.3.0}/examples/langgraph-invoice-agent/agent.py +0 -0
  117. {rootsign-0.2.1 → rootsign-0.3.0}/examples/langgraph-invoice-agent/pyproject.toml +0 -0
  118. {rootsign-0.2.1 → rootsign-0.3.0}/examples/langgraph-invoice-agent/verify.sh +0 -0
  119. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/__init__.py +0 -0
  120. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/__init__.py +0 -0
  121. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/env.py +0 -0
  122. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/script.py.mako +0 -0
  123. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/versions/0001_initial_schema.py +0 -0
  124. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/versions/0002_approval_parent_id.py +0 -0
  125. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/versions/0003_action_timed_out.py +0 -0
  126. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/versions/0004_approval_action_unique.py +0 -0
  127. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_migrations/versions/0005_agents_name_env_unique.py +0 -0
  128. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/_version.py +0 -0
  129. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/config.py +0 -0
  130. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/__init__.py +0 -0
  131. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/agent.py +0 -0
  132. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/approval.py +0 -0
  133. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/base.py +0 -0
  134. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/decision.py +0 -0
  135. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/incident.py +0 -0
  136. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/policy.py +0 -0
  137. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/crud/session.py +0 -0
  138. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/database.py +0 -0
  139. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/hashing.py +0 -0
  140. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/ingest/__init__.py +0 -0
  141. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/ingest/idempotency.py +0 -0
  142. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/mcp/__init__.py +0 -0
  143. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/mcp/proxy.py +0 -0
  144. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/mcp/server.py +0 -0
  145. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/__init__.py +0 -0
  146. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/action.py +0 -0
  147. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/agent.py +0 -0
  148. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/approval.py +0 -0
  149. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/decision.py +0 -0
  150. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/incident.py +0 -0
  151. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/policy.py +0 -0
  152. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/models/session.py +0 -0
  153. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/__init__.py +0 -0
  154. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/action.py +0 -0
  155. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/agent.py +0 -0
  156. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/approval.py +0 -0
  157. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/decision.py +0 -0
  158. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/incident.py +0 -0
  159. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/policy.py +0 -0
  160. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/schemas/session.py +0 -0
  161. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/_async_bridge.py +0 -0
  162. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/context.py +0 -0
  163. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/facade.py +0 -0
  164. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/frameworks/__init__.py +0 -0
  165. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/frameworks/crewai.py +0 -0
  166. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/frameworks/langgraph.py +0 -0
  167. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/hashing.py +0 -0
  168. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/hitl.py +0 -0
  169. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/jsonl_registry.py +0 -0
  170. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/redaction.py +0 -0
  171. {rootsign-0.2.1 → rootsign-0.3.0}/rootsign/sdk/registration.py +0 -0
  172. {rootsign-0.2.1 → rootsign-0.3.0}/scripts/demo_verify_chain.py +0 -0
  173. {rootsign-0.2.1 → rootsign-0.3.0}/scripts/generate_hash_vectors.py +0 -0
  174. {rootsign-0.2.1 → rootsign-0.3.0}/scripts/init_test_db.sql +0 -0
  175. {rootsign-0.2.1 → rootsign-0.3.0}/tests/__init__.py +0 -0
  176. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/__init__.py +0 -0
  177. {rootsign-0.2.1/tests/contract/crewai → rootsign-0.3.0/tests/contract/cloud}/__init__.py +0 -0
  178. {rootsign-0.2.1/tests/integration → rootsign-0.3.0/tests/contract/crewai}/__init__.py +0 -0
  179. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/crewai/test_tool_interception.py +0 -0
  180. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/langgraph/__init__.py +0 -0
  181. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/langgraph/test_tool_interception.py +0 -0
  182. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/mcp/__init__.py +0 -0
  183. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_hitl.py +0 -0
  184. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_interception.py +0 -0
  185. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/mcp/test_server.py +0 -0
  186. {rootsign-0.2.1 → rootsign-0.3.0}/tests/contract/no_extras/__init__.py +0 -0
  187. {rootsign-0.2.1 → rootsign-0.3.0}/tests/fixtures/hash_vectors.json +0 -0
  188. {rootsign-0.2.1 → rootsign-0.3.0}/tests/fixtures/redaction_vectors.json +0 -0
  189. {rootsign-0.2.1/tests/performance → rootsign-0.3.0/tests/integration}/__init__.py +0 -0
  190. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_approval_concurrency.py +0 -0
  191. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_approve_cli.py +0 -0
  192. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_buffered_client.py +0 -0
  193. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_crewai_integration.py +0 -0
  194. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_decision_capture.py +0 -0
  195. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_decision_ingest.py +0 -0
  196. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_facade_implicit_tracers.py +0 -0
  197. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_facade_postgres.py +0 -0
  198. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_ingest.py +0 -0
  199. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_jsonl_buffered.py +0 -0
  200. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_langgraph_integration.py +0 -0
  201. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_mcp_proxy.py +0 -0
  202. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_mcp_server.py +0 -0
  203. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_payload_binding.py +0 -0
  204. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_register_agent.py +0 -0
  205. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_relationships.py +0 -0
  206. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_sdk_smoke.py +0 -0
  207. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_session_context_manager.py +0 -0
  208. {rootsign-0.2.1 → rootsign-0.3.0}/tests/integration/test_verify_cli.py +0 -0
  209. {rootsign-0.2.1/tests/unit → rootsign-0.3.0/tests/performance}/__init__.py +0 -0
  210. {rootsign-0.2.1 → rootsign-0.3.0}/tests/performance/_bench.py +0 -0
  211. {rootsign-0.2.1 → rootsign-0.3.0}/tests/performance/test_benchmarks.py +0 -0
  212. {rootsign-0.2.1 → rootsign-0.3.0}/tests/performance/test_langgraph_benchmarks.py +0 -0
  213. {rootsign-0.2.1 → rootsign-0.3.0}/tests/performance/test_roundtrip_benchmark.py +0 -0
  214. {rootsign-0.2.1 → rootsign-0.3.0}/tests/performance/test_verify_benchmarks.py +0 -0
  215. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_action_record_decision_id.py +0 -0
  216. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_action_sequence_counter.py +0 -0
  217. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_admin_cli.py +0 -0
  218. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_config_guard.py +0 -0
  219. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_context.py +0 -0
  220. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_crewai_tracer.py +0 -0
  221. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_crud_approval.py +0 -0
  222. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_emit_action_record_json_safe.py +0 -0
  223. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_emit_approval_record.py +0 -0
  224. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_emit_decision_record.py +0 -0
  225. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_errors.py +0 -0
  226. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_facade.py +0 -0
  227. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_hashing.py +0 -0
  228. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_hitl.py +0 -0
  229. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_hitl_jsonl.py +0 -0
  230. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_idempotency_bounds.py +0 -0
  231. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_input_payload_override.py +0 -0
  232. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_jsonl_client.py +0 -0
  233. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_jsonl_registry.py +0 -0
  234. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_langgraph_tracer.py +0 -0
  235. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_log_injection.py +0 -0
  236. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_redaction.py +0 -0
  237. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_redaction_pii.py +0 -0
  238. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_schemas.py +0 -0
  239. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_sdk_hashing.py +0 -0
  240. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_session_buffered_flush.py +0 -0
  241. {rootsign-0.2.1 → rootsign-0.3.0}/tests/unit/test_trace_hitl.py +0 -0
  242. {rootsign-0.2.1 → 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
@@ -201,14 +201,17 @@ jobs:
201
201
  python-version: '3.12'
202
202
  - name: Install core only (no extras)
203
203
  run: pip install .
204
- - 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).
205
208
  run: |
206
209
  python - <<'PY'
207
210
  import importlib.util as u
208
- bad = [m for m in ("sqlalchemy","asyncpg","psycopg2","greenlet","alembic")
211
+ bad = [m for m in ("sqlalchemy","asyncpg","psycopg2","greenlet","alembic","httpx")
209
212
  if u.find_spec(m) is not None]
210
- assert not bad, f"core install pulled DB packages: {bad}"
211
- 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")
212
215
  PY
213
216
  - name: import rootsign + facade surface works DB-free
214
217
  run: |
@@ -238,6 +241,24 @@ jobs:
238
241
  assert "rootsign[postgres]" in str(e), e
239
242
  print("RootSignPostgresExtraRequired raised OK")
240
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
241
262
  - name: Console scripts run DB-free (CLI tripwire)
242
263
  # `import rootsign` passing is NOT sufficient. The console script entry
243
264
  # point imports `rootsign.sdk.cli`, a different import graph that the
@@ -256,6 +277,12 @@ jobs:
256
277
  rootsign-admin --help > /dev/null
257
278
  rootsign-admin start-db --help > /dev/null
258
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
259
286
  # Store-exported fixture, not a checked-in file: let the JSONL backend
260
287
  # write a real session so the chain is produced by the same code path
261
288
  # that verifies it. Never hand-construct a self_hash (ADR-001).
@@ -277,6 +304,36 @@ jobs:
277
304
  PY
278
305
  )"
279
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"
280
337
  echo "console scripts OK, DB-free"
281
338
  - name: DB-backed subcommands fail with the install hint, not a traceback
282
339
  # The other half of the same bug: these commands legitimately need the
@@ -286,6 +343,7 @@ jobs:
286
343
  set -uo pipefail
287
344
  # Every DB-backed subcommand across BOTH console scripts.
288
345
  for cmd in "rootsign verify 550e8400-e29b-41d4-a716-446655440001" \
346
+ "rootsign export 550e8400-e29b-41d4-a716-446655440001" \
289
347
  "rootsign approve --list" \
290
348
  "rootsign approve 550e8400-e29b-41d4-a716-446655440001" \
291
349
  "rootsign-admin status" \
@@ -69,12 +69,37 @@ docs(adr): add ADR-004 for retry strategy
69
69
  - Framework integrations must pass contract tests on all supported framework versions (see CI matrix)
70
70
  - Run `ruff check .` and `ruff format .` before pushing
71
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
+
72
96
  ## What we will NOT merge
73
97
 
74
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
75
99
  - Synchronous SQLAlchemy in any non-Alembic code
76
100
  - Mock-based integration tests (real PostgreSQL + TimescaleDB only — see [the data model rationale](AGENTS.md))
77
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))
78
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))
79
104
 
80
105
  ## Adding a new framework integration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rootsign
3
- Version: 0.2.1
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.1.** `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
 
@@ -27,13 +27,14 @@ Compliance-grade audit trails. Zero changes to your agent code.
27
27
 
28
28
  ## Status
29
29
 
30
- **v0.2.1.** `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.
30
+ **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.
31
31
 
32
32
  | Phase | Scope | Status |
33
33
  |---|---|---|
34
34
  | 0 | Data model + storage + ingest handler | ✅ Complete |
35
35
  | 1 | Python SDK — `@rootsign.trace`, LangGraph + CrewAI + MCP proxy, `rootsign verify` CLI, redaction, HiTL checkpoint, decision capture, micro-batching | ✅ v0.1.5 |
36
36
  | 1.5 | Zero-dependency onboarding — JSONL default backend, `rootsign.init()` facade | ✅ v0.2.0 |
37
+ | 1.6 | Evidence bundles (`rootsign export`), cloud transport + offline spool, published ingest spec | ✅ v0.3.0 |
37
38
  | 2 | Hosted ingest backend + compliance dashboard | Planned |
38
39
  | 3 | Policy enforcement + incident workflow | Planned |
39
40
  | 4 | Cross-platform governance | Planned |
@@ -76,7 +77,7 @@ VALID ✓ — 2 records, chain intact
76
77
  Session: f758a636-7bcd-4f96-8940-eff7d80e760a
77
78
  ```
78
79
 
79
- 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:
80
+ 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:
80
81
 
81
82
  ```bash
82
83
  $ rootsign verify --local ~/.rootsign/sessions/f758a636-7bcd-4f96-8940-eff7d80e760a.jsonl
@@ -86,7 +87,27 @@ TAMPERED ✗ — chain broken at record #1
86
87
  WARNING: This session log may have been tampered with.
87
88
  ```
88
89
 
89
- 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.
90
+ When someone outside engineering needs to see it, turn the same session into a self-contained evidence bundle:
91
+
92
+ ```bash
93
+ $ rootsign export --local ~/.rootsign/sessions/f758a636-7bcd-4f96-8940-eff7d80e760a.jsonl
94
+ VALID — 2 records, chain intact
95
+ Bundle: ./evidence-f758a636-7bcd-4f96-8940-eff7d80e760a
96
+ redaction.json
97
+ report.html
98
+ report.md
99
+ timeline.json
100
+ verification.json
101
+ manifest.json
102
+
103
+ manifest.json SHA-256: a1d61338d0f793ea03ac472863fadea5660e1e94846913ce7613d8c117d67cc5
104
+ Record that hash outside the bundle — a ticket, an email, a chain-of-custody log.
105
+ It is what proves a bundle you receive later is the one that was generated.
106
+ ```
107
+
108
+ Open `report.html` in any browser — [see below](#evidence-bundles).
109
+
110
+ 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.
90
111
 
91
112
  A runnable version of the above is [`examples/quickstart-jsonl`](examples/quickstart-jsonl/).
92
113
 
@@ -322,6 +343,94 @@ tools = rootsign.wrap_tools([send_invoice], redaction_config=redaction)
322
343
 
323
344
  `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).
324
345
 
346
+ ## Evidence bundles
347
+
348
+ 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.
349
+
350
+ `rootsign export` turns one session into a directory that answers it:
351
+
352
+ ```bash
353
+ $ rootsign export --local ~/.rootsign/sessions/<session_id>.jsonl --out ./bundles
354
+ ```
355
+
356
+ <p align="center">
357
+ <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" />
358
+ </p>
359
+
360
+ <p align="center"><em><code>report.html</code> — the verdict first, then the chain, then what happened.</em></p>
361
+
362
+ <p align="center">
363
+ <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" />
364
+ </p>
365
+
366
+ <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>
367
+
368
+ | File | What it holds |
369
+ |---|---|
370
+ | `manifest.json` | bundle version, generator, agent identity, source backend, a SHA-256 for every other file, and a reserved `compliance` block |
371
+ | `verification.json` | the verdict plus a per-record listing — sequence, `action_id`, `self_hash`, `prev_action_hash`, and whether that record was verified |
372
+ | `timeline.json` | the session narrative in order: open → decisions → actions → approvals → close, with every field each record carries |
373
+ | `redaction.json` | which field paths held the `[REDACTED]` sentinel — the "PII was never stored" evidence |
374
+ | `report.md` / `report.html` | human renderings of exactly the above; the verdict is the first thing on the page |
375
+
376
+ Three properties make the bundle worth handing over:
377
+
378
+ **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.
379
+
380
+ **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.
381
+
382
+ **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:
383
+
384
+ ```bash
385
+ $ rootsign export --check ./bundles/evidence-<session_id>
386
+ ✓ INTACT — 5 file(s) match manifest.json
387
+
388
+ manifest.json SHA-256: a1d61338d0f793ea03ac472863fadea5660e1e94846913ce7613d8c117d67cc5
389
+ Compare this against the hash recorded when the bundle was exported.
390
+ The file checks above cannot detect an edit that also updated the manifest.
391
+ ```
392
+
393
+ 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.
394
+
395
+ 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.
396
+
397
+ `--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).
398
+
399
+ ## Offline & sync (cloud mode)
400
+
401
+ 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.
402
+
403
+ **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.
404
+
405
+ **Spooled records are ordinary session files**, so the evidence is usable before it is uploaded:
406
+
407
+ ```bash
408
+ $ rootsign verify --local ~/.rootsign/spool/sessions/<session_id>.jsonl
409
+ VALID ✓ — 2 records, chain intact
410
+ Session: 2d333ca5-7b28-4cd3-80b4-8f67350d19ba
411
+
412
+ This is a spooled session — recorded locally because the cloud endpoint was unreachable.
413
+ Upload it (and everything else waiting) with:
414
+
415
+ rootsign-admin sync
416
+ ```
417
+
418
+ > 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`.
419
+
420
+ **When connectivity returns, upload:**
421
+
422
+ ```bash
423
+ $ rootsign-admin sync
424
+ 1 spooled session(s), 4 record(s) under ~/.rootsign/spool
425
+ Uploading to https://ingest.getprovidex.com/v1/ingest
426
+ synced 2d333ca5-7b28-4cd3-80b4-8f67350d19ba 4 accepted, 0 already present -> synced/
427
+ All spooled sessions uploaded.
428
+ ```
429
+
430
+ 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.
431
+
432
+ **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).
433
+
325
434
  ## Micro-batching (opt-in)
326
435
 
327
436
  `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:
@@ -364,14 +473,16 @@ The `-m benchmark` marker keeps the performance suite opt-in. Run it **without**
364
473
  * **`@rootsign.trace`** wraps a tool callable and emits an `ACTION_RECORD` envelope per call. LangGraph `BaseTool` and CrewAI tools are detected automatically.
365
474
  * **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).
366
475
  * **`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).
367
- * **`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).
476
+ * **`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).
368
477
  * **Hash chain** is per-session: each `Action` carries `prev_action_hash` so reconstructing the chain detects any after-the-fact modification.
478
+ * **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.
369
479
  * **`HiTLCheckpoint`** is an async poll loop that opens its own DB session per cycle — see ADR-007 for the loop-binding rationale.
480
+ * **`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.
370
481
  * **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.
371
482
 
372
483
  ## What's next
373
484
 
374
- * **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.
485
+ * **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.
375
486
  * **Web UI for HiTL** — approve/reject pending actions from a browser instead of the CLI.
376
487
  * **AutoGen integration** — same duck-typing shape as CrewAI.
377
488