diff --git a/AGENTS.md b/AGENTS.md index 2df633f498..982d24ece7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,4 +7,7 @@ Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include ( Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). + +OpenCode may repair only trusted `path:line` bindings on LLM probes that already carry an independent proof and source-line digest. See [`docs/doctoring/opencode-llm-review-publication.md`](docs/doctoring/opencode-llm-review-publication.md). + The materialization contract is also covered by [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c48db831fd..285808dbee 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -112,6 +112,9 @@ sequenceDiagram - Required review workflows execute **base-branch** scripts. A PR that edits those workflows cannot widen its own `pull_request_target` token. - Reviewer agents stay `edit: deny`. They judge; they do not implement. +- OpenCode remains the review reasoner. Deterministic code may repair only + trusted `path:line` source-line digest bindings on LLM probes; it never + invents a hypothesis, observed result, or verdict. - Sandbox helpers copy the workspace, drop secret environment values unless explicitly allowlisted by **name**, and run subprocesses with `shell=False`. - Logs and review receipts redact credential shapes (tokens, bearer values, @@ -145,6 +148,8 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for contract. - [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md) — current increment's repair-worker decision and APA 7th citations. +- [`docs/doctoring/opencode-llm-review-publication.md`](docs/doctoring/opencode-llm-review-publication.md) + — LLM probe publication without inventing observed proof. - [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md) — product-specific psychometric repair heartbeat and scientific gates. - [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42afe76a9..8ab82ccea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Semantic Versioning where the repository publishes a release. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. +- Published substantive OpenCode LLM probes when they already carried an independent proof and exact source-line digest but omitted a duplicated `path:line` citation, so NVIDIA NIM / OpenCode review evidence is no longer discarded as `NO_CONCLUSION`. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. diff --git a/CLAUDE.md b/CLAUDE.md index 02d6b3d841..333324e6fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,10 @@ This is the ContextualWisdomLab **organization-wide `.github` special repository **OpenCode judges PRs; GitHub Actions performs mechanical updates and merges.** OpenCode approval is evidence-gated (changed files, CodeGraph evidence, Change Flow DAG, test/coverage/docstring evidence, an actually-executed PoC via `scripts/ci/sandboxed_verify.py` or `scripts/ci/sandboxed_web_e2e.py`, -split `Developer experience:` / `User experience:` sections). The scheduler updates a PR branch only +split `Developer experience:` / `User experience:` sections). Deterministic +code may repair only trusted `path:line` bindings on LLM probes that already +carry an independent proof and source-line digest; it never invents observed +results. The scheduler updates a PR branch only when the latest review is approved, no current-head check has failed, and GitHub reports the PR as behind. The mechanical merge scheduler itself never synthesizes a fix: it gives `DIRTY`/`CONFLICTING` PRs repair guidance. A separate edit-capable autofix flow diff --git a/docs/doctoring/opencode-llm-review-publication.md b/docs/doctoring/opencode-llm-review-publication.md new file mode 100644 index 0000000000..282de15136 --- /dev/null +++ b/docs/doctoring/opencode-llm-review-publication.md @@ -0,0 +1,45 @@ +# OpenCode LLM review publication + +## Decision + +OpenCode remains the review reasoner. Deterministic code may repair only the +serialization binding between an LLM-authored adversarial probe's structured +`path`/`line` and the SHA-256 of that exact immutable current-head source line. +It must not create or alter the hypothesis, counterexample, observed result, +outcome, finding, or verdict. + +## Root cause + +Production run `31310130263` invoked NVIDIA NIM and OpenCode free models, but +several substantive control blocks were discarded even when they carried an exact +source-line receipt because the prose omitted the duplicated `path:line` citation. +Noema is intentionally dispatched only after an exact-head OpenCode approval, +so this publication failure suppressed both review identities. + +## Safety boundary + +Repair is attempted only when original model evidence already contains an +independent proof class and an observed result. The trusted current-head tree +must resolve the structured path and line. A path whose POSIX parts include +`..` is left unchanged (CWE-22; MITRE, n.d.). Changed-file, source-tree, +runtime-receipt, duplicate-probe, outcome, finding-location, coverage, +language, and publication gates still run afterward. Unsupported prose +continues to return `NO_CONCLUSION`. + +## Verification + +The regression suite proves canonical path/line repair only when an exact receipt +already exists, refusal to invent an observed result, rejection of missing or mismatched +receipts, and preservation of malformed and +unverifiable shapes, full normalizer behavior, branch coverage, docstrings, +compileability, and a clean worktree. + +## References + +MITRE. (n.d.). *CWE-22: Improper limitation of a pathname to a restricted +directory ('path traversal')*. Retrieved August 13, 2026, from +https://cwe.mitre.org/data/definitions/22.html + +National Institute of Standards and Technology. (2020). *Security and privacy +controls for information systems and organizations* (NIST Special Publication +800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49a..0ae099d8fe 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 4045d457cb..6e07c29d0d 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,6 +653,82 @@ def adversarial_probe_source_receipt_error( return "" +def repair_adversarial_probe_source_bindings(value: dict[str, Any]) -> dict[str, Any]: + """Canonicalize only the trusted path and line citation of LLM probes. + + The model remains solely responsible for the hypothesis, counterexample, + observed proof, outcome, finding, and verdict. Repair runs only when the + original model evidence already names an independent proof class, an + observed result, and the exact valid source-line digest from the immutable + current-head tree. Missing or mismatched digests remain rejected. + """ + validation = value.get("adversarial_validation") + if not isinstance(validation, dict): + return value + probes = validation.get("probes") + if not isinstance(probes, list): + return value + + repaired_probes: list[Any] = [] + changed = False + for probe in probes: + if not isinstance(probe, dict): + repaired_probes.append(probe) + continue + path_value = probe.get("path") + line_value = probe.get("line") + evidence_value = probe.get("evidence") + if ( + not isinstance(path_value, str) + or not path_value.strip() + or isinstance(line_value, bool) + or not isinstance(line_value, int) + or line_value <= 0 + or not isinstance(evidence_value, str) + or not evidence_value.strip() + ): + repaired_probes.append(probe) + continue + + normalized_path = path_value.strip() + if ".." in PurePosixPath(normalized_path).parts: + repaired_probes.append(probe) + continue + receipt_error = adversarial_probe_source_receipt_error( + evidence_value, + normalized_path, + line_value, + ) + if receipt_error: + repaired_probes.append(probe) + continue + digest = SOURCE_LINE_RECEIPT_RE.findall(evidence_value)[0].casefold() + + lexical_evidence = SOURCE_LINE_RECEIPT_RE.sub("", evidence_value).strip() + receipt_bound_evidence = ( + f"{lexical_evidence} source-line-sha256={digest}" + ).strip() + if adversarial_evidence_rejection_reason(receipt_bound_evidence, ""): + repaired_probes.append(probe) + continue + + canonical_evidence = ( + f"{lexical_evidence} Trusted current-head source binding at " + f"{normalized_path}:{line_value}; source-line-sha256={digest}" + ).strip() + repaired_probes.append( + {**probe, "path": normalized_path, "evidence": canonical_evidence} + ) + changed = True + + if not changed: + return value + return { + **value, + "adversarial_validation": {**validation, "probes": repaired_probes}, + } + + def adversarial_validation_error( value: Any, *, @@ -1267,6 +1343,7 @@ def reject(reason: str) -> None: return reject("APPROVE cannot contain findings") if result == "REQUEST_CHANGES" and not findings: return reject("REQUEST_CHANGES requires at least one finding") + value = repair_adversarial_probe_source_bindings(value) adversarial_error = adversarial_validation_error( value.get("adversarial_validation"), result=result, diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8bd..1c05feb6ff 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,8 +1506,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" - assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != ''" "scheduler scopes targeted manual queue scans to the requested PR" + assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number) }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews" assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads" diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index 590fb3e530..a24c541743 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -2797,3 +2797,84 @@ def test_main_logs_the_exact_control_rejection_reason(tmp_path, capsys): stderr = capsys.readouterr().err assert "CONTROL_REJECTED candidate=1" in stderr assert "adversarial probe 1 evidence must state the observed proof result" in stderr + + +def test_valid_control_repairs_trusted_model_probe_source_bindings( + tmp_path, monkeypatch +): + """Substantive LLM probes survive formatting drift without weaker evidence.""" + require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") + validation = adversarial_validation() + for probe in validation["probes"]: + probe["evidence"] = ( + "Focused regression command passed and disproved the adversarial hypothesis. " + + source_line_receipt(f"line {probe['line']}") + ) + + normalized = norm.valid_control( + control(adversarial_validation=validation), + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", + ) + + assert normalized is not None + for index, probe in enumerate(normalized["adversarial_validation"]["probes"]): + line = 7 + index + assert f"scripts/ci/example.py:{line}" in probe["evidence"] + assert source_line_receipt(f"line {line}") in probe["evidence"] + assert probe["evidence"].count("source-line-sha256=") == 1 + + +def test_probe_binding_repair_does_not_invent_independent_observation( + tmp_path, monkeypatch +): + """Canonical source binding cannot turn unsupported model prose into approval.""" + require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") + validation = adversarial_validation() + for index, probe in enumerate(validation["probes"]): + probe["evidence"] = ( + f"Description at scripts/ci/example.py:{7 + index} repeats the hypothesis. " + + source_line_receipt(f"line {7 + index}") + ) + reasons = [] + + assert ( + norm.valid_control( + control(adversarial_validation=validation), + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", + rejection_reasons=reasons, + ) + is None + ) + assert any("executed command" in reason for reason in reasons) + + +@pytest.mark.parametrize( + "validation", + [ + None, + {"probes": None}, + {"probes": [None]}, + {"probes": [{"path": 7, "line": 1, "evidence": "command passed"}]}, + {"probes": [{"path": "scripts/ci/example.py", "line": True, "evidence": "command passed"}]}, + {"probes": [{"path": "scripts/ci/example.py", "line": 0, "evidence": "command passed"}]}, + {"probes": [{"path": "scripts/ci/example.py", "line": 7, "evidence": 9}]}, + {"probes": [{"path": "missing.py", "line": 7, "evidence": "command passed"}]}, + { + "probes": [ + { + "path": "../secrets/key.py", + "line": 7, + "evidence": "command passed", + } + ] + }, + ], +) +def test_probe_binding_repair_preserves_unrepairable_shapes(validation): + """Malformed, unsafe, or unverifiable model evidence remains unchanged.""" + candidate = control(adversarial_validation=validation) + assert norm.repair_adversarial_probe_source_bindings(candidate) is candidate