feat(awooop): surface mcp investigation evidence
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m6s
CD Pipeline / build-and-deploy (push) Successful in 3m30s
CD Pipeline / post-deploy-checks (push) Successful in 2m12s

This commit is contained in:
Your Name
2026-05-18 13:55:27 +08:00
parent b9597d8d70
commit 9d02ab8080
7 changed files with 218 additions and 26 deletions

View File

@@ -50,6 +50,7 @@ _MAX_STEP_SUMMARY_CHARS = 128
_REMEDIATION_HISTORY_LIMIT = 20
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
_REMEDIATION_STATUS_FILTERS = {
"mcp_observed",
"no_evidence",
"read_only_dry_run",
"write_observed",
@@ -443,6 +444,22 @@ def _route_label_from_remediation(item: dict[str, Any]) -> str:
) or "--"
def _route_label_from_legacy_mcp(record: dict[str, Any]) -> str:
"""Render self-built/legacy MCP evidence as agent/tool/scope for list UX."""
tool = record.get("tool_name")
server = record.get("mcp_server")
tool_label = ".".join(str(part) for part in (server, tool) if part) or tool
return "/".join(
str(part)
for part in (
record.get("agent_role"),
tool_label,
"read",
)
if part
) or "--"
def _remediation_timeline_status(item: dict[str, Any]) -> str:
if item.get("success") is False or item.get("allowed") is False:
return "failed"
@@ -485,18 +502,31 @@ def _run_remediation_list_summary(
run: AwoooPRunState,
incident_ids: list[str],
items: list[dict[str, Any]],
legacy_mcp_records: list[dict[str, Any]] | None = None,
errors: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
"""Summarize durable ADR-100 dry-run evidence for list-level UX."""
"""Summarize durable ADR-100 dry-run and MCP investigation evidence for list UX."""
sorted_items = sorted(
(item for item in items if isinstance(item, dict)),
key=lambda item: str(item.get("created_at") or ""),
reverse=True,
)
sorted_mcp_records = sorted(
(record for record in (legacy_mcp_records or []) if isinstance(record, dict)),
key=lambda record: str(record.get("created_at") or ""),
reverse=True,
)
latest = sorted_items[0] if sorted_items else {}
latest_mcp = sorted_mcp_records[0] if sorted_mcp_records else {}
writes_incident = latest.get("writes_incident_state")
writes_auto_repair = latest.get("writes_auto_repair_result")
route = _route_label_from_remediation(latest) if latest else "--"
route = (
_route_label_from_remediation(latest)
if latest
else _route_label_from_legacy_mcp(latest_mcp)
if latest_mcp
else "--"
)
write_observed = writes_incident is True or writes_auto_repair is True
is_read_only = (
bool(latest)
@@ -504,9 +534,12 @@ def _run_remediation_list_summary(
and writes_incident is False
and writes_auto_repair is False
)
mcp_total = len(sorted_mcp_records)
mcp_success = sum(1 for record in sorted_mcp_records if record.get("success") is True)
mcp_failed = sum(1 for record in sorted_mcp_records if record.get("success") is False)
if not sorted_items:
status_value = "no_evidence"
status_value = "mcp_observed" if mcp_total > 0 else "no_evidence"
elif latest.get("success") is False or latest.get("allowed") is False:
status_value = "blocked"
elif write_observed:
@@ -518,22 +551,28 @@ def _run_remediation_list_summary(
return {
"schema_version": "awooop_run_remediation_summary_v1",
"source": "alert_operation_log",
"source": "alert_operation_log" if sorted_items else "mcp_audit_log" if mcp_total > 0 else "none",
"incident_ids": incident_ids,
"total": len(sorted_items),
"evidence_total": len(sorted_items) + mcp_total,
"status": status_value,
"has_dry_run": bool(sorted_items),
"has_mcp_investigation": mcp_total > 0,
"is_read_only": is_read_only,
"human_gate_open": run.state == "waiting_approval",
"latest_at": latest.get("created_at"),
"latest_preview": latest.get("verification_result_preview"),
"latest_mode": latest.get("mode"),
"latest_route": route,
"latest_agent_id": latest.get("agent_id"),
"latest_tool_name": latest.get("tool_name"),
"latest_required_scope": latest.get("required_scope"),
"latest_agent_id": latest.get("agent_id") or latest_mcp.get("agent_role"),
"latest_tool_name": latest.get("tool_name") or latest_mcp.get("tool_name"),
"latest_required_scope": latest.get("required_scope") or ("read" if latest_mcp else None),
"writes_incident_state": writes_incident,
"writes_auto_repair_result": writes_auto_repair,
"mcp_observation_total": mcp_total,
"mcp_observation_success": mcp_success,
"mcp_observation_failed": mcp_failed,
"latest_mcp_server": latest_mcp.get("mcp_server"),
"errors": errors or [],
}
@@ -602,6 +641,7 @@ async def _build_run_remediation_summaries(
_append_unique(all_incident_ids, incident_id)
histories_by_incident: dict[str, list[dict[str, Any]]] = {}
legacy_mcp_by_incident: dict[str, list[dict[str, Any]]] = {}
errors_by_incident: dict[str, dict[str, str]] = {}
if all_incident_ids:
from src.services.adr100_remediation_service import Adr100RemediationService
@@ -628,20 +668,27 @@ async def _build_run_remediation_summaries(
"incident_id": incident_id,
"error": str(exc),
}
legacy_mcp_by_incident = await _fetch_legacy_mcp_by_incident_ids(
all_incident_ids,
limit=min(max(len(all_incident_ids) * _REMEDIATION_HISTORY_LIMIT, 100), 5_000),
)
summaries: dict[UUID, dict[str, Any]] = {}
for run in runs:
incident_ids = incident_ids_by_run.get(run.run_id, [])
items: list[dict[str, Any]] = []
legacy_mcp_records: list[dict[str, Any]] = []
errors: list[dict[str, str]] = []
for incident_id in incident_ids:
items.extend(histories_by_incident.get(incident_id, []))
legacy_mcp_records.extend(legacy_mcp_by_incident.get(incident_id, []))
if incident_id in errors_by_incident:
errors.append(errors_by_incident[incident_id])
summaries[run.run_id] = _run_remediation_list_summary(
run=run,
incident_ids=incident_ids,
items=items,
legacy_mcp_records=legacy_mcp_records,
errors=errors,
)
return summaries
@@ -744,6 +791,31 @@ def _legacy_mcp_record(row: MCPAuditLog) -> dict[str, Any]:
}
async def _fetch_legacy_mcp_by_incident_ids(
incident_ids: list[str],
*,
limit: int,
) -> dict[str, list[dict[str, Any]]]:
"""Fetch legacy/self-built MCP rows for list evidence summaries."""
if not incident_ids:
return {}
async with get_db_context("awoooi") as db:
result = await db.execute(
select(MCPAuditLog)
.where(MCPAuditLog.incident_id.in_(incident_ids))
.order_by(MCPAuditLog.created_at.desc())
.limit(limit)
)
rows = list(result.scalars().all())
by_incident: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
if row.incident_id:
by_incident[row.incident_id].append(_legacy_mcp_record(row))
return dict(by_incident)
async def _fetch_run_legacy_mcp_history(
incident_ids: list[str],
*,