270 lines
9.6 KiB
Python
270 lines
9.6 KiB
Python
"""市場情報 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
|