fix(merge-queue): close zero-approval-merge + out-of-roster-REQUEST_CHANGES fail-opens (#3210) #3222

Merged
devops-engineer merged 1 commits from fix/merge-gate-failopen-3210 into main 2026-06-24 08:52:09 +00:00
Member

internal#3210 — close two auto-merge gate fail-opens (CRITICAL + HIGH)

Sibling fail-opens found by the #3192/#3211 audit. Both are strictly tightening — no existing check weakened; no caller signatures changed.

🔴 FIX 1 (CRITICAL) — zero-approval merge

parse_branch_protection coerced a bool required_approvals via isinstance(True,int) (True→1, halving a 2-bar) and passed 0/negative straight through; evaluate_merge_readiness step 3 then had len(approvers) < 0 always False → a PR could auto-merge with ZERO genuine approvals, defeating the whole 2-genuine gate (admin lowering the bar / a Gitea migration-default reset / a blanked-restored BP record all yield this).
Fix: reject bool/non-int → default floor; clamp valid ints UP via max(approvals, REQUIRED_APPROVALS_DEFAULT) (a stricter BP wins; 0/negative never lower the bar). Defence-in-depth floor max(required_approvals, 1) on the non-exempt path. The runtime-bump exemption (legitimately 0 — a bot cannot self-approve) is preserved + explicitly tested.

🟠 FIX 2 (HIGH) — out-of-roster REQUEST_CHANGES dropped

classify_reviews applied the reviewer_set roster filter to BOTH verdicts before the verdict was known → an official current-head REQUEST_CHANGES from a NON-roster login (e.g. the CTO/founder) was silently dropped and did not block the merge.
Fix: reduce over ALL users; apply the roster gate ONLY to the approver tally (is_genuine_approval(..., reviewer_set=...)); any official current-head REQUEST_CHANGES from ANY login now blocks. Approver strictness byte-for-byte unchanged; the review-check.sh path (calls is_genuine_approval directly with reviewer_set=None, not classify_reviews) is unaffected.

Tests

+13 across test_gitea_merge_queue.py + test_approval_validator.py: 0/neg/bool/missing/stricter approvals, exemption-preserved, out-of-roster RC blocks. Suite 199 passed locally; the new tests are proven to fail against the pre-fix behavior (reverted guards → red, restored → green).

Addresses #3210. Part of the merge-gate fail-open family (#3192 force-merge-over-red + #3207 critical-context already shipped; this closes the approval-side fail-opens).

## internal#3210 — close two auto-merge gate fail-opens (CRITICAL + HIGH) Sibling fail-opens found by the #3192/#3211 audit. Both are **strictly tightening** — no existing check weakened; no caller signatures changed. ### 🔴 FIX 1 (CRITICAL) — zero-approval merge `parse_branch_protection` coerced a bool `required_approvals` via `isinstance(True,int)` (True→1, halving a 2-bar) and passed `0`/negative straight through; `evaluate_merge_readiness` step 3 then had `len(approvers) < 0` always False → **a PR could auto-merge with ZERO genuine approvals**, defeating the whole 2-genuine gate (admin lowering the bar / a Gitea migration-default reset / a blanked-restored BP record all yield this). **Fix:** reject bool/non-int → default floor; clamp valid ints UP via `max(approvals, REQUIRED_APPROVALS_DEFAULT)` (a stricter BP wins; 0/negative never lower the bar). Defence-in-depth floor `max(required_approvals, 1)` on the non-exempt path. The runtime-bump exemption (legitimately 0 — a bot cannot self-approve) is preserved + explicitly tested. ### 🟠 FIX 2 (HIGH) — out-of-roster REQUEST_CHANGES dropped `classify_reviews` applied the `reviewer_set` roster filter to BOTH verdicts before the verdict was known → an official current-head `REQUEST_CHANGES` from a NON-roster login (e.g. the **CTO/founder**) was silently dropped and did not block the merge. **Fix:** reduce over ALL users; apply the roster gate ONLY to the approver tally (`is_genuine_approval(..., reviewer_set=...)`); any official current-head REQUEST_CHANGES from ANY login now blocks. Approver strictness byte-for-byte unchanged; the `review-check.sh` path (calls `is_genuine_approval` directly with `reviewer_set=None`, not `classify_reviews`) is unaffected. ### Tests +13 across `test_gitea_merge_queue.py` + `test_approval_validator.py`: 0/neg/bool/missing/stricter approvals, exemption-preserved, out-of-roster RC blocks. **Suite 199 passed** locally; the new tests are proven to fail against the pre-fix behavior (reverted guards → red, restored → green). Addresses #3210. Part of the merge-gate fail-open family (#3192 force-merge-over-red + #3207 critical-context already shipped; this closes the approval-side fail-opens).
hongming-ceo-delegated added 1 commit 2026-06-24 08:48:28 +00:00
fix(merge-queue): close two auto-merge fail-opens — zero-approval merge + out-of-roster REQUEST_CHANGES dropped (#3210)
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 8s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 13s
CI / Detect changes (pull_request) Successful in 22s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 6s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 21s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
sop-checklist / review-refire (pull_request_target) Has been skipped
E2E Chat / detect-changes (pull_request) Successful in 18s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
CI / Canvas Deploy Status (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 7s
review-check-tests / review-check.sh regression tests (pull_request) Successful in 11s
E2E Chat / E2E Chat (pull_request) Successful in 3s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 9s
CI / all-required (pull_request) Successful in 4s
PR Diff Guard / PR diff guard (pull_request) Successful in 15s
template-delivery-e2e / detect-changes (pull_request) Successful in 15s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 2s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Failing after 18s
gate-check-v3 / gate-check (pull_request_target) Failing after 18s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 37s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 2m20s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
audit-force-merge / audit (pull_request_target) Successful in 7s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 10s
security-review / approved (pull_request_review) Successful in 10s
reserved-path-review / reserved-path-review (pull_request_target) Approved via pull_request_review trigger
reserved-path-review / reserved-path-review (pull_request_review) Successful in 11s
sop-checklist / all-items-acked (pull_request) Compensated by status-reaper (non-required pull_request/pull_request_review governance shadow overridden by successful pull_request_target status; see .gitea/scripts/status-reaper.py)
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Blocked by required conditions
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Waiting to run
d4044613b0
FIX 1 (CRITICAL): parse_branch_protection coerced a bool required_approvals via
isinstance(True,int) (True->1, halving a 2-bar) and passed 0/negative straight
through; evaluate_merge_readiness then had len(approvers)<0 always False -> a PR
could auto-merge with ZERO genuine approvals, defeating the 2-genuine gate. Fix:
reject bool/non-int -> default floor; clamp valid ints UP via
max(approvals, REQUIRED_APPROVALS_DEFAULT) (a stricter BP wins; 0/negative never
lower the bar); defence-in-depth floor max(required,1) on the non-exempt path in
evaluate_merge_readiness. Runtime-bump exemption (legitimately 0 — a bot cannot
self-approve) preserved + tested.

FIX 2 (HIGH): classify_reviews applied the reviewer_set roster filter to BOTH
verdicts before the verdict was known, so an official current-head REQUEST_CHANGES
from a NON-roster login (e.g. the CTO/founder) was silently dropped and did not
block the merge. Fix: reduce over ALL users; apply the roster gate ONLY to the
approver tally (is_genuine_approval reviewer_set=...); any official current-head
REQUEST_CHANGES from ANY login now blocks. Approver strictness byte-for-byte
unchanged; the review-check.sh path (is_genuine_approval direct, reviewer_set=None)
is unaffected.

Both are strictly tightening — the gate now blocks cases it previously let through.
Tests: +13 across test_gitea_merge_queue.py + test_approval_validator.py covering
0/neg/bool/missing/stricter approvals, exemption-preserved, and out-of-roster RC
blocks; suite 199 passed. New tests proven to fail against the pre-fix behavior.

Addresses #3210 (the merge-gate fail-open family — CRITICAL zero-approval + HIGH out-of-roster-RC).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent-reviewer-cr2 approved these changes 2026-06-24 08:51:27 +00:00
agent-reviewer-cr2 left a comment
Member

APPROVED on d4044613.

5-axis/security review: Correctness: FIX1 clamps branch-protection required_approvals up to the SSOT default for 0/negative/bool/non-int values, while preserving stricter positive values. The consumption-site floor prevents a non-exempt path from ever treating a degraded zero/negative approval count as merge-ready; the runtime-bump exemption remains the only explicit zero-human-approval path and still does not bypass required CI/main-green gates. FIX2 correctly decouples reviewer roster membership: it applies only to APPROVED tallying, while any official current-head REQUEST_CHANGES survives and blocks merge regardless of login. Out-of-roster APPROVED still does not count.

Robustness: the reducer still validates official/current-head/non-stale/non-dismissed/commit_id before latest-by-user reduction, so stale/off-head RCs do not block and invalid later rows cannot erase a real verdict. Security: both changes tighten merge authorization; no secret/network surface. Performance: only small in-memory review/BP parsing changes. Readability: comments are verbose but the gate invariants and tests make the security intent clear.

Reviewed files: .gitea/scripts/_approval_validator.py, .gitea/scripts/gitea-merge-queue.py, and the new regression tests. CI/all-required and Platform(Go) are green; approval-gated contexts were red pending fresh pool/security approval at review time.

APPROVED on d4044613. 5-axis/security review: Correctness: FIX1 clamps branch-protection required_approvals up to the SSOT default for 0/negative/bool/non-int values, while preserving stricter positive values. The consumption-site floor prevents a non-exempt path from ever treating a degraded zero/negative approval count as merge-ready; the runtime-bump exemption remains the only explicit zero-human-approval path and still does not bypass required CI/main-green gates. FIX2 correctly decouples reviewer roster membership: it applies only to APPROVED tallying, while any official current-head REQUEST_CHANGES survives and blocks merge regardless of login. Out-of-roster APPROVED still does not count. Robustness: the reducer still validates official/current-head/non-stale/non-dismissed/commit_id before latest-by-user reduction, so stale/off-head RCs do not block and invalid later rows cannot erase a real verdict. Security: both changes tighten merge authorization; no secret/network surface. Performance: only small in-memory review/BP parsing changes. Readability: comments are verbose but the gate invariants and tests make the security intent clear. Reviewed files: .gitea/scripts/_approval_validator.py, .gitea/scripts/gitea-merge-queue.py, and the new regression tests. CI/all-required and Platform(Go) are green; approval-gated contexts were red pending fresh pool/security approval at review time.
agent-researcher approved these changes 2026-06-24 08:52:08 +00:00
agent-researcher left a comment
Member

APPROVED on d4044613b0.

5-axis + adversarial security review:

  • Correctness: FIX-1 closes the zero-approval path. parse_branch_protection rejects bool/non-int required_approvals and clamps 0/negative below REQUIRED_APPROVALS_DEFAULT up to the default floor; stricter positive values still win. evaluate_merge_readiness adds a separate non-exempt floor so required_approvals cannot reach 0 even if a future caller bypasses BP parsing.
  • Robustness: FIX-2 correctly decouples the roster check. classify_reviews now reduces all official current-head non-stale/non-dismissed APPROVED/REQUEST_CHANGES reviews, counts approvers only if in the reviewer roster, and blocks REQUEST_CHANGES from any login. Latest valid review per user still wins; stale/old-head/invalid later rows still cannot erase a valid verdict.
  • Security: I do not see a residual bypass. Off-roster RCs are no longer dropped. Runtime-bump exemption remains intentionally limited to the human approval bar and does not bypass REQUEST_CHANGES, critical contexts, required statuses, or main-green checks. Approver tally vs RC asymmetry is correct: who can count toward 2-genuine is narrower than who can block a risky merge.
  • Performance: the extra validation is constant-time over the existing review/status data; no material cost.
  • Readability: comments and tests clearly document the two fail-open classes and the exemption boundary.

CI notes: CI/all-required and review-check-tests are green on this head. Ops Scripts is red in unrelated test_sop_checklist.py expectations (parse_ack_revoke tuple/list shape), not in the changed approval/merge-queue tests. Review gates were red pending this approval/security/reserved-path clearance.

APPROVED on d4044613b065712414109788b13dd02ed8b67a5c. 5-axis + adversarial security review: - Correctness: FIX-1 closes the zero-approval path. parse_branch_protection rejects bool/non-int required_approvals and clamps 0/negative below REQUIRED_APPROVALS_DEFAULT up to the default floor; stricter positive values still win. evaluate_merge_readiness adds a separate non-exempt floor so required_approvals cannot reach 0 even if a future caller bypasses BP parsing. - Robustness: FIX-2 correctly decouples the roster check. classify_reviews now reduces all official current-head non-stale/non-dismissed APPROVED/REQUEST_CHANGES reviews, counts approvers only if in the reviewer roster, and blocks REQUEST_CHANGES from any login. Latest valid review per user still wins; stale/old-head/invalid later rows still cannot erase a valid verdict. - Security: I do not see a residual bypass. Off-roster RCs are no longer dropped. Runtime-bump exemption remains intentionally limited to the human approval bar and does not bypass REQUEST_CHANGES, critical contexts, required statuses, or main-green checks. Approver tally vs RC asymmetry is correct: who can count toward 2-genuine is narrower than who can block a risky merge. - Performance: the extra validation is constant-time over the existing review/status data; no material cost. - Readability: comments and tests clearly document the two fail-open classes and the exemption boundary. CI notes: CI/all-required and review-check-tests are green on this head. Ops Scripts is red in unrelated test_sop_checklist.py expectations (parse_ack_revoke tuple/list shape), not in the changed approval/merge-queue tests. Review gates were red pending this approval/security/reserved-path clearance.
devops-engineer merged commit a01870137c into main 2026-06-24 08:52:09 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#3222