diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index ce7939845..4df21856a 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -7932,14 +7932,14 @@ jobs: && needs.validate-pr-metadata.outputs.target_repository != '' && needs.validate-pr-metadata.outputs.head_sha != '' env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }} + GH_TOKEN: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }} GH_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }} PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} OPENCODE_MODEL_POOL_OUTCOME: ${{ steps.opencode_review_model_pool.outputs.review_status }} COVERAGE_EVIDENCE_RESULT: ${{ needs.coverage-evidence.result }} - OPENCODE_STATUS_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} + OPENCODE_STATUS_TOKEN_SOURCE: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }} OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0ef8d44..e7b4bba85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,12 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Prefer the job-scoped `github.token` when the central OpenCode dispatch + publishes a commit status back to the same `.github` repository. The job's + declared `statuses: write` permission now reaches the endpoint instead of an + unrelated OpenCode App installation token that can lack commit-status write + permission; cross-repository status publication keeps the existing explicit + PAT/App credential chain. - Publish only the sanitized cumulative Strix report tree, avoiding a later copy of relative scanner output that could reintroduce known internal warning text into uploaded security evidence. diff --git a/docs/doctoring/opencode-same-repository-status-credential.md b/docs/doctoring/opencode-same-repository-status-credential.md new file mode 100644 index 000000000..e2b32c01b --- /dev/null +++ b/docs/doctoring/opencode-same-repository-status-credential.md @@ -0,0 +1,49 @@ +# OpenCode same-repository status credential + +## Operator outcome + +An OpenCode repository-dispatch run targeting `ContextualWisdomLab/.github` +publishes its optional `opencode-review` commit status with the current job's +`github.token`. Cross-repository targets continue to use the configured PAT or +OpenCode App installation token, because `github.token` is limited to the +repository containing the workflow. + +If status publication fails, inspect the logged token-source label and the +endpoint response. Do not weaken the formal exact-head Reviews API verdict or +branch protection: the commit status is complementary evidence. + +## Root cause and decision + +Run 32560612401 declared `statuses: write` for the OpenCode job but selected the +separate OpenCode App token for a same-repository status write. GitHub rejected +`POST /repos/ContextualWisdomLab/.github/statuses/{sha}` with HTTP 403 because +that installation token did not carry commit-status write permission. + +The smallest repair is credential precedence at the existing publication +boundary. Same-repository publication uses `github.token`, whose effective +permissions are already narrowed by the job. Cross-repository publication +retains the established PAT/App chain and the existing neutral path when only a +repository-scoped workflow token is available. No new credential, permission, +provider, retry, or fallback abstraction is introduced. + +This boundary supports SOC 2 and CSAP evidence expectations by preserving +least privilege, explicit credential provenance, exact-head status binding, +and an auditable failure instead of broadening the OpenCode App installation. + +## Verification + +- The contract test requires both `GH_TOKEN` and its logged source to select + `github-token` first only when the target equals the workflow repository. +- The existing cross-repository notice and fail-closed exact-head review path + remain unchanged. +- The complete Python, shell, compilation, docstring, and branch-coverage gates + remain mandatory before merge. + +## APA 7th references + +GitHub. (n.d.). *GITHUB_TOKEN*. GitHub Docs. Retrieved August 22, 2026, from +https://docs.github.com/en/actions/concepts/security/github_token + +GitHub. (n.d.). *Permissions required for GitHub Apps*. GitHub Docs. Retrieved +August 22, 2026, from +https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e00cc5214..32d4e5ae4 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2063,11 +2063,17 @@ def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch( " - name: Dispatch Noema after current-head OpenCode approval", 1 )[0] assert ( - "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " + "GH_TOKEN: ${{ needs.validate-pr-metadata.outputs.target_repository == " + "github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || " "secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || " "github.token }}" ) in status_step - assert "OPENCODE_STATUS_TOKEN_SOURCE" in status_step + assert ( + "OPENCODE_STATUS_TOKEN_SOURCE: ${{ " + "needs.validate-pr-metadata.outputs.target_repository == github.repository && " + "'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && " + "'PR_REVIEW_MERGE_TOKEN'" + ) in status_step assert "steps.opencode_app_token.outputs.available == 'true' && 'opencode-app'" in status_step assert "OPENCODE_CHANGED_FILES_FILE" in status_step assert "OPENCODE_ARTIFACT_MANIFEST_SHA256" in status_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index d0210b1ab..d72b3ebb1 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "ce7939845286be9668a01d5c640e867a8490ee5c" +REVIEW_DISPATCH_BLOB_SHA = "4df21856a67776a1fdb2cd51c12e9abf1372a442" def _workflow_text(path: Path) -> str: