fix(review): harden #211 decision lock and CLI gate per review blockers
- Bind review decision state to in-process session (pid + profile lock); drop /tmp file path. - Fail closed on review_pr.py CLI; route live reviews through gated MCP tools only. - Require operator_authorized on review correction; allow re-mark after correction. - Validate remote/org/repo on mark and submit; wire review mutation proof into build_final_report. - Add security regression tests for spoofed locks, correction flow, and CLI bypass. Refs #211
This commit is contained in:
+16
-85
@@ -7,16 +7,16 @@ making any API call. Merge is handled solely by the gated `gitea_merge_pr` MCP
|
||||
workflow (#16), which enforces identity/profile/eligibility, explicit
|
||||
confirmation, expected head SHA checking, and self-merge protection.
|
||||
|
||||
Usage (review only):
|
||||
Live review submission is also disabled (#211): use the gated
|
||||
``gitea_submit_pr_review`` MCP workflow, which enforces validation-phase
|
||||
dry-run, final decision marking, and single-terminal review mutation rules.
|
||||
|
||||
Usage (review only — disabled):
|
||||
review_pr.py --pr-number 12 --event APPROVE --body "Approved and signed off"
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Auto-execute using the project's local virtual environment Python
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -24,18 +24,10 @@ venv_python = os.path.join(PROJECT_ROOT, "venv", "bin", "python3")
|
||||
if os.path.exists(venv_python) and sys.executable != venv_python:
|
||||
os.execv(venv_python, [venv_python] + sys.argv)
|
||||
|
||||
from gitea_auth import get_auth_header, resolve_remote, add_remote_args, api_request, repo_api_url, get_profile
|
||||
from gitea_auth import add_remote_args
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if os.environ.get("GITEA_SESSION_PROFILE_LOCK"):
|
||||
print(
|
||||
"Direct CLI review submission is disabled within MCP sessions. "
|
||||
"Use MCP tools instead.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
parser = argparse.ArgumentParser(description="Review and sign-off on a Gitea pull request.")
|
||||
add_remote_args(parser)
|
||||
parser.add_argument("--pr-number", type=int, required=True, help="PR number/index to review.")
|
||||
@@ -50,87 +42,26 @@ def main(argv=None):
|
||||
help="Ignored — CLI merge is disabled (see --merge).")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Fail closed: direct CLI merge is disabled (#16). LLM automations were
|
||||
# using this flag as an ungated merge bypass. Merge is only available via
|
||||
# the gated `gitea_merge_pr` MCP workflow, which enforces
|
||||
# identity/profile/eligibility, explicit confirmation, expected head SHA,
|
||||
# and self-merge protection. No API call is made here.
|
||||
if args.merge:
|
||||
print(
|
||||
"Direct CLI merge is disabled. Merge is only available through the "
|
||||
"gated #16 workflow (MCP tool 'gitea_merge_pr'), which enforces "
|
||||
"identity/profile/eligibility, explicit confirmation, expected head "
|
||||
"SHA checking, and self-merge protection. Re-run without --merge to "
|
||||
"submit a review only.",
|
||||
"see the review-submission guard message.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
host, org, repo = resolve_remote(args)
|
||||
|
||||
# ── Reviewer mutation side-channel wall (#199, refs #194) ──
|
||||
# The launching MCP session exports GITEA_SESSION_PROFILE_LOCK with the
|
||||
# profile it was started with; child processes inherit it. If this CLI
|
||||
# resolves a different profile — e.g. an ad-hoc GITEA_MCP_PROFILE
|
||||
# override escalating an author-bound session to reviewer — refuse
|
||||
# before any API call. No lock in the environment means no session
|
||||
# context (direct operator CLI use), which stays allowed. Unlike a /tmp
|
||||
# lock file, the environment is per-process-tree: other sessions cannot
|
||||
# spoof it and it cannot go stale across sessions.
|
||||
session_lock = (os.environ.get("GITEA_SESSION_PROFILE_LOCK") or "").strip()
|
||||
if session_lock:
|
||||
print(
|
||||
"Direct CLI review submission is disabled within MCP sessions. "
|
||||
"Use the gated 'gitea_submit_pr_review' MCP tool instead.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
body = args.body
|
||||
if args.body_file:
|
||||
if args.body_file == "-":
|
||||
body = sys.stdin.read()
|
||||
else:
|
||||
with open(args.body_file, "r", encoding="utf-8") as fh:
|
||||
body = fh.read()
|
||||
|
||||
auth = get_auth_header(host)
|
||||
if not auth:
|
||||
print(f"Could not get credentials or token for {host}.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 1. Fetch PR to get the latest head commit SHA (required for review validation)
|
||||
pr_url = f"{repo_api_url(host, org, repo)}/pulls/{args.pr_number}"
|
||||
try:
|
||||
pr_data = api_request("GET", pr_url, auth)
|
||||
except Exception as e:
|
||||
print(f"Error fetching PR #{args.pr_number}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
commit_sha = pr_data.get("head", {}).get("sha")
|
||||
if not commit_sha:
|
||||
print(f"Could not find head commit SHA for PR #{args.pr_number}.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 2. Submit the PR review
|
||||
review_url = f"{repo_api_url(host, org, repo)}/pulls/{args.pr_number}/reviews"
|
||||
payload = {
|
||||
"body": body,
|
||||
"event": args.event,
|
||||
"commit_id": commit_sha
|
||||
}
|
||||
|
||||
try:
|
||||
api_request("POST", review_url, auth, payload)
|
||||
print(f"Successfully submitted review for PR #{args.pr_number}: event={args.event}")
|
||||
except Exception as e:
|
||||
print(f"Error submitting review: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Merge is intentionally not performed here — see the fail-closed guard
|
||||
# above. Use the gated `gitea_merge_pr` MCP workflow (#16) to merge.
|
||||
return 0
|
||||
print(
|
||||
"Direct CLI review submission is disabled (#211). Use the gated "
|
||||
"'gitea_submit_pr_review' MCP workflow, which enforces validation-phase "
|
||||
"dry-run, gitea_mark_final_review_decision, and single-terminal review "
|
||||
"mutation rules.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user