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
4 changes: 4 additions & 0 deletions .github/workflows/codeql-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ jobs:
permissions:
contents: read
id-token: write
pull-requests: read
statuses: read
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.detect-languages.outputs.matrix) }}
Expand Down Expand Up @@ -298,6 +300,8 @@ jobs:
contents: read
id-token: write
actions: read
pull-requests: read
statuses: read
steps:
- name: Dispatch current-head CodeQL scan
env:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### CodeQL required workflow denies private consumers a read they need for their own PR

- `.github/workflows/codeql-pr.yml`'s `analyze-head` and `dispatch-current-head` jobs called `gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` and later `repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses` while holding only `contents: read` (plus `id-token: write`, and `actions: read` on the coordinator job) -- reads GitHub's REST contract gates behind the `pull-requests: read` and `statuses: read` fine-grained permissions on a private repository. Public consumers never surfaced this because GET on a public repository needs no such grant, but private consumer ContextualWisdomLab/late-life-anxiety-reanalysis's PR #10 (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700410434`) failed both required-workflow jobs (`103571590442`, `103571810868`) at their first API call with `gh: Resource not accessible by integration (HTTP 403)`. Both jobs now also hold `pull-requests: read` and `statuses: read`; no write permission is added anywhere, and `actions: write` stays absent, so `tests/test_codeql_pr_workflow_contract.py::test_codeql_required_workflow_does_not_gain_actions_write` needed no change. New regression test `test_codeql_pr_jobs_hold_read_grants_private_consumers_need` pins the exact grant set. See `docs/doctoring/codeql-pr-private-consumer-read-permissions.md`. Refs ContextualWisdomLab/late-life-anxiety-reanalysis#10.

### Failed-check finding names the Strix sandbox instead of the gateway

- `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935.
Expand Down
26 changes: 26 additions & 0 deletions docs/doctoring/codeql-pr-private-consumer-read-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# CodeQL required workflow denies private consumers a read they need — 2026-09-13

## Symptom

Private consumer `ContextualWisdomLab/late-life-anxiety-reanalysis` PR #10 (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700410434`) failed both org-required `.github/workflows/codeql-pr.yml` jobs at their first API call, each with `gh: Resource not accessible by integration (HTTP 403)`. `CodeQL compatibility analysis (python)` (job `103571590442`), step "Read current-head CodeQL dispatch verdict", calls `gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` under `GH_TOKEN: ${{ github.token }}`; the runner printed effective token permissions of Contents: read, Metadata: read only (declared: `contents: read`, `id-token: write`). `Dispatch current-head CodeQL scan` (job `103571810868`) makes the same GET, then later reads `repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses`, under `contents: read`, `id-token: write`, `actions: read`. Public consumers (fast-mlsirm, pg-erd-cloud, naruon, html4tree) pass the identical workflow only because GET on a *public* repository needs no fine-grained grant; the defect is specific to private repositories.

## Root cause

Neither job declared the fine-grained read permissions GitHub's REST contract requires for these calls on a private repository: "Get a pull request" needs `pull-requests: read`; "List commit statuses for a reference" needs `statuses: read`. Missing both, the minted `GITHUB_TOKEN` had no read access to pull-request or status data on a private repo, and testing against public consumers never exercised the gap because anonymous-equivalent GETs on public repository resources are always permitted.

## Repair

Added `pull-requests: read` and `statuses: read` to the `analyze-head` and `dispatch-current-head` job `permissions:` blocks in `.github/workflows/codeql-pr.yml`, preserving declaration order (contents, id-token, [actions], pull-requests, statuses). No write permission is added anywhere; `actions: write` remains absent, still guarded by the existing `test_codeql_required_workflow_does_not_gain_actions_write` regression test.

## Local evidence

New test `test_codeql_pr_jobs_hold_read_grants_private_consumers_need` in `tests/test_codeql_pr_workflow_contract.py` slices both permission blocks the same way the neighboring `actions: write` guard does and asserts each holds exactly `pull-requests: read` and `statuses: read` with no `actions: write`. RED: 1 failed (`assert [] == ['read']`). GREEN: 1 passed. Combined focused run across the five CodeQL/required-workflow contract test files: 149 passed. Full repository suite and `actionlint` result are recorded in the pull request description.

## Hosted acceptance still required

This repair is unverified against GitHub's live permission enforcement. A newly loaded central SHA carrying this change must still pass both `analyze-head` and `dispatch-current-head` on the private consumer's exact current head before the defect is resolved end-to-end. Separately, the later `repository_dispatch` POST from `dispatch-current-head` to `ContextualWisdomLab/.github` using the OpenCode app token has not yet been exercised from a private consumer at all, and may surface a distinct scoping issue of its own once this read-permission blocker is cleared.

## References

- GitHub REST, "Get a pull request": https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request (fine-grained permission: `pull-requests: read`)
- GitHub REST, "List commit statuses for a reference": https://docs.github.com/en/rest/commits/statuses#list-commit-statuses-for-a-reference (fine-grained permission: `statuses: read`)
39 changes: 39 additions & 0 deletions tests/test_codeql_pr_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,45 @@ def test_codeql_required_workflow_does_not_gain_actions_write() -> None:
assert "actions: write" not in coordinator_permissions


def test_codeql_pr_jobs_hold_read_grants_private_consumers_need() -> None:
"""analyze-head and dispatch-current-head need pull-requests/statuses reads.

Consumer evidence: ContextualWisdomLab/late-life-anxiety-reanalysis PR #10
(head a1cd5bc6783c6510dfcf937f523c733366e82213, run 34700410434). Both
required-workflow jobs failed at their first API call with
`gh: Resource not accessible by integration (HTTP 403)`:
- job "CodeQL compatibility analysis (python)" (job 103571590442), step
"Read current-head CodeQL dispatch verdict", calling
`gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` with only
`contents: read` + `id-token: write` (effective token printed by the
runner: Contents: read, Metadata: read).
- job "Dispatch current-head CodeQL scan" (job 103571810868), step
"Dispatch current-head CodeQL scan", the same GET plus a later read of
`repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses`, with
`contents: read`, `id-token: write`, `actions: read`.

Public consumers (fast-mlsirm, pg-erd-cloud, naruon, html4tree) passed
only because GET on a public repository does not need the grant.
GitHub's REST contract requires the `pull-requests: read` fine-grained
permission for "Get a pull request" and `statuses: read` for "List commit
statuses for a reference" on private repositories.
"""
workflow = WORKFLOW_PATH.read_text(encoding="utf-8")
shard_permissions = workflow.split(" analyze-head:\n", 1)[1].split(
" strategy:\n", 1
)[0]
coordinator_permissions = workflow.split(" dispatch-current-head:\n", 1)[1].split(
" steps:\n", 1
)[0]

for block in (shard_permissions, coordinator_permissions):
assert re.findall(r"^ pull-requests: (\w+)$", block, re.MULTILINE) == [
"read"
]
assert re.findall(r"^ statuses: (\w+)$", block, re.MULTILINE) == ["read"]
assert "actions: write" not in block


def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch(
tmp_path: Path,
) -> None:
Expand Down
Loading