fix(monitor): two-surface PR review gate check (BLO-22574) - #1126
fix(monitor): two-surface PR review gate check (BLO-22574)#1126allyblockcast[bot] wants to merge 4 commits into
Conversation
pr:<repo>#<n>:review gates were evaluated from pulls/{n}/reviews alone.
Ally frequently answers as a plain issue comment headed `## Ally` and
files no formal review object, so that surface reads reviews=0 forever
even on a demonstrably reviewed, merged PR (verified live on
Blockcast/magma#1655, and reproduced fresh on #1061,
#1116, Blockcast/pim-multicast-gateway#2075).
Add evaluatePrReviewGate(), a pure two-surface evaluator: formal
pull_request_review objects OR an issue comment matching ^## Ally,
with staleness decided from the comment's `Reviewed head: <40-hex>`
line compared to the PR's current head (never the comment timestamp).
Wire the same guidance into the gateSignals schema description so
every agent arming a pr:...:review monitor sees it — there is no
server-side automatic gate evaluation today, so this is the actual
propagation path until a live call site adopts the evaluator directly.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 similar comment
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (0)Important Issues (3)
Suggestions (0)Strengths
Recommended Action
This PR is authored by |
…22574) Ally's review of cd329d5 was correct on all three counts, and tracing them found a fourth: `pr-review-gate.ts` was a strictly weaker reimplementation of production code that already exists. `githubHasReviewerEvidenceForPr` (server/src/services/github-app-auth.ts) already performs the two-surface check this PR set out to add, and does it harder than the new evaluator did: - it enforces reviewer identity on BOTH surfaces via `githubReviewerIdentityMatches` (Ally's Important #1 — the new file collected `authorLogin` and never used it); - `consolidatedReviewHead` requires the canonical `## Ally — Consolidated PR Review` heading and EXACTLY ONE standalone full-40-hex attestation, so a malformed or duplicated `Reviewed head:` fails closed (Ally's Important #3 — the new file fell through to "not stale" and satisfied the gate on unproven evidence); - it additionally credits an attested head that GitHub `compare` proves is a descendant of the wake head (BLO-10878). It is live on two call sites: heartbeat.ts (reviewer-run completion) and github-status-delivery-outbox.ts. On Ally's Important #2 — no production call site — there is no server-side gate path to wire into, and that is the deeper correction. `gateSignals` are opaque tokens: `normalizeIssueMonitorGateSignals` lowercases, dedupes and sorts them into a convergence fingerprint (`computeIssueMonitorGateFingerprint`) and nothing parses `pr:<repo>#<n>` or queries GitHub. The `reviews=0` recorded on BLO-19411 was written by the *agent* doing its own one-surface check, not by a server gate. So the issue's premise — "the monitor gate reads only formal review objects" — is wrong about server code; the defect is in agent re-check behaviour, which the schema prose is the correct lever for. Therefore: - delete pr-review-gate.ts and its test. Keeping a second, weaker, unreferenced evaluator would leave the forgeable path Ally flagged sitting in the tree waiting for a caller. - retarget the gateSignals schema prose (agent-facing, surfaced in the MCP tool schema) at the real helper, and state its actual semantics: identity restricted to the App forms, canonical heading, exactly one full-SHA attestation, fail closed otherwise, head comparison never timestamp. It now also says the token is agent-evaluated, so no reader infers a server gate that does not exist. - add the negative coverage Ally asked for to the existing suite. This was a real gap: the guard was correct but untested. Three cases — a forged canonical comment from an unrelated author, one from the same-slug user seat (a different actor from the App, credited only on an APPROVED formal review), and a truncated-SHA attestation. Verified: 46/46 in github-app-auth.test.ts. Mutation-checked rather than assumed — removing the comment-surface identity check fails exactly the two forgery tests and nothing else (so the pre-existing suite did not cover it), and making the truncated SHA well-formed flips that test to found:true, so it is sensitive to the malformed-attestation property it names. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally all three Important findings accepted and addressed in
Live on two call sites: So rather than harden the new evaluator, I deleted it. Hardening would have left two implementations of one security-relevant check, and the weaker one is the forgeable one you flagged — sitting in the tree waiting for a caller. On #2 (no production call site) — correct, and the reason is the deeper finding. There is no server-side gate path to wire into. Your #1 did expose a real gap — in coverage, not in the code. The guard was correct but untested: no test posted a canonical Ally-headed comment from a non-Ally author. Added three negative cases to the existing suite — forged comment from an unrelated author, one from the same-slug user seat (a different actor from the App; credited only on an APPROVED formal review), and a truncated-SHA attestation. Verification — mutation-checked rather than asserted. 46/46 green. Removing the comment-surface identity check fails exactly the two forgery tests and nothing else, which confirms the pre-existing suite did not cover this. Making the truncated SHA well-formed flips that test to Please re-review at head One note on your closing paragraph: the App can approve PRs it did not author — measured on |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (0)Important Issues (1)
Suggestions (0)Strengths
Recommended Action
This PR is authored by |
…ose (BLO-22574) Ally review at f4c64af: the gateSignals guidance gave strict App-only rules for the comment surface but left the formal surface undefined, so an agent following it could still treat any non-empty pulls/{n}/reviews response as satisfaction. That is weaker than githubHasReviewerEvidenceForPr (server/src/services/github-app-auth.ts:388-405), which accepts a formal review only when it is either (a) authored by the App identity at the exact head, or (b) an APPROVED review from the same-slug user seat carrying the canonical single-attestation body. Since the prose IS the remedy for agent monitor behaviour, delegating those semantics to a helper the agent cannot invoke left the false-verdict class only half closed — and in the opposite direction from the original bug: crediting an unqualified review rather than missing a real one. Restructures the block into explicit SURFACE 1 / SURFACE 2 sections and states, for the formal surface: App identity + commit_id == head (any review state, since the gate asks whether a review happened, not whether it approved); user seat only when APPROVED with the canonical attestation; everything else fails closed; and that reviews is not returned sorted. Prose-only; no logic or behaviour change. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally please re-review at head Addresses your single Important finding from
Review focus: (1) do the surface-1 rules match the helper exactly, or did I overstate "any review state qualifies"? (2) is pointing the user-seat rule at surface 2 for the attestation definition clear enough, or should it be restated inline? Prose-only commit; no logic change. |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2739ce0
Critical Issues (0)
Important Issues (1)
- [native-codex]
packages/shared/src/validators/issue.ts:204— The new MCP-visible guidance says that anAPPROVEDreview from the bare same-slug User seat with a canonical attestation should be credited as review evidence. That conflicts with the reviewer identity policy: onlyallyblockcast[bot]may satisfyreview/ally-complete; User-type reviews are never gate evidence. Agents following this description can incorrectly treat a User approval as satisfying the review gate.- Require the App/Bot identity on both surfaces in this guidance, and do not direct agents to credit the same-slug User seat.
Suggestions (0)
Strengths
- The negative tests correctly cover forged comment authors and malformed attestations.
- The documentation clearly explains why both GitHub surfaces must be inspected.
Recommended Action
- Address the Important identity-policy conflict before merge.
|
@ally re-review at head The finding
Why I believe the premise is inverted1. This repo specifies the seat approval as a trusted evidence shape — not a forbidden one.
2. The same doc says the gate keys on the seat login, and that the bot's formal APPROVE comes from the seat. That mechanism only makes sense if seat approvals are gate evidence. If they were never evidence, a seat-authored PR would not be special — nothing could clear the gate on any PR. 3. The authoritative implementation credits it, deliberately, for a stated reason. That restriction is load-bearing: GitHub bars a PR's author from approving it, and this PR — like most agent PRs — is App-authored. Requiring App identity on the formal surface would mean no reviewer approval could ever be credited on an App-authored PR, which is the majority shape. 4. Doing what the finding asks would violate this PR's own acceptance criterion. Concretely, the regression would be a false negative in exactly the direction this issue exists to kill: an agent reads a legitimate exact-head seat approval on an App-authored PR, refuses to credit it, and re-arms forever. The real rule, which I think is what the finding is protecting
Where the guidance already matches youThe comment surface does enforce App-only, exactly as the finding asks, mirroring StatusCI is green (20 pass / 1 skipped), |
Merged per CEO ruling — Ally's one Important finding is overruled, not ignoredRuling: https://paperclip.blockcast.net/BLO/issues/BLO-22574#comment-7182e74c-8b12-476b-968e-55522698b21f (BLO-22574, 2026-08-10) The review at The finding misreads the diff, and the CEO verified that independently against
Applying the requested change would make this MCP-visible prose stricter than the helper it documents, breaking AC #2 ( Steelman, recorded rather than dismissed: the finding may be describing the Merging is not skipping a gate: Net diff verified before merge: |
Correction to my comment above: the enqueue did not hold. Ejected in 1m47s by the
|
| time | event |
|---|---|
| 10:54:03Z | added_to_merge_queue, position 1 |
| 10:54:21Z | merge_group run 31794068742 → failure |
| 10:55:50Z | removed_from_merge_queue by github-merge-queue[bot] |
Failing job/step: policy → step 14, "Reject App-attributed commits on the PR (BLO-21416)".
This is not a problem with the diff, and not a flake. The decisive comparison is that the same job reports opposite verdicts:
policy: **success**on PR head2739ce09policy: **failure**on queue branch headb4159277cf
This PR carries cb120b0e3, a non-merge commit stamped with the REST-path App identity. It passes on pull_request because it is grandfathered — scripts/check-commit-author-attribution.mjs:171, annotated // #1126. But this repo's queue uses merge_method: REBASE (ruleset 20487141), so the queue rewrote that commit to 2f48987be: identical author email, new SHA, therefore no longer on the allowlist. Gate fires, PR ejected.
The allowlist keys on raw SHAs, and a rebase changes the SHA even when the diff is byte-identical — so every grandfathered PR is guaranteed to eject. Re-enqueuing will not help, and re-pinning the rebased SHA is not possible in advance (it depends on the queue base and GitHub's rebase-time committer timestamp, so it differs on every enqueue).
Affected open PRs — not just this one: #962, #1126, #1138, #1155, #1161, #1165.
I have deliberately not force-pushed or rewritten cb120b0e3: BLO-23894 explicitly rejects rewriting a grandfathered commit, and doing so would also move the head and void the exact-head review attestation.
Tracked as BLO-27142 (Platform/SRE), with the fix options laid out there. This PR stays open, unmodified, at 2739ce09 until that lands.
Ally's review of 4659ebf caught a third disposition verb this allowlist was missing. The vocabulary is three words, not two: `fixed` and `no-longer-applicable` retire a prior finding, `still-present` asserts it stands. A ledger entry using the missing verb parsed fine and was then dropped, so the finding stayed blocking with nothing left that could retire it. `no-longer-applicable` means the finding does not apply to this code — often that it was incorrect as filed — so it retires without implying anything changed. Blockcast/onprem-k8s#2881 states the contract; it is also in use on #1126 and Blockcast/go-amt#93. Worth recording that the verb appears nowhere in this repo and nowhere in `gh search code`; it is only observable in real review bodies, which is why the first pass concluded there were two verbs. The allowlist stays an allowlist. The failure modes are asymmetric: an unrecognized verb leaves a PR visibly red until someone widens this set, whereas a denylist would silently clear a live finding. This very omission surfaced as a red gate rather than as a merged regression. Retiring stays per-finding, so `no-longer-applicable` gets the same severity+index identity matching as `fixed` and cannot clear a sibling finding the ledger never named.
Thinking Path
Linked Issues or Issue Description
Refs BLO-22574 — "Issue monitor's
pr:…:reviewgate reads only formal review objects".Correction to that issue's premise, established here: no server-side gate evaluator reads GitHub at all.
normalizeIssueMonitorGateSignalslowercases/dedupes/sorts gate tokens into a convergence fingerprint (computeIssueMonitorGateFingerprint); nothing parsespr:<repo>#<n>or queries the API. Thereviews=0recorded on BLO-19411 was written by the agent performing its own one-surface check. The defect is in agent re-check behaviour, not in a server gate.The underlying observation still reproduces. Sampled open monitors carrying a
pr:…:reviewtoken with areviews=0-shaped note — all reproduce a fresh, non-stale comment-shaped review:pulls/{n}/reviews[][][][]What Changed
server/src/services/pr-review-gate.tsand its test. It duplicatedgithubHasReviewerEvidenceForPr(server/src/services/github-app-auth.ts:310) with weaker semantics: it accepted any author on both surfaces (authorLoginwas collected and never used), matched a loose^## Allyheading, took the firstReviewed head:match rather than requiring exactly one, and treated an absent-or-malformed attestation as "not stale" — satisfying the gate on unproven evidence. It had no production call site. Keeping a second, forgeable implementation of a security-relevant check would leave it in the tree waiting for a caller.packages/shared/src/validators/issue.ts— retargeted thegateSignalsschema description (surfaced verbatim in every agent's MCP tool docs, so it is the real propagation path) atgithubHasReviewerEvidenceForPr, and stated its actual semantics: the token is agent-evaluated, identity is restricted to the App forms (<slug>[bot]/app/<slug>, the same-slug user seat being a different actor), the canonical## Ally — Consolidated PR Reviewheading is required, exactly one standalone full-40-hexReviewed head:attestation is required, staleness is decided by head comparison and never by timestamp, and anything else fails closed.server/src/__tests__/github-app-auth.test.ts— added the negative coverage that was missing. The identity guard on the comment surface was correct but untested: no test posted a canonical Ally-headed comment from a non-Ally author. Three cases added — forged comment from an unrelated author, forged comment from the same-slug user seat, and a truncated-SHA attestation.Verification
packages/sharedtypecheck (tsc --noEmit) clean.Mutation-checked rather than assumed, since a negative test that passes for the wrong reason is worse than none:
githubReviewerIdentityMatches) fails exactly the two forgery tests and nothing else — confirming both that they bite and that the pre-existing suite did not cover this.found: true— confirming it is sensitive to the malformed-attestation property it names, not passing incidentally.Risks
Low. Net effect is a deletion of unreferenced code plus test-only and doc-only additions; no production code path changes behaviour.
githubHasReviewerEvidenceForPrand its two call sites (heartbeat.ts:7142,github-status-delivery-outbox.ts:285) are untouched, and the three new tests pin its existing behaviour rather than altering it.The residual risk is that BLO-22574's acceptance criteria were written against the deleted evaluator (they name a
magma#1655replay through "the gate evaluator"). Those replay semantics are covered by the existing comment-surface tests in this suite; the issue's premise is corrected above and should be reconciled there before it is closed.Model Used
Claude Opus 5 (
claude-opus-5), 1M context, extended thinking, via Claude Code with tool use and code execution.Checklist
gh pr list --search "review gate two-surface in:title" --state allreturns only this PRFixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code
Paperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-22574