crprotocol 2.2.0__tar.gz → 2.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.
- {crprotocol-2.2.0 → crprotocol-2.3.0}/PKG-INFO +2 -2
- {crprotocol-2.2.0 → crprotocol-2.3.0}/README.md +1 -1
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/__init__.py +24 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/_version.py +1 -1
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_enforcer.py +156 -6
- crprotocol-2.3.0/crp/core/ledger_backends.py +186 -0
- crprotocol-2.3.0/crp/core/manifest_derive.py +243 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/manifest_ledger.py +145 -5
- crprotocol-2.3.0/crp/integrations/__init__.py +42 -0
- crprotocol-2.3.0/crp/integrations/_common.py +66 -0
- crprotocol-2.3.0/crp/integrations/anthropic_hook.py +92 -0
- crprotocol-2.3.0/crp/integrations/langchain_hook.py +118 -0
- crprotocol-2.3.0/crp/integrations/openai_hook.py +171 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/openai.py +10 -4
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/context-sources.md +139 -8
- crprotocol-2.3.0/tests/test_gaps_2_3.py +478 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_smoke.py +1 -1
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.dockerignore +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/CODEOWNERS +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/FUNDING.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/spec-clarification.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/ci.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/docs.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/link-check-config.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/link-check.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/validate-schemas.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.gitignore +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/.pre-commit-config.yaml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/BENCHMARKS.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/CHANGELOG.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/CONTRIBUTING.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/CRP_CAPABILITIES.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/Dockerfile +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/GOVERNANCE.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/HOSTING_POSITIONING.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/INTERNAL_DOCS.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/LICENSE.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/NOTICE +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/RAILWAY_DEPLOYMENT_GUIDE.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/RAILWAY_VARIABLES.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/SECURITY.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/SITE_NAVIGATION_AND_PUBLISHING.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/STRIPE_MONETISATION.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/TRADEMARK.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/__main__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/_typing.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/adapters.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/auto_ingest.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/cqs.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/cross_window.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/curator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/feedback.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/hierarchical.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/meta_learning.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/parallel.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/review_cycle.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/scale_mode.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/source_grounding.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/community.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/fabric.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/gc.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/graph_walk.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/merge.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/pattern_query.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/pubsub.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/semantic.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/main.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/sidecar.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/startup.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/completion.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/degradation.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/document_map.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/flow.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/gap.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/quality_monitor.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/stitch.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/trigger.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/voice.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/batch.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/circuit_breaker.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/config.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_source.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_tools.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/dispatch_router.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/errors.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/extraction_facade.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/facilitator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/idempotency.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/orchestrator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/relay_strategies.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/security_manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/session.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/task_intent.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/window.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/builder.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/decomposer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/formatter.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/packer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/reranker.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/scoring.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/complexity.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/contradiction.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/pipeline.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/quality_gate.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage1_regex.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage2_statistical.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage3_gliner.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage4_uie.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage5_discourse.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage6_llm.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/structured_output.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/types.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/license_guard.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/audit.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/events.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/metrics.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/quality.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/structured_logging.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/telemetry.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/_embeddings.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/_types.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/attribution_scorer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/claim_detector.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/contradiction_detector.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/distortion_detector.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/entailment_verifier.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/fabrication_detector.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/hallucination_scorer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/omission_analyzer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/provenance_chain.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/report_generator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/anthropic.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/base.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/custom.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/diagnostic.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/llamacpp.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/ollama.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/tokenizers.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/py.typed +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/adaptive_allocator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/cost_model.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/overhead_manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/resource_manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/cost-estimate.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/crp-error.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/envelope-preview.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/persisted-state-header.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/quality-report.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/session-handle.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/session-status.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/stream-event.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/task-intent.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/audit_trail.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/binding.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/compliance.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/consent.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/embedding_defense.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/encryption.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/injection.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/integrity.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/privacy.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/quarantine.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/rbac.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/validation.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/__init__.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/cold_storage.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/compaction.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/critical_state.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/event_log.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/fact.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/serialization.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/session_cleanup.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/snapshot.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/warm_store.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/docs/OPERATIONS_RUNBOOK.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/docs/WASA_INTEGRATION_TUTORIAL.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/async_usage.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/benchmark_continuation.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/choose_provider.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/comply_demo.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/README.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/demo.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/demo_v1.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/extraction-pipeline.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/ingestion.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/local-model.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/multi-provider.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/multi_turn.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/quickstart.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/quickstart.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/scribe_demo.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/session-resumption.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/streaming.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/media/logo.svg +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/mkdocs.yml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/pyproject.toml +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/rfcs/0000-template.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/rfcs/0001-initial-release.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/cost-estimate.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/crp-error.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/envelope-preview.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/openapi.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/persisted-state-header.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/quality-report.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/session-handle.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/session-status.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/stream-event.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/task-intent.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/scripts/gen_changelog.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/CNAME +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/client.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/compliance.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/dispatch.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/schemas.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/benchmarks.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/eu-ai-act.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/gdpr.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/iso-42001.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/nist-ai-rmf.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/security.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/contributing.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/cli.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/installation.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/licensing.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/local-models.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/providers.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/quickstart.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/demo-app.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/ingestion.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/multi-turn.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/session-persistence.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/sidecar.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/streaming.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/comply-autocyber.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/comply.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/scribe.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/ckf.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/continuation.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/core.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/dispatch-strategies.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/envelope.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/extraction.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/meta-learning.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/provenance.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/quality-tiers.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/research.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/terms-of-service.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/benchmarks.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/index.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/reproduce.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/running-tests.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/why-crp.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/01_RESEARCH_FOUNDATIONS.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/02_CORE_PROTOCOL.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/03_CONTEXT_ENVELOPE.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/04_TOKEN_GENERATION_PROTOCOL.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/05_SYSTEM_WIDE_INTEGRATION.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/06_IMPLEMENTATION_PLAN.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/07_SECURITY.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/08_MONETIZATION.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/09_DEPLOYMENT.md +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/conftest.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_report.json +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_report.txt +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_test.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/debug_gap.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/debug_gap2.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_adaptive_allocator.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_adversarial_provenance.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_agentic.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_benchmarks.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_ckf_gate.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_compliance_security.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_compliance_wiring.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_context_enforcer.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_context_source.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_decision_provenance.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_decision_provenance_engine.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_entailment_risk.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_fidelity_verification.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_gap_fixes_live.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_integration.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_ip_protection.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_comprehensive.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_full_capture.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_long_generation.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_verification.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_manifest_ledger.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase1.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase2.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase3.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase4.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase5.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase6.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase7.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase8.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase9.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_production_hardening.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_relay_strategies.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_resource_manager.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_security_modules.py +0 -0
- {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_tool_relay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crprotocol
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Context Relay Protocol — unbounded context, unbounded generation, amplified reasoning for LLMs
|
|
5
5
|
Project-URL: Homepage, https://crprotocol.io
|
|
6
6
|
Project-URL: Documentation, https://crprotocol.io
|
|
@@ -73,7 +73,7 @@ Description-Content-Type: text/markdown
|
|
|
73
73
|
<p align="center">
|
|
74
74
|
<a href="LICENSE.md"><img src="https://img.shields.io/badge/Spec-CC_BY--SA_4.0-blue.svg" alt="Spec: CC BY-SA 4.0"></a>
|
|
75
75
|
<a href="LICENSE.md"><img src="https://img.shields.io/badge/Code-ELv2-orange.svg" alt="Code: Elastic License 2.0"></a>
|
|
76
|
-
<img src="https://img.shields.io/badge/Spec_Version-2.
|
|
76
|
+
<img src="https://img.shields.io/badge/Spec_Version-2.2.0-brightgreen.svg" alt="Spec Version: 2.2.0">
|
|
77
77
|
<img src="https://img.shields.io/badge/RFC_2119-Conformant-orange.svg" alt="RFC 2119">
|
|
78
78
|
<img src="https://img.shields.io/badge/Language_Neutral-JSON_Schema-yellow.svg" alt="Language Neutral">
|
|
79
79
|
<img src="https://img.shields.io/badge/Status-Specification_Complete-green.svg" alt="Status: Specification Complete">
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="LICENSE.md"><img src="https://img.shields.io/badge/Spec-CC_BY--SA_4.0-blue.svg" alt="Spec: CC BY-SA 4.0"></a>
|
|
18
18
|
<a href="LICENSE.md"><img src="https://img.shields.io/badge/Code-ELv2-orange.svg" alt="Code: Elastic License 2.0"></a>
|
|
19
|
-
<img src="https://img.shields.io/badge/Spec_Version-2.
|
|
19
|
+
<img src="https://img.shields.io/badge/Spec_Version-2.2.0-brightgreen.svg" alt="Spec Version: 2.2.0">
|
|
20
20
|
<img src="https://img.shields.io/badge/RFC_2119-Conformant-orange.svg" alt="RFC 2119">
|
|
21
21
|
<img src="https://img.shields.io/badge/Language_Neutral-JSON_Schema-yellow.svg" alt="Language Neutral">
|
|
22
22
|
<img src="https://img.shields.io/badge/Status-Specification_Complete-green.svg" alt="Status: Specification Complete">
|
|
@@ -50,10 +50,23 @@ from crp.core.context_enforcer import (
|
|
|
50
50
|
from crp.core.manifest_ledger import (
|
|
51
51
|
EnvVarKeyProvider,
|
|
52
52
|
KeyProvider,
|
|
53
|
+
LedgerChainError,
|
|
53
54
|
ManifestLedger,
|
|
54
55
|
ManifestLedgerEntry,
|
|
55
56
|
RotatingKeyProvider,
|
|
56
57
|
)
|
|
58
|
+
from crp.core.ledger_backends import (
|
|
59
|
+
AsyncBufferedSink,
|
|
60
|
+
HTTPForwardingSink,
|
|
61
|
+
JSONLinesFileSink,
|
|
62
|
+
NullSink,
|
|
63
|
+
)
|
|
64
|
+
from crp.core.manifest_derive import (
|
|
65
|
+
content_hash,
|
|
66
|
+
derive_manifest_from_messages,
|
|
67
|
+
derive_source_from_message,
|
|
68
|
+
derive_sources_from_messages,
|
|
69
|
+
)
|
|
57
70
|
from crp.core.errors import (
|
|
58
71
|
CRPError,
|
|
59
72
|
BudgetExhaustedError,
|
|
@@ -140,9 +153,20 @@ __all__ = [
|
|
|
140
153
|
"set_default_enforcer",
|
|
141
154
|
"ManifestLedger",
|
|
142
155
|
"ManifestLedgerEntry",
|
|
156
|
+
"LedgerChainError",
|
|
143
157
|
"KeyProvider",
|
|
144
158
|
"EnvVarKeyProvider",
|
|
145
159
|
"RotatingKeyProvider",
|
|
160
|
+
# Ledger forwarding sinks (CRP 2.3, §7.14.5)
|
|
161
|
+
"JSONLinesFileSink",
|
|
162
|
+
"HTTPForwardingSink",
|
|
163
|
+
"AsyncBufferedSink",
|
|
164
|
+
"NullSink",
|
|
165
|
+
# Derived manifests (CRP 2.3, §7.14.6)
|
|
166
|
+
"content_hash",
|
|
167
|
+
"derive_source_from_message",
|
|
168
|
+
"derive_sources_from_messages",
|
|
169
|
+
"derive_manifest_from_messages",
|
|
146
170
|
# Error types (§audit L2)
|
|
147
171
|
"CRPError",
|
|
148
172
|
"ErrorCode",
|
|
@@ -47,6 +47,7 @@ from .context_source import (
|
|
|
47
47
|
ContextSource,
|
|
48
48
|
ManifestValidationError,
|
|
49
49
|
SourceKind,
|
|
50
|
+
SourceOrigin,
|
|
50
51
|
TrustLevel,
|
|
51
52
|
check_attestation,
|
|
52
53
|
)
|
|
@@ -572,6 +573,87 @@ class ContextEnforcer:
|
|
|
572
573
|
except (ValueError, ManifestValidationError) as exc:
|
|
573
574
|
logger.warning("CRP ledger record failed: %s", exc)
|
|
574
575
|
|
|
576
|
+
# ------------------------------------------------------------ per-turn
|
|
577
|
+
|
|
578
|
+
def check_messages(
|
|
579
|
+
self,
|
|
580
|
+
messages: Sequence[dict[str, Any]],
|
|
581
|
+
manifest: ContextManifest | None = None,
|
|
582
|
+
*,
|
|
583
|
+
session_id: str | None = None,
|
|
584
|
+
derive_manifest: bool = False,
|
|
585
|
+
emit: bool = True,
|
|
586
|
+
) -> EnforcementResult:
|
|
587
|
+
"""Re-run the enforcement pipeline against a full chat history (CRP 2.3).
|
|
588
|
+
|
|
589
|
+
For every message the enforcer derives a :class:`ContextSource`
|
|
590
|
+
(system/user/assistant/tool → kind+trust) and an observed-content
|
|
591
|
+
bundle, so both *attestation* and *injection* scans run over the
|
|
592
|
+
actual bytes that will be sent to the model. Tool-call loops
|
|
593
|
+
benefit most: ``tool_result`` payloads are re-validated every
|
|
594
|
+
turn instead of only at first assembly.
|
|
595
|
+
|
|
596
|
+
When ``derive_manifest=True`` and *manifest* is ``None``, a fresh
|
|
597
|
+
manifest is derived from the messages themselves (unsigned, one
|
|
598
|
+
source per message). This is the "belt-and-braces" path for
|
|
599
|
+
integrators who want a per-turn audit record even if they never
|
|
600
|
+
authored a manifest.
|
|
601
|
+
"""
|
|
602
|
+
from .manifest_derive import derive_sources_from_messages, derive_manifest_from_messages
|
|
603
|
+
|
|
604
|
+
sid = session_id or self._session_id
|
|
605
|
+
effective_manifest = manifest
|
|
606
|
+
if effective_manifest is None and derive_manifest:
|
|
607
|
+
effective_manifest = derive_manifest_from_messages(
|
|
608
|
+
messages, session_id=sid
|
|
609
|
+
)
|
|
610
|
+
bundles: list[_ObservedContent] = []
|
|
611
|
+
for source, text in derive_sources_from_messages(
|
|
612
|
+
messages, session_id=sid
|
|
613
|
+
):
|
|
614
|
+
bundles.append(_ObservedContent(source=source, content=text))
|
|
615
|
+
return self.check(effective_manifest, bundles, emit=emit)
|
|
616
|
+
|
|
617
|
+
def check_tool_result(
|
|
618
|
+
self,
|
|
619
|
+
content: str | dict[str, Any] | list[Any],
|
|
620
|
+
*,
|
|
621
|
+
source: ContextSource | None = None,
|
|
622
|
+
manifest: ContextManifest | None = None,
|
|
623
|
+
tool_call_id: str | None = None,
|
|
624
|
+
tool_name: str | None = None,
|
|
625
|
+
emit: bool = True,
|
|
626
|
+
) -> EnforcementResult:
|
|
627
|
+
"""Validate a single tool-call / function-call result (CRP 2.3).
|
|
628
|
+
|
|
629
|
+
Runs the same pipeline as :meth:`check` but bounded to one
|
|
630
|
+
payload, so agent loops can re-validate each tool response
|
|
631
|
+
individually. If *source* is not supplied, a derived
|
|
632
|
+
``FUNCTION_CALL`` source is constructed with an ``UNKNOWN`` trust
|
|
633
|
+
level (tool outputs are never implicitly trusted).
|
|
634
|
+
"""
|
|
635
|
+
from .manifest_derive import _flatten_content, content_hash
|
|
636
|
+
|
|
637
|
+
text = _flatten_content(content)
|
|
638
|
+
if source is None:
|
|
639
|
+
metadata: dict[str, Any] = {
|
|
640
|
+
"content_sha256": content_hash(text) if text else "sha256:",
|
|
641
|
+
"content_len": len(text),
|
|
642
|
+
}
|
|
643
|
+
if tool_call_id:
|
|
644
|
+
metadata["tool_call_id"] = str(tool_call_id)[:128]
|
|
645
|
+
if tool_name:
|
|
646
|
+
metadata["tool_name"] = str(tool_name)[:128]
|
|
647
|
+
source = ContextSource(
|
|
648
|
+
kind=SourceKind.FUNCTION_CALL,
|
|
649
|
+
source_id=f"tool_result:{tool_call_id or tool_name or 'anon'}",
|
|
650
|
+
origin=SourceOrigin.OBSERVED,
|
|
651
|
+
trust_level=TrustLevel.UNKNOWN,
|
|
652
|
+
retrieved_at=time.time(),
|
|
653
|
+
metadata=metadata,
|
|
654
|
+
)
|
|
655
|
+
return self.check(manifest, [_ObservedContent(source=source, content=text)], emit=emit)
|
|
656
|
+
|
|
575
657
|
|
|
576
658
|
# ---------------------------------------------------------------------------
|
|
577
659
|
# Convenience: observed-content bundle factory
|
|
@@ -591,29 +673,97 @@ __all__.append("observed_content")
|
|
|
591
673
|
|
|
592
674
|
|
|
593
675
|
# ---------------------------------------------------------------------------
|
|
594
|
-
# Process-wide default (opt-in)
|
|
676
|
+
# Process-wide default (opt-in + env-var safety net, CRP 2.3)
|
|
595
677
|
# ---------------------------------------------------------------------------
|
|
596
678
|
|
|
597
679
|
|
|
598
680
|
_DEFAULT: ContextEnforcer | None = None
|
|
599
681
|
_DEFAULT_LOCK = threading.Lock()
|
|
682
|
+
_ENV_AUTO_INSTALL_DONE = False
|
|
683
|
+
_ENV_VAR = "CRP_DEFAULT_ENFORCEMENT"
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _maybe_install_from_env() -> None:
|
|
687
|
+
"""Auto-install a default enforcer if ``CRP_DEFAULT_ENFORCEMENT`` is set.
|
|
688
|
+
|
|
689
|
+
Runs at most once per process. Accepted values (case-insensitive):
|
|
690
|
+
``observe``, ``warn``, ``reject``, ``off``. Any other value is logged
|
|
691
|
+
and ignored. When ``CRP_MANIFEST_SECRET`` is present, an
|
|
692
|
+
:class:`~crp.core.manifest_ledger.EnvVarKeyProvider` is attached
|
|
693
|
+
automatically so signature verification works without further wiring.
|
|
694
|
+
|
|
695
|
+
This closes the footgun where an integrator forgets to call
|
|
696
|
+
:func:`set_default_enforcer` — operations teams can flip the env var
|
|
697
|
+
to ``observe`` in production and get an audit trail immediately.
|
|
698
|
+
"""
|
|
699
|
+
global _ENV_AUTO_INSTALL_DONE, _DEFAULT
|
|
700
|
+
with _DEFAULT_LOCK:
|
|
701
|
+
if _ENV_AUTO_INSTALL_DONE:
|
|
702
|
+
return
|
|
703
|
+
_ENV_AUTO_INSTALL_DONE = True
|
|
704
|
+
if _DEFAULT is not None:
|
|
705
|
+
return # Explicit opt-in wins — don't clobber.
|
|
706
|
+
import os as _os
|
|
707
|
+
raw = _os.environ.get(_ENV_VAR, "").strip().lower()
|
|
708
|
+
if raw in ("", "off", "none", "disabled", "0", "false"):
|
|
709
|
+
return
|
|
710
|
+
try:
|
|
711
|
+
policy = EnforcementPolicy(raw)
|
|
712
|
+
except ValueError:
|
|
713
|
+
logger.warning(
|
|
714
|
+
"CRP: %s=%r is invalid; expected observe/warn/reject/off",
|
|
715
|
+
_ENV_VAR,
|
|
716
|
+
raw,
|
|
717
|
+
)
|
|
718
|
+
return
|
|
719
|
+
key_provider: Any = None
|
|
720
|
+
if _os.environ.get("CRP_MANIFEST_SECRET"):
|
|
721
|
+
try:
|
|
722
|
+
from crp.core.manifest_ledger import EnvVarKeyProvider
|
|
723
|
+
key_provider = EnvVarKeyProvider()
|
|
724
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
725
|
+
logger.warning(
|
|
726
|
+
"CRP: failed to build EnvVarKeyProvider from env: %s", exc
|
|
727
|
+
)
|
|
728
|
+
_DEFAULT = ContextEnforcer(policy=policy, key_provider=key_provider)
|
|
729
|
+
logger.info(
|
|
730
|
+
"CRP: installed default enforcer policy=%s from %s",
|
|
731
|
+
policy.value,
|
|
732
|
+
_ENV_VAR,
|
|
733
|
+
)
|
|
600
734
|
|
|
601
735
|
|
|
602
736
|
def default_enforcer() -> ContextEnforcer | None:
|
|
603
737
|
"""Return the installed process-wide enforcer, or ``None``.
|
|
604
738
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
739
|
+
On first call, honours the ``CRP_DEFAULT_ENFORCEMENT`` environment
|
|
740
|
+
variable (``observe|warn|reject|off``) as a safety net when
|
|
741
|
+
:func:`set_default_enforcer` was never called. Explicit calls to
|
|
742
|
+
:func:`set_default_enforcer` always take precedence — env auto-install
|
|
743
|
+
only fires when no enforcer has been installed yet.
|
|
608
744
|
"""
|
|
745
|
+
_maybe_install_from_env()
|
|
609
746
|
with _DEFAULT_LOCK:
|
|
610
747
|
return _DEFAULT
|
|
611
748
|
|
|
612
749
|
|
|
613
750
|
def set_default_enforcer(enforcer: ContextEnforcer | None) -> ContextEnforcer | None:
|
|
614
|
-
"""Install *enforcer* as the process-wide default. Returns the previous one.
|
|
615
|
-
|
|
751
|
+
"""Install *enforcer* as the process-wide default. Returns the previous one.
|
|
752
|
+
|
|
753
|
+
Marks env auto-install as "done" so a later ``default_enforcer()`` call
|
|
754
|
+
does not clobber an explicit ``None``.
|
|
755
|
+
"""
|
|
756
|
+
global _DEFAULT, _ENV_AUTO_INSTALL_DONE
|
|
616
757
|
with _DEFAULT_LOCK:
|
|
617
758
|
old = _DEFAULT
|
|
618
759
|
_DEFAULT = enforcer
|
|
760
|
+
_ENV_AUTO_INSTALL_DONE = True
|
|
619
761
|
return old
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _reset_default_enforcer_for_tests() -> None:
|
|
765
|
+
"""Testing hook: clear the default + re-arm env auto-install."""
|
|
766
|
+
global _DEFAULT, _ENV_AUTO_INSTALL_DONE
|
|
767
|
+
with _DEFAULT_LOCK:
|
|
768
|
+
_DEFAULT = None
|
|
769
|
+
_ENV_AUTO_INSTALL_DONE = False
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Copyright © 2025-2026 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Ledger forwarding sinks (§7.14.5, CRP 2.3).
|
|
4
|
+
|
|
5
|
+
CRP 2.2's :class:`~crp.core.manifest_ledger.ManifestLedger` wrote JSONL
|
|
6
|
+
files to the local filesystem. That is sufficient for a single node but
|
|
7
|
+
fails in two real operational settings:
|
|
8
|
+
|
|
9
|
+
* **Distributed workers** — multiple replicas need their ledger entries
|
|
10
|
+
aggregated in a central store (SIEM, SQL, data-lake) so auditors see a
|
|
11
|
+
single timeline.
|
|
12
|
+
* **Tamper-evidence in depth** — the CRP 2.3 hash-chain catches edits on
|
|
13
|
+
the local JSONL file, but a root-level attacker could still delete the
|
|
14
|
+
file. Forwarding every record to an append-only external system closes
|
|
15
|
+
that hole.
|
|
16
|
+
|
|
17
|
+
These sinks implement the existing
|
|
18
|
+
:class:`~crp.core.context_enforcer.AuditSink` protocol (``emit(event)``)
|
|
19
|
+
so the same wiring serves enforcement events *and* ledger writes. Pass
|
|
20
|
+
the sinks in via ``ManifestLedger(forward_to=[...])``.
|
|
21
|
+
|
|
22
|
+
All sinks here are pure-stdlib. Production deployments should use HTTPS
|
|
23
|
+
with rotating tokens and rely on the SIEM's native replay/retention.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import queue
|
|
32
|
+
import threading
|
|
33
|
+
import time
|
|
34
|
+
import urllib.error
|
|
35
|
+
import urllib.request
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Iterable
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"JSONLinesFileSink",
|
|
41
|
+
"HTTPForwardingSink",
|
|
42
|
+
"AsyncBufferedSink",
|
|
43
|
+
"NullSink",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_log = logging.getLogger("crp.ledger_backends")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NullSink:
|
|
51
|
+
"""No-op sink. Useful when a configuration requires a sink object but
|
|
52
|
+
nothing should be forwarded (e.g. in tests)."""
|
|
53
|
+
|
|
54
|
+
def emit(self, event: dict[str, Any]) -> None: # noqa: D401
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class JSONLinesFileSink:
|
|
59
|
+
"""Append every event as one JSON object per line to a local file.
|
|
60
|
+
|
|
61
|
+
Thread-safe: all writes are serialised on an internal lock. Intended
|
|
62
|
+
for replicating ledger records to a location monitored by Splunk
|
|
63
|
+
Universal Forwarder, Vector, Fluentd, etc.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, path: str | os.PathLike[str]) -> None:
|
|
67
|
+
self._path = Path(path)
|
|
68
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
self._lock = threading.Lock()
|
|
70
|
+
|
|
71
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
72
|
+
line = json.dumps(event, separators=(",", ":"), sort_keys=True, default=str)
|
|
73
|
+
with self._lock:
|
|
74
|
+
with self._path.open("a", encoding="utf-8") as fp:
|
|
75
|
+
fp.write(line)
|
|
76
|
+
fp.write("\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HTTPForwardingSink:
|
|
80
|
+
"""POST every event as JSON to an HTTPS endpoint.
|
|
81
|
+
|
|
82
|
+
Intended for Splunk HEC, Elastic Common Schema ingest, Datadog Logs
|
|
83
|
+
HTTP intake, Azure Monitor, AWS CloudWatch Logs (via signed gateway),
|
|
84
|
+
or a bespoke SIEM collector.
|
|
85
|
+
|
|
86
|
+
Failures are logged at WARNING; the sink never raises out to the
|
|
87
|
+
caller. Callers that need guaranteed delivery should wrap this in
|
|
88
|
+
:class:`AsyncBufferedSink` and pair it with a retry layer.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
headers: dict[str, str] | None = None,
|
|
96
|
+
timeout: float = 3.0,
|
|
97
|
+
) -> None:
|
|
98
|
+
if not url or not url.startswith(("http://", "https://")):
|
|
99
|
+
raise ValueError("HTTPForwardingSink: url must be http:// or https://")
|
|
100
|
+
self._url = url
|
|
101
|
+
self._headers = {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"User-Agent": "crp-ledger-forwarder/1.0",
|
|
104
|
+
}
|
|
105
|
+
if headers:
|
|
106
|
+
self._headers.update(headers)
|
|
107
|
+
self._timeout = float(timeout)
|
|
108
|
+
|
|
109
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
110
|
+
body = json.dumps(event, separators=(",", ":"), default=str).encode("utf-8")
|
|
111
|
+
req = urllib.request.Request(
|
|
112
|
+
self._url, data=body, headers=self._headers, method="POST"
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
116
|
+
if resp.status >= 400:
|
|
117
|
+
_log.warning(
|
|
118
|
+
"CRP ledger HTTP forward non-2xx: %d %s",
|
|
119
|
+
resp.status,
|
|
120
|
+
self._url,
|
|
121
|
+
)
|
|
122
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
123
|
+
_log.warning("CRP ledger HTTP forward failed: %s (%s)", self._url, exc)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AsyncBufferedSink:
|
|
127
|
+
"""Background-thread buffer wrapping a downstream sink.
|
|
128
|
+
|
|
129
|
+
Enqueues events in memory (bounded; default 10k) and drains them on a
|
|
130
|
+
single worker thread. Keeps ledger writes off the hot path when the
|
|
131
|
+
downstream sink is slow or flaky.
|
|
132
|
+
|
|
133
|
+
Call :meth:`close` on shutdown to flush. Events that cannot be
|
|
134
|
+
enqueued (queue full) are dropped and logged — operators should size
|
|
135
|
+
the queue for their write rate.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
target: Any,
|
|
141
|
+
*,
|
|
142
|
+
max_queue: int = 10_000,
|
|
143
|
+
drain_on_close: bool = True,
|
|
144
|
+
) -> None:
|
|
145
|
+
if not hasattr(target, "emit"):
|
|
146
|
+
raise TypeError("AsyncBufferedSink target must implement .emit()")
|
|
147
|
+
self._target = target
|
|
148
|
+
self._q: queue.Queue[dict[str, Any] | None] = queue.Queue(maxsize=max_queue)
|
|
149
|
+
self._drain_on_close = drain_on_close
|
|
150
|
+
self._stopped = threading.Event()
|
|
151
|
+
self._worker = threading.Thread(
|
|
152
|
+
target=self._run, name="crp-ledger-sink", daemon=True
|
|
153
|
+
)
|
|
154
|
+
self._worker.start()
|
|
155
|
+
|
|
156
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
157
|
+
if self._stopped.is_set():
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
self._q.put_nowait(dict(event))
|
|
161
|
+
except queue.Full:
|
|
162
|
+
_log.warning("CRP ledger async sink full; dropping event")
|
|
163
|
+
|
|
164
|
+
def _run(self) -> None:
|
|
165
|
+
while True:
|
|
166
|
+
item = self._q.get()
|
|
167
|
+
if item is None:
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
self._target.emit(item)
|
|
171
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
172
|
+
_log.warning("CRP ledger async target raised: %s", exc)
|
|
173
|
+
finally:
|
|
174
|
+
self._q.task_done()
|
|
175
|
+
|
|
176
|
+
def close(self, timeout: float = 5.0) -> None:
|
|
177
|
+
"""Signal shutdown and (optionally) wait for the queue to drain."""
|
|
178
|
+
if self._stopped.is_set():
|
|
179
|
+
return
|
|
180
|
+
self._stopped.set()
|
|
181
|
+
if self._drain_on_close:
|
|
182
|
+
deadline = time.monotonic() + timeout
|
|
183
|
+
while not self._q.empty() and time.monotonic() < deadline:
|
|
184
|
+
time.sleep(0.01)
|
|
185
|
+
self._q.put(None)
|
|
186
|
+
self._worker.join(timeout=timeout)
|