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.
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/.gitignore +1 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/PKG-INFO +4 -2
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/README.md +3 -1
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/_version.py +2 -2
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/dlp.py +87 -7
- open_code_review_toolkit-0.12.1/src/ocr_toolkit/evidence/gitlinks.py +66 -0
- open_code_review_toolkit-0.12.1/src/ocr_toolkit/evidence/opaque_code.py +108 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/admission.py +15 -5
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/contracts.py +14 -1
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/gateway.py +19 -6
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/runtime.py +8 -8
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/local_chatgpt.py +16 -4
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/ocr_result.py +2 -2
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/formatting.py +22 -6
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/workflow.py +45 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/pre_execution.py +100 -13
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/preflight.py +2 -1
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab.py +10 -1
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/dlp.py +98 -8
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/federation.py +67 -4
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_receipt.py +36 -12
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_runner.py +175 -21
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/LICENSE +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/pyproject.toml +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/cli.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/filesystem.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/git.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/language.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/markdown.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/common/redaction.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/config_writer.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/configure.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/broker.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/contracts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/mcp.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/policy.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/recognizers.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/store.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/__main__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/actions.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/artifacts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/categorize.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collect.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/coverage.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/invocation.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/mcp.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/model.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/project.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/repository.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/review_context.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/core.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/readback.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/evidence/store/values.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/registry.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/relay.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/federation/transport.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/mcp_config.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/__main__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/approval.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/comments.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/gitlab.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/markers.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/payloads.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/reconciliation.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/result.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/settings.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/snapshot.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/suggestions.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/posting/transaction.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/provider_config.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/provider_failure.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/local.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/providers/local_config.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/py.typed +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/__init__.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/metadata.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/model.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/outcome.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/result.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/sections.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/text.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/reporting/usage.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/result_contract.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/result_usage.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_debug.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_identity.py +0 -0
- {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/review_progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: open-code-review-toolkit
|
|
3
|
-
Version: 0.12.
|
|
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
|
|
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
|
|
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
|
|
{open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/_version.py
RENAMED
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 12,
|
|
21
|
+
__version__ = version = '0.12.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 12, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/context/dlp.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
if
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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__(
|
|
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(
|
|
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,
|
|
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
|
|
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/
|
|
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:
|
|
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
|
|
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
|
|
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:
|
{open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.12.1}/src/ocr_toolkit/local_chatgpt.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
"""
|
|
83
|
+
"""Require the reviewed native-auth contract after exact binary qualification."""
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
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:
|