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

387 lines
12 KiB
TypeScript

/**
* Governance Routes
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
import { Router, Request, Response } from 'express';
import { governanceService } from '../services/governance/governance.service';
import { revocationService } from '../services/governance/revocation.service';
import { permissionEngine, buildContext } from '../services/governance/permissions';
import { AgentGovLevel, GovernanceScope, RevocationType } from '../domain/governance/types';
import { logger } from '../infra/logger/logger';
const router = Router();
// ============================================================================
// AGENT PROMOTION/DEMOTION
// ============================================================================
/**
* POST /api/v1/governance/agent/promote
* Promote an agent to a higher level
*/
router.post('/agent/promote', async (req: Request, res: Response) => {
try {
const { actorId, targetId, newLevel, scope, reason } = req.body;
if (!actorId || !targetId || !newLevel || !scope) {
return res.status(400).json({ error: 'Missing required fields: actorId, targetId, newLevel, scope' });
}
const result = await governanceService.promoteAgent({
actorId,
targetId,
newLevel: newLevel as AgentGovLevel,
scope: scope as GovernanceScope,
reason,
});
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({ success: true, message: `Agent ${targetId} promoted to ${newLevel}` });
} catch (error) {
logger.error('Error promoting agent', error);
res.status(500).json({ error: 'Failed to promote agent' });
}
});
/**
* POST /api/v1/governance/agent/demote
* Demote an agent to a lower level
*/
router.post('/agent/demote', async (req: Request, res: Response) => {
try {
const { actorId, targetId, newLevel, scope, reason } = req.body;
if (!actorId || !targetId || !newLevel || !scope) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = await governanceService.demoteAgent(
actorId,
targetId,
newLevel as AgentGovLevel,
scope as GovernanceScope,
reason
);
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({ success: true, message: `Agent ${targetId} demoted to ${newLevel}` });
} catch (error) {
logger.error('Error demoting agent', error);
res.status(500).json({ error: 'Failed to demote agent' });
}
});
// ============================================================================
// AGENT REVOCATION
// ============================================================================
/**
* POST /api/v1/governance/agent/revoke
* Revoke an agent
*/
router.post('/agent/revoke', async (req: Request, res: Response) => {
try {
const { actorId, targetId, reason, scope, revocationType } = req.body;
if (!actorId || !targetId || !reason || !scope) {
return res.status(400).json({ error: 'Missing required fields: actorId, targetId, reason, scope' });
}
const result = await revocationService.revokeAgent({
actorId,
targetId,
reason,
scope: scope as GovernanceScope,
revocationType: (revocationType || 'soft') as RevocationType,
});
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({
success: true,
revocationId: result.revocationId,
message: `Agent ${targetId} revoked`
});
} catch (error) {
logger.error('Error revoking agent', error);
res.status(500).json({ error: 'Failed to revoke agent' });
}
});
/**
* POST /api/v1/governance/agent/suspend
* Temporarily suspend an agent
*/
router.post('/agent/suspend', async (req: Request, res: Response) => {
try {
const { actorId, targetId, reason, scope, durationHours } = req.body;
if (!actorId || !targetId || !reason || !scope) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = await revocationService.suspendAgent(
actorId,
targetId,
reason,
scope as GovernanceScope,
durationHours
);
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({ success: true, message: `Agent ${targetId} suspended` });
} catch (error) {
logger.error('Error suspending agent', error);
res.status(500).json({ error: 'Failed to suspend agent' });
}
});
/**
* POST /api/v1/governance/agent/reinstate
* Reinstate a revoked/suspended agent
*/
router.post('/agent/reinstate', async (req: Request, res: Response) => {
try {
const { actorId, targetId, scope, reason } = req.body;
if (!actorId || !targetId || !scope) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = await revocationService.reinstateAgent(
actorId,
targetId,
scope as GovernanceScope,
reason
);
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({ success: true, message: `Agent ${targetId} reinstated` });
} catch (error) {
logger.error('Error reinstating agent', error);
res.status(500).json({ error: 'Failed to reinstate agent' });
}
});
// ============================================================================
// AGENT ROLES & PERMISSIONS
// ============================================================================
/**
* GET /api/v1/governance/agent/:id/roles
* Get agent roles and permissions
*/
router.get('/agent/:id/roles', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const roles = await governanceService.getAgentRoles(id);
res.json(roles);
} catch (error) {
logger.error('Error getting agent roles', error);
res.status(500).json({ error: 'Failed to get agent roles' });
}
});
/**
* GET /api/v1/governance/agent/:id/permissions
* Get agent permissions for a specific target
*/
router.get('/agent/:id/permissions', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { targetType, targetId, action } = req.query;
if (targetType && targetId && action) {
// Get DAIS ID for agent
const context = await buildContext(id, 'city');
const hasPermission = await permissionEngine.hasExplicitPermission(
context.actorDaisId,
targetType as any,
targetId as string,
action as any
);
return res.json({ hasPermission });
}
// Return general permissions
const level = await permissionEngine.getAgentLevel(id);
const powers = permissionEngine.getPowersForLevel(level);
res.json({ level, powers });
} catch (error) {
logger.error('Error getting agent permissions', error);
res.status(500).json({ error: 'Failed to get agent permissions' });
}
});
// ============================================================================
// PERMISSION CHECKS
// ============================================================================
/**
* POST /api/v1/governance/check
* Check if an agent can perform an action
*/
router.post('/check', async (req: Request, res: Response) => {
try {
const { actorId, action, targetId, scope, roomType } = req.body;
if (!actorId || !action) {
return res.status(400).json({ error: 'Missing required fields: actorId, action' });
}
const context = await buildContext(actorId, (scope || 'city') as GovernanceScope);
let result;
switch (action) {
case 'create_microdao':
result = await permissionEngine.canCreateMicrodao(context);
break;
case 'create_district':
result = await permissionEngine.canCreateDistrict(context);
break;
case 'register_node':
result = await permissionEngine.canRegisterNode(context);
break;
case 'create_room':
result = await permissionEngine.canCreateRoom(context, roomType || 'dao-room');
break;
case 'create_front_portal':
result = await permissionEngine.canCreateFrontPortal(context, targetId);
break;
case 'moderate_room':
result = await permissionEngine.canModerateRoom(context, targetId);
break;
default:
return res.status(400).json({ error: `Unknown action: ${action}` });
}
res.json(result);
} catch (error) {
logger.error('Error checking permission', error);
res.status(500).json({ error: 'Failed to check permission' });
}
});
// ============================================================================
// GOVERNANCE AGENTS LISTS
// ============================================================================
/**
* GET /api/v1/governance/agents/city
* Get city governance agents
*/
router.get('/agents/city', async (_req: Request, res: Response) => {
try {
const agents = await governanceService.getCityGovernanceAgents();
res.json(agents);
} catch (error) {
logger.error('Error getting city governance agents', error);
res.status(500).json({ error: 'Failed to get city governance agents' });
}
});
/**
* GET /api/v1/governance/agents/district-leads
* Get district lead agents
*/
router.get('/agents/district-leads', async (req: Request, res: Response) => {
try {
const { districtId } = req.query;
const agents = await governanceService.getDistrictLeadAgents(districtId as string);
res.json(agents);
} catch (error) {
logger.error('Error getting district lead agents', error);
res.status(500).json({ error: 'Failed to get district lead agents' });
}
});
/**
* GET /api/v1/governance/agents/by-level/:level
* Get agents by governance level
*/
router.get('/agents/by-level/:level', async (req: Request, res: Response) => {
try {
const { level } = req.params;
const { limit } = req.query;
const agents = await governanceService.getAgentsByLevel(
level as AgentGovLevel,
limit ? parseInt(limit as string, 10) : 50
);
res.json(agents);
} catch (error) {
logger.error('Error getting agents by level', error);
res.status(500).json({ error: 'Failed to get agents by level' });
}
});
// ============================================================================
// DAIS KEY REVOCATION
// ============================================================================
/**
* POST /api/v1/governance/dais/keys/revoke
* Revoke DAIS keys
*/
router.post('/dais/keys/revoke', async (req: Request, res: Response) => {
try {
const { actorId, daisId, reason } = req.body;
if (!actorId || !daisId || !reason) {
return res.status(400).json({ error: 'Missing required fields: actorId, daisId, reason' });
}
const result = await revocationService.revokeDaisKeys(actorId, daisId, reason);
if (!result.success) {
return res.status(403).json({ error: result.error });
}
res.json({
success: true,
keysRevoked: result.keysRevoked,
message: `${result.keysRevoked} keys revoked for DAIS ${daisId}`
});
} catch (error) {
logger.error('Error revoking DAIS keys', error);
res.status(500).json({ error: 'Failed to revoke DAIS keys' });
}
});
/**
* GET /api/v1/governance/agent/:id/revocations
* Get revocation history for an agent
*/
router.get('/agent/:id/revocations', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const history = await revocationService.getRevocationHistory(id);
res.json(history);
} catch (error) {
logger.error('Error getting revocation history', error);
res.status(500).json({ error: 'Failed to get revocation history' });
}
});
export default router;