open-code-review-toolkit 0.6.2__tar.gz → 0.6.3__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.6.2 → open_code_review_toolkit-0.6.3}/PKG-INFO +3 -8
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/README.md +2 -7
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/_version.py +2 -2
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/review_context.py +31 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/mcp_config.py +128 -23
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/ocr_result.py +5 -3
- open_code_review_toolkit-0.6.3/src/ocr_toolkit/posting/approval.py +266 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/formatting.py +5 -1
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/gitlab.py +226 -40
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/gitlab_approval.py +41 -4
- open_code_review_toolkit-0.6.3/src/ocr_toolkit/posting/gitlab_identity.py +14 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/markers.py +28 -5
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/payloads.py +15 -6
- open_code_review_toolkit-0.6.3/src/ocr_toolkit/posting/reconciliation.py +120 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/snapshot.py +62 -83
- open_code_review_toolkit-0.6.3/src/ocr_toolkit/posting/transaction.py +75 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/workflow.py +82 -48
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/preflight.py +1 -1
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/providers/gitlab.py +28 -12
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/review_runner.py +108 -49
- open_code_review_toolkit-0.6.2/src/ocr_toolkit/posting/approval.py +0 -174
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/.gitignore +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/LICENSE +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/pyproject.toml +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/cli.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/filesystem.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/git.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/language.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/markdown.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/common/redaction.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/config_writer.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/configure.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/__main__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/artifacts.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/categorize.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collect.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/coverage.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/invocation.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/mcp.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/model.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/project.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/repository.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/core.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/readback.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/evidence/store/values.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/__main__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/comments.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/result.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/settings.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/posting/suggestions.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/providers/__init__.py +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/py.typed +0 -0
- {open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/result_contract.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: open-code-review-toolkit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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
|
|
@@ -258,18 +258,13 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
|
|
|
258
258
|
|
|
259
259
|
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.
|
|
260
260
|
|
|
261
|
-
After every current review note publishes, the GitLab adapter can add a
|
|
262
|
-
conservative approval bound to the exact reviewed source SHA. This write is
|
|
263
|
-
enabled by default; set `OCR_AUTO_APPROVE=false` before upgrading when the bot
|
|
264
|
-
must remain comment-only. GitLab approval rules and protected-branch policy
|
|
265
|
-
remain authoritative. The toolkit only adds an eligible approval; it never
|
|
266
|
-
removes an existing approval when a later review is ineligible or disabled.
|
|
261
|
+
After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v3's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata remains eligible, while degraded metadata, legacy receipts, any external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
|
|
267
262
|
|
|
268
263
|
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.
|
|
269
264
|
|
|
270
265
|
## Project architecture
|
|
271
266
|
|
|
272
|
-
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server.
|
|
267
|
+
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. GitLab MR reviews allow operator-configured external MCP only over remote HTTPS; developer-local execution may retain explicit stdio peers. Direct external MCP remains privileged operator configuration and comment-only; planned M5 external records use the separate toolkit-owned broker boundary.
|
|
273
268
|
|
|
274
269
|
- [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
|
|
275
270
|
- [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
|
|
@@ -31,18 +31,13 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
|
|
|
31
31
|
|
|
32
32
|
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.
|
|
33
33
|
|
|
34
|
-
After every current review note publishes, the GitLab adapter can add a
|
|
35
|
-
conservative approval bound to the exact reviewed source SHA. This write is
|
|
36
|
-
enabled by default; set `OCR_AUTO_APPROVE=false` before upgrading when the bot
|
|
37
|
-
must remain comment-only. GitLab approval rules and protected-branch policy
|
|
38
|
-
remain authoritative. The toolkit only adds an eligible approval; it never
|
|
39
|
-
removes an existing approval when a later review is ineligible or disabled.
|
|
34
|
+
After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v3's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata remains eligible, while degraded metadata, legacy receipts, any external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
|
|
40
35
|
|
|
41
36
|
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.
|
|
42
37
|
|
|
43
38
|
## Project architecture
|
|
44
39
|
|
|
45
|
-
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server.
|
|
40
|
+
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. GitLab MR reviews allow operator-configured external MCP only over remote HTTPS; developer-local execution may retain explicit stdio peers. Direct external MCP remains privileged operator configuration and comment-only; planned M5 external records use the separate toolkit-owned broker boundary.
|
|
46
41
|
|
|
47
42
|
- [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
|
|
48
43
|
- [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
|
{open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/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.6.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
21
|
+
__version__ = version = '0.6.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -38,6 +38,22 @@ LABEL_STATUSES = frozenset(
|
|
|
38
38
|
}
|
|
39
39
|
)
|
|
40
40
|
MAX_LABELS = 32
|
|
41
|
+
CONTEXT_MODES = frozenset({"off", "metadata", "enriched"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReviewContextModeError(ValueError):
|
|
45
|
+
"""Report an unsupported or unavailable review-context mode safely."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_review_context_mode(raw: str | None) -> str:
|
|
49
|
+
"""Return the closed context mode without echoing untrusted configuration."""
|
|
50
|
+
|
|
51
|
+
mode = (raw or "").strip().lower() or "off"
|
|
52
|
+
if mode not in CONTEXT_MODES:
|
|
53
|
+
raise ReviewContextModeError("OCR_REVIEW_CONTEXT_MODE is invalid")
|
|
54
|
+
if mode == "enriched":
|
|
55
|
+
raise ReviewContextModeError("OCR_REVIEW_CONTEXT_MODE=enriched is not available")
|
|
56
|
+
return mode or "off"
|
|
41
57
|
|
|
42
58
|
|
|
43
59
|
def context_provenance(provider: str) -> str:
|
|
@@ -84,6 +100,21 @@ class MergeRequestContext:
|
|
|
84
100
|
labels = self.fields.get("labels")
|
|
85
101
|
return bool(isinstance(labels, Mapping) and labels.get("values"))
|
|
86
102
|
|
|
103
|
+
@property
|
|
104
|
+
def state(self) -> str:
|
|
105
|
+
"""Return whether every selected metadata field is completely represented."""
|
|
106
|
+
|
|
107
|
+
complete_text = {"absent", "admitted"}
|
|
108
|
+
complete_labels = {"absent", "admitted"}
|
|
109
|
+
for name in ("title", "description", "source_branch"):
|
|
110
|
+
field = self.fields.get(name)
|
|
111
|
+
if not isinstance(field, Mapping) or field.get("status") not in complete_text:
|
|
112
|
+
return "degraded"
|
|
113
|
+
labels = self.fields.get("labels")
|
|
114
|
+
if not isinstance(labels, Mapping) or labels.get("status") not in complete_labels:
|
|
115
|
+
return "degraded"
|
|
116
|
+
return "complete"
|
|
117
|
+
|
|
87
118
|
def evidence_value(self) -> dict[str, object]:
|
|
88
119
|
"""Return the closed persisted descriptor value."""
|
|
89
120
|
|
{open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/mcp_config.py
RENAMED
|
@@ -35,6 +35,7 @@ SENSITIVE_HEADER_NAME_RE = re.compile(
|
|
|
35
35
|
TRUE_VALUES = {"1", "true", "yes", "on"}
|
|
36
36
|
FALSE_VALUES = {"0", "false", "no", "off", ""}
|
|
37
37
|
BUILTIN_EVIDENCE_SERVER = "ocr_toolkit_evidence"
|
|
38
|
+
MCP_PROFILES = frozenset({"local", "gitlab_mr"})
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class MCPConfigError(Exception):
|
|
@@ -64,6 +65,7 @@ class MCPCapability:
|
|
|
64
65
|
server: str
|
|
65
66
|
tools: tuple[str, ...]
|
|
66
67
|
builtin: bool = False
|
|
68
|
+
transport: str = "stdio"
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
@dataclass(frozen=True, slots=True)
|
|
@@ -93,6 +95,17 @@ def _string_list(value: Any, field: str, *, limit: int) -> list[str]:
|
|
|
93
95
|
return result
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
def _tool_names(value: Any, field: str) -> list[str]:
|
|
99
|
+
"""Validate one explicit OCR-compatible tool-name allowlist."""
|
|
100
|
+
|
|
101
|
+
tools = _string_list(value, field, limit=MAX_MCP_TOOLS)
|
|
102
|
+
if any(not tool for tool in tools):
|
|
103
|
+
raise MCPConfigError(f"{field} must not contain empty tool names")
|
|
104
|
+
if len(set(tools)) != len(tools):
|
|
105
|
+
raise MCPConfigError(f"{field} must not contain duplicate tool names")
|
|
106
|
+
return tools
|
|
107
|
+
|
|
108
|
+
|
|
96
109
|
def _env_assignments(value: Any, env_from: Any, server_name: str) -> tuple[list[str], list[str]]:
|
|
97
110
|
"""Build stdio environment assignments and collect values for redaction."""
|
|
98
111
|
|
|
@@ -187,6 +200,11 @@ def _headers(value: Any, env_from: Any, server_name: str) -> tuple[dict[str, str
|
|
|
187
200
|
)
|
|
188
201
|
if not isinstance(item, str):
|
|
189
202
|
raise MCPConfigError(f"servers.{server_name}.headers.{name} must be a string")
|
|
203
|
+
if item.startswith("$") and MCP_ENV_NAME_RE.fullmatch(item[1:]):
|
|
204
|
+
raise MCPConfigError(
|
|
205
|
+
f"servers.{server_name}.headers.{name} resembles an environment reference; "
|
|
206
|
+
"use headers_from"
|
|
207
|
+
)
|
|
190
208
|
if not item or len(item) > MAX_MCP_HEADER_VALUE_CHARS:
|
|
191
209
|
raise MCPConfigError(
|
|
192
210
|
f"servers.{server_name}.headers.{name} must be non-empty and at most "
|
|
@@ -265,8 +283,11 @@ def _remote_url(value: Any, server_name: str) -> str:
|
|
|
265
283
|
return value
|
|
266
284
|
|
|
267
285
|
|
|
268
|
-
def parse_mcp_servers(raw: str | None = None) -> list[MCPServerConfig]:
|
|
269
|
-
"""Return bounded
|
|
286
|
+
def parse_mcp_servers(raw: str | None = None, *, profile: str = "local") -> list[MCPServerConfig]:
|
|
287
|
+
"""Return bounded external configs admitted by one execution profile."""
|
|
288
|
+
|
|
289
|
+
if profile not in MCP_PROFILES:
|
|
290
|
+
raise MCPConfigError("internal MCP execution profile is invalid")
|
|
270
291
|
|
|
271
292
|
raw = os.environ.get("OCR_MCP_SERVERS_JSON", "") if raw is None else raw
|
|
272
293
|
raw = raw.strip()
|
|
@@ -329,7 +350,11 @@ def parse_mcp_servers(raw: str | None = None) -> list[MCPServerConfig]:
|
|
|
329
350
|
transport = server.get("type", "stdio")
|
|
330
351
|
if transport not in {"stdio", "remote"}:
|
|
331
352
|
raise MCPConfigError(f"servers.{name}.type must be 'stdio' or 'remote'")
|
|
332
|
-
|
|
353
|
+
if profile == "gitlab_mr" and transport != "remote":
|
|
354
|
+
raise MCPConfigError("GitLab merge-request reviews require external remote MCP")
|
|
355
|
+
common_fields = {"name", "type", "enabled", "tools"}
|
|
356
|
+
if transport == "stdio":
|
|
357
|
+
common_fields.add("setup")
|
|
333
358
|
transport_fields = (
|
|
334
359
|
{"command", "args", "env", "env_from"}
|
|
335
360
|
if transport == "stdio"
|
|
@@ -340,7 +365,7 @@ def parse_mcp_servers(raw: str | None = None) -> list[MCPServerConfig]:
|
|
|
340
365
|
fields = ", ".join(sorted(unknown_fields))
|
|
341
366
|
raise MCPConfigError(f"servers.{name} has unsupported fields for {transport}: {fields}")
|
|
342
367
|
|
|
343
|
-
tools =
|
|
368
|
+
tools = _tool_names(server.get("tools", []), f"servers.{name}.tools")
|
|
344
369
|
if not tools:
|
|
345
370
|
raise MCPConfigError(f"servers.{name}.tools must explicitly allow at least one tool")
|
|
346
371
|
if TOOL_NAME in tools:
|
|
@@ -404,28 +429,98 @@ def _replace_configured_servers() -> bool:
|
|
|
404
429
|
raise MCPConfigError("OCR_MCP_REPLACE must be a boolean (true/false, 1/0, yes/no, or on/off)")
|
|
405
430
|
|
|
406
431
|
|
|
407
|
-
def
|
|
408
|
-
|
|
432
|
+
def _existing_server(
|
|
433
|
+
name: str, value: Any, *, profile: str
|
|
434
|
+
) -> tuple[dict[str, Any], MCPCapability, tuple[str, ...]]:
|
|
435
|
+
"""Revalidate one inherited OCR MCP entry through the active exact schema."""
|
|
409
436
|
|
|
410
437
|
if not isinstance(name, str) or not MCP_NAME_RE.fullmatch(name):
|
|
411
438
|
raise MCPConfigError("Existing OCR MCP server name is invalid")
|
|
412
439
|
if not isinstance(value, dict):
|
|
413
440
|
raise MCPConfigError(f"Existing OCR MCP server {name!r} is not a JSON object")
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
441
|
+
if name == BUILTIN_EVIDENCE_SERVER:
|
|
442
|
+
raise MCPConfigError("the toolkit-owned MCP server cannot be inherited")
|
|
443
|
+
normalized = dict(value)
|
|
444
|
+
transport = normalized.get("type", "stdio")
|
|
445
|
+
# Toolkit <=0.6.2 persisted an empty setup key for every remote entry.
|
|
446
|
+
# Normalize only that exact legacy no-op; non-empty inherited setup remains
|
|
447
|
+
# invalid and new remote input never admits the field.
|
|
448
|
+
if transport == "remote" and normalized.get("setup") == "":
|
|
449
|
+
normalized.pop("setup")
|
|
450
|
+
if transport == "stdio" and isinstance(normalized.get("env"), list):
|
|
451
|
+
environment: dict[str, str] = {}
|
|
452
|
+
for assignment in normalized["env"]:
|
|
453
|
+
if not isinstance(assignment, str) or "=" not in assignment:
|
|
454
|
+
raise MCPConfigError(f"Existing OCR MCP server {name!r} has invalid env")
|
|
455
|
+
key, item = assignment.split("=", 1)
|
|
456
|
+
if key in environment:
|
|
457
|
+
raise MCPConfigError(f"Existing OCR MCP server {name!r} has duplicate env name")
|
|
458
|
+
environment[key] = item
|
|
459
|
+
normalized["env"] = environment
|
|
460
|
+
elif transport == "remote" and isinstance(normalized.get("headers"), dict):
|
|
461
|
+
literal_headers: dict[str, object] = {}
|
|
462
|
+
headers_from: dict[str, str] = {}
|
|
463
|
+
for header, item in normalized["headers"].items():
|
|
464
|
+
source = item[1:] if isinstance(item, str) and item.startswith("$") else ""
|
|
465
|
+
if source and MCP_ENV_NAME_RE.fullmatch(source):
|
|
466
|
+
headers_from[header] = source
|
|
467
|
+
else:
|
|
468
|
+
literal_headers[header] = item
|
|
469
|
+
if literal_headers:
|
|
470
|
+
normalized["headers"] = literal_headers
|
|
471
|
+
else:
|
|
472
|
+
normalized.pop("headers", None)
|
|
473
|
+
if headers_from:
|
|
474
|
+
inherited_sources = normalized.get("headers_from")
|
|
475
|
+
if inherited_sources is None:
|
|
476
|
+
normalized["headers_from"] = headers_from
|
|
477
|
+
elif isinstance(inherited_sources, dict):
|
|
478
|
+
normalized_names = {str(header).casefold() for header in inherited_sources}
|
|
479
|
+
if any(header.casefold() in normalized_names for header in headers_from):
|
|
480
|
+
raise MCPConfigError(
|
|
481
|
+
f"Existing OCR MCP server {name!r} has duplicate header sources"
|
|
482
|
+
)
|
|
483
|
+
normalized["headers_from"] = {**inherited_sources, **headers_from}
|
|
484
|
+
raw = json.dumps({name: normalized}, separators=(",", ":"), ensure_ascii=False)
|
|
485
|
+
parsed = parse_mcp_servers(raw, profile=profile)
|
|
486
|
+
if len(parsed) != 1:
|
|
487
|
+
raise MCPConfigError(f"Existing OCR MCP server {name!r} is disabled or invalid")
|
|
488
|
+
server = parsed[0]
|
|
489
|
+
payload: dict[str, Any] = {
|
|
490
|
+
"type": server.transport,
|
|
491
|
+
"tools": server.tools,
|
|
492
|
+
}
|
|
493
|
+
if server.transport == "stdio":
|
|
494
|
+
payload.update(
|
|
495
|
+
{
|
|
496
|
+
"command": server.command,
|
|
497
|
+
"args": server.args,
|
|
498
|
+
"env": server.env,
|
|
499
|
+
"setup": server.setup,
|
|
500
|
+
}
|
|
418
501
|
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return
|
|
502
|
+
else:
|
|
503
|
+
payload.update({"url": server.url, "headers": server.headers})
|
|
504
|
+
return (
|
|
505
|
+
payload,
|
|
506
|
+
MCPCapability(server.name, tuple(server.tools), transport=server.transport),
|
|
507
|
+
tuple(server.secret_values),
|
|
508
|
+
)
|
|
422
509
|
|
|
423
510
|
|
|
424
|
-
def compose_mcp_servers(
|
|
511
|
+
def compose_mcp_servers(
|
|
512
|
+
servers: list[MCPServerConfig], *, replace: bool, profile: str = "local"
|
|
513
|
+
) -> MCPComposition:
|
|
425
514
|
"""Build OCR's registry from independent optional and mandatory MCP entries."""
|
|
426
515
|
|
|
516
|
+
if profile not in MCP_PROFILES:
|
|
517
|
+
raise MCPConfigError("internal MCP execution profile is invalid")
|
|
518
|
+
if profile == "gitlab_mr" and any(server.transport != "remote" for server in servers):
|
|
519
|
+
raise MCPConfigError("GitLab merge-request reviews require external remote MCP")
|
|
520
|
+
|
|
427
521
|
payload: dict[str, dict[str, Any]] = {}
|
|
428
522
|
capabilities: list[MCPCapability] = []
|
|
523
|
+
secret_values: list[str] = []
|
|
429
524
|
if not replace:
|
|
430
525
|
try:
|
|
431
526
|
current = read_ocr_config().get("mcp_servers", {})
|
|
@@ -433,20 +528,26 @@ def compose_mcp_servers(servers: list[MCPServerConfig], *, replace: bool) -> MCP
|
|
|
433
528
|
raise MCPConfigError(str(exc)) from exc
|
|
434
529
|
if not isinstance(current, dict):
|
|
435
530
|
raise MCPConfigError("Existing OCR mcp_servers value is not a JSON object")
|
|
531
|
+
declared_names = {server.name for server in servers}
|
|
436
532
|
for name, value in current.items():
|
|
437
|
-
|
|
533
|
+
# Explicit operator input replaces the same named inherited entry;
|
|
534
|
+
# validate only the state that will survive into the final registry.
|
|
535
|
+
if name == BUILTIN_EVIDENCE_SERVER or name in declared_names:
|
|
438
536
|
continue
|
|
439
|
-
capability =
|
|
440
|
-
|
|
537
|
+
inherited, capability, inherited_secrets = _existing_server(
|
|
538
|
+
name, value, profile=profile
|
|
539
|
+
)
|
|
540
|
+
payload[name] = inherited
|
|
441
541
|
capabilities.append(capability)
|
|
542
|
+
secret_values.extend(inherited_secrets)
|
|
442
543
|
|
|
443
|
-
secret_values: list[str] = []
|
|
444
544
|
for server in servers:
|
|
445
545
|
payload[server.name] = {
|
|
446
546
|
"type": server.transport,
|
|
447
|
-
"setup": server.setup,
|
|
448
547
|
"tools": server.tools,
|
|
449
548
|
}
|
|
549
|
+
if server.transport == "stdio":
|
|
550
|
+
payload[server.name]["setup"] = server.setup
|
|
450
551
|
if server.transport == "stdio":
|
|
451
552
|
payload[server.name].update(
|
|
452
553
|
{"command": server.command, "args": server.args, "env": server.env}
|
|
@@ -454,7 +555,7 @@ def compose_mcp_servers(servers: list[MCPServerConfig], *, replace: bool) -> MCP
|
|
|
454
555
|
else:
|
|
455
556
|
payload[server.name].update({"url": server.url, "headers": server.headers})
|
|
456
557
|
secret_values.extend(server.secret_values)
|
|
457
|
-
capability = MCPCapability(server.name, tuple(server.tools))
|
|
558
|
+
capability = MCPCapability(server.name, tuple(server.tools), transport=server.transport)
|
|
458
559
|
capabilities = [item for item in capabilities if item.server != server.name]
|
|
459
560
|
capabilities.append(capability)
|
|
460
561
|
|
|
@@ -497,10 +598,14 @@ def compose_mcp_servers(servers: list[MCPServerConfig], *, replace: bool) -> MCP
|
|
|
497
598
|
)
|
|
498
599
|
|
|
499
600
|
|
|
500
|
-
def build_mcp_composition() -> MCPComposition:
|
|
501
|
-
"""Parse environment settings into the complete
|
|
601
|
+
def build_mcp_composition(*, profile: str = "local") -> MCPComposition:
|
|
602
|
+
"""Parse environment settings into the complete profiled MCP composition."""
|
|
502
603
|
|
|
503
|
-
return compose_mcp_servers(
|
|
604
|
+
return compose_mcp_servers(
|
|
605
|
+
parse_mcp_servers(profile=profile),
|
|
606
|
+
replace=_replace_configured_servers(),
|
|
607
|
+
profile=profile,
|
|
608
|
+
)
|
|
504
609
|
|
|
505
610
|
|
|
506
611
|
def apply_mcp_composition(composition: MCPComposition) -> None:
|
{open_code_review_toolkit-0.6.2 → open_code_review_toolkit-0.6.3}/src/ocr_toolkit/ocr_result.py
RENAMED
|
@@ -18,11 +18,13 @@ from ocr_toolkit.common.redaction import sanitize_ocr_value
|
|
|
18
18
|
DEFAULT_MAX_RESULT_BYTES = 2_000_000
|
|
19
19
|
MAX_RESULT_BYTES_HARD_LIMIT = 20_000_000
|
|
20
20
|
TOOLKIT_RESULT_KEY = "_ocr_toolkit"
|
|
21
|
-
TOOLKIT_RESULT_SCHEMA_VERSION =
|
|
22
|
-
SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({1, TOOLKIT_RESULT_SCHEMA_VERSION})
|
|
23
|
-
AUTOMATIC_APPROVAL_BLOCK_REASON = "author-controlled merge-request context was admitted"
|
|
21
|
+
TOOLKIT_RESULT_SCHEMA_VERSION = 3
|
|
22
|
+
SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({1, 2, TOOLKIT_RESULT_SCHEMA_VERSION})
|
|
24
23
|
# The receipt can name the 16 configured external servers plus the mandatory built-in.
|
|
25
24
|
MAX_TOOLKIT_MCP_USAGE_SERVERS = 17
|
|
25
|
+
MAX_TOOLKIT_MCP_TOOLS_PER_SERVER = 128
|
|
26
|
+
MAX_TOOLKIT_MCP_TOOL_NAME_CHARS = 4_096
|
|
27
|
+
MAX_TOOLKIT_MCP_USAGE_COUNT = 1_000_000_000
|
|
26
28
|
TOOLKIT_MCP_SERVER_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
27
29
|
|
|
28
30
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Conservative policy and typed outcomes for GitLab automatic approval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ocr_toolkit.ocr_result import (
|
|
10
|
+
MAX_TOOLKIT_MCP_TOOL_NAME_CHARS,
|
|
11
|
+
MAX_TOOLKIT_MCP_TOOLS_PER_SERVER,
|
|
12
|
+
MAX_TOOLKIT_MCP_USAGE_COUNT,
|
|
13
|
+
MAX_TOOLKIT_MCP_USAGE_SERVERS,
|
|
14
|
+
TOOLKIT_MCP_SERVER_NAME_RE,
|
|
15
|
+
)
|
|
16
|
+
from ocr_toolkit.posting.settings import BooleanSetting
|
|
17
|
+
from ocr_toolkit.result_contract import ReviewOutcome
|
|
18
|
+
|
|
19
|
+
ALLOWED_CATEGORIES = frozenset({"style", "documentation", "maintainability"})
|
|
20
|
+
MAX_APPROVABLE_FINDINGS = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ApprovalStatus(str, Enum):
|
|
24
|
+
"""Closed public states for one automatic-approval transaction."""
|
|
25
|
+
|
|
26
|
+
APPROVED = "approved"
|
|
27
|
+
NOT_ELIGIBLE = "not eligible"
|
|
28
|
+
DISABLED = "disabled"
|
|
29
|
+
SKIPPED = "skipped"
|
|
30
|
+
FAILED = "failed"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ApprovalResult:
|
|
35
|
+
"""Bounded status rendered in the review summary and runner log."""
|
|
36
|
+
|
|
37
|
+
status: ApprovalStatus
|
|
38
|
+
reason: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ApprovalEligibility:
|
|
43
|
+
"""Policy conclusion before provider state is consulted."""
|
|
44
|
+
|
|
45
|
+
eligible: bool
|
|
46
|
+
result: ApprovalResult
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def evaluate_approval_policy(
|
|
50
|
+
setting: BooleanSetting,
|
|
51
|
+
outcome: ReviewOutcome,
|
|
52
|
+
comments: list[dict[str, Any]],
|
|
53
|
+
warnings: list[Any],
|
|
54
|
+
omitted_count: int,
|
|
55
|
+
toolkit_metadata: Any = None,
|
|
56
|
+
) -> ApprovalEligibility:
|
|
57
|
+
"""Evaluate the fixed v0.4.7 policy from authoritative OCR data."""
|
|
58
|
+
|
|
59
|
+
if not setting.enabled:
|
|
60
|
+
reason = (
|
|
61
|
+
"configuration was invalid and failed closed"
|
|
62
|
+
if not setting.valid
|
|
63
|
+
else "disabled by OCR_AUTO_APPROVE"
|
|
64
|
+
)
|
|
65
|
+
return ApprovalEligibility(
|
|
66
|
+
False,
|
|
67
|
+
ApprovalResult(ApprovalStatus.DISABLED, reason),
|
|
68
|
+
)
|
|
69
|
+
metadata_reason = automatic_approval_metadata_reason(toolkit_metadata)
|
|
70
|
+
if metadata_reason:
|
|
71
|
+
reason = metadata_reason
|
|
72
|
+
elif toolkit_metadata["evidence"]["mandatory"] is not outcome.requires_evidence_mcp:
|
|
73
|
+
reason = "the review-time approval receipt is missing or invalid"
|
|
74
|
+
elif not outcome.manifest_present:
|
|
75
|
+
reason = "the OCR result has no authoritative coverage manifest"
|
|
76
|
+
elif outcome.kind != "clean" or outcome.budget_exceeded:
|
|
77
|
+
reason = "the OCR review did not complete cleanly"
|
|
78
|
+
elif outcome.failed_count or outcome.waived_count:
|
|
79
|
+
reason = "coverage contained failed or waived items"
|
|
80
|
+
elif warnings:
|
|
81
|
+
reason = "the OCR review reported warnings"
|
|
82
|
+
elif omitted_count:
|
|
83
|
+
reason = "one or more findings were omitted from publication"
|
|
84
|
+
elif len(comments) > MAX_APPROVABLE_FINDINGS:
|
|
85
|
+
reason = f"the review reported more than {MAX_APPROVABLE_FINDINGS} findings"
|
|
86
|
+
else:
|
|
87
|
+
reason = ""
|
|
88
|
+
|
|
89
|
+
if reason:
|
|
90
|
+
return ApprovalEligibility(
|
|
91
|
+
False,
|
|
92
|
+
ApprovalResult(ApprovalStatus.NOT_ELIGIBLE, reason),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for comment in comments:
|
|
96
|
+
severity = comment.get("severity")
|
|
97
|
+
category = comment.get("category")
|
|
98
|
+
if severity != "low":
|
|
99
|
+
return ApprovalEligibility(
|
|
100
|
+
False,
|
|
101
|
+
ApprovalResult(
|
|
102
|
+
ApprovalStatus.NOT_ELIGIBLE,
|
|
103
|
+
"a finding had a blocking or malformed severity",
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
if not isinstance(category, str) or category not in ALLOWED_CATEGORIES:
|
|
107
|
+
return ApprovalEligibility(
|
|
108
|
+
False,
|
|
109
|
+
ApprovalResult(
|
|
110
|
+
ApprovalStatus.NOT_ELIGIBLE,
|
|
111
|
+
"a finding had a blocking or malformed category",
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return ApprovalEligibility(
|
|
116
|
+
True,
|
|
117
|
+
ApprovalResult(
|
|
118
|
+
ApprovalStatus.SKIPPED,
|
|
119
|
+
"awaiting post-publication SHA verification",
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def approval_summary_line(result: ApprovalResult) -> str:
|
|
125
|
+
"""Render exactly one bounded automatic-approval state line."""
|
|
126
|
+
|
|
127
|
+
return f"- Automatic approval: `{result.status.value}` — {result.reason}."
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def provisional_approval_result(eligibility: ApprovalEligibility) -> ApprovalResult:
|
|
131
|
+
"""Return a fail-closed state safe to publish before provider readback."""
|
|
132
|
+
|
|
133
|
+
if not eligibility.eligible:
|
|
134
|
+
return eligibility.result
|
|
135
|
+
return ApprovalResult(
|
|
136
|
+
ApprovalStatus.FAILED,
|
|
137
|
+
"automatic approval has not yet been confirmed",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _full_sha(value: Any) -> bool:
|
|
142
|
+
return (
|
|
143
|
+
isinstance(value, str)
|
|
144
|
+
and len(value) == 40
|
|
145
|
+
and all(char in "0123456789abcdef" for char in value)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _positive_id(value: Any) -> bool:
|
|
150
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
|
|
154
|
+
"""Return the closed review-time receipt blocker for automatic approval."""
|
|
155
|
+
|
|
156
|
+
invalid = "the review-time approval receipt is missing or invalid"
|
|
157
|
+
if not isinstance(toolkit_metadata, dict):
|
|
158
|
+
return invalid
|
|
159
|
+
schema_version = toolkit_metadata.get("schema_version")
|
|
160
|
+
if schema_version in {1, 2}:
|
|
161
|
+
return "the review-time approval receipt predates current eligibility controls"
|
|
162
|
+
if schema_version != 3 or set(toolkit_metadata) != {
|
|
163
|
+
"schema_version",
|
|
164
|
+
"review",
|
|
165
|
+
"context",
|
|
166
|
+
"mcp",
|
|
167
|
+
"evidence",
|
|
168
|
+
}:
|
|
169
|
+
return invalid
|
|
170
|
+
|
|
171
|
+
review = toolkit_metadata.get("review")
|
|
172
|
+
if not isinstance(review, dict) or set(review) != {"source_sha", "policy_sha", "mr_author_id"}:
|
|
173
|
+
return invalid
|
|
174
|
+
if not _full_sha(review.get("source_sha")) or not _full_sha(review.get("policy_sha")):
|
|
175
|
+
return invalid
|
|
176
|
+
if not _positive_id(review.get("mr_author_id")):
|
|
177
|
+
return invalid
|
|
178
|
+
|
|
179
|
+
context = toolkit_metadata.get("context")
|
|
180
|
+
if not isinstance(context, dict) or set(context) != {"mode", "state", "classes"}:
|
|
181
|
+
return invalid
|
|
182
|
+
mode = context.get("mode")
|
|
183
|
+
state = context.get("state")
|
|
184
|
+
classes = context.get("classes")
|
|
185
|
+
expected_classes = [] if mode == "off" else ["merge_request_metadata"]
|
|
186
|
+
if (
|
|
187
|
+
mode not in {"off", "metadata"}
|
|
188
|
+
or state not in {"disabled", "complete", "degraded"}
|
|
189
|
+
or classes != expected_classes
|
|
190
|
+
or (mode == "off" and state != "disabled")
|
|
191
|
+
or (mode == "metadata" and state == "disabled")
|
|
192
|
+
):
|
|
193
|
+
return invalid
|
|
194
|
+
|
|
195
|
+
mcp = toolkit_metadata.get("mcp")
|
|
196
|
+
if not isinstance(mcp, dict) or set(mcp) != {"capabilities", "usage"}:
|
|
197
|
+
return invalid
|
|
198
|
+
capabilities = mcp.get("capabilities")
|
|
199
|
+
usage = mcp.get("usage")
|
|
200
|
+
if (
|
|
201
|
+
not isinstance(capabilities, list)
|
|
202
|
+
or not 1 <= len(capabilities) <= MAX_TOOLKIT_MCP_USAGE_SERVERS
|
|
203
|
+
or not isinstance(usage, dict)
|
|
204
|
+
or len(usage) > MAX_TOOLKIT_MCP_USAGE_SERVERS
|
|
205
|
+
):
|
|
206
|
+
return invalid
|
|
207
|
+
servers: set[str] = set()
|
|
208
|
+
tool_owners: set[str] = set()
|
|
209
|
+
external = False
|
|
210
|
+
builtin_server = "ocr_toolkit_evidence"
|
|
211
|
+
for capability in capabilities:
|
|
212
|
+
if not isinstance(capability, dict) or set(capability) != {"server", "transport", "tools"}:
|
|
213
|
+
return invalid
|
|
214
|
+
server = capability.get("server")
|
|
215
|
+
transport = capability.get("transport")
|
|
216
|
+
tools = capability.get("tools")
|
|
217
|
+
if (
|
|
218
|
+
not isinstance(server, str)
|
|
219
|
+
or TOOLKIT_MCP_SERVER_NAME_RE.fullmatch(server) is None
|
|
220
|
+
or server in servers
|
|
221
|
+
or transport not in {"builtin", "stdio", "remote"}
|
|
222
|
+
or not isinstance(tools, list)
|
|
223
|
+
or not 1 <= len(tools) <= MAX_TOOLKIT_MCP_TOOLS_PER_SERVER
|
|
224
|
+
or any(
|
|
225
|
+
not isinstance(tool, str) or not tool or len(tool) > MAX_TOOLKIT_MCP_TOOL_NAME_CHARS
|
|
226
|
+
for tool in tools
|
|
227
|
+
)
|
|
228
|
+
or len(set(tools)) != len(tools)
|
|
229
|
+
or any(tool in tool_owners for tool in tools)
|
|
230
|
+
or (server == builtin_server) != (transport == "builtin")
|
|
231
|
+
or (server == builtin_server and tools != [builtin_server])
|
|
232
|
+
):
|
|
233
|
+
return invalid
|
|
234
|
+
servers.add(server)
|
|
235
|
+
tool_owners.update(tools)
|
|
236
|
+
external = external or server != builtin_server
|
|
237
|
+
if builtin_server not in servers:
|
|
238
|
+
return invalid
|
|
239
|
+
if any(
|
|
240
|
+
not isinstance(server, str)
|
|
241
|
+
or server not in servers
|
|
242
|
+
or not isinstance(count, int)
|
|
243
|
+
or isinstance(count, bool)
|
|
244
|
+
or not 0 < count <= MAX_TOOLKIT_MCP_USAGE_COUNT
|
|
245
|
+
for server, count in usage.items()
|
|
246
|
+
):
|
|
247
|
+
return invalid
|
|
248
|
+
|
|
249
|
+
evidence = toolkit_metadata.get("evidence")
|
|
250
|
+
if not isinstance(evidence, dict) or set(evidence) != {"mandatory", "used"}:
|
|
251
|
+
return invalid
|
|
252
|
+
mandatory = evidence.get("mandatory")
|
|
253
|
+
used = evidence.get("used")
|
|
254
|
+
evidence_called = usage.get(builtin_server, 0) > 0
|
|
255
|
+
if (
|
|
256
|
+
not isinstance(mandatory, bool)
|
|
257
|
+
or not isinstance(used, bool)
|
|
258
|
+
or used is not evidence_called
|
|
259
|
+
or (mandatory and not used)
|
|
260
|
+
):
|
|
261
|
+
return invalid
|
|
262
|
+
if context.get("state") == "degraded":
|
|
263
|
+
return "the selected review context was degraded"
|
|
264
|
+
if external:
|
|
265
|
+
return "external MCP was configured for a comment-only review"
|
|
266
|
+
return ""
|
|
@@ -488,7 +488,11 @@ def format_mcp_usage_summary(toolkit_metadata: Any) -> str:
|
|
|
488
488
|
or toolkit_metadata.get("schema_version") not in SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS
|
|
489
489
|
):
|
|
490
490
|
return ""
|
|
491
|
-
|
|
491
|
+
if toolkit_metadata.get("schema_version") == 3:
|
|
492
|
+
mcp = toolkit_metadata.get("mcp")
|
|
493
|
+
mcp_usage = mcp.get("usage") if isinstance(mcp, dict) else None
|
|
494
|
+
else:
|
|
495
|
+
mcp_usage = toolkit_metadata.get("mcp_usage")
|
|
492
496
|
if not isinstance(mcp_usage, dict):
|
|
493
497
|
return ""
|
|
494
498
|
used: list[tuple[str, int]] = []
|