mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
Two independent blind reviews of #3698 (one APPROVE, one REJECT). Every confirmed finding from the reject side, plus the cheap hardening both reviews flagged. BLOCKING 1. Sticky-comment hijack. upsertStickyComment adopted ANY comment containing the marker, so a contributor could pre-post `<!-- gbrain-pr-gate -->`, have the gate PATCH it, then edit it into a fake green verdict. isOwnComment() now requires user.type === 'Bot' AND login === 'github-actions[bot]' AND the body to START WITH the marker; anything else gets a fresh comment. 2. LLM output injected as raw Markdown. Model-produced reasons[] and reviewer_checklist[] reached the comment unescaped — PR-body-driven injection could forge headings, a second marker, and live @mentions. sanitizeModelText()/sanitizeList() are now the single choke point in renderComment(): HTML comments stripped, mentions zero-width-broken, leading block markers removed, newlines collapsed, 300 chars per string, 8 entries per list, both caps self-marking. 3. Lane was purely model-decided. A well-written feature pitch could talk itself into merge-lane. The model now RECOMMENDS; applyMechanicalDowngrades forces merge-lane -> needs-maintainer on any of: workflow edits, a new package.json dependency, a new src/core/ai/recipes/ provider file, new KNOWN_CONFIG_KEYS entries, >40 changed files, >400 net source lines outside test/, or a src/ change with no test file touched (#3665). The sticky comment reports them under "Mechanical downgrades applied". 4. The test file did not pin what it claimed. Added: no gh pr checkout / git fetch / refs/pull / pull/*/head in any spelling; exact permissions key->value map plus a single-permissions-block assertion so no job-level grant re-widens contents; the ${{ }}-in-run scanner now covers folded (`run: >`) and chomped blocks, with a guard-the-guard test; and mocked end-to-end runGate() runs for close-lane exit 1, marker hijack, sanitizer, truncation, refusal routing, NEUTRAL label clearing, label swap, and the spend guard. 5. Version-first title regex rejected the documented suffix form. `v0.31.1.1-fixwave fix: ...` now passes. VERSION_AT_END_RE no longer false-positives on `chore: bump zod (3.25.76)`: it fires only on a v-prefixed or 4-segment trailing version, i.e. this project's own shape. 6. Refusal fail-open. stop_reason=refusal exhausted retries into a green NEUTRAL — a deterministic way to dodge the red X. Refusal and schema-invalid output now route to needs-maintainer with an explicit note; only transport failure stays NEUTRAL. Refusal also short-circuits the retry loop, since retrying a deterministic refusal only burns spend. ALSO 7. persist-credentials: false on the checkout step. 8. Dropped pull-requests:write. Everything the script calls is the issues API (comments, label create, label add/remove), so issues:write is the only grant that is actually needed. 9. Spend guard for the edited/synchronize amplification: the LLM call is skipped when sha256(title+body+head_sha) matches the hash recorded in the previous sticky comment's state block, and the stored lane's exit code is reused. 10. NEUTRAL runs now clear every gate:* label instead of leaving a stale verdict behind. Verified: bun test test/pr-gate-workflow.test.ts 63 pass / 0 fail, bun run typecheck clean, actionlint clean, check-privacy / check-no-tracked-symlinks / check-progress-to-stdout / check-bun-test-timeout clean, plus a live Anthropic smoke of the exact request shape (HTTP 200, valid strict JSON, injection attempt rejected). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>