feat(awooop): surface mcp investigation evidence
This commit is contained in:
@@ -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],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user