Skip to content

fix(opencode): source-backed supply-chain fallback so the approve gate reaches a conclusion#356

Open
seonghobae wants to merge 3 commits into
mainfrom
fix/opencode-supply-chain-fallback
Open

fix(opencode): source-backed supply-chain fallback so the approve gate reaches a conclusion#356
seonghobae wants to merge 3 commits into
mainfrom
fix/opencode-supply-chain-fallback

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

The dead-end

When OpenCode's Review Overview control is APPROVE but the PR has failing peer supply-chain checks (osv-scan / osv-scan, trivy-fs, dependency-review), the approve gate collects the failed peer checks and must justify a source-backed REQUEST_CHANGES. It calls run_failed_check_diagnosis, then build_failed_check_fallback_body -> emit_line_specific_fallback_findings -> scripts/ci/emit_opencode_failed_check_fallback_findings.sh.

That helper had emitters for pytest, cancelled checks, Strix reports, and billing-lock — but none for supply-chain scanners. So for a supply-chain failure finding_index stayed 0, build_failed_check_fallback_body returned 1, stop_failed_check_fallback_unavailable fired, no review was posted, and the gate printed NO_CONCLUSION / FAILED_CHECK_DIAGNOSIS_UNAVAILABLE and exited 1. The "Run merge scheduler" step was skipped and the PR was stuck: opencode-review = failure with no actionable review, no conclusion, never merges.

Repro: clearfolio#23 (run 28863381355) — model control = APPROVE, failing peers osv-scan / osv-scan and trivy-fs.

Why a source-backed emitter (not weakening fail-closed)

The guard tests deliberately fail-closed rather than post URL-only reviews. Supply-chain findings are source-backed: osv/trivy/dependency-review name the exact vulnerable package, its manifest file, the CVE/GHSA id, and the fixed version. The correct fix is to map those to concrete findings (package + manifest:line + CVE + "bump A->B"), which makes the fallback succeed and posts an actionable REQUEST_CHANGES. No URL-only path was added and no existing assertion was relaxed.

The fix

scripts/ci/emit_opencode_failed_check_fallback_findings.sh

  • New emit_supply_chain_findings (+ extract_supply_chain_records), mirroring emit_pytest_failure_findings / emit_strix_report_findings. It parses osv-scanner / trivy-fs / dependency-review failed-check blocks for two evidence shapes:
    1. Canonical structured lines: - Supply-chain vulnerability: id=CVE-… severity=… package=… installed=… fixed=… manifest=… line=…
    2. Raw Trivy findings tables grouped by a manifest header (requirements.txt (pip)).
  • Emits one finding per distinct vulnerability with: manifest path resolved under ${GITHUB_WORKSPACE:-$PWD}, a positive line number (locate the package in the manifest, else honor the scanner's SARIF line hint; never line 0), severity, a CVE/GHSA + package title, problem/root_cause quoting the scanner evidence and the failed check label + exact phrase, fix_direction = concrete "bump <pkg> from <installed> to <fixed>", a GitHub-suggestion-ready ```suggestion diff for simple version pins, and a regression_test_direction.
  • Wired into the dispatch sequence next to the other emit_* calls.
  • URL-only supply-chain evidence still yields no finding — fail-closed preserved (stop_failed_check_fallback_unavailable still fires for genuine no-evidence).

scripts/ci/collect_failed_check_evidence.sh (evidence extension)

  • osv-scanner and trivy-fs upload SARIF to code scanning instead of printing findings in the failed job log, so the prior evidence lacked package/CVE/fixed-version. New emit_supply_chain_alert_evidence pulls the current-head code-scanning alerts for the failed check's tool and normalizes each into a canonical - Supply-chain vulnerability: line (package, manifest path+line, advisory id, severity, fixed version) so the fallback has real, source-backed data. Best-effort and guarded — never fails the collector.

scripts/ci/test_strix_quick_gate.sh (guard tests, existing assert_file_contains style)

  • Static assertions: helper defines emit_supply_chain_findings, scopes to osv/trivy/dependency-review, states a concrete "bump <pkg> from <installed> to <fixed>", offers a ```suggestion diff, and is wired into the dispatch; collector defines emit_supply_chain_alert_evidence, reads code-scanning/alerts, and emits canonical supply-chain lines.
  • Functional test assert_opencode_failed_check_fallback_maps_supply_chain_vulnerabilities: feeds synthetic osv (canonical) + trivy (table) evidence plus a matching manifest, and asserts source-backed findings (exact path:line, CVE/GHSA, from->to bump, suggestion pin).
  • Functional test assert_opencode_failed_check_fallback_rejects_url_only_supply_chain: asserts URL-only supply-chain evidence emits no finding and stays fail-closed (exit 1).

Test results

  • New functional + static guard tests: PASS (verified by direct/targeted execution and end-to-end: collector code-scanning-alert output -> emitter -> two source-backed requests findings; trivy table row -> requirements.txt:3 urllib3 finding with bump from 1.25.0 to 1.26.18 and a urllib3==1.26.18 suggestion).
  • URL-only supply-chain evidence -> no finding, exit 1 (fail-closed intact).
  • shellcheck -S warning clean on the emitter and collector; bash -n clean on all three files.
  • No existing assertion was relaxed or deleted.

Pre-existing, unrelated failures (not caused by this PR)

The repo's scripts/ci/test_strix_quick_gate.sh and tests/ pytest suite are not green on main in a clean checkout, due to workflow/test drift in files this PR does not touch (.github/workflows/opencode-review.yml, strix.yml, merge-scheduler, noema-review). Verified identical on a pristine origin/main worktree: 14 guard-suite failures and 3 pytest failures with vs. without this change — this PR adds zero new failures. Those drift failures should be fixed in a separate PR; they are orthogonal to the supply-chain fallback dead-end fixed here.

🤖 Generated with Claude Code

Seongho Bae and others added 2 commits July 8, 2026 08:28
…rove gate reaches a conclusion

When OpenCode's Review Overview control is APPROVE but a PR has failing
peer supply-chain checks (osv-scan, trivy-fs, dependency-review), the
approve gate collects the failed checks and must convert them into a
source-backed REQUEST_CHANGES. The fallback helper
(emit_opencode_failed_check_fallback_findings.sh) had emitters for
pytest, cancelled checks, Strix reports, and billing-lock, but NONE for
supply-chain scanners. So finding_index stayed 0,
build_failed_check_fallback_body returned 1,
stop_failed_check_fallback_unavailable fired, no review was posted, and
the gate printed NO_CONCLUSION / FAILED_CHECK_DIAGNOSIS_UNAVAILABLE and
exited 1 — the "Run merge scheduler" step was skipped and the PR was
stuck forever (opencode-review = failure with no actionable review).

Supply-chain findings ARE source-backed: osv/trivy/dependency-review
name the exact vulnerable package, its manifest file, the CVE/GHSA id,
and the fixed version. This maps those to concrete findings instead of
weakening the fail-closed contract.

Changes:
- emit_opencode_failed_check_fallback_findings.sh: add
  emit_supply_chain_findings (+ extract_supply_chain_records) that
  parses osv-scanner / trivy-fs / dependency-review failed-check blocks
  for two evidence shapes — canonical "- Supply-chain vulnerability:
  id=... severity=... package=... installed=... fixed=... manifest=...
  line=..." lines and raw Trivy findings tables — and emits one
  source-backed finding per distinct vulnerability with: manifest path
  resolved under repo_root, a positive line number (locate the package
  in the manifest, or honor the scanner's SARIF line hint; never line 0),
  severity, a CVE/GHSA + package title, problem/root_cause quoting the
  scanner evidence and failed check label, fix_direction "bump <pkg>
  from <installed> to <fixed>", a GitHub-suggestion-ready ```suggestion
  diff for simple version pins, and a regression_test_direction. Wired
  into the dispatch sequence next to the other emit_* calls. URL-only
  supply-chain evidence still yields no finding (fail-closed preserved).
- collect_failed_check_evidence.sh: extend evidence collection so the
  emitter has real data in production. For failed osv-scanner / trivy
  checks (which upload SARIF to code scanning rather than logging
  findings), pull the current-head code-scanning alerts and normalize
  each into a canonical "- Supply-chain vulnerability:" line (package,
  manifest path+line, advisory id, severity, fixed version). Best-effort
  and guarded; never fails the collector.
- test_strix_quick_gate.sh: add static assertions that the helper defines
  emit_supply_chain_findings, scopes to osv/trivy/dependency-review,
  states a concrete "bump <pkg> from <installed> to <fixed>", offers a
  ```suggestion diff, and is wired into the dispatch; add static
  assertions that the collector defines emit_supply_chain_alert_evidence,
  reads code-scanning/alerts, and emits canonical supply-chain lines.
  Add two functional tests: one feeds synthetic osv (canonical) + trivy
  (table) evidence plus a matching manifest and asserts source-backed
  findings (path, positive line, CVE/GHSA, from->to bump, suggestion);
  the other asserts URL-only supply-chain evidence stays fail-closed.

No existing test assertion was relaxed or deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
The supply-chain fallback joined per-vulnerability records with a TAB and
read them back with IFS=$'\t'. Tab is an IFS-whitespace character, so read
collapses consecutive tabs: an empty interior field (missing installed OR
missing fixed) shifted every later column left by one. Since the collector
appends installed=/fixed= only when present, no-installed-version SARIF
alerts and no-fix advisories are common real inputs, producing garbled
findings (severity word in the advisory-id slot, CVE id in the version slot).

Switch the internal record delimiter to the ASCII Unit Separator (\x1f) for
both the perl join and the read loop. \x1f is not IFS-whitespace, so empty
interior fields are preserved positionally. Guard the emitter so a missing
installed or fixed never yields a broken sentence or a CVE id in a version
slot: "upgrade <pkg> to <fixed>" when installed is absent, and "no fixed
version is available upstream for <pkg> <installed>; remove or replace the
dependency, or pin to a patched fork" when there is no fix. The canonical
human-readable evidence line format is unchanged.

Add assert_opencode_failed_check_fallback_preserves_empty_supply_chain_columns,
a regression test feeding one record with no installed version and one with
no fixed version; it asserts the advisory id lands in the title (not the
severity word), no CVE/GHSA id appears in a version slot, columns are not
shifted, and line numbers stay positive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
@seonghobae

Copy link
Copy Markdown
Contributor Author

Follow-up fix: empty supply-chain columns no longer collapse

Adversarial review found a correctness bug in the new supply-chain path of emit_opencode_failed_check_fallback_findings.sh. Per-vulnerability records were joined with a TAB and read back with IFS=$'\t'. Tab is an IFS-whitespace character, so read collapses consecutive tabs — any empty interior field (missing installed OR missing fixed) shifted every later column left by one. The collector appends installed=/fixed= only when present, so no-installed SARIF alerts and no-fix advisories are common real inputs. Result: garbled findings such as a title of "Supply-chain vulnerability CRITICAL in flask" (severity word landed in the advisory-id slot) and fix text "bump flask from 2.0.1 to CVE-2020-0001" (a CVE id in the version slot).

Fix (commit 082cf89):

  • Switched the internal record delimiter to the ASCII Unit Separator (\x1f) for both the perl join and the read loop. \x1f is not IFS-whitespace, so empty interior fields are preserved positionally.
  • Hardened the emitter so a missing installed or fixed never produces a broken sentence or a CVE/GHSA id in a version slot: upgrade <pkg> to <fixed> when installed is absent, and no fixed version is available upstream for <pkg> <installed>; remove or replace the dependency, or pin to a patched fork when there is no fix. Line numbers stay positive.
  • The canonical human-readable evidence line format is unchanged; only the internal record join/read delimiter changed.

Regression test: assert_opencode_failed_check_fallback_preserves_empty_supply_chain_columns feeds one record with no installed version and one with no fixed version, and asserts the advisory id appears in the title (not the severity word), no CVE/GHSA id lands in a version slot, columns are not shifted, and line numbers are positive.

Verification: bash -n and shellcheck -S warning clean on both edited scripts. The new test plus assert_opencode_failed_check_fallback_maps_supply_chain_vulnerabilities and assert_opencode_failed_check_fallback_rejects_url_only_supply_chain all PASS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant