From 9d5b0d67bc000c1a3e1e48e53840c3cedd688d63 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:19:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9E=84=E5=BB=BA=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=91=8A=E8=AD=A6=E5=B9=B6=E5=88=A0=E9=99=A4=E6=97=A7?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Codex --- docs/ARCHITECTURE.md | 2 - docs/qsl_qrt_dead_contract_inventory.zh-CN.md | 2 +- python/scripts/gate_codex_app_review.py | 320 ------------------ python/tests/test_gate_codex_app_review.py | 36 -- .../frontend/package-lock.json | 6 +- 5 files changed, 4 insertions(+), 362 deletions(-) delete mode 100644 python/scripts/gate_codex_app_review.py delete mode 100644 python/tests/test_gate_codex_app_review.py diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 06a7a766..58537a97 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -65,7 +65,6 @@ platform-config.json (single source of truth) │ │ ├── build_runtime_switch.py # Build transient runtime targets │ │ ├── runtime_settings.py # Core validation & assignment engine │ │ ├── check_internal_dependency_matrix.py -│ │ └── gate_codex_app_review.py # PR merge gate │ ├── tests/ # Python unit tests │ │ ├── test_runtime_settings.py │ │ └── test_internal_dependency_matrix.py @@ -98,7 +97,6 @@ platform-config.json (single source of truth) ├── deploy-strategy-switch-console.yml ├── manual-strategy-switch.yml ├── codex_pr_review.yml # Reusable caller to AIAuditBridge - └── codex_review_gate.yml ``` --- diff --git a/docs/qsl_qrt_dead_contract_inventory.zh-CN.md b/docs/qsl_qrt_dead_contract_inventory.zh-CN.md index c99b6cac..79e2300a 100644 --- a/docs/qsl_qrt_dead_contract_inventory.zh-CN.md +++ b/docs/qsl_qrt_dead_contract_inventory.zh-CN.md @@ -92,7 +92,7 @@ | `reconciliation_record_contract.py` | LIKELY_UNUSED(CI 层面) | import `activation_contract.py`/`deployment_bundle_contract.py`;未见 CI 直接调用;有单测。 | | `deterministic_risk_gate.py` | LIKELY_UNUSED(CI 层面) | 被 `paper_risk_admission_receipt.py` import;被 `docs/qsl_deterministic_risk_gate_kernel_v1.zh-CN.md`、`docs/QSL_P0_P6_CURRENT_STATE_AND_DRIVER_POLICY.zh-CN.md` 描述;未见 CI 调用。 | | `paper_risk_admission_receipt.py` | LIKELY_UNUSED(CI 层面) | 未见 CI 调用;有单测 `test_paper_risk_admission_receipt.py`。 | -| `gate_codex_app_review.py` | LIKELY_UNUSED(CI 层面) | 仅出现在 `docs/ARCHITECTURE.md` 目录树注释与单测 `test_gate_codex_app_review.py` 中;未见任何 `.github/workflows/*.yml` 调用。 | +| `gate_codex_app_review.py` | REMOVED(2026-10-07) | 核对当前 workflow 和调用方后删除旧脚本及专用单测;现有 CI 检查继续保留。 | | `run_codex_pr_review.py` | SUPERSEDED(自述) | 文件自身 docstring 明确:`"Deprecated compatibility entrypoint ... This repository now delegates PR review to QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml. The local runner is intentionally kept as a tiny stub"`。**这是有意保留的废弃兼容 stub,不是意外死代码**,无需处理。 | | `execution_evidence_projection.py` | ACTIVE(跨仓 Action) | 被 `actions/publish-runtime-execution-evidence/action.yml` 调用;该 Action 未见被本仓库任何 workflow 的 `uses:` 引用,推测为供其他仓库通过可复用 Action 消费,本仓库内无直接调用点。 | diff --git a/python/scripts/gate_codex_app_review.py b/python/scripts/gate_codex_app_review.py deleted file mode 100644 index 84e80da5..00000000 --- a/python/scripts/gate_codex_app_review.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -"""PR merge gate: static scan + Codex App review → job exit code = check status. - -Two phases, zero API keys needed: - 1. STATIC — scan diff for secrets, blocked files, metadata issues (<30s). - Fail job immediately on hard violations. - 2. WAIT — poll for Codex GitHub App review up to N min. - Fail job on CHANGES_REQUESTED, pass on APPROVED/timeout. - 3. REACT — on Codex bot review submitted: update instantly. - -The workflow job IS the check — exit 0 = pass, exit 1 = fail. -""" - -from __future__ import annotations - -import json -import os -import re -import sys -import time -import urllib.error -import urllib.request -from pathlib import Path -from typing import Any - -API_BASE = "https://api.github.com" -BOT_LOGIN = "chatgpt-codex-connector[bot]" -POLICY_PATH = Path(".github/codex_auto_merge_policy.json") - - -def env(name: str, default: str = "") -> str: - return os.environ.get(name, default).strip() - - -def env_int(name: str, default: int) -> int: - try: - return int(env(name, str(default))) - except ValueError: - return default - - -def github_request(token: str, method: str, path: str, payload: dict[str, Any] | None = None) -> Any: - url = f"{API_BASE}{path}" if not path.startswith("https://") else path - data = json.dumps(payload).encode() if payload else None - headers = { - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "codex-review-gate", - } - if payload: - headers["Content-Type"] = "application/json" - req = urllib.request.Request(url, data=data, method=method, headers=headers) - try: - with urllib.request.urlopen(req, timeout=30) as resp: - body = resp.read().decode("utf-8") - except urllib.error.HTTPError as exc: - detail = exc.read().decode("utf-8", errors="replace") - raise RuntimeError(f"GitHub API {method} {url}: {exc.code} {detail[:500]}") from exc - return json.loads(body) if body else {} - - -def step_summary(text: str) -> None: - p = os.environ.get("GITHUB_STEP_SUMMARY", "") - if p: - with open(p, "a", encoding="utf-8") as f: - f.write(text + "\n") - - -# ─── policy ────────────────────────────────────────────────────────────────── - - -def load_policy() -> dict[str, Any]: - if POLICY_PATH.exists(): - try: - return json.loads(POLICY_PATH.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - pass - return { - "version": 1, - "blocked_path_patterns": [ - r"(^|/)(\.env|.*secret.*|.*credential.*|.*token.*|.*private.*|.*\.pem|.*\.key)$", - ], - "max_changed_files": 50, - "max_changed_lines": 5000, - } - - -def compile_patterns(policy: dict[str, Any]) -> list[re.Pattern[str]]: - pp: list[re.Pattern[str]] = [] - for p in policy.get("blocked_path_patterns", []): - if isinstance(p, str) and p.strip(): - try: - pp.append(re.compile(p, re.IGNORECASE)) - except re.error: - pass - return pp - - -# ─── static guard ──────────────────────────────────────────────────────────── - -_SENSITIVE = re.compile( - r'(?Papi[_\s]?key|secret|password|token|credential|private[_\s]?key)\s*[:=]\s*["\']' - r'(?!\$\{\{|{{|example|placeholder|test|your[-_\s]|xxx|TODO|CHANGEME)[^"\']{12,}["\']', - re.IGNORECASE, -) - - -def scan_diff(diff_text: str, path_patterns: list[re.Pattern[str]]) -> list[str]: - violations: list[str] = [] - current = "" - for line in diff_text.splitlines(): - if line.startswith("diff --git "): - parts = line.split(" ") - current = parts[3][2:] if len(parts) >= 4 and parts[3].startswith("b/") else "" - for pat in path_patterns: - if current and pat.search(current): - violations.append(f"**Blocked file**: `{current}` matches `{pat.pattern}`") - break - continue - if line.startswith("+++ b/"): - current = line[6:] - continue - if not line.startswith("+") or line.startswith("+++"): - continue - m = _SENSITIVE.search(line[1:]) - if m: - violations.append(f"**Hardcoded secret** in `{current}`: `{m.group('field')}=`") - return list(dict.fromkeys(violations)) - - -def check_metadata(files: list[dict[str, Any]], policy: dict[str, Any]) -> list[str]: - issues: list[str] = [] - mx_f = policy.get("max_changed_files", 50) - mx_l = policy.get("max_changed_lines", 5000) - ta = sum(f.get("additions", 0) or 0 for f in files) - td = sum(f.get("deletions", 0) or 0 for f in files) - for f in files: - fn = f.get("filename", "?") - st = (f.get("status") or "").lower().strip() - if st == "removed": - issues.append(f"**File deleted**: `{fn}` — verify intentional") - elif st == "renamed": - issues.append(f"**File renamed**: `{f.get('previous_filename', '?')}` → `{fn}`") - if len(files) > mx_f: - issues.append(f"**Too many files**: {len(files)} changed (limit {mx_f})") - if ta + td > mx_l: - issues.append(f"**Too many lines**: {ta + td} changed (limit {mx_l})") - return issues - - -def run_static_guard(token: str, repo: str, pr_number: int) -> int: - """Return 0 if clean, 1 if blocked.""" - policy = load_policy() - files: list[dict[str, Any]] = [] - page = 1 - while True: - try: - batch = github_request(token, "GET", f"/repos/{repo}/pulls/{pr_number}/files?per_page=100&page={page}") - except RuntimeError: - break - if not isinstance(batch, list) or not batch: - break - files.extend(batch) - if len(batch) < 100: - break - page += 1 - - diff_text = "" - try: - req = urllib.request.Request( - f"{API_BASE}/repos/{repo}/pulls/{pr_number}", - headers={ - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github.v3.diff", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "codex-review-gate", - }, - ) - with urllib.request.urlopen(req, timeout=30) as resp: - diff_text = resp.read().decode("utf-8", errors="replace") - except Exception: - pass - - issues = check_metadata(files, policy) + scan_diff(diff_text, compile_patterns(policy)) - if not issues: - return 0 - - print(f"STATIC → BLOCKED: {len(issues)} issue(s)") - for i in issues: - print(f" • {i}") - step_summary(f"## Merge blocked: {len(issues)} static issue(s)\n\n" + "\n".join(f"- {i}" for i in issues)) - return 1 - - -# ─── app review ────────────────────────────────────────────────────────────── - - -def get_codex_review(token: str, repo: str, pr_number: int) -> dict[str, Any] | None: - reviews = github_request(token, "GET", f"/repos/{repo}/pulls/{pr_number}/reviews?per_page=100") - if not isinstance(reviews, list): - return None - for r in reversed(reviews): - if isinstance(r, dict) and (r.get("user") or {}).get("login") == BOT_LOGIN: - return r - return None - - -def app_decision(review: dict[str, Any] | None) -> tuple[int, str, str]: - """(exit_code, title, summary)""" - if review is None: - return ( - 0, - "Codex: no review — passed through", - "Codex did not respond in time. Merge allowed to avoid blocking development.", - ) - state = (review.get("state") or "").strip().upper() - url = review.get("html_url", "") - body = (review.get("body") or "").strip() - at = review.get("submitted_at", "") - - if state == "CHANGES_REQUESTED": - snippet = (body[:500] + "...") if len(body) > 500 else body - return ( - 1, - "Codex: changes requested — MERGE BLOCKED", - f"Codex **requested changes** at {at}.\n\n{snippet}\n\n[View review]({url})", - ) - if state == "APPROVED": - return (0, "Codex: approved", f"Codex approved at {at}. [View review]({url})") - return ( - 0, - f"Codex: reviewed ({state.lower()})", - f"Codex state `{state}` at {at}. Not blocking. [View review]({url})", - ) - - -# ─── main ──────────────────────────────────────────────────────────────────── - - -def main() -> int: - token = env("GH_TOKEN") or env("GITHUB_TOKEN") - repo = env("GITHUB_REPOSITORY") - if not token or not repo: - print("::error::GH_TOKEN + GITHUB_REPOSITORY required", file=sys.stderr) - return 1 - - event_path = Path(os.environ.get("GITHUB_EVENT_PATH", "")) - if not event_path.exists(): - print("::error::GITHUB_EVENT_PATH missing", file=sys.stderr) - return 1 - - event = json.loads(event_path.read_text(encoding="utf-8")) - event_name = env("GITHUB_EVENT_NAME", "") - pr = event.get("pull_request") or {} - pr_number = pr.get("number") - head_sha = (pr.get("head") or {}).get("sha") - if not pr_number or not head_sha: - print("::warning::Cannot resolve PR context") - return 0 - - print(f"PR #{pr_number} sha={head_sha[:12]} event={event_name}") - - # ── Phase 1: Static guard (skip on review-only events) ──────────── - if event_name != "pull_request_review": - try: - rc = run_static_guard(token, repo, pr_number) - except RuntimeError as exc: - print(f"::warning::Static guard error: {exc}") - rc = 0 - if rc != 0: - return 1 - print("STATIC → clean") - - # ── Phase 2: App review ─────────────────────────────────────────── - # REACT: Codex just submitted a review - review_event = event.get("review") or {} - if event_name == "pull_request_review" and (review_event.get("user") or {}).get("login") == BOT_LOGIN: - rc, title, summary = app_decision(review_event) - print(f"REACT → exit={rc}: {title}") - step_summary(f"## {title}\n\n{summary}") - return rc - - # WAIT: poll for existing or upcoming review - try: - existing = get_codex_review(token, repo, pr_number) - except RuntimeError: - existing = None - - if existing is not None: - rc, title, summary = app_decision(existing) - print(f"EXISTING → exit={rc}: {title}") - step_summary(f"## {title}\n\n{summary}") - return rc - - poll_s = env_int("CODEX_GATE_POLL_SECONDS", 30) - max_w = env_int("CODEX_GATE_MAX_WAIT_MINUTES", 5) - deadline = time.time() + max_w * 60 - print(f"WAIT → polling every {poll_s}s for up to {max_w}min") - - while time.time() < deadline: - time.sleep(poll_s) - try: - review = get_codex_review(token, repo, pr_number) - except RuntimeError: - continue - if review is not None: - rc, title, summary = app_decision(review) - print(f"WAIT → found review → exit={rc}: {title}") - step_summary(f"## {title}\n\n{summary}") - return rc - - # Timeout - print(f"TIMEOUT → Codex did not respond in {max_w}min; passing through") - step_summary(f"## Codex: timeout after {max_w}min\n\nPassed through to avoid blocking development.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/python/tests/test_gate_codex_app_review.py b/python/tests/test_gate_codex_app_review.py deleted file mode 100644 index d5300676..00000000 --- a/python/tests/test_gate_codex_app_review.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] -MODULE_PATH = ROOT / "python" / "scripts" / "gate_codex_app_review.py" -SPEC = importlib.util.spec_from_file_location("gate_codex_app_review", MODULE_PATH) -gate_codex_app_review = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = gate_codex_app_review -SPEC.loader.exec_module(gate_codex_app_review) - - -class GateCodexAppReviewTest(unittest.TestCase): - def test_scan_diff_redacts_secret_values(self): - diff = "\n".join( - [ - "diff --git a/example.py b/example.py", - "+++ b/example.py", - '+api_key = "sk-' 'live-12345678901234567890"', - ] - ) - - violations = gate_codex_app_review.scan_diff(diff, []) - - self.assertEqual(len(violations), 1) - self.assertIn("api_key=", violations[0]) - self.assertNotIn("sk-live-12345678901234567890", violations[0]) - - -if __name__ == "__main__": - unittest.main() diff --git a/web/strategy-switch-console/frontend/package-lock.json b/web/strategy-switch-console/frontend/package-lock.json index 42373b14..6974b71e 100644 --- a/web/strategy-switch-console/frontend/package-lock.json +++ b/web/strategy-switch-console/frontend/package-lock.json @@ -1120,9 +1120,9 @@ "license": "MIT" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": {