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.
Files changed (328) hide show
  1. {crprotocol-2.2.0 → crprotocol-2.3.0}/PKG-INFO +2 -2
  2. {crprotocol-2.2.0 → crprotocol-2.3.0}/README.md +1 -1
  3. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/__init__.py +24 -0
  4. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/_version.py +1 -1
  5. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_enforcer.py +156 -6
  6. crprotocol-2.3.0/crp/core/ledger_backends.py +186 -0
  7. crprotocol-2.3.0/crp/core/manifest_derive.py +243 -0
  8. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/manifest_ledger.py +145 -5
  9. crprotocol-2.3.0/crp/integrations/__init__.py +42 -0
  10. crprotocol-2.3.0/crp/integrations/_common.py +66 -0
  11. crprotocol-2.3.0/crp/integrations/anthropic_hook.py +92 -0
  12. crprotocol-2.3.0/crp/integrations/langchain_hook.py +118 -0
  13. crprotocol-2.3.0/crp/integrations/openai_hook.py +171 -0
  14. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/openai.py +10 -4
  15. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/context-sources.md +139 -8
  16. crprotocol-2.3.0/tests/test_gaps_2_3.py +478 -0
  17. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_smoke.py +1 -1
  18. {crprotocol-2.2.0 → crprotocol-2.3.0}/.dockerignore +0 -0
  19. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/CODEOWNERS +0 -0
  20. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/FUNDING.yml +0 -0
  21. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  22. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  23. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/ISSUE_TEMPLATE/spec-clarification.yml +0 -0
  24. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/ci.yml +0 -0
  26. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/docs.yml +0 -0
  27. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/link-check-config.json +0 -0
  28. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/link-check.yml +0 -0
  29. {crprotocol-2.2.0 → crprotocol-2.3.0}/.github/workflows/validate-schemas.yml +0 -0
  30. {crprotocol-2.2.0 → crprotocol-2.3.0}/.gitignore +0 -0
  31. {crprotocol-2.2.0 → crprotocol-2.3.0}/.pre-commit-config.yaml +0 -0
  32. {crprotocol-2.2.0 → crprotocol-2.3.0}/BENCHMARKS.md +0 -0
  33. {crprotocol-2.2.0 → crprotocol-2.3.0}/CHANGELOG.md +0 -0
  34. {crprotocol-2.2.0 → crprotocol-2.3.0}/CODE_OF_CONDUCT.md +0 -0
  35. {crprotocol-2.2.0 → crprotocol-2.3.0}/CONTRIBUTING.md +0 -0
  36. {crprotocol-2.2.0 → crprotocol-2.3.0}/CRP_CAPABILITIES.md +0 -0
  37. {crprotocol-2.2.0 → crprotocol-2.3.0}/Dockerfile +0 -0
  38. {crprotocol-2.2.0 → crprotocol-2.3.0}/GOVERNANCE.md +0 -0
  39. {crprotocol-2.2.0 → crprotocol-2.3.0}/HOSTING_POSITIONING.md +0 -0
  40. {crprotocol-2.2.0 → crprotocol-2.3.0}/INTERNAL_DOCS.md +0 -0
  41. {crprotocol-2.2.0 → crprotocol-2.3.0}/LICENSE.md +0 -0
  42. {crprotocol-2.2.0 → crprotocol-2.3.0}/NOTICE +0 -0
  43. {crprotocol-2.2.0 → crprotocol-2.3.0}/RAILWAY_DEPLOYMENT_GUIDE.md +0 -0
  44. {crprotocol-2.2.0 → crprotocol-2.3.0}/RAILWAY_VARIABLES.md +0 -0
  45. {crprotocol-2.2.0 → crprotocol-2.3.0}/SECURITY.md +0 -0
  46. {crprotocol-2.2.0 → crprotocol-2.3.0}/SITE_NAVIGATION_AND_PUBLISHING.md +0 -0
  47. {crprotocol-2.2.0 → crprotocol-2.3.0}/STRIPE_MONETISATION.md +0 -0
  48. {crprotocol-2.2.0 → crprotocol-2.3.0}/TRADEMARK.md +0 -0
  49. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/__main__.py +0 -0
  50. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/_typing.py +0 -0
  51. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/adapters.py +0 -0
  52. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/__init__.py +0 -0
  53. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/auto_ingest.py +0 -0
  54. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/cqs.py +0 -0
  55. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/cross_window.py +0 -0
  56. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/curator.py +0 -0
  57. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/feedback.py +0 -0
  58. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/hierarchical.py +0 -0
  59. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/meta_learning.py +0 -0
  60. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/parallel.py +0 -0
  61. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/review_cycle.py +0 -0
  62. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/scale_mode.py +0 -0
  63. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/advanced/source_grounding.py +0 -0
  64. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/__init__.py +0 -0
  65. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/community.py +0 -0
  66. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/fabric.py +0 -0
  67. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/gc.py +0 -0
  68. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/graph_walk.py +0 -0
  69. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/merge.py +0 -0
  70. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/pattern_query.py +0 -0
  71. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/pubsub.py +0 -0
  72. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/ckf/semantic.py +0 -0
  73. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/__init__.py +0 -0
  74. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/main.py +0 -0
  75. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/sidecar.py +0 -0
  76. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/cli/startup.py +0 -0
  77. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/__init__.py +0 -0
  78. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/completion.py +0 -0
  79. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/degradation.py +0 -0
  80. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/document_map.py +0 -0
  81. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/flow.py +0 -0
  82. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/gap.py +0 -0
  83. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/manager.py +0 -0
  84. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/quality_monitor.py +0 -0
  85. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/stitch.py +0 -0
  86. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/trigger.py +0 -0
  87. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/continuation/voice.py +0 -0
  88. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/__init__.py +0 -0
  89. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/batch.py +0 -0
  90. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/circuit_breaker.py +0 -0
  91. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/config.py +0 -0
  92. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_source.py +0 -0
  93. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/context_tools.py +0 -0
  94. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/dispatch_router.py +0 -0
  95. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/errors.py +0 -0
  96. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/extraction_facade.py +0 -0
  97. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/facilitator.py +0 -0
  98. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/idempotency.py +0 -0
  99. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/orchestrator.py +0 -0
  100. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/relay_strategies.py +0 -0
  101. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/security_manager.py +0 -0
  102. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/session.py +0 -0
  103. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/task_intent.py +0 -0
  104. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/core/window.py +0 -0
  105. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/__init__.py +0 -0
  106. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/builder.py +0 -0
  107. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/decomposer.py +0 -0
  108. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/formatter.py +0 -0
  109. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/packer.py +0 -0
  110. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/reranker.py +0 -0
  111. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/envelope/scoring.py +0 -0
  112. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/__init__.py +0 -0
  113. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/complexity.py +0 -0
  114. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/contradiction.py +0 -0
  115. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/pipeline.py +0 -0
  116. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/quality_gate.py +0 -0
  117. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage1_regex.py +0 -0
  118. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage2_statistical.py +0 -0
  119. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage3_gliner.py +0 -0
  120. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage4_uie.py +0 -0
  121. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage5_discourse.py +0 -0
  122. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/stage6_llm.py +0 -0
  123. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/structured_output.py +0 -0
  124. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/extraction/types.py +0 -0
  125. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/license_guard.py +0 -0
  126. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/__init__.py +0 -0
  127. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/audit.py +0 -0
  128. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/events.py +0 -0
  129. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/metrics.py +0 -0
  130. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/quality.py +0 -0
  131. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/structured_logging.py +0 -0
  132. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/observability/telemetry.py +0 -0
  133. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/__init__.py +0 -0
  134. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/_embeddings.py +0 -0
  135. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/_types.py +0 -0
  136. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/attribution_scorer.py +0 -0
  137. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/claim_detector.py +0 -0
  138. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/contradiction_detector.py +0 -0
  139. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/distortion_detector.py +0 -0
  140. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/entailment_verifier.py +0 -0
  141. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/fabrication_detector.py +0 -0
  142. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/hallucination_scorer.py +0 -0
  143. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/omission_analyzer.py +0 -0
  144. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/provenance_chain.py +0 -0
  145. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/provenance/report_generator.py +0 -0
  146. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/__init__.py +0 -0
  147. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/anthropic.py +0 -0
  148. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/base.py +0 -0
  149. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/custom.py +0 -0
  150. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/diagnostic.py +0 -0
  151. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/llamacpp.py +0 -0
  152. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/manager.py +0 -0
  153. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/ollama.py +0 -0
  154. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/providers/tokenizers.py +0 -0
  155. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/py.typed +0 -0
  156. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/__init__.py +0 -0
  157. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/adaptive_allocator.py +0 -0
  158. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/cost_model.py +0 -0
  159. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/overhead_manager.py +0 -0
  160. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/resources/resource_manager.py +0 -0
  161. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/__init__.py +0 -0
  162. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/cost-estimate.json +0 -0
  163. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/crp-error.json +0 -0
  164. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/envelope-preview.json +0 -0
  165. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/persisted-state-header.json +0 -0
  166. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/quality-report.json +0 -0
  167. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/session-handle.json +0 -0
  168. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/session-status.json +0 -0
  169. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/stream-event.json +0 -0
  170. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/schemas/task-intent.json +0 -0
  171. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/__init__.py +0 -0
  172. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/audit_trail.py +0 -0
  173. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/binding.py +0 -0
  174. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/compliance.py +0 -0
  175. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/consent.py +0 -0
  176. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/embedding_defense.py +0 -0
  177. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/encryption.py +0 -0
  178. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/injection.py +0 -0
  179. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/integrity.py +0 -0
  180. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/privacy.py +0 -0
  181. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/quarantine.py +0 -0
  182. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/rbac.py +0 -0
  183. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/security/validation.py +0 -0
  184. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/__init__.py +0 -0
  185. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/cold_storage.py +0 -0
  186. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/compaction.py +0 -0
  187. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/critical_state.py +0 -0
  188. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/event_log.py +0 -0
  189. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/fact.py +0 -0
  190. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/serialization.py +0 -0
  191. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/session_cleanup.py +0 -0
  192. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/snapshot.py +0 -0
  193. {crprotocol-2.2.0 → crprotocol-2.3.0}/crp/state/warm_store.py +0 -0
  194. {crprotocol-2.2.0 → crprotocol-2.3.0}/docs/OPERATIONS_RUNBOOK.md +0 -0
  195. {crprotocol-2.2.0 → crprotocol-2.3.0}/docs/WASA_INTEGRATION_TUTORIAL.md +0 -0
  196. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/async_usage.py +0 -0
  197. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/benchmark_continuation.py +0 -0
  198. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/choose_provider.py +0 -0
  199. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/comply_demo.py +0 -0
  200. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/README.md +0 -0
  201. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/demo.py +0 -0
  202. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/demo_app/demo_v1.py +0 -0
  203. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/extraction-pipeline.md +0 -0
  204. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/ingestion.py +0 -0
  205. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/local-model.md +0 -0
  206. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/multi-provider.md +0 -0
  207. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/multi_turn.py +0 -0
  208. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/quickstart.md +0 -0
  209. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/quickstart.py +0 -0
  210. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/scribe_demo.py +0 -0
  211. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/session-resumption.md +0 -0
  212. {crprotocol-2.2.0 → crprotocol-2.3.0}/examples/streaming.py +0 -0
  213. {crprotocol-2.2.0 → crprotocol-2.3.0}/media/logo.svg +0 -0
  214. {crprotocol-2.2.0 → crprotocol-2.3.0}/mkdocs.yml +0 -0
  215. {crprotocol-2.2.0 → crprotocol-2.3.0}/pyproject.toml +0 -0
  216. {crprotocol-2.2.0 → crprotocol-2.3.0}/rfcs/0000-template.md +0 -0
  217. {crprotocol-2.2.0 → crprotocol-2.3.0}/rfcs/0001-initial-release.md +0 -0
  218. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/cost-estimate.json +0 -0
  219. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/crp-error.json +0 -0
  220. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/envelope-preview.json +0 -0
  221. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/openapi.json +0 -0
  222. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/persisted-state-header.json +0 -0
  223. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/quality-report.json +0 -0
  224. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/session-handle.json +0 -0
  225. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/session-status.json +0 -0
  226. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/stream-event.json +0 -0
  227. {crprotocol-2.2.0 → crprotocol-2.3.0}/schemas/task-intent.json +0 -0
  228. {crprotocol-2.2.0 → crprotocol-2.3.0}/scripts/gen_changelog.py +0 -0
  229. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/CNAME +0 -0
  230. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/client.md +0 -0
  231. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/compliance.md +0 -0
  232. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/dispatch.md +0 -0
  233. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/index.md +0 -0
  234. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/api/schemas.md +0 -0
  235. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/benchmarks.md +0 -0
  236. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/eu-ai-act.md +0 -0
  237. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/gdpr.md +0 -0
  238. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/index.md +0 -0
  239. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/iso-42001.md +0 -0
  240. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/nist-ai-rmf.md +0 -0
  241. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/compliance/security.md +0 -0
  242. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/contributing.md +0 -0
  243. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/cli.md +0 -0
  244. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/index.md +0 -0
  245. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/installation.md +0 -0
  246. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/licensing.md +0 -0
  247. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/local-models.md +0 -0
  248. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/providers.md +0 -0
  249. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/getting-started/quickstart.md +0 -0
  250. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/demo-app.md +0 -0
  251. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/index.md +0 -0
  252. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/ingestion.md +0 -0
  253. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/multi-turn.md +0 -0
  254. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/session-persistence.md +0 -0
  255. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/sidecar.md +0 -0
  256. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/guides/streaming.md +0 -0
  257. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/index.md +0 -0
  258. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/comply-autocyber.md +0 -0
  259. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/comply.md +0 -0
  260. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/index.md +0 -0
  261. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/products/scribe.md +0 -0
  262. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/ckf.md +0 -0
  263. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/continuation.md +0 -0
  264. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/core.md +0 -0
  265. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/dispatch-strategies.md +0 -0
  266. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/envelope.md +0 -0
  267. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/extraction.md +0 -0
  268. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/index.md +0 -0
  269. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/meta-learning.md +0 -0
  270. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/provenance.md +0 -0
  271. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/quality-tiers.md +0 -0
  272. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/protocol/research.md +0 -0
  273. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/terms-of-service.md +0 -0
  274. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/benchmarks.md +0 -0
  275. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/index.md +0 -0
  276. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/reproduce.md +0 -0
  277. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/testing/running-tests.md +0 -0
  278. {crprotocol-2.2.0 → crprotocol-2.3.0}/site-docs/why-crp.md +0 -0
  279. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/01_RESEARCH_FOUNDATIONS.md +0 -0
  280. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/02_CORE_PROTOCOL.md +0 -0
  281. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/03_CONTEXT_ENVELOPE.md +0 -0
  282. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/04_TOKEN_GENERATION_PROTOCOL.md +0 -0
  283. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/05_SYSTEM_WIDE_INTEGRATION.md +0 -0
  284. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/06_IMPLEMENTATION_PLAN.md +0 -0
  285. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/07_SECURITY.md +0 -0
  286. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/08_MONETIZATION.md +0 -0
  287. {crprotocol-2.2.0 → crprotocol-2.3.0}/specification/09_DEPLOYMENT.md +0 -0
  288. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/conftest.py +0 -0
  289. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_report.json +0 -0
  290. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_report.txt +0 -0
  291. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/crp_killer_test.py +0 -0
  292. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/debug_gap.py +0 -0
  293. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/killer_test/debug_gap2.py +0 -0
  294. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_adaptive_allocator.py +0 -0
  295. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_adversarial_provenance.py +0 -0
  296. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_agentic.py +0 -0
  297. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_benchmarks.py +0 -0
  298. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_ckf_gate.py +0 -0
  299. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_compliance_security.py +0 -0
  300. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_compliance_wiring.py +0 -0
  301. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_context_enforcer.py +0 -0
  302. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_context_source.py +0 -0
  303. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_decision_provenance.py +0 -0
  304. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_decision_provenance_engine.py +0 -0
  305. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_entailment_risk.py +0 -0
  306. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_fidelity_verification.py +0 -0
  307. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_gap_fixes_live.py +0 -0
  308. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_integration.py +0 -0
  309. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_ip_protection.py +0 -0
  310. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_comprehensive.py +0 -0
  311. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_full_capture.py +0 -0
  312. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_long_generation.py +0 -0
  313. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_live_verification.py +0 -0
  314. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_manifest_ledger.py +0 -0
  315. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase1.py +0 -0
  316. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase2.py +0 -0
  317. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase3.py +0 -0
  318. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase4.py +0 -0
  319. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase5.py +0 -0
  320. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase6.py +0 -0
  321. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase7.py +0 -0
  322. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase8.py +0 -0
  323. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_phase9.py +0 -0
  324. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_production_hardening.py +0 -0
  325. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_relay_strategies.py +0 -0
  326. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_resource_manager.py +0 -0
  327. {crprotocol-2.2.0 → crprotocol-2.3.0}/tests/test_security_modules.py +0 -0
  328. {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.2.0
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.1.0-brightgreen.svg" alt="Spec Version: 2.1.0">
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.1.0-brightgreen.svg" alt="Spec Version: 2.1.0">
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",
@@ -2,4 +2,4 @@
2
2
  # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
3
  """CRP — Context Relay Protocol SDK."""
4
4
 
5
- __version__ = "2.2.0"
5
+ __version__ = "2.3.0"
@@ -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
- Returning None is intentional: CRP does not force enforcement on
606
- applications that do not opt in. Consumers that want "off by default"
607
- behaviour check for None and skip.
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
- global _DEFAULT
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)