Files
awoooi/apps/api/src/services/awooop_truth_chain_service.py
Your Name 159f514f55
Some checks failed
CD Pipeline / tests (push) Successful in 1m28s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
fix(awooop): cache heavy operator summaries
2026-06-01 09:20:18 +08:00

1885 lines
72 KiB
Python

"""AwoooP read-only truth chain aggregation.
T0 only: this service does not mutate incident, approval, execution, or channel
state. It stitches existing durable records into one operator-facing status so
Telegram cards can be audited without guessing which subsystem owns the truth.
"""
from __future__ import annotations
import asyncio
import json
import os
import shutil
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
from pathlib import Path
from typing import Any
from uuid import UUID
import structlog
from sqlalchemy import text
from src.core.config import settings
from src.db.base import get_db_context
from src.services.operator_outcome import build_operator_outcome
from src.services.awooop_ansible_check_mode_service import detect_ansible_transport_blockers
from src.services.awooop_ansible_audit_service import build_ansible_truth
from src.services.drift_repeat_state import build_drift_repeat_state
from src.services.operator_summary_cache import (
get_cached_operator_summary,
store_operator_summary,
)
logger = structlog.get_logger(__name__)
_MAX_ROWS = 100
_JSON_TEXT_FIELDS = {"gate_result", "source_envelope"}
_QUALITY_SUMMARY_CONCURRENCY = 8
_QUALITY_SUMMARY_CACHE_TTL_SECONDS = int(
os.getenv("AWOOOP_QUALITY_SUMMARY_CACHE_TTL_SECONDS", "30")
)
def _clean(value: Any) -> Any:
"""Convert DB values into JSON-friendly primitives."""
if isinstance(value, UUID):
return str(value)
if isinstance(value, (datetime, date)):
return value.isoformat()
if isinstance(value, Decimal):
return float(value)
if isinstance(value, dict):
return {str(k): _clean(v) for k, v in value.items()}
if isinstance(value, list):
return [_clean(v) for v in value]
return value
def _clean_row(row: Any) -> dict[str, Any]:
cleaned: dict[str, Any] = {}
for key, value in dict(row).items():
if key in _JSON_TEXT_FIELDS and isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
pass
cleaned[key] = _clean(value)
return cleaned
async def _fetch_all(db: Any, sql: str, params: dict[str, Any]) -> list[dict[str, Any]]:
result = await db.execute(text(sql), params)
return [_clean_row(row) for row in result.mappings().all()]
async def _fetch_one(
db: Any,
sql: str,
params: dict[str, Any],
) -> dict[str, Any] | None:
result = await db.execute(text(sql), params)
row = result.mappings().first()
return _clean_row(row) if row else None
def _source_type(source_id: str, incident: dict[str, Any] | None, drift: dict[str, Any] | None) -> str:
if incident is not None:
return "incident"
if drift is not None:
return "drift_report"
try:
UUID(source_id)
except ValueError:
return "unknown"
return "run"
def _failed_mcp_tools(evidence_rows: list[dict[str, Any]]) -> list[str]:
failed: set[str] = set()
for evidence in evidence_rows:
mcp_health = evidence.get("mcp_health")
if isinstance(mcp_health, dict):
failed.update(str(tool) for tool, ok in mcp_health.items() if ok is False)
return sorted(failed)
def _approval_ids(approvals: list[dict[str, Any]]) -> list[str]:
return [str(row["id"]) for row in approvals if row.get("id")]
def _operation_ids(automation_ops: list[dict[str, Any]]) -> list[str]:
return [str(row["op_id"]) for row in automation_ops if row.get("op_id")]
def _auto_repair_ids(auto_repair_executions: list[dict[str, Any]]) -> list[str]:
return [str(row["id"]) for row in auto_repair_executions if row.get("id")]
def _incident_fingerprints(incident: dict[str, Any] | None) -> list[str]:
"""Extract durable alert fingerprints from incident signals, when present."""
if not incident:
return []
signals = incident.get("signals")
if not isinstance(signals, list):
return []
fingerprints: set[str] = set()
for signal in signals:
if not isinstance(signal, dict):
continue
direct = signal.get("fingerprint")
if direct:
fingerprints.add(str(direct))
labels = signal.get("labels")
if isinstance(labels, dict):
value = labels.get("fingerprint")
if value:
fingerprints.add(str(value))
return sorted(fingerprints)
def _looks_like_no_action(value: Any) -> bool:
text = str(value or "").upper()
return (
"NO_ACTION" in text
or "NO-ACTION" in text
or "NOACTION" in text
or text.startswith("OBSERVE")
or text.startswith("INVESTIGATE")
)
def _approval_has_no_action(approvals: list[dict[str, Any]]) -> bool:
return any(_looks_like_no_action(row.get("action")) for row in approvals)
def _approval_suppresses_repair_execution(approvals: list[dict[str, Any]]) -> bool:
for row in approvals:
metadata = row.get("extra_metadata")
if not isinstance(metadata, dict):
continue
execution_kind = str(metadata.get("execution_kind") or "").lower()
if execution_kind in {"no_action", "diagnostic", "parse_failed", "unsupported_action"}:
return True
if metadata.get("repair_executed") is False and not execution_kind:
return True
return False
def _is_no_action_operation(row: dict[str, Any]) -> bool:
"""Return true for durable audit rows that represent observation, not repair."""
if str(row.get("operation_type") or "") != "playbook_executed":
return False
return any(
_looks_like_no_action(row.get(key))
for key in (
"input_action",
"output_action",
"output_reason",
"output_not_used_reason",
)
)
def _is_audit_only_operation(row: dict[str, Any]) -> bool:
operation_type = str(row.get("operation_type") or "")
status = str(row.get("status") or "").lower()
if status == "dry_run":
return True
return operation_type in {
"ansible_candidate_matched",
"ansible_execution_skipped",
}
def _effective_execution_ops(automation_ops: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
row
for row in automation_ops
if not _is_no_action_operation(row) and not _is_audit_only_operation(row)
]
def build_incident_reconciliation(
*,
incident: dict[str, Any] | None,
approvals: list[dict[str, Any]],
evidence_rows: list[dict[str, Any]],
automation_ops: list[dict[str, Any]],
auto_repair_executions: list[dict[str, Any]] | None = None,
timeline_events: list[dict[str, Any]],
) -> dict[str, Any]:
"""Build a read-only consistency report across incident lifecycle tables."""
if incident is None:
return {
"schema_version": "incident_reconciliation_v1",
"applicable": False,
"consistency_status": "not_applicable",
"operator_next_state": "not_applicable",
"facts": {},
"mismatches": [],
}
incident_status = str(incident.get("status") or "unknown").upper()
incident_closed = incident_status in {"RESOLVED", "CLOSED"}
latest_approval = approvals[0] if approvals else None
approval_status = str((latest_approval or {}).get("status") or "none").upper()
approval_action = str((latest_approval or {}).get("action") or "")
approval_resolved = bool((latest_approval or {}).get("resolved_at"))
attempted = sum(int(row.get("sensors_attempted") or 0) for row in evidence_rows)
succeeded = sum(int(row.get("sensors_succeeded") or 0) for row in evidence_rows)
repair_rows = auto_repair_executions or []
approval_suppressed = _approval_suppresses_repair_execution(approvals)
effective_ops = [] if approval_suppressed else _effective_execution_ops(automation_ops)
executed_ops = [
row
for row in effective_ops
if str(row.get("status") or "").lower()
in {"success", "completed", "executed"}
]
successful_repairs = [row for row in repair_rows if row.get("success") is True]
effective_execution_count = len(executed_ops) + len(successful_repairs)
latest_verification = _latest_verification_result(incident, evidence_rows)
mismatches: list[dict[str, Any]] = []
def add(code: str, severity: str, message: str) -> None:
mismatches.append({
"code": code,
"severity": severity,
"message": message,
})
if (
latest_approval
and not incident_closed
and (approval_resolved or approval_status in {"APPROVED", "REJECTED"})
):
if effective_execution_count:
add(
"incident_open_after_successful_execution",
"high",
"Execution evidence exists while the incident is still open.",
)
else:
add(
"incident_open_after_approval_resolved",
"high",
"Approval reached a terminal state while the incident is still open.",
)
if approval_status == "APPROVED" and not effective_execution_count:
add(
"approval_approved_without_execution_record",
"high",
"Approval is approved but automation_operation_log has no linked execution record.",
)
if (
approval_status == "APPROVED"
and "NO_ACTION" in approval_action.upper()
and not effective_execution_count
):
add(
"approval_no_action_without_execution",
"high",
"Approval resolved to NO_ACTION and no executor produced a successful operation.",
)
if successful_repairs and str(latest_verification or "").lower() == "degraded":
add(
"verification_degraded_after_auto_repair",
"medium",
"Auto-repair succeeded, but post-execution verification reported degraded evidence.",
)
if attempted > 0 and succeeded == 0:
add(
"evidence_all_sensors_failed",
"medium",
"Evidence collection attempted sensors but none succeeded.",
)
if latest_approval and not timeline_events:
add(
"timeline_missing_for_approval",
"medium",
"Approval exists but timeline_events has no linked lifecycle entries.",
)
high_count = sum(1 for row in mismatches if row["severity"] == "high")
medium_count = sum(1 for row in mismatches if row["severity"] == "medium")
if high_count:
consistency_status = "blocked"
operator_next_state = "manual_required"
elif medium_count:
consistency_status = "degraded"
operator_next_state = "investigate"
else:
consistency_status = "consistent"
operator_next_state = "continue"
return {
"schema_version": "incident_reconciliation_v1",
"applicable": True,
"consistency_status": consistency_status,
"operator_next_state": operator_next_state,
"facts": {
"incident_id": incident.get("incident_id"),
"incident_status": incident_status,
"incident_closed": incident_closed,
"latest_approval_id": (latest_approval or {}).get("id"),
"latest_approval_status": approval_status,
"latest_approval_action": approval_action,
"approval_resolved": approval_resolved,
"evidence_records": len(evidence_rows),
"sensors_attempted": attempted,
"sensors_succeeded": succeeded,
"automation_operation_records": len(automation_ops),
"executed_operation_records": len(executed_ops),
"auto_repair_execution_records": len(repair_rows),
"successful_auto_repair_records": len(successful_repairs),
"effective_execution_records": effective_execution_count,
"latest_verification_result": latest_verification,
"timeline_events": len(timeline_events),
},
"mismatches": mismatches,
}
def _truth_status(
*,
incident: dict[str, Any] | None,
approvals: list[dict[str, Any]],
evidence_rows: list[dict[str, Any]],
automation_ops: list[dict[str, Any]],
drift: dict[str, Any] | None,
drift_repeat_count: int,
gateway_mcp_total: int,
legacy_mcp_total: int,
outbound_visible_total: int,
inbound_visible_total: int = 0,
auto_repair_executions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Derive the current operator-visible truth-chain stage."""
blockers: list[str] = []
needs_human = False
stage = "not_found"
stage_status = "missing"
if drift is not None:
stage = "dedup_or_repeat_updated" if drift_repeat_count > 1 else "received"
stage_status = str(drift.get("status") or "unknown")
interpretation = drift.get("interpretation") or {}
confidence = interpretation.get("confidence") if isinstance(interpretation, dict) else None
if stage_status == "pending":
needs_human = True
blockers.append("drift_report_pending_without_resolution")
if confidence in (0, 0.0):
blockers.append("drift_ai_confidence_zero")
if incident is None and drift is None and inbound_visible_total > 0:
stage = "inbound_received"
stage_status = "observed"
if incident is not None:
incident_status = str(incident.get("status") or "unknown")
repair_rows = auto_repair_executions or []
approval_suppressed = _approval_suppresses_repair_execution(approvals)
effective_ops = [] if approval_suppressed else _effective_execution_ops(automation_ops)
has_execution_records = bool(effective_ops or repair_rows)
stage = "received"
stage_status = incident_status.lower()
if incident_status in {"RESOLVED", "CLOSED"}:
stage = "resolved"
stage_status = "success"
elif evidence_rows:
attempted = sum(int(row.get("sensors_attempted") or 0) for row in evidence_rows)
succeeded = sum(int(row.get("sensors_succeeded") or 0) for row in evidence_rows)
if attempted > 0 and succeeded == 0:
stage = "evidence_degraded"
stage_status = "warning"
needs_human = True
blockers.append("all_evidence_sensors_failed")
else:
stage = "evidence_collected"
stage_status = "completed"
approval_statuses = {str(row.get("status") or "").upper() for row in approvals}
approval_actions = " ".join(str(row.get("action") or "") for row in approvals).upper()
approval_no_action = _approval_has_no_action(approvals)
if any(status in {"PENDING", "WAITING_APPROVAL"} for status in approval_statuses):
stage = "approval_required"
stage_status = "waiting"
needs_human = True
blockers.append("pending_human_approval")
elif "EXPIRED" in approval_statuses and not has_execution_records:
stage = "approval_expired"
stage_status = "expired"
needs_human = True
blockers.append("approval_expired_without_operator_decision")
elif "REJECTED" in approval_statuses and not has_execution_records:
stage = "approval_rejected"
stage_status = "closed"
elif "EXECUTION_FAILED" in approval_statuses and not has_execution_records:
stage = "execution_failed"
stage_status = "error"
needs_human = True
blockers.append("approval_execution_failed_without_execution_record")
elif not has_execution_records and (approval_no_action or "NO_ACTION" in approval_actions):
if approval_statuses:
stage = "manual_required"
stage_status = "blocked"
needs_human = True
blockers.append("approval_resolved_no_action_without_execution")
elif "APPROVED" in approval_statuses and not has_execution_records:
stage = "execution_missing"
stage_status = "blocked"
needs_human = True
blockers.append("approved_without_execution_record")
op_statuses = {str(row.get("status") or "").lower() for row in effective_ops}
repair_successes = {row.get("success") for row in repair_rows}
execution_succeeded = (op_statuses & {"success", "completed"}) or True in repair_successes
execution_failed = (op_statuses & {"failed", "error"}) or False in repair_successes
if op_statuses or repair_successes:
if execution_succeeded:
stage = "execution_succeeded"
stage_status = "success"
elif execution_failed:
stage = "execution_failed"
stage_status = "error"
needs_human = True
else:
stage = "execution_started"
stage_status = "running"
if incident_status == "INVESTIGATING" and approvals:
if execution_succeeded:
blockers.append("incident_open_after_successful_execution")
needs_human = True
elif not has_execution_records:
blockers.append("incident_still_investigating_after_approval")
if gateway_mcp_total == 0:
blockers.append("awooop_mcp_gateway_audit_empty")
if legacy_mcp_total == 0 and incident is not None:
blockers.append("legacy_mcp_audit_missing")
if outbound_visible_total == 0:
blockers.append("outbound_mirror_not_visible_for_source")
return {
"current_stage": stage,
"stage_status": stage_status,
"needs_human": needs_human,
"blockers": blockers,
}
def _latest_verification_result(
incident: dict[str, Any] | None,
evidence_rows: list[dict[str, Any]],
) -> str | None:
if incident and incident.get("verification_result"):
return str(incident["verification_result"])
for row in evidence_rows:
if row.get("verification_result"):
return str(row["verification_result"])
return None
def build_automation_quality(
*,
incident: dict[str, Any] | None,
approvals: list[dict[str, Any]],
evidence_rows: list[dict[str, Any]],
automation_ops: list[dict[str, Any]],
auto_repair_executions: list[dict[str, Any]],
gateway_mcp_summary: dict[str, Any],
legacy_mcp_summary: dict[str, Any],
outbound_rows: list[dict[str, Any]],
km_entries: list[dict[str, Any]],
timeline_events: list[dict[str, Any]],
) -> dict[str, Any]:
"""Summarize whether a card reached the real automation flywheel."""
if incident is None:
return {
"schema_version": "automation_quality_v1",
"applicable": False,
"verdict": "not_applicable",
"score": 0,
"gates": [],
"facts": {},
"blockers": [],
}
blockers: list[str] = []
gates: list[dict[str, Any]] = []
def gate(name: str, status: str, detail: str | None = None) -> None:
gates.append({"name": name, "status": status, "detail": detail})
if status in {"failed", "missing"}:
blockers.append(name)
evidence_attempted = sum(int(row.get("sensors_attempted") or 0) for row in evidence_rows)
evidence_succeeded = sum(int(row.get("sensors_succeeded") or 0) for row in evidence_rows)
gateway_total = int(gateway_mcp_summary.get("total") or 0)
legacy_total = int(legacy_mcp_summary.get("total") or 0)
approval_statuses = {str(row.get("status") or "").upper() for row in approvals}
approval_actions = " ".join(str(row.get("action") or "") for row in approvals).upper()
approval_no_action = _approval_has_no_action(approvals)
approval_suppressed = _approval_suppresses_repair_execution(approvals)
effective_ops = (
[]
if approval_suppressed
else _effective_execution_ops(automation_ops)
)
noop_ops = [row for row in automation_ops if _is_no_action_operation(row)]
audit_only_ops = [row for row in automation_ops if _is_audit_only_operation(row)]
automation_statuses = {str(row.get("status") or "").lower() for row in effective_ops}
auto_repair_successes = {row.get("success") for row in auto_repair_executions}
has_execution = bool(effective_ops or auto_repair_executions)
verification_result = _latest_verification_result(incident, evidence_rows)
gate("source_persisted", "passed", str(incident.get("incident_id")))
gate("outbound_recorded", "passed" if outbound_rows else "missing", str(len(outbound_rows)))
if not evidence_rows:
gate("evidence_collected", "missing", "no incident_evidence rows")
elif evidence_attempted > 0 and evidence_succeeded == 0:
gate("evidence_collected", "failed", f"{evidence_succeeded}/{evidence_attempted}")
elif evidence_succeeded > 0:
gate("evidence_collected", "passed", f"{evidence_succeeded}/{evidence_attempted}")
else:
gate("evidence_collected", "warning", f"{evidence_succeeded}/{evidence_attempted}")
if gateway_total > 0:
gate("mcp_gateway_observed", "passed", str(gateway_total))
elif legacy_total > 0:
gate("mcp_gateway_observed", "warning", f"legacy_only={legacy_total}")
else:
gate("mcp_gateway_observed", "missing", "no mcp audit")
if any(status in {"PENDING", "WAITING_APPROVAL"} for status in approval_statuses):
gate("approval_state", "warning", "waiting_approval")
elif "EXPIRED" in approval_statuses and not has_execution:
gate("approval_state", "warning", "expired_without_execution")
elif "REJECTED" in approval_statuses and not has_execution:
gate("approval_state", "passed", "rejected_no_execution")
elif approval_statuses and approval_suppressed and not has_execution:
gate("approval_state", "warning", "approved_diagnostic_or_observe_only")
elif approval_statuses and (approval_no_action or "NO_ACTION" in approval_actions) and not has_execution:
gate("approval_state", "failed", "approved_no_action_without_execution")
elif approvals:
gate("approval_state", "passed", ",".join(sorted(approval_statuses)))
else:
gate("approval_state", "not_applicable", "no approval")
gate("execution_recorded", "passed" if has_execution else "missing", str(len(effective_ops) + len(auto_repair_executions)))
gate("auto_repair_recorded", "passed" if auto_repair_executions else "missing", str(len(auto_repair_executions)))
verification_status = str(verification_result or "").lower()
if not has_execution:
gate("verification_recorded", "not_applicable", "no execution")
elif verification_status == "success":
gate("verification_recorded", "passed", verification_result)
elif verification_result:
gate("verification_recorded", "warning", verification_result)
else:
gate("verification_recorded", "missing", "execution without verification_result")
if not has_execution:
gate("learning_recorded", "not_applicable", "no execution")
elif km_entries:
gate("learning_recorded", "passed", str(len(km_entries)))
else:
gate("learning_recorded", "missing", "execution without KM entry")
gate("timeline_recorded", "passed" if timeline_events else "missing", str(len(timeline_events)))
if has_execution and (
False in auto_repair_successes or automation_statuses & {"failed", "error"}
):
verdict = "execution_failed"
elif has_execution and verification_status == "success":
verdict = "auto_repaired_verified"
elif has_execution and verification_result:
verdict = "auto_repaired_verification_degraded"
elif has_execution:
verdict = "execution_unverified"
elif "EXPIRED" in approval_statuses:
verdict = "approval_expired_manual_review"
elif "REJECTED" in approval_statuses:
verdict = "approval_rejected_no_execution"
elif approval_suppressed:
verdict = "manual_required_diagnostic_only"
elif approval_statuses and (approval_no_action or "NO_ACTION" in approval_actions):
verdict = "manual_required_no_action"
elif any(status in {"PENDING", "WAITING_APPROVAL"} for status in approval_statuses):
verdict = "approval_required"
elif evidence_rows or gateway_total or legacy_total:
verdict = "observed_not_executed"
else:
verdict = "received_only"
score_weights = {
"source_persisted": 10,
"outbound_recorded": 10,
"evidence_collected": 15,
"mcp_gateway_observed": 15,
"approval_state": 10,
"execution_recorded": 15,
"auto_repair_recorded": 10,
"verification_recorded": 10,
"learning_recorded": 3,
"timeline_recorded": 2,
}
score = 0
for row in gates:
weight = score_weights.get(str(row["name"]), 0)
if row["status"] == "passed":
score += weight
elif row["status"] == "not_applicable" and row["name"] == "approval_state":
score += weight
elif row["status"] == "warning":
score += weight // 2
return {
"schema_version": "automation_quality_v1",
"applicable": True,
"verdict": verdict,
"score": score,
"gates": gates,
"facts": {
"incident_id": incident.get("incident_id"),
"evidence_records": len(evidence_rows),
"sensors_attempted": evidence_attempted,
"sensors_succeeded": evidence_succeeded,
"mcp_gateway_total": gateway_total,
"legacy_mcp_total": legacy_total,
"approvals": len(approvals),
"automation_operation_records": len(automation_ops),
"effective_execution_records": len(effective_ops),
"noop_operation_records": len(noop_ops),
"audit_only_operation_records": len(audit_only_ops),
"approval_repair_suppressed": approval_suppressed,
"auto_repair_execution_records": len(auto_repair_executions),
"verification_result": verification_result,
"knowledge_entries": len(km_entries),
"timeline_events": len(timeline_events),
"outbound_messages": len(outbound_rows),
},
"blockers": blockers,
}
def _automation_quality_score_bucket(score: int) -> str:
if score >= 85:
return "green"
if score >= 60:
return "yellow"
return "red"
def _int_value(value: Any, fallback: int = 0) -> int:
try:
return int(value)
except (TypeError, ValueError):
return fallback
def _execution_backend_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
summary = {
"operation_records_total": 0,
"effective_execution_records_total": 0,
"noop_operation_records_total": 0,
"audit_only_operation_records_total": 0,
"auto_repair_execution_records_total": 0,
"ansible_considered_total": 0,
"ansible_audit_record_total": 0,
"ansible_candidate_total": 0,
"ansible_check_mode_total": 0,
"ansible_apply_total": 0,
"ansible_rollback_total": 0,
"ansible_pending_check_mode_total": 0,
}
for record in records:
quality = record.get("automation_quality") if isinstance(record.get("automation_quality"), dict) else {}
facts = quality.get("facts") if isinstance(quality.get("facts"), dict) else {}
execution = record.get("execution") if isinstance(record.get("execution"), dict) else {}
ops = (
execution.get("automation_operation_log")
if isinstance(execution.get("automation_operation_log"), list)
else []
)
auto_repair_executions = (
execution.get("auto_repair_executions")
if isinstance(execution.get("auto_repair_executions"), list)
else []
)
ansible = execution.get("ansible") if isinstance(execution.get("ansible"), dict) else {}
ansible_records = ansible.get("records") if isinstance(ansible.get("records"), list) else []
catalog = (
ansible.get("candidate_catalog")
if isinstance(ansible.get("candidate_catalog"), dict)
else {}
)
candidates = catalog.get("candidates") if isinstance(catalog.get("candidates"), list) else []
summary["operation_records_total"] += _int_value(facts.get("automation_operation_records"), len(ops))
summary["effective_execution_records_total"] += _int_value(
facts.get("effective_execution_records"),
len(_effective_execution_ops([row for row in ops if isinstance(row, dict)])),
)
summary["noop_operation_records_total"] += _int_value(
facts.get("noop_operation_records"),
sum(1 for row in ops if isinstance(row, dict) and _is_no_action_operation(row)),
)
summary["audit_only_operation_records_total"] += _int_value(
facts.get("audit_only_operation_records"),
sum(1 for row in ops if isinstance(row, dict) and _is_audit_only_operation(row)),
)
summary["auto_repair_execution_records_total"] += _int_value(
facts.get("auto_repair_execution_records"),
len(auto_repair_executions),
)
if ansible.get("considered") is True:
summary["ansible_considered_total"] += 1
summary["ansible_audit_record_total"] += len(ansible_records)
summary["ansible_candidate_total"] += len(candidates)
terminal_check_mode_parent_ids = {
str(row.get("parent_op_id"))
for row in ansible_records
if isinstance(row, dict)
and str(row.get("operation_type") or "") in {
"ansible_check_mode_executed",
"ansible_execution_skipped",
}
and row.get("parent_op_id")
}
for row in ansible_records:
if not isinstance(row, dict):
continue
operation_type = str(row.get("operation_type") or "")
status = str(row.get("status") or "").lower()
if operation_type == "ansible_check_mode_executed" and status != "pending":
summary["ansible_check_mode_total"] += 1
elif operation_type == "ansible_apply_executed":
summary["ansible_apply_total"] += 1
elif operation_type == "ansible_rollback_executed":
summary["ansible_rollback_total"] += 1
elif (
operation_type == "ansible_candidate_matched"
and str(row.get("op_id")) not in terminal_check_mode_parent_ids
):
summary["ansible_pending_check_mode_total"] += 1
return summary
def _ansible_observed_runtime_blockers(records: list[dict[str, Any]]) -> list[str]:
blockers: set[str] = set()
for record in records:
execution = record.get("execution") if isinstance(record.get("execution"), dict) else {}
ansible = execution.get("ansible") if isinstance(execution.get("ansible"), dict) else {}
ansible_records = ansible.get("records") if isinstance(ansible.get("records"), list) else []
for row in ansible_records:
if not isinstance(row, dict):
continue
if str(row.get("operation_type") or "") != "ansible_check_mode_executed":
continue
dry_run_result = row.get("dry_run_result") if isinstance(row.get("dry_run_result"), dict) else {}
blockers.update(
detect_ansible_transport_blockers(
row.get("error"),
dry_run_result.get("stdout_tail"),
dry_run_result.get("stderr_tail"),
)
)
return sorted(blockers)
def _ansible_playbook_roots(module_path: Path | None = None) -> list[Path]:
resolved_module_path = (module_path or Path(__file__)).resolve()
return [
Path("/app/infra/ansible"),
Path.cwd() / "infra" / "ansible",
*(parent / "infra" / "ansible" for parent in resolved_module_path.parents),
]
def _path_readable(path: Path) -> bool:
return path.exists() and path.is_file() and os.access(path, os.R_OK)
def _check_mode_uses_repair_forced_command_transport() -> bool:
return Path(settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH) == Path("/etc/repair-ssh/id_ed25519")
def _ansible_runtime_readiness(
*,
check_mode_ssh_key_path: Path | None = None,
check_mode_known_hosts_path: Path | None = None,
repair_ssh_key_path: Path = Path("/etc/repair-ssh/id_ed25519"),
repair_known_hosts_path: Path = Path("/etc/repair-known-hosts/known_hosts"),
) -> dict[str, Any]:
playbook_roots = _ansible_playbook_roots()
playbook_root = next((path for path in playbook_roots if path.exists()), None)
playbook_paths = (
sorted((playbook_root / "playbooks").glob("*.yml"))
if playbook_root is not None and (playbook_root / "playbooks").exists()
else []
)
inventory_path = playbook_root / "inventory" / "hosts.yml" if playbook_root is not None else None
binary_path = shutil.which("ansible-playbook")
selected_ssh_key_path = check_mode_ssh_key_path or Path(settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH)
selected_known_hosts_path = check_mode_known_hosts_path or Path(
settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH
)
check_mode_ssh_key_readable = _path_readable(selected_ssh_key_path)
check_mode_known_hosts_readable = _path_readable(selected_known_hosts_path)
repair_ssh_key_readable = _path_readable(repair_ssh_key_path)
repair_known_hosts_readable = _path_readable(repair_known_hosts_path)
blockers: list[str] = []
if not binary_path:
blockers.append("ansible_playbook_binary_missing")
if playbook_root is None:
blockers.append("ansible_playbook_catalog_missing")
if inventory_path is None or not inventory_path.exists():
blockers.append("ansible_inventory_missing")
if not playbook_paths:
blockers.append("ansible_playbooks_missing")
if not check_mode_ssh_key_readable:
blockers.append("ansible_check_mode_ssh_key_missing")
if not check_mode_known_hosts_readable:
blockers.append("ansible_check_mode_known_hosts_missing")
return {
"ansible_playbook_binary_present": bool(binary_path),
"ansible_playbook_binary_path": binary_path,
"playbook_root_present": playbook_root is not None,
"playbook_root": str(playbook_root) if playbook_root is not None else None,
"inventory_present": bool(inventory_path and inventory_path.exists()),
"playbook_count": len(playbook_paths),
"check_mode_transport_profile": settings.AWOOOP_ANSIBLE_CHECK_MODE_TRANSPORT_PROFILE,
"check_mode_ssh_key_present": selected_ssh_key_path.exists(),
"check_mode_ssh_key_readable": check_mode_ssh_key_readable,
"check_mode_ssh_key_path": str(selected_ssh_key_path),
"check_mode_known_hosts_present": selected_known_hosts_path.exists(),
"check_mode_known_hosts_readable": check_mode_known_hosts_readable,
"check_mode_known_hosts_path": str(selected_known_hosts_path),
"check_mode_candidate_max_age_hours": settings.AWOOOP_ANSIBLE_CHECK_MODE_CANDIDATE_MAX_AGE_HOURS,
"repair_ssh_key_present": repair_ssh_key_path.exists(),
"repair_ssh_key_readable": repair_ssh_key_readable,
"repair_ssh_key_path": str(repair_ssh_key_path),
"repair_known_hosts_present": repair_known_hosts_path.exists(),
"repair_known_hosts_readable": repair_known_hosts_readable,
"repair_known_hosts_path": str(repair_known_hosts_path),
"can_run_check_mode": not blockers,
"blockers": blockers,
}
def summarize_automation_quality_records(
*,
project_id: str,
window_hours: int,
records: list[dict[str, Any]],
limit: int,
) -> dict[str, Any]:
"""Aggregate per-incident automation quality into an operator summary."""
verdicts: dict[str, dict[str, Any]] = {}
gate_failures: dict[str, dict[str, Any]] = {}
score_buckets: dict[str, int] = {"green": 0, "yellow": 0, "red": 0}
examples: list[dict[str, Any]] = []
total_score = 0
evaluated_total = 0
verified_total = 0
for record in records:
incident = record.get("incident") if isinstance(record.get("incident"), dict) else {}
truth_status = record.get("truth_status") if isinstance(record.get("truth_status"), dict) else {}
quality = record.get("automation_quality") if isinstance(record.get("automation_quality"), dict) else {}
if quality.get("applicable") is not True:
continue
evaluated_total += 1
score = int(quality.get("score") or 0)
total_score += score
bucket = _automation_quality_score_bucket(score)
score_buckets[bucket] += 1
verdict = str(quality.get("verdict") or "unknown")
if verdict == "auto_repaired_verified":
verified_total += 1
verdict_row = verdicts.setdefault(
verdict,
{
"verdict": verdict,
"total": 0,
"score_sum": 0,
"min_score": score,
"max_score": score,
"needs_human": False,
},
)
verdict_row["total"] += 1
verdict_row["score_sum"] += score
verdict_row["min_score"] = min(int(verdict_row["min_score"]), score)
verdict_row["max_score"] = max(int(verdict_row["max_score"]), score)
verdict_row["needs_human"] = bool(
verdict_row["needs_human"] or truth_status.get("needs_human")
)
for gate in quality.get("gates") or []:
if not isinstance(gate, dict):
continue
gate_status = str(gate.get("status") or "")
if gate_status not in {"failed", "missing"}:
continue
gate_name = str(gate.get("name") or "unknown")
gate_row = gate_failures.setdefault(
gate_name,
{"gate": gate_name, "total": 0, "statuses": {}},
)
gate_row["total"] += 1
gate_row["statuses"][gate_status] = int(gate_row["statuses"].get(gate_status, 0)) + 1
examples.append({
"incident_id": incident.get("incident_id"),
"alertname": incident.get("alertname"),
"severity": incident.get("severity"),
"status": incident.get("status"),
"created_at": incident.get("created_at"),
"truth_stage": truth_status.get("current_stage"),
"truth_stage_status": truth_status.get("stage_status"),
"needs_human": bool(truth_status.get("needs_human")),
"verdict": verdict,
"score": score,
"score_bucket": bucket,
"blockers": list(quality.get("blockers") or [])[:8],
})
by_verdict = []
for row in verdicts.values():
total = int(row["total"])
row["avg_score"] = round(float(row.pop("score_sum")) / total, 1) if total else 0.0
by_verdict.append(row)
by_verdict.sort(key=lambda row: (-int(row["total"]), str(row["verdict"])))
failing_gates = sorted(
gate_failures.values(),
key=lambda row: (-int(row["total"]), str(row["gate"])),
)
ansible_runtime = _ansible_runtime_readiness()
observed_ansible_blockers = _ansible_observed_runtime_blockers(records)
if observed_ansible_blockers:
ansible_runtime["observed_transport_blockers"] = observed_ansible_blockers
if _check_mode_uses_repair_forced_command_transport():
ansible_runtime["blockers"] = sorted(
set(ansible_runtime.get("blockers") or []) | set(observed_ansible_blockers)
)
ansible_runtime["can_run_check_mode"] = False
else:
ansible_runtime["historical_transport_blockers"] = observed_ansible_blockers
return {
"schema_version": "automation_quality_summary_v1",
"project_id": project_id,
"window_hours": window_hours,
"limit": limit,
"incident_total": len(records),
"evaluated_total": evaluated_total,
"verified_auto_repair_total": verified_total,
"average_score": round(total_score / evaluated_total, 1) if evaluated_total else 0.0,
"score_buckets": score_buckets,
"by_verdict": by_verdict,
"gate_failures": failing_gates,
"execution_backend_summary": _execution_backend_summary(records),
"ansible_runtime": ansible_runtime,
"examples": examples[:25],
"production_claim": {
"can_claim_full_auto_repair": evaluated_total > 0 and verified_total == evaluated_total,
"reason": (
"all_evaluated_incidents_auto_repaired_verified"
if evaluated_total > 0 and verified_total == evaluated_total
else "some_incidents_are_not_auto_repaired_verified"
),
},
}
def _summarize_mcp(rows: list[dict[str, Any]]) -> dict[str, Any]:
by_tool: dict[str, dict[str, Any]] = {}
success_count = 0
failure_count = 0
for row in rows:
key = f"{row.get('mcp_server') or 'unknown'}:{row.get('tool_name') or 'unknown'}"
item = by_tool.setdefault(
key,
{
"mcp_server": row.get("mcp_server"),
"tool_name": row.get("tool_name"),
"success": 0,
"failed": 0,
"last_error": None,
},
)
if row.get("success") is True:
item["success"] += 1
success_count += 1
else:
item["failed"] += 1
failure_count += 1
item["last_error"] = row.get("error_message")
return {
"total": len(rows),
"success": success_count,
"failed": failure_count,
"by_tool": list(by_tool.values()),
}
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:
incident = await _fetch_one(
db,
"""
SELECT
incident_id,
project_id,
status::text AS status,
severity::text AS severity,
alertname,
alert_category,
notification_type,
created_at,
updated_at,
resolved_at,
verification_result,
frequency_snapshot,
signals,
decision_chain
FROM incidents
WHERE incident_id = :source_id
AND (project_id = :project_id OR project_id IS NULL)
""",
{"source_id": source_id, "project_id": project_id},
)
drift = await _fetch_one(
db,
"""
SELECT
report_id,
namespace,
status,
triggered_by,
high_count,
medium_count,
info_count,
scanned_at,
created_at,
resolved_at,
interpretation,
items,
narrative_text
FROM drift_reports
WHERE report_id = :source_id
""",
{"source_id": source_id},
)
runs = await _fetch_all(
db,
"""
SELECT
run_id,
project_id,
agent_id,
state,
trigger_type,
trigger_ref,
is_shadow,
step_count,
created_at,
started_at,
completed_at,
error_code,
error_detail
FROM awooop_run_state
WHERE project_id = :project_id
AND (run_id::text = :source_id OR trigger_ref = :source_id)
ORDER BY created_at DESC
LIMIT :limit
""",
{"source_id": source_id, "project_id": project_id, "limit": _MAX_ROWS},
)
approvals: list[dict[str, Any]] = []
evidence_rows: list[dict[str, Any]] = []
timeline_events: list[dict[str, Any]] = []
legacy_mcp_rows: list[dict[str, Any]] = []
automation_ops: list[dict[str, Any]] = []
auto_repair_executions: list[dict[str, Any]] = []
km_entries: list[dict[str, Any]] = []
inbound_rows: list[dict[str, Any]] = []
gateway_mcp_rows: list[dict[str, Any]] = []
outbound_rows: list[dict[str, Any]] = []
if incident is not None:
incident_id = str(incident["incident_id"])
approvals = await _fetch_all(
db,
"""
SELECT
id,
incident_id,
status::text AS status,
risk_level::text AS risk_level,
action,
description,
hit_count,
created_at,
updated_at,
resolved_at,
matched_playbook_id,
extra_metadata,
decision_fusion_details
FROM approval_records
WHERE incident_id = :incident_id
ORDER BY created_at DESC
LIMIT :limit
""",
{"incident_id": incident_id, "limit": _MAX_ROWS},
)
approval_ids = _approval_ids(approvals)
evidence_rows = await _fetch_all(
db,
"""
SELECT
id,
incident_id,
matched_playbook_id,
collected_at,
collection_duration_ms,
sensors_attempted,
sensors_succeeded,
evidence_summary,
metrics_snapshot,
mcp_health,
verification_result,
pre_execution_state,
post_execution_state,
self_healing_score,
self_healing_detail
FROM incident_evidence
WHERE incident_id = :incident_id
ORDER BY collected_at DESC
LIMIT :limit
""",
{"incident_id": incident_id, "limit": _MAX_ROWS},
)
timeline_events = await _fetch_all(
db,
"""
SELECT
id,
event_type,
status,
title,
description,
actor,
actor_role,
risk_level,
approval_id,
incident_id,
created_at
FROM timeline_events
WHERE incident_id = :incident_id
OR approval_id = ANY(:approval_ids)
ORDER BY created_at ASC
LIMIT :limit
""",
{
"incident_id": incident_id,
"approval_ids": approval_ids or ["__none__"],
"limit": _MAX_ROWS,
},
)
legacy_mcp_rows = await _fetch_all(
db,
"""
SELECT
id,
session_id,
flywheel_node,
mcp_server,
tool_name,
duration_ms,
success,
error_message,
incident_id,
agent_role,
created_at
FROM mcp_audit_log
WHERE incident_id = :incident_id
ORDER BY created_at ASC
LIMIT :limit
""",
{"incident_id": incident_id, "limit": _MAX_ROWS},
)
automation_ops = await _fetch_all(
db,
"""
SELECT
op_id,
operation_type,
status,
incident_id,
run_id,
parent_op_id,
actor,
dry_run_result,
error,
duration_ms,
tags,
input ->> 'action' AS input_action,
input ->> 'executor' AS input_executor,
input ->> 'execution_backend' AS input_execution_backend,
input ->> 'catalog_id' AS input_catalog_id,
input ->> 'execution_mode' AS input_execution_mode,
input ->> 'approval_source' AS input_approval_source,
input ->> 'apply_enabled' AS input_apply_enabled,
input ->> 'apply_executed' AS input_apply_executed,
input ->> 'check_mode_executed' AS input_check_mode_executed,
input ->> 'returncode' AS input_returncode,
input ->> 'playbook_id' AS input_playbook_id,
input ->> 'playbook_path' AS input_playbook_path,
input ->> 'ansible_playbook_path' AS input_ansible_playbook_path,
input ->> 'check_mode' AS input_check_mode,
input ->> 'not_used_reason' AS input_not_used_reason,
output ->> 'action' AS output_action,
output ->> 'reason' AS output_reason,
output ->> 'executor' AS output_executor,
output ->> 'execution_backend' AS output_execution_backend,
output ->> 'catalog_id' AS output_catalog_id,
output ->> 'execution_mode' AS output_execution_mode,
output ->> 'approval_source' AS output_approval_source,
output ->> 'apply_enabled' AS output_apply_enabled,
output ->> 'apply_executed' AS output_apply_executed,
output ->> 'check_mode_executed' AS output_check_mode_executed,
output ->> 'returncode' AS output_returncode,
output ->> 'playbook_id' AS output_playbook_id,
output ->> 'playbook_path' AS output_playbook_path,
output ->> 'ansible_playbook_path' AS output_ansible_playbook_path,
output ->> 'check_mode' AS output_check_mode,
output ->> 'not_used_reason' AS output_not_used_reason,
dry_run_result ->> 'returncode' AS dry_run_returncode,
dry_run_result ->> 'apply_executed' AS dry_run_apply_executed,
dry_run_result ->> 'check_mode_executed' AS dry_run_check_mode_executed,
created_at
FROM automation_operation_log
WHERE incident_id::text = :incident_id
OR coalesce(input::text, '') LIKE :needle
OR coalesce(output::text, '') LIKE :needle
OR coalesce(array_to_string(tags, ','), '') LIKE :needle
ORDER BY created_at DESC
LIMIT :limit
""",
{"incident_id": incident_id, "needle": f"%{incident_id}%", "limit": _MAX_ROWS},
)
auto_repair_executions = await _fetch_all(
db,
"""
SELECT
id,
incident_id,
playbook_id,
playbook_name,
success,
executed_steps,
error_message,
triggered_by,
similarity_score,
risk_level,
execution_time_ms,
created_at
FROM auto_repair_executions
WHERE incident_id = :incident_id
ORDER BY created_at DESC
LIMIT :limit
""",
{"incident_id": incident_id, "limit": _MAX_ROWS},
)
km_entries = await _fetch_all(
db,
"""
SELECT
id,
title,
entry_type::text AS entry_type,
status::text AS status,
related_incident_id,
related_approval_id,
created_at
FROM knowledge_entries
WHERE related_incident_id = :incident_id
ORDER BY created_at DESC
LIMIT :limit
""",
{"incident_id": incident_id, "limit": _MAX_ROWS},
)
drift_repeats: dict[str, Any] = {
"occurrences_12h": 0,
"first_scanned_at": None,
"last_scanned_at": None,
"reports": [],
}
if drift is not None:
recent_drift_reports = await _fetch_all(
db,
"""
SELECT
report_id,
namespace,
status,
scanned_at,
created_at,
items,
interpretation,
narrative_text
FROM drift_reports
WHERE created_at > now() - interval '24 hours'
AND namespace = :namespace
ORDER BY scanned_at DESC
LIMIT 200
""",
{"namespace": drift["namespace"]},
)
drift_repeats = build_drift_repeat_state(drift, recent_drift_reports)
gateway_mcp_rows = await _fetch_all(
db,
"""
SELECT
call_id,
project_id,
run_id,
trace_id,
agent_id,
tool_name,
gate_result,
result_status,
block_gate,
block_reason,
latency_ms,
created_at
FROM awooop_mcp_gateway_audit
WHERE project_id = :project_id
AND (
trace_id = :source_id
OR run_id::text = :source_id
)
ORDER BY created_at ASC
LIMIT :limit
""",
{"source_id": source_id, "project_id": project_id, "limit": _MAX_ROWS},
)
incident_fingerprints = _incident_fingerprints(incident)
fingerprint_needle = (
f"%{incident_fingerprints[0]}%"
if incident_fingerprints
else ""
)
fingerprint_value = incident_fingerprints[0] if incident_fingerprints else ""
inbound_rows = await _fetch_all(
db,
"""
SELECT
event_id,
project_id,
channel_type,
provider_event_id,
platform_subject_id,
channel_user_id,
channel_chat_id,
run_id,
content_type,
content_hash,
content_preview,
content_redacted,
redaction_version,
source_envelope,
attachment_sha256,
is_duplicate,
provider_ts,
received_at
FROM awooop_conversation_event
WHERE project_id = :project_id
AND (
run_id::text = :source_id
OR provider_event_id = :source_id
OR content_preview ILIKE :source_needle
OR coalesce(source_envelope #> '{source_refs,event_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,incident_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,approval_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,alert_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,sentry_issue_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,signoz_alerts}', '[]'::jsonb) ? :source_id
OR (
:fingerprint_needle != ''
AND (
provider_event_id ILIKE :fingerprint_needle
OR content_preview ILIKE :fingerprint_needle
OR coalesce(source_envelope #> '{source_refs,fingerprints}', '[]'::jsonb) ? :fingerprint_value
)
)
)
ORDER BY received_at DESC
LIMIT :limit
""",
{
"source_id": source_id,
"project_id": project_id,
"source_needle": f"%{source_id}%",
"fingerprint_needle": fingerprint_needle,
"fingerprint_value": fingerprint_value,
"limit": _MAX_ROWS,
},
)
outbound_rows = await _fetch_all(
db,
"""
SELECT
message_id,
project_id,
run_id,
channel_type,
message_type,
content_hash,
content_preview,
content_redacted,
redaction_version,
source_envelope,
provider_message_id,
send_status,
queued_at,
sent_at,
triggered_by_state
FROM awooop_outbound_message
WHERE project_id = :project_id
AND (
run_id::text = :source_id
OR content_preview ILIKE :needle
OR coalesce(source_envelope #> '{source_refs,incident_ids}', '[]'::jsonb) ? :source_id
OR coalesce(source_envelope #> '{source_refs,code_refs}', '[]'::jsonb) ? :source_id
)
ORDER BY queued_at DESC
LIMIT :limit
""",
{
"source_id": source_id,
"project_id": project_id,
"needle": f"%{source_id}%",
"limit": _MAX_ROWS,
},
)
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,
evidence_rows=evidence_rows,
automation_ops=automation_ops,
drift=drift,
drift_repeat_count=int(drift_repeats.get("occurrences_12h") or 0),
gateway_mcp_total=len(gateway_mcp_rows),
legacy_mcp_total=legacy_mcp_summary["total"],
outbound_visible_total=len(outbound_rows),
inbound_visible_total=len(inbound_rows),
auto_repair_executions=auto_repair_executions,
)
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,
evidence_rows=evidence_rows,
automation_ops=automation_ops,
auto_repair_executions=auto_repair_executions,
timeline_events=timeline_events,
)
evidence_totals = {
"records": len(evidence_rows),
"sensors_attempted": sum(int(row.get("sensors_attempted") or 0) for row in evidence_rows),
"sensors_succeeded": sum(int(row.get("sensors_succeeded") or 0) for row in evidence_rows),
"failed_tools": _failed_mcp_tools(evidence_rows),
}
automation_quality = build_automation_quality(
incident=incident,
approvals=approvals,
evidence_rows=evidence_rows,
automation_ops=automation_ops,
auto_repair_executions=auto_repair_executions,
gateway_mcp_summary=gateway_mcp_summary,
legacy_mcp_summary=legacy_mcp_summary,
outbound_rows=outbound_rows,
km_entries=km_entries,
timeline_events=timeline_events,
)
automation_quality["operator_outcome"] = build_operator_outcome(
truth_status=truth_status,
automation_quality=automation_quality,
source_id=source_id,
)
result = {
"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)
or bool(gateway_mcp_rows)
or bool(inbound_rows)
or bool(outbound_rows)
),
"truth_status": truth_status,
"linked_ids": {
"incident_id": incident.get("incident_id") if incident else None,
"approval_ids": _approval_ids(approvals),
"run_ids": [row["run_id"] for row in runs],
"drift_report_id": drift.get("report_id") if drift else None,
"operation_ids": _operation_ids(automation_ops),
"auto_repair_execution_ids": _auto_repair_ids(auto_repair_executions),
"conversation_event_ids": [row["event_id"] for row in inbound_rows],
},
"incident": incident,
"drift": {
"report": drift,
"repeat_state": drift_repeats,
},
"approvals": approvals,
"evidence": {
"summary": evidence_totals,
"records": evidence_rows,
},
"mcp": {
"awooop_gateway": {
**gateway_mcp_summary,
"records": gateway_mcp_rows,
},
"legacy": {
**legacy_mcp_summary,
"records": legacy_mcp_rows,
},
},
"execution": {
"automation_operation_log": automation_ops,
"auto_repair_executions": auto_repair_executions,
"ansible": build_ansible_truth(automation_ops, incident=incident, drift=drift),
},
"automation_quality": automation_quality,
"operator_outcome": automation_quality["operator_outcome"],
"reconciliation": reconciliation,
"learning": {
"knowledge_entries": km_entries,
},
"timeline_events": timeline_events,
"channel": {
"inbound_events_visible": len(inbound_rows),
"inbound_events": inbound_rows,
"outbound_messages_visible": len(outbound_rows),
"outbound_messages": outbound_rows,
"visibility_note": (
"If inbound is zero while Alertmanager fired, or outbound is zero while "
"Telegram delivered a card, channel mirrors or RLS project context are "
"not reliable sources of truth yet."
),
},
}
logger.info(
"awooop_truth_chain_fetched",
project_id=project_id,
source_id=source_id,
source_type=source_type,
current_stage=truth_status["current_stage"],
)
return result
async def fetch_automation_quality_summary(
*,
project_id: str = "awoooi",
hours: int = 24,
limit: int = 200,
refresh: bool = False,
) -> dict[str, Any]:
"""Return a recent incident-level quality summary for the automation flywheel."""
bounded_hours = max(1, min(int(hours), 168))
bounded_limit = max(1, min(int(limit), 500))
normalized_project_id = project_id or "awoooi"
cache_key = {
"project_id": normalized_project_id,
"hours": bounded_hours,
"limit": bounded_limit,
}
if not refresh:
cached_summary = get_cached_operator_summary(
"truth_chain_quality_summary",
cache_key,
ttl_seconds=_QUALITY_SUMMARY_CACHE_TTL_SECONDS,
)
if cached_summary is not None:
logger.info(
"awooop_automation_quality_summary_cache_hit",
project_id=normalized_project_id,
window_hours=bounded_hours,
limit=bounded_limit,
ttl_seconds=_QUALITY_SUMMARY_CACHE_TTL_SECONDS,
)
return cached_summary
cutoff = datetime.now(UTC) - timedelta(hours=bounded_hours)
async with get_db_context(normalized_project_id) as db:
incidents = await _fetch_all(
db,
"""
WITH source_candidates AS (
SELECT
incident_id::text AS incident_id,
created_at AS seen_at
FROM incidents
WHERE (project_id = :project_id OR project_id IS NULL)
AND created_at >= :cutoff
UNION ALL
SELECT
coalesce(incident_id::text, input ->> 'incident_id') AS incident_id,
created_at AS seen_at
FROM automation_operation_log
WHERE created_at >= :cutoff
AND operation_type IN (
'ansible_candidate_matched',
'ansible_check_mode_executed',
'ansible_execution_skipped',
'ansible_apply_executed',
'ansible_rollback_executed'
)
AND coalesce(incident_id::text, input ->> 'incident_id', '') <> ''
),
source_ids AS (
SELECT
incident_id,
max(seen_at) AS recent_evidence_at
FROM source_candidates
WHERE incident_id IS NOT NULL AND incident_id <> ''
GROUP BY incident_id
)
SELECT
incidents.incident_id,
incidents.project_id,
incidents.status::text AS status,
incidents.severity::text AS severity,
incidents.alertname,
incidents.alert_category,
incidents.notification_type,
incidents.created_at,
incidents.updated_at,
incidents.resolved_at,
incidents.verification_result,
source_ids.recent_evidence_at
FROM source_ids
JOIN incidents ON incidents.incident_id = source_ids.incident_id
WHERE (incidents.project_id = :project_id OR incidents.project_id IS NULL)
ORDER BY source_ids.recent_evidence_at DESC
LIMIT :limit
""",
{
"project_id": normalized_project_id,
"cutoff": cutoff,
"limit": bounded_limit,
},
)
semaphore = asyncio.Semaphore(_QUALITY_SUMMARY_CONCURRENCY)
async def _quality_record(incident: dict[str, Any]) -> dict[str, Any] | None:
incident_id = str(incident.get("incident_id") or "")
if not incident_id:
return None
async with semaphore:
truth_chain = await fetch_truth_chain(
source_id=incident_id,
project_id=normalized_project_id,
)
return {
"incident": truth_chain.get("incident") or incident,
"truth_status": truth_chain.get("truth_status") or {},
"automation_quality": truth_chain.get("automation_quality") or {},
"execution": truth_chain.get("execution") or {},
}
records = [
record
for record in await asyncio.gather(*(_quality_record(incident) for incident in incidents))
if record is not None
]
summary = summarize_automation_quality_records(
project_id=normalized_project_id,
window_hours=bounded_hours,
records=records,
limit=bounded_limit,
)
logger.info(
"awooop_automation_quality_summary_fetched",
project_id=normalized_project_id,
window_hours=bounded_hours,
incident_total=summary["incident_total"],
evaluated_total=summary["evaluated_total"],
can_claim_full_auto_repair=summary["production_claim"]["can_claim_full_auto_repair"],
cache_status="miss",
cache_ttl_seconds=_QUALITY_SUMMARY_CACHE_TTL_SECONDS,
)
return store_operator_summary(
"truth_chain_quality_summary",
cache_key,
summary,
ttl_seconds=_QUALITY_SUMMARY_CACHE_TTL_SECONDS,
)