- 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
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
/**
|
|
* Audit Routes
|
|
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { auditService } from '../services/governance/audit.service';
|
|
import { GovernanceEventType, GovernanceScope } from '../domain/governance/types';
|
|
import { logger } from '../infra/logger/logger';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* GET /api/v1/audit/events
|
|
* Get audit events with filters
|
|
*/
|
|
router.get('/events', async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
eventType,
|
|
actorId,
|
|
targetId,
|
|
scope,
|
|
createdAtFrom,
|
|
createdAtTo,
|
|
limit,
|
|
offset,
|
|
} = req.query;
|
|
|
|
const result = await auditService.getEvents({
|
|
eventType: eventType as GovernanceEventType | undefined,
|
|
actorId: actorId as string | undefined,
|
|
targetId: targetId as string | undefined,
|
|
scope: scope as GovernanceScope | undefined,
|
|
createdAtFrom: createdAtFrom ? new Date(createdAtFrom as string) : undefined,
|
|
createdAtTo: createdAtTo ? new Date(createdAtTo as string) : undefined,
|
|
limit: limit ? parseInt(limit as string, 10) : 50,
|
|
offset: offset ? parseInt(offset as string, 10) : 0,
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
logger.error('Error getting audit events', error);
|
|
res.status(500).json({ error: 'Failed to get audit events' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/events/:id
|
|
* Get single audit event
|
|
*/
|
|
router.get('/events/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const event = await auditService.getEvent(id);
|
|
|
|
if (!event) {
|
|
return res.status(404).json({ error: 'Event not found' });
|
|
}
|
|
|
|
res.json(event);
|
|
} catch (error) {
|
|
logger.error('Error getting audit event', error);
|
|
res.status(500).json({ error: 'Failed to get audit event' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/actor/:actorId
|
|
* Get events by actor
|
|
*/
|
|
router.get('/actor/:actorId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { actorId } = req.params;
|
|
const { limit } = req.query;
|
|
|
|
const events = await auditService.getEventsByActor(
|
|
actorId,
|
|
limit ? parseInt(limit as string, 10) : 50
|
|
);
|
|
|
|
res.json(events);
|
|
} catch (error) {
|
|
logger.error('Error getting events by actor', error);
|
|
res.status(500).json({ error: 'Failed to get events by actor' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/target/:targetId
|
|
* Get events by target
|
|
*/
|
|
router.get('/target/:targetId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { targetId } = req.params;
|
|
const { limit } = req.query;
|
|
|
|
const events = await auditService.getEventsByTarget(
|
|
targetId,
|
|
limit ? parseInt(limit as string, 10) : 50
|
|
);
|
|
|
|
res.json(events);
|
|
} catch (error) {
|
|
logger.error('Error getting events by target', error);
|
|
res.status(500).json({ error: 'Failed to get events by target' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/scope/:scope
|
|
* Get events by scope
|
|
*/
|
|
router.get('/scope/:scope', async (req: Request, res: Response) => {
|
|
try {
|
|
const { scope } = req.params;
|
|
const { limit } = req.query;
|
|
|
|
const events = await auditService.getEventsByScope(
|
|
scope as GovernanceScope,
|
|
limit ? parseInt(limit as string, 10) : 50
|
|
);
|
|
|
|
res.json(events);
|
|
} catch (error) {
|
|
logger.error('Error getting events by scope', error);
|
|
res.status(500).json({ error: 'Failed to get events by scope' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/stats
|
|
* Get event statistics
|
|
*/
|
|
router.get('/stats', async (req: Request, res: Response) => {
|
|
try {
|
|
const { fromDate, toDate } = req.query;
|
|
|
|
const stats = await auditService.getEventStats(
|
|
fromDate ? new Date(fromDate as string) : undefined,
|
|
toDate ? new Date(toDate as string) : undefined
|
|
);
|
|
|
|
res.json(stats);
|
|
} catch (error) {
|
|
logger.error('Error getting audit stats', error);
|
|
res.status(500).json({ error: 'Failed to get audit stats' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/audit/entity/:entityType/:entityId
|
|
* Get governance history for specific entity
|
|
*/
|
|
router.get('/entity/:entityType/:entityId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { entityType, entityId } = req.params;
|
|
const { limit } = req.query;
|
|
|
|
if (!['agent', 'microdao', 'district', 'node', 'room'].includes(entityType)) {
|
|
return res.status(400).json({ error: 'Invalid entity type' });
|
|
}
|
|
|
|
const events = await auditService.getEntityHistory(
|
|
entityType as 'agent' | 'microdao' | 'district' | 'node' | 'room',
|
|
entityId,
|
|
limit ? parseInt(limit as string, 10) : 50
|
|
);
|
|
|
|
res.json(events);
|
|
} catch (error) {
|
|
logger.error('Error getting entity history', error);
|
|
res.status(500).json({ error: 'Failed to get entity history' });
|
|
}
|
|
});
|
|
|
|
export default router;
|
|
|