From 6b9e2f0a62ccdbd3f41141f3cef2ace5f31cb686 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 17 Jul 2026 01:55:35 +0900 Subject: [PATCH 1/4] fix(strix): scope cross-repo dispatch with target token --- .github/workflows/strix.yml | 6 +++++- .../test_required_workflow_queue_contract.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 879aab65b..a1f72e536 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -718,7 +718,11 @@ jobs: BUN_CONFIG_IGNORE_SCRIPTS: "true" STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM STRIX_DISABLE_PR_SCOPING: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '0' || '1' }} - GH_TOKEN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && github.token || '' }} + # A repository_dispatch executes in this central repository, so its + # github.token cannot read the target repository's PR. Reuse the + # target-app token that already validated and fetched that exact PR; + # preserve the target-repository token for pull_request_target runs. + GH_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && (steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token) || github.event_name == 'pull_request_target' && github.token || '' }} PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.client_payload.pr_number }} PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha }} PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }} diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 384750c80..70f46dc6c 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -886,6 +886,26 @@ def test_strix_provider_outage_without_findings_is_neutralized() -> None: ) +def test_strix_cross_repo_dispatch_uses_target_token_for_pr_scoping() -> None: + workflow = workflow_text("strix.yml") + run_step = workflow.split(" - name: Run Strix (quick)", 1)[1].split( + " - name:", 1 + )[0] + + assert "STRIX_TARGET_PATH:" in run_step + assert "github.event_name == 'repository_dispatch'" in run_step + assert "github.event.client_payload.pr_number != ''" in run_step + assert ( + "steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || " + "github.token" + ) in run_step + assert "github.event_name == 'pull_request_target' && github.token" in run_step + assert ( + "(github.event_name == 'pull_request_target' || " + "github.event.client_payload.pr_number != '') && github.token" + ) not in run_step + + def test_pr_scorecard_sarif_delegates_sast_and_vulnerability_posture_to_hard_gates() -> ( None ): From df8ebe2a73d82a6fc446d2d7386194d0ad442e27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 17 Jul 2026 02:02:14 +0900 Subject: [PATCH 2/4] fix(strix): normalize executable artifact before hashing --- .github/workflows/strix.yml | 5 +++++ scripts/ci/test_strix_quick_gate.sh | 1 + tests/test_required_workflow_queue_contract.py | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index a1f72e536..828e485bb 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -504,6 +504,11 @@ jobs: exit 1 ;; esac + # pip can preserve an existing console-script mode instead of applying + # the process umask, so normalize the resolved artifact itself before + # pinning its digest. The runtime gate still fails closed if anything + # relaxes these bits after this trusted installation step. + chmod go-w -- "$strix_executable" strix_scripts_root="$(python3 -c 'import sysconfig; print(sysconfig.get_path("scripts"))')" strix_executable_sha256="$(python3 - "$strix_executable" <<'PY' import hashlib diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3a1cb943f..4f1005a12 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -890,6 +890,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'STRIX_EXECUTABLE_SHA256=%s' "Strix workflow pins the installed executable digest before scanning" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'STRIX_EXECUTABLE_ROOT=%s' "Strix workflow pins the installed executable root before scanning" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'umask 022' "Strix workflow creates the credential-bearing executable without group/world write access" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'chmod go-w -- "$strix_executable"' "Strix workflow normalizes the resolved executable before hashing" assert_file_contains "$GATE_SCRIPT" 'STRIX_EXECUTABLE_PATH must name the trusted installed Strix executable' "Strix gate requires an explicit trusted executable path" assert_file_contains "$GATE_SCRIPT" 'did not match the pinned SHA-256 digest' "Strix gate rejects executable substitution after trusted installation" assert_file_contains "$GATE_SCRIPT" 'STRIX_EXECUTABLE_PATH must be outside the untrusted scan target' "Strix executable cannot come from the scan target" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 70f46dc6c..670581369 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -191,6 +191,18 @@ def test_strix_cancels_superseded_pr_head_security_evidence() -> None: ) +def test_strix_install_normalizes_executable_permissions_before_hashing() -> None: + workflow = workflow_text("strix.yml") + install_step = workflow_step(workflow, "Install Strix") + + assert install_step.index("umask 022") < install_step.index( + "python3 -m pip install" + ) + assert install_step.index('chmod go-w -- "$strix_executable"') < install_step.index( + 'strix_executable_sha256="' + ) + + def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() -> None: workflows = ( "close-empty-pr.yml", From ede4799182200f217e3fbab8af9bb7c137f0f98b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 17 Jul 2026 02:11:31 +0900 Subject: [PATCH 3/4] ci(strix): retrigger restored required gates From 49c09e4c091a35637ba4c4e2cb1246ea0c004ad0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 17 Jul 2026 02:21:12 +0900 Subject: [PATCH 4/4] fix(review): dispatch trusted OpenCode retries --- scripts/ci/pr_review_merge_scheduler.py | 13 ++++++++----- tests/test_pr_review_merge_scheduler.py | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index c77a9207e..c13adfe9a 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -2043,7 +2043,14 @@ def cancel_stale_opencode_runs(repo: str, workflow: str, pr: dict[str, Any], *, def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> str: - """Dispatch OpenCode for the PR head, or report an active same-head run.""" + """Dispatch trusted OpenCode for the PR head, or report an active run. + + The review job is intentionally restricted to ``repository_dispatch``. A + check-run job exposed by the original ``pull_request_target`` workflow is + therefore not a reusable execution entrypoint: rerunning that job preserves + the original event and leaves the review job skipped. Always use the + default-branch dispatch entrypoint after same-head deduplication. + """ if not dry_run: require_github_actions_control_actor("inspect-active-opencode-review") current_run_refs, stale_run_refs = active_opencode_run_refs(repo, workflow, pr) @@ -2056,10 +2063,6 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr ) ) return "already_running" - job_id = matching_actions_job_id(pr, is_opencode_context) - if job_id: - rerun_actions_job(repo, job_id, dry_run=dry_run, action="rerun-opencode-review") - return "rerun" if dry_run: return "dry_run" base_ref, base_sha, head_sha = validated_pr_dispatch_fields(pr) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index e910296f7..1fe10da73 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1628,6 +1628,8 @@ def fake_run(args, stdin=None): calls.clear() required_workflow_pr = make_pr( + baseRefOid=base_sha, + headRefOid=head_sha, statusCheckRollup={ "contexts": { "nodes": [ @@ -1644,7 +1646,7 @@ def fake_run(args, stdin=None): ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", "-f", "status=in_progress", "-F", "per_page=100"], ] assert calls[2:] == [ - ["gh", "api", "-X", "POST", "repos/owner/repo/actions/jobs/101/rerun"], + ["gh", "api", "-X", "POST", "repos/owner/repo/dispatches", "--input", "-"], ["gh", "api", "-X", "POST", "repos/owner/repo/actions/jobs/202/rerun"], ]