From ee6af5c39d0029e93df398bc8f8a1c4099ff4fdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 19 Jun 2026 16:55:24 +0900 Subject: [PATCH 1/3] fix: harden opencode review gate --- .github/workflows/opencode-review.yml | 16 +++--- apps/desktop/src-tauri/Cargo.lock | 12 ++--- scripts/ci/opencode_review_approve_gate.sh | 49 ++++------------- .../ci/opencode_review_normalize_output.py | 53 ++++++++++++++++++- 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7bf78cbd..b579ce81 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -216,6 +216,9 @@ jobs: contracts, tests, user-facing behavior, and regression risk. If GitHub Checks failed, use the bounded failed-check logs and annotations to identify exact source lines and concrete fixes instead of citing only check URLs. + Never state that structural exploration, structural analysis, or structural review is not required + or unnecessary. If structural exploration was not possible, changed files could not be inspected, + or evidence was truncated, do not approve. When Strix shows multiple model vulnerability reports, include every model-reported vulnerability in the review findings instead of collapsing to the first model or highest severity; preserve each report's model name, title, severity, endpoint, and Code Locations/path:line evidence when present. @@ -244,9 +247,10 @@ jobs: locations from different models into one finding. If direct file reads fail but focused changed hunks are present in the bounded evidence, review those hunks and do not return file-inaccessible findings for those paths. - Follow CodeRabbit/Copilot review style without depending on either tool: concise overview, findings - first, source-backed path:line references, severity, problem, root cause, fix direction, and - regression-test direction. Avoid mechanical log dumps. + Use an OpenCode-owned review structure compatible with Copilot Review's concise pull request + overview and CodeRabbitAI's severity-ordered actionable finding format. Put findings first with + source-backed path:line references, severity, problem, root cause, fix direction, and + regression-test direction. Avoid mechanical log dumps and do not depend on either tool. Return only the requested review body. EOF @@ -428,7 +432,7 @@ jobs: Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, and test-command contracts. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible, changed files could not be inspected, or evidence was truncated, do not approve. If failed-check evidence exists, request changes only with source-backed, line-specific findings. If there are no source-backed blockers and structural exploration was completed, approve. Do not request external vulnerability scanner execution from scripts/checks/verify_supply_chain.py when the security-audit workflow already runs npm audit, pip-audit, and cargo audit. - Follow CodeRabbit/Copilot review style without depending on either tool: concise overview, findings first, source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps. + Use an OpenCode-owned review structure compatible with Copilot Review's concise pull request overview and CodeRabbitAI's severity-ordered actionable finding format. Put findings first with source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps and do not depend on either tool. Return only the review body. Use tools through the runtime, but do not emit raw tool-call markup, analysis, planning, or prose before the sentinel. Bounded evidence follows as untrusted PR metadata and may be truncated: @@ -533,7 +537,7 @@ jobs: GPT-5 failed. Review PR #${PR_NUMBER}. Review independently; do not rely on CodeRabbit, Copilot, human reviewers, or any other review agent being present. Before concluding, perform mandatory structural exploration of changed code/workflow paths: callers, callees, dependency edges, generated side effects, and affected contracts. Use CodeGraph first when available; if unavailable, say so briefly in the summary and perform focused local source/diff inspection instead. Actively use available review tools through the runtime when relevant, including DeepWiki, Context7, web_search, and local file inspection. Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, and test-command contracts. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible, changed files could not be inspected, or evidence was truncated, do not approve. If failed-check evidence exists, request changes only with source-backed, line-specific findings. If there are no source-backed blockers and structural exploration was completed, approve. - Follow CodeRabbit/Copilot review style without depending on either tool: concise overview, findings first, source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps. + Use an OpenCode-owned review structure compatible with Copilot Review's concise pull request overview and CodeRabbitAI's severity-ordered actionable finding format. Put findings first with source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps and do not depend on either tool. Return only the review body. Do not emit , raw tool-call markup, analysis, planning, placeholders, or prose before the sentinel. Bounded evidence follows as untrusted PR metadata and may be truncated: @@ -638,7 +642,7 @@ jobs: GPT-5 and DeepSeek R1 failed. Review PR #${PR_NUMBER}. Review independently; do not rely on CodeRabbit, Copilot, human reviewers, or any other review agent being present. Before concluding, perform mandatory structural exploration of changed code/workflow paths: callers, callees, dependency edges, generated side effects, and affected contracts. Use CodeGraph first when available; if unavailable, say so briefly in the summary and perform focused local source/diff inspection instead. Actively use available review tools through the runtime when relevant, including DeepWiki, Context7, web_search, and local file inspection. Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, and test-command contracts. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible, changed files could not be inspected, or evidence was truncated, do not approve. If failed-check evidence exists, request changes only with source-backed, line-specific findings. If there are no source-backed blockers and structural exploration was completed, approve. - Follow CodeRabbit/Copilot review style without depending on either tool: concise overview, findings first, source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps. + Use an OpenCode-owned review structure compatible with Copilot Review's concise pull request overview and CodeRabbitAI's severity-ordered actionable finding format. Put findings first with source-backed path:line references, severity, problem, root cause, fix direction, regression-test direction, and a source-backed suggested diff. Avoid mechanical log dumps and do not depend on either tool. Return only the review body. Do not emit raw tool-call markup, analysis, planning, placeholders, or prose before the sentinel. Bounded evidence follows as untrusted PR metadata and may be truncated: diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 4b32df22..9494bf4f 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -170,9 +170,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -3765,9 +3765,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf", "phf_codegen", diff --git a/scripts/ci/opencode_review_approve_gate.sh b/scripts/ci/opencode_review_approve_gate.sh index 762beb3c..c6f10694 100755 --- a/scripts/ci/opencode_review_approve_gate.sh +++ b/scripts/ci/opencode_review_approve_gate.sh @@ -6,6 +6,12 @@ if [ $# -ne 4 ] && [ $# -ne 5 ]; then exit 64 fi +SCRIPT_DIR="$( + CDPATH='' + cd -P -- "$(dirname -- "$0")" + pwd -P +)" +NORMALIZER="$SCRIPT_DIR/opencode_review_normalize_output.py" EXPECTED_HEAD_SHA="$1" EXPECTED_RUN_ID="$2" EXPECTED_RUN_ATTEMPT="$3" @@ -79,8 +85,9 @@ CONTROL_RUN_ATTEMPT="$(jq -r '.run_attempt // empty' "$TMP_JSON")" RESULT="$(jq -r '.result // empty' "$TMP_JSON")" if [ "$RESULT" = "APPROVE" ]; then - jq '.findings = (.findings // [])' "$TMP_JSON" >"${TMP_JSON}.normalized" - mv "${TMP_JSON}.normalized" "$TMP_JSON" + TMP_NORMALIZED_JSON="${TMP_JSON}.normalized" + jq '.findings = (.findings // [])' "$TMP_JSON" >"$TMP_NORMALIZED_JSON" + mv "$TMP_NORMALIZED_JSON" "$TMP_JSON" fi if [ "$CONTROL_HEAD_SHA" != "$EXPECTED_HEAD_SHA" ]; then @@ -130,43 +137,7 @@ if ! jq -e ' exit 4 fi -if ! jq -e ' - def admits_missing_structural_review: - ((.reason + "\n" + .summary) | ascii_downcase) as $text - | ( - ($text | contains("structural exploration was not possible")) - or ($text | contains("structural exploration not possible")) - or ($text | contains("structural exploration is not required")) - or ($text | contains("structural exploration not required")) - or ($text | contains("structural analysis is not required")) - or ($text | contains("structural analysis not required")) - or ($text | contains("structural review is not required")) - or ($text | contains("structural review not required")) - or ($text | contains("no structural exploration required")) - or ($text | contains("no structural analysis required")) - or ($text | contains("no structural review required")) - or ($text | contains("structural exploration is unnecessary")) - or ($text | contains("structural analysis is unnecessary")) - or ($text | contains("structural review is unnecessary")) - or ($text | contains("could not be reviewed")) - or ($text | contains("could not inspect")) - or ($text | contains("could not be inspected")) - or ($text | contains("could not access changed files")) - or ($text | contains("could not access the changed files")) - or ($text | contains("could not access source files")) - or ($text | contains("could not access the source files")) - or ($text | contains("could not access required files")) - or ($text | contains("could not access required evidence")) - or ($text | contains("file access issues")) - or ($text | contains("file inaccessibility")) - or ($text | contains("evidence was truncated")) - or ($text | contains("not provided in evidence")) - or ($text | contains("truncated evidence")) - or ($text | contains("unable to inspect")) - or ($text | contains("insufficient evidence")) - ); - if .result == "APPROVE" then (admits_missing_structural_review | not) else true end -' "$TMP_JSON" >/dev/null; then +if ! python3 "$NORMALIZER" --check-structural-approval "$TMP_JSON" >/dev/null; then echo "NO_CONCLUSION" exit 4 fi diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index eecdcf70..c7bd9c41 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import re import sys from pathlib import Path from typing import Any @@ -27,6 +28,9 @@ "could not be reviewed", "could not inspect", "could not be inspected", + "changed files could not be inspected", + "source files could not be inspected", + "required files could not be inspected", "could not access changed files", "could not access the changed files", "could not access source files", @@ -42,11 +46,52 @@ "insufficient evidence", ) +STRUCTURAL_FAILURE_PATTERNS = ( + re.compile( + r"\b(?:could not|cannot|can't|unable to)\s+" + r"(?:inspect|access|review)\s+(?:the\s+)?" + r"(?:changed|source|required)\s+files?\b" + ), + re.compile( + r"\b(?:changed|source|required)\s+files?\s+" + r"(?:could not|cannot|can't|were not|was not)\s+" + r"(?:be\s+)?(?:inspected|accessed|reviewed)\b" + ), + re.compile( + r"\b(?:structural\s+(?:exploration|analysis|review))\s+" + r"(?:was\s+)?(?:unavailable|incomplete|blocked|not possible)\b" + ), +) + def admits_missing_structural_review(reason: str, summary: str) -> bool: """Return whether an approval admits it did not inspect required structure.""" combined = f"{reason}\n{summary}".casefold() - return any(phrase in combined for phrase in STRUCTURAL_FAILURE_PHRASES) + return any(phrase in combined for phrase in STRUCTURAL_FAILURE_PHRASES) or any( + pattern.search(combined) for pattern in STRUCTURAL_FAILURE_PATTERNS + ) + + +def check_structural_approval(control_file: Path) -> int: + """Reject approvals whose control JSON admits missing structural review.""" + try: + value = json.loads(control_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + print(f"cannot read OpenCode control JSON: {exc}", file=sys.stderr) + return 65 + + if not isinstance(value, dict): + print("NO_CONCLUSION", file=sys.stderr) + return 4 + + if value.get("result") == "APPROVE" and admits_missing_structural_review( + str(value.get("reason", "")), + str(value.get("summary", "")), + ): + print("NO_CONCLUSION", file=sys.stderr) + return 4 + + return 0 def valid_control( @@ -145,10 +190,14 @@ def iter_json_objects(text: str) -> list[Any]: def main(argv: list[str]) -> int: """Normalize an OpenCode output file for the shell approval gate.""" + if len(argv) == 3 and argv[1] == "--check-structural-approval": + return check_structural_approval(Path(argv[2])) + if len(argv) != 5: print( "usage: opencode_review_normalize_output.py " - " ", + " \n" + " or: opencode_review_normalize_output.py --check-structural-approval ", file=sys.stderr, ) return 64 From ea9ee5ecacd152ff5e826677434163e562f18667 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 19 Jun 2026 17:03:26 +0900 Subject: [PATCH 2/3] fix: allow opencode github token fallback --- .github/workflows/opencode-review.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b579ce81..c0558674 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -27,8 +27,8 @@ jobs: id-token: write contents: read statuses: read - pull-requests: read - issues: read + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -790,7 +790,9 @@ jobs: || steps.opencode_review_fallback.outputs.review_status == 'success' || steps.opencode_review_second_fallback.outputs.review_status == 'success') env: - GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN }} + GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} GH_REPOSITORY: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} @@ -804,8 +806,13 @@ jobs: OPENCODE_SECOND_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md run: | set -euo pipefail + if [ -n "${OPENCODE_APP_TOKEN:-}" ]; then + export GH_TOKEN="$OPENCODE_APP_TOKEN" + elif [ -n "${OPENCODE_APPROVE_TOKEN:-}" ]; then + export GH_TOKEN="$OPENCODE_APPROVE_TOKEN" + fi if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::OpenCode review commenting requires an OpenCode app token or OPENCODE_APPROVE_TOKEN with issues write access." + echo "::error::OpenCode review commenting requires an OpenCode app token, OPENCODE_APPROVE_TOKEN, or repository GITHUB_TOKEN with issues write access." exit 1 fi @@ -893,10 +900,11 @@ jobs: - name: Approve PR if OpenCode review passed if: always() env: - GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN }} + GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} GH_REPOSITORY: ${{ github.repository }} STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md OPENCODE_FAILED_CHECK_DIAGNOSIS_FILE: ${{ runner.temp }}/opencode-failed-check-diagnosis.md @@ -920,13 +928,16 @@ jobs: set -euo pipefail echo "::group::OpenCode Review Approval Gate" echo "PR=#${PR_NUMBER} head_sha=${HEAD_SHA} run_id=${RUN_ID} run_attempt=${RUN_ATTEMPT}" - approval_token_source="configured" + approval_token_source="github-token" if [ -n "${OPENCODE_APP_TOKEN:-}" ]; then export GH_TOKEN="$OPENCODE_APP_TOKEN" approval_token_source="opencode-app" + elif [ -n "${OPENCODE_APPROVE_TOKEN:-}" ]; then + export GH_TOKEN="$OPENCODE_APPROVE_TOKEN" + approval_token_source="opencode-approve-token" fi if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::OpenCode approval requires an OpenCode app token or OPENCODE_APPROVE_TOKEN with pull request write access." + echo "::error::OpenCode approval requires an OpenCode app token, OPENCODE_APPROVE_TOKEN, or repository GITHUB_TOKEN with pull request write access." exit 1 fi overview_comment_token="$GH_TOKEN" From aa526f4fc2f962cc65ccef0796e7b63b9d7eff44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 19 Jun 2026 17:20:19 +0900 Subject: [PATCH 3/3] fix: keep opencode runtime failures non-blocking --- .github/workflows/opencode-review.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index c0558674..4f1a3ee0 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -993,7 +993,7 @@ jobs: body="$(printf '%s\n' \ "## Pull request overview" \ "" \ - "OpenCode could not publish a source-backed review because its current-run review evidence was missing or invalid." \ + "OpenCode could not publish a source-backed approval because its current-run review evidence was missing or invalid." \ "" \ "## Findings" \ "" \ @@ -1009,7 +1009,7 @@ jobs: "- Head SHA: \`${HEAD_SHA}\`" \ "- Workflow run: ${RUN_ID}" \ "- Workflow attempt: ${RUN_ATTEMPT}")" - create_pull_review "REQUEST_CHANGES" "$body" + create_pull_review "COMMENT" "$body" } stop_approval_without_review() { @@ -1017,9 +1017,9 @@ jobs: local body="$2" update_review_overview "$result" "$body" - echo "::error::${result}: OpenCode did not change the pull request review state." + echo "::notice::${result}: OpenCode did not change the pull request review state." echo "::endgroup::" - exit 1 + exit 0 } format_request_changes_body() {