From 4a14860c600a632922d04bee50cbc154d0311f92 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Jun 2026 00:26:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(governance):=20=E6=96=B0=E5=A2=9E=E5=85=A8?= =?UTF-8?q?=E7=94=A2=E5=93=81=20Code=20Review=20=E9=98=B2=E6=9C=A8?= =?UTF-8?q?=E9=A6=AC=20Gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/api/v1/agents.py | 31 ++ .../src/services/product_code_review_gate.py | 193 +++++++++ .../tests/test_product_code_review_gate.py | 199 ++++++++++ .../test_product_code_review_gate_api.py | 38 ++ apps/web/messages/en.json | 84 ++++ apps/web/messages/zh-TW.json | 84 ++++ .../web/src/app/[locale]/code-review/page.tsx | 322 +++++++++++++++ .../product_code_review_gate_2026-06-19.json | 370 ++++++++++++++++++ .../product_code_review_gate_v1.schema.json | 175 +++++++++ 9 files changed, 1496 insertions(+) create mode 100644 apps/api/src/services/product_code_review_gate.py create mode 100644 apps/api/tests/test_product_code_review_gate.py create mode 100644 apps/api/tests/test_product_code_review_gate_api.py create mode 100644 docs/evaluations/product_code_review_gate_2026-06-19.json create mode 100644 docs/schemas/product_code_review_gate_v1.schema.json diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index e4a72a7c..3654b019 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -319,6 +319,9 @@ from src.services.offsite_escrow_readiness_status import ( from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) +from src.services.product_code_review_gate import ( + load_latest_product_code_review_gate, +) from src.services.runtime_surface_inventory import ( load_latest_runtime_surface_inventory, ) @@ -3388,6 +3391,34 @@ async def get_dependency_supply_chain_drift_monitor() -> dict[str, Any]: ) from exc +@router.get( + "/product-code-review-gate", + response_model=dict[str, Any], + summary="取得 P2-111 全產品 Code Review 防木馬 Gate", + description=( + "讀取最新已提交的 P2-111 全產品推版前後 Code Review / 防木馬 Gate;" + "此端點會把 AwoooP tenants 資產台帳、Gitea code-review、供應鏈漂移、Aider 事件與 AI reviewer " + "分工收斂成只讀 read model。它不啟用外部 scanner、不寫 workflow、不 auto-merge、" + "不部署、不讀 secret、不推 registry、不簽 artifact、不送 Telegram、不寫 Gateway queue、不做 host probe。" + ), +) +async def get_product_code_review_gate() -> dict[str, Any]: + """Return the latest read-only all-product code review gate.""" + try: + return await asyncio.to_thread(load_latest_product_code_review_gate) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("product_code_review_gate_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P2-111 全產品 Code Review Gate 快照無效", + ) from exc + + @router.get( "/dependency-upgrade-approval-package-template", response_model=dict[str, Any], diff --git a/apps/api/src/services/product_code_review_gate.py b/apps/api/src/services/product_code_review_gate.py new file mode 100644 index 00000000..0c9b7f62 --- /dev/null +++ b/apps/api/src/services/product_code_review_gate.py @@ -0,0 +1,193 @@ +""" +Product code review gate snapshot. + +Loads the latest committed, read-only all-product code-review gate and enriches +it with the existing AwoooP tenant asset inventory. The loader never enables +external scanners, writes workflows, auto-merges code, deploys, reads secrets, +pushes registry artifacts, sends Telegram messages, or opens runtime gates. +""" + +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path +from typing import Any + +from src.services.platform_operator_service import build_tenant_asset_inventory +from src.services.snapshot_paths import default_evaluations_dir + +_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__)) +_SNAPSHOT_PATTERN = "product_code_review_gate_*.json" +_SCHEMA_VERSION = "product_code_review_gate_v1" + +_BLOCKED_BOUNDARY_FLAGS = { + "workflow_write_allowed", + "external_scanner_activation_allowed", + "paid_ai_review_allowed", + "repo_app_install_allowed", + "auto_merge_allowed", + "production_deploy_authorized", + "aider_auto_patch_allowed", + "elephantalpha_write_allowed", + "secret_read_allowed", + "post_deploy_write_allowed", + "runtime_execution_allowed", + "telegram_send_allowed", + "gateway_queue_write_allowed", + "host_probe_allowed", + "registry_push_allowed", + "artifact_signing_allowed", + "action_buttons_allowed", +} + + +def load_latest_product_code_review_gate( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed all-product code-review gate snapshot.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no product code review gate snapshots found in {directory}") + + latest = candidates[-1] + with latest.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{latest}: expected JSON object") + + inventory = build_tenant_asset_inventory([]) + enriched = deepcopy(payload) + enriched["tenant_asset_inventory_summary"] = inventory.get("summary") or {} + enriched["product_review_matrix"] = _build_product_review_matrix(inventory) + + _require_schema(enriched, _SCHEMA_VERSION, str(latest)) + _require_read_only_boundaries(enriched, str(latest)) + _require_rollup_consistency(enriched, str(latest)) + _require_gate_evidence(enriched, str(latest)) + _require_agent_boundaries(enriched, str(latest)) + return enriched + + +def _build_product_review_matrix(inventory: dict[str, Any]) -> list[dict[str, Any]]: + products = inventory.get("products") or [] + matrix: list[dict[str, Any]] = [] + for product in products: + status = product.get("coverage_status") or "read_only_candidate" + source_repo_count = int(product.get("source_repo_count") or 0) + public_route_count = int(product.get("public_route_count") or 0) + gate_state = "owner_review_required" if status == "owner_response_required" else "read_only_visible" + if source_repo_count == 0: + gate_state = "source_mapping_required" + matrix.append( + { + "product_id": product.get("product_id"), + "product_name": product.get("product_name"), + "category": product.get("category"), + "surface_kind": product.get("surface_kind"), + "owner_lane": product.get("owner_lane"), + "coverage_status": status, + "public_route_count": public_route_count, + "source_repo_count": source_repo_count, + "gate_state": gate_state, + "pre_deploy_gate": "required", + "post_deploy_gate": "required", + "owner_response_received_count": int( + product.get("owner_response_received_count") or 0 + ), + "owner_response_accepted_count": int( + product.get("owner_response_accepted_count") or 0 + ), + "runtime_gate_count": int(product.get("runtime_gate_count") or 0), + "action_button_count": int(product.get("action_button_count") or 0), + } + ) + return matrix + + +def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None: + actual = payload.get("schema_version") + if actual != expected: + raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}") + + +def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None: + program_status = payload.get("program_status") or {} + if program_status.get("read_only_mode") is not True: + raise ValueError(f"{label}: program_status.read_only_mode must be true") + + boundaries = payload.get("gate_boundaries") or {} + if boundaries.get("read_only_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + + allowed = sorted(flag for flag in _BLOCKED_BOUNDARY_FLAGS if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: gate boundaries must remain false: {allowed}") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + rollups = payload.get("rollups") or {} + tenant_summary = payload.get("tenant_asset_inventory_summary") or {} + product_matrix = payload.get("product_review_matrix") or [] + pre_deploy = payload.get("pre_deploy_gates") or [] + post_deploy = payload.get("post_deploy_gates") or [] + reviewers = payload.get("ai_reviewer_lanes") or [] + tools = payload.get("mainstream_tool_lanes") or [] + boundaries = payload.get("gate_boundaries") or {} + + product_count = int(tenant_summary.get("product_surface_count") or 0) + route_count = int(tenant_summary.get("public_route_count") or 0) + source_count = int(tenant_summary.get("source_candidate_repo_count") or 0) + + if rollups.get("product_scope_count") != product_count: + raise ValueError(f"{label}: rollups.product_scope_count must match tenant inventory") + if rollups.get("product_scope_count") != len(product_matrix): + raise ValueError(f"{label}: product_review_matrix length must match product scope count") + if route_count < int(rollups.get("public_route_count_minimum") or 0): + raise ValueError(f"{label}: tenant route count is lower than product gate minimum") + if rollups.get("source_candidate_repo_count") != source_count: + raise ValueError(f"{label}: source candidate count must match tenant inventory") + if rollups.get("pre_deploy_gate_count") != len(pre_deploy): + raise ValueError(f"{label}: pre_deploy_gate_count must match gates") + if rollups.get("post_deploy_gate_count") != len(post_deploy): + raise ValueError(f"{label}: post_deploy_gate_count must match gates") + if rollups.get("ai_reviewer_count") != len(reviewers): + raise ValueError(f"{label}: ai_reviewer_count must match reviewer lanes") + if rollups.get("mainstream_tool_count") != len(tools): + raise ValueError(f"{label}: mainstream_tool_count must match tool lanes") + + false_count = sum(1 for flag in _BLOCKED_BOUNDARY_FLAGS if boundaries.get(flag) is False) + if rollups.get("blocked_operation_count") != false_count: + raise ValueError(f"{label}: blocked_operation_count must match false boundaries") + if rollups.get("active_write_gate_count") != 0: + raise ValueError(f"{label}: active_write_gate_count must remain 0") + if rollups.get("action_button_count") != 0: + raise ValueError(f"{label}: action_button_count must remain 0") + + +def _require_gate_evidence(payload: dict[str, Any], label: str) -> None: + for key in ("pre_deploy_gates", "post_deploy_gates"): + for gate in payload.get(key) or []: + gate_id = gate.get("gate_id") or "" + if not gate.get("evidence_refs"): + raise ValueError(f"{label}: gate {gate_id} must include evidence_refs") + if not gate.get("next_action"): + raise ValueError(f"{label}: gate {gate_id} must include next_action") + + +def _require_agent_boundaries(payload: dict[str, Any], label: str) -> None: + reviewers = payload.get("ai_reviewer_lanes") or [] + for reviewer in reviewers: + agent_id = reviewer.get("agent_id") or "" + if reviewer.get("write_allowed") is not False: + raise ValueError(f"{label}: reviewer {agent_id} write_allowed must remain false") + + matrix = payload.get("product_review_matrix") or [] + active_runtime = [item for item in matrix if item.get("runtime_gate_count") != 0] + action_buttons = [item for item in matrix if item.get("action_button_count") != 0] + if active_runtime: + raise ValueError(f"{label}: product runtime gates must remain 0") + if action_buttons: + raise ValueError(f"{label}: product action buttons must remain 0") diff --git a/apps/api/tests/test_product_code_review_gate.py b/apps/api/tests/test_product_code_review_gate.py new file mode 100644 index 00000000..ad3a5e98 --- /dev/null +++ b/apps/api/tests/test_product_code_review_gate.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import json + +import pytest + +from src.services.product_code_review_gate import load_latest_product_code_review_gate + + +def test_load_latest_product_code_review_gate_enriches_tenant_inventory(tmp_path): + _write_snapshot(tmp_path, _snapshot()) + + loaded = load_latest_product_code_review_gate(tmp_path) + + assert loaded["schema_version"] == "product_code_review_gate_v1" + assert loaded["program_status"]["current_task_id"] == "P2-111" + assert loaded["program_status"]["next_task_id"] == "P2-112" + assert loaded["program_status"]["read_only_mode"] is True + assert loaded["rollups"]["product_scope_count"] == len(loaded["product_review_matrix"]) >= 16 + assert loaded["tenant_asset_inventory_summary"]["public_route_count"] >= 31 + assert loaded["rollups"]["source_candidate_repo_count"] == 10 + assert loaded["gate_boundaries"]["auto_merge_allowed"] is False + assert loaded["gate_boundaries"]["aider_auto_patch_allowed"] is False + assert loaded["gate_boundaries"]["elephantalpha_write_allowed"] is False + assert all(item["runtime_gate_count"] == 0 for item in loaded["product_review_matrix"]) + assert all(item["action_button_count"] == 0 for item in loaded["product_review_matrix"]) + assert any(item["product_id"] == "PRD-001" for item in loaded["product_review_matrix"]) + assert any(tool["tool_id"] == "codeql" for tool in loaded["mainstream_tool_lanes"]) + assert any(tool["tool_id"] == "gitleaks" for tool in loaded["mainstream_tool_lanes"]) + + +def test_product_code_review_gate_requires_read_only_mode(tmp_path): + snapshot = _snapshot() + snapshot["program_status"]["read_only_mode"] = False + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="read_only_mode"): + load_latest_product_code_review_gate(tmp_path) + + +def test_product_code_review_gate_blocks_auto_merge(tmp_path): + snapshot = _snapshot() + snapshot["gate_boundaries"]["auto_merge_allowed"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="gate boundaries"): + load_latest_product_code_review_gate(tmp_path) + + +def test_product_code_review_gate_requires_rollup_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["pre_deploy_gate_count"] = 99 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="pre_deploy_gate_count"): + load_latest_product_code_review_gate(tmp_path) + + +def test_product_code_review_gate_requires_gate_evidence(tmp_path): + snapshot = _snapshot() + snapshot["pre_deploy_gates"][0]["evidence_refs"] = [] + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="evidence_refs"): + load_latest_product_code_review_gate(tmp_path) + + +def test_product_code_review_gate_requires_reviewer_no_write(tmp_path): + snapshot = _snapshot() + snapshot["ai_reviewer_lanes"][0]["write_allowed"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="write_allowed"): + load_latest_product_code_review_gate(tmp_path) + + +def test_product_code_review_gate_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_product_code_review_gate(tmp_path) + + +def _write_snapshot(tmp_path, payload: dict) -> None: + (tmp_path / "product_code_review_gate_2026-06-19.json").write_text( + json.dumps(payload), + encoding="utf-8", + ) + + +def _snapshot() -> dict: + return { + "schema_version": "product_code_review_gate_v1", + "generated_at": "2026-06-19T00:42:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P2", + "current_task_id": "P2-111", + "next_task_id": "P2-112", + "read_only_mode": True, + "runtime_authority": "repo_only", + "status_note": "只讀 Code Review Gate。", + }, + "source_refs": [".gitea/workflows/code-review.yaml"], + "rollups": { + "product_scope_count": 16, + "public_route_count_minimum": 31, + "source_candidate_repo_count": 10, + "pre_deploy_gate_count": 1, + "post_deploy_gate_count": 1, + "ai_reviewer_count": 2, + "mainstream_tool_count": 2, + "owner_review_required_count": 1, + "critical_gap_count": 1, + "blocked_operation_count": 17, + "active_write_gate_count": 0, + "action_button_count": 0, + }, + "pre_deploy_gates": [_gate("pre", "hermes")], + "post_deploy_gates": [_gate("post", "nemotron")], + "ai_reviewer_lanes": [ + _reviewer("hermes"), + _reviewer("aider"), + ], + "mainstream_tool_lanes": [ + _tool("codeql"), + _tool("gitleaks"), + ], + "decision_matrix": [ + { + "risk_lane": "low", + "reviewer": "Hermes", + "aider_role": "draft", + "required_gate": "owner", + "post_deploy": "smoke", + } + ], + "gate_boundaries": { + "read_only_api_allowed": True, + "workflow_write_allowed": False, + "external_scanner_activation_allowed": False, + "paid_ai_review_allowed": False, + "repo_app_install_allowed": False, + "auto_merge_allowed": False, + "production_deploy_authorized": False, + "aider_auto_patch_allowed": False, + "elephantalpha_write_allowed": False, + "secret_read_allowed": False, + "post_deploy_write_allowed": False, + "runtime_execution_allowed": False, + "telegram_send_allowed": False, + "gateway_queue_write_allowed": False, + "host_probe_allowed": False, + "registry_push_allowed": False, + "artifact_signing_allowed": False, + "action_buttons_allowed": False, + }, + "next_actions": [ + { + "task_id": "P2-112", + "priority": "P1", + "summary": "下一步", + "gate": "owner", + } + ], + } + + +def _gate(gate_id: str, owner_agent: str) -> dict: + return { + "gate_id": gate_id, + "label": gate_id, + "coverage": "repo", + "status": "wired", + "owner_agent": owner_agent, + "evidence_refs": ["docs/LOGBOOK.md"], + "current_gap": "gap", + "next_action": "review", + } + + +def _reviewer(agent_id: str) -> dict: + return { + "agent_id": agent_id, + "label": agent_id, + "role": "review", + "allowed_output": "packet", + "write_allowed": False, + } + + +def _tool(tool_id: str) -> dict: + return { + "tool_id": tool_id, + "label": tool_id, + "category": "scanner", + "source_url": "https://example.com", + "integration_status": "candidate", + "recommended_role": "scan", + "blocked_now": ["external lookup"], + } diff --git a/apps/api/tests/test_product_code_review_gate_api.py b/apps/api/tests/test_product_code_review_gate_api.py new file mode 100644 index 00000000..44fc124d --- /dev/null +++ b/apps/api/tests/test_product_code_review_gate_api.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_product_code_review_gate_endpoint_returns_all_product_gate(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/product-code-review-gate") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "product_code_review_gate_v1" + assert data["program_status"]["current_task_id"] == "P2-111" + assert data["program_status"]["next_task_id"] == "P2-112" + assert data["program_status"]["read_only_mode"] is True + assert data["rollups"]["product_scope_count"] == len(data["product_review_matrix"]) >= 16 + assert data["tenant_asset_inventory_summary"]["public_route_count"] >= 31 + assert data["rollups"]["pre_deploy_gate_count"] == len(data["pre_deploy_gates"]) == 8 + assert data["rollups"]["post_deploy_gate_count"] == len(data["post_deploy_gates"]) == 6 + assert data["rollups"]["ai_reviewer_count"] == len(data["ai_reviewer_lanes"]) == 5 + assert data["rollups"]["mainstream_tool_count"] == len(data["mainstream_tool_lanes"]) == 9 + assert data["rollups"]["active_write_gate_count"] == 0 + assert data["rollups"]["action_button_count"] == 0 + assert data["gate_boundaries"]["auto_merge_allowed"] is False + assert data["gate_boundaries"]["aider_auto_patch_allowed"] is False + assert data["gate_boundaries"]["external_scanner_activation_allowed"] is False + assert data["gate_boundaries"]["artifact_signing_allowed"] is False + assert any(product["product_id"] == "PRD-001" for product in data["product_review_matrix"]) + assert any(product["product_id"] == "PRD-006" for product in data["product_review_matrix"]) + assert any(tool["tool_id"] == "codeql" for tool in data["mainstream_tool_lanes"]) + assert any(tool["tool_id"] == "semgrep" for tool in data["mainstream_tool_lanes"]) + assert any(tool["tool_id"] == "osv_scanner" for tool in data["mainstream_tool_lanes"]) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index edf2147b..1e18f013 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -213,6 +213,90 @@ "subtitle": "Hermes、OpenClaw、Elephant Alpha 與 NemoTron 只做審查、分級與候選整理;修正、推版與主機操作仍留在人工閘門後面。", "evidenceLink": "查看 AwoooP 執行紀錄" }, + "gateBoard": { + "eyebrow": "全產品防木馬 Gate", + "title": "推版前後 Code Review 總控", + "subtitle": "把 Tenants 全產品資產、Gitea 審查、供應鏈漂移、Aider 事件、ElephantAlpha 高風險審查與主流掃描工具候選收斂成同一張只讀 Gate;目前只呈現證據與缺口,不 auto-merge、不自動部署、不讀 secret。", + "loading": "讀取全產品 Code Review Gate", + "unavailable": "暫時無法讀取全產品 Code Review Gate;請查看 API / Gitea Actions readback。", + "metrics": { + "current": "目前任務", + "next": "下一步", + "writeGate": "寫入閘門", + "actions": "操作入口" + }, + "metricCards": { + "products": { + "label": "產品 / 專案", + "detail": "沿用 Tenants 全域資產台帳。" + }, + "routes": { + "label": "網站入口", + "detail": "公開前台、後台與平台工具入口。" + }, + "pre": { + "label": "推版前 Gate", + "detail": "diff、secret、供應鏈、owner gate。" + }, + "post": { + "label": "推版後 Gate", + "detail": "marker、smoke、rollback、verifier。" + }, + "reviewers": { + "label": "AI Reviewer", + "detail": "Hermes / OpenClaw / ElephantAlpha / NemoTron / Aider。" + }, + "tools": { + "label": "主流工具候選", + "detail": "CodeQL、Semgrep、Gitleaks、OSV、Trivy 等。" + } + }, + "flow": { + "assets": { + "title": "資產範圍", + "detail": "所有網站、產品、工具先進同一張資產台帳。" + }, + "preDeploy": { + "title": "推版前", + "detail": "先做 diff、secret、供應鏈與高風險審查。" + }, + "aiReview": { + "title": "AI 審查", + "detail": "ElephantAlpha 只讀 hold;Aider 只在批准後起草。" + }, + "postDeploy": { + "title": "推版後", + "detail": "驗證 marker、路由、回滾與 verifier receipt。" + }, + "ownerGate": { + "title": "Owner Gate", + "detail": "高風險進人工接受;結果回寫 KM / PlayBook。" + } + } + }, + "products": { + "title": "產品 / 專案 Code Review 覆蓋矩陣", + "subtitle": "每個產品都必須有推版前與推版後 Gate;來源缺口或負責人未接受時,只能列為候選。", + "labels": { + "routes": "入口", + "sources": "來源", + "gates": "閘門" + }, + "states": { + "visible": "已納管", + "ownerReview": "待 owner", + "sourceMapping": "待來源" + } + }, + "prepost": { + "title": "推版前 / 推版後 Gate", + "pre": "Pre-deploy", + "post": "Post-deploy" + }, + "tools": { + "title": "主流工具與 AI 工具整合候選", + "candidate": "需批准" + }, "handoff": { "eyebrow": "Code Review → Codex", "title": "審查後 Coding 工作橋接", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index edf2147b..1e18f013 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -213,6 +213,90 @@ "subtitle": "Hermes、OpenClaw、Elephant Alpha 與 NemoTron 只做審查、分級與候選整理;修正、推版與主機操作仍留在人工閘門後面。", "evidenceLink": "查看 AwoooP 執行紀錄" }, + "gateBoard": { + "eyebrow": "全產品防木馬 Gate", + "title": "推版前後 Code Review 總控", + "subtitle": "把 Tenants 全產品資產、Gitea 審查、供應鏈漂移、Aider 事件、ElephantAlpha 高風險審查與主流掃描工具候選收斂成同一張只讀 Gate;目前只呈現證據與缺口,不 auto-merge、不自動部署、不讀 secret。", + "loading": "讀取全產品 Code Review Gate", + "unavailable": "暫時無法讀取全產品 Code Review Gate;請查看 API / Gitea Actions readback。", + "metrics": { + "current": "目前任務", + "next": "下一步", + "writeGate": "寫入閘門", + "actions": "操作入口" + }, + "metricCards": { + "products": { + "label": "產品 / 專案", + "detail": "沿用 Tenants 全域資產台帳。" + }, + "routes": { + "label": "網站入口", + "detail": "公開前台、後台與平台工具入口。" + }, + "pre": { + "label": "推版前 Gate", + "detail": "diff、secret、供應鏈、owner gate。" + }, + "post": { + "label": "推版後 Gate", + "detail": "marker、smoke、rollback、verifier。" + }, + "reviewers": { + "label": "AI Reviewer", + "detail": "Hermes / OpenClaw / ElephantAlpha / NemoTron / Aider。" + }, + "tools": { + "label": "主流工具候選", + "detail": "CodeQL、Semgrep、Gitleaks、OSV、Trivy 等。" + } + }, + "flow": { + "assets": { + "title": "資產範圍", + "detail": "所有網站、產品、工具先進同一張資產台帳。" + }, + "preDeploy": { + "title": "推版前", + "detail": "先做 diff、secret、供應鏈與高風險審查。" + }, + "aiReview": { + "title": "AI 審查", + "detail": "ElephantAlpha 只讀 hold;Aider 只在批准後起草。" + }, + "postDeploy": { + "title": "推版後", + "detail": "驗證 marker、路由、回滾與 verifier receipt。" + }, + "ownerGate": { + "title": "Owner Gate", + "detail": "高風險進人工接受;結果回寫 KM / PlayBook。" + } + } + }, + "products": { + "title": "產品 / 專案 Code Review 覆蓋矩陣", + "subtitle": "每個產品都必須有推版前與推版後 Gate;來源缺口或負責人未接受時,只能列為候選。", + "labels": { + "routes": "入口", + "sources": "來源", + "gates": "閘門" + }, + "states": { + "visible": "已納管", + "ownerReview": "待 owner", + "sourceMapping": "待來源" + } + }, + "prepost": { + "title": "推版前 / 推版後 Gate", + "pre": "Pre-deploy", + "post": "Post-deploy" + }, + "tools": { + "title": "主流工具與 AI 工具整合候選", + "candidate": "需批准" + }, "handoff": { "eyebrow": "Code Review → Codex", "title": "審查後 Coding 工作橋接", diff --git a/apps/web/src/app/[locale]/code-review/page.tsx b/apps/web/src/app/[locale]/code-review/page.tsx index f939932b..7acdfbb9 100644 --- a/apps/web/src/app/[locale]/code-review/page.tsx +++ b/apps/web/src/app/[locale]/code-review/page.tsx @@ -3,6 +3,7 @@ import { AppLayout } from '@/components/layout' import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge' import { publicBoundaryText } from '@/lib/public-security-redaction' +import { useEffect, useMemo, useState } from 'react' import { Activity, ArrowRight, @@ -13,15 +14,92 @@ import { ExternalLink, FileCheck2, GitBranch, + Globe2, Gauge, ListChecks, LockKeyhole, + PackageSearch, SearchCheck, + ShieldAlert, ShieldCheck, Workflow, } from 'lucide-react' import { useTranslations } from 'next-intl' +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +type ProductCodeReviewGateSnapshot = { + schema_version: string + generated_at: string + program_status: { + overall_completion_percent: number + current_task_id: string + next_task_id: string + read_only_mode: boolean + } + rollups: { + product_scope_count: number + public_route_count_minimum: number + source_candidate_repo_count: number + pre_deploy_gate_count: number + post_deploy_gate_count: number + ai_reviewer_count: number + mainstream_tool_count: number + owner_review_required_count: number + critical_gap_count: number + active_write_gate_count: number + action_button_count: number + } + tenant_asset_inventory_summary: { + public_route_count?: number + source_candidate_repo_count?: number + } + product_review_matrix: Array<{ + product_id: string + product_name: string + category: string + owner_lane: string + coverage_status: string + public_route_count: number + source_repo_count: number + gate_state: string + runtime_gate_count: number + action_button_count: number + }> + pre_deploy_gates: Array<{ + gate_id: string + label: string + status: string + owner_agent: string + current_gap: string + next_action: string + }> + post_deploy_gates: Array<{ + gate_id: string + label: string + status: string + owner_agent: string + current_gap: string + next_action: string + }> + ai_reviewer_lanes: Array<{ + agent_id: string + label: string + role: string + allowed_output: string + write_allowed: boolean + }> + mainstream_tool_lanes: Array<{ + tool_id: string + label: string + category: string + integration_status: string + recommended_role: string + blocked_now: string[] + }> + gate_boundaries: Record +} + const agents = [ { name: 'Hermes', roleKey: 'hermes', state: 'wired' }, { name: 'OpenClaw', roleKey: 'openclaw', state: 'wired' }, @@ -89,9 +167,50 @@ const summaryCards = [ { key: 'report', icon: Gauge, iconClassName: 'text-lime-300' }, ] +async function fetchProductCodeReviewGate(): Promise { + if (!API_BASE) return null + const response = await fetch(`${API_BASE}/api/v1/agents/product-code-review-gate`, { + cache: 'no-store', + }) + if (!response.ok) return null + return response.json() +} + export default function CodeReviewPage({ params }: { params: { locale: string } }) { const t = useTranslations('codeReview') + const [gateSnapshot, setGateSnapshot] = useState(null) + const [gateLoaded, setGateLoaded] = useState(false) const evidenceHref = `/${params.locale}/awooop/runs` + const products = useMemo( + () => gateSnapshot?.product_review_matrix ?? [], + [gateSnapshot] + ) + const preDeployGates = useMemo( + () => gateSnapshot?.pre_deploy_gates.slice(0, 4) ?? [], + [gateSnapshot] + ) + const postDeployGates = useMemo( + () => gateSnapshot?.post_deploy_gates.slice(0, 4) ?? [], + [gateSnapshot] + ) + const toolLanes = useMemo( + () => gateSnapshot?.mainstream_tool_lanes ?? [], + [gateSnapshot] + ) + + useEffect(() => { + let cancelled = false + fetchProductCodeReviewGate() + .then((payload) => { + if (!cancelled) setGateSnapshot(payload) + }) + .finally(() => { + if (!cancelled) setGateLoaded(true) + }) + return () => { + cancelled = true + } + }, []) return ( @@ -117,6 +236,209 @@ export default function CodeReviewPage({ params }: { params: { locale: string } +
+
+
+
+ + {t('gateBoard.eyebrow')} +
+

{t('gateBoard.title')}

+

{t('gateBoard.subtitle')}

+
+
+
+
{t('gateBoard.metrics.current')}
+
{gateSnapshot?.program_status.current_task_id ?? '--'}
+
+
+
{t('gateBoard.metrics.next')}
+
{gateSnapshot?.program_status.next_task_id ?? '--'}
+
+
+
{t('gateBoard.metrics.writeGate')}
+
{gateSnapshot?.rollups.active_write_gate_count ?? 0}
+
+
+
{t('gateBoard.metrics.actions')}
+
{gateSnapshot?.rollups.action_button_count ?? 0}
+
+
+
+ +
+ {[ + { + key: 'products', + value: gateSnapshot?.rollups.product_scope_count ?? products.length, + icon: Globe2, + }, + { + key: 'routes', + value: gateSnapshot?.tenant_asset_inventory_summary.public_route_count ?? gateSnapshot?.rollups.public_route_count_minimum ?? 0, + icon: GitBranch, + }, + { + key: 'pre', + value: gateSnapshot?.rollups.pre_deploy_gate_count ?? 0, + icon: ShieldCheck, + }, + { + key: 'post', + value: gateSnapshot?.rollups.post_deploy_gate_count ?? 0, + icon: CheckCircle2, + }, + { + key: 'reviewers', + value: gateSnapshot?.rollups.ai_reviewer_count ?? 0, + icon: Bot, + }, + { + key: 'tools', + value: gateSnapshot?.rollups.mainstream_tool_count ?? 0, + icon: PackageSearch, + }, + ].map((metric) => { + const Icon = metric.icon + return ( +
+
+ + {t(`gateBoard.metricCards.${metric.key}.label` as never)} +
+
{metric.value}
+
{t(`gateBoard.metricCards.${metric.key}.detail` as never)}
+
+ ) + })} +
+ +
+ {['assets', 'preDeploy', 'aiReview', 'postDeploy', 'ownerGate'].map((step, index) => ( +
+
{String(index + 1).padStart(2, '0')}
+
{t(`gateBoard.flow.${step}.title` as never)}
+
{t(`gateBoard.flow.${step}.detail` as never)}
+
+ ))} +
+ + {!gateLoaded ? ( +
+ {t('gateBoard.loading')} +
+ ) : gateSnapshot ? null : ( +
+ {t('gateBoard.unavailable')} +
+ )} +
+ +
+
+
+
+ + {t('products.title')} +
+
{t('products.subtitle')}
+
+
+ {publicBoundaryText('runtime_execution_authorized=false')} +
+
+
+ {products.map((product) => ( +
+
+
+
{product.product_id}
+
{product.product_name}
+
+ + {product.gate_state === 'owner_review_required' + ? t('products.states.ownerReview') + : product.gate_state === 'source_mapping_required' + ? t('products.states.sourceMapping') + : t('products.states.visible')} + +
+
+
+
{t('products.labels.routes')}
+
{product.public_route_count}
+
+
+
{t('products.labels.sources')}
+
{product.source_repo_count}
+
+
+
{t('products.labels.gates')}
+
{product.runtime_gate_count}
+
+
+
+ {product.owner_lane} · {product.coverage_status} +
+
+ ))} +
+
+ +
+
+
+ + {t('prepost.title')} +
+
+
+
{t('prepost.pre')}
+ {preDeployGates.map((gate) => ( +
+
{gate.label}
+
{gate.owner_agent} · {gate.status}
+
+ ))} +
+
+
{t('prepost.post')}
+ {postDeployGates.map((gate) => ( +
+
{gate.label}
+
{gate.owner_agent} · {gate.status}
+
+ ))} +
+
+
+ +
+
+ + {t('tools.title')} +
+
+ {toolLanes.map((tool) => ( +
+
+
{tool.label}
+
{t('tools.candidate')}
+
+
{tool.category} · {tool.integration_status}
+
+ ))} +
+
+
+
work item -> fix receipt -> KM / PlayBook trust 的 writeback gate。" + } + ], + "ai_reviewer_lanes": [ + { + "agent_id": "hermes", + "label": "Hermes", + "role": "證據整理、產品資產與 KM / 報表沉澱", + "allowed_output": "readback packet, KM draft, report digest draft", + "write_allowed": false + }, + { + "agent_id": "openclaw", + "label": "OpenClaw", + "role": "風險仲裁、供應鏈 Gate、owner packet 編排", + "allowed_output": "risk verdict, owner gate, policy decision packet", + "write_allowed": false + }, + { + "agent_id": "elephant_alpha", + "label": "ElephantAlpha", + "role": "高風險與防木馬 read-only reviewer", + "allowed_output": "security review finding, high-risk hold, rollback requirement", + "write_allowed": false + }, + { + "agent_id": "nemotron", + "label": "NemoTron", + "role": "長任務回放、供應鏈版本與 post-deploy verifier", + "allowed_output": "replay scorecard, smoke matrix, drift comparison", + "write_allowed": false + }, + { + "agent_id": "aider", + "label": "Aider", + "role": "批准後的 patch pair-programmer,不作審批者", + "allowed_output": "draft patch, lint/test receipt, rollback note", + "write_allowed": false + } + ], + "mainstream_tool_lanes": [ + { + "tool_id": "codeql", + "label": "CodeQL", + "category": "semantic_sast", + "source_url": "https://docs.github.com/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "跨語言語意分析與高風險資料流審查", + "blocked_now": ["enable workflow", "upload alerts"] + }, + { + "tool_id": "semgrep", + "label": "Semgrep", + "category": "sast_policy", + "source_url": "https://semgrep.dev/docs/semgrep-code/overview", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "快速規則化 SAST、框架風險與組織 policy guard", + "blocked_now": ["install scanner", "network rule fetch"] + }, + { + "tool_id": "gitleaks", + "label": "Gitleaks", + "category": "secret_scanning", + "source_url": "https://gitleaks.io/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "repo / diff / history secret scanning", + "blocked_now": ["full-history scan", "write report artifact"] + }, + { + "tool_id": "osv_scanner", + "label": "OSV-Scanner", + "category": "dependency_vulnerability", + "source_url": "https://google.github.io/osv-scanner/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "dependency manifest / lockfile vulnerability matching", + "blocked_now": ["external vulnerability lookup"] + }, + { + "tool_id": "trivy", + "label": "Trivy", + "category": "container_iac_vulnerability", + "source_url": "https://trivy.dev/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "container image、filesystem、IaC 與 secret scan", + "blocked_now": ["image pull", "external DB update"] + }, + { + "tool_id": "openssf_scorecard", + "label": "OpenSSF Scorecard", + "category": "repo_security_posture", + "source_url": "https://scorecard.dev/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "repository security posture / branch / token / CI hygiene score", + "blocked_now": ["external repo metadata lookup"] + }, + { + "tool_id": "slsa", + "label": "SLSA", + "category": "provenance_framework", + "source_url": "https://slsa.dev/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "build provenance 與 artifact integrity framework", + "blocked_now": ["provenance emission", "workflow write"] + }, + { + "tool_id": "sigstore_cosign", + "label": "Sigstore / cosign", + "category": "artifact_signing", + "source_url": "https://www.sigstore.dev/", + "integration_status": "candidate_owner_approval_required", + "recommended_role": "container / artifact signing and verification", + "blocked_now": ["keyless signing", "registry write"] + }, + { + "tool_id": "coderabbit_or_snyk", + "label": "CodeRabbit / Snyk", + "category": "ai_appsec_platform", + "source_url": "https://docs.coderabbit.ai/", + "integration_status": "paid_or_external_candidate_owner_approval_required", + "recommended_role": "PR AI review、SCA、container / IaC 風險與 developer workflow", + "blocked_now": ["paid external service", "repo app install"] + } + ], + "decision_matrix": [ + { + "risk_lane": "low_ui_or_docs", + "reviewer": "Hermes + deterministic guard", + "aider_role": "可在批准後起草 patch", + "required_gate": "owner_scope_confirmed", + "post_deploy": "route smoke + screenshot" + }, + { + "risk_lane": "medium_application_logic", + "reviewer": "OpenClaw + ElephantAlpha", + "aider_role": "只產 patch 與 test receipt", + "required_gate": "manual_approval_and_rollback_note", + "post_deploy": "API / UI smoke + incident watch" + }, + { + "risk_lane": "high_security_supply_chain", + "reviewer": "ElephantAlpha + OpenClaw + NemoTron replay", + "aider_role": "預設禁用,除非單項批准", + "required_gate": "owner_response + security_acceptance + verifier_plan", + "post_deploy": "deploy marker + provenance / verifier receipt" + }, + { + "risk_lane": "critical_runtime_or_secret", + "reviewer": "ElephantAlpha hold", + "aider_role": "禁止", + "required_gate": "break_glass_or_formal_change_window", + "post_deploy": "rollback owner + audit evidence + SRE digest" + } + ], + "gate_boundaries": { + "read_only_api_allowed": true, + "workflow_write_allowed": false, + "external_scanner_activation_allowed": false, + "paid_ai_review_allowed": false, + "repo_app_install_allowed": false, + "auto_merge_allowed": false, + "production_deploy_authorized": false, + "aider_auto_patch_allowed": false, + "elephantalpha_write_allowed": false, + "secret_read_allowed": false, + "post_deploy_write_allowed": false, + "runtime_execution_allowed": false, + "telegram_send_allowed": false, + "gateway_queue_write_allowed": false, + "host_probe_allowed": false, + "registry_push_allowed": false, + "artifact_signing_allowed": false, + "action_buttons_allowed": false + }, + "next_actions": [ + { + "task_id": "P2-112", + "priority": "P1", + "summary": "把 product_code_review_gate 接到 Gitea workflow pre-deploy policy readback;高風險先 fail-close,低風險仍需 reviewer receipt。", + "gate": "workflow_change_owner_approval_required" + }, + { + "task_id": "P2-113", + "priority": "P1", + "summary": "建立 post-deploy release receipt:deploy marker、image revision、product route smoke、rollback owner、verifier receipt。", + "gate": "read_only_release_receipt_first" + }, + { + "task_id": "P2-114", + "priority": "P1", + "summary": "設計外部 scanner 啟用批准包:CodeQL、Semgrep、Gitleaks、OSV、Trivy、OpenSSF、SLSA、Sigstore。", + "gate": "cost_and_external_lookup_approval_required" + } + ] +} diff --git a/docs/schemas/product_code_review_gate_v1.schema.json b/docs/schemas/product_code_review_gate_v1.schema.json new file mode 100644 index 00000000..f2c0fd90 --- /dev/null +++ b/docs/schemas/product_code_review_gate_v1.schema.json @@ -0,0 +1,175 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:product-code-review-gate-v1", + "title": "AWOOOI product code review gate v1", + "description": "全產品推版前後 Code Review / 防木馬 Gate 只讀讀回。此 schema 不授權外部 scanner 啟用、workflow 寫入、auto merge、production deploy、Aider 自動 patch、secret 讀取、host probe、registry push、artifact signing、Telegram 實發或 Gateway queue write。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "rollups", + "pre_deploy_gates", + "post_deploy_gates", + "ai_reviewer_lanes", + "mainstream_tool_lanes", + "decision_matrix", + "gate_boundaries", + "next_actions" + ], + "properties": { + "schema_version": { "type": "string", "const": "product_code_review_gate_v1" }, + "generated_at": { "type": "string", "minLength": 1 }, + "program_status": { + "type": "object", + "required": [ + "overall_completion_percent", + "current_priority", + "current_task_id", + "next_task_id", + "read_only_mode", + "runtime_authority", + "status_note" + ], + "properties": { + "overall_completion_percent": { "type": "integer", "minimum": 0, "maximum": 100 }, + "current_priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"] }, + "current_task_id": { "type": "string", "const": "P2-111" }, + "next_task_id": { "type": "string", "minLength": 1 }, + "read_only_mode": { "type": "boolean", "const": true }, + "runtime_authority": { "type": "string", "minLength": 1 }, + "status_note": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "rollups": { + "type": "object", + "required": [ + "product_scope_count", + "public_route_count_minimum", + "source_candidate_repo_count", + "pre_deploy_gate_count", + "post_deploy_gate_count", + "ai_reviewer_count", + "mainstream_tool_count", + "owner_review_required_count", + "critical_gap_count", + "blocked_operation_count", + "active_write_gate_count", + "action_button_count" + ], + "additionalProperties": { "type": "integer" } + }, + "pre_deploy_gates": { "$ref": "#/$defs/gateArray" }, + "post_deploy_gates": { "$ref": "#/$defs/gateArray" }, + "ai_reviewer_lanes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["agent_id", "label", "role", "allowed_output", "write_allowed"], + "properties": { + "agent_id": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 }, + "role": { "type": "string", "minLength": 1 }, + "allowed_output": { "type": "string", "minLength": 1 }, + "write_allowed": { "type": "boolean", "const": false } + }, + "additionalProperties": false + } + }, + "mainstream_tool_lanes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "tool_id", + "label", + "category", + "source_url", + "integration_status", + "recommended_role", + "blocked_now" + ], + "properties": { + "tool_id": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 }, + "source_url": { "type": "string", "minLength": 1 }, + "integration_status": { "type": "string", "minLength": 1 }, + "recommended_role": { "type": "string", "minLength": 1 }, + "blocked_now": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + }, + "additionalProperties": false + } + }, + "decision_matrix": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["risk_lane", "reviewer", "aider_role", "required_gate", "post_deploy"], + "additionalProperties": { "type": "string" } + } + }, + "gate_boundaries": { + "type": "object", + "required": ["read_only_api_allowed"], + "additionalProperties": { "type": "boolean" } + }, + "next_actions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["task_id", "priority", "summary", "gate"], + "additionalProperties": { "type": "string" } + } + } + }, + "$defs": { + "gateArray": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "gate_id", + "label", + "coverage", + "status", + "owner_agent", + "evidence_refs", + "current_gap", + "next_action" + ], + "properties": { + "gate_id": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 }, + "coverage": { "type": "string", "minLength": 1 }, + "status": { "type": "string", "minLength": 1 }, + "owner_agent": { "type": "string", "minLength": 1 }, + "evidence_refs": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "current_gap": { "type": "string", "minLength": 1 }, + "next_action": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": true +}