From 6406cfd7ec648b1468bdf40751d0368bb9ede36a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 15 Jul 2026 00:45:02 +0900 Subject: [PATCH 01/29] fix(review): repair verified adversarial line bindings --- .../ci/opencode_review_normalize_output.py | 64 +++++++++++++++++++ scripts/ci/run_opencode_review_model_pool.sh | 3 +- tests/test_opencode_model_pool_runner.py | 8 +++ .../test_opencode_review_normalize_output.py | 56 ++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 4045d457c..c116bccdb 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,6 +653,69 @@ def adversarial_probe_source_receipt_error( return "" +def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | Any: + """Bind a verified structured probe location into otherwise valid evidence. + + Models sometimes place the exact changed-file path and positive line in the + structured ``path``/``line`` fields and copy the correct trusted source-line + receipt, but omit the duplicate ``path:line`` text from ``evidence``. This + repair is deliberately narrower than the validator: it only prefixes that + structured location after the receipt matches the current-head source bytes + and the resulting evidence satisfies every independent-proof and observed- + result check. Invalid digests, unsafe paths, missing changed-file evidence, + and circular or unobserved claims remain unmodified and fail closed. + """ + if not isinstance(value, dict): + return value + validation = value.get("adversarial_validation") + if not isinstance(validation, dict): + return value + probes = validation.get("probes") + if not isinstance(probes, list): + return value + + changed_files = current_changed_files() + repaired_probes: list[Any] = [] + changed = False + for probe in probes: + if not isinstance(probe, dict): + repaired_probes.append(probe) + continue + path = probe.get("path") + line = probe.get("line") + evidence = probe.get("evidence") + if ( + not isinstance(path, str) + or path not in changed_files + or isinstance(line, bool) + or not isinstance(line, int) + or line <= 0 + or not isinstance(evidence, str) + or not evidence.strip() + or adversarial_probe_location_error(path, line) + or adversarial_probe_source_receipt_error(evidence, path, line) + ): + repaired_probes.append(probe) + continue + rejection = adversarial_evidence_rejection_reason(evidence, path, line) + if rejection != "must cite the exact probe path and positive line": + repaired_probes.append(probe) + continue + repaired_evidence = f"{path}:{line} {evidence.strip()}" + if adversarial_evidence_rejection_reason(repaired_evidence, path, line): + repaired_probes.append(probe) + continue + repaired_probes.append({**probe, "evidence": repaired_evidence}) + changed = True + + if not changed: + return value + return { + **value, + "adversarial_validation": {**validation, "probes": repaired_probes}, + } + + def adversarial_validation_error( value: Any, *, @@ -1267,6 +1330,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_evidence_bindings(value) adversarial_error = adversarial_validation_error( value.get("adversarial_validation"), result=result, diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 85e122ab4..9e70f0736 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -186,9 +186,10 @@ write_prompt() { printf 'Do not request changes solely because your tool call, MCP call, or full-file read was not executed. Treat that as a review source limitation unless current-head evidence explicitly reports a materialization failure; any such finding must be tied to that evidence, not a generic model-exhaustion message. REQUEST_CHANGES findings must cite a positive source/evidence line; never use line 0.\n' printf 'Always return a final control block instead of a progress summary. Return only the final review body.\n\n' printf 'Adversarial evidence must state a concrete observed pass, failure, rejection, return value, exit code, or trace outcome and exactly one source-line-sha256=<64 lowercase hex> digest computed from the cited current-head line bytes without its line ending; generic source-inspection or coverage-verification claims are invalid.\n' + printf 'Every adversarial_validation.probes[].evidence string must literally include the same path:positive-line declared by that probe before the observed result and receipt; the separate path and line JSON fields do not replace this evidence citation.\n' printf 'Required control block shape:\n' printf '```json\n' - printf '{"head_sha":"%s","run_id":"%s","run_attempt":"%s","result":"APPROVE or REQUEST_CHANGES","reason":"short reason","summary":"short review summary with concrete evidence and all required labels","adversarial_validation":{"status":"passed or failed","probes":[{"path":"exact/current-head/changed-file","line":1,"hypothesis":"concrete failure hypothesis","attack_or_counterexample":"input, state, race, threat, or boundary used to challenge it","evidence":"executed command or source-backed trace, observed outcome, and source-line-sha256=","outcome":"falsified or confirmed"}],"residual_risk":"bounded residual risk after the probes"},"findings":[]}\n' "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" + printf '{"head_sha":"%s","run_id":"%s","run_attempt":"%s","result":"APPROVE or REQUEST_CHANGES","reason":"short reason","summary":"short review summary with concrete evidence and all required labels","adversarial_validation":{"status":"passed or failed","probes":[{"path":"exact/current-head/changed-file","line":1,"hypothesis":"concrete failure hypothesis","attack_or_counterexample":"input, state, race, threat, or boundary used to challenge it","evidence":"exact/current-head/changed-file:1 source trace or executed command observed a concrete outcome; source-line-sha256=","outcome":"falsified or confirmed"}],"residual_risk":"bounded residual risk after the probes"},"findings":[]}\n' "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" printf '```\n' if [ -s "$evidence_excerpt_file" ]; then printf '\nCurrent-head evidence packet:\n\n' diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 71e96addb..bd52751fe 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -704,3 +704,11 @@ def test_deepseek_prompt_still_inlines_bounded_evidence_excerpt(tmp_path: Path) prompt = prompt_capture.read_text(encoding="utf-8") assert evidence_excerpt in prompt assert "Evidence excerpt omitted" not in prompt + assert ( + "Every adversarial_validation.probes[].evidence string must literally include " + "the same path:positive-line" in prompt + ) + assert ( + '"evidence":"exact/current-head/changed-file:1 source trace or executed command ' + "observed a concrete outcome; source-line-sha256=" in prompt + ) diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index 590fb3e53..d8755ea36 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -400,6 +400,62 @@ def test_adversarial_validation_rejects_unbound_or_mismatched_source_receipts( assert "does not match the cited current-head line" in mismatch_reasons[-1] +def test_valid_control_repairs_only_verified_structured_probe_location_binding( + tmp_path, monkeypatch +): + """A verified receipt may restore missing path:line prose without weakening proof.""" + require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") + validation = adversarial_validation() + unbound_probes = [] + for probe in validation["probes"]: + unbound = dict(probe) + unbound["evidence"] = re.sub( + rf"Focused source trace at {re.escape(probe['path'])}:{probe['line']} and ", + "Regression command ", + probe["evidence"], + ) + unbound_probes.append(unbound) + + normalized = norm.valid_control( + control( + adversarial_validation={ + **validation, + "probes": unbound_probes, + } + ), + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", + ) + + assert normalized is not None + repaired_probes = normalized["adversarial_validation"]["probes"] + assert repaired_probes[0]["evidence"].startswith("scripts/ci/example.py:7 ") + assert repaired_probes[1]["evidence"].startswith("scripts/ci/example.py:8 ") + + +def test_adversarial_probe_binding_repair_fails_closed_for_malformed_or_unobserved_input(): + """Malformed shapes and receipt-only prose remain unchanged and unpublishable.""" + assert norm.repair_adversarial_probe_evidence_bindings(None) is None + + malformed = {"adversarial_validation": {"probes": "not-an-array"}} + assert norm.repair_adversarial_probe_evidence_bindings(malformed) is malformed + + receipt_only = { + "adversarial_validation": { + "probes": [ + "not-an-object", + { + "path": "scripts/ci/example.py", + "line": 7, + "evidence": source_line_receipt("line 7"), + }, + ] + } + } + assert norm.repair_adversarial_probe_evidence_bindings(receipt_only) is receipt_only + + def test_adversarial_source_receipt_helpers_fail_closed_at_trust_boundaries( tmp_path, monkeypatch ): From 1fda560fd2f7ca4e6baa030c97ecd61ae70c65ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 15 Jul 2026 17:54:53 +0900 Subject: [PATCH 02/29] fix(review): derive trusted probe receipts --- .../ci/opencode_review_normalize_output.py | 48 +++++++++++++------ tests/test_opencode_existing_approval_gate.py | 2 + .../test_opencode_review_normalize_output.py | 45 ++++++++++++----- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index c116bccdb..3a18997b7 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -654,16 +654,15 @@ def adversarial_probe_source_receipt_error( def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | Any: - """Bind a verified structured probe location into otherwise valid evidence. + """Bind trusted source receipts and locations into otherwise valid evidence. Models sometimes place the exact changed-file path and positive line in the - structured ``path``/``line`` fields and copy the correct trusted source-line - receipt, but omit the duplicate ``path:line`` text from ``evidence``. This - repair is deliberately narrower than the validator: it only prefixes that - structured location after the receipt matches the current-head source bytes - and the resulting evidence satisfies every independent-proof and observed- - result check. Invalid digests, unsafe paths, missing changed-file evidence, - and circular or unobserved claims remain unmodified and fail closed. + structured ``path``/``line`` fields but either miscompute the receipt or omit + the duplicate ``path:line`` text from ``evidence``. The receipt is a trusted + binding rather than independent proof, so derive it from the sealed current- + head tree only after validating the changed path and positive line. Missing + or duplicate receipts, unsafe paths, missing changed-file evidence, and + circular or unobserved claims remain unmodified and fail closed. """ if not isinstance(value, dict): return value @@ -693,20 +692,39 @@ def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | A or not isinstance(evidence, str) or not evidence.strip() or adversarial_probe_location_error(path, line) - or adversarial_probe_source_receipt_error(evidence, path, line) ): repaired_probes.append(probe) continue - rejection = adversarial_evidence_rejection_reason(evidence, path, line) - if rejection != "must cite the exact probe path and positive line": + + receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) + expected_digest = adversarial_probe_source_line_digest(path, line) + if len(receipts) != 1 or expected_digest is None: repaired_probes.append(probe) continue - repaired_evidence = f"{path}:{line} {evidence.strip()}" - if adversarial_evidence_rejection_reason(repaired_evidence, path, line): + repaired_evidence = SOURCE_LINE_RECEIPT_RE.sub( + f"source-line-sha256={expected_digest}", + evidence.strip(), + count=1, + ) + rejection = adversarial_evidence_rejection_reason( + repaired_evidence, path, line + ) + if rejection == "must cite the exact probe path and positive line": + repaired_evidence = f"{path}:{line} {repaired_evidence}" + elif rejection: repaired_probes.append(probe) continue - repaired_probes.append({**probe, "evidence": repaired_evidence}) - changed = True + if ( + adversarial_probe_source_receipt_error(repaired_evidence, path, line) + or adversarial_evidence_rejection_reason(repaired_evidence, path, line) + ): + repaired_probes.append(probe) + continue + if repaired_evidence == evidence: + repaired_probes.append(probe) + else: + repaired_probes.append({**probe, "evidence": repaired_evidence}) + changed = True if not changed: return value diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 7602b2a18..9f6aea4fe 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -68,6 +68,8 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): "OPENCODE_ARTIFACT_MANIFEST_SHA256", hashlib.sha256(manifest.read_bytes()).hexdigest(), ) + opencode_review_normalize_output.current_changed_files.cache_clear() + opencode_review_normalize_output.trusted_execution_receipts.cache_clear() def valid_body(head: str = HEAD) -> str: diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index d8755ea36..a2a71fbcc 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -346,10 +346,10 @@ def test_adversarial_validation_canonicalizes_case_and_whitespace_for_duplicates assert "duplicates an earlier probe" in reasons[-1] -def test_adversarial_validation_rejects_unbound_or_mismatched_source_receipts( +def test_adversarial_validation_rejects_missing_and_repairs_mismatched_receipts( tmp_path, monkeypatch ): - """Lexical proof prose cannot authorize approval without exact line binding.""" + """Missing receipts fail closed while trusted source bytes repair one mismatch.""" require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") validation = adversarial_validation() @@ -386,18 +386,16 @@ def test_adversarial_validation_rejects_unbound_or_mismatched_source_receipts( "probes": [mismatched, validation["probes"][1]], } ) - mismatch_reasons: list[str] = [] - assert ( - norm.valid_control( - invalid, - expected_head_sha="head", - expected_run_id="run", - expected_run_attempt="attempt", - rejection_reasons=mismatch_reasons, - ) - is None + normalized = norm.valid_control( + invalid, + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", ) - assert "does not match the cited current-head line" in mismatch_reasons[-1] + assert normalized is not None + repaired_evidence = normalized["adversarial_validation"]["probes"][0]["evidence"] + assert "source-line-sha256=" + "0" * 64 not in repaired_evidence + assert source_line_receipt("line 7") in repaired_evidence def test_valid_control_repairs_only_verified_structured_probe_location_binding( @@ -455,6 +453,27 @@ def test_adversarial_probe_binding_repair_fails_closed_for_malformed_or_unobserv } assert norm.repair_adversarial_probe_evidence_bindings(receipt_only) is receipt_only + duplicate_receipt = { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/example.py", + "line": 7, + "evidence": ( + "Focused source trace at scripts/ci/example.py:7 confirmed " + "the guard; " + f"{source_line_receipt('line 7')} " + f"{source_line_receipt('line 7')}" + ), + } + ] + } + } + assert ( + norm.repair_adversarial_probe_evidence_bindings(duplicate_receipt) + is duplicate_receipt + ) + def test_adversarial_source_receipt_helpers_fail_closed_at_trust_boundaries( tmp_path, monkeypatch From 9d7774c53ad17a4245c40860dff5d1b36c323ae8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 15 Jul 2026 18:19:46 +0900 Subject: [PATCH 03/29] refactor(review): isolate trusted receipt repair --- scripts/ci/opencode_review_normalize_output.py | 11 ++++------- scripts/ci/run_opencode_review_model_pool.sh | 3 +-- tests/test_opencode_existing_approval_gate.py | 3 +-- tests/test_opencode_model_pool_runner.py | 8 -------- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 3a18997b7..074377359 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -706,18 +706,15 @@ def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | A evidence.strip(), count=1, ) - rejection = adversarial_evidence_rejection_reason( - repaired_evidence, path, line - ) + rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) if rejection == "must cite the exact probe path and positive line": repaired_evidence = f"{path}:{line} {repaired_evidence}" elif rejection: repaired_probes.append(probe) continue - if ( - adversarial_probe_source_receipt_error(repaired_evidence, path, line) - or adversarial_evidence_rejection_reason(repaired_evidence, path, line) - ): + if adversarial_probe_source_receipt_error( + repaired_evidence, path, line + ) or adversarial_evidence_rejection_reason(repaired_evidence, path, line): repaired_probes.append(probe) continue if repaired_evidence == evidence: diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 9e70f0736..85e122ab4 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -186,10 +186,9 @@ write_prompt() { printf 'Do not request changes solely because your tool call, MCP call, or full-file read was not executed. Treat that as a review source limitation unless current-head evidence explicitly reports a materialization failure; any such finding must be tied to that evidence, not a generic model-exhaustion message. REQUEST_CHANGES findings must cite a positive source/evidence line; never use line 0.\n' printf 'Always return a final control block instead of a progress summary. Return only the final review body.\n\n' printf 'Adversarial evidence must state a concrete observed pass, failure, rejection, return value, exit code, or trace outcome and exactly one source-line-sha256=<64 lowercase hex> digest computed from the cited current-head line bytes without its line ending; generic source-inspection or coverage-verification claims are invalid.\n' - printf 'Every adversarial_validation.probes[].evidence string must literally include the same path:positive-line declared by that probe before the observed result and receipt; the separate path and line JSON fields do not replace this evidence citation.\n' printf 'Required control block shape:\n' printf '```json\n' - printf '{"head_sha":"%s","run_id":"%s","run_attempt":"%s","result":"APPROVE or REQUEST_CHANGES","reason":"short reason","summary":"short review summary with concrete evidence and all required labels","adversarial_validation":{"status":"passed or failed","probes":[{"path":"exact/current-head/changed-file","line":1,"hypothesis":"concrete failure hypothesis","attack_or_counterexample":"input, state, race, threat, or boundary used to challenge it","evidence":"exact/current-head/changed-file:1 source trace or executed command observed a concrete outcome; source-line-sha256=","outcome":"falsified or confirmed"}],"residual_risk":"bounded residual risk after the probes"},"findings":[]}\n' "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" + printf '{"head_sha":"%s","run_id":"%s","run_attempt":"%s","result":"APPROVE or REQUEST_CHANGES","reason":"short reason","summary":"short review summary with concrete evidence and all required labels","adversarial_validation":{"status":"passed or failed","probes":[{"path":"exact/current-head/changed-file","line":1,"hypothesis":"concrete failure hypothesis","attack_or_counterexample":"input, state, race, threat, or boundary used to challenge it","evidence":"executed command or source-backed trace, observed outcome, and source-line-sha256=","outcome":"falsified or confirmed"}],"residual_risk":"bounded residual risk after the probes"},"findings":[]}\n' "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" printf '```\n' if [ -s "$evidence_excerpt_file" ]; then printf '\nCurrent-head evidence packet:\n\n' diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 9f6aea4fe..5f676b94d 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -85,8 +85,7 @@ def valid_body(head: str = HEAD) -> str: "evidence": ( f"Source trace at .github/workflows/opencode-review.yml:{line} " "confirmed the gate rejected the forged evidence. " - "source-line-sha256=" - + hashlib.sha256(source_line).hexdigest() + "source-line-sha256=" + hashlib.sha256(source_line).hexdigest() ), "outcome": "falsified", } diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index bd52751fe..71e96addb 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -704,11 +704,3 @@ def test_deepseek_prompt_still_inlines_bounded_evidence_excerpt(tmp_path: Path) prompt = prompt_capture.read_text(encoding="utf-8") assert evidence_excerpt in prompt assert "Evidence excerpt omitted" not in prompt - assert ( - "Every adversarial_validation.probes[].evidence string must literally include " - "the same path:positive-line" in prompt - ) - assert ( - '"evidence":"exact/current-head/changed-file:1 source trace or executed command ' - "observed a concrete outcome; source-line-sha256=" in prompt - ) From c86c4b4ebd55ac665e0d75649c8a83bbe2e159d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 15 Jul 2026 18:29:16 +0900 Subject: [PATCH 04/29] test(review): restore normalizer coverage gate --- .../test_opencode_review_normalize_output.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index a2a71fbcc..e3490d2a3 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -453,6 +453,43 @@ def test_adversarial_probe_binding_repair_fails_closed_for_malformed_or_unobserv } assert norm.repair_adversarial_probe_evidence_bindings(receipt_only) is receipt_only + unchanged_location = { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/not-changed.py", + "line": 7, + "evidence": ( + "Focused source trace at scripts/ci/not-changed.py:7 confirmed " + f"the guard; {source_line_receipt('line 7')}" + ), + } + ] + } + } + assert ( + norm.repair_adversarial_probe_evidence_bindings(unchanged_location) + is unchanged_location + ) + + unproved_binding = { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/example.py", + "line": 7, + "evidence": ( + f"scripts/ci/example.py:7 {source_line_receipt('line 7')}" + ), + } + ] + } + } + assert ( + norm.repair_adversarial_probe_evidence_bindings(unproved_binding) + is unproved_binding + ) + duplicate_receipt = { "adversarial_validation": { "probes": [ @@ -498,6 +535,31 @@ def test_adversarial_source_receipt_helpers_fail_closed_at_trust_boundaries( norm.adversarial_probe_source_receipt_error(receipt, "missing.py", 1) == "source-line receipt could not be verified from the trusted tree" ) + assert ( + norm.adversarial_probe_source_receipt_error(receipt, "one_line.py", 1) + == "source-line-sha256 receipt does not match the cited current-head line" + ) + + +def test_adversarial_validation_still_rejects_unrepaired_receipt_mismatch(): + """The terminal validator rejects a bad digest when no trusted repair occurred.""" + validation = adversarial_validation() + mismatched = dict(validation["probes"][0]) + mismatched["evidence"] = re.sub( + r"source-line-sha256=[0-9a-f]{64}", + "source-line-sha256=" + "0" * 64, + mismatched["evidence"], + ) + validation["probes"][0] = mismatched + + assert norm.adversarial_validation_error( + validation, + result="APPROVE", + findings=[], + ) == ( + "adversarial probe 1 evidence source-line-sha256 receipt does not match " + "the cited current-head line" + ) def test_adversarial_request_changes_requires_confirmed_probe_at_finding( From 196c5a4c76dcbde28a374c55e975f9b45c74fc5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 15 Jul 2026 19:23:41 +0900 Subject: [PATCH 05/29] fix(review): bind only verified probe locations --- .../ci/opencode_review_normalize_output.py | 22 +-- tests/test_opencode_existing_approval_gate.py | 5 +- tests/test_opencode_model_pool_runner.py | 176 ++++++++++++++++++ .../test_opencode_review_normalize_output.py | 161 ++-------------- 4 files changed, 199 insertions(+), 165 deletions(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 074377359..25d3c393c 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -657,12 +657,11 @@ def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | A """Bind trusted source receipts and locations into otherwise valid evidence. Models sometimes place the exact changed-file path and positive line in the - structured ``path``/``line`` fields but either miscompute the receipt or omit - the duplicate ``path:line`` text from ``evidence``. The receipt is a trusted - binding rather than independent proof, so derive it from the sealed current- - head tree only after validating the changed path and positive line. Missing - or duplicate receipts, unsafe paths, missing changed-file evidence, and - circular or unobserved claims remain unmodified and fail closed. + structured ``path``/``line`` fields but omit the duplicate ``path:line`` text + from ``evidence``. Restore only that redundant location after the existing + single receipt already matches the sealed current-head line. Missing, + duplicate, or mismatched receipts, unsafe paths, missing changed-file + evidence, and circular or unobserved claims remain unmodified and fail closed. """ if not isinstance(value, dict): return value @@ -697,15 +696,12 @@ def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | A continue receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) - expected_digest = adversarial_probe_source_line_digest(path, line) - if len(receipts) != 1 or expected_digest is None: + if len(receipts) != 1 or adversarial_probe_source_receipt_error( + evidence, path, line + ): repaired_probes.append(probe) continue - repaired_evidence = SOURCE_LINE_RECEIPT_RE.sub( - f"source-line-sha256={expected_digest}", - evidence.strip(), - count=1, - ) + repaired_evidence = evidence rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) if rejection == "must cite the exact probe path and positive line": repaired_evidence = f"{path}:{line} {repaired_evidence}" diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 5f676b94d..7602b2a18 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -68,8 +68,6 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): "OPENCODE_ARTIFACT_MANIFEST_SHA256", hashlib.sha256(manifest.read_bytes()).hexdigest(), ) - opencode_review_normalize_output.current_changed_files.cache_clear() - opencode_review_normalize_output.trusted_execution_receipts.cache_clear() def valid_body(head: str = HEAD) -> str: @@ -85,7 +83,8 @@ def valid_body(head: str = HEAD) -> str: "evidence": ( f"Source trace at .github/workflows/opencode-review.yml:{line} " "confirmed the gate rejected the forged evidence. " - "source-line-sha256=" + hashlib.sha256(source_line).hexdigest() + "source-line-sha256=" + + hashlib.sha256(source_line).hexdigest() ), "outcome": "falsified", } diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 71e96addb..ad78fd734 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -14,6 +14,8 @@ import pytest +from scripts.ci import opencode_review_normalize_output as normalizer + ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "scripts" / "ci" / "run_opencode_review_model_pool.sh" @@ -29,6 +31,16 @@ } +@pytest.fixture(autouse=True) +def clear_normalizer_artifact_caches(): + """Keep trusted artifact caches isolated from later review-gate tests.""" + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + yield + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + + def bash_command() -> str: """Return a Bash executable that can run repository shell scripts locally.""" if os.name == "nt": @@ -87,6 +99,170 @@ def seal_artifacts( return hashlib.sha256(manifest.read_bytes()).hexdigest() +def prepare_probe_binding_artifacts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> tuple[str, int, str]: + """Create one sealed changed source line for normalizer binding tests.""" + runner_temp = tmp_path / "binding-runner-temp" + source_root = tmp_path / "binding-source" + source_path = source_root / "scripts" / "ci" / "example.py" + runner_temp.mkdir() + source_path.parent.mkdir(parents=True) + source_path.write_text("return False\n", encoding="utf-8") + changed_files = runner_temp / "opencode-changed-files.txt" + changed_files.write_text("scripts/ci/example.py\n", encoding="utf-8") + manifest_digest = seal_artifacts( + runner_temp, + head_sha="binding-head", + run_id="binding-run", + run_attempt="1", + paths=(changed_files,), + ) + monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) + monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) + monkeypatch.setenv("OPENCODE_ARTIFACT_MANIFEST_SHA256", manifest_digest) + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + digest = hashlib.sha256(b"return False").hexdigest() + return "scripts/ci/example.py", 1, f"source-line-sha256={digest}" + + +def test_normalizer_binds_only_a_verified_structured_probe_location( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A matching receipt may restore only redundant path:line evidence text.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + evidence = ( + f"Regression command rejected malformed input with exit code 1; {receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + repaired = normalizer.repair_adversarial_probe_evidence_bindings(value) + + assert repaired is not value + assert repaired["adversarial_validation"]["probes"][0]["evidence"] == ( + f"{path}:{line} {evidence}" + ) + + +def test_normalizer_probe_binding_repair_remains_fail_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Malformed, untrusted, circular, and already-bound probes are not rewritten.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + assert normalizer.repair_adversarial_probe_evidence_bindings(None) is None + + missing_validation: dict[str, object] = {} + assert ( + normalizer.repair_adversarial_probe_evidence_bindings(missing_validation) + is missing_validation + ) + malformed = {"adversarial_validation": {"probes": "not-a-list"}} + assert normalizer.repair_adversarial_probe_evidence_bindings(malformed) is malformed + + invalid_values = [ + { + "adversarial_validation": { + "probes": [ + "not-an-object", + {"path": path, "line": 0, "evidence": receipt}, + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + "source-line-sha256=" + "0" * 64 + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt} {receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/not-changed.py", + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": receipt, + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": f"Source inspection properly handles all cases; {receipt}", + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + f"Regression command at {path}:{line} rejected malformed input " + f"with exit code 1; {receipt}" + ), + } + ] + } + }, + ] + for invalid in invalid_values: + assert normalizer.repair_adversarial_probe_evidence_bindings(invalid) is invalid + + def skip_if_windows_bash_is_unresponsive(command: str) -> None: """Skip with a visible reason when local Git Bash cannot start on Windows.""" if os.name != "nt": diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index e3490d2a3..590fb3e53 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -346,10 +346,10 @@ def test_adversarial_validation_canonicalizes_case_and_whitespace_for_duplicates assert "duplicates an earlier probe" in reasons[-1] -def test_adversarial_validation_rejects_missing_and_repairs_mismatched_receipts( +def test_adversarial_validation_rejects_unbound_or_mismatched_source_receipts( tmp_path, monkeypatch ): - """Missing receipts fail closed while trusted source bytes repair one mismatch.""" + """Lexical proof prose cannot authorize approval without exact line binding.""" require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") validation = adversarial_validation() @@ -386,130 +386,18 @@ def test_adversarial_validation_rejects_missing_and_repairs_mismatched_receipts( "probes": [mismatched, validation["probes"][1]], } ) - normalized = norm.valid_control( - invalid, - expected_head_sha="head", - expected_run_id="run", - expected_run_attempt="attempt", - ) - assert normalized is not None - repaired_evidence = normalized["adversarial_validation"]["probes"][0]["evidence"] - assert "source-line-sha256=" + "0" * 64 not in repaired_evidence - assert source_line_receipt("line 7") in repaired_evidence - - -def test_valid_control_repairs_only_verified_structured_probe_location_binding( - tmp_path, monkeypatch -): - """A verified receipt may restore missing path:line prose without weakening proof.""" - require_adversarial_validation(tmp_path, monkeypatch, "scripts/ci/example.py") - validation = adversarial_validation() - unbound_probes = [] - for probe in validation["probes"]: - unbound = dict(probe) - unbound["evidence"] = re.sub( - rf"Focused source trace at {re.escape(probe['path'])}:{probe['line']} and ", - "Regression command ", - probe["evidence"], - ) - unbound_probes.append(unbound) - - normalized = norm.valid_control( - control( - adversarial_validation={ - **validation, - "probes": unbound_probes, - } - ), - expected_head_sha="head", - expected_run_id="run", - expected_run_attempt="attempt", - ) - - assert normalized is not None - repaired_probes = normalized["adversarial_validation"]["probes"] - assert repaired_probes[0]["evidence"].startswith("scripts/ci/example.py:7 ") - assert repaired_probes[1]["evidence"].startswith("scripts/ci/example.py:8 ") - - -def test_adversarial_probe_binding_repair_fails_closed_for_malformed_or_unobserved_input(): - """Malformed shapes and receipt-only prose remain unchanged and unpublishable.""" - assert norm.repair_adversarial_probe_evidence_bindings(None) is None - - malformed = {"adversarial_validation": {"probes": "not-an-array"}} - assert norm.repair_adversarial_probe_evidence_bindings(malformed) is malformed - - receipt_only = { - "adversarial_validation": { - "probes": [ - "not-an-object", - { - "path": "scripts/ci/example.py", - "line": 7, - "evidence": source_line_receipt("line 7"), - }, - ] - } - } - assert norm.repair_adversarial_probe_evidence_bindings(receipt_only) is receipt_only - - unchanged_location = { - "adversarial_validation": { - "probes": [ - { - "path": "scripts/ci/not-changed.py", - "line": 7, - "evidence": ( - "Focused source trace at scripts/ci/not-changed.py:7 confirmed " - f"the guard; {source_line_receipt('line 7')}" - ), - } - ] - } - } + mismatch_reasons: list[str] = [] assert ( - norm.repair_adversarial_probe_evidence_bindings(unchanged_location) - is unchanged_location - ) - - unproved_binding = { - "adversarial_validation": { - "probes": [ - { - "path": "scripts/ci/example.py", - "line": 7, - "evidence": ( - f"scripts/ci/example.py:7 {source_line_receipt('line 7')}" - ), - } - ] - } - } - assert ( - norm.repair_adversarial_probe_evidence_bindings(unproved_binding) - is unproved_binding - ) - - duplicate_receipt = { - "adversarial_validation": { - "probes": [ - { - "path": "scripts/ci/example.py", - "line": 7, - "evidence": ( - "Focused source trace at scripts/ci/example.py:7 confirmed " - "the guard; " - f"{source_line_receipt('line 7')} " - f"{source_line_receipt('line 7')}" - ), - } - ] - } - } - assert ( - norm.repair_adversarial_probe_evidence_bindings(duplicate_receipt) - is duplicate_receipt + norm.valid_control( + invalid, + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", + rejection_reasons=mismatch_reasons, + ) + is None ) + assert "does not match the cited current-head line" in mismatch_reasons[-1] def test_adversarial_source_receipt_helpers_fail_closed_at_trust_boundaries( @@ -535,31 +423,6 @@ def test_adversarial_source_receipt_helpers_fail_closed_at_trust_boundaries( norm.adversarial_probe_source_receipt_error(receipt, "missing.py", 1) == "source-line receipt could not be verified from the trusted tree" ) - assert ( - norm.adversarial_probe_source_receipt_error(receipt, "one_line.py", 1) - == "source-line-sha256 receipt does not match the cited current-head line" - ) - - -def test_adversarial_validation_still_rejects_unrepaired_receipt_mismatch(): - """The terminal validator rejects a bad digest when no trusted repair occurred.""" - validation = adversarial_validation() - mismatched = dict(validation["probes"][0]) - mismatched["evidence"] = re.sub( - r"source-line-sha256=[0-9a-f]{64}", - "source-line-sha256=" + "0" * 64, - mismatched["evidence"], - ) - validation["probes"][0] = mismatched - - assert norm.adversarial_validation_error( - validation, - result="APPROVE", - findings=[], - ) == ( - "adversarial probe 1 evidence source-line-sha256 receipt does not match " - "the cited current-head line" - ) def test_adversarial_request_changes_requires_confirmed_probe_at_finding( From 534e578319d773e82945f01c3c406538d9e789e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 19 Jul 2026 21:44:41 +0900 Subject: [PATCH 06/29] fix(review): add Saju CalDAV offline Python dependencies --- requirements-opencode-review-ci-hashes.txt | 296 ++++++++++++++++++ requirements-opencode-review-ci.txt | 9 + tests/test_opencode_python_dependency_lock.py | 32 ++ 3 files changed, 337 insertions(+) create mode 100644 tests/test_opencode_python_dependency_lock.py diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 2846355f3..4880434f1 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -1,30 +1,326 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-opencode-review-ci.txt -o requirements-opencode-review-ci-hashes.txt +annotated-doc==0.0.4 \ + --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ + --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 + # via fastapi +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f + # via + # httpx + # starlette attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 + # via interrogate +bcrypt==5.0.0 \ + --hash=sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4 \ + --hash=sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a \ + --hash=sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464 \ + --hash=sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4 \ + --hash=sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746 \ + --hash=sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2 \ + --hash=sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41 \ + --hash=sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd \ + --hash=sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9 \ + --hash=sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e \ + --hash=sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538 \ + --hash=sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10 \ + --hash=sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb \ + --hash=sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef \ + --hash=sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4 \ + --hash=sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23 \ + --hash=sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef \ + --hash=sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75 \ + --hash=sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42 \ + --hash=sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a \ + --hash=sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172 \ + --hash=sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683 \ + --hash=sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2 \ + --hash=sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4 \ + --hash=sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba \ + --hash=sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da \ + --hash=sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493 \ + --hash=sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254 \ + --hash=sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534 \ + --hash=sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f \ + --hash=sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c \ + --hash=sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c \ + --hash=sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83 \ + --hash=sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff \ + --hash=sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d \ + --hash=sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861 \ + --hash=sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5 \ + --hash=sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9 \ + --hash=sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b \ + --hash=sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac \ + --hash=sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e \ + --hash=sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f \ + --hash=sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb \ + --hash=sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86 \ + --hash=sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980 \ + --hash=sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd \ + --hash=sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d \ + --hash=sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1 \ + --hash=sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911 \ + --hash=sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993 \ + --hash=sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191 \ + --hash=sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4 \ + --hash=sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2 \ + --hash=sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8 \ + --hash=sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db \ + --hash=sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927 \ + --hash=sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be \ + --hash=sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb \ + --hash=sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e \ + --hash=sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf \ + --hash=sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd \ + --hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \ + --hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b + # via -r requirements-opencode-review-ci.txt +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via + # httpcore + # httpx click==8.4.2 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via interrogate colorama==0.4.6 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via interrogate coverage==7.14.3 \ --hash=sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727 \ --hash=sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3 + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov +fastapi==0.139.2 \ + --hash=sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e \ + --hash=sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c + # via -r requirements-opencode-review-ci.txt +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via -r requirements-opencode-review-ci.txt +icalendar==7.2.0 \ + --hash=sha256:32dacc396101825b82f9f1bbdf691c02be613130d5ab7a457e553fcd20959fdd \ + --hash=sha256:77922b6be57dfcc2e94f93063d2fd7e948ada9b5bdf7b08bebbc684b1b66c7c4 + # via -r requirements-opencode-review-ci.txt +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via + # anyio + # httpx iniconfig==2.3.0 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest interrogate==1.7.0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 + # via -r requirements-opencode-review-ci.txt +korean-lunar-calendar==0.4.0 \ + --hash=sha256:be56f27bc0594fdbbdf7bbe00f504a9f929a31e311bd7d9bb93561b645afade7 \ + --hash=sha256:c042e20de0bb702add6bec8d0f6da1ea8d3b170838e63846f70420cf341fe4e7 + # via -r requirements-opencode-review-ci.txt packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + # via pytest pluggy==1.6.0 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # pytest + # pytest-cov py==1.11.0 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via interrogate +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 + # via fastapi +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae + # via pydantic pygments==2.20.0 \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via pytest pytest==9.1.1 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov pytest-cov==7.1.0 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via -r requirements-opencode-review-ci.txt +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via icalendar +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via fastapi tabulate==0.10.0 \ --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via interrogate +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # anyio + # fastapi + # icalendar + # pydantic + # pydantic-core + # starlette + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via icalendar uv==0.11.25 \ --hash=sha256:d2bc05e17ae3e1f232abf93e7dcfb3b68702dfcde34a00c29cbce7e07d1ecbfb \ --hash=sha256:560b0fbaa6356af533923a349658c21d4f410d16e835787d8a05da451d4ee859 + # via -r requirements-opencode-review-ci.txt diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index b73fe9833..de7a75716 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -3,3 +3,12 @@ interrogate==1.7.0 pytest==9.1.1 pytest-cov==7.1.0 uv==0.11.25 + +# Trusted runtime set required to execute ContextualWisdomLab/saju-caldav tests +# inside the networkless OpenCode coverage sandbox. Keep these exact pins aligned +# with that repository's reviewed lockfile; PR-controlled manifests remain inert. +bcrypt==5.0.0 +fastapi==0.139.2 +httpx==0.28.1 +icalendar==7.2.0 +korean-lunar-calendar==0.4.0 diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py new file mode 100644 index 000000000..a3097abeb --- /dev/null +++ b/tests/test_opencode_python_dependency_lock.py @@ -0,0 +1,32 @@ +"""Contracts for application dependencies trusted by offline OpenCode coverage.""" + +from pathlib import Path + + +TRUSTED_SAJU_WHEELS = { + "bcrypt": "5.0.0", + "fastapi": "0.139.2", + "httpx": "0.28.1", + "icalendar": "7.2.0", + "korean-lunar-calendar": "0.4.0", +} + + +def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: + source = Path("requirements-opencode-review-ci.txt").read_text(encoding="utf-8") + lock = Path("requirements-opencode-review-ci-hashes.txt").read_text(encoding="utf-8") + + for package, version in TRUSTED_SAJU_WHEELS.items(): + requirement = f"{package}=={version}" + assert requirement in source + locked_requirement = lock.split(requirement, 1)[1].split("\n", 1)[0] + assert locked_requirement.rstrip().endswith("\\") + assert "--hash=sha256:" in lock.split(requirement, 1)[1].split("\n# via", 1)[0] + + assert "lunar-python==" not in source + assert "lunar-python==" not in lock + assert ( + "uv pip compile --generate-hashes --python-version 3.12 " + "--python-platform x86_64-manylinux_2_28 requirements-opencode-review-ci.txt " + "-o requirements-opencode-review-ci-hashes.txt" + ) in lock From 3b2255b9f5a74a6287fd73d5577278b257a2e124 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 11:03:49 +0900 Subject: [PATCH 07/29] fix(review): ignore cancelled scheduler REST checks --- .github/workflows/opencode-review.yml | 1 + scripts/ci/test_strix_quick_gate.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7010b2620..4a083a0b3 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -6118,6 +6118,7 @@ jobs: | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) | select((.status // "") == "completed") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) + | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index aae63c5da..f88c730ef 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1014,6 +1014,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" assert_file_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode approval ignores cancelled scheduler queue replacement checks without source evidence" + assert_file_contains "$workflow_file" '(.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not' "opencode approval also ignores cancelled scheduler queue replacement checks in the workflow-less REST fallback" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" assert_file_contains "$workflow_file" 'current_head_manual_strix_success_status()' "opencode approval can identify same-head manual Strix success status evidence" assert_file_contains "$workflow_file" 'manual_run_line="$(latest_current_head_manual_strix_run || true)"' "opencode approval falls back to same-head manual Strix check-run success when commit status publication is unavailable" From 55219f0b317a63a5effab6e19e38fc5e2ab639cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 17:32:13 +0900 Subject: [PATCH 08/29] fix(review): bind probes to verified evidence lines --- .../ci/opencode_review_normalize_output.py | 89 ++++++++++++++++--- tests/test_opencode_model_pool_runner.py | 66 +++++++++++++- 2 files changed, 142 insertions(+), 13 deletions(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 25d3c393c..cabfc73fd 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,15 +653,55 @@ def adversarial_probe_source_receipt_error( return "" +def receipt_verified_evidence_location( + evidence: str, + changed_files: frozenset[str], +) -> tuple[str, int] | None: + """Return one evidence citation uniquely bound to its trusted source receipt. + + A model can place a valid changed-file ``path:line`` citation and its exact + source-line receipt in ``evidence`` while copying a different location into + the redundant structured fields. Consider only safe current-head changed + files cited by the evidence, recompute every cited line receipt from the + sealed source tree, and return a location only when exactly one distinct + citation matches the single model receipt. Ambiguous or unverified evidence + remains unrepairable and fails closed. + """ + receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) + if len(receipts) != 1: + return None + receipt = receipts[0].casefold() + matches: set[tuple[str, int]] = set() + for path in changed_files: + escaped_path = rf"(? dict[str, Any] | Any: """Bind trusted source receipts and locations into otherwise valid evidence. Models sometimes place the exact changed-file path and positive line in the structured ``path``/``line`` fields but omit the duplicate ``path:line`` text - from ``evidence``. Restore only that redundant location after the existing - single receipt already matches the sealed current-head line. Missing, - duplicate, or mismatched receipts, unsafe paths, missing changed-file - evidence, and circular or unobserved claims remain unmodified and fail closed. + from ``evidence``, or cite and receipt-bind one changed source line in + ``evidence`` while copying a different valid changed-file location into the + redundant structured fields. Restore only a missing citation or rebind the + structured location when exactly one cited line matches the existing single + receipt in the sealed current-head tree. Missing, duplicate, ambiguous, or + mismatched receipts, unsafe paths, missing changed-file evidence, and + circular or unobserved claims remain unmodified and fail closed. """ if not isinstance(value, dict): return value @@ -696,27 +736,52 @@ def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | A continue receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) - if len(receipts) != 1 or adversarial_probe_source_receipt_error( - evidence, path, line - ): + if len(receipts) != 1: repaired_probes.append(probe) continue repaired_evidence = evidence + repaired_path = path + repaired_line = line rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) if rejection == "must cite the exact probe path and positive line": - repaired_evidence = f"{path}:{line} {repaired_evidence}" + if not adversarial_probe_source_receipt_error(evidence, path, line): + repaired_evidence = f"{path}:{line} {repaired_evidence}" + else: + rebound_location = receipt_verified_evidence_location( + evidence, + changed_files, + ) + if rebound_location is None: + repaired_probes.append(probe) + continue + repaired_path, repaired_line = rebound_location elif rejection: repaired_probes.append(probe) continue if adversarial_probe_source_receipt_error( - repaired_evidence, path, line - ) or adversarial_evidence_rejection_reason(repaired_evidence, path, line): + repaired_evidence, repaired_path, repaired_line + ) or adversarial_evidence_rejection_reason( + repaired_evidence, + repaired_path, + repaired_line, + ): repaired_probes.append(probe) continue - if repaired_evidence == evidence: + if ( + repaired_evidence == evidence + and repaired_path == path + and repaired_line == line + ): repaired_probes.append(probe) else: - repaired_probes.append({**probe, "evidence": repaired_evidence}) + repaired_probes.append( + { + **probe, + "path": repaired_path, + "line": repaired_line, + "evidence": repaired_evidence, + } + ) changed = True if not changed: diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index ad78fd734..b5450a91e 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -108,7 +108,10 @@ def prepare_probe_binding_artifacts( source_path = source_root / "scripts" / "ci" / "example.py" runner_temp.mkdir() source_path.parent.mkdir(parents=True) - source_path.write_text("return False\n", encoding="utf-8") + source_path.write_text( + "return False\nraise SystemExit(1)\nraise SystemExit(1)\n", + encoding="utf-8", + ) changed_files = runner_temp / "opencode-changed-files.txt" changed_files.write_text("scripts/ci/example.py\n", encoding="utf-8") manifest_digest = seal_artifacts( @@ -156,6 +159,67 @@ def test_normalizer_binds_only_a_verified_structured_probe_location( ) +def test_normalizer_rebinds_structured_location_to_unique_receipted_citation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A unique cited changed line may repair redundant structured location drift.""" + path, line, _ = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + cited_line = 2 + receipt = "source-line-sha256=" + hashlib.sha256( + b"raise SystemExit(1)" + ).hexdigest() + evidence = ( + f"Source trace at {path}:{cited_line} rejected malformed input with exit code 1; " + f"{receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + repaired = normalizer.repair_adversarial_probe_evidence_bindings(value) + + assert repaired is not value + repaired_probe = repaired["adversarial_validation"]["probes"][0] + assert repaired_probe["path"] == path + assert repaired_probe["line"] == cited_line + assert repaired_probe["evidence"] == evidence + + +def test_normalizer_does_not_rebind_ambiguous_receipted_citations( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Two cited lines with the same trusted bytes remain ambiguous and fail closed.""" + path, line, _ = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + receipt = "source-line-sha256=" + hashlib.sha256( + b"raise SystemExit(1)" + ).hexdigest() + evidence = ( + f"Source traces at {path}:2 and {path}:3 rejected malformed input with exit code 1; " + f"{receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + assert normalizer.repair_adversarial_probe_evidence_bindings(value) is value + + def test_normalizer_probe_binding_repair_remains_fail_closed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From 90f4c5171b6d6e4906b792100547ffef30b2cc5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 17:52:48 +0900 Subject: [PATCH 09/29] test: cover receipt binding rejection edges --- tests/test_opencode_model_pool_runner.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index b5450a91e..f56342f58 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -220,6 +220,30 @@ def test_normalizer_does_not_rebind_ambiguous_receipted_citations( assert normalizer.repair_adversarial_probe_evidence_bindings(value) is value +def test_receipt_verified_location_rejects_unverifiable_evidence( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Missing receipts and invalid cited lines cannot yield a trusted location.""" + path, _, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + changed_files = frozenset({path}) + + assert ( + normalizer.receipt_verified_evidence_location( + f"Source trace at {path}:1 rejected malformed input with exit code 1", + changed_files, + ) + is None + ) + assert ( + normalizer.receipt_verified_evidence_location( + f"Source trace at {path}:99 rejected malformed input with exit code 1; " + f"{receipt}", + changed_files, + ) + is None + ) + + def test_normalizer_probe_binding_repair_remains_fail_closed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From 631cdc84b5344d78673e48356f70148b36fa86a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 22:59:47 +0900 Subject: [PATCH 10/29] fix(review): try GPT-4.1 first --- .github/workflows/opencode-review.yml | 2 +- tests/test_opencode_agent_contract.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7010b2620..ce7572dbc 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3524,7 +3524,7 @@ jobs: # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c8eb1d343..3c8ade2d6 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -103,9 +103,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], - ["github-models", "openai/gpt-4.1"], ["github-models", "openai/gpt-5"], ["github-models", "openai/gpt-5-chat"], ["github-models", "openai/o3"], @@ -115,8 +115,8 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ - "deepseek/deepseek-v3-0324", "openai/gpt-4.1", + "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", "openai/o3", @@ -1090,9 +1090,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " @@ -1213,9 +1213,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " From f8021828133f94b8d339b84691e17b23e874e0c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 23:23:05 +0900 Subject: [PATCH 11/29] docs(review): align candidate-order guidance --- .github/workflows/opencode-review.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index ce7572dbc..f54173125 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,10 +3516,9 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. DeepSeek V3 has been the - # most reliable first-pass reviewer in the org queue, then the pool - # falls through to the direct GPT-5.6 Luna slot, then the full-size - # GPT-4.1 long-context endpoint and provider-specific GPT/o3 fallbacks. + # High-sensitivity review candidates only. GPT-4.1 is the first-pass + # long-context endpoint, then the pool falls through to DeepSeek V3, + # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget From 871db61579d7ed2652af2fba00fb225b32427278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 06:06:07 +0900 Subject: [PATCH 12/29] fix(review): exclude GPT-4.1 approval candidate --- .github/workflows/opencode-review.yml | 9 +++++---- tests/test_opencode_agent_contract.py | 16 ++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f54173125..29f67e7a6 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,14 +3516,15 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. GPT-4.1 is the first-pass - # long-context endpoint, then the pool falls through to DeepSeek V3, - # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. + # High-sensitivity review candidates only. DeepSeek V3 is the + # first-pass reviewer, then the pool falls through to the direct + # GPT-5.6 Luna slot and provider-specific GPT-5/o3 fallbacks. GPT-4.1 + # and weaker review candidates are excluded from approval evidence. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 3c8ade2d6..6e86a065b 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -21,9 +21,8 @@ def test_code_reviewer_subagent_contract_is_configured(): assert reviewer["color"] == "#7c3aed" # Reasoning effort is model-level only (see the model configs below and the # ci-autofix agent). An agent-level reasoningEffort is applied to every - # candidate the agent runs, including non-reasoning models like - # github-models/openai/gpt-4.1, whose OpenAI backend rejects the - # reasoning_effort request argument outright. + # candidate the agent runs, including non-reasoning candidates whose + # provider backends reject the reasoning_effort request argument outright. assert "reasoningEffort" not in reviewer assert "model" not in reviewer assert "Reviews only; never edits code" in reviewer["description"] @@ -42,7 +41,7 @@ def test_code_reviewer_subagent_contract_is_configured(): for primary_agent in ("ci-review", "ci-review-fallback"): # Reasoning effort must NOT be set at the agent level: it would be sent - # to every pool candidate, and non-reasoning models (gpt-4.1) reject the + # to every pool candidate, and non-reasoning candidates reject the # reasoning_effort argument. Reasoning models carry it per-model instead. assert "reasoningEffort" not in agents[primary_agent] permission = agents[primary_agent]["permission"] @@ -103,7 +102,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ - ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["github-models", "openai/gpt-5"], @@ -115,7 +113,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ - "openai/gpt-4.1", "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", @@ -125,6 +122,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] banned_review_candidates = { "gpt-5-nano", + "openai/gpt-4.1", "openai/gpt-5-nano", "openai/o3-mini", } @@ -1090,8 +1088,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' - "github-models/deepseek/deepseek-v3-0324 " + 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " @@ -1213,8 +1210,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' - "github-models/deepseek/deepseek-v3-0324 " + 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " From 447e062bdd4e4c869ced6f7f6e387b02b3b73805 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 07:41:17 +0900 Subject: [PATCH 13/29] fix(review): restore evidence-backed GPT-4.1 lead --- .github/workflows/opencode-review.yml | 9 ++++----- scripts/ci/test_strix_quick_gate.sh | 6 +++--- tests/test_opencode_agent_contract.py | 16 ++++++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 29f67e7a6..f54173125 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,15 +3516,14 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. DeepSeek V3 is the - # first-pass reviewer, then the pool falls through to the direct - # GPT-5.6 Luna slot and provider-specific GPT-5/o3 fallbacks. GPT-4.1 - # and weaker review candidates are excluded from approval evidence. + # High-sensitivity review candidates only. GPT-4.1 is the first-pass + # long-context endpoint, then the pool falls through to DeepSeek V3, + # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index aae63c5da..b51edd555 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -691,7 +691,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode fallback tries the catalog promptly instead of spending the entire review on one model" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool" assert_file_not_contains "$workflow_file" "steps.opencode_review_model_pool.outcome == 'success'" "opencode approval gate still runs after model pool failure to publish a reason" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with DeepSeek V3 before full-size GPT fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review starts with GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree" assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback" assert_file_contains "$workflow_file" '"openai/o4-mini"' "opencode config declares OpenAI o4-mini fallback" @@ -838,7 +838,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode catalog fallback preserves legitimate full-hour provider sessions" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review tries DeepSeek V3 before OpenAI fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review tries GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" "opencode review keeps DeepSeek reasoning fallback coverage after OpenAI candidates" assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" @@ -1155,7 +1155,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review starts the central model pool" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with DeepSeek V3 before full-size GPT fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review starts with GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 6e86a065b..3c8ade2d6 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -21,8 +21,9 @@ def test_code_reviewer_subagent_contract_is_configured(): assert reviewer["color"] == "#7c3aed" # Reasoning effort is model-level only (see the model configs below and the # ci-autofix agent). An agent-level reasoningEffort is applied to every - # candidate the agent runs, including non-reasoning candidates whose - # provider backends reject the reasoning_effort request argument outright. + # candidate the agent runs, including non-reasoning models like + # github-models/openai/gpt-4.1, whose OpenAI backend rejects the + # reasoning_effort request argument outright. assert "reasoningEffort" not in reviewer assert "model" not in reviewer assert "Reviews only; never edits code" in reviewer["description"] @@ -41,7 +42,7 @@ def test_code_reviewer_subagent_contract_is_configured(): for primary_agent in ("ci-review", "ci-review-fallback"): # Reasoning effort must NOT be set at the agent level: it would be sent - # to every pool candidate, and non-reasoning candidates reject the + # to every pool candidate, and non-reasoning models (gpt-4.1) reject the # reasoning_effort argument. Reasoning models carry it per-model instead. assert "reasoningEffort" not in agents[primary_agent] permission = agents[primary_agent]["permission"] @@ -102,6 +103,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["github-models", "openai/gpt-5"], @@ -113,6 +115,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ + "openai/gpt-4.1", "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", @@ -122,7 +125,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] banned_review_candidates = { "gpt-5-nano", - "openai/gpt-4.1", "openai/gpt-5-nano", "openai/o3-mini", } @@ -1088,7 +1090,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " @@ -1210,7 +1213,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " From 54538d1d7767b0c2a71050d1c41391c27aed607a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 10:36:03 +0900 Subject: [PATCH 14/29] fix(review): close trusted log and symlink boundaries --- .github/workflows/opencode-review.yml | 21 ++++ tests/test_opencode_agent_contract.py | 169 ++++++++++++++++++++++++-- 2 files changed, 183 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f54173125..7b6dd9c87 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1626,7 +1626,13 @@ jobs: printf 'Published compact coverage decision output after sanitization (%s bytes); full command logs remain in the job log and step summary.\n' \ "$(wc -c <"$summary_output_file" | tr -d ' ')" + coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + while grep -Fq "$coverage_log_stop_token" "$summary_file"; do + coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + done + printf '::stop-commands::%s\n' "$coverage_log_stop_token" cat "$summary_file" + printf '\n::%s::\n' "$coverage_log_stop_token" # No process running pull-request code may survive into the trusted # publication phase. The result is copied from a root-only tmpfs only # after every low-privilege process has been terminated. @@ -1857,6 +1863,20 @@ jobs: git cat-file -e "${PR_HEAD_SHA}^{commit}" rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" + exit 1 + ;; + esac + done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z) + if find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit | grep -q .; then + echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." + exit 1 + fi git -C "$OPENCODE_SOURCE_WORKDIR" status --short - name: Configure git identity for OpenCode action @@ -2090,6 +2110,7 @@ jobs: CODEGRAPH_BIN="${CODEGRAPH_TRUSTED_ROOT}/node_modules/.bin/codegraph" test -x "$CODEGRAPH_BIN" printf 'Using trusted CodeGraph CLI version %s.\n' "$("$CODEGRAPH_BIN" --version)" + rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph" cd "$OPENCODE_SOURCE_WORKDIR" "$CODEGRAPH_BIN" init -i codegraph_status="$(mktemp)" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 3c8ade2d6..04c0124f6 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -329,7 +329,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step - assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step + assert ( + 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' + in measure_step + ) assert "The networked build context contains only this" in measure_step assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step assert "docker build --pull --no-cache --network=default" in measure_step @@ -406,9 +409,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "uv run --no-project" not in measure_step assert "uv run --no-build" not in measure_step assert "python3 -m coverage run -m pytest tests" in measure_step - trusted_requirements = Path( - "requirements-opencode-review-ci-hashes.txt" - ).read_text(encoding="utf-8") + trusted_requirements = Path("requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) assert "pytest-cov==7.1.0" in trusted_requirements assert ( "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" @@ -496,9 +499,9 @@ def test_opencode_model_exhaustion_retry_stays_owned_by_central_scheduler(): def test_opencode_python_coverage_never_resolves_pr_dependency_manifests(): """Use only the trusted image toolchain during networkless PR execution.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - measure = workflow.split( - " - name: Measure test and docstring evidence\n", 1 - )[1].split("\n - name:", 1)[0] + measure = workflow.split(" - name: Measure test and docstring evidence\n", 1)[ + 1 + ].split("\n - name:", 1)[0] assert "verify_trusted_python_test_toolchain()" in measure assert "PR-selected dependency manifests are never resolved" in measure @@ -1566,6 +1569,18 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) assert 'cat "$summary_output_file"' in coverage_job assert "Published compact coverage decision output" in coverage_job + assert 'coverage_log_stop_token="$(python3 -I -c' in coverage_job + assert 'grep -Fq "$coverage_log_stop_token" "$summary_file"' in coverage_job + assert ( + "printf '::stop-commands::%s\\n' \"$coverage_log_stop_token\"" in coverage_job + ) + assert 'cat "$summary_file"' in coverage_job + assert "printf '\\n::%s::\\n' \"$coverage_log_stop_token\"" in coverage_job + assert ( + coverage_job.index("printf '::stop-commands::%s\\n'") + < coverage_job.index('cat "$summary_file"') + < coverage_job.index("printf '\\n::%s::\\n'") + ) assert "actions: read" in coverage_job assert "contents: read" not in coverage_job assert 'GITHUB_TOKEN: ""' in coverage_job @@ -1617,6 +1632,13 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) < target_job.index( "Exchange OpenCode app token for target repository review reads" ) + materialize_step = target_job.split( + " - name: Materialize pull request head for OpenCode review data", 1 + )[1].split("\n - name:", 1)[0] + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z' in materialize_step + assert "100644 | 100755" in materialize_step + assert 'find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l' in materialize_step + assert "refusing trusted review processing" in materialize_step codegraph_step = target_job.split( " - name: Initialize CodeGraph index for OpenCode", 1 )[1].split("\n - name:", 1)[0] @@ -1639,6 +1661,7 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert '"$CODEGRAPH_BIN" --version' in codegraph_step assert 'cat "$codegraph_status" >&2' in codegraph_step assert 'cat "$codegraph_raw" >&2' in codegraph_step + assert 'rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph"' in codegraph_step assert "CodeGraph status failed; approval evidence is incomplete." in codegraph_step assert ( "CodeGraph changed-scope exploration failed; approval evidence is incomplete." @@ -1669,6 +1692,138 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) +def test_coverage_log_replay_disables_runner_commands_and_retries_token_collision( + tmp_path, +): + """Untrusted coverage bytes stay inside a collision-free stop-command envelope.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(' coverage_log_stop_token="$(python3 -I -c') + replay_end = workflow.index( + " # No process running pull-request code", replay_start + ) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + summary = tmp_path / "coverage-evidence.md" + summary.write_text( + "coverage-log-collision\n" + "::set-output name=coverage_summary::ATTACKER\n" + "::add-path::/tmp/attacker\n", + encoding="utf-8", + ) + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + counter = tmp_path / "counter" + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + 'count="$(cat "$FAKE_COUNTER" 2>/dev/null || printf 0)"\n' + 'printf "%s" "$((count + 1))" >"$FAKE_COUNTER"\n' + 'if [ "$count" -eq 0 ]; then\n' + " printf '%s\\n' coverage-log-collision\n" + "else\n" + " printf '%s\\n' coverage-log-safe\n" + "fi\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_COUNTER": str(counter), + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert lines[0] == "::stop-commands::coverage-log-safe" + assert "::set-output name=coverage_summary::ATTACKER" in lines[1:-1] + assert "::add-path::/tmp/attacker" in lines[1:-1] + assert lines[-1] == "::coverage-log-safe::" + assert counter.read_text(encoding="utf-8") == "2" + + +def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): + """A tracked symlink cannot escape the PR worktree into runner credentials.""" + if not hasattr(os, "symlink"): + pytest.skip("symlinks are unavailable") + + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + validation_start = workflow.index( + " while IFS= read -r -d '' indexed_entry; do" + ) + validation_end = workflow.index( + ' git -C "$OPENCODE_SOURCE_WORKDIR" status --short', + validation_start, + ) + validation = textwrap.dedent(workflow[validation_start:validation_end]) + + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Trust Boundary Test"], cwd=repo, check=True + ) + subprocess.run( + ["git", "config", "user.email", "trust@example.invalid"], + cwd=repo, + check=True, + ) + (repo / "safe.txt").write_text("safe\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + + outside = tmp_path / "runner-credential" + outside.write_text("synthetic-secret\n", encoding="utf-8") + (repo / "credential-link").symlink_to(outside) + subprocess.run(["git", "add", "credential-link"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "symlink head"], cwd=repo, check=True) + + clean_worktree = tmp_path / "clean-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(clean_worktree), base_sha], + cwd=repo, + capture_output=True, + check=True, + ) + clean = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert clean.returncode == 0, clean.stderr + + malicious_worktree = tmp_path / "malicious-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(malicious_worktree), "HEAD"], + cwd=repo, + capture_output=True, + check=True, + ) + rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(malicious_worktree)}, + text=True, + capture_output=True, + check=False, + ) + + assert rejected.returncode == 1 + assert "refusing trusted review processing" in rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + + def test_opencode_pending_peer_checks_hold_blocks_required_workflow_until_approval(): """Pending peer checks cannot satisfy the required gate without a review.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") From 826a628a6612da400a5786c4fe3dde999c38d106 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 12:42:23 +0900 Subject: [PATCH 15/29] fix(review): strip git history from coverage artifacts --- .github/workflows/opencode-review.yml | 17 +++++++++ tests/test_opencode_agent_contract.py | 51 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7b6dd9c87..7c6608f22 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -293,7 +293,24 @@ jobs: mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" git -C "$COVERAGE_SOURCE_WORKDIR" status --short + # Only the current merged source tree may cross the repository boundary. + # The fetch repository contains target history, including deleted files, + # so remove its metadata before producing the same-run artifact. + rm -rf "$COVERAGE_SOURCE_WORKDIR/.git" tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . + if tar -tf "$COVERAGE_SOURCE_ARCHIVE" | awk ' + { + member = $0 + sub(/^\.\//, "", member) + if (member == ".git" || member ~ /(^|\/)\.git(\/|$)/) { + found = 1 + } + } + END { exit(found ? 0 : 1) } + '; then + echo "::error::Coverage source archive unexpectedly contains Git metadata." + exit 1 + fi - name: Upload materialized pull request merge tree uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 04c0124f6..041b6f32d 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -3,6 +3,7 @@ import re import shutil import subprocess +import tarfile import textwrap from pathlib import Path @@ -425,6 +426,56 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "github.event_name == 'pull_request_target'" not in target_condition +def test_coverage_source_artifact_excludes_git_history(tmp_path): + """The cross-job source archive must not carry target repository history.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + package_start = workflow.index( + ' git -C "$COVERAGE_SOURCE_WORKDIR" status --short\n' + ) + package_end = workflow.index("\n\n - name:", package_start) + package_script = textwrap.dedent(workflow[package_start:package_end]) + + repo = tmp_path / "source" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Artifact Boundary Test"], + cwd=repo, + check=True, + ) + subprocess.run( + ["git", "config", "user.email", "artifact@example.invalid"], + cwd=repo, + check=True, + ) + (repo / ".env").write_text("PRIVATE_TOKEN=deleted-history\n", encoding="utf-8") + subprocess.run(["git", "add", ".env"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "historical secret"], cwd=repo, check=True) + (repo / ".env").unlink() + (repo / "safe.txt").write_text("current source\n", encoding="utf-8") + subprocess.run(["git", "add", "-A"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "current source"], cwd=repo, check=True) + + archive = tmp_path / "source.tar" + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + package_script], + env={ + **os.environ, + "COVERAGE_SOURCE_WORKDIR": str(repo), + "COVERAGE_SOURCE_ARCHIVE": str(archive), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + with tarfile.open(archive) as bundle: + names = [name.removeprefix("./") for name in bundle.getnames()] + assert "safe.txt" in names + assert not any(name == ".git" or name.startswith(".git/") for name in names) + + def test_opencode_repository_dispatch_authorization_is_fail_closed(): """Reject an untrusted dispatcher or a target outside the exact allowlist.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") From 039c3c1bc844ce3913fc1408d18a6a3da65d5a79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:01:00 +0900 Subject: [PATCH 16/29] fix(review): keep Git history out of coverage artifacts --- .github/workflows/opencode-review.yml | 187 +++++++++++++++++++++----- tests/test_opencode_agent_contract.py | 127 +++++++++++++---- 2 files changed, 249 insertions(+), 65 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7c6608f22..5171766c4 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -248,12 +248,13 @@ jobs: PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + COVERAGE_BASE_WORKDIR: ${{ runner.temp }}/opencode-coverage-base COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar run: | set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" - rm -rf "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" + rm -rf "$fetch_dir" "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" if [ -z "${GH_TOKEN:-}" ]; then echo "::error::Coverage merge tree materialization requires a GitHub token." exit 1 @@ -290,27 +291,72 @@ jobs: echo "::error::Coverage merge tree could not be materialized for base ${PR_BASE_SHA} and head ${PR_HEAD_SHA}; resolve merge conflicts or rerun after GitHub can synthesize the PR merge commit." exit 1 fi - mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" + + validate_coverage_tree_modes() { + local treeish="$1" + local indexed_entry indexed_mode + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" + exit 1 + ;; + esac + done < <(git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish") + } + validate_coverage_tree_modes "$PR_BASE_SHA" + validate_coverage_tree_modes HEAD + + # BEGIN_COVERAGE_SOURCE_EXPORT + # Export both snapshots without transferring the target repository's + # Git object database into the central repository artifact namespace. + git -C "$fetch_dir" worktree add --detach "$COVERAGE_BASE_WORKDIR" "$PR_BASE_SHA" + rm -f -- "$COVERAGE_BASE_WORKDIR/.git" + rm -rf -- "$fetch_dir/.git" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" - git -C "$COVERAGE_SOURCE_WORKDIR" status --short - # Only the current merged source tree may cross the repository boundary. - # The fetch repository contains target history, including deleted files, - # so remove its metadata before producing the same-run artifact. - rm -rf "$COVERAGE_SOURCE_WORKDIR/.git" - tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . - if tar -tf "$COVERAGE_SOURCE_ARCHIVE" | awk ' - { - member = $0 - sub(/^\.\//, "", member) - if (member == ".git" || member ~ /(^|\/)\.git(\/|$)/) { - found = 1 - } - } - END { exit(found ? 0 : 1) } - '; then - echo "::error::Coverage source archive unexpectedly contains Git metadata." - exit 1 - fi + for source_tree in "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR"; do + if [ -e "$source_tree/.git" ] || [ -L "$source_tree/.git" ]; then + echo "::error::Coverage source export retained forbidden Git metadata at ${source_tree}/.git." + exit 1 + fi + if find -P "$source_tree" -mindepth 1 -type l -print -quit | grep -q .; then + echo "::error::Coverage source export contains a symbolic link; refusing cross-job artifact materialization." + exit 1 + fi + done + tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$RUNNER_TEMP" \ + "$(basename "$COVERAGE_BASE_WORKDIR")" \ + "$(basename "$COVERAGE_SOURCE_WORKDIR")" + python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' + from pathlib import Path, PurePosixPath + import sys + import tarfile + + archive = Path(sys.argv[1]) + if not archive.is_file() or archive.is_symlink(): + raise SystemExit( + f"Coverage source archive is not a regular non-symlink file: {archive}" + ) + with tarfile.open(archive, mode="r:") as bundle: + for member in bundle.getmembers(): + path = PurePosixPath(member.name) + if path.is_absolute() or ".." in path.parts: + raise SystemExit( + f"Coverage source archive contains an unsafe path: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) + if not (member.isfile() or member.isdir()): + raise SystemExit( + "Coverage source archive contains a forbidden non-regular " + f"member: {member.name!r}" + ) + PY + # END_COVERAGE_SOURCE_EXPORT - name: Upload materialized pull request merge tree uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -408,12 +454,14 @@ jobs: COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head run: | set -euo pipefail + artifact_extract_dir="${RUNNER_TEMP}/opencode-coverage-extracted" + rm -rf "$artifact_extract_dir" + mkdir -p "$artifact_extract_dir" rm -rf "$COVERAGE_SOURCE_WORKDIR" - mkdir -p "$COVERAGE_SOURCE_WORKDIR" # The archive contains pull-request-controlled paths. Validate every # member before extraction so a symlink, hardlink, device, FIFO, or # traversal path cannot redirect a later trusted host-side parser. - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR" <<'PY' + python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' import os from pathlib import Path, PurePosixPath import sys @@ -429,6 +477,10 @@ jobs: with tarfile.open(archive, mode="r:*") as bundle: members = bundle.getmembers() seen: set[str] = set() + allowed_roots = { + "opencode-coverage-base", + "opencode-coverage-source", + } for member in members: path = PurePosixPath(member.name) normalized = path.as_posix() @@ -436,6 +488,14 @@ jobs: raise SystemExit( f"Coverage source archive contains an unsafe path: {member.name!r}" ) + if not path.parts or path.parts[0] not in allowed_roots: + raise SystemExit( + f"Coverage source archive contains an unexpected root: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) if normalized in seen: raise SystemExit( f"Coverage source archive contains a duplicate path: {member.name!r}" @@ -453,6 +513,39 @@ jobs: ) bundle.extractall(destination, members=members, filter="data") PY + coverage_base_tree="${artifact_extract_dir}/opencode-coverage-base" + coverage_head_tree="${artifact_extract_dir}/opencode-coverage-source" + for source_tree in "$coverage_base_tree" "$coverage_head_tree"; do + if [ ! -d "$source_tree" ] || [ -L "$source_tree" ]; then + echo "::error::Coverage source artifact did not contain both regular base/head snapshot directories." + exit 1 + fi + done + + # Reconstruct only the two snapshots needed for diff-aware coverage. + # Synthetic Git metadata is created locally in this job and is never + # uploaded to the central repository artifact namespace. + git init "$COVERAGE_SOURCE_WORKDIR" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.name "github-actions[bot]" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + cp -a -- "$coverage_base_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage base snapshot" + coverage_base_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + find "$COVERAGE_SOURCE_WORKDIR" -mindepth 1 -maxdepth 1 ! -name .git \ + -exec rm -rf -- {} + + cp -a -- "$coverage_head_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage head snapshot" + coverage_head_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + { + printf 'COVERAGE_BASE_SHA=%s\n' "$coverage_base_sha" + printf 'COVERAGE_HEAD_SHA=%s\n' "$coverage_head_sha" + } >>"$GITHUB_ENV" + rm -rf "$artifact_extract_dir" git -C "$COVERAGE_SOURCE_WORKDIR" status --short - name: Enforce post-merge stale agent replay guard @@ -473,8 +566,8 @@ jobs: replay_status=0 python3 "$GITHUB_WORKSPACE/scripts/ci/pr_head_replay_guard.py" \ --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? + --base-sha "$COVERAGE_BASE_SHA" \ + --head-sha "$COVERAGE_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? cat "$replay_report" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { @@ -502,7 +595,7 @@ jobs: # files, so a syntax error in a changed file that no test imports (or # in a language with no wired-in runner) could otherwise be approved. changed_files_file="${RUNNER_TEMP}/opencode-syntax-changed-files.txt" - if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$PR_BASE_SHA" HEAD >"$changed_files_file" 2>/dev/null; then + if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" >"$changed_files_file" 2>/dev/null; then : >"$changed_files_file" fi syntax_report="${RUNNER_TEMP}/opencode-syntax-report.txt" @@ -656,6 +749,8 @@ jobs: --env COVERAGE_SOURCE_WORKDIR=/work \ --env PR_BASE_SHA="$PR_BASE_SHA" \ --env PR_HEAD_SHA="$PR_HEAD_SHA" \ + --env COVERAGE_BASE_SHA="$COVERAGE_BASE_SHA" \ + --env COVERAGE_HEAD_SHA="$COVERAGE_HEAD_SHA" \ --env RUNNER_TEMP=/secure-output \ --env GITHUB_OUTPUT=/secure-output/github-output \ --env GITHUB_STEP_SUMMARY=/secure-output/step-summary \ @@ -846,10 +941,10 @@ jobs: } changed_files_for_coverage() { - if [ -n "${PR_BASE_SHA:-}" ] && [ -n "${PR_HEAD_SHA:-}" ] \ - && trusted_git rev-parse --verify --quiet "$PR_BASE_SHA^{commit}" >/dev/null \ - && trusted_git rev-parse --verify --quiet "$PR_HEAD_SHA^{commit}" >/dev/null; then - trusted_git diff --name-only --find-renames "$PR_BASE_SHA" "$PR_HEAD_SHA" + if [ -n "${COVERAGE_BASE_SHA:-}" ] && [ -n "${COVERAGE_HEAD_SHA:-}" ] \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_BASE_SHA^{commit}" >/dev/null \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_HEAD_SHA^{commit}" >/dev/null; then + trusted_git diff --name-only --find-renames "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" else trusted_git ls-files fi @@ -1217,8 +1312,8 @@ jobs: run_and_capture "JavaScript/TypeScript coverage threshold" \ python3 "$GITHUB_WORKSPACE/scripts/ci/javascript_coverage_gate.py" \ --repo-root . \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" \ + --base-sha "$COVERAGE_BASE_SHA" \ + --head-sha "$COVERAGE_HEAD_SHA" \ --summary-list "$summary_list" } @@ -1631,9 +1726,17 @@ jobs: python3 -I "$GITHUB_WORKSPACE/scripts/ci/sanitize_github_output_summary.py" \ "$coverage_output_file" "$summary_output_file" - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi while grep -Fqx "$coverage_output_delimiter" "$summary_output_file"; do - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi done { printf 'coverage_summary<<%s\n' "$coverage_output_delimiter" @@ -1643,13 +1746,23 @@ jobs: printf 'Published compact coverage decision output after sanitization (%s bytes); full command logs remain in the job log and step summary.\n' \ "$(wc -c <"$summary_output_file" | tr -d ' ')" - coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + # BEGIN_COVERAGE_LOG_REPLAY + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi while grep -Fq "$coverage_log_stop_token" "$summary_file"; do - coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi done printf '::stop-commands::%s\n' "$coverage_log_stop_token" cat "$summary_file" printf '\n::%s::\n' "$coverage_log_stop_token" + # END_COVERAGE_LOG_REPLAY # No process running pull-request code may survive into the trusted # publication phase. The result is copied from a root-only tmpfs only # after every low-privilege process has been terminated. @@ -1880,6 +1993,7 @@ jobs: git cat-file -e "${PR_HEAD_SHA}^{commit}" rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" + # BEGIN_PR_WORKTREE_ENTRY_VALIDATION while IFS= read -r -d '' indexed_entry; do indexed_mode="${indexed_entry%% *}" case "$indexed_mode" in @@ -1894,6 +2008,7 @@ jobs: echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." exit 1 fi + # END_PR_WORKTREE_ENTRY_VALIDATION git -C "$OPENCODE_SOURCE_WORKDIR" status --short - name: Configure git identity for OpenCode action diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 041b6f32d..ac3cc4094 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1,6 +1,7 @@ import json import os import re +import shlex import shutil import subprocess import tarfile @@ -311,6 +312,19 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step assert "Coverage merge tree could not be materialized" in step assert "PR_HEAD_SHA:" in step + assert 'validate_coverage_tree_modes "$PR_BASE_SHA"' in step + assert "validate_coverage_tree_modes HEAD" in step + assert 'ls-tree -r -z --full-tree "$treeish"' in step + assert "100644 | 100755" in step + assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step + assert 'rm -rf -- "$fetch_dir/.git"' in step + assert "Coverage source export retained forbidden Git metadata" in step + assert "Coverage source archive contains forbidden Git metadata" in step + assert step.index('rm -rf -- "$fetch_dir/.git"') < step.index( + 'tar -cf "$COVERAGE_SOURCE_ARCHIVE"' + ) + assert '"$(basename "$COVERAGE_BASE_WORKDIR")"' in step + assert '"$(basename "$COVERAGE_SOURCE_WORKDIR")"' in step measure_start = workflow.index( " - name: Measure test and docstring evidence\n" @@ -322,9 +336,23 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "secrets." not in measure_step assert "COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head" in workflow assert ( - 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR"' in workflow + 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' + in coverage_job ) + assert '"opencode-coverage-base"' in coverage_job + assert '"opencode-coverage-source"' in coverage_job assert "member.isfile() or member.isdir()" in workflow + assert "Coverage source archive contains an unexpected root" in coverage_job + assert "Coverage source archive contains forbidden Git metadata" in coverage_job + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in coverage_job + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in coverage_job + assert "coverage base snapshot" in coverage_job + assert "coverage head snapshot" in coverage_job + assert "COVERAGE_BASE_SHA=%s" in coverage_job + assert "COVERAGE_HEAD_SHA=%s" in coverage_job + assert 'diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA"' in coverage_job + assert '--base-sha "$COVERAGE_BASE_SHA"' in coverage_job + assert '--head-sha "$COVERAGE_HEAD_SHA"' in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow assert "docker.io/library/ubuntu@sha256:" in measure_step @@ -429,13 +457,12 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): def test_coverage_source_artifact_excludes_git_history(tmp_path): """The cross-job source archive must not carry target repository history.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - package_start = workflow.index( - ' git -C "$COVERAGE_SOURCE_WORKDIR" status --short\n' - ) - package_end = workflow.index("\n\n - name:", package_start) - package_script = textwrap.dedent(workflow[package_start:package_end]) + export_start = workflow.index(" # BEGIN_COVERAGE_SOURCE_EXPORT\n") + export_start = workflow.index("\n", export_start) + 1 + export_end = workflow.index(" # END_COVERAGE_SOURCE_EXPORT", export_start) + export_script = textwrap.dedent(workflow[export_start:export_end]) - repo = tmp_path / "source" + repo = tmp_path / "fetch" repo.mkdir() subprocess.run(["git", "init", "-q"], cwd=repo, check=True) subprocess.run( @@ -454,15 +481,27 @@ def test_coverage_source_artifact_excludes_git_history(tmp_path): (repo / ".env").unlink() (repo / "safe.txt").write_text("current source\n", encoding="utf-8") subprocess.run(["git", "add", "-A"], cwd=repo, check=True) - subprocess.run(["git", "commit", "-qm", "current source"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "safe base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + (repo / "safe.txt").write_text("current head source\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "current head"], cwd=repo, check=True) - archive = tmp_path / "source.tar" + base_worktree = tmp_path / "opencode-coverage-base" + source_worktree = tmp_path / "opencode-coverage-source" + archive = tmp_path / "opencode-coverage-source.tar" result = subprocess.run( - ["bash", "-c", "set -euo pipefail\n" + package_script], + ["bash", "-c", "set -euo pipefail\n" + export_script], env={ **os.environ, - "COVERAGE_SOURCE_WORKDIR": str(repo), + "COVERAGE_BASE_WORKDIR": str(base_worktree), + "COVERAGE_SOURCE_WORKDIR": str(source_worktree), "COVERAGE_SOURCE_ARCHIVE": str(archive), + "PR_BASE_SHA": base_sha, + "RUNNER_TEMP": str(tmp_path), + "fetch_dir": str(repo), }, text=True, capture_output=True, @@ -471,9 +510,11 @@ def test_coverage_source_artifact_excludes_git_history(tmp_path): assert result.returncode == 0, result.stderr with tarfile.open(archive) as bundle: - names = [name.removeprefix("./") for name in bundle.getnames()] - assert "safe.txt" in names - assert not any(name == ".git" or name.startswith(".git/") for name in names) + names = [name.removeprefix("./").rstrip("/") for name in bundle.getnames()] + assert "opencode-coverage-base/safe.txt" in names + assert "opencode-coverage-source/safe.txt" in names + assert not any(".git" in name.split("/") for name in names) + assert b"deleted-history" not in archive.read_bytes() def test_opencode_repository_dispatch_authorization_is_fail_closed(): @@ -1620,7 +1661,16 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) assert 'cat "$summary_output_file"' in coverage_job assert "Published compact coverage decision output" in coverage_job - assert 'coverage_log_stop_token="$(python3 -I -c' in coverage_job + assert 'coverage_log_stop_token="$(/usr/bin/python3 -I -c' in coverage_job + assert ( + '[[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]' + in coverage_job + ) + assert ( + '[[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]' in coverage_job + ) + assert "# BEGIN_COVERAGE_LOG_REPLAY" in coverage_job + assert "# END_COVERAGE_LOG_REPLAY" in coverage_job assert 'grep -Fq "$coverage_log_stop_token" "$summary_file"' in coverage_job assert ( "printf '::stop-commands::%s\\n' \"$coverage_log_stop_token\"" in coverage_job @@ -1748,15 +1798,16 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio ): """Untrusted coverage bytes stay inside a collision-free stop-command envelope.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - replay_start = workflow.index(' coverage_log_stop_token="$(python3 -I -c') - replay_end = workflow.index( - " # No process running pull-request code", replay_start - ) + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_start = workflow.index("\n", replay_start) + 1 + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY", replay_start) replay = textwrap.dedent(workflow[replay_start:replay_end]) + collision_token = "coverage-log-" + "a" * 48 + safe_token = "coverage-log-" + "b" * 48 summary = tmp_path / "coverage-evidence.md" summary.write_text( - "coverage-log-collision\n" + f"{collision_token}\n" "::set-output name=coverage_summary::ATTACKER\n" "::add-path::/tmp/attacker\n", encoding="utf-8", @@ -1770,16 +1821,22 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio 'count="$(cat "$FAKE_COUNTER" 2>/dev/null || printf 0)"\n' 'printf "%s" "$((count + 1))" >"$FAKE_COUNTER"\n' 'if [ "$count" -eq 0 ]; then\n' - " printf '%s\\n' coverage-log-collision\n" + f" printf '%s\\n' {collision_token}\n" "else\n" - " printf '%s\\n' coverage-log-safe\n" + f" printf '%s\\n' {safe_token}\n" "fi\n", encoding="utf-8", ) fake_python.chmod(0o755) + # Replace the absolute trusted interpreter only inside this test harness so + # the collision-retry branch can be deterministic. The production contract + # itself must remain immune to PATH-prepended executables. + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) result = subprocess.run( - ["bash", "-c", "set -euo pipefail\n" + replay], + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], env={ **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", @@ -1793,25 +1850,25 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio assert result.returncode == 0, result.stderr lines = result.stdout.splitlines() - assert lines[0] == "::stop-commands::coverage-log-safe" + assert lines[0] == f"::stop-commands::{safe_token}" assert "::set-output name=coverage_summary::ATTACKER" in lines[1:-1] assert "::add-path::/tmp/attacker" in lines[1:-1] - assert lines[-1] == "::coverage-log-safe::" + assert lines[-1] == f"::{safe_token}::" assert counter.read_text(encoding="utf-8") == "2" def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): - """A tracked symlink cannot escape the PR worktree into runner credentials.""" + """Tracked and untracked symlinks cannot escape into runner credentials.""" if not hasattr(os, "symlink"): pytest.skip("symlinks are unavailable") workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") validation_start = workflow.index( - " while IFS= read -r -d '' indexed_entry; do" + " # BEGIN_PR_WORKTREE_ENTRY_VALIDATION\n" ) + validation_start = workflow.index("\n", validation_start) + 1 validation_end = workflow.index( - ' git -C "$OPENCODE_SOURCE_WORKDIR" status --short', - validation_start, + " # END_PR_WORKTREE_ENTRY_VALIDATION", validation_start ) validation = textwrap.dedent(workflow[validation_start:validation_end]) @@ -1855,6 +1912,18 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa ) assert clean.returncode == 0, clean.stderr + (clean_worktree / "untracked-credential-link").symlink_to(outside) + untracked_rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert untracked_rejected.returncode == 1 + assert "PR worktree contains a symbolic link" in untracked_rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + malicious_worktree = tmp_path / "malicious-worktree" subprocess.run( ["git", "worktree", "add", "--detach", str(malicious_worktree), "HEAD"], From 6b129f8456351247b4e0dd4038d59858604a30d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:24:48 +0900 Subject: [PATCH 17/29] fix(review): fail closed on artifact validation --- .github/workflows/opencode-review.yml | 46 ++++++++++++++------------- tests/test_opencode_agent_contract.py | 7 +++- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 5171766c4..5c2676bd2 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -295,16 +295,17 @@ jobs: validate_coverage_tree_modes() { local treeish="$1" local indexed_entry indexed_mode - while IFS= read -r -d '' indexed_entry; do - indexed_mode="${indexed_entry%% *}" - case "$indexed_mode" in - 100644 | 100755) ;; - *) - printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" - exit 1 - ;; - esac - done < <(git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish") + git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish" | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" + exit 1 + ;; + esac + done } validate_coverage_tree_modes "$PR_BASE_SHA" validate_coverage_tree_modes HEAD @@ -329,7 +330,7 @@ jobs: tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$RUNNER_TEMP" \ "$(basename "$COVERAGE_BASE_WORKDIR")" \ "$(basename "$COVERAGE_SOURCE_WORKDIR")" - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' from pathlib import Path, PurePosixPath import sys import tarfile @@ -461,7 +462,7 @@ jobs: # The archive contains pull-request-controlled paths. Validate every # member before extraction so a symlink, hardlink, device, FIFO, or # traversal path cannot redirect a later trusted host-side parser. - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' import os from pathlib import Path, PurePosixPath import sys @@ -1994,16 +1995,17 @@ jobs: rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" # BEGIN_PR_WORKTREE_ENTRY_VALIDATION - while IFS= read -r -d '' indexed_entry; do - indexed_mode="${indexed_entry%% *}" - case "$indexed_mode" in - 100644 | 100755) ;; - *) - printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" - exit 1 - ;; - esac - done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z) + git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" + exit 1 + ;; + esac + done if find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit | grep -q .; then echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." exit 1 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index ac3cc4094..f188fe926 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -315,6 +315,8 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'validate_coverage_tree_modes "$PR_BASE_SHA"' in step assert "validate_coverage_tree_modes HEAD" in step assert 'ls-tree -r -z --full-tree "$treeish"' in step + assert 'ls-tree -r -z --full-tree "$treeish" |' in step + assert 'done < <(git -C "$fetch_dir" ls-tree' not in step assert "100644 | 100755" in step assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step assert 'rm -rf -- "$fetch_dir/.git"' in step @@ -336,9 +338,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "secrets." not in measure_step assert "COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head" in workflow assert ( - 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' + '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' in coverage_job ) + assert '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<' in source_job assert '"opencode-coverage-base"' in coverage_job assert '"opencode-coverage-source"' in coverage_job assert "member.isfile() or member.isdir()" in workflow @@ -1737,6 +1740,8 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): " - name: Materialize pull request head for OpenCode review data", 1 )[1].split("\n - name:", 1)[0] assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z' in materialize_step + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z |' in materialize_step + assert 'done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files' not in materialize_step assert "100644 | 100755" in materialize_step assert 'find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l' in materialize_step assert "refusing trusted review processing" in materialize_step From 2fbfdf6fc9eb8b2059ac4d78d7b05e7c3307da29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:43:40 +0900 Subject: [PATCH 18/29] fix(review): preserve replay guard history --- .github/workflows/opencode-review.yml | 110 ++++++++++++++++++-------- tests/test_opencode_agent_contract.py | 24 ++++-- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 5c2676bd2..786980e48 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -171,6 +171,55 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Resolve trusted replay-guard source ref + id: coverage_source_trusted + env: + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} + run: | + set -euo pipefail + /usr/bin/python3 -I <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + trusted_ref = str( + job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" + ).strip() + + if not trusted_ref: + trusted_ref = "main" + prefix = "ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@" + if workflow_ref.startswith(prefix): + trusted_ref = workflow_ref.split("@", 1)[1] + + if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): + print("::error::Trusted OpenCode workflow ref resolved to an invalid value.", file=sys.stderr) + raise SystemExit(1) + + print(f"ref={trusted_ref}") + PY + + - name: Checkout trusted replay guard + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + fetch-depth: 1 + persist-credentials: false + ref: ${{ steps.coverage_source_trusted.outputs.ref }} + path: trusted-replay-guard + - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- @@ -251,6 +300,7 @@ jobs: COVERAGE_BASE_WORKDIR: ${{ runner.temp }}/opencode-coverage-base COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar + TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/scripts/ci/pr_head_replay_guard.py run: | set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" @@ -310,6 +360,32 @@ jobs: validate_coverage_tree_modes "$PR_BASE_SHA" validate_coverage_tree_modes HEAD + # Evaluate replay/unmerge evidence while the real fetched commit + # graph still exists. Snapshot-only artifacts intentionally omit + # target-repository Git history and cannot reproduce this decision. + if [ ! -f "$TRUSTED_REPLAY_GUARD" ] || [ -L "$TRUSTED_REPLAY_GUARD" ]; then + echo "::error::Trusted PR head replay guard is missing or not a regular file." + exit 1 + fi + replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" + replay_status=0 + /usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD" \ + --repo-root "$fetch_dir" \ + --base-sha "$PR_BASE_SHA" \ + --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? + cat "$replay_report" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + printf '## PR head replay guard\n\n```text\n' + cat "$replay_report" + printf '\n```\n' + } >>"$GITHUB_STEP_SUMMARY" + fi + if [ "$replay_status" -ne 0 ]; then + echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." + exit "$replay_status" + fi + # BEGIN_COVERAGE_SOURCE_EXPORT # Export both snapshots without transferring the target repository's # Git object database into the central repository artifact namespace. @@ -384,6 +460,7 @@ jobs: coverage_summary: ${{ steps.measure.outputs.coverage_summary }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITHUB_TOKEN: "" steps: - name: Resolve trusted OpenCode source ref id: trusted_source @@ -549,39 +626,6 @@ jobs: rm -rf "$artifact_extract_dir" git -C "$COVERAGE_SOURCE_WORKDIR" status --short - - name: Enforce post-merge stale agent replay guard - env: - PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} - PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} - COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head - # Dependency resolution may consume wheels/packages, but PR-defined - # install/build hooks are never executed implicitly. - UV_NO_BUILD: "1" - NPM_CONFIG_IGNORE_SCRIPTS: "true" - PNPM_CONFIG_IGNORE_SCRIPTS: "true" - YARN_ENABLE_SCRIPTS: "false" - GITHUB_TOKEN: "" - run: | - set -euo pipefail - replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" - replay_status=0 - python3 "$GITHUB_WORKSPACE/scripts/ci/pr_head_replay_guard.py" \ - --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$COVERAGE_BASE_SHA" \ - --head-sha "$COVERAGE_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? - cat "$replay_report" - if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - { - printf '## PR head replay guard\n\n```text\n' - cat "$replay_report" - printf '\n```\n' - } >>"$GITHUB_STEP_SUMMARY" - fi - if [ "$replay_status" -ne 0 ]; then - echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." - exit "$replay_status" - fi - # Run every trusted follow-up before executing pull-request code. Even a # credential-free test can write runner command files, so no trusted shell # step may consume state after coverage measurement begins. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index f188fe926..10427c264 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -185,18 +185,18 @@ def test_opencode_trusted_source_ref_is_not_controlled_by_workflow_inputs(): assert workflow.count("ref: ${{ steps.trusted_source.outputs.ref }}") == 1 assert "TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.ref }}" in workflow assert "ref: ${{ github.workflow_sha }}" not in workflow - assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 2 - assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 2 + assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 3 + assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 3 assert ( workflow.count( 'job_context.get("workflow_sha") or github_context.get("workflow_sha")' ) - == 2 + == 3 ) - assert workflow.count('workflow_ref.split("@", 1)[1]') == 2 + assert workflow.count('workflow_ref.split("@", 1)[1]') == 3 assert ( workflow.count("Trusted OpenCode workflow ref resolved to an invalid value.") - == 2 + == 3 ) @@ -273,6 +273,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert ( "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in source_job ) + assert "Checkout trusted replay guard" in source_job + assert "persist-credentials: false" in source_job + assert "ref: ${{ steps.coverage_source_trusted.outputs.ref }}" in source_job coverage_start = workflow.index(" coverage-evidence:\n") coverage_end = workflow.index("\n opencode-review-target:", coverage_start) @@ -318,6 +321,14 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'ls-tree -r -z --full-tree "$treeish" |' in step assert 'done < <(git -C "$fetch_dir" ls-tree' not in step assert "100644 | 100755" in step + assert "TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/" in step + assert '/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"' in step + assert '--repo-root "$fetch_dir"' in step + assert '--base-sha "$PR_BASE_SHA"' in step + assert '--head-sha "$PR_HEAD_SHA"' in step + assert step.index('/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"') < step.index( + 'rm -rf -- "$fetch_dir/.git"' + ) assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step assert 'rm -rf -- "$fetch_dir/.git"' in step assert "Coverage source export retained forbidden Git metadata" in step @@ -354,8 +365,7 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "COVERAGE_BASE_SHA=%s" in coverage_job assert "COVERAGE_HEAD_SHA=%s" in coverage_job assert 'diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA"' in coverage_job - assert '--base-sha "$COVERAGE_BASE_SHA"' in coverage_job - assert '--head-sha "$COVERAGE_HEAD_SHA"' in coverage_job + assert "pr_head_replay_guard.py" not in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow assert "docker.io/library/ubuntu@sha256:" in measure_step From 71528f67447e7cc98388d711a8dce59a0fd17435 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:44:51 +0900 Subject: [PATCH 19/29] fix(review): close cross-job source and log boundaries --- .github/workflows/opencode-review.yml | 20 ++++-- scripts/ci/run_opencode_review_model_pool.sh | 9 ++- scripts/ci/test_strix_quick_gate.sh | 18 ++++- tests/test_opencode_agent_contract.py | 73 +++++++++++++++++++- tests/test_opencode_model_pool_runner.py | 11 +++ 5 files changed, 117 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 786980e48..a72d00270 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -324,6 +324,11 @@ jobs: "${PR_HEAD_SHA:-}" exit 1 fi + target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)" + if [ "$target_visibility" != "public" ]; then + echo "::error::Cross-repository coverage artifacts require a public target repository; ${TARGET_REPOSITORY} reported visibility=${target_visibility:-unknown}." + exit 1 + fi auth_header="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$auth_header" git init "$fetch_dir" @@ -341,7 +346,6 @@ jobs: echo "::error::Coverage merge tree could not be materialized for base ${PR_BASE_SHA} and head ${PR_HEAD_SHA}; resolve merge conflicts or rerun after GitHub can synthesize the PR merge commit." exit 1 fi - validate_coverage_tree_modes() { local treeish="$1" local indexed_entry indexed_mode @@ -373,7 +377,8 @@ jobs: --repo-root "$fetch_dir" \ --base-sha "$PR_BASE_SHA" \ --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? - cat "$replay_report" + printf 'Replay guard completed with status %s and captured %s bytes of bounded evidence.\n' \ + "$replay_status" "$(wc -c <"$replay_report" | tr -d ' ')" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## PR head replay guard\n\n```text\n' @@ -2295,15 +2300,17 @@ jobs: codegraph_raw="$(mktemp)" changed_scope="$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" | sed -n '1,80p' | tr '\n' ' ')" if ! "$CODEGRAPH_BIN" status >"$codegraph_status" 2>&1; then - cat "$codegraph_status" >&2 + printf 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_status" | tr -d ' ')" >&2 echo "::error::CodeGraph status failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 fi if ! timeout 120s "$CODEGRAPH_BIN" explore \ "Review the blast radius, call paths, security boundaries, and focused tests for these current-head changed files: ${changed_scope}" \ - >"$codegraph_raw" 2>&1; then - cat "$codegraph_raw" >&2 + >"$codegraph_raw" 2>&1; then + printf 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_raw" | tr -d ' ')" >&2 echo "::error::CodeGraph changed-scope exploration failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 @@ -2316,7 +2323,8 @@ jobs: } >"$CODEGRAPH_EVIDENCE_FILE" rm -f "$codegraph_status" "$codegraph_raw" test -s "$CODEGRAPH_EVIDENCE_FILE" - cat "$CODEGRAPH_EVIDENCE_FILE" + printf 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.\n' \ + "$(wc -c <"$CODEGRAPH_EVIDENCE_FILE" | tr -d ' ')" - name: Prepare bounded OpenCode review evidence timeout-minutes: 12 diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 85e122ab4..fe0a4b44f 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -498,9 +498,6 @@ main() { fi fi deadline=0 - if [ "$budget_seconds" -gt 0 ]; then - deadline=$((SECONDS + budget_seconds)) - fi : >"$OPENCODE_OUTPUT_FILE" cd "$OPENCODE_REVIEW_WORKDIR" read -r -a model_candidates <<<"${OPENCODE_MODEL_CANDIDATES:-}" @@ -528,6 +525,12 @@ main() { opencode_json_file="${candidate_output_file}.jsonl" opencode_export_file="${candidate_output_file}.session.json" write_prompt "$model_candidate" "$prompt_file" + # The retry budget measures provider attempts. Trusted local prompt + # preparation can be slower on a busy runner and must not exhaust the + # budget before the first provider is invoked. + if [ "$deadline" -eq 0 ] && [ "$budget_seconds" -gt 0 ]; then + deadline=$((SECONDS + budget_seconds)) + fi for attempt in $(seq 1 "$attempts"); do now="$SECONDS" if [ "$deadline" -gt 0 ] && [ "$now" -ge "$deadline" ]; then diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b51edd555..131734bb1 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -87,6 +87,16 @@ assert_file_not_contains() { fi } +assert_file_not_matches() { + local file_path="$1" + local pattern="$2" + local message="$3" + + if [ -f "$file_path" ] && grep -Eq -- "$pattern" "$file_path"; then + record_failure "$message (unexpected pattern '$pattern')" + fi +} + seal_opencode_test_artifacts() { local runner_temp="$1" local head_sha="$2" @@ -593,8 +603,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'locked_version" != "4.0.4"' "opencode review verifies both nested installed and locked picomatch evidence" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" explore' "opencode review precomputes structural evidence outside the model process" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" --version' "opencode review logs the exact trusted CodeGraph version" - assert_file_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review exposes CodeGraph status failures in the job log" - assert_file_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review exposes CodeGraph exploration failures in the job log" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review does not replay PR-derived CodeGraph status bytes as workflow commands" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review does not replay PR-derived CodeGraph exploration bytes as workflow commands" + assert_file_not_matches "$workflow_file" '^[[:space:]]{10}cat "\$CODEGRAPH_EVIDENCE_FILE"[[:space:]]*$' "opencode review does not replay assembled PR-derived CodeGraph evidence into the command channel" + assert_file_contains "$workflow_file" 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph status failure metadata" + assert_file_contains "$workflow_file" 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph exploration failure metadata" + assert_file_contains "$workflow_file" 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.' "opencode review reports bounded CodeGraph success metadata" assert_file_not_contains "$workflow_file" "serve --mcp" "opencode review must not fetch or launch CodeGraph again for MCP" assert_file_not_contains "$workflow_file" "https://mcp.deepwiki.com/mcp" "opencode review does not expose remote MCP to the model" assert_file_not_contains "$workflow_file" "@upstash/context7-mcp@3.1.0" "opencode review does not install Context7 at runtime" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 10427c264..ab0098c84 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -311,6 +311,13 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' in step ) + assert ( + 'target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)"' + in step + ) + assert ( + "Cross-repository coverage artifacts require a public target repository" in step + ) assert "Coverage fetch could not authenticate" in step assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step assert "Coverage merge tree could not be materialized" in step @@ -338,6 +345,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): ) assert '"$(basename "$COVERAGE_BASE_WORKDIR")"' in step assert '"$(basename "$COVERAGE_SOURCE_WORKDIR")"' in step + assert 'cat "$replay_report"' not in step.split( + 'if [ -n "${GITHUB_STEP_SUMMARY:-}" ]', 1 + )[0] + assert "Replay guard completed with status" in step measure_start = workflow.index( " - name: Measure test and docstring evidence\n" @@ -368,6 +379,17 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "pr_head_replay_guard.py" not in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow + prepare_start = workflow.index( + " - name: Prepare pull request merge tree for coverage measurement\n" + ) + prepare_end = workflow.index("\n - name:", prepare_start + 1) + prepare_step = workflow[prepare_start:prepare_end] + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign' in prepare_step + assert prepare_step.index("bundle.extractall") < prepare_step.index( + 'git init "$COVERAGE_SOURCE_WORKDIR"' + ) assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step @@ -1775,8 +1797,12 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert '"$CODEGRAPH_BIN" init -i' in codegraph_step assert '"$CODEGRAPH_BIN" status' in codegraph_step assert '"$CODEGRAPH_BIN" --version' in codegraph_step - assert 'cat "$codegraph_status" >&2' in codegraph_step - assert 'cat "$codegraph_raw" >&2' in codegraph_step + assert 'cat "$codegraph_status" >&2' not in codegraph_step + assert 'cat "$codegraph_raw" >&2' not in codegraph_step + assert 'cat "$CODEGRAPH_EVIDENCE_FILE"' not in codegraph_step + assert "CodeGraph status command failed; captured" in codegraph_step + assert "CodeGraph exploration command failed; captured" in codegraph_step + assert "Captured bounded CodeGraph evidence" in codegraph_step assert 'rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph"' in codegraph_step assert "CodeGraph status failed; approval evidence is incomplete." in codegraph_step assert ( @@ -1872,6 +1898,45 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio assert counter.read_text(encoding="utf-8") == "2" +def test_coverage_log_replay_rejects_unsafe_stop_token(tmp_path): + """A compromised PATH generator cannot inject a workflow command token.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY\n", replay_start) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + summary = tmp_path / "coverage-evidence.md" + summary.write_text("safe log\n", encoding="utf-8") + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + "printf 'coverage-log-safe\\n::set-output name=pwned::yes\\n'\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert "stop-token generation returned an unsafe value" in result.stdout + assert "::stop-commands::" not in result.stdout + + def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): """Tracked and untracked symlinks cannot escape into runner credentials.""" if not hasattr(os, "symlink"): @@ -1927,7 +1992,8 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa ) assert clean.returncode == 0, clean.stderr - (clean_worktree / "untracked-credential-link").symlink_to(outside) + untracked_link = clean_worktree / "untracked-credential-link" + untracked_link.symlink_to(outside) untracked_rejected = subprocess.run( ["bash", "-c", "set -euo pipefail\n" + validation], env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, @@ -1938,6 +2004,7 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa assert untracked_rejected.returncode == 1 assert "PR worktree contains a symbolic link" in untracked_rejected.stdout assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + untracked_link.unlink() malicious_worktree = tmp_path / "malicious-worktree" subprocess.run( diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 71e96addb..fdb592733 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -608,6 +608,17 @@ def test_dynamic_review_cadence_uses_small_change_timeout(tmp_path: Path) -> Non assert "retry budget remaining." in result.stdout +def test_review_retry_budget_starts_after_trusted_prompt_preparation() -> None: + """Trusted preflight work cannot consume the provider-attempt retry budget.""" + runner = RUNNER.read_text(encoding="utf-8") + main = runner[runner.index("main() {") :] + prompt = main.index('write_prompt "$model_candidate" "$prompt_file"') + deadline = main.index("deadline=$((SECONDS + budget_seconds))") + first_budget_check = main.index('if [ "$deadline" -gt 0 ]', prompt) + + assert prompt < deadline < first_budget_check + + def test_dynamic_review_cadence_caps_large_change_queue_budget(tmp_path: Path) -> None: """Large PR cadence caps queue time without converting unlimited cycles to one cycle.""" changed_files = [f"backend/changed_{index}.py" for index in range(21)] From df7be3744a321a1cb61d1c0028446413e13e9375 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 15:20:35 +0900 Subject: [PATCH 20/29] fix(review): bound DeepSeek fallback runtime --- .github/workflows/opencode-review.yml | 3 ++ scripts/ci/run_opencode_review_model_pool.sh | 5 +++- tests/test_opencode_agent_contract.py | 3 +- tests/test_opencode_model_pool_runner.py | 30 +++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index d60c40958..00e45ced9 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3767,6 +3767,9 @@ jobs: # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" + # Bound GitHub Models DeepSeek endpoints that return no usable provider + # detail so they cannot consume an entire 90-minute cadence slot. + OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300" OPENCODE_DYNAMIC_MAX_CYCLES: "0" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index fe0a4b44f..4cfb022d2 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -346,6 +346,9 @@ cap_model_run_timeout() { github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS 45)" ;; + github-models/deepseek/*) + cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS 300)" + ;; *) printf '%s\n' "$run_timeout_seconds" return 0 @@ -551,7 +554,7 @@ main() { uncapped_run_timeout="$OPENCODE_RUN_TIMEOUT_SECONDS" OPENCODE_RUN_TIMEOUT_SECONDS="$(cap_model_run_timeout "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS")" if [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -lt "$uncapped_run_timeout" ]; then - printf 'OpenCode %s runtime cap selected %ss instead of %ss because this installation has returned a constrained request-body limit for that endpoint.\n' \ + printf 'OpenCode %s runtime cap selected %ss instead of %ss because the configured provider-specific cap is lower than the cadence timeout.\n' \ "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS" "$uncapped_run_timeout" fi export OPENCODE_RUN_TIMEOUT_SECONDS diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index ab0098c84..bf354d767 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1252,6 +1252,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "0"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow publish_step = workflow.split(" - name: Publish OpenCode review outcome", 1)[ @@ -1309,7 +1310,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "while :" in model_pool_runner assert "should_skip_model_candidate" in model_pool_runner assert "cap_model_run_timeout" in model_pool_runner - assert "constrained request-body limit" in model_pool_runner + assert "configured provider-specific cap" in model_pool_runner assert "run_central_adversarial_harness" not in model_pool_runner assert "finish_pool_without_model" in model_pool_runner assert "central-current-head-adversarial-harness" not in model_pool_runner diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 73d3e977d..604a9b283 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -929,7 +929,7 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert result.returncode == 1 assert ( "OpenCode github-models/openai/gpt-5 runtime cap selected 3s instead of 9s " - "because this installation has returned a constrained request-body limit for that endpoint." + "because the configured provider-specific cap is lower than the cadence timeout." ) in result.stdout attempt_budget = re.search( r"OpenCode github-models/openai/gpt-5 attempt 1/1 using (\d+)s run timeout " @@ -942,6 +942,34 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert run_timeout <= remaining_budget <= 30 +def test_github_deepseek_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: + """No-output DeepSeek endpoints cannot consume a full cadence slot.""" + result = run_failed_model( + tmp_path, + model_candidates="github-models/deepseek/deepseek-v3-0324", + extra_env={ + "OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + ) + + assert result.returncode == 1 + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 runtime cap selected 2s " + "instead of 9s because the configured provider-specific cap is lower than " + "the cadence timeout." + ) in result.stdout + attempt_budget = re.search( + r"OpenCode github-models/deepseek/deepseek-v3-0324 attempt 1/1 using " + r"(\d+)s run timeout with (\d+)s retry budget remaining\.", + result.stdout, + ) + assert attempt_budget is not None + run_timeout, remaining_budget = map(int, attempt_budget.groups()) + assert run_timeout == 2 + assert run_timeout <= remaining_budget <= 30 + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: From 28c71a9830e8bcdee41d6048bf88f882c5c1af6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 19:56:52 +0900 Subject: [PATCH 21/29] test(review): clarify missing dependency locks --- tests/test_opencode_python_dependency_lock.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py index a3097abeb..e6abe90ad 100644 --- a/tests/test_opencode_python_dependency_lock.py +++ b/tests/test_opencode_python_dependency_lock.py @@ -14,14 +14,20 @@ def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: source = Path("requirements-opencode-review-ci.txt").read_text(encoding="utf-8") - lock = Path("requirements-opencode-review-ci-hashes.txt").read_text(encoding="utf-8") + lock = Path("requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) for package, version in TRUSTED_SAJU_WHEELS.items(): requirement = f"{package}=={version}" assert requirement in source - locked_requirement = lock.split(requirement, 1)[1].split("\n", 1)[0] + assert requirement in lock, ( + f"{requirement} is missing from the hashed dependency lock" + ) + locked_section = lock.split(requirement, 1)[1] + locked_requirement = locked_section.split("\n", 1)[0] assert locked_requirement.rstrip().endswith("\\") - assert "--hash=sha256:" in lock.split(requirement, 1)[1].split("\n# via", 1)[0] + assert "--hash=sha256:" in locked_section.split("\n# via", 1)[0] assert "lunar-python==" not in source assert "lunar-python==" not in lock From 533accda3545f674c591d7ea03ab1fd065085fd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 08:47:37 +0900 Subject: [PATCH 22/29] test(review): scope dependency hash assertions --- tests/test_opencode_python_dependency_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py index e6abe90ad..891e02626 100644 --- a/tests/test_opencode_python_dependency_lock.py +++ b/tests/test_opencode_python_dependency_lock.py @@ -27,7 +27,7 @@ def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: locked_section = lock.split(requirement, 1)[1] locked_requirement = locked_section.split("\n", 1)[0] assert locked_requirement.rstrip().endswith("\\") - assert "--hash=sha256:" in locked_section.split("\n# via", 1)[0] + assert "--hash=sha256:" in locked_section.split("\n # via", 1)[0] assert "lunar-python==" not in source assert "lunar-python==" not in lock From 8cc232e0fb67ccaa1c73e86f3eced2fe8b2ee9c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 11:06:38 +0900 Subject: [PATCH 23/29] fix(security): patch Strix pyasn1 DoS --- requirements-strix-ci-hashes.txt | 10 ++++++---- requirements-strix-ci.txt | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index 4cf45203b..85c6bf606 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -1501,10 +1501,12 @@ protobuf==6.33.6 \ # grpc-google-iam-v1 # grpcio-status # proto-plus -pyasn1==0.6.3 \ - --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \ - --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde - # via pyasn1-modules +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via + # -r requirements-strix-ci.txt + # pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index d4889459d..43deaeef8 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -3,3 +3,5 @@ google-cloud-aiplatform==1.133.0 protobuf<7.0.0 cryptography==49.0.0 python-multipart==0.0.31 +# CVE-2026-59885 and CVE-2026-59886: ASN.1 decoder DoS; fixed in 0.6.4 +pyasn1>=0.6.4 From 4d8a00de6ebc82d82581d9ec35739aabbcec2b01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 11:37:21 +0900 Subject: [PATCH 24/29] test(review): harden dependency lock contract --- requirements-strix-ci.txt | 2 +- tests/test_opencode_python_dependency_lock.py | 81 ++++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 43deaeef8..5e5c3ee1c 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -4,4 +4,4 @@ protobuf<7.0.0 cryptography==49.0.0 python-multipart==0.0.31 # CVE-2026-59885 and CVE-2026-59886: ASN.1 decoder DoS; fixed in 0.6.4 -pyasn1>=0.6.4 +pyasn1==0.6.4 diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py index 891e02626..3394d6506 100644 --- a/tests/test_opencode_python_dependency_lock.py +++ b/tests/test_opencode_python_dependency_lock.py @@ -3,6 +3,7 @@ from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[1] TRUSTED_SAJU_WHEELS = { "bcrypt": "5.0.0", "fastapi": "0.139.2", @@ -12,22 +13,82 @@ } +def _lock_stanza(lock: str, requirement: str) -> list[str]: + """Return one top-level requirement stanza without borrowing later hashes.""" + + lines = lock.splitlines() + start = next( + ( + index + for index, line in enumerate(lines) + if line + and not line[0].isspace() + and not line.startswith("#") + and line.split(maxsplit=1)[0] == requirement + ), + None, + ) + assert start is not None, f"{requirement} is missing from the hashed dependency lock" + + stanza = [lines[start]] + for line in lines[start + 1 :]: + if not line.strip(): + break + if not line[0].isspace() and not line.startswith("#"): + break + stanza.append(line) + return stanza + + +def test_lock_stanza_accepts_indented_via_comments() -> None: + lock = ( + "demo==1.0 \\\n" + " --hash=sha256:abc123\n" + " # via example\n" + "next-package==2.0 \\\n" + " --hash=sha256:def456\n" + ) + + assert _lock_stanza(lock, "demo==1.0") == [ + "demo==1.0 \\", + " --hash=sha256:abc123", + " # via example", + ] + + +def test_lock_stanza_does_not_borrow_a_later_requirement_hash() -> None: + lock = ( + "demo==1.0\n" + "next-package==2.0 \\\n" + " --hash=sha256:def456\n" + ) + + assert not any( + line.lstrip().startswith("--hash=sha256:") + for line in _lock_stanza(lock, "demo==1.0") + ) + + def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: - source = Path("requirements-opencode-review-ci.txt").read_text(encoding="utf-8") - lock = Path("requirements-opencode-review-ci-hashes.txt").read_text( + source = (REPO_ROOT / "requirements-opencode-review-ci.txt").read_text( + encoding="utf-8" + ) + lock = (REPO_ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( encoding="utf-8" ) + source_requirements = { + line.split(maxsplit=1)[0] + for line in source.splitlines() + if line.strip() and not line.startswith("#") + } for package, version in TRUSTED_SAJU_WHEELS.items(): requirement = f"{package}=={version}" - assert requirement in source - assert requirement in lock, ( - f"{requirement} is missing from the hashed dependency lock" - ) - locked_section = lock.split(requirement, 1)[1] - locked_requirement = locked_section.split("\n", 1)[0] - assert locked_requirement.rstrip().endswith("\\") - assert "--hash=sha256:" in locked_section.split("\n # via", 1)[0] + assert requirement in source_requirements + stanza = _lock_stanza(lock, requirement) + assert any( + line.lstrip().startswith("--hash=sha256:") for line in stanza[1:] + ), f"{requirement} has no artifact hash in its lock stanza" assert "lunar-python==" not in source assert "lunar-python==" not in lock From a63ac72bc13cc399b6ad26a24366f5755735d9b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 22 Jul 2026 12:04:26 +0900 Subject: [PATCH 25/29] test(review): normalize dependency lock parsing --- tests/test_opencode_python_dependency_lock.py | 77 +++++++++++++++---- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py index 3394d6506..33c55d028 100644 --- a/tests/test_opencode_python_dependency_lock.py +++ b/tests/test_opencode_python_dependency_lock.py @@ -13,6 +13,27 @@ } +def _top_level_requirement(line: str) -> str | None: + """Return a requirement token from one non-indented, non-comment line.""" + + if not line or line[0].isspace() or line.startswith("#"): + return None + header = line.split("\\", 1)[0].strip() + if not header or header.startswith("-"): + return None + return header.split(maxsplit=1)[0] + + +def _top_level_requirements(text: str) -> set[str]: + """Return parsed top-level requirements without comments or stanza details.""" + + return { + requirement + for line in text.splitlines() + if (requirement := _top_level_requirement(line)) is not None + } + + def _lock_stanza(lock: str, requirement: str) -> list[str]: """Return one top-level requirement stanza without borrowing later hashes.""" @@ -21,10 +42,7 @@ def _lock_stanza(lock: str, requirement: str) -> list[str]: ( index for index, line in enumerate(lines) - if line - and not line[0].isspace() - and not line.startswith("#") - and line.split(maxsplit=1)[0] == requirement + if _top_level_requirement(line) == requirement ), None, ) @@ -56,6 +74,15 @@ def test_lock_stanza_accepts_indented_via_comments() -> None: ] +def test_lock_stanza_accepts_attached_line_continuation() -> None: + lock = "demo==1.0\\\n --hash=sha256:abc123\n" + + assert _lock_stanza(lock, "demo==1.0") == [ + "demo==1.0\\", + " --hash=sha256:abc123", + ] + + def test_lock_stanza_does_not_borrow_a_later_requirement_hash() -> None: lock = ( "demo==1.0\n" @@ -69,6 +96,17 @@ def test_lock_stanza_does_not_borrow_a_later_requirement_hash() -> None: ) +def test_top_level_requirements_ignore_comments_and_stanza_details() -> None: + requirements = _top_level_requirements( + "# lunar-python==9.9.9 is intentionally not installed\n" + "demo==1.0 \\\n" + " --hash=sha256:abc123\n" + " # via lunar-python==9.9.9\n" + ) + + assert requirements == {"demo==1.0"} + + def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: source = (REPO_ROOT / "requirements-opencode-review-ci.txt").read_text( encoding="utf-8" @@ -76,11 +114,8 @@ def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: lock = (REPO_ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( encoding="utf-8" ) - source_requirements = { - line.split(maxsplit=1)[0] - for line in source.splitlines() - if line.strip() and not line.startswith("#") - } + source_requirements = _top_level_requirements(source) + lock_requirements = _top_level_requirements(lock) for package, version in TRUSTED_SAJU_WHEELS.items(): requirement = f"{package}=={version}" @@ -90,10 +125,20 @@ def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: line.lstrip().startswith("--hash=sha256:") for line in stanza[1:] ), f"{requirement} has no artifact hash in its lock stanza" - assert "lunar-python==" not in source - assert "lunar-python==" not in lock - assert ( - "uv pip compile --generate-hashes --python-version 3.12 " - "--python-platform x86_64-manylinux_2_28 requirements-opencode-review-ci.txt " - "-o requirements-opencode-review-ci-hashes.txt" - ) in lock + assert not any( + requirement.startswith("lunar-python==") + for requirement in source_requirements | lock_requirements + ) + + generation_header = "\n".join( + line for line in lock.splitlines() if line.startswith("#") + ) + for fragment in ( + "uv pip compile", + "--generate-hashes", + "--python-version 3.12", + "--python-platform x86_64-manylinux_2_28", + "requirements-opencode-review-ci.txt", + "requirements-opencode-review-ci-hashes.txt", + ): + assert fragment in generation_header From 358db7f664ed22eed7d18a8d0dad5f5be10507e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 26 Jul 2026 16:34:10 +0900 Subject: [PATCH 26/29] fix(review): reject contradictory probe citations --- .../ci/opencode_review_normalize_output.py | 16 +++++++++++ tests/test_opencode_model_pool_runner.py | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index cabfc73fd..72de9a7d3 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,6 +653,19 @@ def adversarial_probe_source_receipt_error( return "" +def evidence_cites_probe_path_at_any_line(evidence: str, path: str) -> bool: + """Return whether evidence already cites any positive line for the probe path.""" + escaped_path = rf"(? dict[str, Any] | A rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) if rejection == "must cite the exact probe path and positive line": if not adversarial_probe_source_receipt_error(evidence, path, line): + if evidence_cites_probe_path_at_any_line(evidence, path): + repaired_probes.append(probe) + continue repaired_evidence = f"{path}:{line} {repaired_evidence}" else: rebound_location = receipt_verified_evidence_location( diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 9854302a6..f7e1af8c7 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -159,6 +159,33 @@ def test_normalizer_binds_only_a_verified_structured_probe_location( ) +@pytest.mark.parametrize("citation_template", ("{path}:2", "{path}#L2")) +def test_normalizer_does_not_add_a_contradictory_redundant_citation( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + citation_template: str, +) -> None: + """A different existing citation for the probe path remains fail-closed.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + evidence = ( + f"Source trace at {citation_template.format(path=path)} rejected malformed input " + f"with exit code 1; {receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + assert normalizer.repair_adversarial_probe_evidence_bindings(value) is value + + def test_normalizer_rebinds_structured_location_to_unique_receipted_citation( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From 039f409971e030c639cfdc562c93eaee06b57267 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 26 Jul 2026 16:41:58 +0900 Subject: [PATCH 27/29] fix(review): fail closed on trust-boundary scans --- .github/workflows/opencode-review.yml | 13 ++++-- scripts/ci/test_strix_quick_gate.sh | 2 +- tests/test_opencode_agent_contract.py | 58 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b39367cc5..51b598503 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -403,7 +403,11 @@ jobs: echo "::error::Coverage source export retained forbidden Git metadata at ${source_tree}/.git." exit 1 fi - if find -P "$source_tree" -mindepth 1 -type l -print -quit | grep -q .; then + if ! coverage_symlink_path="$(find -P "$source_tree" -mindepth 1 -type l -print -quit)"; then + echo "::error::Coverage source export could not scan ${source_tree} for symbolic links." + exit 1 + fi + if [ -n "$coverage_symlink_path" ]; then echo "::error::Coverage source export contains a symbolic link; refusing cross-job artifact materialization." exit 1 fi @@ -2055,7 +2059,11 @@ jobs: ;; esac done - if find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit | grep -q .; then + if ! pr_symlink_path="$(find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit)"; then + echo "::error::Could not scan PR worktree for symbolic links; refusing trusted review processing." + exit 1 + fi + if [ -n "$pr_symlink_path" ]; then echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." exit 1 fi @@ -6359,7 +6367,6 @@ jobs: | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) | select((.status // "") == "completed") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index c2c4f8bcc..54a608734 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1038,7 +1038,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" assert_file_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode approval ignores cancelled scheduler queue replacement checks without source evidence" - assert_file_contains "$workflow_file" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "opencode approval also ignores cancelled scheduler queue replacement checks in the workflow-less REST fallback" + assert_file_not_contains "$workflow_file" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "opencode approval keeps ownership-ambiguous cancelled scan-pr-queue checks blocking in the workflow-less REST fallback" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" assert_file_contains "$workflow_file" 'current_head_manual_strix_success_status()' "opencode approval can identify same-head manual Strix success status evidence" assert_file_contains "$workflow_file" 'manual_run_line="$(latest_current_head_manual_strix_run || true)"' "opencode approval falls back to same-head manual Strix check-run success when commit status publication is unavailable" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 8ef821773..c19ed3beb 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -563,6 +563,45 @@ def test_coverage_source_artifact_excludes_git_history(tmp_path): assert b"deleted-history" not in archive.read_bytes() +def test_coverage_source_symlink_scan_fails_closed_on_find_error(tmp_path): + """Artifact export must stop when its symbolic-link scan cannot complete.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + scan_start = workflow.index( + ' for source_tree in "$COVERAGE_BASE_WORKDIR" ' + '"$COVERAGE_SOURCE_WORKDIR"; do\n' + ) + scan_end = workflow.index( + ' tar -cf "$COVERAGE_SOURCE_ARCHIVE"', scan_start + ) + scan_script = textwrap.dedent(workflow[scan_start:scan_end]) + + base_worktree = tmp_path / "opencode-coverage-base" + source_worktree = tmp_path / "opencode-coverage-source" + base_worktree.mkdir() + source_worktree.mkdir() + fake_bin = tmp_path / "fake-bin" + fake_bin.mkdir() + fake_find = fake_bin / "find" + fake_find.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8") + fake_find.chmod(0o755) + + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + scan_script], + env={ + **os.environ, + "COVERAGE_BASE_WORKDIR": str(base_worktree), + "COVERAGE_SOURCE_WORKDIR": str(source_worktree), + "PATH": f"{fake_bin}:{os.environ['PATH']}", + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 1 + assert "could not scan" in result.stdout + + def test_opencode_repository_dispatch_authorization_is_fail_closed(): """Reject an untrusted dispatcher or a target outside the exact allowlist.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") @@ -2008,6 +2047,25 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa ) assert clean.returncode == 0, clean.stderr + fake_bin = tmp_path / "fake-bin" + fake_bin.mkdir() + fake_find = fake_bin / "find" + fake_find.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8") + fake_find.chmod(0o755) + scan_error = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={ + **os.environ, + "OPENCODE_SOURCE_WORKDIR": str(clean_worktree), + "PATH": f"{fake_bin}:{os.environ['PATH']}", + }, + text=True, + capture_output=True, + check=False, + ) + assert scan_error.returncode == 1 + assert "Could not scan PR worktree for symbolic links" in scan_error.stdout + untracked_link = clean_worktree / "untracked-credential-link" untracked_link.symlink_to(outside) untracked_rejected = subprocess.run( From 4f8981a370801eab534fd32028b029b1922b4522 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 26 Jul 2026 16:55:13 +0900 Subject: [PATCH 28/29] fix(review): preserve primary model review budget --- .github/workflows/opencode-review.yml | 7 ++-- scripts/ci/run_opencode_review_model_pool.sh | 4 +-- scripts/ci/test_strix_quick_gate.sh | 30 +++++++++++++++- tests/test_opencode_agent_contract.py | 8 ++++- tests/test_opencode_model_pool_runner.py | 36 ++++++++++++++++---- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 51b598503..5ce3d147d 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3807,9 +3807,10 @@ jobs: # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" - # Bound GitHub Models DeepSeek endpoints that return no usable provider - # detail so they cannot consume an entire 90-minute cadence slot. - OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300" + # Bound the GitHub Models DeepSeek R1 endpoints that return no usable + # provider detail. The primary DeepSeek V3 candidate keeps the full + # review cadence because it has produced usable long-form reviews. + OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS: "300" OPENCODE_DYNAMIC_MAX_CYCLES: "0" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index d77941e7f..14cc7ff87 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -384,8 +384,8 @@ cap_model_run_timeout() { github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS 45)" ;; - github-models/deepseek/*) - cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS 300)" + github-models/deepseek/deepseek-r1 | github-models/deepseek/deepseek-r1-0528) + cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS 300)" ;; *) printf '%s\n' "$run_timeout_seconds" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 54a608734..9c588f3e1 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -91,9 +91,19 @@ assert_file_not_matches() { local file_path="$1" local pattern="$2" local message="$3" + local grep_status - if [ -f "$file_path" ] && grep -Eq -- "$pattern" "$file_path"; then + if [ ! -f "$file_path" ]; then + return + fi + if grep -Eq -- "$pattern" "$file_path"; then record_failure "$message (unexpected pattern '$pattern')" + else + grep_status=$? + if [ "$grep_status" -ne 1 ]; then + record_failure "$message (grep failed with exit $grep_status for pattern '$pattern')" + print_assertion_source "$file_path" + fi fi } @@ -5652,11 +5662,29 @@ run_gate_case_allow_provider_signal() { run_gate_case_with_provider_signal_mode "0" "$@" } +test_assert_file_not_matches_fails_closed() { + local fixture_file + + fixture_file="$(mktemp)" + printf 'safe fixture\n' >"$fixture_file" + if ! ( + FAILURES=0 + assert_file_not_matches "$fixture_file" "[" "invalid regex must fail closed" + [ "$FAILURES" -eq 1 ] + ) >/dev/null 2>&1; then + record_failure "assert_file_not_matches must record grep regex/read failures" + fi + rm -f "$fixture_file" +} + run_filtered_gate_case_if_requested() { case "${STRIX_TEST_CASE_FILTER:-}" in "") return 0 ;; + assert-file-not-matches-errors-fail-closed) + test_assert_file_not_matches_fails_closed + ;; success) run_gate_case "success" \ "vertex_ai/ready-primary" \ diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c19ed3beb..3add4598a 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1304,7 +1304,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "0"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow - assert 'OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS: "300"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300"' not in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow publish_step = workflow.split(" - name: Publish OpenCode review outcome", 1)[ @@ -1363,6 +1364,11 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "should_skip_model_candidate" in model_pool_runner assert "cap_model_run_timeout" in model_pool_runner assert "configured provider-specific cap" in model_pool_runner + assert ( + "github-models/deepseek/deepseek-r1 | " + "github-models/deepseek/deepseek-r1-0528)" + ) in model_pool_runner + assert "github-models/deepseek/*)" not in model_pool_runner assert "run_central_adversarial_harness" not in model_pool_runner assert "finish_pool_without_model" in model_pool_runner assert "central-current-head-adversarial-harness" not in model_pool_runner diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index f7e1af8c7..1f93c06ad 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -1074,25 +1074,27 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert run_timeout <= remaining_budget <= 30 -def test_github_deepseek_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: - """No-output DeepSeek endpoints cannot consume a full cadence slot.""" +def test_github_deepseek_r1_runtime_cap_preserves_queue_budget( + tmp_path: Path, +) -> None: + """No-output DeepSeek R1 endpoints cannot consume a full cadence slot.""" result = run_failed_model( tmp_path, - model_candidates="github-models/deepseek/deepseek-v3-0324", + model_candidates="github-models/deepseek/deepseek-r1-0528", extra_env={ - "OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS": "2", "OPENCODE_RUN_TIMEOUT_SECONDS": "9", }, ) assert result.returncode == 1 assert ( - "OpenCode github-models/deepseek/deepseek-v3-0324 runtime cap selected 2s " + "OpenCode github-models/deepseek/deepseek-r1-0528 runtime cap selected 2s " "instead of 9s because the configured provider-specific cap is lower than " "the cadence timeout." ) in result.stdout attempt_budget = re.search( - r"OpenCode github-models/deepseek/deepseek-v3-0324 attempt 1/1 using " + r"OpenCode github-models/deepseek/deepseek-r1-0528 attempt 1/1 using " r"(\d+)s run timeout with (\d+)s retry budget remaining\.", result.stdout, ) @@ -1102,6 +1104,28 @@ def test_github_deepseek_runtime_cap_preserves_queue_budget(tmp_path: Path) -> N assert run_timeout <= remaining_budget <= 30 +def test_github_deepseek_v3_preserves_full_review_cadence(tmp_path: Path) -> None: + """The primary DeepSeek V3 candidate is not subject to the R1 runtime cap.""" + result = run_failed_model( + tmp_path, + model_candidates="github-models/deepseek/deepseek-v3-0324", + extra_env={ + "OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + ) + + assert result.returncode == 1 + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 attempt 1/1 using 9s " + "run timeout" + ) in result.stdout + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 runtime cap selected" + not in result.stdout + ) + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: From 0e15a23686976c38c58f59f6a9206258b79beb54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 26 Jul 2026 17:05:05 +0900 Subject: [PATCH 29/29] fix(review): stop when no models are runnable --- scripts/ci/run_opencode_review_model_pool.sh | 3 +- scripts/ci/test_strix_quick_gate.sh | 9 ++++-- tests/test_opencode_model_pool_runner.py | 29 ++++++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 14cc7ff87..0a1842e85 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -573,6 +573,7 @@ main() { continue fi if should_skip_model_candidate "$model_candidate"; then + dead_candidate_reasons[$model_candidate]="not runnable with the credentials available to this job" continue fi assert_reasoning_effort_for_candidate "$model_candidate" @@ -671,7 +672,7 @@ main() { fi done if [ "$alive_candidates" -eq 0 ]; then - printf 'Every OpenCode model candidate is marked failed for this run; ending the pool without further provider spend.\n' + printf 'No runnable OpenCode model candidates remain for this run; ending the pool without further provider spend or idle cycles.\n' if finish_pool_without_model; then exit 0 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 9c588f3e1..a2c2e98a3 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -94,6 +94,8 @@ assert_file_not_matches() { local grep_status if [ ! -f "$file_path" ]; then + record_failure "$message (missing file '$file_path')" + print_assertion_source "$file_path" return fi if grep -Eq -- "$pattern" "$file_path"; then @@ -624,7 +626,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" --version' "opencode review logs the exact trusted CodeGraph version" assert_file_not_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review does not replay PR-derived CodeGraph status bytes as workflow commands" assert_file_not_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review does not replay PR-derived CodeGraph exploration bytes as workflow commands" - assert_file_not_matches "$workflow_file" '^[[:space:]]{10}cat "\$CODEGRAPH_EVIDENCE_FILE"[[:space:]]*$' "opencode review does not replay assembled PR-derived CodeGraph evidence into the command channel" + assert_file_not_matches "$workflow_file" '^[[:space:]]*cat "\$CODEGRAPH_EVIDENCE_FILE"[[:space:]]*$' "opencode review does not replay assembled PR-derived CodeGraph evidence into the command channel" assert_file_contains "$workflow_file" 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph status failure metadata" assert_file_contains "$workflow_file" 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph exploration failure metadata" assert_file_contains "$workflow_file" 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.' "opencode review reports bounded CodeGraph success metadata" @@ -5670,9 +5672,10 @@ test_assert_file_not_matches_fails_closed() { if ! ( FAILURES=0 assert_file_not_matches "$fixture_file" "[" "invalid regex must fail closed" - [ "$FAILURES" -eq 1 ] + assert_file_not_matches "${fixture_file}.missing" "safe" "missing file must fail closed" + [ "$FAILURES" -eq 2 ] ) >/dev/null 2>&1; then - record_failure "assert_file_not_matches must record grep regex/read failures" + record_failure "assert_file_not_matches must record missing-file and grep failures" fi rm -f "$fixture_file" } diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 1f93c06ad..94cc9222a 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -914,12 +914,37 @@ def test_credit_exhausted_402_ends_pool_without_further_spend(tmp_path: Path) -> assert result.returncode == 1 assert "provider credits are exhausted" in result.stdout assert "marking this candidate failed for the rest of the run" in result.stdout - assert "Every OpenCode model candidate is marked failed for this run" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout assert "class=credit-exhausted" in result.stdout assert "Restarting OpenCode model pool" not in result.stdout assert elapsed < 20 +def test_all_credentialless_candidates_end_without_idle_cycles(tmp_path: Path) -> None: + """A fully skipped catalog exits even when cycles and deadlines are unbounded.""" + start = time.monotonic() + result = run_failed_model( + tmp_path, + model_candidates=( + "openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2" + ), + extra_env={ + "OPENAI_API_KEY": "", + "OPENROUTER_API_KEY": "", + "OPENCODE_POOL_MAX_CYCLES": "0", + "OPENCODE_TOTAL_RETRY_BUDGET_SECONDS": "0", + }, + ) + elapsed = time.monotonic() - start + + assert result.returncode == 1 + assert "OPENAI_API_KEY is not configured" in result.stdout + assert "OPENROUTER_API_KEY is not configured" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout + assert "Restarting OpenCode model pool" not in result.stdout + assert elapsed < 10 + + def test_invalid_control_output_cap_marks_candidate_failed(tmp_path: Path) -> None: """Repeated control-rejected output stops retrying at the cap, not the budget.""" result = run_failed_model( @@ -949,7 +974,7 @@ def test_invalid_control_output_cap_marks_candidate_failed(tmp_path: Path) -> No assert result.returncode == 1 assert "produced 2 control-rejected outputs" in result.stdout assert "marking this candidate failed for the rest of the run" in result.stdout - assert "Every OpenCode model candidate is marked failed for this run" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout assert "attempt 3/3" not in result.stdout