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
15 changes: 15 additions & 0 deletions .github/cursor-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ skipped if no Slack token is configured). A skip for the diff-size cap is
announced on the PR rather than passing for a clean review — see [Over the
diff-size cap](#over-the-diff-size-cap).

**Post review is its own job, and that is a security boundary.** No job both
checks out PR code and holds a write-scoped credential. Every job that checks out
PR code and runs `cursor-agent` over it — the 8 panel cells and the judge's
`Consolidate panel` — holds `contents: read` and nothing else. They hand their
result to `Post review` as an artifact; that job checks out no PR code, loads
only this directory's scripts from the pinned `workflows_ref`, and carries
`pull-requests: write` plus the review bot's App-key mint (as does the
over-the-cap comment job, which likewise checks out nothing).
The panel and the judge run `--print --trust` with no `--sandbox`, and
print mode can use write and shell tools — so a model steered by a malicious diff
could rewrite the assets checkout or a downloaded action *inside its own job*. On
a fresh runner with a fresh pinned checkout there is nothing tampered left for the
minted token to meet. `tests/test_workflow_job_isolation.py` pins the property, and
[`pr-size.yml`](../workflows/pr-size.yml) uses the identical split for its comment job.

### The panel

| Lab | Model (Cursor catalog) |
Expand Down
14 changes: 8 additions & 6 deletions .github/cursor-review/build-ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@

Prompt injection
----------------
The ledger imports PR comment text into a reviewer prompt on a workflow whose
``consolidate`` job holds ``pull-requests: write``, so it is a new untrusted
channel. The workflow's ``gate`` job already skips fork PRs, so the surface is
The ledger imports PR comment text into a reviewer prompt read by agent jobs
that hold ``contents: read`` only, so it cannot reach a write-scoped credential
directly — but it is still a new untrusted channel: it steers what the panel and
the judge report, and the review those jobs produce is posted verbatim by the
separate ``post-review`` job. The workflow's ``gate`` job already skips fork PRs, so the surface is
same-repo PRs plus bot-authored text (Dependabot, cloud-code-bot) — and, on a
public repo, anything any reader can post to the PR. Four controls, in order of
how much they carry:
Expand Down Expand Up @@ -471,9 +473,9 @@ def unknown_ledger(call: str, reason: str) -> dict:
"""A ledger that could not be read. NEVER reported as `empty`.

A guard that cannot read its input says so: the prompt section is omitted
(there is nothing truthful to put in it) and consolidate renders a banner on
the review naming the failed call, so a context-free re-review can never look
identical to a genuine first round.
(there is nothing truthful to put in it) and the `post-review` job renders a
banner on the review naming the failed call, so a context-free re-review can
never look identical to a genuine first round.
"""
return {
"status": "unknown",
Expand Down
Loading
Loading