- 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
387 lines
12 KiB
TypeScript
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;
|
|
|