"""市場情報 MCP 啟用證據審核 preview。 操作員可貼上外部 MCP 啟用後的 redacted evidence;本模組只做結構化審核, 不打 health endpoint、不開 DB、不寫入、不啟動 fetch。 """ REQUIRED_ENV_VARS = ("MCP_POSTGRES_PASSWORD", "TAVILY_API_KEY", "EXA_API_KEY") EXPECTED_HEALTH_TARGETS = ( "http://localhost:3001/health", "http://localhost:3002/health", "http://localhost:3003/health", "http://localhost:3004/health", ) SAFE_LITERAL_VALUES = {"redacted", "***", "set", "present", "true", "yes", "ok"} PROHIBITED_EVIDENCE_FLAGS = ( "database_write_executed", "database_commit_executed", "scheduler_attached", "crawler_job_started", "external_fetch_executed", "manual_fetch_executed", ) def _truthy(value): if isinstance(value, bool): return value if isinstance(value, (int, float)): return bool(value) if isinstance(value, str): return value.strip().lower() in {"1", "true", "yes", "y", "ok", "pass", "passed", "healthy", "set", "present", "redacted", "***"} return False def _looks_like_secret_literal(value): if not isinstance(value, str): return False normalized = value.strip().lower() if not normalized or normalized in SAFE_LITERAL_VALUES: return False return len(value.strip()) > 2 def _evidence_map(evidence, key): value = evidence.get(key) return value if isinstance(value, dict) else {} def _ack(evidence, key): acknowledgements = _evidence_map(evidence, "operator_acknowledgements") return _truthy(evidence.get(key)) or _truthy(acknowledgements.get(key)) def _env_statuses(evidence): env = _evidence_map(evidence, "required_env_vars") or _evidence_map(evidence, "env") statuses = [] for name in REQUIRED_ENV_VARS: value = env.get(name) statuses.append( { "name": name, "present": _truthy(value), "secret_literal_in_payload": _looks_like_secret_literal(value), "value_redacted": bool(value) and not _looks_like_secret_literal(value), } ) return statuses def _health_item_ok(item): if isinstance(item, bool): return item if isinstance(item, (int, float)): return int(item) == 200 if isinstance(item, str): return item.strip().lower() in {"200", "ok", "pass", "passed", "healthy"} if isinstance(item, dict): return ( int(item.get("status_code") or item.get("code") or 0) == 200 or _truthy(item.get("healthy")) or _truthy(item.get("passed")) or _truthy(item.get("status")) ) return False def _health_item_for_target(health, target): if isinstance(health, dict): if target in health: return health[target] port = target.rsplit(":", 1)[-1].split("/", 1)[0] return health.get(port) or health.get(f"localhost:{port}") if isinstance(health, list): for item in health: if not isinstance(item, dict): continue value = item.get("url") or item.get("target") or item.get("endpoint") port = str(item.get("port") or "") if value == target or (port and port in target): return item return None def _health_statuses(evidence): health = evidence.get("health_checks") or evidence.get("health_targets") or {} return [ { "target": target, "passed": _health_item_ok(_health_item_for_target(health, target)), } for target in EXPECTED_HEALTH_TARGETS ] def _sample_evidence_template(): return { "required_env_vars": { "MCP_POSTGRES_PASSWORD": "redacted", "TAVILY_API_KEY": "redacted", "EXA_API_KEY": "redacted", }, "health_checks": { "http://localhost:3001/health": 200, "http://localhost:3002/health": 200, "http://localhost:3003/health": 200, "http://localhost:3004/health": 200, }, "mcp_router_enabled": True, "market_intel_readonly_smoke_passed": True, "telemetry": { "mcp_calls_table_exists": True, "read_only_query_executed": True, }, "operator_acknowledgements": { "no_remove_orphans": True, "momo_db_preserved": True, "router_enabled_after_health": True, "rollback_plan_confirmed": True, "fetch_gate_left_closed": True, }, } def build_mcp_activation_evidence_preview(*, evidence=None, phase=None): """審核 redacted MCP runtime evidence;不執行任何外部動作。""" evidence = evidence or {} payload_received = bool(evidence) env_statuses = _env_statuses(evidence) health_statuses = _health_statuses(evidence) telemetry = _evidence_map(evidence, "telemetry") prohibited_flags = [ key for key in PROHIBITED_EVIDENCE_FLAGS if _truthy(evidence.get(key)) ] secret_literal_keys = [ item["name"] for item in env_statuses if item["secret_literal_in_payload"] ] gates = [ { "key": "evidence_payload_received", "passed": payload_received, "label": "已提供 redacted MCP runtime evidence payload", }, { "key": "no_secret_literals_in_payload", "passed": not secret_literal_keys, "label": "payload 只允許 redacted / boolean,不接受真實 secret 字串", }, { "key": "required_env_vars_acknowledged", "passed": all(item["present"] for item in env_statuses), "label": "三個必要 env 已由操作員確認存在", }, { "key": "all_health_targets_passed", "passed": all(item["passed"] for item in health_statuses), "label": "四個 localhost MCP health target 全部 200 / healthy", }, { "key": "mcp_router_enabled_after_health", "passed": _truthy(evidence.get("mcp_router_enabled")) and _ack(evidence, "router_enabled_after_health"), "label": "router 僅在 health 全過後啟用", }, { "key": "market_intel_readonly_smoke_passed", "passed": _truthy(evidence.get("market_intel_readonly_smoke_passed")), "label": "market_intel MCP read-only smoke 已通過", }, { "key": "telemetry_runtime_confirmed", "passed": _truthy(telemetry.get("mcp_calls_table_exists")) and _truthy(telemetry.get("read_only_query_executed")), "label": "mcp_calls telemetry read-only 查詢鏈路已確認", }, { "key": "operator_fallback_confirmed", "passed": _ack(evidence, "rollback_plan_confirmed"), "label": "router kill switch 與 stop MCP stack fallback 已確認", }, { "key": "production_boundaries_acknowledged", "passed": _ack(evidence, "no_remove_orphans") and _ack(evidence, "momo_db_preserved"), "label": "已確認不使用 remove-orphans 且不動 momo-db lifecycle", }, { "key": "manual_fetch_gate_left_closed", "passed": _ack(evidence, "fetch_gate_left_closed"), "label": "啟用 MCP 後仍未打開人工 fetch gate", }, { "key": "no_write_or_scheduler_evidence", "passed": not prohibited_flags, "label": "證據未宣告 DB write、scheduler 或 fetch 已執行", }, ] blocked_reasons = [ gate["key"] for gate in gates if not gate["passed"] ] accepted = payload_received and not blocked_reasons payload = { "mode": ( "mcp_activation_evidence_review" if payload_received else "mcp_activation_evidence_preview" ), "phase": phase, "evidence_payload_received": payload_received, "activation_evidence_accepted": accepted, "ready_for_runtime_promotion": accepted, "ready_for_fetch_gate_review": accepted, "external_mcp_runtime_evidence_complete": accepted, "internal_mcp_runtime_evidence_complete": accepted, "gate_count": len(gates), "passed_gate_count": sum(1 for gate in gates if gate["passed"]), "blocked_reasons": blocked_reasons, "gates": gates, "env_statuses": env_statuses, "health_statuses": health_statuses, "secret_literal_keys": secret_literal_keys, "prohibited_flags": prohibited_flags, "sample_evidence_template": _sample_evidence_template(), "next_operator_steps": [ "若本審核通過,再跑 /api/market_intel/mcp_readiness?execute=true&timeout=3 做只讀 runtime smoke", "確認 completion audit 的 external/internal runtime 缺口被證據補齊", "人工 fetch gate 仍需另行審核,不可因 MCP runtime ready 自動抓外站", ], "payload_persisted": False, "evidence_persisted": False, "api_executes_health_check": False, "api_executes_docker": False, "api_executes_ssh": False, "api_opens_database_connection": False, "api_writes_database": False, "api_uses_external_network": False, "database_session_created": False, "database_write_executed": False, "database_commit_executed": False, "external_network_executed": False, "scheduler_attached": False, "writes_executed": False, "would_write_database": False, } return payload