diff --git a/PLANS.md b/PLANS.md index ca171a2..ff22863 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1,7 +1,154 @@ # Execution Plans -Use this file for active or blocked repository work. Update it before implementation and before handoff or commit. Completed stable plans are indexed in [the execution-history archive](docs/engineering/execution_history/README.md). +Use this file for active or blocked repository work. Update it before implementation and +before handoff or commit. Completed stable plans are indexed in +[the execution-history archive](docs/engineering/execution_history/README.md). ## Active Work -No active or blocked repository work. +### Toolkit 0.8.6 — OCR 1.11.0 and precise security signals + +Status: active — Draft implementation exists; independent review and full stable delivery +are in progress. + +Release classification: `release-required` + +Target stable version: `0.8.6` + +#### Goal + +Deliver toolkit 0.8.6 as an immutable stable release. The release must qualify and require +OCR 1.11.0, fix false security promotion for neutral domain phrases, keep provider-private +reasoning and request state outside every public/receipt/approval sink, pass local and hosted +validation, and finish with independently verified registry and GitHub artifacts, closed +release issues and milestone, and synchronized clean `main`. + +#### Scope And Decisions + +- Continue from Draft PR #154 and issues #153/#155; inspect the complete Draft rather than + assuming its existing green checks prove readiness. +- OCR 1.11.0 is the sole supported runtime. OCR 1.10.2 remains comparison evidence only. +- Preserve one provider-neutral flow: + `private OCR state -> canonical projection -> DLP -> publication -> receipt/approval`. +- Keep grouping inventory, timeout scaling, Handlebars/Mustache Rules, `file_find`, result, + manifest, receipt v5, DLP, telemetry, summary, and approval contracts internally + consistent and covered by synthetic tests. +- Fix #153 only in reviewer-guide analytics: neutral phrases such as knowledge or dependency + injection must not increase the security count or effort, while explicit metadata and a + closed set of vulnerability phrases remain promoted. +- Required configuration variables remain bold in public documentation. +- Do not set or recommend a provider-specific `4096` completion cap. The active default is + unset and inherits the qualified OCR/provider contract. Historical wire evidence may retain + its exact tested value when explicitly labelled as historical evidence. +- Update the PATH-effective local Darwin arm64 OCR atomically to checksum-verified 1.11.0. +- Run one semantic local OCR review against the exact final feature head with concurrency 1. + If it fails exclusively because every provider request is HTTP 429, record the gate as + owner-authorized `waived`, not passed, do not retry again, and continue release delivery. +- Keep full test and OCR output in ignored owner-only temporary logs. Public plans, issues, + PRs, changelog, release notes, and summaries contain only synthetic or bounded structural + evidence and never private paths, credentials, provider/model identity, prompts, reasoning, + tool payloads, request IDs, or raw OCR output. +- macOS hosted checks remain advisory; Linux supported-Python checks and protected release + authorization are the release priority. +- No B2B, `core/common`, shared-template, or consumer-repository integration is in scope. + +#### Requirements And Evidence + +| ID | Requirement | Authoritative evidence | +| --- | --- | --- | +| `R1` | Exact OCR 1.11.0 qualification and sole-runtime pin | Upstream release/source review, compatibility evidence/hash validation, preflight/example tests, local binary digest/version/help | +| `R2` | Correct grouping/rules/timeout/file lookup behavior | Qualification-only parser tests, installed-artifact no-LLM probes, runtime contract tests | +| `R3` | Provider-private fields cannot affect public state | Hostile projection, DLP, receipt, cleanup, telemetry, summary, and approval tests | +| `R4` | #153 false-positive classification fixed without broad regression | Exact reproducer plus positive, neutral, Unicode, boundary, determinism, and immutability tests | +| `R5` | Documentation/examples/changelog are current and consistent | Documentation contracts, version searches, Towncrier draft, complete diff review | +| `R6` | Feature tree is release-ready | Targeted tests, full quality/coverage, security, package determinism, local OCR, self-review, exact PR head/check/thread readback | +| `R7` | Stable release is authorized and published | Release plan/receipt, release PR exact-head checks, protected stable workflow success | +| `R8` | Publication is independently closed | Byte equality across workflow/TestPyPI/PyPI/Release, provenance and attestations, tag target, Python 3.12–3.14 installs, release-note/asset readback | +| `R9` | Repository and tracking state are closed | Actions-owned issue receipts, #153/#155 closed, milestone closed, archived plan, clean `main == origin/main == v0.8.6^{}` | + +#### Work Queue + +| Queue | Status | Deliverable | +| --- | --- | --- | +| `WQ-01` | `done` | Live `main`, Draft PR, issues, milestone, upstream release, roadmap/backlog, and release owners inspected. | +| `WQ-02` | `done` | Draft #154 implementation and documentation audited; release plan and completion-cap wording corrected. | +| `WQ-03` | `done` | Exact local OCR 1.11.0 installed and qualified; the single concurrency-1 semantic review completed 7/7 selected files. | +| `WQ-04` | `done` | The one confirmed OCR finding is fixed; full gates and holistic self-review pass, and the signed final feature-head commit is ready to bind. | +| `WQ-05` | `in progress` | Push the final feature head, update and mark PR ready, verify protected checks/threads/policy, merge, and verify development publication. | +| `WQ-06` | `pending` | Prepare signed `release/v0.8.6` state, generated changelog, archived plan, receipt inputs, and release PR. | +| `WQ-07` | `pending` | Verify release PR, merge exact reviewed tree, and complete the protected stable workflow. | +| `WQ-08` | `pending` | Independently verify registries, bytes, provenance, attestations, tag, immutable Release, receipt, and supported-Python installs. | +| `WQ-09` | `pending` | Verify issue receipts, close issues/milestone, synchronize clean `main`, remove task-owned temporary material, and perform final audit. | + +#### Validation Contract + +- During iteration: focused tests for every changed parser, subprocess, persistence, DLP, + posting, receipt, configuration, compatibility, and documentation boundary. +- Final feature gate: `scripts/quality.sh check`, all coverage floors, compatibility manifest, + lockfile, Towncrier draft, Ruff format/lint, strict MyPy, Bandit, checksum-verified + Gitleaks, package build/determinism, and `git diff --check`. +- OCR gate: exact installed 1.11.0 identity and one semantic review at concurrency 1; inspect + the result/manifest and coverage rather than process exit alone. A pure repeated HTTP 429 is + a documented waiver, never a pass. +- PR gates: exact head/base/tree, all required checks complete, no unresolved review threads, + current merge policy, and re-read immediately before merge. +- Stable closure: independent artifact bytes, TestPyPI/PyPI metadata and PEP 740 provenance, + GitHub attestations and immutable Release, annotated tag peeled target, receipt schema and + hashes, clean wheel/sdist installs on Python 3.12–3.14, issue/milestone receipts, and clean + synchronized `main`. + +#### Risks And Recovery + +- A Draft-green check can miss semantic or privacy defects. Fix only evidence-backed findings, + repeat the affected gate, and then rerun the single complete final gate. +- Local OCR replacement can fail. Verify the candidate before atomic replacement and retain a + verified rollback copy until 1.11.0 passes local identity and no-LLM probes. +- Provider rate limiting can make semantic OCR unavailable. After the single concurrency-1 run, + accept only an all-429 outcome for the authorized waiver; mixed or product failures require + diagnosis and remediation. +- Registry propagation can lag. Verify JSON/simple-index state and retry once without cache; + do not misclassify cache lag as an artifact defect. +- Stable release publication is irreversible. Re-read exact release head, authorization, + checks, and receipt inputs immediately before merge; stop on any mismatch. + +#### Current Evidence + +- Draft PR #154 starts from released v0.8.5 and had 13/13 hosted checks green at head + `79d5587`; issues #153/#155 and milestone v0.8.6 are open. +- Hosted OCR compatibility run 33158664020 verified the official 1.11.0 assets and generated + the accepted human-review-required evidence. Adjacent source review maps each consumed + contract or records it as no-impact. +- PATH-effective OCR is official Darwin arm64 1.11.0 with SHA-256 + `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. + The installed `probe-local` contract passed version/help, preview, grouping, Rules, result, + budget, numeric CLI, and completion-cap checks. +- The only configured-provider OCR review ran at concurrency 1 on exact head `d2249ab` and + finished in 396 seconds with manifest `complete`: 7 selected, 7 completed, 0 failed, + 0 reused, and 0 waived. It returned one medium bug finding; this is a passing semantic + gate, so no provider waiver applies. +- The finding correctly identified that the qualification-only grouping parser accepted the + historical wire shape only for 1.10.2 even though repeatable semantic probes cover the full + qualified 1.10.0–1.10.2 line. The parser now binds that exact old shape to all 1.10.x + releases while production preflight remains exact 1.11.0. All 86 compatibility tests pass. +- Targeted privacy/posting/configuration/release suites pass 480 tests plus 243 subtests; + manifest validation, Towncrier draft, Ruff format check, and diff checks pass. +- The final canonical gate passes 1,321 tests plus 363 subtests at 86.52% branch + coverage. The four locked risk groups pass at 85%, 82%, 86%, and 87%; Ruff format/lint, + strict MyPy, and Bandit pass in the same run. +- Independent lock and OCR-manifest validation, rendered 0.8.6 Towncrier draft, + dependency audit, pinned Gitleaks, and `git diff --check` pass. Two deterministic + wheel/sdist builds are byte-identical; Twine and archive content/privacy checks pass, + and clean wheel plus sdist installs and CLI smoke pass on Python 3.12, 3.13, and 3.14. +- Holistic feature-diff self-review found and fixed one documentation drift: the current + compatibility guide now describes only an operator-selected positive completion-cap + override and the full qualification-only historical 1.10.x inventory parser range. + Machine qualification evidence retains its exact checksum-bound tested probe. No other + correctness, privacy, release, documentation, or example inconsistency remains. + +#### Closure Gate + +- [ ] Every requirement has direct current-state evidence. +- [ ] Feature and release PRs are merged from exact reviewed heads with required checks green. +- [ ] Stable 0.8.6 artifacts, provenance, attestations, tag, Release, receipt, and installs are independently verified. +- [ ] Issues #153/#155 and milestone v0.8.6 are closed through truthful release evidence. +- [ ] The active plan is archived, local and remote release branches are removed, task-owned temporary material is cleaned, and `main` is clean and synchronized. diff --git a/README.md b/README.md index 73cbd9b..564dec3 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,45 @@ Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Aliba ## Install -Install the Python package from PyPI and install a supported OCR binary separately: +Python 3.12 through 3.14 is supported. Install the toolkit CLI in an isolated tool +environment: ```console +uv tool install open-code-review-toolkit +``` + +If `uv` is unavailable, use `pip` only inside an activated virtual environment; do not +install the toolkit into a global Python environment: + +```console +python -m venv .venv +. .venv/bin/activate python -m pip install open-code-review-toolkit +``` + +The toolkit does not bundle OCR. Download the platform asset for exact +[Open Code Review 1.11.0](https://github.com/alibaba/open-code-review/releases/tag/v1.11.0), +verify it before installation against the [compatibility manifest](compatibility/ocr-support.json), +and place the verified binary on `PATH`. The current Linux amd64 digest is +`13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; Darwin arm64 is +`ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. The manifest owns +the other platform hashes. + +Verify the installation without contacting an LLM provider: + +```console ocr --version ocr-ci --help ``` +`ocr --version` must report `open-code-review v1.11.0`; `ocr-ci --help` must exit +successfully. `ocr-ci preflight` is an operational configuration check, not the installation +smoke test. + The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution. The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases. Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`. -The current OCR 1.10.2 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries. +The current OCR 1.11.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries. Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI. diff --git a/changelog.d/153.bugfix.md b/changelog.d/153.bugfix.md new file mode 100644 index 0000000..ed74a41 --- /dev/null +++ b/changelog.d/153.bugfix.md @@ -0,0 +1,5 @@ +Correct reviewer-guide security classification: + +- **Fixed:** command/shell, SQL/NoSQL, code, template, prompt, LDAP, XPath, CRLF/header, log, HTML/script, and expression injection require explicit vulnerability context; the standalone word `injection` is no longer promoted. +- **Changed:** neutral phrases such as knowledge or dependency injection no longer inflate the published security count or estimated review effort. +- **Unchanged:** finding severity, lifecycle, suppression, and approval behavior. diff --git a/changelog.d/155.bugfix.md b/changelog.d/155.bugfix.md new file mode 100644 index 0000000..0996ec7 --- /dev/null +++ b/changelog.d/155.bugfix.md @@ -0,0 +1,5 @@ +Contain provider-private OCR state at the publication boundary: + +- **Fixed:** reasoning, encrypted/native replay payloads, and request `tool_choice` fields are removed before the canonical OCR result is persisted or receipt v5 is bound. +- **Changed:** private-only removal preserves a byte-equivalent review projection and approval eligibility; the same fields laundered through findings or warnings become `publication-filtered`. +- **Unchanged:** receipt v5, public tool-call and reasoning-token counters, DLP reason counts for private-only state, and approval rules. diff --git a/changelog.d/155.doc.md b/changelog.d/155.doc.md new file mode 100644 index 0000000..4977f54 --- /dev/null +++ b/changelog.d/155.doc.md @@ -0,0 +1,4 @@ +Refresh installation and operating guidance: + +- **Changed:** Python 3.12–3.14 uses `uv tool install` by default or `pip` only in an activated virtual environment; OCR is installed separately as checksum-verified 1.11.0 and verified with `ocr --version` plus `ocr-ci --help`. +- **Clarified:** max-tools runtime is unchanged despite corrected help text; reasoning and `tool_choice` stay private; reviewer-guide injection matching is contextual; Ruff formatting is applied and checked before each Python commit. diff --git a/changelog.d/155.maintenance.md b/changelog.d/155.maintenance.md new file mode 100644 index 0000000..62976cf --- /dev/null +++ b/changelog.d/155.maintenance.md @@ -0,0 +1,6 @@ +Qualify the toolkit 0.8.6 OCR dependency: + +- **Changed:** Open Code Review 1.11.0 is the sole accepted runtime; grouping inventory is status-first with churn and the GitLab job timeout is 45 minutes for the default-medium/highest supported round envelope. +- **Added:** checksum-verified Linux and Darwin evidence plus strict historical-1.10.x-old/1.11.0-new qualification fixtures. +- **Migration:** deploy toolkit 0.8.6 directly with OCR 1.11.0. OCR 1.10.2 is comparison evidence only, not a supported fallback. +- **Unchanged:** result/manifest, inherited completion cap `16384`, support for an operator-selected positive completion-cap override, default effort `medium`, receipt v5, DLP, telemetry, summary, and approval contracts. The toolkit leaves that override unset by default and does not recommend a provider-specific value. OCR 1.11.0 corrects max-tools help text only: effective omitted/`0`/`49`/`50` remains `100`, while `101` raises the cap to `101`. diff --git a/changelog.d/155.rules.md b/changelog.d/155.rules.md new file mode 100644 index 0000000..e7a9bd6 --- /dev/null +++ b/changelog.d/155.rules.md @@ -0,0 +1 @@ +**Added:** review Handlebars (`.hbs`) and Mustache (`.mustache`) files through OCR 1.11.0's built-in allowlist and language-specific Rules. Toolkit evidence and publication authority are unchanged. diff --git a/compatibility/evidence/ocr-1.11.0.json b/compatibility/evidence/ocr-1.11.0.json new file mode 100644 index 0000000..cb688b8 --- /dev/null +++ b/compatibility/evidence/ocr-1.11.0.json @@ -0,0 +1,206 @@ +{ + "assets": [ + { + "name": "opencodereview-darwin-amd64", + "sha256": "c129af341b52ead65f860831593573b11d1099b434f98cb37d2f885673592109", + "size": 57703712 + }, + { + "name": "opencodereview-darwin-arm64", + "sha256": "ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78", + "size": 54760802 + }, + { + "name": "opencodereview-linux-amd64", + "sha256": "13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60", + "size": 55959714 + }, + { + "name": "opencodereview-linux-arm64", + "sha256": "e2177621b967ed332240dd30841b538b38467b778245b68af37583e4c76f583e", + "size": 52691106 + }, + { + "name": "opencodereview-windows-amd64.exe", + "sha256": "2809ff67e36f40c75dba756f77cb48c5fe12bc6893b0a896a51b6f1522e285a9", + "size": 57422336 + }, + { + "name": "opencodereview-windows-arm64.exe", + "sha256": "c308ee95825191291b56d817681e3248bfc3380f532df679cad2236e95eb0a9c", + "size": 53395968 + }, + { + "name": "sha256sum.txt", + "sha256": "9dff050ec859882bef26037415b8bd9e5db70c5a7d960e5eb3989385372311ee", + "size": 572 + } + ], + "classification": "human-review-required", + "classification_reasons": [ + "candidate is not a newer patch in the tested major/minor line", + "release notes contain a material or ambiguous compatibility signal" + ], + "comparison_version": "1.10.2", + "contracts": { + "comment_thinking_probe": { + "additive_field_preserved": true, + "posting_exposes_thinking": false, + "result": "passed" + }, + "completion_cap_probe": { + "explicit": 4096, + "inherited": 16384, + "result": "passed", + "wire_field": "max_completion_tokens" + }, + "numeric_cli_probe": { + "max_tokens_budget": { + "cases": { + "invalid_below": { + "effective": null, + "input": -1, + "outcome": "rejected" + }, + "minimum": { + "effective": 1, + "input": 1, + "outcome": "accepted" + }, + "omitted": { + "effective": "unlimited", + "input": null, + "outcome": "accepted" + }, + "representative": { + "effective": 30000, + "input": 30000, + "outcome": "accepted" + }, + "sentinel": { + "effective": "unlimited", + "input": 0, + "outcome": "accepted" + } + }, + "maximum": null, + "owner": "ocr-cli" + }, + "max_tools": { + "cases": { + "invalid_below": { + "effective": null, + "input": -1, + "outcome": "rejected" + }, + "minimum": { + "effective": 100, + "input": 50, + "outcome": "accepted" + }, + "minimum_minus_one": { + "effective": 100, + "input": 49, + "outcome": "normalized", + "reported_normalization": 50 + }, + "omitted": { + "effective": 100, + "input": null, + "outcome": "accepted" + }, + "representative": { + "effective": 101, + "input": 101, + "outcome": "accepted" + }, + "sentinel": { + "effective": 100, + "input": 0, + "outcome": "accepted" + } + }, + "maximum": null, + "owner": "ocr-template-or-higher-cli", + "reported_minimum": 50 + }, + "result": "passed" + }, + "optional_capabilities": [ + "llm_result_identity", + "per_run_model_override", + "per_run_provider_override", + "review_effort", + "semantic_grouping" + ], + "preview_probe": { + "format": "json", + "path": "example.py", + "result": "passed", + "session_store_created": false + }, + "required_review_flags": [ + "--audience", + "--background-file", + "--effort", + "--format", + "--from", + "--max-tokens-budget", + "--max-tools", + "--preview", + "--rule", + "--to" + ], + "result_contract_probe": { + "additive_fields_allowed": true, + "comment_fields": [ + "category", + "content", + "end_line", + "existing_code", + "path", + "severity", + "start_line", + "thinking" + ], + "manifest_schema": "ocr.run-manifest/v1", + "normalized_outcome": "clean", + "result": "passed" + }, + "review_budget_probe": { + "budget": 30000, + "completed": 2, + "failed_budget": 1, + "partial_findings_preserved": true, + "result": "passed", + "selected": 3 + }, + "semantic_grouping_probe": { + "default_effort": "medium", + "filter_requests": 1, + "grouping_completion_cap": 16384, + "grouping_requests": 1, + "main_requests": 3, + "result": "passed", + "review_rounds": 2 + }, + "target_rule_selection_probe": { + "format": "json", + "from_to_unchanged": true, + "path": "synthetic-template.ocrfixture", + "result": "passed", + "source_exclusion": "unsupported_ext", + "target_selected": true + }, + "version_probe": "passed" + }, + "published_at": "2026-08-28T06:06:56Z", + "release_changes": "## 🚀 Features\n\n- feat(allowlist): add Handlebars and Mustache support (#1086)\n- feat(config): validate provider existence before auxiliary fields (#1093)\n- feat(agent): include churn stats (+N/-M) in the other-changed-files list (#1078) (#1082)\n\n## 🐛 Bug Fixes\n\n- fix(llm): preserve provider reasoning/thinking across assistant turns (#1070)\n- fix(agent): scale subtask timeout linearly with effort review rounds (#1085)\n- fix(filter): omit forced tool choice (#1065)\n- fix(tool): support subpath and cross-platform queries in file_find (#1075)\n\n## 📖 Documentation\n\n- docs(ko): polish the six new Korean pages (#1087)\n- docs(ko): complete the Korean docs locale (#1084)\n- docs(ko): add Korean translation for the core user guide (#1080)\n- docs(cli): align --max-tools help text with min 50 clamp (#1077)\n\n**Full Changelog**: https://github.com/alibaba/open-code-review/compare/v1.10.2...v1.11.0", + "release_notes_sha256": "a68802ee02c22c1b02955f5307f79427b379c14a76b5c2ce6545b5e9fc82b5b0", + "result": "compatible", + "schema_version": 2, + "tag": "v1.11.0", + "tested_baseline_version": "1.10.2", + "upstream_repository": "alibaba/open-code-review", + "version": "1.11.0" +} diff --git a/compatibility/ocr-support.json b/compatibility/ocr-support.json index 3949488..e3ce86c 100644 --- a/compatibility/ocr-support.json +++ b/compatibility/ocr-support.json @@ -1,6 +1,6 @@ { - "monitoring_floor": "1.10.2", - "recommended_version": "1.10.2", + "monitoring_floor": "1.11.0", + "recommended_version": "1.11.0", "releases": [ { "assets": [ @@ -1293,6 +1293,59 @@ "release_url": "https://github.com/alibaba/open-code-review/releases/tag/v1.10.2", "status": "tested", "version": "1.10.2" + }, + { + "assets": [ + { + "name": "opencodereview-darwin-amd64", + "sha256": "c129af341b52ead65f860831593573b11d1099b434f98cb37d2f885673592109", + "size": 57703712 + }, + { + "name": "opencodereview-darwin-arm64", + "sha256": "ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78", + "size": 54760802 + }, + { + "name": "opencodereview-linux-amd64", + "sha256": "13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60", + "size": 55959714 + }, + { + "name": "opencodereview-linux-arm64", + "sha256": "e2177621b967ed332240dd30841b538b38467b778245b68af37583e4c76f583e", + "size": 52691106 + }, + { + "name": "opencodereview-windows-amd64.exe", + "sha256": "2809ff67e36f40c75dba756f77cb48c5fe12bc6893b0a896a51b6f1522e285a9", + "size": 57422336 + }, + { + "name": "opencodereview-windows-arm64.exe", + "sha256": "c308ee95825191291b56d817681e3248bfc3380f532df679cad2236e95eb0a9c", + "size": 53395968 + }, + { + "name": "sha256sum.txt", + "sha256": "9dff050ec859882bef26037415b8bd9e5db70c5a7d960e5eb3989385372311ee", + "size": 572 + } + ], + "capabilities": [ + "llm_result_identity", + "per_run_model_override", + "per_run_provider_override", + "review_effort", + "semantic_grouping" + ], + "evidence": "compatibility/evidence/ocr-1.11.0.json", + "evidence_sha256": "97c56e301aab5298d5b8d2182afdda33d6cb704abf7bf476f04c668b95b61f22", + "human_conclusion": "Compatible after hosted checksum-verified Linux qualification in run 33158664020 and direct adjacent source review in issue #155. OCR 1.11.0 changes grouping and other-file inventory to status-first entries with churn, preserves provider-native reasoning across assistant turns, removes the filter's forced tool choice, scales subtask timeout by review rounds, improves file_find subpaths, and adds Handlebars/Mustache rules. The toolkit qualification harness consumes the inventory shape and runtime-facing boundaries while provider reasoning and native payloads remain private and unconsumed. Result/manifest schemas, inherited 16384 completion cap, operator-selected positive override support, medium effort, max-tools, receipt v5, DLP, telemetry, and approval contracts remain unchanged; the toolkit leaves the override unset and recommends no provider-specific value. Upstream action, plugin, launcher, locale, provider preset, and config-command validation changes are not consumed.", + "published_at": "2026-08-28T06:06:56Z", + "release_url": "https://github.com/alibaba/open-code-review/releases/tag/v1.11.0", + "status": "tested", + "version": "1.11.0" } ], "schema_version": 1, diff --git a/docs/codex/TASKS_BACKLOG.md b/docs/codex/TASKS_BACKLOG.md index f6f0819..a0e6784 100644 --- a/docs/codex/TASKS_BACKLOG.md +++ b/docs/codex/TASKS_BACKLOG.md @@ -89,7 +89,7 @@ Provider/network telemetry remains outside M1 and M5. OCR owns token, cost, budg - **Acceptance criteria:** The use case and completion signal are documented before implementation; false-positive behavior and unsupported versions are explicit through the shared scoped coverage contract. - **Exclusions:** Checkbox coverage, network resolution, runtime execution, or unrelated bundles. - **Validation:** Pack fixtures plus common evidence/bootstrap/MCP contracts. -- **Upstream overlap:** OCR language allowlists and review rules are review-engine capabilities; a new reviewable language alone does not activate an evidence pack. +- **Upstream overlap:** OCR language allowlists and review rules are review-engine capabilities; OCR 1.11.0 Handlebars/Mustache selection improves review coverage but does not supply a framework evidence contract or activate an evidence pack. - **Release classification expectation:** `release-required`. ### BL-019: Run a native fuzzing campaign @@ -127,7 +127,7 @@ Provider/network telemetry remains outside M1 and M5. OCR owns token, cost, budg - **Roadmap theme:** M7 Later and conditional work - **Dependencies:** Stable provider-neutral core contracts and a funded non-GitLab use case. GitLab-first M5 does not depend on it. - **Activation trigger:** A named forge has an owner, synthetic fixtures, and explicit parity requirements for CI orchestration, positioning, deduplication, discussion ownership, and safe publication. -- **Upstream overlap:** OCR 1.10.2 reusable GitHub Action checkpoint ranges cover only upstream Action range selection. They do not provide toolkit forge acquisition, discussion, publication, or lifecycle parity, so the trigger and acceptance criteria remain unmet. +- **Upstream overlap:** OCR 1.10.2 reusable GitHub Action checkpoint ranges and OCR 1.11.0 Action/plugin changes cover only upstream execution surfaces. They do not provide toolkit forge acquisition, discussion, publication, or lifecycle parity, so the trigger and acceptance criteria remain unmet. - **Goal:** Add one coherent host adapter without leaking forge semantics into evidence or core result handling. - **Scoped deliverables:** The capability matrix covers authentication, diff positions, drafts, discussion acquisition, provider-declared account classification, thread/reply structure, edit/version identity, anchors, resolved/stale state, pagination/snapshot mutation, ambiguous writes, permissions, and idempotency. - **Acceptance criteria:** Core remains provider-neutral, GitLab behavior does not regress, unsupported host capabilities fail or degrade explicitly rather than emulate unsafe parity, and the new host meets the approved lifecycle and security matrix. diff --git a/docs/compatibility.md b/docs/compatibility.md index 4063d72..6c135ab 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -63,7 +63,7 @@ Deploy toolkit 0.8.4 directly with OCR 1.10.1. The public Linux amd64 example ve ### OCR 1.10.2 — toolkit 0.8.5 target -OCR 1.10.2 is the only version accepted by the toolkit 0.8.5 development preflight and pinned by the current GitLab example. Hosted run `33055459209`, canonical issue #151, adjacent source review, and checksum-verified isolated Darwin checks preserve the OCR result, `ocr.run-manifest/v1`, default-medium two-round review, effective max-tools, completion-cap override, DLP, receipt v5, telemetry ownership, and approval contracts. +OCR 1.10.2 is the historical version accepted by toolkit 0.8.5 preflight and pinned by its published GitLab example. Hosted run `33055459209`, canonical issue #151, adjacent source review, and checksum-verified isolated Darwin checks preserve the OCR result, `ocr.run-manifest/v1`, default-medium two-round review, effective max-tools, completion-cap override, DLP, receipt v5, telemetry ownership, and approval contracts. OCR now records grouping tasks and grace-round requests in its private session/retry state. The grouping request uses the review template's inherited OpenAI completion cap `16384` instead of the separate `4096` used by the predecessor; an explicit positive `OCR_LLM_MAX_COMPLETION_TOKENS` still overlays every protocol request. Its value remains deployment-owned. These private task records and group/path values do not enter toolkit findings, GitLab text, receipts, DLP signals, telemetry, or approval inputs. @@ -73,6 +73,20 @@ The hosted qualification initially labelled the patch `automatic-safe` because m Deploy toolkit 0.8.5 directly with OCR 1.10.2. The public Linux amd64 example verifies SHA-256 `e9205614f80e009ee7b1f444c9da08486fb9ff6db022954fe9203d923ab720b2`; the independently checked Darwin arm64 artifact verifies `74fc7bcc0e6d0790c5ca033fd82a5474b6f05d443ed51a26a6f61c0cac6589fd`, and the upstream `sha256sum.txt` verifies `b5176aaa04a7f00bd84dd61556ca29e6cbdfcfe64cc50af6653163d9be4e7654`. +### OCR 1.11.0 — toolkit 0.8.6 target + +OCR 1.11.0 is the only runtime version accepted by toolkit 0.8.6 preflight and pinned by its GitLab example. OCR 1.10.2 remains a strict comparison baseline for the grouping-inventory transition and is not a supported fallback. Hosted run `33158664020`, canonical issue #155, adjacent source review, and checksum-verified evidence preserve the OCR result, `ocr.run-manifest/v1`, inherited OpenAI completion cap `16384`, support for an operator-selected positive completion-cap override, default-medium two-round review, receipt v5, DLP, toolkit telemetry, summary, and approval contracts. The toolkit leaves that override unset and does not recommend a provider-specific value. + +The private grouping and other-files prompts now use status-first `STATUS path (+N/-M)` inventory entries. Repeatable qualification accepts the old `path (STATUS, +N/-M)` shape only for historical OCR 1.10.0 through 1.10.2 and the new shape from OCR 1.11.0 onward; the 1.11.0 promotion compares the adjacent 1.10.2 and 1.11.0 evidence. Mixed, duplicate, reordered, malformed, status-inconsistent, truncated, or churn-losing evidence fails closed. Production toolkit code consumes neither format. `file_find` now supports repository-relative subpaths and both slash styles. The built-in allowlist and Rules add Handlebars (`.hbs`) and Mustache (`.mustache`). OCR's base subtask timeout is now 15 minutes and scales linearly with low/medium/high review rounds to 15/30/45 minutes, so the GitLab job allows 45 minutes. + +OCR 1.11.0 preserves provider-native reasoning across assistant turns, removes the filter's forced `tool_choice`, and retains readable reasoning plus signed or encrypted native replay payloads inside the OCR session. The toolkit does not consume that state: its isolated session home is removed before publication, and reasoning, native payloads, and request controls cannot enter findings, GitLab notes, receipt v5, DLP counters, toolkit telemetry, tool/token summary, or approval inputs. Explicit operator-owned `OCR_LLM_EXTRA_BODY.tool_choice` remains distinct from OCR's removed forced filter value and is not published. + +The release corrects stale `--max-tools` help text; max-tools runtime behavior is unchanged from the behaviorally qualified 1.10.2 baseline. Omitted and sentinel `0` select template default `100`; `1-49` reports normalization to `50`, while normalized `50` and explicit `50` remain effectively `100`; only a value above `100` raises the effective cap. The toolkit's closed normalization notice remains diagnostics-only and cannot affect findings, completeness, receipt, DLP, telemetry, lifecycle, or approval. + +Upstream GitHub Action, OpenCode plugin, npm launcher, Korean documentation, built-in provider presets, and interactive provider-validation changes are not toolkit execution paths and are not claimed as toolkit behavior. + +Deploy toolkit 0.8.6 directly with OCR 1.11.0; do not install 1.10.2 as an intermediate version. The public Linux amd64 example verifies SHA-256 `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; the independently verified Darwin arm64 artifact uses `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`, and the upstream `sha256sum.txt` verifies `9dff050ec859882bef26037415b8bd9e5db70c5a7d960e5eb3989385372311ee`. + The contiguous 1.9.7 through 1.9.8 promotion combines the repository-owned hosted Linux qualification with adjacent upstream source review. It also repeats the required 1.9.8 contract probes through the checksum-verified Darwin arm64 artifact. Multi-tool MCP initialization, fixed tool discovery and dispatch, and owner-only session persistence remain compatible with the M5 design: the toolkit may expose several fixed tools from its one built-in process, while an isolated `HOME` contains OCR's `.opencodereview/sessions` state for deterministic cleanup. Gemini and Bedrock are optional upstream provider capabilities that the toolkit neither selects nor configures; mirror-aware installation is outside the toolkit-owned download path; native severity retains the structured result fields already consumed; and human-audience progress routing does not affect the toolkit's agent-audience invocation. Therefore this promotion requires no toolkit CI behavior adaptation beyond version and checksum pins. These source observations establish capability only; the M5 production-path tests remain responsible for proving the toolkit's composition and containment. An automatic-safe result is not an automatic stable release. It must still pass a normal protected compatibility PR and a separate signed stable-release PR. If a dedicated OCR update bot credential is not configured, the workflow publishes the exact patch as an artifact and records the resume action in the issue; the default `GITHUB_TOKEN` is intentionally not used to create a PR that would fail to trigger the full protected workflow set. diff --git a/docs/configuration.md b/docs/configuration.md index 55091df..241519d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -22,7 +22,7 @@ These are the complete supported toolkit-owned runtime inputs. `Required` is sco | `OCR_LLM_VALIDATE_MODEL` | Operator / `ocr-ci preflight` | No | `false` | `true` validates through `/models`; `auto` may use the offline allowlist; false values skip validation. | | `OCR_LLM_MODELS_URL` | Operator / `ocr-ci preflight` | No | Derived from `OCR_LLM_URL` | Explicit absolute credential-free HTTPS metadata URL when validation is enabled or inference query parameters make derivation ambiguous. | | `OCR_LLM_ALLOWED_MODELS` | Operator / `ocr-ci preflight` | No | Empty list | Comma-separated exact model identifiers for offline or `auto` validation. | -| `OCR_TELEMETRY_ENABLED` | Operator / `ocr-ci configure` | No | `false` | Exact `true` enables OCR telemetry configuration; OCR 1.10.2 spans may include path-derived group keys and model-produced labels. | +| `OCR_TELEMETRY_ENABLED` | Operator / `ocr-ci configure` | No | `false` | Exact `true` enables OCR telemetry configuration; OCR 1.11.0 spans may include path-derived group keys and model-produced labels. | | `OCR_TELEMETRY_CONTENT_LOGGING` | Operator / `ocr-ci configure` | No | `false` | Exact `true` enables OCR content logging; keep disabled for private review data. | | `OCR_TELEMETRY_EXPORTER` | Operator / `ocr-ci configure` | No | Empty string | Exporter name written only when telemetry is enabled. | | `OCR_TELEMETRY_OTLP_ENDPOINT` | Operator / `ocr-ci configure` | No | Unset | OTLP endpoint written only when telemetry is enabled and the value is non-empty. | @@ -60,11 +60,11 @@ If `OCR_LLM_EXTRA_BODY` already owns that field, an exactly equal JSON integer i The toolkit does not derive this value from `/models.max_completion_tokens`. That metadata is a model capability boundary, not an account spending limit or proof of how a gateway reserves request cost. -The inherited value is version-owned and therefore changes with a qualified OCR upgrade. The toolkit observed `max_completion_tokens=58888` from OCR 1.9.10 and `16384` from OCR 1.10.0 through 1.10.2 when the variable was unset. In OCR 1.10.2 the grouping request also uses this template-owned cap instead of a separate `4096`; an explicit toolkit override still applies to every protocol request. Deployments that require an invariant gateway-specific cap must set `OCR_LLM_MAX_COMPLETION_TOKENS` explicitly rather than depending on an OCR default. +The inherited value is version-owned and therefore changes with a qualified OCR upgrade. The toolkit observed `max_completion_tokens=58888` from OCR 1.9.10 and `16384` from OCR 1.10.0 through 1.11.0 when the variable was unset. Grouping requests use the same template-owned cap; an explicit toolkit override still applies to every protocol request. Deployments that require an invariant gateway-specific cap must set `OCR_LLM_MAX_COMPLETION_TOKENS` explicitly rather than depending on an OCR default. ### Review effort -`OCR_REVIEW_EFFORT` defaults to `medium` and is written to OCR's root `effort` configuration key. OCR 1.10.2 maps `low`, `medium`, and `high` to one, two, and three review rounds respectively. The environment is operator-owned; merge-request text cannot change it. An explicit caller `--effort` passed after `ocr-ci review --` has normal OCR CLI precedence over the generated config, while an unknown environment value fails configuration before preview or model execution. +`OCR_REVIEW_EFFORT` defaults to `medium` and is written to OCR's root `effort` configuration key. OCR 1.11.0 maps `low`, `medium`, and `high` to one, two, and three review rounds and scales its 15-minute subtask base to 15, 30, or 45 minutes. The environment is operator-owned; merge-request text cannot change it. An explicit caller `--effort` passed after `ocr-ci review --` has normal OCR CLI precedence over the generated config, while an unknown environment value fails configuration before preview or model execution. Effort controls review depth, not the prompt/context ceiling, per-call completion cap, aggregate token budget, or per-round tool limit. Semantic grouping and filtering can add requests even at `low`; higher effort can add further rounds until OCR stops early, reaches a coverage/budget boundary, or completes the configured depth. @@ -92,13 +92,13 @@ These names belong to `examples/gitlab/ocr-review.gitlab-ci.yml`; they are shell | Variable | Source / owner | Required | Exact default | Behavior | | --- | --- | --- | --- | --- | -| **`OCR_VERSION`** | Example pipeline | Yes | `v1.10.2` | Checksum-pinned recommended OCR binary release for toolkit 0.8.5. | -| **`OCR_SHA256`** | Example pipeline | Yes | `e9205614f80e009ee7b1f444c9da08486fb9ff6db022954fe9203d923ab720b2` | Expected Linux AMD64 OCR binary digest. | +| **`OCR_VERSION`** | Example pipeline | Yes | `v1.11.0` | Checksum-pinned recommended OCR binary release for the toolkit 0.8.6 development line. | +| **`OCR_SHA256`** | Example pipeline | Yes | `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60` | Expected Linux AMD64 OCR binary digest. | | **`OCR_TOOLKIT_VERSION`** | Example pipeline | Yes | `0.8.5` | Exact toolkit wheel release installed by the current published example. | | **`OCR_TOOLKIT_CHECKSUMS_URL`** | Example pipeline | Yes | Release URL derived from `OCR_TOOLKIT_VERSION` | Toolkit `SHA256SUMS` URL. | | `OCR_TOOLKIT_WHEEL` | Example shell | Computed | `open_code_review_toolkit-${OCR_TOOLKIT_VERSION}-py3-none-any.whl` | Exact wheel filename selected from the release. | | `OCR_TOOLKIT_WHEEL_SHA256` | Example shell | Computed | Matching value from `SHA256SUMS` | Digest checked before installing the toolkit wheel. | -| `OCR_MAX_TOOLS` | Example pipeline / OCR CLI | No | `0` | Non-negative tool-call control passed to OCR. `0` delegates to the installed OCR template; a positive value can raise that template-owned loop limit, subject to OCR's runtime validation. | +| `OCR_MAX_TOOLS` | Example pipeline / OCR CLI | No | `0` | OCR 1.11.0 uses template default `100`; `1-49` reports normalization to `50` but remains effectively `100`, and only a value above `100` raises the cap. | | `OCR_MAX_TOKENS_BUDGET` | Example pipeline / OCR CLI | No | `0` | Non-negative aggregate OCR token ceiling; `0` is unlimited. | ## Dynamic adapter and MCP inputs @@ -165,14 +165,15 @@ for automatic approval. The cap is approximate because already-running work may finish and OCR accounts the provider-reported input plus output tokens. `OCR_MAX_TOOLS` is independent of all three token controls. Leave the example -default at `0` so the installed OCR template owns the effective per-file -tool-call limit. For the currently qualified OCR, behavioral qualification -observes a CLI minimum and normalization target of `50`, while omitted, `0`, -and values through `100` all retain the template's effective `100` rounds; a -value of `101` raises the effective limit to `101`. The OCR help text is not the -source of truth for these values. A recognized normalization is emitted only as -a toolkit-authored CI notice; its raw stderr is not added to findings, result -warnings, receipts, DLP inputs, telemetry, or automatic-approval signals. +default at `0` so OCR uses its embedded template limit of `100` per subtask. +OCR 1.11.0 reports values `1-49` as normalized to the minimum `50`, but both +that target and explicit `50` remain below the template default and therefore +remain effectively `100`. Use `101` or greater only when deliberately raising +the loop cap. OCR 1.11.0 corrected stale help text for this already-qualified +behavior; it did not change the runtime semantics. A recognized normalization +is emitted only as a toolkit-authored CI notice; its raw stderr is not added to +findings, result warnings, receipts, DLP inputs, telemetry, or +automatic-approval signals. ## Posting controls diff --git a/docs/development.md b/docs/development.md index 4b8902c..536fb69 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,6 +4,7 @@ Install [uv](https://docs.astral.sh/uv/) and use the committed lockfile: ```console uv sync --frozen +uv run ruff format . uv run ruff format --check . uv run ruff check . uv run mypy src/ocr_toolkit @@ -13,7 +14,7 @@ uv run python -m build uv run twine check dist/* ``` -For routine agent and contributor checks, run focused tests for each logical change and `scripts/quality.sh check` once on the completed handoff head. It replaces the selected mode's prior log, captures current output under ignored `.quality-logs/`, and prints only a short status; on failure it prints the last 80 lines. Individual modes are `format`, `lint`, `test`, `coverage`, `types`, and `security`. The `coverage` and `check` modes reuse that single branch-aware test run, then enforce scoped floors for result/preflight and GitLab posting transactions at 80%, plus review/context/DLP/approval and MCP/provider/policy/result contracts at 85%; a high combined result cannot hide a weak risk group. Hosted pull requests still run the complete suite on all five supported OS/Python combinations. The three Linux jobs are release-blocking, with Ubuntu on the newest supported Python as the sole coverage owner. The two macOS endpoint jobs are best-effort compatibility diagnostics: they continue to run and remain visible for follow-up, but a platform-only failure does not block a Linux-priority release. The Bandit gate scans only the supported runtime package at medium-or-higher severity and confidence; tests and synthetic fixtures are intentionally outside that bounded gate. +For routine agent and contributor checks, run focused tests for each logical change and `scripts/quality.sh check` once on the completed handoff head. After changing Python, run `scripts/quality.sh format` before self-review so Ruff's formatter is applied immediately; before every Python commit, require a clean repository-wide `uv run --frozen ruff format --check .` result. The full wrapper remains the final owner and repeats that check, so a formatting failure discovered only after push is a missed local commit gate rather than a missing quality control. It replaces the selected mode's prior log, captures current output under ignored `.quality-logs/`, and prints only a short status; on failure it prints the last 80 lines. Individual modes are `format`, `lint`, `test`, `coverage`, `types`, and `security`. The `coverage` and `check` modes reuse that single branch-aware test run, then enforce scoped floors for result/preflight and GitLab posting transactions at 80%, plus review/context/DLP/approval and MCP/provider/policy/result contracts at 85%; a high combined result cannot hide a weak risk group. Hosted pull requests still run the complete suite on all five supported OS/Python combinations. The three Linux jobs are release-blocking, with Ubuntu on the newest supported Python as the sole coverage owner. The two macOS endpoint jobs are best-effort compatibility diagnostics: they continue to run and remain visible for follow-up, but a platform-only failure does not block a Linux-priority release. The Bandit gate scans only the supported runtime package at medium-or-higher severity and confidence; tests and synthetic fixtures are intentionally outside that bounded gate. Runtime code must remain compatible with Python 3.12-3.14 and standard-library-only. Tests must use synthetic data; public examples must use safe placeholder hosts and credentials while describing the real operating behavior rather than labelling the feature itself as synthetic. User-visible changes require a fragment in `changelog.d/`. Repository-only qualification tools and evidence live under `scripts/` and `compatibility/`; they are excluded from both published distributions. Validate the manifest with `PYTHONPATH=src python scripts/ocr_compat.py validate`. diff --git a/docs/engineering/test_evidence_matrix.md b/docs/engineering/test_evidence_matrix.md index 55c540f..4219494 100644 --- a/docs/engineering/test_evidence_matrix.md +++ b/docs/engineering/test_evidence_matrix.md @@ -123,6 +123,16 @@ The M5 negative suite is an attacker matrix, not merely a replay of observed rev | New language Rules are selected without widening toolkit authority | exact OCR preview/rule resolver -> recommended built-in Rules | `.sol` and `.vy` inputs are reviewable and resolve Solidity/Vyper Rules; no toolkit result, DLP, receipt, telemetry, or approval contract changes | isolated Darwin arm64 no-LLM preview/rule readback plus upstream semantic audit | selection proves language/rule routing, not finding quality | proven | | Upstream Action/session additions do not claim backlog completion | source audit -> BL-021 and M6 ownership records | Action checkpoint ranges remain upstream-Action-only; session comparison and private grouping/grace records are not toolkit forge parity or telemetry | adjacent source/release audit and backlog/strategy reconciliation | source evidence classifies overlap only; no second forge integration is claimed | proven as partial overlap, backlog remains open | +## v0.8.6 OCR 1.11.0 evidence + +| Requirement or boundary | Production owner and entry point | Required observable result | Evidence | Double boundary and claim limit | State | +| --- | --- | --- | --- | --- | --- | +| Exact OCR 1.11.0 review contract | compatibility gateway -> hosted Linux binary -> human-reviewed manifest/evidence promotion | status-first grouping inventory preserves status/churn; result/manifest, inherited `16384`, an explicitly tested positive override, medium rounds, and effective max-tools remain compatible | hosted run 33158664020; checksum-pinned `compatibility/evidence/ocr-1.11.0.json`; strict historical-1.10/1.11.0 inventory fixtures; adjacent source audit | deterministic gateways prove structural wire/result/loop contracts, not provider quality or production reasoning replay; the tested override is evidence, not a toolkit default or recommendation | proven for exact qualified OCR contract | +| Max-tools help correction does not imply runtime drift | exact numeric CLI and effective loop probes -> public operator docs | omitted/`0` remain effective `100`; `49` reports `50` but stays effective `100`; `50` stays effective `100`; `101` becomes effective `101` | identical hosted 1.10.2 and 1.11.0 evidence plus upstream #1077 source/release audit | behavioral probes prove current embedded-template ownership; they do not promise future OCR defaults | proven unchanged | +| Private reasoning and request controls do not widen toolkit schemas | OCR isolated session -> canonical result projection -> DLP/publication/receipt/approval | provider-native reasoning and `tool_choice` remain absent from every toolkit public or authority-bearing sink; cleanup precedes publication | upstream adapter/session audit, result-contract probe, toolkit hostile projection/cleanup regressions, and external configured-environment checklist | local tests prove toolkit containment, while real provider replay remains an external qualification item | toolkit containment proven; provider replay pending external qualification | +| New selection behavior stays at the OCR edge | exact no-LLM preview/rule resolver -> built-in OCR Rules | repository-relative/backslash `file_find` fallback and `.hbs`/`.mustache` rule selection work without changing toolkit evidence authority | adjacent source audit plus isolated checksum-verified Darwin version/help, rule readback, and soft/hard background previews | selection proves routing, not review finding quality or evidence-pack completion | proven | +| Contextual security-signal classification | DLP-checked published finding -> closed metadata/term matcher -> reviewer guide | explicit vulnerability classes remain promoted while neutral injection domains do not increase the count or effort | issue #153 reproducer, closed positive/neutral separator matrix, deterministic ranking, and immutability regressions in `test_posting_helpers.py` | guide analytics only; severity, finding lifecycle, suppression, and approval inputs remain unchanged | proven | + ## Complete suite module audit Every top-level test module is classified below. A module can contain more than one evidence class; the strongest class applies only to the named boundary, never to all tests in that file. diff --git a/docs/engineering/toolkit_strategy.md b/docs/engineering/toolkit_strategy.md index 4d09a91..e16f560 100644 --- a/docs/engineering/toolkit_strategy.md +++ b/docs/engineering/toolkit_strategy.md @@ -154,9 +154,10 @@ Qualified OCR releases expose explicit per-run provider/model overrides, additiv The completed [BL-017 signal-ownership audit](review_signal_ownership.md) reaches `no-new-layer`. OCR owns provider, request, latency, cost, grouping, round, and tool telemetry. The toolkit owns only deterministic closed result, receipt, -context, publication-DLP, posting, and approval projections. OCR 1.10.2 group -keys contain changed paths and its labels are model-produced, so neither becomes -a toolkit metric, routing input, or approval authority. A single bounded +context, publication-DLP, posting, and approval projections. OCR 1.11.0 group +keys contain changed paths, its labels are model-produced, and its private +session can retain provider-native reasoning/tool state, so none becomes a +toolkit metric, routing input, public summary, or approval authority. A single bounded toolkit-authored failure diagnostic derived from closed retry-report enums and counts is operator feedback, not a second telemetry or exporter layer. diff --git a/docs/gitlab.md b/docs/gitlab.md index 310ffe8..06036ad 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -18,11 +18,11 @@ The complete variable inventory, owner, requirement, exact default, and behavior The public pipeline stores the OCR binary checksum as the non-secret `OCR_SHA256` pin. Store actual credentials as masked, protected CI variables; do not place their values in YAML, command arguments, repository evidence, or the generated bootstrap. GitLab job tokens are not accepted for posting. -`OCR_REVIEW_LANGUAGE` defaults to `English`; `Russian` is one example of an explicit review language. The example pins its qualified OCR release and sets `OCR_REVIEW_EFFORT=medium`, allowing two review rounds; `low` and `high` explicitly select one or three. OCR may stop early when a round adds no finding. `OCR_MAX_TOOLS=0` delegates the per-file tool-call limit to the installed OCR template; use a positive value only after checking that release's behaviorally qualified minimum, normalization, and effective value. `OCR_MAX_TOKENS_BUDGET` defaults to `0`, meaning unlimited; a positive budget may stop dispatch and produce an explicitly partial, automatic-approval-ineligible review. `OCR_LLM_MAX_COMPLETION_TOKENS` defaults to unset and separately controls only the provider request's completion/output cap. Select an explicit value only from the deployment's provider/model contract; the toolkit does not recommend or hardcode a provider-specific cap. +`OCR_REVIEW_LANGUAGE` defaults to `English`; `Russian` is one example of an explicit review language. The example pins its qualified OCR release and sets `OCR_REVIEW_EFFORT=medium`, allowing two review rounds; `low` and `high` explicitly select one or three. OCR may stop early when a round adds no finding. OCR 1.11.0 scales its 15-minute per-subtask base to 15/30/45 minutes for low/medium/high, so the example allows 45 minutes at the GitLab job boundary. `OCR_MAX_TOOLS=0` selects the embedded template default `100`. Values `1-49` report normalization to `50` but remain effectively `100`; explicit `50` also cannot lower the template, and only a value above `100` raises the cap. `OCR_MAX_TOKENS_BUDGET` defaults to `0`, meaning unlimited; a positive budget may stop dispatch and produce an explicitly partial, automatic-approval-ineligible review. `OCR_LLM_MAX_COMPLETION_TOKENS` defaults to unset and separately controls only the provider request's completion/output cap. Select an explicit value only from the deployment's provider/model contract; the toolkit does not recommend or hardcode a provider-specific cap. -OCR 1.10.2 semantically groups related changed files before review and filters candidates per group. Grouping, filtering, and multiple rounds can increase provider requests, latency, and token cost, while the manifest and aggregate budget continue to report completeness. Grouping and grace-round requests are tracked inside OCR's private session/retry diagnostics. Group labels and path-derived keys remain untrusted: the toolkit does not publish or use them for severity, fingerprints, lifecycle commands, receipts, telemetry, or approval. OCR's separately configured telemetry may export those high-cardinality repository-derived values, so keep `OCR_TELEMETRY_ENABLED=false` unless the exporter and retention policy are approved. +OCR 1.11.0 semantically groups related changed files before review and filters candidates per group. Its private grouping and other-files prompts use status-first `STATUS path (+N/-M)` entries; the toolkit does not parse that inventory in production. Grouping, filtering, and multiple rounds can increase provider requests, latency, and token cost, while the manifest and aggregate budget continue to report completeness. Grouping, grace-round, provider-native reasoning/thinking, and tool-choice request state are retained only inside OCR's isolated private session. Group labels, paths, reasoning, signed/encrypted native payloads, and request controls remain untrusted: the toolkit does not publish or use them for severity, fingerprints, lifecycle commands, receipts, DLP counts, telemetry, tool/token summaries, or approval. OCR's separately configured telemetry may export high-cardinality repository-derived values, so keep `OCR_TELEMETRY_ENABLED=false` unless the exporter and retention policy are approved. -The summary keeps four inputs separate: OCR manifest coverage, publication integrity, published findings, and an optional OCR core advisory. Complete manifest coverage that later loses public fields is labelled `Review complete with publication filtering`; it is warning-bearing and approval-ineligible, but it is not called incomplete OCR coverage and does not invent failed-file diagnostics. Actual partial, failed, waived, or budget-stopped manifest coverage keeps its stronger status. Ordinary OCR warnings retain their own complete-with-warnings state. The reviewer guide ranks bounded focus areas only when at least two findings are published; one finding remains in its inline or fallback discussion without a duplicate snippet. +The summary keeps four inputs separate: OCR manifest coverage, publication integrity, published findings, and an optional OCR core advisory. Complete manifest coverage that later loses public fields is labelled `Review complete with publication filtering`; it is warning-bearing and approval-ineligible, but it is not called incomplete OCR coverage and does not invent failed-file diagnostics. Actual partial, failed, waived, or budget-stopped manifest coverage keeps its stronger status. Ordinary OCR warnings retain their own complete-with-warnings state. The reviewer guide ranks bounded focus areas only when at least two findings are published; one finding remains in its inline or fallback discussion without a duplicate snippet. Security focus uses explicit published metadata, strong vulnerability terms, and closed injection classes rather than the standalone word `injection`, so neutral knowledge/dependency terminology cannot increase its count or effort estimate. The collapsed technical details keep aggregate input/output/cache token usage separate from OCR tool activity. The existing `all OCR tool calls` line lists every non-zero count for the closed review set (`file_read`, `file_read_diff`, `file_find`, `code_search`, `code_comment`, `task_done`, `ocr_toolkit_evidence`, `context_list`, and `context_get`) and is omitted when that admitted list is empty. These counts help explain whether OCR read context, searched the repository, checked cross-file diffs, consulted toolkit evidence, or emitted review output; they are not per-tool token attribution. Dynamic external MCP tool names remain private and appear only through the existing verified per-server aggregate. Tool and token lines remain independent of `passed`, `private-sanitized`, or `publication-filtered` receipt state. If OCR accepts a background above its recommended character count, a separate `OCR core advisory` line contains only the actual and recommended counts. It does not become a warning or change review completeness or approval eligibility. diff --git a/docs/operations.md b/docs/operations.md index 7107858..b45e0ce 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -34,9 +34,9 @@ suppressed. `OCR_MAX_TOKENS_BUDGET` can set an aggregate input-plus-output token ceiling for the OCR diff review. The default `0` is unlimited. A positive ceiling is approximate rather than a hard billing cutoff because already-running work may complete; when it stops further dispatch, completed findings remain publishable and unreviewed files stay explicit as budget-attributed failed coverage. Such a run is partial and cannot automatically approve. -This aggregate budget is separate from both OCR's prompt/context `max_tokens` ceiling and the provider request's completion/output cap. The toolkit does not add an environment alias for OCR's prompt/context control. `OCR_LLM_MAX_COMPLETION_TOKENS` defaults to unset and, when set, overlays only the protocol-specific output field. The inherited OpenAI value was 58,888 in OCR 1.9.10 and is 16,384 in OCR 1.10.0 through 1.10.2. OCR 1.10.2 also applies that template-owned cap to grouping requests; an explicit toolkit override applies to all requests. A gateway may reserve cost against that requested maximum before generation even when the eventual response would be short. The `/models` capability value does not reveal an account spending limit or reservation policy, so the toolkit never selects the cap from it automatically. Set an explicit cap only from the deployment's provider/model contract when that deployment must not change with the qualified OCR version; the toolkit does not recommend or hardcode a provider-specific value. +This aggregate budget is separate from both OCR's prompt/context `max_tokens` ceiling and the provider request's completion/output cap. The toolkit does not add an environment alias for OCR's prompt/context control. `OCR_LLM_MAX_COMPLETION_TOKENS` defaults to unset and, when set, overlays only the protocol-specific output field. The inherited OpenAI value was 58,888 in OCR 1.9.10 and is 16,384 in OCR 1.10.0 through 1.11.0, including grouping requests; an explicit toolkit override applies to all requests. A gateway may reserve cost against that requested maximum before generation even when the eventual response would be short. The `/models` capability value does not reveal an account spending limit or reservation policy, so the toolkit never selects the cap from it automatically. Set an explicit cap only from the deployment's provider/model contract when that deployment must not change with the qualified OCR version; the toolkit does not recommend or hardcode a provider-specific value. -`OCR_REVIEW_EFFORT=medium` is the toolkit default for the qualified OCR release and permits two review rounds; `low` permits one and `high` permits three. This is a review-depth choice, not one of the three token controls. OCR first groups related changed files and may make group-filter requests; additional rounds can add requests, latency, and cost, but may stop early when they add no finding. The published GitLab example separately passes `OCR_MAX_TOOLS=0`, delegating the effective per-file tool-call limit to the installed OCR template instead of carrying a release-specific minimum. A positive value can raise that limit, but operators should check the release's behavioral qualification because CLI help, normalization text, and effective template value can differ. Exhausting the effective limit, an aggregate budget stop, or incomplete manifest coverage remains explicit and approval-ineligible; increasing either effort or tools is never a way to hide partial coverage. +`OCR_REVIEW_EFFORT=medium` is the toolkit default for the qualified OCR release and permits two review rounds; `low` permits one and `high` permits three. OCR 1.11.0 multiplies its 15-minute subtask base by those rounds, yielding 15/30/45-minute limits; the GitLab example's 45-minute job timeout accommodates the largest preset. This is a review-depth choice, not one of the three token controls. OCR first groups related changed files and may make group-filter requests; additional rounds can add requests, latency, and cost, but may stop early when they add no finding. The published GitLab example separately passes `OCR_MAX_TOOLS=0`, selecting the embedded template default `100`. The CLI reports `1-49` as normalized to `50`, but both that target and explicit `50` remain effectively `100`; only values above `100` raise the cap. OCR 1.11.0 corrects stale help text for the already-qualified behavior rather than changing the runtime loop. Exhausting the effective limit, an aggregate budget stop, or incomplete manifest coverage remains explicit and approval-ineligible; increasing either effort or tools is never a way to hide partial coverage. The outcome wording distinguishes skipped, complete, complete-with-warnings, incomplete, token-budget, and failed reviews while preserving the finding state in that same line. A complete clean review is visibly positive; a complete review with findings or only reviewer-suppressed findings is neutral; warning, partial, budget, and failed states never look clean. Findings withheld by the posting limit remain counted even when the limit allows no individual finding note. Recommended focus areas ranks only its copy of already-published findings by the closed severity, category, safe repository location, and stable-identity order before its existing display cap; inline and fallback discussion order, suppression, counts, security focus, and approval policy remain unchanged. OCR 1.8.5 and later manifest failures provide the canonical failed-file receipt; legacy warnings are a bounded fallback, and `summary.files_reviewed` is never treated as proof of successful coverage. Technical details label the aggregate as all OCR tool calls and retain the existing inline format while listing every admitted non-zero count for the closed native/context/evidence review set. An empty admitted list produces no tool-call line. The counts describe review activity, not per-tool token consumption: one read or search can return a different amount of context from another. Dynamic external tool names remain private; toolkit-verified MCP-server calls stay in their separate aggregate, and built-in evidence `summary`/`list`/`get` counts appear only after exact reconciliation. Zero-valued token counters and configured-but-unused MCP servers are omitted. Token usage renders only validated input/output/cached/reasoning/total/derived-other buckets; malformed or contradictory counters are unavailable and unknown provider keys are not published. Status and aggregate semantic-category emoji are enabled by default and can be disabled together with `OCR_POST_EMOJI=false`; finding labels remain text unless their separate badge mode is enabled. @@ -130,7 +130,7 @@ Suppression checks both the recorded inline position and compatible fingerprints Run OCR through `ocr-ci review --result PATH --stderr PATH -- ...`. This wrapper does not post to GitLab: it creates private artifacts, acquires enriched context when selected, asks the exact resolved and preflight-qualified OCR executable to preview the production refs/rules/selection/background without an LLM, then runs the model review only if OCR accepts that background. OCR owns the current recommendation and rejection thresholds; the toolkit has no threshold setting. A recognized soft diagnostic is reduced to a toolkit-authored `ocr.toolkit-advisory/v1` enum and two positive character counts. It is attached after publication DLP, rendered only with an exact receipt v5 in Technical details, and does not change warnings, coverage, DLP counts, telemetry, or automatic approval. A recognized hard character/file-size rejection stops before the model and lets `ocr-ci post` publish only a static numeric failure summary; the OCR path and raw diagnostic remain private. Unknown preview failures use the generic fail-closed diagnostic path. The ordinary review still validates the same background, the wrapper validates the complete output, and context/session/configuration data is removed. On an unclassified ordinary failure it prints only a bounded redacted stderr excerpt to the runner log; a classified provider failure keeps that excerpt private. Pass the paths and captured exit code to `ocr-ci post` afterward. Set `OCR_POST_ERROR_DETAILS=1` only when the generic path's safe excerpt should also appear in the merge-request failure note. Cleanup uncertainty blocks result publication. DLP atomically converts unsafe publication output into a safe `completed_with_errors` subset, but sanitizes unsafe private-only result fields without discarding an otherwise valid manifest or finding set. Safe findings are posted, unsafe finding content/warnings and unsafe optional fields are omitted, previous OCR comments remain, and matching prior findings are consumed one-for-one rather than duplicated. Receipt v5 and the `ocr.publication-dlp-signal/v2` marker distinguish `private-sanitized`, where the canonical published and approval-relevant projection is unchanged, from approval-ineligible `publication-filtered`, where the public projection is incomplete even when the receipt proves OCR coverage was complete. Posting uses only the validated original coverage kind and counts for status and Technical details; it never reconstructs or republishes rejected values. Horizontal tabs survive only in `existing_code` and `suggestion_code`; the unchanged values pass the same secret, PII, forbidden-value, laundering, and budget checks, while tabs elsewhere and all other control/format characters remain blocking. The same count-only JSON is logged as `OCR toolkit telemetry event` for optional CI collection/alerting. It is not an OTLP/network exporter and contains no rejected value or location. Never interpret a filtered subset as a full publishable review or as proof that OCR coverage was partial. -OCR 1.10.2 may add group labels, file membership, grouping-task records, and round diagnostics to its private result/session data. Safe values remain private; DLP removes or replaces unsafe result values before atomic retention. These fields are deliberately absent from the canonical finding/posting projection and receipt v5, so private-only sanitization does not block an otherwise eligible auto-approval. If any group or round field appears inside receipt v5, the receipt is invalid and approval fails closed. Caller `--output`/`-o` is rejected before preview: only `ocr-ci review --result` owns the result descriptor and posting handoff. +OCR 1.11.0 may add group labels, file membership, grouping-task records, round diagnostics, readable reasoning, and provider-native signed or encrypted replay payloads to its private session data. Safe values remain private; the isolated OCR home is removed before publication. Unknown private-only result fields are sanitized through the existing DLP owner, while any value reaching a canonical finding or warning makes publication incomplete and approval-ineligible. Reasoning, native payloads, and `tool_choice` are deliberately absent from the canonical finding/posting projection, receipt v5, DLP counts, toolkit telemetry, tool/token summary, and approval inputs. If they appear inside receipt v5, the receipt is invalid and approval fails closed. Caller `--output`/`-o` is rejected before preview: only `ocr-ci review --result` owns the result descriptor and posting handoff. When OCR exits nonzero with a valid bounded `ocr.llm-retry-report/v1`, the toolkit reads only its closed error class, failure phase, terminal outcome, HTTP status, and schema counters. It maps those facts to `authentication`, `authorization`, `rate-or-spending-limit`, `overloaded`, `timeout`, `network`, `endpoint-or-model-not-found`, `request-rejected`, `provider-unavailable`, `invalid-response`, `cancelled`, `mixed`, or `unknown`, then writes a completely toolkit-authored note. A runtime `404` remains `endpoint-or-model-not-found` because safely distinguishing the endpoint from the model would require trusting the raw response body. diff --git a/docs/security.md b/docs/security.md index 8617770..e41b434 100644 --- a/docs/security.md +++ b/docs/security.md @@ -115,7 +115,7 @@ Policy v2 adds optional toolkit-owned remediation threads without changing these - **Identity spoofing and privacy:** represent authors by provider-declared class plus run-local pseudonym; omit name, email, avatar, and profile URL. Unknown classification fails closed. - **TOCTOU/replay/cache poisoning:** bind snapshots and handles to version/ETag or digest, policy version, run, and expiry; use atomic store commits and uniform unavailable outcomes. - **PII/secret bypass and output laundering:** make retrieval, model egress, publication sinks, and private retention separate decisions. Minimize before model egress; publication DLP cannot undo prior disclosure. Scan decoded source and rendered forms so HTML comments/entities/tags, Markdown inline/reference/autolink destinations, controls, and Unicode formatting cannot hide exact protected values. Sink uncertainty yields a safe partial result; private-only uncertainty yields an atomically sanitized valid result or falls back to that partial form. Expose only low-cardinality receipt/summary/log signals in ordinary runs. The explicit local-only diagnostic switch retains owner-only raw private state, emits no receipt, and is rejected by the validated GitLab MR profile. -- **Upstream telemetry cardinality:** OCR 1.10.2 can place sorted changed paths in group span names/attributes and attach model-produced group labels and round counters. OCR telemetry is disabled by default. Enabling it authorizes the configured exporter to receive repository-derived high-cardinality data; content-logging disablement is not a promise that these identifiers disappear. The toolkit neither ingests nor duplicates them. +- **Upstream session and telemetry privacy:** OCR 1.11.0 can retain readable reasoning, signed/encrypted provider-native replay payloads, tool arguments/results, sorted changed paths, model-produced group labels, and round counters in its owner-only session. Telemetry may also carry repository-derived high-cardinality grouping values. OCR telemetry is disabled by default, and the toolkit removes the isolated session home before publication. Enabling an exporter authorizes it to receive the fields OCR emits; content-logging disablement is not a promise that every identifier disappears. The toolkit neither ingests nor duplicates these values. - **OCR advisory spoofing:** raw OCR output cannot supply `_ocr_toolkit_advisory`. The toolkit adds only the exact numeric `ocr.toolkit-advisory/v1` shape after publication DLP; posting requires a valid receipt v5 and rejects duplicate, extended, unknown, or malformed forms. The advisory cannot change warnings, coverage, DLP, telemetry, findings, or approval. - **Local executable and subprocess confused deputy:** accept the OCR executable only through absolute search-path entries and reject resolution inside the reviewed repository. Stdio adapter deadlines cover request delivery as well as response/exit, use a clean environment and isolated directory, and terminate the process group on uncertainty. - **Marker and retry spoofing:** recognize ownership, run-summary, and setup identities only in the exact toolkit preamble. Repository/provider text containing marker lookalikes has no authority. Duplicate findings consume visible prior occurrences one-for-one; successful setup retries replace only previous toolkit setup-pending notes. diff --git a/examples/gitlab/README.md b/examples/gitlab/README.md index 1d563c1..1270994 100644 --- a/examples/gitlab/README.md +++ b/examples/gitlab/README.md @@ -2,7 +2,7 @@ [`ocr-review.gitlab-ci.yml`](ocr-review.gitlab-ci.yml) is the complete checksum-pinned pipeline. It defaults to identity-only review and contains the install, preflight, configure, review, and post lifecycle. Copy one mode file below into the pipeline's top-level `variables` mapping, or include exactly one file after reviewing its trust boundary. -The pipeline pins OCR 1.10.2 and explicitly sets `OCR_REVIEW_EFFORT=medium`, which permits two review rounds. Use `low` for one round when latency/cost is the priority, or `high` for three only after accepting the additional provider work. Semantic grouping and filtering are OCR behavior shared by every context mode; they do not change which merge-request text a mode admits. `OCR_MAX_TOOLS=0` delegates the effective per-file tool-call limit to the installed OCR template. `OCR_MAX_TOKENS_BUDGET` and the optional completion cap remain independent controls. +The pipeline pins OCR 1.11.0 and explicitly sets `OCR_REVIEW_EFFORT=medium`, which permits two review rounds. Use `low` for one round when latency/cost is the priority, or `high` for three only after accepting the additional provider work. OCR uses a 15-minute subtask base and scales it to 15/30/45 minutes for low/medium/high; the example's 45-minute job timeout accommodates every preset without adding another environment control. Semantic grouping and filtering are OCR behavior shared by every context mode; they do not change which merge-request text a mode admits. `OCR_MAX_TOOLS=0` selects the embedded template default `100`. Values `1-49` report normalization to `50` but remain effectively `100`; only values above `100` raise the per-subtask cap. `OCR_MAX_TOKENS_BUDGET` and the optional completion cap remain independent controls. | Mode | Recipe | MR text admitted | External access | Automatic approval | | --- | --- | --- | --- | --- | diff --git a/examples/gitlab/ocr-review.gitlab-ci.yml b/examples/gitlab/ocr-review.gitlab-ci.yml index 6b3eb84..ac8645f 100644 --- a/examples/gitlab/ocr-review.gitlab-ci.yml +++ b/examples/gitlab/ocr-review.gitlab-ci.yml @@ -6,10 +6,10 @@ default: image: python:3.12-slim variables: - OCR_VERSION: "v1.10.2" + OCR_VERSION: "v1.11.0" OCR_TOOLKIT_VERSION: "0.8.5" OCR_TOOLKIT_CHECKSUMS_URL: "https://github.com/xeonvs/open-code-review-toolkit/releases/download/v${OCR_TOOLKIT_VERSION}/SHA256SUMS" - OCR_SHA256: "e9205614f80e009ee7b1f444c9da08486fb9ff6db022954fe9203d923ab720b2" + OCR_SHA256: "13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60" OCR_POST_MODE: "draft" OCR_STRICT_POSTING: "true" # Default-on receipt-bound approval; external MCP always remains comment-only. @@ -44,7 +44,8 @@ open_code_review: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' when: on_success - when: never - timeout: 30m + # OCR 1.11.0 scales the 15-minute subtask base by low/medium/high rounds (15/30/45). + timeout: 45m script: - apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git - >- diff --git a/scripts/ocr_compat.py b/scripts/ocr_compat.py index c0a6691..ac819da 100644 --- a/scripts/ocr_compat.py +++ b/scripts/ocr_compat.py @@ -46,6 +46,9 @@ MAX_RELEASE_CHANGES_LINES = 50 MAX_QUALIFICATION_CHAIN = 10 MAX_CLI_PROBE_BYTES = 100_000 +MAX_GROUPING_INVENTORY_ENTRIES = 100 +MAX_GROUPING_PATH_CHARS = 1_000 +MAX_GROUPING_CHURN = 1_000_000_000 DOWNLOAD_ATTEMPTS = 3 VERSION_RE = re.compile(r"^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") SHA256_RE = re.compile(r"^(?:sha256:)?([0-9a-f]{64})$") @@ -166,6 +169,16 @@ def __init__(self, message: str, *, phase: str, reason: str) -> None: self.reason = reason +@dataclass(frozen=True) +class GroupingInventoryEntry: + """One strictly parsed, qualification-only grouping inventory entry.""" + + status: str + path: str + insertions: int + deletions: int + + class _NoRedirectHandler(urllib.request.HTTPRedirectHandler): """Reject metadata redirects so the reviewed API origin cannot change.""" @@ -763,6 +776,8 @@ class _StubHandler(http.server.BaseHTTPRequestHandler): tokens_per_request = 2 grouping_tokens_per_request = 2 grouping_mode = "singletons" + grouping_inventory_version: str | None = None + grouping_inventories: list[list[GroupingInventoryEntry]] = [] main_mode = "findings" completion_caps: list[object] = [] request_stages: list[str] = [] @@ -779,18 +794,13 @@ def _message_contents(messages: list[Any]) -> list[str]: @classmethod def _grouping_files(cls, messages: list[Any]) -> list[str]: - """Extract the public grouping prompt's changed-file inventory.""" + """Extract one version-bound grouping inventory and retain structural evidence.""" - paths: list[str] = [] - for content in cls._message_contents(messages): - for match in re.finditer( - r"(?m)^([^\r\n]+) \((?:ADDED|MODIFIED|DELETED|RENAMED), \+[0-9]+/-[0-9]+\)$", - content, - ): - candidate = match.group(1) - if candidate not in paths: - paths.append(candidate) - return paths + if cls.grouping_inventory_version is None: + _fail("grouping inventory version is not configured") + entries = parse_grouping_inventory(messages, cls.grouping_inventory_version) + cls.grouping_inventories.append(entries) + return [entry.path for entry in entries] @classmethod def _review_path(cls, messages: list[Any]) -> str | None: @@ -960,6 +970,7 @@ def _stub_gateway( tokens_per_request: int = 2, grouping_tokens_per_request: int = 2, grouping_mode: str = "singletons", + grouping_inventory_version: str | None = None, main_mode: str = "findings", ) -> Iterator[str]: """Serve deterministic responses with configurable real usage accounting.""" @@ -974,6 +985,8 @@ def _stub_gateway( _StubHandler.tokens_per_request = tokens_per_request _StubHandler.grouping_tokens_per_request = grouping_tokens_per_request _StubHandler.grouping_mode = grouping_mode + _StubHandler.grouping_inventory_version = grouping_inventory_version + _StubHandler.grouping_inventories = [] _StubHandler.main_mode = main_mode _StubHandler.completion_caps = [] _StubHandler.request_stages = [] @@ -988,6 +1001,79 @@ def _stub_gateway( thread.join(timeout=5) +def parse_grouping_inventory(messages: list[Any], version: str) -> list[GroupingInventoryEntry]: + """Parse the exact grouping prompt shape qualified for one OCR release line.""" + + parsed_version = _version(version) + if (1, 10, 0) <= parsed_version < (1, 11, 0): + pattern = re.compile( + r"(?P[^\r\n]{1,1000}) " + r"\((?PADDED|MODIFIED|DELETED|RENAMED), " + r"\+(?P0|[1-9][0-9]{0,9})/-(?P0|[1-9][0-9]{0,9})\)" + ) + elif parsed_version >= (1, 11, 0): + pattern = re.compile( + r"(?PADDED|MODIFIED|DELETED|RENAMED) " + r"(?P[^\r\n]{1,1000}) " + r"\(\+(?P0|[1-9][0-9]{0,9})/-(?P0|[1-9][0-9]{0,9})\)" + ) + else: + _fail(f"grouping inventory format is not qualified for OCR {version}") + + prefix = "Group the following changed files:\n\n" + suffix = "\n\nRespond with a JSON array:" + blocks: list[str] = [] + for content in _StubHandler._message_contents(messages): + if not content.startswith(prefix) or suffix not in content: + continue + inventory, remainder = content[len(prefix) :].split(suffix, 1) + if suffix in remainder: + _fail("grouping prompt contains duplicate response boundaries") + blocks.append(inventory) + if len(blocks) != 1: + _fail("grouping prompt must contain exactly one changed-file inventory") + + lines = blocks[0].splitlines() + if not lines or len(lines) > MAX_GROUPING_INVENTORY_ENTRIES: + _fail("grouping inventory has an invalid entry count") + entries: list[GroupingInventoryEntry] = [] + observed_paths: set[str] = set() + for line in lines: + match = pattern.fullmatch(line) + if match is None: + _fail(f"OCR {version} emitted an invalid grouping inventory entry") + path = match.group("path") + insertions = int(match.group("insertions")) + deletions = int(match.group("deletions")) + if ( + len(path) > MAX_GROUPING_PATH_CHARS + or path in observed_paths + or insertions > MAX_GROUPING_CHURN + or deletions > MAX_GROUPING_CHURN + ): + _fail(f"OCR {version} emitted an invalid grouping inventory value") + observed_paths.add(path) + entries.append( + GroupingInventoryEntry( + status=match.group("status"), + path=path, + insertions=insertions, + deletions=deletions, + ) + ) + return entries + + +def _require_exact_grouping_inventory( + observed: list[GroupingInventoryEntry], + expected: list[GroupingInventoryEntry], +) -> None: + """Reject status, churn, path, or ordering drift in one grouping inventory.""" + + if observed != expected: + _fail("semantic grouping review emitted an unexpected status/churn inventory") + + def detect_optional_capabilities(help_output: str, sample: dict[str, Any]) -> list[str]: """Validate additive OCR identity fields and return observed optional capabilities.""" @@ -1053,7 +1139,7 @@ def _validate_file_groups(value: Any, expected_paths: set[str] | None = None) -> _fail("candidate semantic grouping did not cover the expected paths") -def _budget_result_probe(binary: Path, directory: Path) -> dict[str, object]: +def _budget_result_probe(binary: Path, version: str, directory: Path) -> dict[str, object]: """Drive the real OCR review budget gate and validate its partial manifest.""" git_env = _isolated_probe_environment(directory / "budget-git-home") @@ -1074,7 +1160,10 @@ def _budget_result_probe(binary: Path, directory: Path) -> dict[str, object]: home = directory / "budget-review-home" env = _isolated_probe_environment(home) - with _stub_gateway(tokens_per_request=20_000) as gateway_url: + with _stub_gateway( + tokens_per_request=20_000, + grouping_inventory_version=version, + ) as gateway_url: env.update( { "OCR_LLM_URL": gateway_url, @@ -1470,7 +1559,10 @@ def _semantic_grouping_probe(binary: Path, version: str, directory: Path) -> dic head = _run(["git", "rev-parse", "HEAD"], cwd=repo, env=git_env).strip() env = _isolated_probe_environment(root / "review-home") - with _stub_gateway(grouping_mode="combined") as gateway_url: + with _stub_gateway( + grouping_mode="combined", + grouping_inventory_version=version, + ) as gateway_url: env.update( { "OCR_LLM_URL": gateway_url, @@ -1500,6 +1592,7 @@ def _semantic_grouping_probe(binary: Path, version: str, directory: Path) -> dic ) stages = list(_StubHandler.request_stages) completion_caps = list(_StubHandler.completion_caps) + grouping_inventories = list(_StubHandler.grouping_inventories) try: sample = json.loads(output) except json.JSONDecodeError as exc: @@ -1516,6 +1609,10 @@ def _semantic_grouping_probe(binary: Path, version: str, directory: Path) -> dic _fail("semantic grouping review did not preserve the accepted group") if stages != ["grouping", "main", "main", "filter", "main"]: _fail(f"default medium review emitted an unexpected stage sequence: {stages!r}") + expected_inventory = [GroupingInventoryEntry("MODIFIED", path, 1, 1) for path in paths] + if len(grouping_inventories) != 1: + _fail("semantic grouping review emitted an unexpected grouping inventory count") + _require_exact_grouping_inventory(grouping_inventories[0], expected_inventory) expected_grouping_cap = 16_384 if _version(version) >= (1, 10, 2) else 4_096 if len(completion_caps) != len(stages) or completion_caps[0] != expected_grouping_cap: _fail( @@ -1838,7 +1935,7 @@ def run_contracts(binary: Path, version: str, directory: Path) -> dict[str, Any] contracts: dict[str, Any] = { "numeric_cli_probe": _numeric_cli_probe(binary, repo, base, head, directory), "optional_capabilities": optional_capabilities, - "review_budget_probe": _budget_result_probe(binary, directory), + "review_budget_probe": _budget_result_probe(binary, version, directory), "target_rule_selection_probe": _target_rule_selection_probe(binary, version, directory), "version_probe": "passed", "required_review_flags": sorted(required_review_flags), diff --git a/src/ocr_toolkit/posting/formatting.py b/src/ocr_toolkit/posting/formatting.py index 42aea9f..bec5490 100644 --- a/src/ocr_toolkit/posting/formatting.py +++ b/src/ocr_toolkit/posting/formatting.py @@ -650,10 +650,17 @@ def format_token_usage_summary(result: dict[str, Any]) -> str: r"(?i)\b(" r"security|credential|secret|token|password|private[_ -]?token|" r"client[_ -]?secret|api[_ -]?key|authorization|auth|" - r"injection|xss|csrf|ssrf|rce|path traversal|host header|" + r"xss|csrf|ssrf|rce|path traversal|host header|" r"privilege|permission|access control|vault|leak" r")\b" ) +SECURITY_INJECTION_RE = re.compile( + r"(?i)(? str: def comment_has_security_signal(comment: dict[str, Any]) -> bool: """Return true only when OCR text explicitly carries security wording.""" - return bool(SECURITY_SIGNAL_RE.search(comment_signal_text(comment))) + text = comment_signal_text(comment) + return bool(SECURITY_SIGNAL_RE.search(text) or SECURITY_INJECTION_RE.search(text)) def comment_is_high_signal(comment: dict[str, Any]) -> bool: diff --git a/src/ocr_toolkit/preflight.py b/src/ocr_toolkit/preflight.py index e049dbb..e8c8bd7 100644 --- a/src/ocr_toolkit/preflight.py +++ b/src/ocr_toolkit/preflight.py @@ -28,7 +28,7 @@ "Accept": "application/json", "User-Agent": "open-code-review-ci-preflight/1.0", } -EXPECTED_OCR_VERSION = "1.10.2" +EXPECTED_OCR_VERSION = "1.11.0" class PreflightError(Exception): diff --git a/src/ocr_toolkit/review_runner.py b/src/ocr_toolkit/review_runner.py index 8e116eb..7381d96 100644 --- a/src/ocr_toolkit/review_runner.py +++ b/src/ocr_toolkit/review_runner.py @@ -154,6 +154,16 @@ "suggestion_code", } ) +PROVIDER_PRIVATE_RESULT_KEYS = frozenset( + { + "encrypted_content", + "native_payload", + "reasoning_content", + "redacted_thinking", + "thinking", + "tool_choice", + } +) class ReviewRunnerError(Exception): @@ -463,8 +473,10 @@ def _dlp_reasons( while stack: nested = stack.pop() if isinstance(nested, dict): - stack.extend(nested.keys()) - stack.extend(nested.values()) + for key, value in nested.items(): + if isinstance(key, str) and key in PROVIDER_PRIVATE_RESULT_KEYS: + reasons["invalid_text"] += 1 + stack.extend((key, value)) elif isinstance(nested, list): stack.extend(nested) elif isinstance(nested, str): @@ -693,6 +705,11 @@ def _sanitize_nonpublication_fields( child_path = (*path, key) if isinstance(source, dict): assert isinstance(key, str) + if key in PROVIDER_PRIVATE_RESULT_KEYS and not _is_publication_sink_path( + child_path + ): + redacted_fields += 1 + continue if not _is_publication_sink_path( child_path ) and not _is_static_public_tool_key_path(child_path): @@ -894,7 +911,7 @@ def _publication_projection( sanitized, private_reasons, redacted_fields = _sanitize_nonpublication_fields( payload, budgets=budgets, matcher=matcher ) - if not sink_reasons and not private_reasons: + if not sink_reasons and not private_reasons and redacted_fields == 0: return payload, {"state": "passed"}, False try: @@ -932,6 +949,8 @@ def _publication_projection( projected["summary"] = {"budget_exceeded": True} else: reasons = sink_reasons + private_reasons + if not reasons: + return sanitized, {"state": "passed"}, False return ( sanitized, { diff --git a/tests/test_environment_contract.py b/tests/test_environment_contract.py index 5f80992..4f8a9ee 100644 --- a/tests/test_environment_contract.py +++ b/tests/test_environment_contract.py @@ -69,8 +69,8 @@ } EXAMPLE_DEFAULTS = { - "OCR_VERSION": "v1.10.2", - "OCR_SHA256": "e9205614f80e009ee7b1f444c9da08486fb9ff6db022954fe9203d923ab720b2", + "OCR_VERSION": "v1.11.0", + "OCR_SHA256": "13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60", "OCR_TOOLKIT_VERSION": STABLE_TOOLKIT_VERSION, "OCR_TOOLKIT_CHECKSUMS_URL": "Release URL derived from `OCR_TOOLKIT_VERSION`", "OCR_TOOLKIT_WHEEL": "open_code_review_toolkit-${OCR_TOOLKIT_VERSION}-py3-none-any.whl", @@ -299,7 +299,7 @@ def test_example_local_defaults_match_the_pipeline() -> None: encoding="utf-8" ) for name, value in { - "OCR_VERSION": "v1.10.2", + "OCR_VERSION": "v1.11.0", "OCR_SHA256": EXAMPLE_DEFAULTS["OCR_SHA256"], "OCR_TOOLKIT_VERSION": STABLE_TOOLKIT_VERSION, "OCR_MAX_TOOLS": "0", diff --git a/tests/test_integration_contracts.py b/tests/test_integration_contracts.py index 8eda256..06e8150 100644 --- a/tests/test_integration_contracts.py +++ b/tests/test_integration_contracts.py @@ -130,6 +130,7 @@ def test_gitlab_example_preserves_review_gating_and_manual_self_test() -> None: assert 'OCR_MAX_TOKENS_BUDGET: "0"' in workflow assert 'OCR_REVIEW_EFFORT: "medium"' in workflow assert 'OCR_MAX_TOOLS: "0"' in workflow + assert "timeout: 45m" in review_job assert '--max-tools "${OCR_MAX_TOOLS:-0}"' in review_job assert '--max-tokens-budget "${OCR_MAX_TOKENS_BUDGET:-0}"' in review_job assert "lint:\n stage: lint" in workflow diff --git a/tests/test_ocr_compat.py b/tests/test_ocr_compat.py index ffee380..b704504 100644 --- a/tests/test_ocr_compat.py +++ b/tests/test_ocr_compat.py @@ -43,8 +43,8 @@ def test_committed_manifest_is_valid_and_has_recommended_tested_baseline() -> No module.validate_manifest(manifest, PROJECT_ROOT) - assert manifest["recommended_version"] == "1.10.2" - assert manifest["monitoring_floor"] == "1.10.2" + assert manifest["recommended_version"] == "1.11.0" + assert manifest["monitoring_floor"] == "1.11.0" assert [(item["version"], item["status"]) for item in manifest["releases"]] == [ ("1.7.17", "tested"), ("1.8.0", "tested"), @@ -72,6 +72,7 @@ def test_committed_manifest_is_valid_and_has_recommended_tested_baseline() -> No ("1.10.0", "tested"), ("1.10.1", "tested"), ("1.10.2", "tested"), + ("1.11.0", "tested"), ] @@ -156,9 +157,9 @@ def test_discovery_filters_known_prerelease_and_old_versions() -> None: def test_discovery_pages_until_the_monitoring_floor() -> None: module = load_script() manifest = module.load_json(MANIFEST) - first_page = [release("1.10.3")] + first_page = [release("1.11.1")] first_page.extend({"draft": True} for _ in range(module.MAX_RELEASES_PER_PAGE - 1)) - second_page = [release("1.10.2")] + second_page = [release("1.11.0")] requested: list[str] = [] def fake_request(url: str) -> list[dict[str, Any]]: @@ -168,14 +169,14 @@ def fake_request(url: str) -> list[dict[str, Any]]: with patched_attr(module, "_request_json", fake_request): unseen = module.discover_unseen(manifest) - assert [item["tag_name"] for item in unseen] == ["v1.10.3"] + assert [item["tag_name"] for item in unseen] == ["v1.11.1"] assert len(requested) == 2 def test_discovery_fails_when_bounded_pages_do_not_reach_floor() -> None: module = load_script() manifest = module.load_json(MANIFEST) - page = [release("1.10.3")] + page = [release("1.11.1")] page.extend({"draft": True} for _ in range(module.MAX_RELEASES_PER_PAGE - 1)) with patched_attr(module, "_request_json", lambda _url: page): @@ -220,14 +221,14 @@ def test_qualification_matrix_accepts_the_next_manual_patch() -> None: module = load_script() manifest = module.load_json(MANIFEST) - matrix = module.qualification_matrix(manifest, [release("1.10.3")]) + matrix = module.qualification_matrix(manifest, [release("1.11.1")]) assert matrix == { "include": [ { - "comparison_version": "1.10.2", - "tag": "v1.10.3", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tag": "v1.11.1", + "tested_baseline_version": "1.11.0", } ] } @@ -1078,6 +1079,166 @@ def test_compatibility_gateway_rejects_malformed_messages_over_real_http() -> No assert error.value.code == 400 +def test_grouping_inventory_strictly_parses_old_and_new_release_shapes() -> None: + """Qualification pins the 1.10 line and 1.11.0 to different exact wire shapes.""" + + module = load_script() + + def messages(inventory: str) -> list[dict[str, str]]: + """Wrap one inventory in OCR's public grouping user prompt.""" + + return [ + { + "role": "user", + "content": ( + "Group the following changed files:\n\n" + f"{inventory}\n\n" + "Respond with a JSON array:\n" + '[{"label": "theme", "files": ["path"]}]' + ), + } + ] + + old_inventory = messages( + "src/space (unicode) λ.py (ADDED, +10/-0)\n" + "win\\deleted.hbs (DELETED, +0/-5)\n" + "renamed.mustache (RENAMED, +0/-0)" + ) + old_results = [ + module.parse_grouping_inventory(old_inventory, version) + for version in ("1.10.0", "1.10.1", "1.10.2") + ] + new = module.parse_grouping_inventory( + messages( + "ADDED src/space (unicode) λ.py (+10/-0)\n" + "DELETED win\\deleted.hbs (+0/-5)\n" + "RENAMED renamed.mustache (+0/-0)" + ), + "1.11.0", + ) + + assert ( + old_results[0] + == old_results[1] + == old_results[2] + == new + == [ + module.GroupingInventoryEntry("ADDED", "src/space (unicode) λ.py", 10, 0), + module.GroupingInventoryEntry("DELETED", "win\\deleted.hbs", 0, 5), + module.GroupingInventoryEntry("RENAMED", "renamed.mustache", 0, 0), + ] + ) + with pytest.raises(module.CompatibilityError, match="invalid grouping inventory entry"): + module.parse_grouping_inventory(messages("ADDED path.py (+1/-0)"), "1.10.2") + with pytest.raises(module.CompatibilityError, match="invalid grouping inventory entry"): + module.parse_grouping_inventory(messages("path.py (ADDED, +1/-0)"), "1.11.0") + with pytest.raises(module.CompatibilityError, match="not qualified"): + module.parse_grouping_inventory(messages("path.py (ADDED, +1/-0)"), "1.9.10") + + +@pytest.mark.parametrize( + "inventory", + [ + "MODIFIED one.py (+1/-1)\none.py (MODIFIED, +1/-1)", + "MODIFIED one.py (+1/-1)", + "BINARY image.bin (+0/-0)", + "UNKNOWN one.py (+1/-1)", + "MODIFIED one.py (+01/-1)", + "MODIFIED one.py (+1000000001/-0)", + "MODIFIED one.py (+1/-1)\nMODIFIED one.py (+1/-1)", + "MODIFIED one.py (+1/-1) trailing", + ], +) +def test_grouping_inventory_rejects_mixed_duplicate_and_malformed_values( + inventory: str, +) -> None: + """Untrusted grouping prompts fail closed on ambiguity, binary markers, and bounds.""" + + module = load_script() + messages = [ + { + "role": "user", + "content": ( + "Group the following changed files:\n\n" + f"{inventory}\n\nRespond with a JSON array:\n[]" + ), + } + ] + + with pytest.raises(module.CompatibilityError, match="invalid grouping inventory"): + module.parse_grouping_inventory(messages, "1.11.0") + + +def test_grouping_inventory_rejects_missing_duplicate_and_oversized_blocks() -> None: + """Prompt boundaries and entry counts remain singular and bounded.""" + + module = load_script() + valid = { + "role": "user", + "content": ( + "Group the following changed files:\n\n" + "MODIFIED one.py (+1/-1)\n\nRespond with a JSON array:\n[]" + ), + } + + with pytest.raises(module.CompatibilityError, match="exactly one"): + module.parse_grouping_inventory([], "1.11.0") + with pytest.raises(module.CompatibilityError, match="exactly one"): + module.parse_grouping_inventory([valid, valid], "1.11.0") + oversized = "\n".join( + f"MODIFIED file-{index}.py (+1/-1)" + for index in range(module.MAX_GROUPING_INVENTORY_ENTRIES + 1) + ) + with pytest.raises(module.CompatibilityError, match="invalid entry count"): + module.parse_grouping_inventory( + [ + { + "role": "user", + "content": ( + "Group the following changed files:\n\n" + f"{oversized}\n\nRespond with a JSON array:\n[]" + ), + } + ], + "1.11.0", + ) + + +@pytest.mark.parametrize( + "observed", + [ + [ + ("MODIFIED", "second.py", 1, 1), + ("MODIFIED", "first.py", 1, 1), + ], + [ + ("ADDED", "first.py", 1, 1), + ("MODIFIED", "second.py", 1, 1), + ], + [ + ("MODIFIED", "first.py", 0, 0), + ("MODIFIED", "second.py", 1, 1), + ], + ], +) +def test_grouping_inventory_rejects_reorder_status_and_churn_drift( + observed: list[tuple[str, str, int, int]], +) -> None: + """Semantic qualification binds order, status, and churn to the controlled fixture.""" + + module = load_script() + expected = [ + module.GroupingInventoryEntry("MODIFIED", "first.py", 1, 1), + module.GroupingInventoryEntry("MODIFIED", "second.py", 1, 1), + ] + + with pytest.raises(module.CompatibilityError, match="unexpected status/churn"): + module._require_exact_grouping_inventory( + [module.GroupingInventoryEntry(*entry) for entry in observed], + expected, + ) + + def test_numeric_cli_probe_records_closed_boundaries_and_effective_values() -> None: """Derive effective numeric behavior without retaining raw OCR diagnostics.""" @@ -1437,11 +1598,11 @@ def test_prepare_update_rejects_human_review_candidate(tmp_path: Path) -> None: module = load_script() evidence = { "schema_version": 2, - "version": "1.10.3", + "version": "1.11.1", "result": "compatible", "classification": "human-review-required", - "comparison_version": "1.10.2", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tested_baseline_version": "1.11.0", } with pytest.raises(module.CompatibilityError, match="bounded conclusion"): @@ -1457,11 +1618,11 @@ def test_prepare_update_requires_human_review_for_minor_transition() -> None: module = load_script() evidence = { "schema_version": 2, - "version": "1.11.0", + "version": "1.12.0", "result": "compatible", "classification": "automatic-safe", - "comparison_version": "1.10.2", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tested_baseline_version": "1.11.0", } with pytest.raises(module.CompatibilityError, match="explicit human review"): @@ -1473,7 +1634,7 @@ def test_prepare_update_requires_human_review_for_minor_transition() -> None: ) -@pytest.mark.parametrize("version", ["1.11.0", "2.0.0"]) +@pytest.mark.parametrize("version", ["1.12.0", "2.0.0"]) def test_prepare_update_rejects_schema_one_minor_or_major_transition(version: str) -> None: """Legacy evidence cannot prove a chain across a semantic-version boundary.""" @@ -1499,11 +1660,11 @@ def test_prepare_update_rejects_nonadjacent_minor_transition() -> None: module = load_script() evidence = { "schema_version": 2, - "version": "1.12.0", + "version": "1.13.0", "result": "compatible", "classification": "human-review-required", - "comparison_version": "1.10.2", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tested_baseline_version": "1.11.0", } with pytest.raises(module.CompatibilityError, match="contiguous release sequence"): @@ -1511,7 +1672,7 @@ def test_prepare_update_rejects_nonadjacent_minor_transition() -> None: manifest_path=MANIFEST, evidence=evidence, fragment_number=73, - human_conclusions={"1.12.0": "Synthetic reviewed conclusion."}, + human_conclusions={"1.13.0": "Synthetic reviewed conclusion."}, root=PROJECT_ROOT, ) @@ -1520,11 +1681,11 @@ def test_prepare_update_rejects_conclusion_outside_evidence_chain() -> None: module = load_script() evidence = { "schema_version": 2, - "version": "1.10.3", + "version": "1.11.1", "result": "compatible", "classification": "automatic-safe", - "comparison_version": "1.10.2", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tested_baseline_version": "1.11.0", } with pytest.raises(module.CompatibilityError, match="only evidence versions"): @@ -1532,7 +1693,7 @@ def test_prepare_update_rejects_conclusion_outside_evidence_chain() -> None: manifest_path=MANIFEST, evidence=evidence, fragment_number=72, - human_conclusions={"1.10.4": "Synthetic unrelated conclusion."}, + human_conclusions={"1.11.2": "Synthetic unrelated conclusion."}, root=PROJECT_ROOT, ) @@ -1544,11 +1705,11 @@ def test_prepare_update_rejects_invalid_optional_reviewed_conclusion( module = load_script() evidence = { "schema_version": 2, - "version": "1.10.3", + "version": "1.11.1", "result": "compatible", "classification": "automatic-safe", - "comparison_version": "1.10.2", - "tested_baseline_version": "1.10.2", + "comparison_version": "1.11.0", + "tested_baseline_version": "1.11.0", } with pytest.raises(module.CompatibilityError, match="bounded plain text"): @@ -1556,7 +1717,7 @@ def test_prepare_update_rejects_invalid_optional_reviewed_conclusion( manifest_path=MANIFEST, evidence=evidence, fragment_number=72, - human_conclusions={"1.10.3": conclusion}, + human_conclusions={"1.11.1": conclusion}, root=PROJECT_ROOT, ) diff --git a/tests/test_operations_docs.py b/tests/test_operations_docs.py index 4b04b40..af49c97 100644 --- a/tests/test_operations_docs.py +++ b/tests/test_operations_docs.py @@ -1,5 +1,6 @@ """Contracts for the public GitLab operations documentation.""" +import json from pathlib import Path PROJECT_ROOT = Path(__file__).parents[1] @@ -39,6 +40,32 @@ def test_readme_and_gitlab_guide_link_to_operations() -> None: assert "## How reviews evolve" in readme +def test_readme_install_is_isolated_checksum_pinned_and_no_llm() -> None: + """Keep the root installation path exact without using preflight as a smoke test.""" + + readme = README.read_text(encoding="utf-8") + install = readme.split("## Install", 1)[1].split("## How reviews evolve", 1)[0] + manifest = json.loads( + (PROJECT_ROOT / "compatibility" / "ocr-support.json").read_text(encoding="utf-8") + ) + recommended = manifest["recommended_version"] + release = next(item for item in manifest["releases"] if item["version"] == recommended) + digests = {asset["name"]: asset["sha256"] for asset in release["assets"]} + + assert "Python 3.12 through 3.14" in install + assert "uv tool install open-code-review-toolkit" in install + assert install.index(". .venv/bin/activate") < install.index( + "python -m pip install open-code-review-toolkit" + ) + assert f"Open Code Review {recommended}" in install + assert f"open-code-review v{recommended}" in install + assert digests["opencodereview-linux-amd64"] in install + assert digests["opencodereview-darwin-arm64"] in install + assert "ocr --version" in install + assert "ocr-ci --help" in install + assert "not the installation\nsmoke test" in install + + def test_documentation_indexes_route_to_canonical_owners() -> None: docs_index = (PROJECT_ROOT / "docs" / "README.md").read_text(encoding="utf-8") codex_index = (PROJECT_ROOT / "docs" / "codex" / "README.md").read_text(encoding="utf-8") @@ -197,6 +224,7 @@ def test_completion_cap_and_provider_failure_boundaries_are_public() -> None: configuration = CONFIGURATION.read_text(encoding="utf-8") gitlab = GITLAB_GUIDE.read_text(encoding="utf-8") security = (PROJECT_ROOT / "docs" / "security.md").read_text(encoding="utf-8") + compatibility = (PROJECT_ROOT / "docs" / "compatibility.md").read_text(encoding="utf-8") for document in (configuration, operations, gitlab): assert "OCR_LLM_MAX_COMPLETION_TOKENS" in document @@ -205,6 +233,10 @@ def test_completion_cap_and_provider_failure_boundaries_are_public() -> None: assert "provider-specific" in document for document in (configuration, operations, gitlab): assert "OCR_LLM_MAX_COMPLETION_TOKENS=4096" not in document + current_compatibility = compatibility.split("### OCR 1.11.0 — toolkit 0.8.6 target", 1)[1] + assert "override `4096`" not in current_compatibility + assert "operator-selected positive completion-cap override" in current_compatibility + assert "historical OCR 1.10.0 through 1.10.2" in current_compatibility for field in ("max_completion_tokens", "max_output_tokens", "max_tokens"): assert field in configuration for phrase in ( @@ -508,12 +540,15 @@ def test_ocr_compatibility_workflow_is_bounded_and_protected() -> None: "OCR 1.10.0 — toolkit 0.8.2 and 0.8.3 target", "OCR 1.10.1 — toolkit 0.8.4 target", "OCR 1.10.2 — toolkit 0.8.5 target", + "OCR 1.11.0 — toolkit 0.8.6 target", "ocr.toolkit-advisory/v1", "ocr.llm-retry-report/v1", "not toolkit telemetry", "Deploy toolkit 0.8.2 or 0.8.3 directly with OCR 1.10.0", "Deploy toolkit 0.8.4 directly with OCR 1.10.1", "Deploy toolkit 0.8.5 directly with OCR 1.10.2", + "Deploy toolkit 0.8.6 directly with OCR 1.11.0", + "max-tools runtime behavior is unchanged", "max_completion_tokens=16384", "do not install OCR 1.9.10 as an intermediate step", ): @@ -542,11 +577,12 @@ def test_numeric_ocr_controls_use_behavioral_qualification_and_template_delegati compatibility = (PROJECT_ROOT / "docs" / "compatibility.md").read_text(encoding="utf-8") development = (PROJECT_ROOT / "docs" / "development.md").read_text(encoding="utf-8") - assert "`0` delegates to the installed OCR template" in configuration - assert "template's effective `100` rounds" in configuration - assert "result\nwarnings, receipts, DLP inputs, telemetry" in configuration + assert "default at `0` so OCR uses its embedded template limit of `100`" in configuration + assert "explicit `50` remain below the template default" in configuration + assert "raw stderr is not added to\nfindings, result warnings" in configuration + assert "receipts, DLP inputs, telemetry" in configuration assert "`OCR_MAX_TOOLS=0`" in operations - assert "CLI help, normalization text, and effective template value can differ" in operations + assert "corrects stale help text for the already-qualified behavior" in operations assert "effective `100` for omitted, sentinel `0`, `49`, and `50`" in compatibility assert "help text\n alone is not compatibility evidence" in development diff --git a/tests/test_posting_approval.py b/tests/test_posting_approval.py index 8d0463f..1174b81 100644 --- a/tests/test_posting_approval.py +++ b/tests/test_posting_approval.py @@ -261,6 +261,16 @@ def test_publication_dlp_filtered_receipt_is_valid_but_never_eligible(self) -> N "publication DLP filtered the complete review result", ) + def test_provider_private_fields_cannot_enter_approval_receipt(self) -> None: + """Reject replay or request-control fields at the closed receipt boundary.""" + + for field in ("reasoning_content", "native_payload", "tool_choice"): + receipt = receipt_v5() + receipt[field] = "private" + + with self.subTest(field=field): + self.assertFalse(approval.toolkit_receipt_is_valid(receipt)) + def test_filtered_receipt_rejects_outcomes_that_contradict_coverage(self) -> None: """Accept run-level failure but reject impossible coverage/outcome combinations.""" diff --git a/tests/test_posting_helpers.py b/tests/test_posting_helpers.py index 7d45723..c5d24fd 100644 --- a/tests/test_posting_helpers.py +++ b/tests/test_posting_helpers.py @@ -3396,6 +3396,95 @@ def test_security_signal_is_promoted_when_present(self) -> None: self.assertIn("## Security review focus", guide) self.assertIn("**Security signal:**", guide) + def test_reviewer_guide_does_not_promote_neutral_injection_phrase(self) -> None: + """Count only the real security finding from the issue reproducer.""" + + comments = [ + { + "severity": "high", + "category": "security", + "path": "src/command_builder.py", + "line": 12, + "content": "Untrusted input can cause shell command injection.", + }, + { + "severity": "medium", + "category": "bug", + "path": "src/documentation_plan.py", + "line": 34, + "content": "Validate the knowledge injection payload before rendering.", + }, + { + "severity": "medium", + "category": "bug", + "path": "src/schema.py", + "line": 56, + "content": "Constrain the accepted enum values.", + }, + ] + original = [dict(comment) for comment in comments] + + guide = posting_formatting.format_reviewer_guide(comments, omitted_count=0) + + self.assertEqual(comments, original) + self.assertIn("**Security signal:** 1 published OCR finding", guide) + self.assertIn("Estimated effort to review: 2/5", guide) + + def test_security_signal_accepts_only_closed_injection_classes(self) -> None: + """Recognize explicit vulnerability classes across safe separators and casing.""" + + positive = ( + "command injection", + "OS command injection", + "shell command injection", + "SQL-injection", + "NoSQL_INJECTION", + "code injection", + "server-side template injection", + "prompt injection", + "LDAP injection", + "XPath injection", + "CRLF injection", + "HTTP header injection", + "log injection", + "HTML injection", + "script injection", + "expression\u2014injection", + ) + + for content in positive: + with self.subTest(content=content): + self.assertTrue( + posting_formatting.comment_has_security_signal({"content": content}) + ) + + def test_security_signal_rejects_neutral_and_partial_injection_terms(self) -> None: + """Do not promote neutral domains or substrings outside closed word boundaries.""" + + neutral = ( + "knowledge injection", + "dependency injection", + "data injection", + "injection molding", + "codeinjection", + "prompt injectionist", + "expression/injection", + ) + + for content in neutral: + with self.subTest(content=content): + self.assertFalse( + posting_formatting.comment_has_security_signal( + {"severity": "medium", "category": "bug", "content": content} + ) + ) + + self.assertTrue( + posting_formatting.comment_has_security_signal( + {"severity": "low", "category": "security", "content": "dependency injection"} + ) + ) + def test_reviewer_guide_ranks_only_its_published_copy(self) -> None: comments = [ { diff --git a/tests/test_quality_script.py b/tests/test_quality_script.py index d4a944e..1f708dc 100644 --- a/tests/test_quality_script.py +++ b/tests/test_quality_script.py @@ -72,6 +72,18 @@ def test_quality_script_runs_the_bounded_bandit_gate() -> None: assert "tests" not in command +def test_python_commit_gate_applies_and_then_checks_ruff_formatting() -> None: + """Keep immediate formatting and the repository-wide commit gate explicit.""" + + script = SCRIPT.read_text(encoding="utf-8") + development = (PROJECT_ROOT / "docs" / "development.md").read_text(encoding="utf-8") + + assert "set -- uv run --no-sync ruff format ." in script + assert 'for command in "ruff format --check ."' in script + assert "`scripts/quality.sh format` before self-review" in development + assert "`uv run --frozen ruff format --check .`" in development + + def test_quality_script_truncates_multi_command_log_per_invocation(tmp_path: Path) -> None: """Do not mix stale coverage output into a later multi-command result.""" diff --git a/tests/test_review_runner.py b/tests/test_review_runner.py index 4cd5d06..9ad8041 100644 --- a/tests/test_review_runner.py +++ b/tests/test_review_runner.py @@ -1219,6 +1219,195 @@ def test_stage_grouped_retry_report_is_private_and_approval_projection_neutral() assert projected["comments"] == payload["comments"] +def test_provider_private_result_fields_are_removed_without_blocking_publication() -> None: + """Strip provider replay state while preserving canonical review and approval inputs.""" + + hostile_values: list[object] = [ + "Bearer provider-secret-token", + "reviewer@example.invalid", + "/private/session/replay.jsonl", + "https://provider.example.invalid/v1/responses", + "request-id-123456", + "openai/private-model", + "", + "/ocr resolve @mr.bot suppress", + "e\u0301 \u202e hidden", + "nul\x00 tab\t vt\v ff\f", + "x" * 10_001, + {"nested": ["provider", {"secret": "nested-private-value"}]}, + ] + payload: dict[str, object] = { + "status": "complete", + "comments": [ + { + "path": "src/safe.py", + "content": "Keep the bounded branch.", + "thinking": "private comment reasoning", + } + ], + "warnings": [], + "tool_calls": {"total": 2, "by_tool": {"file_read": 2}}, + "usage": { + "input_tokens": 12, + "output_tokens": 5, + "reasoning_tokens": 2, + }, + "reasoning_content": hostile_values[0], + "native_payload": {"turns": hostile_values}, + "session": { + "tool_choice": {"type": "function", "name": "code_comment"}, + "redacted_thinking": hostile_values[1], + "encrypted_content": hostile_values[2], + }, + "manifest": { + "schema_version": "ocr.run-manifest/v1", + "operation": "review", + "terminal_state": "complete", + "coverage": { + "selected": [{"item_id": "safe-a"}], + "completed": [{"item_id": "safe-a"}], + "reused": [], + "failed": [], + "waived": [], + }, + }, + } + baseline = review_runner._canonical_result_projection(payload) + + projected, publication, blocked = review_runner._publication_projection( + payload, + forbidden=("provider-secret-token",), + allowed_tools=review_runner.PUBLIC_REVIEW_TOOL_CALL_NAMES, + ) + + assert blocked is False + assert publication == {"state": "passed"} + assert approval.publication_dlp_state(publication) == "passed" + assert review_runner._canonical_result_projection(projected) == baseline + assert projected["usage"] == payload["usage"] + assert projected["tool_calls"] == payload["tool_calls"] + assert "thinking" not in projected["comments"][0] # type: ignore[index] + assert projected["session"] == {} + serialized = json.dumps(projected, ensure_ascii=False) + for key in review_runner.PROVIDER_PRIVATE_RESULT_KEYS: + assert key not in serialized + for value in ("provider-secret-token", "reviewer@example.invalid", "request-id-123456"): + assert value not in serialized + + +def test_finalized_result_drops_provider_private_fields_before_receipt_binding( + tmp_path: Path, +) -> None: + """Persist only the canonical result and a valid unchanged receipt projection.""" + + result = tmp_path / "result.json" + result.write_text( + json.dumps( + { + "status": "complete", + "comments": [ + { + "path": "src/safe.py", + "content": "Keep the validated branch.", + "thinking": "private reasoning projection", + } + ], + "warnings": [], + "tool_calls": {"total": 1, "by_tool": {"ocr_toolkit_evidence": 1}}, + "reasoning_content": "private chat reasoning", + "native_payload": {"encrypted_content": "private ciphertext"}, + "request": {"tool_choice": "auto"}, + "manifest": { + "schema_version": "ocr.run-manifest/v1", + "operation": "review", + "terminal_state": "complete", + "coverage": { + "selected": [{"item_id": "safe-a"}], + "completed": [{"item_id": "safe-a"}], + "reused": [], + "failed": [], + "waived": [], + }, + }, + } + ), + encoding="utf-8", + ) + composition = MCPComposition( + payload={}, + capabilities=(MCPCapability("ocr_toolkit_evidence", ("ocr_toolkit_evidence",), True),), + external_servers=(), + secret_values=(), + ) + + usage, blocked, publication = review_runner._finalize_ocr_result( + result, + composition, + replace(DEFAULT_IDENTITY, mr_author_id=41), + None, + forbidden=(), + ) + + persisted = json.loads(result.read_text(encoding="utf-8")) + metadata = persisted[ocr_result.TOOLKIT_RESULT_KEY] + assert usage == {"ocr_toolkit_evidence": 1} + assert blocked is False + assert publication == {"state": "passed"} + assert approval.toolkit_receipt_is_valid(metadata) + assert metadata["publication"] == {"state": "passed"} + assert persisted["comments"] == [ + {"path": "src/safe.py", "content": "Keep the validated branch."} + ] + assert persisted["request"] == {} + serialized = result.read_text(encoding="utf-8") + for key in review_runner.PROVIDER_PRIVATE_RESULT_KEYS: + assert key not in serialized + + +@pytest.mark.parametrize("public_field", ["comment", "warning"]) +def test_provider_private_fields_in_public_projection_fail_closed(public_field: str) -> None: + """Treat provider replay state laundered through a public field as filtered output.""" + + comments: list[dict[str, object]] = [] + warnings: list[object] = [] + private_value = {"reasoning_content": "apparently harmless private reasoning"} + if public_field == "comment": + comments.append({"path": "src/safe.py", "content": private_value}) + else: + warnings.append(private_value) + payload: dict[str, object] = { + "status": "complete", + "comments": comments, + "warnings": warnings, + "manifest": { + "schema_version": "ocr.run-manifest/v1", + "operation": "review", + "terminal_state": "complete", + "coverage": { + "selected": [{"item_id": "safe-a"}], + "completed": [{"item_id": "safe-a"}], + "reused": [], + "failed": [], + "waived": [], + }, + }, + } + + projected, publication, blocked = review_runner._publication_projection( + payload, + forbidden=(), + allowed_tools=review_runner.PUBLIC_REVIEW_TOOL_CALL_NAMES, + ) + + assert blocked is True + assert publication["state"] == "publication-filtered" + assert publication["reason_counts"]["invalid_text"] == 1 # type: ignore[index] + assert approval.publication_dlp_state(publication) == "publication-filtered" + assert projected["comments"] == [] + assert projected["warnings"] == [] + assert "reasoning_content" not in json.dumps(projected) + + def test_review_groups_remain_private_and_cannot_change_approval(tmp_path: Path) -> None: """Keep additive group and round diagnostics outside every approval authority.""" diff --git a/tests/test_runtime_helpers.py b/tests/test_runtime_helpers.py index 72ae82e..6f9149f 100644 --- a/tests/test_runtime_helpers.py +++ b/tests/test_runtime_helpers.py @@ -565,6 +565,23 @@ def test_runtime_config_preserves_explicit_empty_extra_body(self) -> None: self.assertEqual(updates["llm.extra_body"], {}) + def test_runtime_config_preserves_explicit_tool_choice_without_defaulting_it(self) -> None: + """Leave tool selection absent unless the operator explicitly owns it.""" + + common = { + "OCR_LLM_URL": "https://gateway.example/v1/chat/completions", + "OCR_LLM_TOKEN": "llm-secret", + "OCR_LLM_MODEL": "openai/gpt-test", + "OCR_LLM_PROTOCOL": "openai", + } + with cleared_env("OCR_LLM_EXTRA_BODY"), patched_env(**common): + inherited = ocr_configure.build_config_updates() + with patched_env(**common, OCR_LLM_EXTRA_BODY='{"tool_choice":"auto"}'): + explicit = ocr_configure.build_config_updates() + + self.assertNotIn("llm.extra_body", inherited) + self.assertEqual(explicit["llm.extra_body"], {"tool_choice": "auto"}) + def test_runtime_config_maps_completion_cap_by_protocol(self) -> None: expected = { "openai": "max_completion_tokens", @@ -1476,7 +1493,7 @@ def test_invalid_json_error_does_not_echo_secret_payload(self) -> None: class PreflightTests(unittest.TestCase): def test_validate_ocr_binary_accepts_supported_version(self) -> None: completed = subprocess.CompletedProcess( - args=["ocr", "--version"], returncode=0, stdout="ocr 1.10.2\n", stderr="" + args=["ocr", "--version"], returncode=0, stdout="ocr 1.11.0\n", stderr="" ) with ( patched_attr(preflight.shutil, "which", lambda _name: "/usr/bin/ocr"),