""" Agent Replay Fixture Builder ============================ Builds sanitized incident fixtures for OpenClaw replacement candidate replay. Fixtures separate the input context shown to candidate Agents from evaluation labels used by the offline scoring harness. This prevents candidates from self-grading against the answer key while keeping replay runs reproducible. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from typing import Any REDACTED = "[REDACTED]" SENSITIVE_KEY_MARKERS = ( "authorization", "cookie", "password", "passwd", "secret", "token", "api_key", "apikey", "private_key", ) SENSITIVE_VALUE_MARKERS = ( "bearer ", "basic ", "-----begin private key-----", ) @dataclass(frozen=True) class AgentReplayFixture: """One sanitized incident fixture for candidate Agent offline replay.""" run_id: str incident_id: str schema_version: str = "agent_replay_fixture_v1" incident_context: dict[str, Any] = field(default_factory=dict) evaluation_labels: dict[str, Any] = field(default_factory=dict) source_metadata: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { "schema_version": self.schema_version, "run_id": self.run_id, "incident_id": self.incident_id, "incident_context": dict(self.incident_context), "evaluation_labels": dict(self.evaluation_labels), "source_metadata": dict(self.source_metadata), } def build_agent_replay_fixture( *, run_id: str, incident, evidence=None, execution=None, agent_turn_count: int = 0, ) -> AgentReplayFixture: """Build a sanitized fixture from DB model objects.""" incident_context = { "severity": _scalar_value(getattr(incident, "severity", None)), "status": _scalar_value(getattr(incident, "status", None)), "alertname": getattr(incident, "alertname", None), "alert_category": getattr(incident, "alert_category", None), "notification_type": getattr(incident, "notification_type", None), "affected_services": list(getattr(incident, "affected_services", None) or []), "signals": _sanitize_for_fixture(getattr(incident, "signals", None) or []), "frequency_snapshot": _sanitize_for_fixture( getattr(incident, "frequency_snapshot", None) ), "evidence_summary": _sanitize_for_fixture( getattr(evidence, "evidence_summary", None) if evidence else None ), "mcp_health": _sanitize_for_fixture( getattr(evidence, "mcp_health", None) if evidence else None ), "sensors_attempted": getattr(evidence, "sensors_attempted", None) if evidence else None, "sensors_succeeded": getattr(evidence, "sensors_succeeded", None) if evidence else None, "historical_context": _sanitize_for_fixture( getattr(evidence, "historical_context", None) if evidence else None ), "dependency_topology": _sanitize_for_fixture( getattr(evidence, "dependency_topology", None) if evidence else None ), "business_metrics": _sanitize_for_fixture( getattr(evidence, "business_metrics", None) if evidence else None ), } expected_action_markers = _expected_action_markers( incident_context=incident_context, execution=execution, ) evaluation_labels = { "verification_result": getattr(evidence, "verification_result", None) if evidence else None, "self_healing_score": getattr(evidence, "self_healing_score", None) if evidence else None, "execution_success": getattr(execution, "success", None) if execution else None, "execution_error": _sanitize_for_fixture( getattr(execution, "error_message", None) if execution else None ), "resolved_at": _iso_or_none(getattr(incident, "resolved_at", None)), "closed_at": _iso_or_none(getattr(incident, "closed_at", None)), } if expected_action_markers: evaluation_labels["expected_action_markers"] = expected_action_markers source_metadata = { "created_at": _iso_or_none(getattr(incident, "created_at", None)), "updated_at": _iso_or_none(getattr(incident, "updated_at", None)), "agent_turn_count": agent_turn_count, "source": "awoooi_incident_replay_fixture", } return AgentReplayFixture( run_id=run_id, incident_id=str(incident.incident_id), incident_context=_drop_none(incident_context), evaluation_labels=_drop_none(evaluation_labels), source_metadata=_drop_none(source_metadata), ) def _sanitize_for_fixture(value: Any) -> Any: if isinstance(value, dict): sanitized: dict[str, Any] = {} for key, nested in value.items(): key_text = str(key) if _is_sensitive_key(key_text): sanitized[key_text] = REDACTED else: sanitized[key_text] = _sanitize_for_fixture(nested) return sanitized if isinstance(value, list): return [_sanitize_for_fixture(item) for item in value] if isinstance(value, tuple): return [_sanitize_for_fixture(item) for item in value] if isinstance(value, str): return _sanitize_string(value) if isinstance(value, datetime): return value.isoformat() return value def _sanitize_string(value: str) -> str: lowered = value.lower() if any(marker in lowered for marker in SENSITIVE_VALUE_MARKERS): return REDACTED return value def _is_sensitive_key(key: str) -> bool: lowered = key.lower() return any(marker in lowered for marker in SENSITIVE_KEY_MARKERS) def _drop_none(payload: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in payload.items() if value is not None} def _iso_or_none(value: Any) -> str | None: if value is None: return None if isinstance(value, datetime): return value.isoformat() return str(value) def _scalar_value(value: Any) -> Any: return getattr(value, "value", value) def _expected_action_markers( *, incident_context: dict[str, Any], execution: Any, ) -> list[str]: if execution is None: return [] parts = [ getattr(execution, "playbook_name", None), _sanitize_for_fixture(getattr(execution, "executed_steps", None) or []), ] haystack = " ".join( json_part.lower() for json_part in (_json_text(part) for part in parts) if json_part ) markers: list[str] = [] if "rollout restart" in haystack or ("rollout" in haystack and "restart" in haystack): markers.append("rollout restart") else: for marker in ("restart", "rollback", "scale", "describe", "logs", "delete"): if marker in haystack: markers.append(marker) for service in incident_context.get("affected_services") or []: service_marker = str(service).strip().lower() if service_marker: markers.append(service_marker) break return list(dict.fromkeys(markers)) def _json_text(value: Any) -> str: if value is None: return "" if isinstance(value, str): return value return str(value)