🤖 The Codex Security Review gate trusted GitHub's `author_association`
(`MEMBER`/`OWNER`), but GitHub only reports `MEMBER` to viewers who can
see the membership. The workflow `GITHUB_TOKEN` cannot see private
`block` org membership, which is GitHub's default, so most Block
employees read as `CONTRIBUTOR` and their PRs never reviewed
automatically. The same applied to the comment path, where private
members could not authorize a review with `@buzz-security-review <sha>`.
Trust is now repo write access, read from `GET
/repos/{owner}/{repo}/collaborators/{user}/permission` via
`getCollaboratorPermissionLevel`, which is independent of membership
visibility and works with the authorize job's existing `contents: read`
and `pull-requests: read` permissions. A user is trusted only when
`permission` is `write` (which includes `maintain`) or `admin`. A
successful lookup is not enough: on this public repository, an account
with no relationship to it returns `200` with `read`, not 404. Outside
contributors can only push to forks and come back `read`, so they still
need an explicit command from a trusted user.
The check covers the PR author on `pull_request_target`, the commenter
on `issue_comment` (the job-level `author_association` filter is removed
so the trusted script makes that decision), and the stale-marking path
on PR updates. Transient API errors are retried. Authorization fails
closed: any lookup error other than 404 fails the job instead of
authorizing. Stale-marking is conservative: an existing review is always
marked stale without consulting permissions, and a failed lookup counts
as untrusted, so a previous range's review can never appear current for
a new head. A commenter without write access is logged and ignored
instead of failing the run.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Changes `CODEX_REASONING_EFFORT` from `max` to `high` in the
`security-review` job.
`max` increases both normal review latency and exposure to the upstream
PTY-shutdown hang
([openai/codex-action#169](https://github.com/openai/codex-action/issues/169))
where Codex finishes writing its output file but holds stdio open. The
composite `uses:` action ignores the 30-minute step timeout and
continues running until the 40-minute job timeout fires; at that point
GitHub cancels the entire job and the `always()` salvage step never gets
to run. `high` retains a high reasoning setting while trading some depth
for speed, reducing typical completion time and shrinking the window
during which a hung process blocks the salvage path.
No other behavior changes. Action pin, CLI version, model, output
schema, prompt, and timeout values are unchanged.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Codex CLI can leave a PTY descendant holding the action's inherited
stdio after the turn completes. The `runCodexExec.ts` wrapper waits on a
`close` event that never fires, so the `Review pull request` step hangs
until the job timeout kills it — discarding the finished review the CLI
already wrote to disk.
The CLI writes the completed review to the `--output-last-message` file
(exposed as `output-file`) **before** the hang. This PR adds a salvage
step that recovers it, and sets the step and job timeouts to preserve
the full 30-minute Codex execution budget.
**Changes (`codex-security-review.yml`):**
- Add `output-file: ${{ runner.temp }}/codex-review.json` to the `Review
pull request` step so the CLI writes the result before the hang.
(`runner` context is valid in `steps.with`; not in `jobs.env`.)
- Add `timeout-minutes: 30` and `continue-on-error: true` to the Codex
step — a hang now costs ≤30 minutes instead of 40, and the salvage step
still runs.
- Set job `timeout-minutes: 40` to give setup, step cancellation, and
salvage sufficient headroom without colliding with the Codex execution
budget. The original 30-minute job timeout was too narrow: evidence from
run
[33114428326](https://github.com/block/buzz/actions/runs/33114428326/job/98665369165)
shows completed output appearing 28m46s after step start, meaning a
20-minute step timeout could kill a legitimate review before the salvage
file exists.
- Add a `Salvage review output` step with `if: always()`: prefers
`steps.run_codex.outputs.final-message` on a clean exit; falls back to
the output file when the step timed out. The output file path is set in
the step's own `env` block (`CODEX_OUTPUT_FILE: ${{ runner.temp
}}/codex-review.json`), where `runner` is valid. Validates shape
(non-empty JSON object, has `overall_risk`); fails the job hard if
neither source is present.
- Wire the job `outputs.review_json` to
`steps.salvage.outputs.review_json`.
**Changes (`Justfile`, `ci.yml`):**
- Add `actionlint .github/workflows/codex-security-review.yml` to
`security-review-check` so expression-validity errors are caught
locally.
- Provision `actionlint` via Hermit (pinned v1.7.12) rather than a
one-off `Install actionlint` curl step, so the same binary is used
locally and in CI.
**Security posture is unchanged:** the salvage step reads the action's
own output and a file written to `runner.temp` — neither is
PR-controlled. Credential-stripping env block on the Codex step is
untouched.
Note this is a temporary workaround until
https://github.com/openai/codex-action/issues/169 is addressed
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Codex CLI 0.149.x can leave a PTY descendant holding
`openai/codex-action`'s inherited stdio after the review turn completes.
The action's runner waits on the child's stdio *streams closing* rather
than on process exit
([`runCodexExec.ts`](https://github.com/openai/codex-action/blob/86365089/src/runCodexExec.ts#L322)),
so the `Review pull request` step never returns — the job idles until
its 30-minute `timeout-minutes` kills it and the already-written review
result is discarded.
Every `Run Codex Security Review` job since the workflow merged has hung
this way: the final JSON result and `tokens used` count are the last log
lines, with no step-end marker. Heavy runs (`gpt-5.6-sol` at `max`
effort, ~295k tokens) sit firmly in the failing regime.
Upstream:
[openai/codex-action#150](https://github.com/openai/codex-action/issues/150),
fixed in Codex CLI 0.150.0 by
[openai/codex@bf3eb2e](bf3eb2ec91)
("Prevent Unix PTY I/O from blocking runtime shutdown"). The fix commit
is in the `rust-v0.150.x` line and not in `0.149.x`.
The action pin (`v1.12`) is unchanged — only the `codex-version` CLI pin
moves from `0.149.0` to `0.150.1` (latest stable).
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The Codex security review workflow now authorizes automatic reviews from
the live pull request instead of the webhook snapshot. This prevents a
stale author association from sending an internal PR down the
external-contributor invalidation path.
Internal PRs with no previous review now exit without attempting a
comment or label mutation. The jobs that update PR comments and labels
use `pull-requests: write`, and the review job runs only after the
trusted prepare step emits `authorized=true`.
Regression tests cover stale author associations in both directions, the
no-op invalidation path, and the mutation-job permission contract.
Checked with `just ci`, `just security-review-check`, and `actionlint
.github/workflows/codex-security-review.yml`.
The failing `Mark Previous Review Stale` check on this PR runs the
pre-fix `pull_request_target` workflow from `main`. GitHub will not
execute this branch’s trusted workflow until it is merged; the new
workflow contract and state-machine tests exercise the fix before then.
Signed-off-by: Jordan Mecom <jm@squareup.com>
Add a gated Codex security review for Buzz pull requests.
Reviews run automatically for Block organization members. External PRs
require a `@buzz-security-review` comment from a Block member, and every
new external push invalidates the previous result.
Codex reviews the PR SHA in a read-only, network-restricted job with no
write GitHub token. A separate trusted job validates and sanitizes
structured findings before posting them. The workflow uses the dedicated
`CODEX_REVIEW_API_KEY` secret.
---------
Signed-off-by: Jordan Mecom <jm@squareup.com>