from __future__ import annotations from typing import Any from uuid import UUID from sqlalchemy import select from packages.hub_policy import AuditEvent, AuditPage from services.control_api.persistence.models import ( AuditEventRow, EnvironmentRow, ProjectRow, WorkspaceRow, ) class SqlAlchemyAuditLog: """Append-only production audit sink backed by the Hub-owned database.""" def __init__(self, sessions: Any, *, issuer: str) -> None: self.sessions = sessions self.issuer = issuer @staticmethod def _workspace_id(session: Any, workspace: str) -> UUID | None: try: identifier = UUID(workspace) except ValueError: identifier = None if identifier is not None and session.get(WorkspaceRow, identifier) is not None: return identifier value = session.scalar(select(WorkspaceRow.id).where(WorkspaceRow.slug == workspace)) return value if isinstance(value, UUID) else None def append(self, event: AuditEvent) -> None: with self.sessions() as session, session.begin(): workspace_id = self._workspace_id(session, event.workspace) if workspace_id is None: raise RuntimeError("audit workspace is not registered") project = session.scalar( select(ProjectRow).where( ProjectRow.workspace_id == workspace_id, ProjectRow.slug == event.project, ) ) if project is None: raise RuntimeError("audit project is not registered") environment_id = session.scalar( select(EnvironmentRow.id).where( EnvironmentRow.workspace_id == workspace_id, EnvironmentRow.project_id == project.id, EnvironmentRow.name == event.environment, ) ) if environment_id is None: raise RuntimeError("audit environment is not registered") session.add( AuditEventRow( id=event.id, occurred_at=event.occurred_at, trace_id=event.trace_id, workspace_id=workspace_id, project_id=project.id, environment_id=environment_id, event_type=event.event_type, principal_issuer=self.issuer, principal_subject=event.principal, client_id=event.client_id, tool_name=event.tool, policy_decision=event.policy_decision, matched_rule_ids=list(event.matched_rule_ids), policy_trace=list(event.policy_trace), arguments_hash=event.arguments_hash, sanitized_arguments=event.sanitized_arguments, duration_ms=event.duration_ms, outcome=event.outcome, error_code=event.error_code, retention_until=event.retention_until, ) ) def search( self, *, workspace: str, project: str, limit: int = 100, offset: int = 0 ) -> AuditPage: if not 1 <= limit <= 500 or offset < 0: raise ValueError("invalid audit pagination") with self.sessions() as session: workspace_id = self._workspace_id(session, workspace) if workspace_id is None: return AuditPage(items=()) project_row = session.scalar( select(ProjectRow).where( ProjectRow.workspace_id == workspace_id, ProjectRow.slug == project, ) ) if project_row is None: return AuditPage(items=()) rows = session.scalars( select(AuditEventRow) .where( AuditEventRow.workspace_id == workspace_id, AuditEventRow.project_id == project_row.id, ) .order_by(AuditEventRow.occurred_at, AuditEventRow.id) .offset(offset) .limit(limit + 1) ).all() selected = rows[:limit] environments = { row.id: row.name for row in session.scalars( select(EnvironmentRow).where(EnvironmentRow.workspace_id == workspace_id) ).all() } items = tuple( AuditEvent( id=row.id, occurred_at=row.occurred_at, trace_id=row.trace_id, event_type=row.event_type, workspace=workspace, project=project, environment=environments.get(row.environment_id, "unknown"), principal=row.principal_subject or "unknown", client_id=row.client_id, tool=row.tool_name or "mcp.request", policy_decision=row.policy_decision or "deny", matched_rule_ids=tuple(row.matched_rule_ids), policy_trace=tuple(row.policy_trace), arguments_hash=row.arguments_hash or "0" * 64, sanitized_arguments=dict(row.sanitized_arguments), duration_ms=row.duration_ms or 0, outcome=row.outcome, error_code=row.error_code, retention_until=row.retention_until, ) for row in selected ) return AuditPage( items=items, next_offset=offset + limit if len(rows) > limit else None, ) def export_metadata(self, *, workspace: str, project: str) -> dict[str, Any]: page = self.search(workspace=workspace, project=project, limit=500) return { "workspace": workspace, "project": project, "eventCount": len(page.items), "lastEventAt": page.items[-1].occurred_at.isoformat() if page.items else None, "format": "audit-event-v1", "access": "policy-restricted", }