Files
microdao-daarion/backend/http/incidents.routes.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

294 lines
7.7 KiB
TypeScript

/**
* Incidents Routes
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
import { Router, Request, Response } from 'express';
import { incidentsService } from '../services/governance/incidents.service';
import {
IncidentStatus,
IncidentPriority,
EscalationLevel,
TargetScopeType,
} from '../domain/governance/types';
import { logger } from '../infra/logger/logger';
const router = Router();
/**
* POST /api/v1/incidents
* Create a new incident
*/
router.post('/', async (req: Request, res: Response) => {
try {
const {
createdByDaisId,
targetScopeType,
targetScopeId,
priority,
title,
description,
metadata,
} = req.body;
if (!createdByDaisId || !targetScopeType || !targetScopeId || !title) {
return res.status(400).json({
error: 'Missing required fields: createdByDaisId, targetScopeType, targetScopeId, title',
});
}
const incident = await incidentsService.createIncident({
createdByDaisId,
targetScopeType: targetScopeType as TargetScopeType,
targetScopeId,
priority: priority as IncidentPriority | undefined,
title,
description,
metadata,
});
res.status(201).json(incident);
} catch (error) {
logger.error('Error creating incident', error);
res.status(500).json({ error: 'Failed to create incident' });
}
});
/**
* GET /api/v1/incidents
* List incidents with filters
*/
router.get('/', async (req: Request, res: Response) => {
try {
const {
status,
priority,
escalationLevel,
targetScopeType,
targetScopeId,
assignedToDaisId,
limit,
offset,
} = req.query;
const result = await incidentsService.listIncidents({
status: status as IncidentStatus | undefined,
priority: priority as IncidentPriority | undefined,
escalationLevel: escalationLevel as EscalationLevel | undefined,
targetScopeType: targetScopeType as TargetScopeType | undefined,
targetScopeId: targetScopeId as string | undefined,
assignedToDaisId: assignedToDaisId 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 listing incidents', error);
res.status(500).json({ error: 'Failed to list incidents' });
}
});
/**
* GET /api/v1/incidents/count
* Get open incidents count by level
*/
router.get('/count', async (_req: Request, res: Response) => {
try {
const counts = await incidentsService.getOpenIncidentsCount();
res.json(counts);
} catch (error) {
logger.error('Error getting incidents count', error);
res.status(500).json({ error: 'Failed to get incidents count' });
}
});
/**
* GET /api/v1/incidents/:id
* Get incident by ID
*/
router.get('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const incident = await incidentsService.getIncident(id);
if (!incident) {
return res.status(404).json({ error: 'Incident not found' });
}
res.json(incident);
} catch (error) {
logger.error('Error getting incident', error);
res.status(500).json({ error: 'Failed to get incident' });
}
});
/**
* GET /api/v1/incidents/:id/history
* Get incident history
*/
router.get('/:id/history', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const history = await incidentsService.getIncidentHistory(id);
res.json(history);
} catch (error) {
logger.error('Error getting incident history', error);
res.status(500).json({ error: 'Failed to get incident history' });
}
});
/**
* POST /api/v1/incidents/:id/assign
* Assign incident to an agent
*/
router.post('/:id/assign', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { assignedToDaisId, actorDaisId } = req.body;
if (!assignedToDaisId || !actorDaisId) {
return res.status(400).json({
error: 'Missing required fields: assignedToDaisId, actorDaisId',
});
}
const result = await incidentsService.assignIncident({
incidentId: id,
assignedToDaisId,
actorDaisId,
});
if (!result.success) {
return res.status(400).json({ error: result.error });
}
res.json({ success: true, message: 'Incident assigned' });
} catch (error) {
logger.error('Error assigning incident', error);
res.status(500).json({ error: 'Failed to assign incident' });
}
});
/**
* POST /api/v1/incidents/:id/escalate
* Escalate incident to higher level
*/
router.post('/:id/escalate', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { newLevel, actorDaisId, reason } = req.body;
if (!newLevel || !actorDaisId) {
return res.status(400).json({
error: 'Missing required fields: newLevel, actorDaisId',
});
}
const result = await incidentsService.escalateIncident({
incidentId: id,
newLevel: newLevel as EscalationLevel,
actorDaisId,
reason,
});
if (!result.success) {
return res.status(400).json({ error: result.error });
}
res.json({ success: true, message: `Incident escalated to ${newLevel}` });
} catch (error) {
logger.error('Error escalating incident', error);
res.status(500).json({ error: 'Failed to escalate incident' });
}
});
/**
* POST /api/v1/incidents/:id/resolve
* Resolve incident
*/
router.post('/:id/resolve', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { resolution, actorDaisId } = req.body;
if (!resolution || !actorDaisId) {
return res.status(400).json({
error: 'Missing required fields: resolution, actorDaisId',
});
}
const result = await incidentsService.resolveIncident({
incidentId: id,
resolution,
actorDaisId,
});
if (!result.success) {
return res.status(400).json({ error: result.error });
}
res.json({ success: true, message: 'Incident resolved' });
} catch (error) {
logger.error('Error resolving incident', error);
res.status(500).json({ error: 'Failed to resolve incident' });
}
});
/**
* POST /api/v1/incidents/:id/close
* Close incident
*/
router.post('/:id/close', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { actorDaisId } = req.body;
if (!actorDaisId) {
return res.status(400).json({ error: 'Missing required field: actorDaisId' });
}
const result = await incidentsService.closeIncident(id, actorDaisId);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
res.json({ success: true, message: 'Incident closed' });
} catch (error) {
logger.error('Error closing incident', error);
res.status(500).json({ error: 'Failed to close incident' });
}
});
/**
* POST /api/v1/incidents/:id/comment
* Add comment to incident
*/
router.post('/:id/comment', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { actorDaisId, comment } = req.body;
if (!actorDaisId || !comment) {
return res.status(400).json({
error: 'Missing required fields: actorDaisId, comment',
});
}
const result = await incidentsService.addComment(id, actorDaisId, comment);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
res.json({ success: true, message: 'Comment added' });
} catch (error) {
logger.error('Error adding comment to incident', error);
res.status(500).json({ error: 'Failed to add comment' });
}
});
export default router;