feat(awooop): summarize gateway usage in truth chain
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 T10:MCP 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%。
|
||||
|
||||
Reference in New Issue
Block a user