Skip to content

feat(adapter-utils): refuse a PR review whose attestation is unreachable in the target repo (BLO-32844) - #1744

Merged
allyblockcast[bot] merged 4 commits into
masterfrom
BLO-32844-ally-posted-an-approved-review-of-one-pr-onto-a-different-pr-in-a-different-repo-commit_id-said-the-right-head
Sep 11, 2026
Merged

allyblockcast[bot] merged 4 commits into
masterfrom
BLO-32844-ally-posted-an-approved-review-of-one-pr-onto-a-different-pr-in-a-different-repo-commit_id-said-the-right-head

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 10, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • One of those agents, Ally, reviews GitHub pull requests, and several repos gate merge on its review — so the review gate is a trust boundary, not a convenience
  • A review computed against Blockcast/pim-multicast-gateway#2864 was submitted as APPROVED onto Blockcast/mediamtx#33, an unrelated PR in a different repository
  • It failed open and was undetectable by GitHub's own metadata: commit_id is stamped from the target PR at submission time, so it read mediamtx#33's real head and every state === "APPROVED" && commit_id === head check was satisfied. Only the body's Reviewed head: marker disagreed, naming a SHA that resolves nowhere in mediamtx
  • Every existing defence is consumer-side, so it can only notice the bad review after it is already posted and merge-visible; nothing stopped it being emitted
  • This pull request adds a producer-side, fail-closed guard at the gh egress boundary — the only interposition point in front of an agent's review submission — that refuses to post a review whose attestation is malformed or names a commit unreachable in the target repository
  • The benefit is that an approval can no longer be granted to code that was never reviewed, and the opposite defect (a corrupt marker that makes a gate permanently unsatisfiable) is refused at emission instead of livelocking

Linked Issues or Issue Description

Path (B) — tracked in Paperclip, not GitHub Issues: BLO-32844. This PR closes its AC #2 (fail-closed guard) and AC #5 (regression coverage).

Bug report, in template terms:

  • What happened. Blockcast/mediamtx#33 received review 5146990033APPROVED, author allyblockcast[bot] — whose body reviewed a different PR in a different repo. Body marker 81d63ac9ca72ddafc12919d3b40ae0870a901b7f (the head of pim-multicast-gateway#2864); GET /repos/Blockcast/mediamtx/commits/81d63ac9… returns 422. The file it cites, .github/workflows/docker-e2e-tests.yml, 404s in mediamtx. The PR actually changed one runs-on: line in test.yml.
  • Expected. A review that cannot be attributed to the target pull request is not posted at all.
  • Severity. Fails open. In a repo where an Ally approval gates merge, it admits entirely unreviewed code under a green signal.
  • A second, opposite instance found while fixing it. Blockcast/review-gate-action#7 reviews 5013561307 and 5013569224 carry a 42-character marker (…a072 + f6 + f9a9… — two characters spliced into a real SHA) against a 40-char commit_id. Both bodies byte-identical, 1m45s apart; the three other reviews on that PR are correct, so this is intermittent corrupt emission. It fails closed: REVIEWED_HEAD_ATTESTATION_PATTERN matches nothing, so the review attests no head and can never satisfy the gate at any head, with no in-band recovery.
  • Scope of occurrence. A prior org-wide sweep on BLO-32844 covered 95/95 non-archived Blockcast repos and 4,446 Ally reviews: 1 contamination (the one above) plus the 2 malformed markers. One occurrence, under outage conditions — that lowers urgency but nothing structural prevented a recurrence, which is what this PR changes.

Related PRs (searched; all consumer-side, none duplicates of this producer-side guard): Refs #1721 (BLO-32695, structured verdict block — no file overlap with this PR), Refs #1687 (BLO-32198), Refs #1667 (BLO-32044), Refs #1559, Refs #1141 (BLO-19778).

What Changed

  • New pure module packages/adapter-utils/src/github-review-attestation.ts. Recognises a review submission in a gh argv (gh pr review, and gh api repos/{o}/{r}/pulls/{n}/reviews with a write method), extracts the Reviewed head: attestation, and classifies it as absent / well-formed / malformed / ambiguous. Argv-in, verdict-out; the only I/O is injected.
  • Wired into github-cli-egress-runtime.ts, which is what the Helm-seeded gh wrapper execs (deploy/helm/paperclip/templates/statefulset.yaml:458). The guard runs before the credential scrub and before the CLI is spawned, and aborts with exit 65 and a diagnostic naming the SHA, the repo, and a stable machine-readable reason.
  • Reachability resolver built on the same GitHub CLI the wrapper fronts: GET /repos/{owner}/{repo}/commits/{sha}, with 404/422 read as a definite absence and everything else as indeterminate.
  • A guardIo option on the runtime options so tests can inject a resolver instead of reaching the network.
  • Operator runbook runbooks/agent-review-submission-refused.md, indexed in runbooks/README.md: what each refusal reason means, how to distinguish cross-repository contamination from a benign force-push, and why there is no bypass.
  • 29 tests in github-review-attestation.test.ts, pinned to the real SHAs and the real contaminated body from both incidents rather than synthetic fixtures.

Deliberate design choices, each of which was a live trap:

  • Reachability, not head equality. Reviewing a prior head is legitimate and common, and the gate's own staleness rules handle it; only "does this commit exist in this repository" separates that from a review of another repo. A head-equality check would refuse ordinary review traffic.
  • Detection looser than the consumer's grammar, validation stricter. ally-review-detection.ts is right to ignore a 42-char marker when asking "is there a usable attestation?" — but a producer asking that same question waves the malformed review straight through, because it also sees none. So this finds any line that intends to attest, then demands exactly 40 hex.
  • Indeterminate refuses. The costs are asymmetric: a refused review is recoverable by re-running, an admitted false approval is merge-visible immediately and only a human dismissal removes it.
  • Fenced blocks are stripped, mirroring ally-review-detection.ts — otherwise a review of this guard would be refused for quoting the very line it validates.
  • Reads are untouched. Ally's idempotency step GETs the same .../reviews path; guarding it would double the cost of every review. Note -F means --body-file on gh pr review but --field on gh api, so the two forms are parsed separately.
  • No attestation ⇒ passthrough. An --approve with no marker is a different defect (invariant I2d in scripts/check-ally-review-consistency.mjs); refusing it here would block every non-Ally agent's ordinary gh pr review.

Verification

pnpm install
npx tsc --noEmit -p packages/adapter-utils          # clean
npx vitest run packages/adapter-utils/src/          # 392 passed, 29 of them new
node scripts/check-forbidden-tokens.mjs             # no forbidden tokens

Exercised end-to-end against the live GitHub API with the real resolver, not mocks (replaying the actual incident):

check result
81d63ac9… reachable in Blockcast/mediamtx unreachable
81d63ac9… reachable in Blockcast/pim-multicast-gateway reachable
real contaminated body submitted at mediamtx#33 refusedunreachable-attestation, exit 65, CLI never started
same body against its correct repo (pim#2864) allowed
genuine mediamtx#33 review at dbe4e10e… allowed
real 42-char marker refused locally, zero API calls
gh api repos/Blockcast/mediamtx/pulls/33/reviews (GET) passthrough

The process-level path was checked too, through the module's CLI entry with a fake target that records any review attempt: exit 65, refusal on stderr, and the recorder never written — i.e. the review genuinely does not leave the pod.

Test-suite honesty: the full packages/adapter-utils run shows 1 failure, mcp-isolation.integration.test.ts. It is a pre-existing parallel-load flake, not from this change — I ran the control both ways: clean master fails 2 (mcp-isolation.integration and acpx-engine/session-store), and both pass in isolation on either tree. This branch is strictly better than master on that suite.

Risks

  • Fail-closed by design, so a false refusal blocks a legitimate review. The realistic trigger is a commit force-pushed away between composing the review and submitting it — the attested SHA genuinely no longer exists, so the review is worthless and re-reviewing at the live head is the correct outcome. Recoverable by re-running; contrast an admitted false approval, which is not.
  • A GitHub outage now blocks review submission rather than posting an unverified review, because indeterminate refuses. Accepted for the same asymmetry. The refusal names the reason, so it is diagnosable rather than silent.
  • One extra API call per review submission that carries an attestation. Malformed and ambiguous markers are rejected locally with no call; non-review invocations and reads are not touched at all.
  • Not yet armed in the fleet by merging. Agent pods load this from the published dist/, so it takes effect on the next adapter-utils build/publish — merging this PR alone does not change fleet behaviour. Called out so nobody reads the merge as the mitigation.
  • Grammar is duplicated with server/src/services/ally-review-detection.ts, intentionally: adapter-utils ships to agent pods and must not depend on server/. Unification is tracked as BLO-32512; the loose/strict split is the property most easily lost in that merge, and the module header says so.
  • No migration, no schema, no API or UI surface. Additive module plus one call site.

Model Used

Claude Opus (Anthropic), model ID claude-opus-5[1m] — the 1M-context variant, running in Claude Code with extended thinking, tool use, and shell/code execution. Used for the investigation, the implementation, the tests, and this description. All live measurements quoted above were executed as real commands against the GitHub API and the test suite, not recalled.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work — its "Agent Reviews and Approvals" item is Paperclip's own in-app review/approval workflow, not the GitHub PR review gate this touches
  • I have searched GitHub for duplicate or related PRs and linked them above — five neighbours found, all consumer-side; no existing producer-side submission guard
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — N/A, no UI surface
  • I have updated relevant documentation to reflect my changes — added runbooks/agent-review-submission-refused.md (triage per refusal reason, how to tell cross-repo contamination from a benign force-push, why there is no bypass) and indexed it in runbooks/README.md. I deliberately did not touch the producer prose in .planning/ally-agent/AGENTS.md, which feat(review-gate): key the ally-comment gate on a structured verdict block, not prose regexes (BLO-32695) #1721 is currently editing.
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — still running at time of writing; not claiming it.
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — not yet reviewed.
  • I will address all Greptile and reviewer comments before requesting merge

…ble in the target repo (BLO-32844)

A review computed against Blockcast/pim-multicast-gateway#2864 was submitted to
Blockcast/mediamtx#33 as APPROVED. GitHub stamps `commit_id` from the *target*
PR at submission time, so it read mediamtx#33's real head and every consumer
checking `state === "APPROVED" && commit_id === head` saw a correctly approved
head. Only the body's `Reviewed head:` marker disagreed — it named 81d63ac9,
which resolves nowhere in mediamtx (HTTP 422). That is a fail-open: in a repo
where an Ally approval gates merge it admits entirely unreviewed code.

The control plane never posts a review, so there is no server/ code path to
hook. Agents shell out to `gh`, and the Helm-seeded launcher in front of
/usr/bin/gh (statefulset.yaml:458) is the only interposition point. The guard
goes there, beside the existing stdin fail-closed check.

Detection is deliberately looser than the consumer's grammar and validation
stricter. ally-review-detection.ts answers "is there a usable attestation?" and
correctly ignores a malformed one; a producer asking that question would wave
the malformed review through. So this finds any line that *intends* to attest,
then demands exactly 40 hex — which also closes the opposite, fail-CLOSED
defect: review-gate-action#7 reviews 5013561307/5013569224 carry a 42-character
marker, so REVIEWED_HEAD_ATTESTATION_PATTERN matches nothing, the review
attests no head, and it can never satisfy the gate at any head.

Reachability rather than head equality is the test: reviewing a prior head is
legitimate, and only "does this commit exist in this repository" separates
that from a review of another repo. Probed via GET /repos/{o}/{r}/commits/{sha}
rather than commit search, which is index-backed and empty for a fresh push.
Every indeterminate outcome refuses, because the costs are asymmetric — a
refused review is recoverable by re-running, an admitted false approval is
merge-visible immediately and only a human dismissal removes it.

Verified against the live API: 81d63ac9 reads unreachable in mediamtx and
reachable in pim-multicast-gateway; replaying the real contaminated body at
mediamtx#33 refuses with exit 65 and never starts the CLI, while the genuine
mediamtx review at dbe4e10e and the same body against its correct repo both
pass. Ally's idempotency GET of the reviews path is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 10, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-32512
🔗 Paperclip issue: BLO-32695
🔗 Paperclip issue: BLO-32844
🔗 Paperclip issue: BLO-14866

@allyblockcast

allyblockcast Bot commented Sep 10, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 849ee29

Critical Issues (0)

Important Issues (1)

  • [native-codex] packages/adapter-utils/src/github-review-attestation.ts:958 — the loose candidate regex does not require the attestation line to end after the captured token. A body such as Reviewed head: <40-hex> extra (or a valid SHA followed by an unmatched closing delimiter and extra text) is classified as well-formed and the commit is checked, but the consumer's strict REVIEWED_HEAD_ATTESTATION_PATTERN will not recognize that line because it requires optional delimiters/whitespace followed by end-of-line. The guard can therefore allow a submission that still cannot satisfy the review gate.
    • Require the candidate line to contain only the optional Markdown delimiters and whitespace after the token, or capture any trailing non-whitespace/suffix as part of the malformed candidate so this shape is refused locally. Add regression tests for trailing prose and unmatched delimiter plus trailing prose.

Suggestions (0)

Strengths

  • The guard is placed before the scrubbed subprocess is spawned, so rejected review submissions cannot reach GitHub.
  • Repository reachability is checked with the direct commit endpoint and indeterminate results fail closed.
  • The tests cover the cross-repository contamination case, malformed token lengths, ambiguous attestations, stdin handling, and CLI pass-through behavior.

Recommended Action

  1. Fix the Important issue before merge.
  2. Keep the existing fail-closed behavior for malformed or indeterminate attestations.

The guard added in the previous commit refuses a review before it leaves the
pod, with exit 65 and a stable bracketed reason. Without a runbook the obvious
operator reactions are both wrong: disable the guard, or hand-post the refused
body under a human seat — which re-creates the exact defect with a human
identity attached.

Documents what each reason means and, for `unreachable-attestation`, how to
tell the three causes apart: cross-repository contamination (the BLO-32844
defect, worth capturing because AC #1's transport is still open), a commit
force-pushed away (benign, and the refusal is still correct because the review
describes code that is gone), and GitHub being unreachable (`could not confirm`
rather than `does not exist`). Also carries the marker-resolves-in-its-own-repo
check for auditing reviews that predate the guard, and notes that a prior-head
marker which still resolves is normal staleness rather than contamination.

Co-Authored-By: Claude <noreply@anthropic.com>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4628bce

Prior Findings Dispositioned (1)

  • prior:849ee29 important 1 — still-present — packages/adapter-utils/src/github-review-attestation.ts:346-348 — The candidate regex stops at whitespace after the captured token, so Reviewed head: <40-hex> extra is classified as well-formed and reachability is checked, but the consumer's strict attestation grammar rejects the line because it requires only delimiters/whitespace through end-of-line. The same mismatch remains for an unmatched delimiter followed by trailing prose.

Critical Issues (0)

Important Issues (1)

  • [native-codex; prior:849ee29 important 1] packages/adapter-utils/src/github-review-attestation.ts:346-348 — The loose candidate regex does not require the attestation line to end after the captured token. A body such as Reviewed head: <40-hex> extra is allowed through this producer guard, but the review consumer will not recognize it, so the submission can still be posted without a usable attestation.
    • Require the candidate line to contain only the optional Markdown delimiters and whitespace after the token, or capture trailing suffixes as malformed candidates. Add regression tests for trailing prose and unmatched delimiter plus trailing prose.

Suggestions (0)

Strengths

  • The guard is placed before the scrubbed subprocess is spawned, so rejected review submissions cannot reach GitHub.
  • Repository reachability is checked with the direct commit endpoint and indeterminate results fail closed.
  • The test suite covers the cross-repository contamination case, malformed token lengths, ambiguous attestations, stdin handling, and CLI pass-through behavior.

Recommended Action

  1. Fix the Important issue before merge.
  2. Preserve the current fail-closed behavior for malformed or indeterminate attestations.

… token (BLO-32844)

Addresses Ally's Important finding on #1744 at head 849ee29. The finding is
correct and the hole was in the same failure family the guard was written to
close, which is worth stating plainly.

The loose candidate regex captured the token after `Reviewed head:` but did not
require the line to end there. So `Reviewed head: <40-hex> and some prose`
yielded a perfectly good token, was classified `well-formed`, resolved fine
against the target repo, and was ALLOWED — while the consumer's
REVIEWED_HEAD_ATTESTATION_PATTERN requires optional delimiters and whitespace
followed by end-of-line, so it matches that line nowhere. The review would
attest no head and the gate would be permanently unsatisfiable: exactly the
fail-closed livelock this guard exists to refuse, waved through by the guard.

The loose/strict split was only half implemented. Detection was loose, but
validation checked the token rather than the line. Now the pattern captures the
rest of the line too and the trailer must match what the consumer tolerates —
an unbalanced emphasis run and trailing whitespace, nothing else. Malformed
now carries `detail` so the refusal says whether the token is not a SHA or the
line does not end after it. Both are refused locally, with no API call.

Verified by cross-checking against the real extractAllyReviewedHeadSha rather
than by re-reading its regex: the two now agree on all 8 forms exercised
(plain, trailing whitespace, backticked, bold SHA, underscored SHA, upper-case,
3-space indent, bolded label). That cross-check also corrected a test I had
written from assumption — a bolded *label* is rejected by the consumer as well,
because the emphasis run cannot span `**` plus the space before the SHA, so
refusing it is agreement rather than added strictness. Pinned with a test that
will fail if the consumer's grammar is ever widened to accept it, so the two
grammars are re-aligned deliberately instead of drifting (cf. BLO-32512).

Re-ran the live replay after the change: the real contaminated body at
mediamtx#33 still refuses, the same body against pim-multicast-gateway and the
genuine dbe4e10e review still pass, and trailing prose on a genuinely reachable
head is now refused where it previously would have been posted.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 10, 2026

Copy link
Copy Markdown
Author

Important issue fixed in c121fdec6

The finding is correct, and worth naming precisely: the hole was in the same failure family this guard was written to close, so the guard would have waved through the defect it exists to catch.

The loose/strict split was only half implemented. Detection was loose (good — that is what catches a malformed token the consumer reads as simply absent), but validation checked the token rather than the line. So a body attesting a valid, resolvable SHA followed by trailing prose was classified well-formed, passed reachability, and was allowed — while the consumer requires optional delimiters plus whitespace then end-of-line, so it matches that line nowhere, the review attests no head, and the gate is permanently unsatisfiable.

What changed: the candidate pattern now also captures the remainder of the line, and the trailer must match exactly what the consumer tolerates — an unbalanced emphasis run and trailing whitespace, nothing else. malformed carries a detail discriminator so the refusal states whether the token is not a SHA or the line does not end after it. Both are refused locally with no API call.

On verifying it rather than asserting it. I cross-checked against the real extractAllyReviewedHeadSha instead of re-reading its regex. The two now agree on all 8 forms exercised: plain, trailing whitespace, backticked, bold SHA, underscored SHA, upper-case, 3-space indent, and bolded label.

That cross-check also corrected a test I had written from assumption — a bolded label (** around the label itself) is rejected by the consumer too, because its emphasis run cannot span ** plus the space before the SHA. So refusing that form is agreement, not added strictness. I pinned it with a test that fails if the consumer's grammar is ever widened to accept it, so the two grammars get re-aligned deliberately rather than drifting apart (cf. BLO-32512, which tracks unifying them).

Regression tests added, both shapes you asked for plus the accepted-forms list: trailing prose after a valid SHA, unmatched closing delimiter followed by trailing prose, and a guard-level test asserting trailing content on a reachable head is refused without consulting the network — that last one is the case that previously would have been posted.

Re-ran the live replay after the change: the real contaminated body still refuses at mediamtx#33, the same body against pim-multicast-gateway#2864 and the genuine dbe4e10e review both still pass, and trailing prose on a genuinely reachable head is now refused. 57 tests pass, typecheck clean.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c121fde

Prior Findings Dispositioned (1)

  • prior:849ee29 important 1 — fixed — packages/adapter-utils/src/github-review-attestation.ts:355-367 — The candidate scanner now captures the complete remainder of the attestation line and validates it against ACCEPTED_ATTESTATION_TRAILER_PATTERN, so a 40-character SHA followed by prose or an unmatched delimiter is classified as malformed instead of being allowed through. Regression coverage is present at packages/adapter-utils/src/github-review-attestation.test.ts:210-225.

Critical Issues (0)

Important Issues (1)

  • [native-codex] packages/adapter-utils/src/github-review-attestation.ts:253-258gh api repos/{owner}/{repo}/pulls/{n}/reviews --method POST --input <file> is recognized as a review submission, but the input file is passed wholesale to inspectReviewAttestation rather than being decoded and scanned for its JSON body field. A normal JSON request body containing an attestation therefore has no Markdown line match, returns absent, and bypasses the repository reachability check.
    • Parse --input as the review request JSON and inspect its string body value (or reject unsupported input shapes). Add a regression test using a JSON file with event, commit_id, and an attestation-bearing body, asserting the target repository resolver is called and an unreachable SHA is refused.

Suggestions (0)

Strengths

  • The guard runs before scrubbing and before the real GitHub CLI is spawned, so refused submissions cannot reach GitHub.
  • Direct commit endpoint reachability is used, and indeterminate results fail closed.
  • The parser and attestation tests cover the real contamination and malformed-marker incidents, alternate flag spellings, fenced blocks, and prior-head reachability.

Recommended Action

  1. Fix the Important issue before merge.
  2. Preserve the current fail-closed behavior for malformed or indeterminate attestations.

…ning it (BLO-32844)

Addresses Ally's second Important finding on #1744, at head c121fde. Like the
first, it was the guard's own defect rather than a gap around it — and this one
re-opened the original fail-open hole in a second shape.

`gh api repos/{o}/{r}/pulls/{n}/reviews --method POST --input req.json` was
recognised as a review submission, but the file was handed to
inspectReviewAttestation as if it were Markdown. It is not: it is the whole
JSON request payload, and its newlines are `\n` escapes rather than real ones.
Every pattern here is line-anchored, so nothing matched, the attestation read
`absent`, and the reachability check was skipped entirely.

Reproduced before fixing: an APPROVE payload whose `body` attests
81d63ac9 (pim#2864's head) targeted at mediamtx#33 was ALLOWED with zero
reachability calls. That is the exact fail-open this guard exists to close,
reached through the JSON door.

`--input` now yields a distinct `json-request-file` body source, decoded before
scanning. A payload with no `body` member is allowed rather than refused —
`{"event":"APPROVE"}` is a valid review with no comment, so it attests nothing
and there is nothing to check. A file that is not a JSON object, or whose
`body` is present but not a string, is refused as `unparsable-request-body`:
the request would fail at GitHub anyway, so refusing costs nothing, while
guessing could pass an unverified attestation in an unanticipated shape.

Re-verified against the live API after the change: the cross-repo JSON payload
now refuses, the correct-attestation and no-body payloads are allowed, and the
Markdown `--body-file` path still refuses cross-repo. `gh api --input` against
a non-review endpoint is untouched, with a test pinning that.

Also corrected a defect in my own test helper found by these tests: overriding
`resolveCommitReachability` replaced the call recorder, so two assertions that
the resolver had been consulted were vacuously reading an empty array. The
verdict is now set via a `reachability` option that keeps the recorder, which
is what lets those tests assert the resolver actually ran — the property whose
absence was the bug being fixed here.

Runbook updated with the new refusal reason.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 10, 2026

Copy link
Copy Markdown
Author

Important issue fixed in 5e72e1a74

Correct again, and worth saying plainly: like the first finding, this was the guard's own defect rather than a gap around it — and this one re-opened the original fail-open hole in a second shape, which makes it the more serious of the two.

Reproduced before fixing. An APPROVE payload whose body attests 81d63ac9 (pim#2864's head), targeted at mediamtx#33 via --input, was ALLOWED with zero reachability calls. The cause is exactly as described: --input points at the whole JSON request payload, not Markdown, and its newlines are \n escapes rather than real ones. Every pattern here is line-anchored, so nothing matched, the attestation read absent, and the check was skipped.

What changed. --input now yields a distinct json-request-file body source that is decoded before scanning. The three outcomes are deliberate:

  • body is a string → decode and inspect it normally.
  • no body member → allowed. {"event":"APPROVE"} is a valid review with no comment, so it attests nothing and there is nothing to check. Refusing it would break legitimate approvals.
  • not a JSON object, or body present but not a string → refused as unparsable-request-body. The request would fail at GitHub anyway, so refusing costs nothing, whereas guessing could pass an unverified attestation in a shape nobody anticipated.

Tests, all of the shapes you named plus the boundary: cross-repo attestation inside a JSON body refused and asserting the resolver was actually called with (repo, sha); a resolvable JSON attestation allowed; a malformed marker inside a JSON body refused without any network call; a no-body payload allowed with the resolver not called; four unparsable shapes refused; and --input against a non-review endpoint left untouched, so this cannot start parsing every gh api --input call in the fleet as a review payload.

These tests also caught a defect in my own test helper, which is worth recording because it is the same class of error as the finding: overriding resolveCommitReachability replaced the call recorder, so two assertions that the resolver had been consulted were vacuously passing against an empty array. The verdict is now set through a reachability option that keeps the recorder — which is precisely what lets those tests assert the resolver actually ran, the property whose absence was this bug.

Re-verified against the live API after the change: the cross-repo JSON payload refuses, the correct-attestation and no-body payloads are allowed, and the Markdown --body-file path still refuses cross-repo. 64 tests pass, typecheck clean. Runbook updated with the new reason.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5e72e1a

Prior Findings Dispositioned (1)

  • prior:c121fde important 1 — fixed — packages/adapter-utils/src/github-review-attestation.ts:480-493--input review submissions are now decoded as JSON and their string body member is inspected before attestation validation; regression coverage at packages/adapter-utils/src/github-review-attestation.test.ts:624-638 verifies an unreachable cross-repository attestation is refused and the resolver is called.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The guard runs before scrubbing and before the target GitHub CLI is spawned, so refused reviews cannot reach GitHub.
  • Reachability checks use the target repository and fail closed for indeterminate results while allowing valid prior commits.
  • The parser distinguishes gh pr review, field-based API submissions, and JSON --input payloads, with focused regression coverage for malformed, ambiguous, fenced, and cross-repository attestations.
  • The operator runbook documents each refusal reason and explicitly avoids bypassing the guard.

Recommended Action

  1. No Critical or Important issues remain from the current diff or active prior findings.
  2. Merge when the repository's remaining required checks are satisfied.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 10, 2026
Merged via the queue into master with commit 5bd1651 Sep 11, 2026
38 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants