"""The audit log: who changed what in Steward, including changes made through MCP.""" from __future__ import annotations import json from sqlalchemy import or_, select from steward.core.like import escape_like from steward.models.audit import AuditEvent from steward.models.users import UserRole from ._audit import mcp_username from ._shape import age_s, bounded, clamp_limit, db_session, iso, lookback async def list_audit_events(action: str = "", user: str = "", entity_type: str = "", range: str = "7d", limit: int = 50) -> dict: """Changes recorded in Steward's audit log, newest first. action: a prefix of the action name, e.g. "monitor." or "ansible.run"; empty for all. user: a username, exactly; "system" for automated changes. Includes the changes that user made through MCP, which are recorded as " via MCP". entity_type: e.g. "monitor", "host", "alert_rule"; empty for all. range: "1h", "6h", "24h", "7d" (default) or "30d". limit: default 50, max 500. Answers "what changed before this broke?": line an event's `at` up against a monitor's down_since or an alert's since. """ since, key = lookback(range) limit = clamp_limit(limit, 50, 500) stmt = (select(AuditEvent).where(AuditEvent.timestamp >= since) .order_by(AuditEvent.timestamp.desc()).limit(limit + 1)) if action: stmt = stmt.where(AuditEvent.action.like(escape_like(action) + "%", escape="\\")) if user: stmt = stmt.where(or_(AuditEvent.username == user, AuditEvent.username == mcp_username(user))) if entity_type: stmt = stmt.where(AuditEvent.entity_type == entity_type) async with db_session() as db: rows = list((await db.execute(stmt)).scalars()) def _detail(raw): if not raw: return None try: return json.loads(raw) except ValueError: return raw[:500] events = [ { "at": iso(e.timestamp), "age_s": age_s(e.timestamp), "user": e.username, "action": e.action, "entity_type": e.entity_type, "entity_id": e.entity_id, "detail": _detail(e.detail_json), } for e in rows ] out = {"range": key, **bounded(events, limit, key="events")} if out["truncated"]: out["total"] = None out["more"] = (f"More than {limit} events in range. Pass a larger `limit` (max 500), " "a shorter `range`, or filter by `action`, `user` or `entity_type`.") return out def register(register_tool) -> None: register_tool(list_audit_events, role=UserRole.viewer, read_only=True)