diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 505053287b..f3078a9e71 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -1091,6 +1091,15 @@ jobs: - name: Publish same-head manual Strix status env: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} + # Same conditional as the strix job's own GITHUB_STATUS_TOKEN above: + # github.token only has statuses:write on THIS repository, so it is + # only worth offering as a publish credential when the dispatch + # target is this repository itself (the self-referential case + # target-app-token cannot cover -- the OpenCode app is scoped to + # sibling repos, not to ContextualWisdomLab/.github as its own + # target). Empty for a cross-repo target, matching post_strix_status's + # existing empty-token skip. + GITHUB_STATUS_TOKEN: ${{ (github.event.client_payload.target_repository == '' || github.event.client_payload.target_repository == github.repository) && github.token || '' }} GITHUB_STATUS_READ_TOKEN: ${{ github.token }} PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} @@ -1202,6 +1211,9 @@ jobs: if post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then exit 0 fi + if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then + exit 0 + fi if post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then exit 0 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c61c142b..6b2cfadc83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Fix `strix.yml`'s `publish-manual-pr-evidence-status` job failing to + publish a manual Strix status back to `.github` when it is the + `repository_dispatch` target of its own Strix run: `target-app-token` is + scoped for sibling repositories and always 403s for this self-referential + case, and the job was missing the `github.token` fallback (conditioned on + `target_repository == github.repository`, matching the job's own + already-declared `statuses: write` permission) that a near-identical + block elsewhere in the same file already had. No behavior change for + genuine cross-repo targets. - Harden the review sidecar's per-account catalog cap against silent drift: `contextual_orchestrator_review_launcher.py`'s two `build_zdr_prioritized_catalog` call sites now source their diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 758ef2961a..b7dfd9638a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1133,20 +1133,33 @@ then a 502 on the actual gateway request). has not been extended to PR #1434 specifically, so this pass did not self-authorize one) or a `repository_dispatch` targeting a *different* repository that does not itself edit these trusted files. -- **Secondary, separate finding on the same run**: the follow-up - `publish-manual-pr-evidence-status` job also failed — +- **Secondary, separate finding on the same run — now fixed.** The + follow-up `publish-manual-pr-evidence-status` job also failed — `target-app-token` got `HTTP 403: Resource not accessible by integration` publishing the (correctly non-success, per the self-test failure above) Strix status back to `.github`'s own PR #1434. The publisher's own logic only tolerates a publish failure silently when `STRIX_RESULT=success`; a - non-success result that also cannot be published hard-fails by design, so - this is arguably correct fail-closed behavior surfacing a real, - previously-unobserved token-scoping gap, not a logic bug. Plausibly an - edge case specific to `.github` being the `target_repository` of its own - `repository_dispatch` Strix run (this central repo normally dispatches - Strix *to* sibling repos, not to itself) rather than a gap sibling repos - would hit; not investigated further or fixed this pass given it is - downstream of, and only surfaced by, the self-test failure above. + non-success result that also cannot be published hard-fails by design, + which is correct fail-closed behavior, not a logic bug — but the actual + token-scoping gap it surfaced was real: `target-app-token` (an OpenCode + app-token exchange) is scoped for *sibling* repositories, not for + `.github` as the target of its own `repository_dispatch` (this central + repo normally dispatches Strix *to* siblings, not to itself), so it can + never succeed for this specific self-referential case. The job's own + `permissions: statuses: write` block already grants `github.token` + exactly the scope this case needs, and the sibling status-publish block + earlier in the same file (the `strix` job's own inline publish step, ~40 + lines above) already had a `github-token` fallback conditioned on + `target_repository == '' || target_repository == github.repository` — + this second, near-identical block had simply drifted without it. Fixed + by adding the same conditional `GITHUB_STATUS_TOKEN` env var and a + matching `post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"` call + to this job's fallback chain, mirroring the existing pattern exactly; + empty (never attempted) for a genuine cross-repo target, so no behavior + change for the normal sibling-repo case. Full local suite unaffected + (1882 passed, 100% interrogate); not verified on a live hosted run since + reproducing it requires a `repository_dispatch` Strix run targeting + `.github` itself, the same rare trigger path that surfaced the bug. ## 2026-08-30 ZDR/NIM-routing architecture review (owner-directed) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 4053f4fd53..a29c3f0acb 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -206,8 +206,29 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "default-branch repository_dispatch evidence cannot cancel" "strix workflow documents manual evidence isolation from branch protection contexts" assert_file_contains "$workflow_file" "re-dispatches exact-head evidence" "strix workflow documents current-head queue recovery" assert_file_contains "$workflow_file" "refs/pull//head has already advanced before this queued run starts" "strix workflow documents stale scan queue avoidance" + # Two declarations as of 2026-08-30: the strix job's own inline status + # publish, and publish-manual-pr-evidence-status's -- the latter added a + # github-token fallback for the self-referential case where .github is + # the repository_dispatch target of its own Strix run (target-app-token + # is scoped for sibling repositories and always 403s there). Both must + # stay byte-identical same-repository conditionals, not merely + # same-named: a third assertion below pins the exact conditional + # expression to the same count so a divergent copy (e.g. a typo'd + # comparison, or one job's copy missing the || github.repository + # fallback) fails closed here instead of only in the field the next + # time .github dispatches Strix at itself. status_token_count="$(grep -c '^[[:space:]]*GITHUB_STATUS_TOKEN:' "$workflow_file")" - assert_equals "1" "$status_token_count" "strix workflow defines GITHUB_STATUS_TOKEN once so GitHub can parse repository_dispatch" + assert_equals "2" "$status_token_count" "strix workflow defines GITHUB_STATUS_TOKEN exactly twice, once per status-publishing job" + status_token_conditional_count="$(grep -c "GITHUB_STATUS_TOKEN: \${{ (github.event.client_payload.target_repository == '' || github.event.client_payload.target_repository == github.repository) && github.token || '' }}" "$workflow_file")" + assert_equals "2" "$status_token_conditional_count" "both GITHUB_STATUS_TOKEN declarations use the identical same-repository conditional" + # Three invocations, not two: the strix job's own inline publish tries + # the github-token fallback twice (once mid-chain, once as the final + # last-resort retry after every other credential has failed), while + # publish-manual-pr-evidence-status tries it once. Both jobs actually + # consuming their own declared GITHUB_STATUS_TOKEN (not just declaring + # and ignoring it) is the property under test here. + github_status_token_fallback_count="$(grep -c 'post_strix_status "github-token" "\$GITHUB_STATUS_TOKEN"' "$workflow_file")" + assert_equals "3" "$github_status_token_fallback_count" "both status-publishing jobs actually invoke their own GITHUB_STATUS_TOKEN fallback, not just declare it" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "strix workflow must not hard-code repository-specific PR bypasses" assert_file_contains "$workflow_file" "models: read" "strix workflow grants only the GitHub Models read permission needed for Strix" assert_file_contains "$workflow_file" "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0" "strix workflow pins actions/setup-python" @@ -522,7 +543,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge was removed to avoid duplicate required-check resource use" assert_file_not_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge no longer reconsumes stale trusted review state" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "opencode review workflow must not hard-code repository-specific PR bypasses" - if awk '/^ required-workflow-bootstrap:$/,/^[^ ]/' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then + if awk '/^ required-workflow-bootstrap:$/{p=1; print; next} p && /^ [A-Za-z0-9_-]+:/{exit} p' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then record_failure "opencode required workflow bootstrap must not depend on required-workflow event payload fields" fi assert_file_contains "$workflow_file" 'github.event.client_payload.target_repository || github.repository' "opencode review scopes concurrency by target repository"