open-code-review-toolkit 0.12.0__tar.gz → 0.12.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.
Files changed (138) hide show
  1. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/.gitignore +1 -0
  2. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/PKG-INFO +4 -2
  3. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/README.md +3 -1
  4. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/_version.py +2 -2
  5. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/dlp.py +87 -7
  6. open_code_review_toolkit-0.12.1/src/ocr_toolkit/evidence/gitlinks.py +66 -0
  7. open_code_review_toolkit-0.12.1/src/ocr_toolkit/evidence/opaque_code.py +108 -0
  8. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/admission.py +15 -5
  9. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/contracts.py +14 -1
  10. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/gateway.py +19 -6
  11. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/runtime.py +8 -8
  12. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/local_chatgpt.py +16 -4
  13. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/ocr_result.py +2 -2
  14. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/formatting.py +22 -6
  15. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/workflow.py +45 -0
  16. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/pre_execution.py +100 -13
  17. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/preflight.py +2 -1
  18. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab.py +10 -1
  19. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/dlp.py +98 -8
  20. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/federation.py +67 -4
  21. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_receipt.py +36 -12
  22. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_runner.py +175 -21
  23. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/LICENSE +0 -0
  24. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/pyproject.toml +0 -0
  25. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/__init__.py +0 -0
  26. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/cli.py +0 -0
  27. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/__init__.py +0 -0
  28. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/filesystem.py +0 -0
  29. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/git.py +0 -0
  30. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/language.py +0 -0
  31. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/markdown.py +0 -0
  32. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/redaction.py +0 -0
  33. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/config_writer.py +0 -0
  34. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/configure.py +0 -0
  35. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/__init__.py +0 -0
  36. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/broker.py +0 -0
  37. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
  38. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/contracts.py +0 -0
  39. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/mcp.py +0 -0
  40. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/policy.py +0 -0
  41. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/recognizers.py +0 -0
  42. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/store.py +0 -0
  43. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/__init__.py +0 -0
  44. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/__main__.py +0 -0
  45. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/actions.py +0 -0
  46. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  47. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/categorize.py +0 -0
  48. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collect.py +0 -0
  49. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  50. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  51. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  52. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  53. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  54. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  55. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/coverage.py +0 -0
  56. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  57. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  58. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  59. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  60. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  61. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  62. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  63. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  64. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  65. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  66. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  67. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  68. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  69. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  70. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  71. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  72. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  73. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  74. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  75. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  76. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  77. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/invocation.py +0 -0
  78. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/mcp.py +0 -0
  79. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/model.py +0 -0
  80. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  81. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  82. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  83. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  84. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  85. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  86. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  87. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/project.py +0 -0
  88. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/repository.py +0 -0
  89. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/review_context.py +0 -0
  90. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  91. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  92. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  93. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/core.py +0 -0
  94. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  95. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/values.py +0 -0
  96. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/__init__.py +0 -0
  97. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/registry.py +0 -0
  98. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/relay.py +0 -0
  99. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/transport.py +0 -0
  100. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/mcp_config.py +0 -0
  101. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/__init__.py +0 -0
  102. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/__main__.py +0 -0
  103. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/approval.py +0 -0
  104. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/comments.py +0 -0
  105. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/gitlab.py +0 -0
  106. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  107. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/markers.py +0 -0
  108. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/payloads.py +0 -0
  109. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  110. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/result.py +0 -0
  111. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/settings.py +0 -0
  112. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/snapshot.py +0 -0
  113. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/suggestions.py +0 -0
  114. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/transaction.py +0 -0
  115. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/provider_config.py +0 -0
  116. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/provider_failure.py +0 -0
  117. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/__init__.py +0 -0
  118. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
  119. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  120. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  121. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  122. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  123. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/local.py +0 -0
  124. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/local_config.py +0 -0
  125. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/py.typed +0 -0
  126. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/__init__.py +0 -0
  127. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/metadata.py +0 -0
  128. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/model.py +0 -0
  129. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/outcome.py +0 -0
  130. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/result.py +0 -0
  131. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/sections.py +0 -0
  132. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/text.py +0 -0
  133. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/usage.py +0 -0
  134. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/result_contract.py +0 -0
  135. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/result_usage.py +0 -0
  136. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_debug.py +0 -0
  137. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_identity.py +0 -0
  138. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_progress.py +0 -0
@@ -16,3 +16,4 @@ dist/
16
16
  .idea/
17
17
  *.log
18
18
  .codex
19
+ .playwright-cli/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: open-code-review-toolkit
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: Unofficial GitLab CI integration layer for Open Code Review
5
5
  Project-URL: Homepage, https://github.com/xeonvs/open-code-review-toolkit
6
6
  Project-URL: Repository, https://github.com/xeonvs/open-code-review-toolkit
@@ -242,6 +242,8 @@ Description-Content-Type: text/markdown
242
242
 
243
243
  Open Code Review Toolkit is an unofficial review control and integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review), with GitLab CI and standalone local providers. It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, local Markdown reports, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
244
244
 
245
+ [Explore the toolkit site](https://open-code-review-toolkit.pages.dev/) for a quick overview, demo video, and latest release notes.
246
+
245
247
  > [!NOTE]
246
248
  > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
247
249
 
@@ -319,7 +321,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
319
321
 
320
322
  Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
321
323
 
322
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v9's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
324
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v10's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
323
325
 
324
326
  Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable target-ref decisions and never lets a source change self-authorize its review. Root and nested target `AGENTS.md`/`CLAUDE.md` guidance is similarly exposed through the existing evidence MCP with deterministic scope and precedence, while any guidance touched by the merge request is excluded. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) and [Target project guidance](docs/configuration.md#target-project-guidance) for formats and trust boundaries.
325
327
 
@@ -9,6 +9,8 @@
9
9
 
10
10
  Open Code Review Toolkit is an unofficial review control and integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review), with GitLab CI and standalone local providers. It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, local Markdown reports, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
11
11
 
12
+ [Explore the toolkit site](https://open-code-review-toolkit.pages.dev/) for a quick overview, demo video, and latest release notes.
13
+
12
14
  > [!NOTE]
13
15
  > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
14
16
 
@@ -86,7 +88,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
86
88
 
87
89
  Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
88
90
 
89
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v9's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
91
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v10's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
90
92
 
91
93
  Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable target-ref decisions and never lets a source change self-authorize its review. Root and nested target `AGENTS.md`/`CLAUDE.md` guidance is similarly exposed through the existing evidence MCP with deterministic scope and precedence, while any guidance touched by the merge request is excluded. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) and [Target project guidance](docs/configuration.md#target-project-guidance) for formats and trust boundaries.
92
94
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.12.0'
22
- __version_tuple__ = version_tuple = (0, 12, 0)
21
+ __version__ = version = '0.12.1'
22
+ __version_tuple__ = version_tuple = (0, 12, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -3,14 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import html
6
+ import ipaddress
6
7
  import re
7
8
  import unicodedata
8
9
  from collections.abc import Iterator, Mapping
9
10
  from contextlib import contextmanager
10
11
  from contextvars import ContextVar
11
12
  from dataclasses import dataclass
13
+ from datetime import date
12
14
 
13
- from ocr_toolkit.common.redaction import redact_env_secret_values, redact_sensitive
15
+ from ocr_toolkit.common.redaction import (
16
+ SENSITIVE_NAMED_KEY_PATTERN,
17
+ redact_env_secret_values,
18
+ redact_sensitive,
19
+ )
14
20
  from ocr_toolkit.context.contracts import TextBudgets
15
21
 
16
22
  EMAIL_RE = re.compile(
@@ -18,6 +24,20 @@ EMAIL_RE = re.compile(
18
24
  r"@(?:[a-z0-9-]{1,63}\.)+[a-z]{2,63}(?![a-z0-9_-])"
19
25
  )
20
26
  PHONE_RE = re.compile(r"(?<!\w)(?:\+?[1-9][\d .()/-]{7,24}\d)(?!\w)")
27
+ PURE_CREDENTIAL_ASSIGNMENT_RE = re.compile(
28
+ rf"(?im)^[ \t]*{SENSITIVE_NAMED_KEY_PATTERN}[ \t]*:[ \t]*"
29
+ r"(?:(?P<quote>['\"])\{\{[ \t]*[A-Za-z_][A-Za-z0-9_]*[ \t]*\}\}(?P=quote)|"
30
+ r"(?P<empty>\"\"|''))[ \t]*$"
31
+ )
32
+ ISO_DATE_RE = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})")
33
+ VERSION_CONTEXT_RE = re.compile(r"(?i)\b(?:version|release|build|pin)[ \t]+(?:v)?$")
34
+ DATE_CONTEXT_RE = re.compile(
35
+ r"(?i)\b(?:date|dated|on|since|until|before|after|expires)(?:[ \t]+on)?[ \t]+$"
36
+ )
37
+ IP_CONTEXT_RE = re.compile(
38
+ r"(?i)(?:\b|_)(?:ipv4|ip|cidr|subnet|address|host|loginhost|listen|allow|network)\b"
39
+ )
40
+ PHONE_CONTEXT_RE = re.compile(r"(?i)\b(?:call|phone|tel|contact)[ \t]*$")
21
41
  MARKDOWN_DEST_RE = re.compile(
22
42
  r"(?:\]\(\s*<?(?:https?://|mailto:)|^\s*\[[^\]\n]+\]:\s*<?(?:https?://|mailto:)|"
23
43
  r"<(?:https?://|mailto:))",
@@ -95,11 +115,59 @@ def _contains_phone(value: str) -> bool:
95
115
  """Recognize formatted phone-like values without classifying bare identifiers."""
96
116
 
97
117
  return any(
98
- match.group(0).startswith("+") or any(separator in match.group(0) for separator in " .()/-")
118
+ (
119
+ match.group(0).startswith("+")
120
+ or any(separator in match.group(0) for separator in " .()/-")
121
+ )
122
+ and not _technical_number(value, match)
99
123
  for match in PHONE_RE.finditer(value)
100
124
  )
101
125
 
102
126
 
127
+ def _technical_number(value: str, match: re.Match[str]) -> bool:
128
+ """Exclude only bounded, parsed technical syntax from phone candidates."""
129
+
130
+ candidate = match.group(0)
131
+ if candidate.startswith("+") or candidate != candidate.strip():
132
+ return False
133
+ before, after = value[: match.start()], value[match.end() :]
134
+ if before.endswith((".", "/", "-")) or re.match(r"(?:[./-]\d)", after):
135
+ return False
136
+ if PHONE_CONTEXT_RE.search(before[-24:]):
137
+ return False
138
+ try:
139
+ if "/" in candidate:
140
+ ipaddress.IPv4Network(candidate, strict=False)
141
+ return True
142
+ elif isinstance(ipaddress.ip_address(candidate), ipaddress.IPv4Address):
143
+ if after.startswith(":"):
144
+ port = re.match(r":(\d{1,5})\b", after)
145
+ return bool(port and 1 <= int(port[1]) <= 65535)
146
+ return bool(IP_CONTEXT_RE.search(before[-96:]))
147
+ except ValueError:
148
+ pass
149
+ if ISO_DATE_RE.fullmatch(candidate) and DATE_CONTEXT_RE.search(before[-32:]):
150
+ try:
151
+ date(int(candidate[:4]), int(candidate[5:7]), int(candidate[8:10]))
152
+ except ValueError:
153
+ return False
154
+ return True
155
+ if re.fullmatch(r"\d{4}\.\d{1,2}\.\d{1,3}", candidate):
156
+ return bool(VERSION_CONTEXT_RE.search(before[-32:]))
157
+ if re.fullmatch(r"\d+\.\d+", candidate):
158
+ return bool(
159
+ re.search(r"(?i)\bdecimal\([ \t]*['\"]$", before[-32:])
160
+ and re.match(r"['\"][ \t]*\)", after)
161
+ )
162
+ return False
163
+
164
+
165
+ def _semantic_secret_text(value: str) -> str:
166
+ """Remove proven no-value assignments only from generic secret classification."""
167
+
168
+ return PURE_CREDENTIAL_ASSIGNMENT_RE.sub("safe_reference_assignment", value)
169
+
170
+
103
171
  def _display_normalize(value: str) -> str:
104
172
  """Approximate closed text rendered by common Markdown/HTML constructs."""
105
173
 
@@ -235,6 +303,7 @@ def check_text(
235
303
  forbidden: tuple[str | ForbiddenValue, ...] = (),
236
304
  forbidden_matcher: ForbiddenMatcher | None = None,
237
305
  allow_horizontal_tabs: bool = False,
306
+ allow_opaque_code: bool = False,
238
307
  enabled: bool | None = None,
239
308
  ) -> DLPResult:
240
309
  """Apply independent units, redaction, PII, and optional publication checks."""
@@ -258,15 +327,26 @@ def check_text(
258
327
  return DLPResult(False, None, "limit", detector)
259
328
  if not (_DLP_ENABLED.get() if enabled is None else enabled):
260
329
  return DLPResult(True, normalized, "disabled")
261
- redacted = redact_env_secret_values(redact_sensitive(normalized))
262
- if redacted != normalized:
330
+ # Exact environment values retain priority even when they resemble a reference.
331
+ if redact_env_secret_values(normalized) != normalized:
332
+ return DLPResult(False, None, "secret", "normalized")
333
+ semantic = _semantic_secret_text(normalized)
334
+ if redact_sensitive(semantic) != semantic:
263
335
  return DLPResult(False, None, "secret", "normalized")
264
336
  decoded = _html_decode(normalized) if publication else normalized
265
337
  displayed = _display_normalize(normalized) if publication else normalized
266
338
  source = _source_normalize(normalized) if publication else normalized
267
- if publication and redact_env_secret_values(redact_sensitive(displayed)) != displayed:
339
+ semantic_displayed = _display_normalize(semantic) if publication else semantic
340
+ semantic_source = _source_normalize(semantic) if publication else semantic
341
+ if publication and (
342
+ redact_env_secret_values(displayed) != displayed
343
+ or redact_sensitive(semantic_displayed) != semantic_displayed
344
+ ):
268
345
  return DLPResult(False, None, "secret", "displayed")
269
- if publication and redact_env_secret_values(redact_sensitive(source)) != source:
346
+ if publication and (
347
+ redact_env_secret_values(source) != source
348
+ or redact_sensitive(semantic_source) != semantic_source
349
+ ):
270
350
  return DLPResult(False, None, "secret", "source")
271
351
  pii_candidates = (("normalized", normalized),)
272
352
  if publication:
@@ -289,7 +369,7 @@ def check_text(
289
369
  else "html_comment"
290
370
  if HTML_COMMENT_RE.search(decoded)
291
371
  else "html_tag"
292
- if HTML_TAG_RE.search(decoded)
372
+ if HTML_TAG_RE.search(decoded) and not (allow_opaque_code and decoded == normalized)
293
373
  else "display_control"
294
374
  if any(_is_display_control(character) for character in decoded)
295
375
  else None
@@ -0,0 +1,66 @@
1
+ """Identify changed Git submodule pointers from immutable tree objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ from ocr_toolkit.evidence.repository import (
10
+ GitRepositoryReader,
11
+ RepositoryEvidenceError,
12
+ RepositoryObject,
13
+ )
14
+
15
+ GitlinkChangeKind = Literal["added", "updated", "deleted"]
16
+ _SHA_RE = re.compile(r"[0-9a-f]{40}\Z")
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class GitlinkChange:
21
+ """Private tree evidence; paths and object IDs must not enter public receipts."""
22
+
23
+ path: str
24
+ kind: GitlinkChangeKind
25
+ before_oid: str | None
26
+ after_oid: str | None
27
+
28
+
29
+ def _entries_by_path(entries: tuple[RepositoryObject, ...]) -> dict[str, RepositoryObject]:
30
+ by_path: dict[str, RepositoryObject] = {}
31
+ for entry in entries:
32
+ if entry.path in by_path:
33
+ raise RepositoryEvidenceError("Git tree contains a duplicate path")
34
+ if (entry.mode == "160000") != (entry.object_type == "commit"):
35
+ raise RepositoryEvidenceError("Git tree contains an invalid gitlink entry")
36
+ by_path[entry.path] = entry
37
+ return by_path
38
+
39
+
40
+ def changed_gitlinks(
41
+ reader: GitRepositoryReader, base_sha: str, head_sha: str
42
+ ) -> tuple[GitlinkChange, ...]:
43
+ """Return changed mode-160000 entries without reading or following their targets.
44
+
45
+ Both inputs must be full immutable commit IDs. The reader's bounded, isolated
46
+ ``ls-tree -z`` path is the only source of evidence. A gitlink target object
47
+ need not exist in the parent repository, so it is never looked up here.
48
+ """
49
+
50
+ if _SHA_RE.fullmatch(base_sha) is None or _SHA_RE.fullmatch(head_sha) is None:
51
+ raise RepositoryEvidenceError("gitlink inspection requires immutable commit IDs")
52
+ before = _entries_by_path(reader.list_objects(base_sha))
53
+ after = _entries_by_path(reader.list_objects(head_sha))
54
+ changes: list[GitlinkChange] = []
55
+ for path in sorted(before.keys() | after.keys()):
56
+ prior = before.get(path)
57
+ current = after.get(path)
58
+ prior_oid = prior.object_sha if prior is not None and prior.is_submodule else None
59
+ current_oid = current.object_sha if current is not None and current.is_submodule else None
60
+ if prior_oid == current_oid:
61
+ continue
62
+ kind: GitlinkChangeKind = (
63
+ "added" if prior_oid is None else "deleted" if current_oid is None else "updated"
64
+ )
65
+ changes.append(GitlinkChange(path, kind, prior_oid, current_oid))
66
+ return tuple(changes)
@@ -0,0 +1,108 @@
1
+ """Bounded immutable-source proof for optional OCR code fields.
2
+
3
+ This module establishes source identity only. Publication still needs to check
4
+ the complete original text for sensitive content and prove its renderer safe.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from ocr_toolkit.posting.comments import line_number
14
+ from ocr_toolkit.posting.settings import (
15
+ MAX_SUGGESTION_CODE_CHARS,
16
+ MAX_SUGGESTION_SPAN_LINES,
17
+ )
18
+ from ocr_toolkit.posting.suggestions import (
19
+ SuggestionState,
20
+ evaluate_suggestion,
21
+ normalize_replacement,
22
+ safe_repository_path,
23
+ )
24
+
25
+ MAX_PROOF_SOURCE_CHARS = 256_000
26
+ MAX_PROOF_PATH_CHARS = 1_024
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class OpaqueCodeProof:
31
+ """Only closed proof facts; no source text, hash, or rejected value."""
32
+
33
+ existing_verified: bool = False
34
+ suggestion_verified: bool = False
35
+
36
+
37
+ def _bounded_line(value: Any) -> int:
38
+ """Parse an OCR line without admitting unbounded decimal conversion."""
39
+
40
+ if isinstance(value, str) and len(value) > 12:
41
+ return 0
42
+ try:
43
+ return line_number(value)
44
+ except ValueError:
45
+ return 0
46
+
47
+
48
+ def prove_opaque_code(
49
+ comment: dict[str, Any],
50
+ path: str,
51
+ read_head_blob: Callable[[str], str | None],
52
+ ) -> OpaqueCodeProof:
53
+ """Prove code against one exact, bounded range of the reviewed head.
54
+
55
+ The caller owns the immutable commit binding of ``read_head_blob`` and the
56
+ sink-specific rendering guarantee. A failed read or malformed model value
57
+ returns only false proof facts, so it cannot open a publication exception.
58
+ """
59
+
60
+ existing = comment.get("existing_code")
61
+ suggestion = comment.get("suggestion_code")
62
+ if not isinstance(existing, str) or not existing:
63
+ return OpaqueCodeProof()
64
+ if len(existing) > MAX_SUGGESTION_CODE_CHARS:
65
+ return OpaqueCodeProof()
66
+ suggestion_well_formed = suggestion is None or (
67
+ isinstance(suggestion, str) and len(suggestion) <= MAX_SUGGESTION_CODE_CHARS
68
+ )
69
+ if not isinstance(path, str) or len(path) > MAX_PROOF_PATH_CHARS:
70
+ return OpaqueCodeProof()
71
+ if not safe_repository_path(path):
72
+ return OpaqueCodeProof()
73
+
74
+ raw_start = comment.get("start_line")
75
+ raw_end = comment.get("end_line")
76
+ start = _bounded_line(comment.get("line") if raw_start is None else raw_start)
77
+ end = _bounded_line(comment.get("line") if raw_end is None else raw_end)
78
+ if start <= 0 or end < start or end - start > MAX_SUGGESTION_SPAN_LINES:
79
+ return OpaqueCodeProof()
80
+
81
+ try:
82
+ source = read_head_blob(path)
83
+ except Exception: # External repository-reader failures must close the proof.
84
+ return OpaqueCodeProof()
85
+ if not isinstance(source, str) or len(source) > MAX_PROOF_SOURCE_CHARS:
86
+ return OpaqueCodeProof()
87
+ lines = source.replace("\r\n", "\n").replace("\r", "\n").splitlines()
88
+ if end > len(lines):
89
+ return OpaqueCodeProof()
90
+ selected = "\n".join(lines[start - 1 : end])
91
+ if normalize_replacement(existing) != normalize_replacement(selected):
92
+ return OpaqueCodeProof()
93
+
94
+ if not suggestion or not suggestion_well_formed:
95
+ return OpaqueCodeProof(existing_verified=True)
96
+ # A no-op is source-proven too, but cannot smuggle an authored fence into
97
+ # a fallback rendering path. Actionable replacements use the evaluator's
98
+ # stricter shape checks.
99
+ if "```" in suggestion:
100
+ return OpaqueCodeProof(existing_verified=True)
101
+ try:
102
+ decision = evaluate_suggestion(comment, path, lambda _path: source)
103
+ except (ValueError, TypeError):
104
+ return OpaqueCodeProof(existing_verified=True)
105
+ return OpaqueCodeProof(
106
+ existing_verified=True,
107
+ suggestion_verified=decision.state in {SuggestionState.ACTIONABLE, SuggestionState.NO_OP},
108
+ )
@@ -100,7 +100,13 @@ def strings(value: object, *, depth: int = 16, nodes: int = 1024) -> Iterator[st
100
100
  raise FederationError("protocol_invalid")
101
101
 
102
102
 
103
- def check_dlp(value: object, matcher: ForbiddenMatcher, *, dlp_enabled: bool = True) -> None:
103
+ def check_dlp(
104
+ value: object,
105
+ matcher: ForbiddenMatcher,
106
+ *,
107
+ boundary: str,
108
+ dlp_enabled: bool = True,
109
+ ) -> None:
104
110
  """Reject a complete JSON value if any string discloses protected content."""
105
111
  chars = 0
106
112
  for text in strings(value):
@@ -113,7 +119,11 @@ def check_dlp(value: object, matcher: ForbiddenMatcher, *, dlp_enabled: bool = T
113
119
  text, budgets=_TEXT_BUDGETS, forbidden_matcher=matcher, allow_horizontal_tabs=True
114
120
  )
115
121
  if not checked.admitted:
116
- raise FederationError("oversized" if checked.reason == "limit" else "dlp_rejected")
122
+ raise FederationError(
123
+ "oversized" if checked.reason == "limit" else "dlp_rejected",
124
+ boundary=boundary,
125
+ detector=checked.reason,
126
+ )
117
127
 
118
128
 
119
129
  def compile_schema(
@@ -156,7 +166,7 @@ def compile_schema(
156
166
  if multiplied > 64:
157
167
  raise FederationError("schema_invalid")
158
168
  pending.extend((child, multiplied) for child in branches)
159
- check_dlp(schema, matcher, dlp_enabled=dlp_enabled)
169
+ check_dlp(schema, matcher, boundary="schema", dlp_enabled=dlp_enabled)
160
170
  Draft202012Validator.check_schema(schema)
161
171
  return Draft202012Validator(schema, registry=SchemaRegistry())
162
172
  except FederationError:
@@ -190,7 +200,7 @@ def arguments(
190
200
  raise FederationError("origin_denied") from None
191
201
  if candidate not in policy.resource_origins:
192
202
  raise FederationError("origin_denied")
193
- check_dlp(value, matcher, dlp_enabled=dlp_enabled)
203
+ check_dlp(value, matcher, boundary="arguments", dlp_enabled=dlp_enabled)
194
204
  return value, canonical
195
205
 
196
206
 
@@ -211,7 +221,7 @@ def result(
211
221
  content = [types.TextContent(type="text", text=block.text) for block in value.content]
212
222
  structured = value.structured_content
213
223
  candidate = {"content": [block.text for block in content], "structured": structured}
214
- check_dlp(candidate, matcher, dlp_enabled=dlp_enabled)
224
+ check_dlp(candidate, matcher, boundary="result", dlp_enabled=dlp_enabled)
215
225
  if len(encode(candidate)) > RESPONSE_BYTES:
216
226
  raise FederationError("oversized")
217
227
  if validator is not None and (
@@ -31,12 +31,21 @@ COUNTERS = (
31
31
  "cache_hits",
32
32
  "single_flight",
33
33
  )
34
+ DLP_GATE_REASONS = ("forbidden", "invalid_text", "limit", "pii", "secret")
35
+ DLP_CALL_GATES = ("arguments", "result")
36
+ DLP_BOUNDARIES = frozenset(("schema", *DLP_CALL_GATES))
34
37
 
35
38
 
36
39
  class FederationError(ValueError):
37
40
  """Expose only one closed, content-free failure code."""
38
41
 
39
- def __init__(self, code: str = "federation_failed") -> None:
42
+ def __init__(
43
+ self,
44
+ code: str = "federation_failed",
45
+ *,
46
+ boundary: str | None = None,
47
+ detector: str | None = None,
48
+ ) -> None:
40
49
  allowed = {
41
50
  "registry_invalid",
42
51
  "secret_missing",
@@ -56,6 +65,10 @@ class FederationError(ValueError):
56
65
  "tool_failed",
57
66
  }
58
67
  self.code = code if code in allowed else "federation_failed"
68
+ self.boundary = boundary if boundary in DLP_BOUNDARIES else None
69
+ self.detector = detector if detector in DLP_GATE_REASONS else None
70
+ if self.detector is None:
71
+ self.boundary = None
59
72
  super().__init__(self.code)
60
73
 
61
74
 
@@ -25,6 +25,8 @@ from .contracts import (
25
25
  CONCURRENCY,
26
26
  COUNTERS,
27
27
  DELIVERY_BYTES,
28
+ DLP_CALL_GATES,
29
+ DLP_GATE_REASONS,
28
30
  MAX_CALLS,
29
31
  METADATA_BYTES,
30
32
  FederationError,
@@ -79,6 +81,7 @@ class Gateway:
79
81
  "server": spec.name,
80
82
  "assurance": tool.assurance,
81
83
  **dict.fromkeys(COUNTERS, 0),
84
+ "dlp_gates": {gate: dict.fromkeys(DLP_GATE_REASONS, 0) for gate in DLP_CALL_GATES},
82
85
  }
83
86
  for spec in registry.servers
84
87
  for tool in spec.tools
@@ -153,7 +156,9 @@ class Gateway:
153
156
  else None
154
157
  )
155
158
  description = tool.description or ""
156
- admission.check_dlp(description, self.matcher, dlp_enabled=self.dlp_enabled)
159
+ admission.check_dlp(
160
+ description, self.matcher, boundary="schema", dlp_enabled=self.dlp_enabled
161
+ )
157
162
  definition = types.Tool(
158
163
  name=policy.alias, description=description, input_schema=tool.input_schema
159
164
  )
@@ -212,9 +217,10 @@ class Gateway:
212
217
  finally:
213
218
  self.dispatches.pop(key, None)
214
219
 
215
- def _terminal(self, alias: str, reason: str) -> None:
220
+ def _terminal(self, alias: str, error: FederationError) -> None:
216
221
  """Record one outcome and optional diagnostic subset without retaining content."""
217
222
  counts = self.counts[alias]
223
+ reason = error.code
218
224
  if reason in {
219
225
  "arguments_invalid",
220
226
  "origin_denied",
@@ -230,6 +236,8 @@ class Gateway:
230
236
  counts[outcome] += 1
231
237
  if reason in {"dlp_rejected", "oversized"}:
232
238
  counts[reason] += 1
239
+ if error.boundary in DLP_CALL_GATES and error.detector in DLP_GATE_REASONS:
240
+ counts["dlp_gates"][error.boundary][error.detector] += 1
233
241
 
234
242
  async def call_tool(
235
243
  self, _context: Any, params: types.CallToolRequestParams
@@ -292,12 +300,12 @@ class Gateway:
292
300
  counts["completed"] += 1
293
301
  return result
294
302
  except asyncio.CancelledError:
295
- self._terminal(params.name, "tool_failed")
303
+ self._terminal(params.name, FederationError("tool_failed"))
296
304
  raise
297
305
  except FederationError as error:
298
- self._terminal(params.name, error.code)
306
+ self._terminal(params.name, error)
299
307
  except Exception:
300
- self._terminal(params.name, "tool_failed")
308
+ self._terminal(params.name, FederationError("tool_failed"))
301
309
  return types.CallToolResult(
302
310
  content=[types.TextContent(type="text", text="federation_unavailable")], is_error=True
303
311
  )
@@ -344,8 +352,13 @@ class Gateway:
344
352
  counts[key] for key in ("completed", "denied", "failed", "timed_out")
345
353
  ):
346
354
  raise FederationError("cleanup_failed")
355
+ dlp_gates = counts["dlp_gates"]
356
+ if sum(sum(gate.values()) for gate in dlp_gates.values()) > (
357
+ counts["dlp_rejected"] + counts["oversized"]
358
+ ):
359
+ raise FederationError("cleanup_failed")
347
360
  return {
348
- "schema": "ocr.federation/v1",
361
+ "schema": "ocr.federation/v2",
349
362
  "run_id": self.run_id,
350
363
  "state": "finalized",
351
364
  "cleanup": "clean",
@@ -69,7 +69,7 @@ class RunningGateway:
69
69
  self._loop: asyncio.AbstractEventLoop | None = None
70
70
  self._main_task: asyncio.Task[Any] | None = None
71
71
  self._stop: asyncio.Event | None = None
72
- self._error: str | None = None
72
+ self._error: FederationError | None = None
73
73
  self._receipt: dict[str, Any] | None = None
74
74
  self._gateway: Gateway | None = None
75
75
  self._closed = False
@@ -97,7 +97,7 @@ class RunningGateway:
97
97
  self._thread.join(20)
98
98
  if not self._thread.is_alive():
99
99
  shutil.rmtree(self._directory)
100
- raise FederationError(self._error)
100
+ raise self._error
101
101
 
102
102
  @property
103
103
  def secret_values(self) -> tuple[str, ...]:
@@ -121,7 +121,7 @@ class RunningGateway:
121
121
  asyncio.run(self._serve(registry, selected, forbidden, run_id))
122
122
  except BaseException:
123
123
  if self._error is None:
124
- self._error = "cleanup_failed"
124
+ self._error = FederationError("cleanup_failed")
125
125
  finally:
126
126
  self._ready.set()
127
127
 
@@ -157,11 +157,11 @@ class RunningGateway:
157
157
  try:
158
158
  await asyncio.wait_for(self._stop.wait(), RUN_SECONDS)
159
159
  except TimeoutError:
160
- self._error = "timed_out"
160
+ self._error = FederationError("timed_out")
161
161
  except FederationError as error:
162
- self._error = error.code
162
+ self._error = error
163
163
  except BaseException:
164
- self._error = "gateway_unavailable"
164
+ self._error = FederationError("gateway_unavailable")
165
165
  finally:
166
166
  if listener is not None:
167
167
  listener.close()
@@ -170,12 +170,12 @@ class RunningGateway:
170
170
  try:
171
171
  await stack.aclose()
172
172
  except BaseException:
173
- self._error = "cleanup_failed"
173
+ self._error = FederationError("cleanup_failed")
174
174
  if self._error is None:
175
175
  try:
176
176
  self._receipt = gateway.receipt()
177
177
  except FederationError:
178
- self._error = "cleanup_failed"
178
+ self._error = FederationError("cleanup_failed")
179
179
  self._ready.set()
180
180
 
181
181
  def _request_stop(self, *, cancel: bool = False) -> None:
@@ -10,7 +10,10 @@ from typing import Any
10
10
 
11
11
  from ocr_toolkit.provider_config import ProviderConfigError, request_controls_from_environment
12
12
 
13
- CHATGPT_OCR_VERSIONS = frozenset({"1.13.2"})
13
+ # Exact binary versions are qualified by preflight.validate_ocr_binary first.
14
+ # Native auth is an independently reviewed capability within that qualified line.
15
+ CHATGPT_CONTRACT_LINE = (1, 13)
16
+ CHATGPT_MIN_PATCH = 2
14
17
  _CONFLICTING_SETTINGS = (
15
18
  "OCR_LLM_URL",
16
19
  "OCR_LLM_TOKEN",
@@ -77,10 +80,19 @@ def provider_entry(environment: Mapping[str, str]) -> dict[str, Any]:
77
80
 
78
81
 
79
82
  def require_supported_version(version: str) -> None:
80
- """Keep native auth limited to the separately qualified OCR release."""
83
+ """Require the reviewed native-auth contract after exact binary qualification."""
81
84
 
82
- if version not in CHATGPT_OCR_VERSIONS:
83
- raise LocalChatGPTError("ChatGPT authentication requires qualified OCR 1.13.2")
85
+ parts = version.split(".")
86
+ if (
87
+ len(version) > 32
88
+ or len(parts) != 3
89
+ or any(not part.isascii() or not part.isdecimal() for part in parts)
90
+ or tuple(map(int, parts[:2])) != CHATGPT_CONTRACT_LINE
91
+ or int(parts[2]) < CHATGPT_MIN_PATCH
92
+ ):
93
+ raise LocalChatGPTError(
94
+ "ChatGPT authentication requires qualified OCR 1.13.2 or later in the 1.13 line"
95
+ )
84
96
 
85
97
 
86
98
  def _directory(path: Path, *, private: bool = False, owned: bool = False) -> None: