#!/usr/bin/env python3 """ IwoooS AI provider / model routing owner response acceptance 只讀帳本產生器。 本工具建立 AI provider、Ollama proxy、fallback order、cost、privacy、 benchmark、dry-run 與 agent replacement candidate 的 metadata-only acceptance ledger。它不呼叫任何模型、不讀 live endpoint、不切 provider、 不改 Nginx、不送 prompt、不建立外部 API 呼叫、不收 secret value,也不開 runtime gate。 """ from __future__ import annotations import argparse import hashlib import json import subprocess import sys from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any TAIPEI = timezone(timedelta(hours=8)) ACCEPTANCE_FIELDS = [ "acceptance_candidate_id", "surface_id", "label", "expected_scope", "config_kind", "control_tier", "source_refs", "source_ref_sha256", "write_capable_surface", "paid_provider_related", "data_egress_related", "requires_live_evidence", "owner_response_ref", "owner_role_or_team", "decision", "decision_reason", "affected_scope", "provider_owner", "fallback_order_ref", "dry_run_result_ref", "benchmark_result_ref", "cost_review_ref", "privacy_review_ref", "data_classification_ref", "prompt_redaction_ref", "secret_handling_ref", "quota_budget_ref", "latency_slo_ref", "quality_gate_ref", "rollback_owner", "rollback_plan_ref", "maintenance_window", "postcheck_evidence_refs", "redacted_evidence_refs", "reviewer_outcome", "followup_owner", "not_approval", ] REQUIRED_OWNER_FIELDS = [ "owner_role_or_team", "decision", "decision_reason", "affected_scope", "provider_owner", "fallback_order_ref", "dry_run_result_ref", "benchmark_result_ref", "cost_review_ref", "privacy_review_ref", "data_classification_ref", "prompt_redaction_ref", "secret_handling_ref", "quota_budget_ref", "latency_slo_ref", "quality_gate_ref", "rollback_owner", "rollback_plan_ref", "maintenance_window", "postcheck_evidence_refs", "redacted_evidence_refs", "followup_owner", "not_approval", "no_secret_value_attestation", ] REVIEWER_CHECKS = [ {"check_id": "owner_identity_present", "instruction": "owner role / team 與 provider owner 必須可追溯。"}, {"check_id": "decision_reason_present", "instruction": "decision 與 decision reason 必須同時存在。"}, {"check_id": "affected_scope_matches_surface", "instruction": "affected scope 必須能對回既有 AI provider surface。"}, {"check_id": "redacted_refs_only", "instruction": "evidence 只能是脫敏 ref、hash、ticket、commit 或 artifact pointer。"}, {"check_id": "secret_value_absent", "instruction": "不得出現 API key、token、cookie、private key、env dump、完整 DSN 或 partial secret。"}, {"check_id": "live_endpoint_absent", "instruction": "不得保存 raw live endpoint、內網位址或可直接連線 URL。"}, {"check_id": "fallback_order_present", "instruction": "必須提供 fallback order 與 degraded mode ref,不接受憑印象切 provider。"}, {"check_id": "dry_run_result_present", "instruction": "必須有 dry-run 結果或明確不適用理由;dry-run 不得呼叫 production LLM。"}, {"check_id": "benchmark_result_present", "instruction": "必須有 benchmark / replay / fixture 結果與評分方法 ref。"}, {"check_id": "cost_review_present", "instruction": "涉及雲端或付費 provider 時必須有成本上限、quota、告警與停損 ref。"}, {"check_id": "privacy_review_present", "instruction": "涉及外部 provider、prompt、log 或 embedding 時必須有資料外送與脫敏審查 ref。"}, {"check_id": "data_classification_present", "instruction": "必須標示可送出、不可送出、需遮罩與只能本地處理的資料分類。"}, {"check_id": "prompt_redaction_present", "instruction": "prompt / log / incident context 需有 redaction proof,不接受 raw prompt 或 raw log。"}, {"check_id": "secret_handling_metadata_only", "instruction": "secret handling 只能描述注入路徑與 owner,不得貼 secret value / hash / partial token。"}, {"check_id": "quota_budget_present", "instruction": "付費 provider、雲端 provider 或外部 API 必須有 quota budget 與 stop condition。"}, {"check_id": "latency_slo_present", "instruction": "需要 latency / timeout / circuit breaker / degradation SLO ref。"}, {"check_id": "quality_gate_present", "instruction": "模型切換或 agent replacement 必須有品質門檻與 incumbent baseline。"}, {"check_id": "rollback_owner_present", "instruction": "rollback owner 與 rollback plan 必須存在。"}, {"check_id": "maintenance_window_present", "instruction": "provider route、proxy、env 或 model change 必須另有維護窗口。"}, {"check_id": "postcheck_evidence_present", "instruction": "post-check 必須覆蓋 provider route、fallback、cost、privacy、observability 與 rollback stop condition。"}, {"check_id": "no_runtime_request", "instruction": "夾帶 provider switch、Nginx reload、env change、external call、prompt send、benchmark live call 或 deploy 時拒收。"}, {"check_id": "no_false_green_provider_health", "instruction": "不得把 provider route 200、dashboard up、模型回覆一次成功當成 routing / privacy / cost 已驗收。"}, {"check_id": "cross_project_sync_present", "instruction": "影響 AwoooP、IwoooS、agent-bounty、code review、StockPlatform 或公開網站時需有跨專案同步 ref。"}, {"check_id": "counts_transition_safe", "instruction": "只有 reviewer record 可更新 received / accepted;不得同時開 runtime gate。"}, ] OUTCOME_LANES = [ {"lane_id": "waiting_owner_response", "meaning": "尚未收到 AI provider owner response;所有 accepted / runtime count 維持 0。"}, {"lane_id": "quarantine_secret_or_raw_payload", "meaning": "收到 secret、raw prompt、raw log、raw endpoint、未脫敏截圖或 env dump 時隔離。"}, {"lane_id": "reject_execution_request", "meaning": "夾帶 provider switch、外部呼叫、prompt send、Nginx reload、env change 或 deploy 要求時拒收。"}, {"lane_id": "request_fallback_supplement", "meaning": "缺 fallback order、degraded mode、circuit breaker 或 rollback 時要求補件。"}, {"lane_id": "request_cost_privacy_supplement", "meaning": "缺 cost / quota / privacy / data classification / prompt redaction 時要求補件。"}, {"lane_id": "request_benchmark_supplement", "meaning": "缺 dry-run、benchmark、fixture replay、quality gate 或 incumbent baseline 時要求補件。"}, {"lane_id": "legacy_model_card_redaction_required", "meaning": "舊模型卡、runbook 或報表含 raw endpoint / 主機識別時,先改成脫敏 alias。"}, {"lane_id": "ready_for_ai_provider_review", "meaning": "metadata 合格後,只能進 AI provider reviewer review。"}, {"lane_id": "owner_review_only_update", "meaning": "只允許更新只讀 owner review ledger,不切 provider、不呼叫模型、不改 proxy。"}, {"lane_id": "waiting_runtime_gate", "meaning": "即使 owner response accepted,runtime gate 仍需獨立人工批准。"}, ] BLOCKED_ACTIONS = [ "provider_switch", "model_route_change", "fallback_order_change", "ollama_proxy_change", "nginx_reload", "env_change", "runtime_config_change", "external_provider_call", "paid_provider_call", "prompt_send", "raw_prompt_storage", "raw_log_storage", "raw_endpoint_storage", "live_endpoint_probe", "benchmark_live_call", "dry_run_external_call", "quota_increase", "cost_limit_change", "privacy_boundary_change", "data_egress_change", "secret_value_collection", "secret_hash_collection", "partial_token_collection", "api_key_rotation", "secret_store_read", "model_download", "sdk_install", "agent_runtime_enable", "shadow_or_canary_enable", "production_deploy", "workflow_dispatch", "host_write", "ssh_read", "ssh_write", "active_scan", "mark_owner_response_accepted_without_reviewer_record", "open_runtime_gate", "add_action_button", ] SURFACES = [ { "surface_id": "ai_router_provider_policy", "label": "AI router provider policy / purpose mapping", "expected_scope": "purpose routing、provider priority、degraded mode", "config_kind": "provider_policy", "source_refs": [ "apps/api/src/services/ai_router.py", "apps/api/src/services/ai_provider_route_matrix.py", "apps/api/models.json", "docs/ai/AI-MODEL-CARDS.md", ], "write_capable_surface": True, "paid_provider_related": True, "data_egress_related": True, "requires_live_evidence": True, }, { "surface_id": "ollama_proxy_gateway", "label": "Ollama proxy gateway / local fallback boundary", "expected_scope": "proxy route、local fallback、gateway health", "config_kind": "proxy_gateway", "source_refs": [ "infra/ansible/roles/nginx/templates/110-ollama-proxy.conf.j2", "apps/api/src/services/ollama_endpoint_resolver.py", "apps/api/src/services/ollama_failover_manager.py", "apps/api/src/services/provider_proxy.py", ], "write_capable_surface": True, "paid_provider_related": False, "data_egress_related": False, "requires_live_evidence": True, }, { "surface_id": "fallback_order_and_circuit_breaker", "label": "Fallback order / circuit breaker", "expected_scope": "GCP / local / cloud fallback order、timeout、circuit breaker", "config_kind": "routing_guard", "source_refs": [ "apps/api/src/services/ollama_endpoint_circuit_breaker.py", "apps/api/src/services/ollama_health_monitor.py", "apps/api/src/services/failover_alerter.py", "apps/web/messages/zh-TW.json", ], "write_capable_surface": True, "paid_provider_related": True, "data_egress_related": True, "requires_live_evidence": True, }, { "surface_id": "cost_budget_and_quota", "label": "Cost budget / quota / paid provider stop condition", "expected_scope": "quota、token budget、cost alert、paid provider stop condition", "config_kind": "cost_policy", "source_refs": [ "docs/ai/AI-MODEL-CARDS.md", "apps/api/src/services/ai_rate_limiter.py", "apps/api/src/services/ai_slo_calculator.py", "apps/web/messages/zh-TW.json", ], "write_capable_surface": False, "paid_provider_related": True, "data_egress_related": True, "requires_live_evidence": True, }, { "surface_id": "privacy_data_egress_boundary", "label": "Privacy / data egress / prompt redaction boundary", "expected_scope": "prompt redaction、資料分級、外部 provider 資料外送", "config_kind": "privacy_policy", "source_refs": [ "docs/HARD_RULES.md", "docs/ai/AI-MODEL-CARDS.md", "apps/api/src/services/ai_providers/permissions.py", "apps/api/src/services/alertmanager_llm_guard.py", ], "write_capable_surface": False, "paid_provider_related": True, "data_egress_related": True, "requires_live_evidence": True, }, { "surface_id": "benchmark_dry_run_pack", "label": "Dry-run / benchmark / replay pack", "expected_scope": "dry-run、benchmark、fixture replay、quality gate", "config_kind": "evaluation_pack", "source_refs": [ "docs/runbooks/OPENCLAW-REPLACEMENT-EVALUATION.md", "docs/ai/agent-replacement-candidates.v1.json", "scripts/agents/run-agent-replacement-replay.py", "scripts/agents/evaluate-agent-promotion-gate.py", ], "write_capable_surface": False, "paid_provider_related": False, "data_egress_related": True, "requires_live_evidence": False, }, { "surface_id": "model_card_and_version_inventory", "label": "Model card / model version inventory", "expected_scope": "model card、version freshness、source-of-truth、redacted endpoint", "config_kind": "model_metadata", "source_refs": [ "docs/ai/AI-MODEL-CARDS.md", "apps/api/src/services/model_registry.py", "apps/api/src/services/model_version_tracker.py", "apps/api/src/services/model_version_probe.py", ], "write_capable_surface": True, "paid_provider_related": False, "data_egress_related": False, "requires_live_evidence": False, }, { "surface_id": "agent_replacement_candidate_runtime_boundary", "label": "Agent replacement / NemoTron / Hermes / OpenClaw candidate boundary", "expected_scope": "agent candidate、promotion gate、shadow/canary boundary", "config_kind": "agent_runtime_boundary", "source_refs": [ "docs/runbooks/OPENCLAW-REPLACEMENT-EVALUATION.md", "docs/ai/agent-market-watch-sources.v1.json", "docs/ai/agent-replacement-candidates.v1.json", "apps/api/src/services/ai_agent_tool_adoption_approval_package.py", ], "write_capable_surface": True, "paid_provider_related": True, "data_egress_related": True, "requires_live_evidence": True, }, ] def git_short_sha(root: Path) -> str: try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], cwd=root, check=True, capture_output=True, text=True, ) return result.stdout.strip() except Exception: return "unknown" def ref_digest(root: Path, refs: list[str]) -> str: digest = hashlib.sha256() for ref in refs: path = root / ref digest.update(ref.encode("utf-8")) if path.is_file(): digest.update(path.read_bytes()) elif path.is_dir(): digest.update(b"