225 lines
7.3 KiB
Python
225 lines
7.3 KiB
Python
"""
|
|
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)
|