feat(awooop): summarize gateway usage in truth chain
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m8s
CD Pipeline / build-and-deploy (push) Successful in 3m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m14s

This commit is contained in:
Your Name
2026-05-13 11:30:08 +08:00
parent 90603ad9bb
commit a99dccfc73
3 changed files with 193 additions and 2 deletions

View File

@@ -346,6 +346,122 @@ def _summarize_mcp(rows: list[dict[str, Any]]) -> dict[str, Any]:
}
def _as_bool(value: Any) -> bool:
if isinstance(value, bool):
return value
return str(value).lower() == "true"
def _counter_bucket(
buckets: dict[str, dict[str, Any]],
key: str,
*,
label: str,
) -> dict[str, Any]:
return buckets.setdefault(
key,
{
label: key,
"total": 0,
"success": 0,
"failed": 0,
"blocked": 0,
},
)
def _summarize_gateway_mcp(rows: list[dict[str, Any]]) -> dict[str, Any]:
by_agent: dict[str, dict[str, Any]] = {}
by_tool: dict[str, dict[str, Any]] = {}
by_scope: dict[str, dict[str, Any]] = {}
success_count = 0
failed_count = 0
blocked_count = 0
first_class_count = 0
bridge_count = 0
policy_enforced_count = 0
approval_executor_count = 0
for row in rows:
gate_result = row.get("gate_result") if isinstance(row.get("gate_result"), dict) else {}
gateway_path = str(gate_result.get("gateway_path") or "")
schema_version = str(gate_result.get("schema_version") or "")
policy_enforced = _as_bool(gate_result.get("policy_enforced"))
required_scope = str(gate_result.get("required_scope") or "unknown")
status = str(row.get("result_status") or "unknown").lower()
is_blocked = status == "blocked" or row.get("block_gate") is not None
is_success = status == "success"
is_failed = status == "failed"
agent_id = str(row.get("agent_id") or "unknown")
tool_name = str(row.get("tool_name") or "unknown")
if gateway_path == "awooop_mcp_gateway" and policy_enforced:
first_class_count += 1
if schema_version == "legacy_mcp_bridge_v1" or policy_enforced is False:
bridge_count += 1
if policy_enforced:
policy_enforced_count += 1
if agent_id == "approval_executor":
approval_executor_count += 1
if is_success:
success_count += 1
if is_failed:
failed_count += 1
if is_blocked:
blocked_count += 1
for bucket in (
_counter_bucket(by_agent, agent_id, label="agent_id"),
_counter_bucket(by_tool, tool_name, label="tool_name"),
_counter_bucket(by_scope, required_scope, label="required_scope"),
):
bucket["total"] += 1
if is_success:
bucket["success"] += 1
if is_failed:
bucket["failed"] += 1
if is_blocked:
bucket["blocked"] += 1
blockers: list[str] = []
if blocked_count:
stage = "gateway_blocked"
stage_status = "blocked"
blockers.append("mcp_gateway_blocked")
elif first_class_count and failed_count:
stage = "provider_failed_after_gateway"
stage_status = "failed"
blockers.append("provider_failed_after_gateway")
elif success_count:
stage = "gateway_execution_succeeded"
stage_status = "success"
elif bridge_count and not first_class_count:
stage = "legacy_bridge_only"
stage_status = "observed"
blockers.append("legacy_bridge_not_policy_enforced")
else:
stage = "no_gateway_records"
stage_status = "missing"
return {
"total": len(rows),
"success": success_count,
"failed": failed_count,
"blocked": blocked_count,
"first_class_total": first_class_count,
"legacy_bridge_total": bridge_count,
"policy_enforced_total": policy_enforced_count,
"approval_executor_total": approval_executor_count,
"stage": stage,
"stage_status": stage_status,
"needs_human": bool(blocked_count or (first_class_count and failed_count)),
"blockers": blockers,
"by_agent": list(by_agent.values()),
"by_tool": list(by_tool.values()),
"by_scope": list(by_scope.values()),
}
async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[str, Any]:
"""Return a read-only truth chain for an incident, drift report, or run id."""
async with get_db_context(project_id) as db:
@@ -683,6 +799,7 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
source_type = _source_type(source_id, incident, drift)
legacy_mcp_summary = _summarize_mcp(legacy_mcp_rows)
gateway_mcp_summary = _summarize_gateway_mcp(gateway_mcp_rows)
truth_status = _truth_status(
incident=incident,
approvals=approvals,
@@ -694,6 +811,13 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
legacy_mcp_total=legacy_mcp_summary["total"],
outbound_visible_total=len(outbound_rows),
)
if incident is None and drift is None and not runs and gateway_mcp_rows:
truth_status = {
"current_stage": gateway_mcp_summary["stage"],
"stage_status": gateway_mcp_summary["stage_status"],
"needs_human": gateway_mcp_summary["needs_human"],
"blockers": gateway_mcp_summary["blockers"],
}
reconciliation = build_incident_reconciliation(
incident=incident,
approvals=approvals,
@@ -713,7 +837,7 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
"project_id": project_id,
"source_id": source_id,
"source_type": source_type,
"found": incident is not None or drift is not None or bool(runs),
"found": incident is not None or drift is not None or bool(runs) or bool(gateway_mcp_rows),
"truth_status": truth_status,
"linked_ids": {
"incident_id": incident.get("incident_id") if incident else None,
@@ -734,7 +858,7 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
},
"mcp": {
"awooop_gateway": {
"total": len(gateway_mcp_rows),
**gateway_mcp_summary,
"records": gateway_mcp_rows,
},
"legacy": {

View File

@@ -10,6 +10,7 @@ from src.services.awooop_ansible_audit_service import (
from src.services.awooop_truth_chain_service import (
build_incident_reconciliation,
_clean_row,
_summarize_gateway_mcp,
_truth_status,
)
from src.services.drift_repeat_state import (
@@ -78,6 +79,38 @@ def test_truth_status_marks_repeated_pending_drift_as_human_needed() -> None:
assert "drift_ai_confidence_zero" in status["blockers"]
def test_gateway_summary_surfaces_first_class_approval_execution() -> None:
summary = _summarize_gateway_mcp([
{
"agent_id": "approval_executor",
"tool_name": "ssh_docker_restart",
"result_status": "failed",
"block_gate": None,
"gate_result": {
"schema_version": "awooop_mcp_gateway_audit_v1",
"gateway_path": "awooop_mcp_gateway",
"policy_enforced": True,
"required_scope": "write",
"is_shadow": False,
"gate5_approval": True,
},
}
])
assert summary["total"] == 1
assert summary["first_class_total"] == 1
assert summary["legacy_bridge_total"] == 0
assert summary["policy_enforced_total"] == 1
assert summary["approval_executor_total"] == 1
assert summary["stage"] == "provider_failed_after_gateway"
assert summary["stage_status"] == "failed"
assert summary["needs_human"] is True
assert summary["by_agent"][0]["agent_id"] == "approval_executor"
assert summary["by_tool"][0]["tool_name"] == "ssh_docker_restart"
assert summary["by_scope"][0]["required_scope"] == "write"
assert summary["by_scope"][0]["failed"] == 1
def _drift_item(
*,
resource_name: str = "awoooi-api",

View File

@@ -7104,3 +7104,37 @@ provider_error=Host '192.0.2.1' not in SSH_MCP_ALLOWED_HOSTS
- T9 已完成:已批准 SSH execution 會進 first-class `McpGateway`,不再是 `legacy_direct_provider`
- smoke 使用 `192.0.2.1` 保留位址,故 provider failure 是預期安全結果;重點是五閘門與 audit 已真實通過到 provider 前一層。
- 目前整體進度更新:約 65%。
### 2026-05-13 — AwoooP truth-chain T10MCP Gateway 使用狀態摘要local green
**目的**
- 讓 Operator 不只看到 Gateway raw records也能直接判斷「是否真的經過 AwoooP MCP Gateway」、「是不是 legacy bridge」、「哪個 agent/tool/scope」、「卡在 gate 還是 provider」。
- 對應 Telegram 告警看不出 AI 自動化流程進度的問題truth-chain 要提供可查、可聚合的狀態面。
**變更**
- `awooop_truth_chain_service.py` 新增 `_summarize_gateway_mcp()`
- `mcp.awooop_gateway` 現在包含:
- `first_class_total`
- `legacy_bridge_total`
- `policy_enforced_total`
- `approval_executor_total`
- `stage` / `stage_status` / `needs_human` / `blockers`
- `by_agent` / `by_tool` / `by_scope`
- 只用 Gateway trace id 查詢時,`found=true`,並以 Gateway summary 推導 truth status。
**local verification**
```text
python -m pytest tests/test_awooop_truth_chain_service.py tests/test_platform_router_order.py tests/test_awooop_operator_auth.py -q
19 passed
python -m ruff check --select F821 src/services/awooop_truth_chain_service.py tests/test_awooop_truth_chain_service.py
All checks passed
python -m py_compile src/services/awooop_truth_chain_service.py tests/test_awooop_truth_chain_service.py
OK
```
**目前整體進度**:約 66%。