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
This commit is contained in:
386
backend/http/governance.routes.ts
Normal file
386
backend/http/governance.routes.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
Reference in New Issue
Block a user