Files
microdao-daarion/backend/services/governance/audit.service.ts
Apple e233d32ae7 feat(governance): Governance Engine MVP implementation
- Backend:
  - Migration 032: agent_gov_level, status, incidents, permissions tables
  - Domain types for governance layer
  - Permission Engine with all governance checks
  - Governance Service (promote/demote/roles)
  - Revocation Service (revoke/suspend/reinstate)
  - Audit Service (events filtering and stats)
  - Incidents Service (create/assign/escalate/resolve)
  - REST API routes for governance, audit, incidents

- Frontend:
  - TypeScript types for governance
  - API clients for governance, audit, incidents
  - GovernanceLevelBadge component
  - CityGovernancePanel component
  - AuditDashboard component
  - IncidentsList component with detail modal

Based on: Agent_Governance_Protocol_v1.md
2025-11-29 16:02:06 -08:00

292 lines
7.6 KiB
TypeScript

/**
* Audit Service
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Provides access to governance events for audit and analysis
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import {
GovernanceEvent,
GovernanceEventType,
AuditEventFilter,
GovernanceScope,
} from '../../domain/governance/types';
export interface AuditEventRow {
id: string;
event_type: GovernanceEventType;
subject: string;
actor_id: string;
target_id: string;
scope: GovernanceScope;
payload: Record<string, unknown>;
version: string;
status: 'pending' | 'published' | 'failed';
created_at: Date;
published_at?: Date;
}
export class AuditService {
/**
* Get audit events with filters
*/
async getEvents(filter: AuditEventFilter): Promise<{
events: GovernanceEvent[];
total: number;
}> {
const conditions: string[] = [];
const params: (string | Date | number)[] = [];
let paramIndex = 1;
if (filter.eventType) {
conditions.push(`event_type = $${paramIndex++}`);
params.push(filter.eventType);
}
if (filter.actorId) {
conditions.push(`actor_id = $${paramIndex++}`);
params.push(filter.actorId);
}
if (filter.targetId) {
conditions.push(`target_id = $${paramIndex++}`);
params.push(filter.targetId);
}
if (filter.scope) {
conditions.push(`scope = $${paramIndex++}`);
params.push(filter.scope);
}
if (filter.createdAtFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(filter.createdAtFrom);
}
if (filter.createdAtTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(filter.createdAtTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Get total count
const countResult = await db.query<{ count: string }>(
`SELECT COUNT(*) as count FROM event_outbox ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
// Get events
const limit = filter.limit || 50;
const offset = filter.offset || 0;
params.push(limit, offset);
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
params
);
const events: GovernanceEvent[] = result.rows.map(row => ({
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
}));
return { events, total };
}
/**
* Get single event by ID
*/
async getEvent(eventId: string): Promise<GovernanceEvent | null> {
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
WHERE id = $1`,
[eventId]
);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
return {
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
};
}
/**
* Get events by actor
*/
async getEventsByActor(actorId: string, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ actorId, limit });
return result.events;
}
/**
* Get events by target
*/
async getEventsByTarget(targetId: string, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ targetId, limit });
return result.events;
}
/**
* Get events by scope
*/
async getEventsByScope(scope: GovernanceScope, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ scope, limit });
return result.events;
}
/**
* Get event statistics
*/
async getEventStats(
fromDate?: Date,
toDate?: Date
): Promise<{
totalEvents: number;
eventsByType: Record<string, number>;
eventsByDay: Array<{ date: string; count: number }>;
topActors: Array<{ actorId: string; count: number }>;
}> {
const conditions: string[] = [];
const params: (string | Date)[] = [];
let paramIndex = 1;
if (fromDate) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(fromDate);
}
if (toDate) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(toDate);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Total events
const totalResult = await db.query<{ count: string }>(
`SELECT COUNT(*) as count FROM event_outbox ${whereClause}`,
params
);
const totalEvents = parseInt(totalResult.rows[0].count, 10);
// Events by type
const byTypeResult = await db.query<{ event_type: string; count: string }>(
`SELECT event_type, COUNT(*) as count
FROM event_outbox ${whereClause}
GROUP BY event_type`,
params
);
const eventsByType: Record<string, number> = {};
byTypeResult.rows.forEach(row => {
eventsByType[row.event_type] = parseInt(row.count, 10);
});
// Events by day
const byDayResult = await db.query<{ date: string; count: string }>(
`SELECT DATE(created_at) as date, COUNT(*) as count
FROM event_outbox ${whereClause}
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30`,
params
);
const eventsByDay = byDayResult.rows.map(row => ({
date: row.date,
count: parseInt(row.count, 10),
}));
// Top actors
const topActorsResult = await db.query<{ actor_id: string; count: string }>(
`SELECT actor_id, COUNT(*) as count
FROM event_outbox ${whereClause} AND actor_id IS NOT NULL
GROUP BY actor_id
ORDER BY count DESC
LIMIT 10`,
params
);
const topActors = topActorsResult.rows.map(row => ({
actorId: row.actor_id,
count: parseInt(row.count, 10),
}));
return {
totalEvents,
eventsByType,
eventsByDay,
topActors,
};
}
/**
* Get governance events for specific entity
*/
async getEntityHistory(
entityType: 'agent' | 'microdao' | 'district' | 'node' | 'room',
entityId: string,
limit = 50
): Promise<GovernanceEvent[]> {
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
WHERE target_id = $1 OR actor_id = $1 OR scope LIKE $2
ORDER BY created_at DESC
LIMIT $3`,
[entityId, `%${entityId}%`, limit]
);
return result.rows.map(row => ({
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
}));
}
}
export const auditService = new AuditService();