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.
- {rootsign-0.2.0 → rootsign-0.3.0}/.env.example +23 -3
- rootsign-0.3.0/.github/dependabot.yml +31 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/workflows/ci.yml +140 -16
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/workflows/release.yml +9 -3
- {rootsign-0.2.0 → rootsign-0.3.0}/CONTRIBUTING.md +32 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/PKG-INFO +120 -7
- {rootsign-0.2.0 → rootsign-0.3.0}/README.md +116 -5
- rootsign-0.3.0/docs/adr/ADR-013-http-ingest-client-spool.md +283 -0
- rootsign-0.3.0/docs/adr/ADR-014-export-evidence-bundle.md +182 -0
- rootsign-0.3.0/docs/demo.gif +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/demo.tape +12 -2
- rootsign-0.3.0/docs/evidence-report-approval.png +0 -0
- rootsign-0.3.0/docs/evidence-report.png +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/framework-support.md +15 -1
- rootsign-0.3.0/docs/ingest-spec-v1.md +731 -0
- rootsign-0.3.0/docs/releases/v0.3.0.md +113 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/quickstart-jsonl/README.md +45 -2
- rootsign-0.3.0/examples/quickstart-jsonl/quickstart.py +72 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/pyproject.toml +12 -5
- rootsign-0.3.0/rootsign/chain_state.py +173 -0
- rootsign-0.3.0/rootsign/cli.py +484 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/action.py +79 -44
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/errors.py +104 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/handler.py +13 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/schemas.py +20 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/server.py +14 -7
- rootsign-0.3.0/rootsign/replay.py +148 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/__init__.py +1 -1
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/buffered_client.py +103 -11
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/chain.py +106 -53
- rootsign-0.3.0/rootsign/sdk/cli.py +645 -0
- rootsign-0.3.0/rootsign/sdk/client.py +850 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/config.py +58 -5
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/decorator.py +82 -6
- rootsign-0.3.0/rootsign/sdk/export.py +870 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/jsonl_client.py +52 -40
- rootsign-0.3.0/rootsign/sdk/loss_ledger.py +110 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/registration.py +9 -6
- rootsign-0.3.0/rootsign/sdk/report.py +594 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/session.py +3 -2
- rootsign-0.3.0/rootsign/sdk/spool.py +282 -0
- rootsign-0.3.0/rootsign/verdict.py +140 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/scripts/demo_for_gif.py +5 -1
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/conftest.py +42 -0
- rootsign-0.3.0/tests/contract/cloud/test_http_transport.py +701 -0
- rootsign-0.3.0/tests/contract/no_extras/test_packaging_isolation.py +538 -0
- rootsign-0.3.0/tests/fixtures/evidence_bundle_v1.json +186 -0
- rootsign-0.3.0/tests/integration/test_cross_backend_hash.py +536 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_crud.py +78 -0
- rootsign-0.3.0/tests/integration/test_dead_spool.py +372 -0
- rootsign-0.3.0/tests/integration/test_export_cli_postgres.py +180 -0
- rootsign-0.3.0/tests/integration/test_export_sources.py +205 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_ingest.py +6 -32
- rootsign-0.3.0/tests/integration/test_kill_network.py +287 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_show_hn_quickstart.py +51 -21
- rootsign-0.3.0/tests/integration/test_verdict_parity.py +341 -0
- rootsign-0.3.0/tests/performance/_bench.py +55 -0
- rootsign-0.3.0/tests/performance/test_benchmarks.py +178 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/performance/test_langgraph_benchmarks.py +8 -0
- rootsign-0.3.0/tests/performance/test_roundtrip_benchmark.py +142 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/performance/test_verify_benchmarks.py +19 -9
- rootsign-0.3.0/tests/support/__init__.py +0 -0
- rootsign-0.3.0/tests/support/mock_ingest_store.py +206 -0
- rootsign-0.3.0/tests/support/session_files.py +142 -0
- rootsign-0.3.0/tests/unit/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_admin_cli.py +11 -4
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_buffered_client.py +16 -3
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_chain.py +113 -2
- rootsign-0.3.0/tests/unit/test_chain_state.py +145 -0
- rootsign-0.3.0/tests/unit/test_export_bundle.py +391 -0
- rootsign-0.3.0/tests/unit/test_export_check.py +228 -0
- rootsign-0.3.0/tests/unit/test_export_cli.py +251 -0
- rootsign-0.3.0/tests/unit/test_export_golden.py +359 -0
- rootsign-0.3.0/tests/unit/test_export_report.py +328 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hitl_jsonl.py +1 -1
- rootsign-0.3.0/tests/unit/test_http_ingest_client.py +894 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_ingest_client.py +16 -10
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_jsonl_client.py +9 -6
- rootsign-0.3.0/tests/unit/test_log_injection.py +94 -0
- rootsign-0.3.0/tests/unit/test_record_loss_ladder.py +475 -0
- rootsign-0.3.0/tests/unit/test_replay.py +268 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_sdk_config.py +18 -0
- rootsign-0.3.0/tests/unit/test_spool_sync.py +600 -0
- rootsign-0.3.0/tests/unit/test_verdict.py +133 -0
- rootsign-0.2.0/docs/demo.gif +0 -0
- rootsign-0.2.0/examples/quickstart-jsonl/quickstart.py +0 -51
- rootsign-0.2.0/rootsign/cli.py +0 -264
- rootsign-0.2.0/rootsign/sdk/cli.py +0 -313
- rootsign-0.2.0/rootsign/sdk/client.py +0 -237
- rootsign-0.2.0/tests/contract/no_extras/test_packaging_isolation.py +0 -113
- rootsign-0.2.0/tests/integration/test_cross_backend_hash.py +0 -133
- rootsign-0.2.0/tests/performance/test_benchmarks.py +0 -125
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/CLA_SIGNED.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/CODEOWNERS +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/ISSUE_TEMPLATE/framework_integration.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.gitignore +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/.vscode/settings.json +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/CLA.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/CONTRIBUTORS.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/LICENSE +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/NOTICE +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/SECURITY.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/alembic.ini +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docker-compose.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-001-hash-canonical-spec.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-002-transport-agnostic-client.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-003-framework-contract-tests.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-004-langgraph-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-005-crewai-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-006-redaction-contract.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-007-hitl-checkpoint-design.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-008-decision-capture.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-009-buffered-ingest-client.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-010-mcp-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-011-jsonl-local-backend.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/adr/ADR-012-init-facade-contextvar-session.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.0.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.1.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.2.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.3.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.4.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.1.5.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/releases/v0.2.0.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/docs/rootsign-logo.png +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/README.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/docker-compose.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/.env.example +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/README.md +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/pyproject.toml +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/examples/langgraph-invoice-agent/verify.sh +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/env.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/script.py.mako +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0001_initial_schema.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0002_approval_parent_id.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0003_action_timed_out.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0004_approval_action_unique.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_migrations/versions/0005_agents_name_env_unique.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/_version.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/config.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/base.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/crud/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/database.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/ingest/idempotency.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/mcp/proxy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/action.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/models/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/action.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/schemas/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/_async_bridge.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/context.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/facade.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/crewai.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/frameworks/langgraph.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/jsonl_registry.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/rootsign/sdk/redaction.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/scripts/demo_verify_chain.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/scripts/generate_hash_vectors.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/scripts/init_test_db.sql +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/__init__.py +0 -0
- {rootsign-0.2.0/tests/contract/crewai → rootsign-0.3.0/tests/contract/cloud}/__init__.py +0 -0
- {rootsign-0.2.0/tests/integration → rootsign-0.3.0/tests/contract/crewai}/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/crewai/test_tool_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/langgraph/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/langgraph/test_tool_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_proxy_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/mcp/test_server.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/contract/no_extras/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/fixtures/hash_vectors.json +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/fixtures/redaction_vectors.json +0 -0
- {rootsign-0.2.0/tests/performance → rootsign-0.3.0/tests/integration}/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_approval_concurrency.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_approve_cli.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_buffered_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_crewai_integration.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_decision_capture.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_decision_ingest.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_facade_implicit_tracers.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_facade_postgres.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_jsonl_buffered.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_langgraph_integration.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_mcp_proxy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_mcp_server.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_payload_binding.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_register_agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_relationships.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_sdk_smoke.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_session_context_manager.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/integration/test_verify_cli.py +0 -0
- {rootsign-0.2.0/tests/unit → rootsign-0.3.0/tests/performance}/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_action_record_decision_id.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_action_sequence_counter.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_config_guard.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_context.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_crewai_tracer.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_crud_approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_action_record_json_safe.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_approval_record.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_emit_decision_record.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_errors.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_facade.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_idempotency_bounds.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_input_payload_override.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_jsonl_registry.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_langgraph_tracer.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_redaction.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_redaction_pii.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_schemas.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_sdk_hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_session_buffered_flush.py +0 -0
- {rootsign-0.2.0 → rootsign-0.3.0}/tests/unit/test_trace_hitl.py +0 -0
- {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'
|
|
21
|
-
#
|
|
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
|
-
#
|
|
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@
|
|
42
|
-
- uses: actions/setup-python@
|
|
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@
|
|
85
|
-
- uses: actions/setup-python@
|
|
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@
|
|
122
|
-
- uses: actions/setup-python@
|
|
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@
|
|
150
|
-
- uses: actions/setup-python@
|
|
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@
|
|
174
|
-
- uses: actions/setup-python@
|
|
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@
|
|
192
|
-
- uses: actions/setup-python@
|
|
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
|
|
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@
|
|
26
|
-
- uses: actions/setup-python@
|
|
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
|
-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: rootsign
|
|
3
|
-
Version: 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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|