Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,146 changes: 7,146 additions & 0 deletions .github/workflows/opencode-review-dispatch.yml

Large diffs are not rendered by default.

7,544 changes: 25 additions & 7,519 deletions .github/workflows/opencode-review.yml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions PR_GOVERNANCE_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,9 @@ PR #381: wait: OpenCode review is already in progress
- PR #37 head `9bbf641` exposed a remaining race: OpenCode can finish before same-head manual Strix publishes the superseding `strix` status, causing stale cancelled PR-target Strix checks to become REQUEST_CHANGES. The evidence preparation step now waits, within a 40 minute bound, whenever peer checks are still running, even if completed failed check evidence is already visible.
- The same race also showed that PR `statusCheckRollup` does not see a manual Strix `workflow_dispatch` run until it publishes a commit status. OpenCode evidence preparation now queries current-head `strix.yml` workflow runs directly and treats in-progress same-head Strix runs as peer checks.
- Strix run `28014156427` also reported sensitive log disclosure risk in failed-check evidence handling. The collector now redacts common token, API key, password, secret, authorization, Slack token, and AWS access-key patterns before any failed logs are summarized or embedded in review evidence.
- Strix run `28015621232` reported `GitHub Actions pull_request_target with PR Code Execution` against an earlier `.github/workflows/opencode-review.yml` shape. The current required-workflow posture allows `pull_request_target` only with trusted central-source scripts and PR-head content treated as review data; PR-head code execution remains bounded by same-repository coverage gating and same-head workflow evidence. This follows GitHub's secure-use guidance to avoid `pull_request_target` with untrusted PR checkout/execution: https://docs.github.com/en/actions/reference/security/secure-use and GitHub Security Lab's "Preventing pwn requests": https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/.
- Strix run `28015621232` first reported `GitHub Actions pull_request_target with PR Code Execution`; the later default-branch scheduled CodeQL analysis made the remaining mixed-event trust boundary explicit as Critical alerts `182`–`185`. The organization-required `.github/workflows/opencode-review.yml` is now a metadata-only `pull_request_target` bootstrap with no checkout or secret binding. PR-head materialization, coverage evidence, model execution, and review publication live in `.github/workflows/opencode-review-dispatch.yml`, whose only trigger is default-branch `repository_dispatch`. This follows GitHub's secure-use guidance to avoid `pull_request_target` with untrusted PR checkout/execution: https://docs.github.com/en/actions/reference/security/secure-use and GitHub Security Lab's "Preventing pwn requests": https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/.
- OpenCode run `28017920517` failed without posting a PR review because every model attempt failed to produce a valid control block; the primary `github-models/openai/gpt-5` error was `Request body too large for gpt-5 model. Max size: 4000 tokens.` The prompt now requires reading `bounded-review-evidence.md` instead of inlining `bounded-review-evidence-excerpt.md`.
- PR #37 head `ce5591e` reproduced the self-modifying workflow hazard: the base-branch `pull_request_target` OpenCode run `28019367683` posted `REQUEST_CHANGES` from skipped coverage evidence, while the same-head manual `coverage-evidence` job in run `28019384032` proved 100% test and docstring coverage. The current central policy keeps required-workflow `pull_request_target`, but narrows trust: it runs trusted `.github` scripts, fetches PR-head source as data, gates coverage for fork heads, and lets later same-head evidence supersede stale base-branch review output.
- PR #37 head `ce5591e` reproduced the self-modifying workflow hazard: the base-branch `pull_request_target` OpenCode run `28019367683` posted `REQUEST_CHANGES` from skipped coverage evidence, while the same-head manual `coverage-evidence` job in run `28019384032` proved 100% test and docstring coverage. The final central split removes review execution from that event entirely: the required path only materializes the ruleset run, and an authenticated current-head `repository_dispatch` performs coverage, review, and publication from protected default-branch workflow code.
- OpenCode run `28019384032` also showed a model-output repair gap: DeepSeek V3 returned an `APPROVE` control block but wrote `Coverage: Not applicable` and `Docstring coverage: Not applicable` even though bounded current-head evidence proved both at 100%. The normalizer now reads the last concrete verification label after evidence-based repair, so an appended repair summary can replace earlier invalid model labels without accepting missing coverage.
- Strix run `28022323798` caught that the first label repair changed normalizer parsing too narrowly: inline approval summaries in `test_strix_quick_gate.sh` no longer normalized. Label parsing now accepts inline verification labels while excluding the `Coverage:` suffix inside `Docstring coverage:`, preserving both inline transcript controls and appended evidence repair.
- PR #37 same-head manual Strix run `28023392848` succeeded for head `07a6b76`, but the concurrently dispatched same-head manual OpenCode run `28023401894` spent its early lifetime waiting in `Prepare bounded OpenCode review evidence`. That exposed a scheduler-level resource issue: dispatching Strix and OpenCode together can turn OpenCode into a long poller whenever Strix is queued or slow. The scheduler now serializes the process: first dispatch Strix, then wait for a later scheduler pass to dispatch OpenCode after Strix evidence is complete.
Expand Down
10 changes: 5 additions & 5 deletions docs/org-required-workflow-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ This keeps Strix security evidence, OpenCode and independent Noema review eviden

The central `.github/workflows/opencode-review.yml` is now part of the active organization required workflow ruleset.

- Required workflow trigger support: `pull_request` (supported by GitHub ruleset workflows)
- Stable required check job name: `opencode-review`
- Required workflow trigger support: metadata-only `pull_request_target`; the file contains no checkout, PR-head execution, or secret expression
- Stable branch-protection job names: `required-workflow-bootstrap`, `coverage-source-tree`, `coverage-evidence`, and `opencode-review`; these jobs are data-only sentinels, while approval remains a separate current-head PR-review requirement
- Trusted source: `ContextualWisdomLab/.github`
- PR-head handling: the ruleset-supported `pull_request` event executes PR coverage in the unprivileged PR context; trusted scripts still come from the central `.github` workflow source
- Manual target support: OpenCode and Strix `workflow_dispatch` runs can still pass `target_repository` for targeted diagnostics, but required-workflow coverage comes from the organization ruleset rather than repo-local workflow copies
- PR-head handling: authenticated current-head `repository_dispatch` runs `.github/workflows/opencode-review-dispatch.yml` from the protected default branch; that workflow owns metadata validation, bounded coverage, source-as-data inspection, model review, and publication
- Manual target support: the central scheduler sends exact repository, PR, base, and head metadata through `repository_dispatch`; the dispatch workflow rejects an unauthorized actor, an unallowlisted repository, a fork head, or any live metadata mismatch
- Model token posture: use the organization `STRIX_GITHUB_MODELS_TOKEN` secret for GitHub Models calls, with `github.token` as the fallback; live workflow evidence showed `github.token` alone can return 403 from `models.github.ai/inference`
- Write posture: OpenCode may create review/comment side effects through the OpenCode app token when available; the workflow token is limited to the same-repository PR context and publication failures remain visible
- Coverage execution posture: PR-controlled package, test, build, R, Rust, and Docker inputs are never executed from `pull_request_target`; same-repository coverage runs in the ruleset-supported `pull_request` context, while cross-repository workflow dispatch remains metadata-bound and explicitly authenticated
- Coverage execution posture: PR-controlled package, test, build, R, Rust, and Docker inputs are never executed from `pull_request_target`; the dispatch workflow runs bounded low-privilege coverage only after exact live metadata and scheduler identity validation
- Fork posture: PR heads are fetched through `refs/pull/<number>/head` when direct head-SHA fetch is not available, so review can inspect fork PR source as data without executing it in the trusted workflow context
- Runtime posture: pre-model failed-check evidence waits are capped at about five minutes; the later approval gate rechecks current-head peer checks and extends its bounded wait only while image-validation checks remain pending, logging the reason before approval
- Model-exhaustion posture: command exit codes and deterministic checks cannot synthesize an approval. Exhaustion remains `MODEL_OUTPUT_UNAVAILABLE`; only a prior real-model approval bound to the exact current head can satisfy the review gate after all checks, alerts, and threads are revalidated.
Expand Down
155 changes: 155 additions & 0 deletions scripts/ci/opencode_review_comment_helpers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash

# Shared rendering helpers for the trusted central OpenCode review publisher.
# This file is sourced by workflow run blocks after the trusted .github
# repository has been checked out.

emit_change_flow_mermaid_graph() {
local merge_state="${1:-UNKNOWN}"
local changed_files_file surfaces_file idx next_node

changed_files_file="$(mktemp)"
surfaces_file="$(mktemp)"
if ! timeout "${REVIEW_PUBLISH_GH_API_TIMEOUT_SECONDS:-120}s" \
gh pr diff "$PR_NUMBER" --repo "$GH_REPOSITORY" --name-only >"$changed_files_file" 2>/dev/null ||
[ ! -s "$changed_files_file" ]; then
printf '```mermaid\n'
printf 'flowchart LR\n'
printf ' Evidence["OpenCode evidence"] --> Review["Current PR review path"]\n'
printf ' Review --> Verify["Required checks"]\n'
printf '```\n'
rm -f "$changed_files_file" "$surfaces_file"
return 0
fi

awk '
function basename(path) {
sub(/^.*\//, "", path)
return path
}
function clean(value) {
gsub(/"/, "", value)
gsub(/[\r\n\t]/, " ", value)
return value
}
function add(key, surface, impact, verify, path) {
if (!(key in count)) {
keys[++n] = key
label[key] = surface ": " basename(path)
impacts[key] = impact
verifies[key] = verify
}
count[key]++
}
/^\.github\/workflows\// {
add("workflow", "Workflow", "GitHub Actions review job", "actionlint plus required checks", $0)
next
}
/^scripts\/ci\// {
add("ci", "CI script", "review and security gate shell path", "bash -n plus Strix self-test", $0)
next
}
/^backend\// {
add("backend", "Backend", "API and service runtime", "backend tests", $0)
next
}
/^frontend\// {
add("frontend", "Frontend", "browser runtime and bundle", "frontend tests", $0)
next
}
/^tests?\// || /(^|\/)test_/ {
add("tests", "Test", "regression suite", "targeted test run", $0)
next
}
/^docs\// {
add("docs", "Docs", "operator or user guidance", "docs review", $0)
next
}
{
add("other", "Changed file", "repository behavior", "required checks", $0)
}
END {
for (i = 1; i <= n; i++) {
key = keys[i]
if (count[key] > 1) {
sub(/: .*/, " (" count[key] " files)", label[key])
}
print clean(label[key]) "\t" clean(impacts[key]) "\t" clean(verifies[key])
}
}
' "$changed_files_file" >"$surfaces_file"

printf '```mermaid\n'
printf 'flowchart LR\n'
printf ' PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]\n'
idx=1
while IFS="$(printf '\t')" read -r surface impact verify; do
[ -n "$surface" ] || continue
printf ' Evidence --> S%s["%s"]\n' "$idx" "$surface"
printf ' S%s --> I%s["%s"]\n' "$idx" "$idx" "$impact"
if [ "$merge_state" = "DIRTY" ] || [ "$merge_state" = "CONFLICTING" ]; then
printf ' I%s --> Conflict["Merge conflict blocks this path"]\n' "$idx"
next_node="Conflict"
else
printf ' I%s --> R%s["Review risk: %s"]\n' "$idx" "$idx" "$surface"
next_node="R${idx}"
fi
printf ' %s --> V%s["%s"]\n' "$next_node" "$idx" "$verify"
idx=$((idx + 1))
done <"$surfaces_file"
printf '```\n'
rm -f "$changed_files_file" "$surfaces_file"
}

append_mermaid_review_graph() {
local pr_json merge_state
pr_json="$(timeout "${REVIEW_PUBLISH_GH_API_TIMEOUT_SECONDS:-120}s" \
gh pr view "$PR_NUMBER" --repo "$GH_REPOSITORY" --json mergeStateStatus 2>/dev/null || true)"
merge_state="$(printf '%s' "$pr_json" | jq -r '.mergeStateStatus // "UNKNOWN"' 2>/dev/null || printf 'UNKNOWN')"
printf '\n## Changed-File Evidence Map\n\n'
emit_change_flow_mermaid_graph "$merge_state"
}

ensure_review_body_has_change_graph() {
local body="$1"
printf '%s\n' "$body"
if grep -Fq "## Changed-File Evidence Map" <<<"$body"; then
return 0
fi
append_mermaid_review_graph
}

append_merge_conflict_guidance() {
local pr_json merge_state base_ref head_ref base_fetch_ref base_origin_ref head_push_ref
pr_json="$(timeout "${REVIEW_PUBLISH_GH_API_TIMEOUT_SECONDS:-120}s" \
gh pr view "$PR_NUMBER" --repo "$GH_REPOSITORY" --json baseRefName,headRefName,mergeStateStatus 2>/dev/null || true)"
if [ -z "$pr_json" ]; then
return 0
fi
merge_state="$(printf '%s' "$pr_json" | jq -r '.mergeStateStatus // ""')"
if [ "$merge_state" != "DIRTY" ] && [ "$merge_state" != "CONFLICTING" ]; then
return 0
fi
base_ref="$(printf '%s' "$pr_json" | jq -r '.baseRefName // "base"')"
head_ref="$(printf '%s' "$pr_json" | jq -r '.headRefName // "head"')"
printf -v base_fetch_ref '%q' "$base_ref"
printf -v base_origin_ref '%q' "origin/${base_ref}"
printf -v head_push_ref '%q' "HEAD:${head_ref}"
printf '\n## Merge Conflict Guidance\n\n'
printf '%s\n' "- Current merge state: \`${merge_state}\`"
printf '%s\n' "- Base branch: \`${base_ref}\`"
printf '%s\n' "- Head branch: \`${head_ref}\`"
printf '%s\n' "- Fix direction: merge or rebase \`origin/${base_ref}\` into \`${head_ref}\`, resolve conflict markers in the changed files, rerun the focused checks, then push the same branch."
printf '%s\n' "- Repair commands:"
printf '%s\n' '```bash'
printf 'gh pr checkout %s --repo %s\n' "$PR_NUMBER" "$GH_REPOSITORY"
printf 'git fetch origin %s\n' "$base_fetch_ref"
printf 'git merge --no-ff %s # or: git rebase %s\n' "$base_origin_ref" "$base_origin_ref"
printf 'git status --short\n'
printf '# resolve files, then git add <resolved-files>\n'
printf '# merge path: git commit\n'
printf '# rebase path: git rebase --continue\n'
printf 'git push origin %s\n' "$head_push_ref"
printf '# rebase path only: git push --force-with-lease origin %s\n' "$head_push_ref"
printf '%s\n' '```'
}
25 changes: 18 additions & 7 deletions scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ARTIFACT_REPORTS_DIR="$REPO_ROOT/strix_runs"
STRIX_RUNTIME_DIR="$(mktemp -d /tmp/strix-runtime.XXXXXX)"
STRIX_LOG="$STRIX_RUNTIME_DIR/strix.log"
ACTIVE_REPORTS_DIR="$STRIX_RUNTIME_DIR/reports"
ATTEMPT_LOGS_DIR="$STRIX_RUNTIME_DIR/gate-attempts"
STRIX_REPORTS_DIR="$ACTIVE_REPORTS_DIR"
STRIX_PROCESS_TIMEOUT_SECONDS="${STRIX_PROCESS_TIMEOUT_SECONDS:-1200}"
STRIX_TOTAL_TIMEOUT_SECONDS="${STRIX_TOTAL_TIMEOUT_SECONDS:-0}"
Expand Down Expand Up @@ -122,6 +123,9 @@ publish_artifact_reports() {
if [ -d "$ACTIVE_REPORTS_DIR" ]; then
cp -R -- "$ACTIVE_REPORTS_DIR"/. "$ARTIFACT_REPORTS_DIR"/
fi
if [ -d "$ATTEMPT_LOGS_DIR" ] && [ ! -L "$ATTEMPT_LOGS_DIR" ]; then
cp -R -- "$ATTEMPT_LOGS_DIR" "$ARTIFACT_REPORTS_DIR/gate-attempts"
fi
if [ -f "$STRIX_LOG" ] && [ ! -L "$STRIX_LOG" ]; then
cp -- "$STRIX_LOG" "$ARTIFACT_REPORTS_DIR/gate-last-attempt.log"
fi
Expand All @@ -140,7 +144,7 @@ preserve_attempt_log() {
local safe_model attempt_dir attempt_log
ATTEMPT_LOG_SEQUENCE=$((ATTEMPT_LOG_SEQUENCE + 1))
safe_model="$(printf '%s' "$model" | tr -c 'A-Za-z0-9._-' '_')"
attempt_dir="$ACTIVE_REPORTS_DIR/gate-attempts"
attempt_dir="$ATTEMPT_LOGS_DIR"
mkdir -p -- "$attempt_dir"
attempt_log="$(printf '%s/%03d-%s-rc%s.log' "$attempt_dir" "$ATTEMPT_LOG_SEQUENCE" "$safe_model" "$rc")"
if [ -f "$STRIX_LOG" ] && [ ! -L "$STRIX_LOG" ]; then
Expand Down Expand Up @@ -2146,6 +2150,7 @@ fail_unmapped_threshold_report() {
fi
PR_FINDINGS_DECISION="block_unmapped"
echo "Unable to map Strix findings to changed files; failing closed for pull request." >&2
echo "Strix quick scan failed with a non-recoverable error." >&2
return 0
}

Expand Down Expand Up @@ -2581,8 +2586,10 @@ PY

if [ "$rc" -eq 0 ]; then
if has_blocking_vulnerability_reports; then
echo "Strix exited successfully but emitted a vulnerability at or above '$STRIX_FAIL_ON_MIN_SEVERITY'; failing closed." >&2
return 1
if ! evaluate_pull_request_findings || [ "$PR_FINDINGS_DECISION" != "allow_baseline" ]; then
echo "Strix exited successfully but emitted a vulnerability at or above '$STRIX_FAIL_ON_MIN_SEVERITY'; failing closed." >&2
return 1
fi
fi
printf "Strix run succeeded for model '%s' in %ds.\n" "$model" "$elapsed" >&2
return 0
Expand Down Expand Up @@ -3583,12 +3590,14 @@ opencode_config_source_candidates() {
resolved_scan_target="$(resolve_current_target_path "$TARGET_PATH" 2>/dev/null || true)"

if [ -n "$resolved_scan_target" ]; then
printf '%s\n' "$resolved_scan_target/.github/workflows/opencode-review-dispatch.yml"
printf '%s\n' "$resolved_scan_target/.github/workflows/opencode-review.yml"
printf '%s\n' "$resolved_scan_target/opencode.jsonc"
fi
if pull_request_head_blob_required || [ "$TARGET_PATH_IS_INTERNAL_PR_SCOPE" -eq 1 ]; then
return 0
fi
printf '%s\n' "$REPO_ROOT/.github/workflows/opencode-review-dispatch.yml"
printf '%s\n' "$REPO_ROOT/.github/workflows/opencode-review.yml"
printf '%s\n' "$REPO_ROOT/opencode.jsonc"
}
Expand Down Expand Up @@ -3869,9 +3878,10 @@ run_current_target_scan() {

case "$PR_FINDINGS_DECISION" in
block_changed | block_unmapped | block_manifest_unverified)
if [ "$strict_primary_provider_fallback" -eq 1 ]; then
fail_reported_vulnerabilities_before_fallback_success || true
if [ "$strict_primary_provider_fallback" -eq 1 ] && fail_reported_vulnerabilities_before_fallback_success; then
return 1
fi
echo "Strix quick scan failed with a non-recoverable error." >&2
return 1
;;
esac
Expand Down Expand Up @@ -3945,9 +3955,10 @@ run_current_target_scan() {

case "$PR_FINDINGS_DECISION" in
block_changed | block_unmapped | block_manifest_unverified)
if [ "$strict_fallback_provider_signal" -eq 1 ]; then
fail_reported_vulnerabilities_before_fallback_success || true
if [ "$strict_fallback_provider_signal" -eq 1 ] && fail_reported_vulnerabilities_before_fallback_success; then
return 1
fi
echo "Strix quick scan failed with a non-recoverable error." >&2
return 1
;;
esac
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/test_opencode_fact_gate_contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repo_root="$(
cd -P -- "$(dirname -- "$0")/../.."
pwd -P
)"
workflow_file="$repo_root/.github/workflows/opencode-review.yml"
workflow_file="$repo_root/.github/workflows/opencode-review-dispatch.yml"

check_contains() {
local needle="$1"
Expand Down
Loading
Loading