- 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
292 lines
7.6 KiB
TypeScript
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();
|
|
|