rootsign 0.2.0__tar.gz → 0.2.1__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.1/.github/dependabot.yml +31 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/workflows/ci.yml +78 -12
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/workflows/release.yml +9 -3
- {rootsign-0.2.0 → rootsign-0.2.1}/CONTRIBUTING.md +7 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/PKG-INFO +3 -3
- {rootsign-0.2.0 → rootsign-0.2.1}/README.md +1 -1
- {rootsign-0.2.0 → rootsign-0.2.1}/pyproject.toml +1 -1
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/cli.py +38 -7
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/errors.py +31 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/mcp/server.py +14 -7
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/chain.py +4 -1
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/cli.py +57 -10
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/client.py +3 -5
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/decorator.py +54 -4
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/registration.py +9 -6
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/conftest.py +24 -0
- rootsign-0.2.1/tests/contract/no_extras/test_packaging_isolation.py +273 -0
- rootsign-0.2.1/tests/integration/test_cross_backend_hash.py +321 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_ingest.py +6 -32
- rootsign-0.2.1/tests/performance/_bench.py +55 -0
- rootsign-0.2.1/tests/performance/test_benchmarks.py +178 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/performance/test_langgraph_benchmarks.py +8 -0
- rootsign-0.2.1/tests/performance/test_roundtrip_benchmark.py +142 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/performance/test_verify_benchmarks.py +19 -9
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_admin_cli.py +11 -4
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_hitl_jsonl.py +1 -1
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_jsonl_client.py +9 -6
- rootsign-0.2.1/tests/unit/test_log_injection.py +94 -0
- 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.2.1}/.env.example +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/CLA_SIGNED.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/CODEOWNERS +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/ISSUE_TEMPLATE/framework_integration.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.gitignore +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/.vscode/settings.json +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/CLA.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/CONTRIBUTORS.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/LICENSE +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/NOTICE +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/SECURITY.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/alembic.ini +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docker-compose.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-001-hash-canonical-spec.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-002-transport-agnostic-client.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-003-framework-contract-tests.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-004-langgraph-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-005-crewai-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-006-redaction-contract.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-007-hitl-checkpoint-design.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-008-decision-capture.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-009-buffered-ingest-client.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-010-mcp-interception-strategy.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-011-jsonl-local-backend.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/adr/ADR-012-init-facade-contextvar-session.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/demo.gif +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/demo.tape +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/framework-support.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.0.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.1.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.2.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.3.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.4.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.1.5.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/releases/v0.2.0.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/docs/rootsign-logo.png +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/README.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/docker-compose.yml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/langgraph-invoice-agent/.env.example +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/langgraph-invoice-agent/README.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/langgraph-invoice-agent/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/langgraph-invoice-agent/pyproject.toml +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/langgraph-invoice-agent/verify.sh +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/quickstart-jsonl/README.md +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/examples/quickstart-jsonl/quickstart.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/env.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/script.py.mako +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/versions/0001_initial_schema.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/versions/0002_approval_parent_id.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/versions/0003_action_timed_out.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/versions/0004_approval_action_unique.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_migrations/versions/0005_agents_name_env_unique.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/_version.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/config.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/action.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/base.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/crud/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/database.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/ingest/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/ingest/handler.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/ingest/idempotency.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/ingest/schemas.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/mcp/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/mcp/proxy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/action.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/models/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/action.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/decision.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/incident.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/policy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/schemas/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/_async_bridge.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/buffered_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/config.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/context.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/facade.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/frameworks/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/frameworks/crewai.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/frameworks/langgraph.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/jsonl_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/jsonl_registry.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/redaction.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/rootsign/sdk/session.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/scripts/demo_for_gif.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/scripts/demo_verify_chain.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/scripts/generate_hash_vectors.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/scripts/init_test_db.sql +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/crewai/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/crewai/test_tool_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/langgraph/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/langgraph/test_tool_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/mcp/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/mcp/test_proxy_hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/mcp/test_proxy_interception.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/mcp/test_server.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/contract/no_extras/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/fixtures/hash_vectors.json +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/fixtures/redaction_vectors.json +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_approval_concurrency.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_approve_cli.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_buffered_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_crewai_integration.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_crud.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_decision_capture.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_decision_ingest.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_facade_implicit_tracers.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_facade_postgres.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_jsonl_buffered.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_langgraph_integration.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_mcp_proxy.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_mcp_server.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_payload_binding.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_register_agent.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_relationships.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_sdk_smoke.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_session_context_manager.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_show_hn_quickstart.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/integration/test_verify_cli.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/performance/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/__init__.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_action_record_decision_id.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_action_sequence_counter.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_buffered_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_chain.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_config_guard.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_context.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_crewai_tracer.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_crud_approval.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_emit_action_record_json_safe.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_emit_approval_record.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_emit_decision_record.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_errors.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_facade.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_idempotency_bounds.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_ingest_client.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_input_payload_override.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_jsonl_registry.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_langgraph_tracer.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_redaction.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_redaction_pii.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_schemas.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_sdk_config.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_sdk_hashing.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_session_buffered_flush.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/tests/unit/test_trace_hitl.py +0 -0
- {rootsign-0.2.0 → rootsign-0.2.1}/uv.lock +0 -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,8 +195,8 @@ 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)
|
|
@@ -231,6 +238,65 @@ jobs:
|
|
|
231
238
|
assert "rootsign[postgres]" in str(e), e
|
|
232
239
|
print("RootSignPostgresExtraRequired raised OK")
|
|
233
240
|
PY
|
|
241
|
+
- name: Console scripts run DB-free (CLI tripwire)
|
|
242
|
+
# `import rootsign` passing is NOT sufficient. The console script entry
|
|
243
|
+
# point imports `rootsign.sdk.cli`, a different import graph that the
|
|
244
|
+
# checks above never touch — a module-level `from rootsign.database
|
|
245
|
+
# import AsyncSessionLocal` there crashed EVERY `rootsign ...`
|
|
246
|
+
# invocation on a bare install (even `version` and `verify --local`,
|
|
247
|
+
# neither of which needs a DB) while this job stayed green. This step
|
|
248
|
+
# exercises the script itself so that class of regression fails here.
|
|
249
|
+
run: |
|
|
250
|
+
set -euo pipefail
|
|
251
|
+
rootsign version
|
|
252
|
+
rootsign --help > /dev/null
|
|
253
|
+
# The operator CLI is a separate entry point (rootsign.cli:app) with
|
|
254
|
+
# its own import graph. --help and the docker-only subcommands must
|
|
255
|
+
# work without the postgres extra.
|
|
256
|
+
rootsign-admin --help > /dev/null
|
|
257
|
+
rootsign-admin start-db --help > /dev/null
|
|
258
|
+
rootsign-admin stop-db --help > /dev/null
|
|
259
|
+
# Store-exported fixture, not a checked-in file: let the JSONL backend
|
|
260
|
+
# write a real session so the chain is produced by the same code path
|
|
261
|
+
# that verifies it. Never hand-construct a self_hash (ADR-001).
|
|
262
|
+
SESSION_ID="$(python - <<'PY'
|
|
263
|
+
import asyncio, rootsign
|
|
264
|
+
|
|
265
|
+
rootsign.init(agent="ci-tripwire")
|
|
266
|
+
|
|
267
|
+
@rootsign.trace()
|
|
268
|
+
async def add(a: int, b: int) -> int:
|
|
269
|
+
return a + b
|
|
270
|
+
|
|
271
|
+
async def main():
|
|
272
|
+
async with rootsign.session() as ctx:
|
|
273
|
+
await add(2, 3)
|
|
274
|
+
return ctx.session_id
|
|
275
|
+
|
|
276
|
+
print(asyncio.run(main()))
|
|
277
|
+
PY
|
|
278
|
+
)"
|
|
279
|
+
rootsign verify --local "$HOME/.rootsign/sessions/$SESSION_ID.jsonl"
|
|
280
|
+
echo "console scripts OK, DB-free"
|
|
281
|
+
- name: DB-backed subcommands fail with the install hint, not a traceback
|
|
282
|
+
# The other half of the same bug: these commands legitimately need the
|
|
283
|
+
# extra, but must say so in one line and exit 1 rather than dumping a
|
|
284
|
+
# ModuleNotFoundError traceback at the user.
|
|
285
|
+
run: |
|
|
286
|
+
set -uo pipefail
|
|
287
|
+
# Every DB-backed subcommand across BOTH console scripts.
|
|
288
|
+
for cmd in "rootsign verify 550e8400-e29b-41d4-a716-446655440001" \
|
|
289
|
+
"rootsign approve --list" \
|
|
290
|
+
"rootsign approve 550e8400-e29b-41d4-a716-446655440001" \
|
|
291
|
+
"rootsign-admin status" \
|
|
292
|
+
"rootsign-admin init"; do
|
|
293
|
+
out="$($cmd 2>&1)" && { echo "expected failure: $cmd"; exit 1; }
|
|
294
|
+
echo "$out" | grep -q "rootsign\[postgres\]" || {
|
|
295
|
+
echo "missing install hint for: $cmd"; echo "$out"; exit 1; }
|
|
296
|
+
echo "$out" | grep -qi "Traceback" && {
|
|
297
|
+
echo "leaked traceback for: $cmd"; echo "$out"; exit 1; }
|
|
298
|
+
echo "$cmd -> actionable error OK"
|
|
299
|
+
done
|
|
234
300
|
# NOTE: we deliberately do NOT run `pytest tests/contract/no_extras/`
|
|
235
301
|
# here — the root tests/conftest.py imports sqlalchemy at module level
|
|
236
302
|
# (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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: rootsign
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -89,7 +89,7 @@ Compliance-grade audit trails. Zero changes to your agent code.
|
|
|
89
89
|
|
|
90
90
|
## Status
|
|
91
91
|
|
|
92
|
-
**v0.2.
|
|
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.
|
|
93
93
|
|
|
94
94
|
| Phase | Scope | Status |
|
|
95
95
|
|---|---|---|
|
|
@@ -27,7 +27,7 @@ Compliance-grade audit trails. Zero changes to your agent code.
|
|
|
27
27
|
|
|
28
28
|
## Status
|
|
29
29
|
|
|
30
|
-
**v0.2.
|
|
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.
|
|
31
31
|
|
|
32
32
|
| Phase | Scope | Status |
|
|
33
33
|
|---|---|---|
|
|
@@ -18,16 +18,24 @@ import re
|
|
|
18
18
|
import shutil
|
|
19
19
|
import subprocess
|
|
20
20
|
import time
|
|
21
|
-
from importlib import resources
|
|
21
|
+
from importlib import import_module, resources
|
|
22
22
|
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
23
24
|
|
|
24
|
-
import psycopg2
|
|
25
25
|
import typer
|
|
26
|
-
from alembic import command
|
|
27
|
-
from alembic.config import Config
|
|
28
26
|
|
|
29
27
|
from rootsign.config import settings
|
|
30
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from alembic.config import Config
|
|
31
|
+
|
|
32
|
+
# psycopg2 and alembic live in the optional `postgres` extra (ADR-011), so they
|
|
33
|
+
# are imported inside the commands that need them via `_require()`. At module
|
|
34
|
+
# level they made `rootsign-admin` unusable on a bare install — even
|
|
35
|
+
# `rootsign-admin --help` and the docker-only `start-db` / `stop-db` died with
|
|
36
|
+
# ModuleNotFoundError before Typer could dispatch. Same defect class as the
|
|
37
|
+
# `rootsign` console script (rootsign/sdk/cli.py).
|
|
38
|
+
|
|
31
39
|
app = typer.Typer(
|
|
32
40
|
name="rootsign-admin",
|
|
33
41
|
help="RootSign Phase 0 — schema and storage operations (powered by Providex AI).",
|
|
@@ -35,6 +43,24 @@ app = typer.Typer(
|
|
|
35
43
|
no_args_is_help=True,
|
|
36
44
|
)
|
|
37
45
|
|
|
46
|
+
|
|
47
|
+
def _require(module: str) -> Any:
|
|
48
|
+
"""Import an optional DB-stack module, or exit with the install hint.
|
|
49
|
+
|
|
50
|
+
Mirrors `rootsign.sdk.cli._session_factory`: a missing `postgres` extra
|
|
51
|
+
becomes a one-line actionable error and exit 1, never a raw traceback.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
return import_module(module)
|
|
55
|
+
except ModuleNotFoundError as exc:
|
|
56
|
+
from rootsign.errors import RootSignPostgresExtraRequired
|
|
57
|
+
|
|
58
|
+
typer.echo(
|
|
59
|
+
f"Error: {RootSignPostgresExtraRequired(f'missing module: {exc.name}')}",
|
|
60
|
+
err=True,
|
|
61
|
+
)
|
|
62
|
+
raise typer.Exit(code=1) from exc
|
|
63
|
+
|
|
38
64
|
# Default container name + image. Mirrors docker-compose.yml so devs can swap
|
|
39
65
|
# freely between `rootsign-admin start-db` and `docker-compose up -d db`.
|
|
40
66
|
_DB_CONTAINER = "rootsign-timescaledb"
|
|
@@ -61,7 +87,7 @@ def _alembic_config(sync_url: str | None = None) -> Config:
|
|
|
61
87
|
are set directly, so PyPI users (who have no alembic.ini) and devs in the
|
|
62
88
|
repo root behave identically.
|
|
63
89
|
"""
|
|
64
|
-
cfg = Config()
|
|
90
|
+
cfg = _require("alembic.config").Config()
|
|
65
91
|
cfg.set_main_option("script_location", str(_migrations_dir()))
|
|
66
92
|
cfg.set_main_option("sqlalchemy.url", sync_url or settings.DATABASE_URL_SYNC)
|
|
67
93
|
return cfg
|
|
@@ -77,7 +103,8 @@ def _dsn_role() -> str:
|
|
|
77
103
|
|
|
78
104
|
|
|
79
105
|
def _drop_public_schema() -> None:
|
|
80
|
-
|
|
106
|
+
psycopg2 = _require("psycopg2")
|
|
107
|
+
sql = _require("psycopg2.sql")
|
|
81
108
|
|
|
82
109
|
dsn = _strip_driver(settings.DATABASE_URL_SYNC)
|
|
83
110
|
role = sql.Identifier(_dsn_role())
|
|
@@ -211,6 +238,9 @@ def init(
|
|
|
211
238
|
),
|
|
212
239
|
) -> None:
|
|
213
240
|
"""Apply migrations to the dev database. Use --reset to start from scratch."""
|
|
241
|
+
# Resolve the DB stack before any output or the destructive --reset prompt,
|
|
242
|
+
# so a missing extra fails cleanly instead of half-way through.
|
|
243
|
+
alembic_command = _require("alembic.command")
|
|
214
244
|
start = time.perf_counter()
|
|
215
245
|
if reset:
|
|
216
246
|
# audit #11b: --reset drops the ENTIRE public schema. Require an
|
|
@@ -225,7 +255,7 @@ def init(
|
|
|
225
255
|
typer.echo("Dropping public schema...")
|
|
226
256
|
_drop_public_schema()
|
|
227
257
|
typer.echo("Running alembic upgrade head...")
|
|
228
|
-
|
|
258
|
+
alembic_command.upgrade(_alembic_config(), "head")
|
|
229
259
|
elapsed = time.perf_counter() - start
|
|
230
260
|
typer.echo(f"Done in {elapsed:.2f}s")
|
|
231
261
|
|
|
@@ -233,6 +263,7 @@ def init(
|
|
|
233
263
|
@app.command()
|
|
234
264
|
def status() -> None:
|
|
235
265
|
"""Print row counts for every RootSign table."""
|
|
266
|
+
psycopg2 = _require("psycopg2")
|
|
236
267
|
dsn = _strip_driver(settings.DATABASE_URL_SYNC)
|
|
237
268
|
conn = psycopg2.connect(dsn)
|
|
238
269
|
try:
|
|
@@ -24,6 +24,8 @@ import from here without back-edges.
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from collections.abc import Iterator
|
|
27
29
|
from uuid import UUID
|
|
28
30
|
|
|
29
31
|
|
|
@@ -194,6 +196,35 @@ class RootSignPostgresExtraRequired(RootSignError):
|
|
|
194
196
|
super().__init__(message)
|
|
195
197
|
|
|
196
198
|
|
|
199
|
+
@contextmanager
|
|
200
|
+
def postgres_extra_required() -> "Iterator[None]":
|
|
201
|
+
"""Translate a missing DB stack into `RootSignPostgresExtraRequired`.
|
|
202
|
+
|
|
203
|
+
Wrap the lazy imports of any code path that needs Postgres::
|
|
204
|
+
|
|
205
|
+
with postgres_extra_required():
|
|
206
|
+
from sqlalchemy import select
|
|
207
|
+
|
|
208
|
+
from rootsign.database import AsyncSessionLocal
|
|
209
|
+
|
|
210
|
+
Since ADR-011 those imports are deferred into function bodies so the core
|
|
211
|
+
install stays DB-free. Deferring alone only moves *where* the failure
|
|
212
|
+
happens, though — without this, a user on `pip install rootsign` who calls
|
|
213
|
+
a DB-backed path gets `ModuleNotFoundError: No module named 'sqlalchemy'`
|
|
214
|
+
from somewhere deep in the import graph, with no hint that an extra exists.
|
|
215
|
+
`get_ingest_client` already translated; several SDK entry points did not,
|
|
216
|
+
so the same mistake produced a helpful message or a bare traceback
|
|
217
|
+
depending on which door you came through.
|
|
218
|
+
|
|
219
|
+
Only wrap the imports themselves. A `ModuleNotFoundError` from unrelated
|
|
220
|
+
application code inside the block would be mislabelled as a missing extra.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
yield
|
|
224
|
+
except ModuleNotFoundError as exc:
|
|
225
|
+
raise RootSignPostgresExtraRequired(f"missing module: {exc.name}") from exc
|
|
226
|
+
|
|
227
|
+
|
|
197
228
|
class HiTLUnsupportedBackendError(RootSignError):
|
|
198
229
|
"""`require_approval=True` used with a backend that can't support it here.
|
|
199
230
|
|
|
@@ -30,6 +30,8 @@ import logging
|
|
|
30
30
|
from typing import TYPE_CHECKING, Any, Callable
|
|
31
31
|
from uuid import UUID
|
|
32
32
|
|
|
33
|
+
from rootsign.errors import postgres_extra_required
|
|
34
|
+
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
36
|
from contextlib import AbstractAsyncContextManager
|
|
35
37
|
|
|
@@ -45,7 +47,8 @@ logger = logging.getLogger("rootsign.mcp.server")
|
|
|
45
47
|
def _default_session_factory() -> Any:
|
|
46
48
|
"""Lazily resolve the app's AsyncSessionLocal so importing this module
|
|
47
49
|
never constructs a DB engine. Only touched when a tool actually runs."""
|
|
48
|
-
|
|
50
|
+
with postgres_extra_required():
|
|
51
|
+
from rootsign.database import AsyncSessionLocal
|
|
49
52
|
|
|
50
53
|
return AsyncSessionLocal()
|
|
51
54
|
|
|
@@ -63,9 +66,10 @@ async def _list_sessions(
|
|
|
63
66
|
limit: int = 20,
|
|
64
67
|
) -> list[dict[str, Any]]:
|
|
65
68
|
"""Most-recent sessions, optionally filtered by agent, newest first."""
|
|
66
|
-
|
|
69
|
+
with postgres_extra_required():
|
|
70
|
+
from sqlalchemy import select
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
from rootsign.models.session import AgentSession
|
|
69
73
|
|
|
70
74
|
async with session_factory() as db:
|
|
71
75
|
stmt = select(AgentSession).order_by(AgentSession.start_time.desc()).limit(limit)
|
|
@@ -92,7 +96,8 @@ async def _query_session_chain(
|
|
|
92
96
|
session_id: str,
|
|
93
97
|
) -> list[dict[str, Any]]:
|
|
94
98
|
"""The ordered Action chain for a session (metadata + hashes, no payloads)."""
|
|
95
|
-
|
|
99
|
+
with postgres_extra_required():
|
|
100
|
+
from rootsign.crud import action as action_crud
|
|
96
101
|
|
|
97
102
|
async with session_factory() as db:
|
|
98
103
|
actions = await action_crud.get_session_chain(db, session_id=UUID(session_id))
|
|
@@ -118,7 +123,8 @@ async def _verify_session_chain(
|
|
|
118
123
|
session_id: str,
|
|
119
124
|
) -> dict[str, Any]:
|
|
120
125
|
"""Recompute + verify the chain — identical result to `rootsign verify`."""
|
|
121
|
-
|
|
126
|
+
with postgres_extra_required():
|
|
127
|
+
from rootsign.crud import action as action_crud
|
|
122
128
|
|
|
123
129
|
async with session_factory() as db:
|
|
124
130
|
return await action_crud.verify_chain(db, session_id=UUID(session_id))
|
|
@@ -130,9 +136,10 @@ async def _get_approval_records(
|
|
|
130
136
|
action_id: str,
|
|
131
137
|
) -> list[dict[str, Any]]:
|
|
132
138
|
"""All approval rows for an action (a list — escalation can yield several)."""
|
|
133
|
-
|
|
139
|
+
with postgres_extra_required():
|
|
140
|
+
from sqlalchemy import select
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
from rootsign.models.approval import Approval
|
|
136
143
|
|
|
137
144
|
async with session_factory() as db:
|
|
138
145
|
stmt = (
|
|
@@ -20,6 +20,8 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
from uuid import UUID
|
|
22
22
|
|
|
23
|
+
from rootsign.errors import postgres_extra_required
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
@dataclass
|
|
25
27
|
class VerifyResult:
|
|
@@ -53,7 +55,8 @@ async def verify_session(session_id: UUID, db: Any) -> VerifyResult:
|
|
|
53
55
|
`db` is an `AsyncSession`. Returns a `VerifyResult`. Never raises on
|
|
54
56
|
chain inconsistency — the result's `valid` field is the verdict.
|
|
55
57
|
"""
|
|
56
|
-
|
|
58
|
+
with postgres_extra_required():
|
|
59
|
+
from rootsign.crud import action as action_crud
|
|
57
60
|
|
|
58
61
|
raw = await action_crud.verify_chain(db, session_id=session_id)
|
|
59
62
|
return VerifyResult(
|
|
@@ -20,15 +20,25 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import asyncio
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Optional
|
|
23
|
+
from typing import Any, Optional
|
|
24
24
|
from uuid import UUID
|
|
25
25
|
|
|
26
26
|
import typer
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
28
|
+
# A test seam, deliberately NOT an import. Tests monkeypatch
|
|
29
|
+
# `rootsign.sdk.cli.AsyncSessionLocal` to bind the CLI to the test engine
|
|
30
|
+
# (tests/integration/test_verify_cli.py, test_approve_cli.py,
|
|
31
|
+
# test_show_hn_quickstart.py), so the name has to exist at module scope.
|
|
32
|
+
#
|
|
33
|
+
# Importing `rootsign.database` here to provide it would drag SQLAlchemy in at
|
|
34
|
+
# module-import time, and since ADR-011 the DB stack lives in the optional
|
|
35
|
+
# `postgres` extra. On a no-extras install that made EVERY `rootsign ...`
|
|
36
|
+
# invocation die with ModuleNotFoundError before Typer could dispatch —
|
|
37
|
+
# including `rootsign version` and `rootsign verify --local`, neither of which
|
|
38
|
+
# touches a database. That violates T1.2 / exit criterion 1.
|
|
39
|
+
#
|
|
40
|
+
# `_session_factory()` resolves the real factory on demand instead.
|
|
41
|
+
AsyncSessionLocal: Any = None
|
|
32
42
|
|
|
33
43
|
app = typer.Typer(
|
|
34
44
|
name="rootsign",
|
|
@@ -38,6 +48,31 @@ app = typer.Typer(
|
|
|
38
48
|
)
|
|
39
49
|
|
|
40
50
|
|
|
51
|
+
def _session_factory() -> Any:
|
|
52
|
+
"""Return the AsyncSession factory, importing the DB stack on demand.
|
|
53
|
+
|
|
54
|
+
Mirrors `LocalIngestClient._factory` (rootsign/sdk/client.py): a missing
|
|
55
|
+
`postgres` extra becomes the actionable install hint rather than a bare
|
|
56
|
+
ModuleNotFoundError from deep in the import graph. Only the DB-backed
|
|
57
|
+
subcommands call this, so DB-free ones stay usable without the extra.
|
|
58
|
+
"""
|
|
59
|
+
if AsyncSessionLocal is not None:
|
|
60
|
+
# Monkeypatched by tests, or already resolved.
|
|
61
|
+
return AsyncSessionLocal
|
|
62
|
+
try:
|
|
63
|
+
from rootsign.database import AsyncSessionLocal as factory
|
|
64
|
+
except ModuleNotFoundError as exc:
|
|
65
|
+
from rootsign.errors import RootSignPostgresExtraRequired
|
|
66
|
+
|
|
67
|
+
# Reuse the canonical error so the CLI hint can never drift from the SDK's.
|
|
68
|
+
typer.echo(
|
|
69
|
+
f"Error: {RootSignPostgresExtraRequired(f'missing module: {exc.name}')}",
|
|
70
|
+
err=True,
|
|
71
|
+
)
|
|
72
|
+
raise typer.Exit(code=1) from exc
|
|
73
|
+
return factory
|
|
74
|
+
|
|
75
|
+
|
|
41
76
|
@app.command()
|
|
42
77
|
def version() -> None:
|
|
43
78
|
"""Print the installed RootSign SDK version."""
|
|
@@ -99,12 +134,17 @@ def _verify_local(path: Path, verbose: bool) -> None:
|
|
|
99
134
|
|
|
100
135
|
|
|
101
136
|
def _verify_remote(session_id: UUID, verbose: bool) -> None:
|
|
102
|
-
#
|
|
103
|
-
# to point at a test
|
|
137
|
+
# The session factory is resolved lazily via `_session_factory()`; tests
|
|
138
|
+
# monkeypatch the module-level `AsyncSessionLocal` seam to point at a test
|
|
139
|
+
# engine. See tests/integration/test_verify_cli.py.
|
|
104
140
|
from rootsign.sdk.chain import verify_session
|
|
105
141
|
|
|
142
|
+
# Resolve the factory up front: a missing `postgres` extra should print the
|
|
143
|
+
# install hint and exit, not surface from inside asyncio.run().
|
|
144
|
+
factory = _session_factory()
|
|
145
|
+
|
|
106
146
|
async def _run():
|
|
107
|
-
async with
|
|
147
|
+
async with factory() as db:
|
|
108
148
|
return await verify_session(session_id, db)
|
|
109
149
|
|
|
110
150
|
result = asyncio.run(_run())
|
|
@@ -204,12 +244,16 @@ def _list_pending_approvals() -> None:
|
|
|
204
244
|
Phase 2 will add `--session` and `--owner` filters once multi-tenant
|
|
205
245
|
auth ships.
|
|
206
246
|
"""
|
|
247
|
+
# Before any DB import — otherwise a no-extras install tracebacks on
|
|
248
|
+
# `from sqlalchemy import select` instead of getting the install hint.
|
|
249
|
+
factory = _session_factory()
|
|
250
|
+
|
|
207
251
|
from sqlalchemy import select
|
|
208
252
|
|
|
209
253
|
from rootsign.models.action import Action
|
|
210
254
|
|
|
211
255
|
async def _run() -> list[Action]:
|
|
212
|
-
async with
|
|
256
|
+
async with factory() as db:
|
|
213
257
|
result = await db.execute(
|
|
214
258
|
select(Action)
|
|
215
259
|
.where(Action.authorization_status == "pending")
|
|
@@ -241,6 +285,9 @@ def _submit_approval(*, action_id: UUID, decision: str, reason: str | None) -> N
|
|
|
241
285
|
"""
|
|
242
286
|
import getpass
|
|
243
287
|
|
|
288
|
+
# Before any DB import — see _list_pending_approvals.
|
|
289
|
+
factory = _session_factory()
|
|
290
|
+
|
|
244
291
|
from sqlalchemy import select
|
|
245
292
|
|
|
246
293
|
from rootsign.crud.approval import approval as approval_crud
|
|
@@ -256,7 +303,7 @@ def _submit_approval(*, action_id: UUID, decision: str, reason: str | None) -> N
|
|
|
256
303
|
approver_id = "cli:operator"
|
|
257
304
|
|
|
258
305
|
async def _run() -> None:
|
|
259
|
-
async with
|
|
306
|
+
async with factory() as db:
|
|
260
307
|
result = await db.execute(
|
|
261
308
|
select(Action).where(
|
|
262
309
|
Action.action_id == action_id,
|
|
@@ -141,12 +141,10 @@ class ManagedLocalIngestClient(IngestClient):
|
|
|
141
141
|
return self._session_factory
|
|
142
142
|
# Lazy — the DB stack is the postgres extra (ADR-011). Translate a
|
|
143
143
|
# missing extra into the actionable error, same as get_ingest_client.
|
|
144
|
-
|
|
145
|
-
from rootsign.database import AsyncSessionLocal
|
|
146
|
-
except ModuleNotFoundError as exc:
|
|
147
|
-
from rootsign.errors import RootSignPostgresExtraRequired
|
|
144
|
+
from rootsign.errors import postgres_extra_required
|
|
148
145
|
|
|
149
|
-
|
|
146
|
+
with postgres_extra_required():
|
|
147
|
+
from rootsign.database import AsyncSessionLocal
|
|
150
148
|
self._session_factory = AsyncSessionLocal
|
|
151
149
|
return self._session_factory
|
|
152
150
|
|