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:
Apple
2025-11-29 16:02:06 -08:00
parent 2627205663
commit e233d32ae7
20 changed files with 5837 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
/**
* Audit Service
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Provides access to governance events for audit and analysis
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import {
GovernanceEvent,
GovernanceEventType,
AuditEventFilter,
GovernanceScope,
} from '../../domain/governance/types';
export interface AuditEventRow {
id: string;
event_type: GovernanceEventType;
subject: string;
actor_id: string;
target_id: string;
scope: GovernanceScope;
payload: Record<string, unknown>;
version: string;
status: 'pending' | 'published' | 'failed';
created_at: Date;
published_at?: Date;
}
export class AuditService {
/**
* Get audit events with filters
*/
async getEvents(filter: AuditEventFilter): Promise<{
events: GovernanceEvent[];
total: number;
}> {
const conditions: string[] = [];
const params: (string | Date | number)[] = [];
let paramIndex = 1;
if (filter.eventType) {
conditions.push(`event_type = $${paramIndex++}`);
params.push(filter.eventType);
}
if (filter.actorId) {
conditions.push(`actor_id = $${paramIndex++}`);
params.push(filter.actorId);
}
if (filter.targetId) {
conditions.push(`target_id = $${paramIndex++}`);
params.push(filter.targetId);
}
if (filter.scope) {
conditions.push(`scope = $${paramIndex++}`);
params.push(filter.scope);
}
if (filter.createdAtFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(filter.createdAtFrom);
}
if (filter.createdAtTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(filter.createdAtTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Get total count
const countResult = await db.query<{ count: string }>(
`SELECT COUNT(*) as count FROM event_outbox ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
// Get events
const limit = filter.limit || 50;
const offset = filter.offset || 0;
params.push(limit, offset);
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
params
);
const events: GovernanceEvent[] = result.rows.map(row => ({
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
}));
return { events, total };
}
/**
* Get single event by ID
*/
async getEvent(eventId: string): Promise<GovernanceEvent | null> {
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
WHERE id = $1`,
[eventId]
);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
return {
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
};
}
/**
* Get events by actor
*/
async getEventsByActor(actorId: string, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ actorId, limit });
return result.events;
}
/**
* Get events by target
*/
async getEventsByTarget(targetId: string, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ targetId, limit });
return result.events;
}
/**
* Get events by scope
*/
async getEventsByScope(scope: GovernanceScope, limit = 50): Promise<GovernanceEvent[]> {
const result = await this.getEvents({ scope, limit });
return result.events;
}
/**
* Get event statistics
*/
async getEventStats(
fromDate?: Date,
toDate?: Date
): Promise<{
totalEvents: number;
eventsByType: Record<string, number>;
eventsByDay: Array<{ date: string; count: number }>;
topActors: Array<{ actorId: string; count: number }>;
}> {
const conditions: string[] = [];
const params: (string | Date)[] = [];
let paramIndex = 1;
if (fromDate) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(fromDate);
}
if (toDate) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(toDate);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Total events
const totalResult = await db.query<{ count: string }>(
`SELECT COUNT(*) as count FROM event_outbox ${whereClause}`,
params
);
const totalEvents = parseInt(totalResult.rows[0].count, 10);
// Events by type
const byTypeResult = await db.query<{ event_type: string; count: string }>(
`SELECT event_type, COUNT(*) as count
FROM event_outbox ${whereClause}
GROUP BY event_type`,
params
);
const eventsByType: Record<string, number> = {};
byTypeResult.rows.forEach(row => {
eventsByType[row.event_type] = parseInt(row.count, 10);
});
// Events by day
const byDayResult = await db.query<{ date: string; count: string }>(
`SELECT DATE(created_at) as date, COUNT(*) as count
FROM event_outbox ${whereClause}
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30`,
params
);
const eventsByDay = byDayResult.rows.map(row => ({
date: row.date,
count: parseInt(row.count, 10),
}));
// Top actors
const topActorsResult = await db.query<{ actor_id: string; count: string }>(
`SELECT actor_id, COUNT(*) as count
FROM event_outbox ${whereClause} AND actor_id IS NOT NULL
GROUP BY actor_id
ORDER BY count DESC
LIMIT 10`,
params
);
const topActors = topActorsResult.rows.map(row => ({
actorId: row.actor_id,
count: parseInt(row.count, 10),
}));
return {
totalEvents,
eventsByType,
eventsByDay,
topActors,
};
}
/**
* Get governance events for specific entity
*/
async getEntityHistory(
entityType: 'agent' | 'microdao' | 'district' | 'node' | 'room',
entityId: string,
limit = 50
): Promise<GovernanceEvent[]> {
const result = await db.query<AuditEventRow>(
`SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at
FROM event_outbox
WHERE target_id = $1 OR actor_id = $1 OR scope LIKE $2
ORDER BY created_at DESC
LIMIT $3`,
[entityId, `%${entityId}%`, limit]
);
return result.rows.map(row => ({
id: row.id,
eventType: row.event_type,
subject: row.subject,
actorId: row.actor_id,
targetId: row.target_id,
scope: row.scope,
payload: row.payload,
version: row.version,
status: row.status,
createdAt: row.created_at,
publishedAt: row.published_at,
}));
}
}
export const auditService = new AuditService();

View File

@@ -0,0 +1,316 @@
/**
* Governance Service
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Handles agent promotion, demotion, and role management
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import { v4 as uuidv4 } from 'uuid';
import {
AgentGovLevel,
AgentStatus,
PromoteAgentRequest,
GovernanceEvent,
GovernanceEventType,
GovernanceScope,
GOV_LEVEL_TO_NUM,
} from '../../domain/governance/types';
import { permissionEngine, buildContext } from './permissions';
export class GovernanceService {
/**
* Promote an agent to a new level
*/
async promoteAgent(request: PromoteAgentRequest): Promise<{ success: boolean; error?: string }> {
const context = await buildContext(request.actorId, request.scope);
// Check permission
const canPromote = await permissionEngine.canPromoteAgent(
context,
request.targetId,
request.newLevel
);
if (!canPromote.allowed) {
logger.warn(`Promotion denied: ${request.actorId}${request.targetId}`, {
reason: canPromote.reason,
});
return { success: false, error: canPromote.reason };
}
try {
// Get current level for event
const currentLevel = await permissionEngine.getAgentLevel(request.targetId);
// Update agent level
await db.query(
`UPDATE agents
SET gov_level = $1, updated_at = now()
WHERE id = $2`,
[request.newLevel, request.targetId]
);
// If promoting to orchestrator, update DAIS trust level
if (request.newLevel === 'orchestrator' || request.newLevel === 'district_lead' || request.newLevel === 'city_governance') {
await db.query(
`UPDATE dais_identities
SET trust_level = 'orchestrator', updated_at = now()
WHERE id = (SELECT dais_identity_id FROM agents WHERE id = $1)`,
[request.targetId]
);
}
// Log governance event
await this.logEvent('agent.promoted', request.actorId, request.targetId, request.scope, {
previousLevel: currentLevel,
newLevel: request.newLevel,
reason: request.reason,
});
logger.info(`Agent promoted: ${request.targetId}${request.newLevel}`, {
actorId: request.actorId,
previousLevel: currentLevel,
});
return { success: true };
} catch (error) {
logger.error('Failed to promote agent', error);
throw error;
}
}
/**
* Demote an agent to a lower level
*/
async demoteAgent(
actorId: string,
targetId: string,
newLevel: AgentGovLevel,
scope: GovernanceScope,
reason?: string
): Promise<{ success: boolean; error?: string }> {
const context = await buildContext(actorId, scope);
const targetLevel = await permissionEngine.getAgentLevel(targetId);
const actorLevelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
const targetLevelNum = GOV_LEVEL_TO_NUM[targetLevel];
const newLevelNum = GOV_LEVEL_TO_NUM[newLevel];
// Verify demotion is valid
if (targetLevelNum >= actorLevelNum) {
return { success: false, error: 'Cannot demote agent at same or higher level' };
}
if (newLevelNum >= targetLevelNum) {
return { success: false, error: 'New level must be lower than current level' };
}
if (actorLevelNum < 4) {
return { success: false, error: 'Only Core-team and above can demote agents' };
}
try {
await db.query(
`UPDATE agents
SET gov_level = $1, updated_at = now()
WHERE id = $2`,
[newLevel, targetId]
);
// Update DAIS trust level if demoted from orchestrator
if (['orchestrator', 'district_lead', 'city_governance'].includes(targetLevel)) {
await db.query(
`UPDATE dais_identities
SET trust_level = 'verified', updated_at = now()
WHERE id = (SELECT dais_identity_id FROM agents WHERE id = $1)`,
[targetId]
);
}
await this.logEvent('agent.demoted', actorId, targetId, scope, {
previousLevel: targetLevel,
newLevel,
reason,
});
logger.info(`Agent demoted: ${targetId}${newLevel}`, { actorId });
return { success: true };
} catch (error) {
logger.error('Failed to demote agent', error);
throw error;
}
}
/**
* Get agent roles and permissions
*/
async getAgentRoles(agentId: string): Promise<{
level: AgentGovLevel;
status: AgentStatus;
powers: string[];
assignments: Array<{
microdaoId: string;
role: string;
scope: string;
}>;
}> {
const level = await permissionEngine.getAgentLevel(agentId);
const powers = permissionEngine.getPowersForLevel(level);
const agentData = await db.query<{ status: AgentStatus }>(
`SELECT status FROM agents WHERE id = $1`,
[agentId]
);
const assignments = await db.query<{
target_microdao_id: string;
role: string;
scope: string;
}>(
`SELECT target_microdao_id, role, scope
FROM agent_assignments
WHERE agent_id = $1 AND end_ts IS NULL`,
[agentId]
);
return {
level,
status: agentData.rows[0]?.status || 'active',
powers,
assignments: assignments.rows.map(a => ({
microdaoId: a.target_microdao_id,
role: a.role,
scope: a.scope,
})),
};
}
/**
* Get agents by governance level
*/
async getAgentsByLevel(level: AgentGovLevel, limit = 50): Promise<Array<{
id: string;
name: string;
level: AgentGovLevel;
status: AgentStatus;
homeMicrodaoId?: string;
}>> {
const result = await db.query<{
id: string;
name: string;
gov_level: AgentGovLevel;
status: AgentStatus;
home_microdao_id: string;
}>(
`SELECT id, name, gov_level, status, home_microdao_id
FROM agents
WHERE gov_level = $1 AND status = 'active'
ORDER BY updated_at DESC
LIMIT $2`,
[level, limit]
);
return result.rows.map(r => ({
id: r.id,
name: r.name,
level: r.gov_level,
status: r.status,
homeMicrodaoId: r.home_microdao_id,
}));
}
/**
* Get city governance agents
*/
async getCityGovernanceAgents(): Promise<Array<{
id: string;
name: string;
role: string;
}>> {
const result = await db.query<{
id: string;
name: string;
}>(
`SELECT id, name FROM agents
WHERE gov_level = 'city_governance' AND status = 'active'`
);
const roleMap: Record<string, string> = {
daarwizz: 'Mayor',
dario: 'Community',
daria: 'Tech Governance',
};
return result.rows.map(r => ({
id: r.id,
name: r.name,
role: roleMap[r.id] || 'City Agent',
}));
}
/**
* Get district lead agents
*/
async getDistrictLeadAgents(districtId?: string): Promise<Array<{
agentId: string;
agentName: string;
districtId: string;
districtName: string;
}>> {
let query = `
SELECT a.id as agent_id, a.name as agent_name,
m.id as district_id, m.name as district_name
FROM agents a
JOIN microdaos m ON m.primary_orchestrator_agent_id = a.id
WHERE m.dao_type = 'district' AND a.status = 'active'
`;
const params: string[] = [];
if (districtId) {
query += ` AND m.id = $1`;
params.push(districtId);
}
const result = await db.query<{
agent_id: string;
agent_name: string;
district_id: string;
district_name: string;
}>(query, params);
return result.rows.map(r => ({
agentId: r.agent_id,
agentName: r.agent_name,
districtId: r.district_id,
districtName: r.district_name,
}));
}
/**
* Log governance event to event_outbox
*/
async logEvent(
eventType: GovernanceEventType,
actorId: string,
targetId: string,
scope: GovernanceScope,
payload: Record<string, unknown>
): Promise<void> {
const eventId = uuidv4();
const subject = `dagion.governance.${eventType}`;
await db.query(
`INSERT INTO event_outbox (id, event_type, subject, actor_id, target_id, scope, payload, version)
VALUES ($1, $2, $3, $4, $5, $6, $7, '1.0')`,
[eventId, eventType, subject, actorId, targetId, scope, JSON.stringify(payload)]
);
logger.debug(`Logged governance event: ${eventType}`, { eventId, actorId, targetId });
}
}
export const governanceService = new GovernanceService();

View File

@@ -0,0 +1,552 @@
/**
* Incidents Service
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Handles incident creation, escalation, and resolution
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import { v4 as uuidv4 } from 'uuid';
import {
Incident,
IncidentStatus,
IncidentPriority,
EscalationLevel,
TargetScopeType,
IncidentHistory,
CreateIncidentRequest,
AssignIncidentRequest,
EscalateIncidentRequest,
ResolveIncidentRequest,
} from '../../domain/governance/types';
import { governanceService } from './governance.service';
interface IncidentRow {
id: string;
created_by_dais_id: string;
target_scope_type: TargetScopeType;
target_scope_id: string;
status: IncidentStatus;
priority: IncidentPriority;
assigned_to_dais_id: string | null;
escalation_level: EscalationLevel;
title: string;
description: string | null;
resolution: string | null;
metadata: Record<string, unknown>;
created_at: Date;
updated_at: Date;
resolved_at: Date | null;
closed_at: Date | null;
}
export class IncidentsService {
/**
* Create a new incident
*/
async createIncident(request: CreateIncidentRequest): Promise<Incident> {
const id = uuidv4();
// Determine initial escalation level based on target scope
const escalationLevel = this.determineInitialEscalation(request.targetScopeType);
try {
const result = await db.query<IncidentRow>(
`INSERT INTO incidents (
id, created_by_dais_id, target_scope_type, target_scope_id,
status, priority, escalation_level, title, description, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`,
[
id,
request.createdByDaisId,
request.targetScopeType,
request.targetScopeId,
'open',
request.priority || 'medium',
escalationLevel,
request.title,
request.description || null,
JSON.stringify(request.metadata || {}),
]
);
const incident = this.mapRowToIncident(result.rows[0]);
// Log history
await this.addHistory(id, 'created', request.createdByDaisId, null, {
title: request.title,
priority: request.priority || 'medium',
escalationLevel,
});
// Log governance event
await governanceService.logEvent(
'incident.created',
request.createdByDaisId,
id,
`${request.targetScopeType}:${request.targetScopeId}`,
{ title: request.title, priority: request.priority }
);
logger.info(`Incident created: ${id}`, { title: request.title });
return incident;
} catch (error) {
logger.error('Failed to create incident', error);
throw error;
}
}
/**
* Get incident by ID
*/
async getIncident(incidentId: string): Promise<Incident | null> {
const result = await db.query<IncidentRow>(
`SELECT * FROM incidents WHERE id = $1`,
[incidentId]
);
if (result.rows.length === 0) {
return null;
}
return this.mapRowToIncident(result.rows[0]);
}
/**
* List incidents with filters
*/
async listIncidents(filters: {
status?: IncidentStatus;
priority?: IncidentPriority;
escalationLevel?: EscalationLevel;
targetScopeType?: TargetScopeType;
targetScopeId?: string;
assignedToDaisId?: string;
limit?: number;
offset?: number;
}): Promise<{ incidents: Incident[]; total: number }> {
const conditions: string[] = [];
const params: (string | number)[] = [];
let paramIndex = 1;
if (filters.status) {
conditions.push(`status = $${paramIndex++}`);
params.push(filters.status);
}
if (filters.priority) {
conditions.push(`priority = $${paramIndex++}`);
params.push(filters.priority);
}
if (filters.escalationLevel) {
conditions.push(`escalation_level = $${paramIndex++}`);
params.push(filters.escalationLevel);
}
if (filters.targetScopeType) {
conditions.push(`target_scope_type = $${paramIndex++}`);
params.push(filters.targetScopeType);
}
if (filters.targetScopeId) {
conditions.push(`target_scope_id = $${paramIndex++}`);
params.push(filters.targetScopeId);
}
if (filters.assignedToDaisId) {
conditions.push(`assigned_to_dais_id = $${paramIndex++}`);
params.push(filters.assignedToDaisId);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Get total count
const countResult = await db.query<{ count: string }>(
`SELECT COUNT(*) as count FROM incidents ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
// Get incidents
const limit = filters.limit || 50;
const offset = filters.offset || 0;
params.push(limit, offset);
const result = await db.query<IncidentRow>(
`SELECT * FROM incidents ${whereClause}
ORDER BY
CASE priority
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
params
);
const incidents = result.rows.map(row => this.mapRowToIncident(row));
return { incidents, total };
}
/**
* Assign incident to an agent
*/
async assignIncident(request: AssignIncidentRequest): Promise<{ success: boolean; error?: string }> {
try {
const incident = await this.getIncident(request.incidentId);
if (!incident) {
return { success: false, error: 'Incident not found' };
}
if (incident.status === 'closed') {
return { success: false, error: 'Cannot assign closed incident' };
}
const oldValue = { assignedToDaisId: incident.assignedToDaisId };
await db.query(
`UPDATE incidents
SET assigned_to_dais_id = $1,
status = 'in_progress',
updated_at = now()
WHERE id = $2`,
[request.assignedToDaisId, request.incidentId]
);
await this.addHistory(
request.incidentId,
'assigned',
request.actorDaisId,
oldValue,
{ assignedToDaisId: request.assignedToDaisId }
);
await governanceService.logEvent(
'incident.assigned',
request.actorDaisId,
request.incidentId,
'city',
{ assignedTo: request.assignedToDaisId }
);
logger.info(`Incident assigned: ${request.incidentId}${request.assignedToDaisId}`);
return { success: true };
} catch (error) {
logger.error('Failed to assign incident', error);
throw error;
}
}
/**
* Escalate incident to higher level
*/
async escalateIncident(request: EscalateIncidentRequest): Promise<{ success: boolean; error?: string }> {
try {
const incident = await this.getIncident(request.incidentId);
if (!incident) {
return { success: false, error: 'Incident not found' };
}
if (incident.status === 'closed' || incident.status === 'resolved') {
return { success: false, error: 'Cannot escalate resolved/closed incident' };
}
const levelOrder: EscalationLevel[] = ['microdao', 'district', 'city'];
const currentIndex = levelOrder.indexOf(incident.escalationLevel);
const newIndex = levelOrder.indexOf(request.newLevel);
if (newIndex <= currentIndex) {
return { success: false, error: 'Can only escalate to higher level' };
}
const oldValue = { escalationLevel: incident.escalationLevel };
await db.query(
`UPDATE incidents
SET escalation_level = $1,
updated_at = now()
WHERE id = $2`,
[request.newLevel, request.incidentId]
);
await this.addHistory(
request.incidentId,
'escalated',
request.actorDaisId,
oldValue,
{ escalationLevel: request.newLevel, reason: request.reason }
);
await governanceService.logEvent(
'incident.escalated',
request.actorDaisId,
request.incidentId,
'city',
{
fromLevel: incident.escalationLevel,
toLevel: request.newLevel,
reason: request.reason,
}
);
logger.info(`Incident escalated: ${request.incidentId}${request.newLevel}`);
return { success: true };
} catch (error) {
logger.error('Failed to escalate incident', error);
throw error;
}
}
/**
* Resolve incident
*/
async resolveIncident(request: ResolveIncidentRequest): Promise<{ success: boolean; error?: string }> {
try {
const incident = await this.getIncident(request.incidentId);
if (!incident) {
return { success: false, error: 'Incident not found' };
}
if (incident.status === 'closed' || incident.status === 'resolved') {
return { success: false, error: 'Incident already resolved/closed' };
}
await db.query(
`UPDATE incidents
SET status = 'resolved',
resolution = $1,
resolved_at = now(),
updated_at = now()
WHERE id = $2`,
[request.resolution, request.incidentId]
);
await this.addHistory(
request.incidentId,
'resolved',
request.actorDaisId,
{ status: incident.status },
{ status: 'resolved', resolution: request.resolution }
);
await governanceService.logEvent(
'incident.resolved',
request.actorDaisId,
request.incidentId,
'city',
{ resolution: request.resolution }
);
logger.info(`Incident resolved: ${request.incidentId}`);
return { success: true };
} catch (error) {
logger.error('Failed to resolve incident', error);
throw error;
}
}
/**
* Close incident
*/
async closeIncident(
incidentId: string,
actorDaisId: string
): Promise<{ success: boolean; error?: string }> {
try {
const incident = await this.getIncident(incidentId);
if (!incident) {
return { success: false, error: 'Incident not found' };
}
await db.query(
`UPDATE incidents
SET status = 'closed',
closed_at = now(),
updated_at = now()
WHERE id = $1`,
[incidentId]
);
await this.addHistory(
incidentId,
'closed',
actorDaisId,
{ status: incident.status },
{ status: 'closed' }
);
await governanceService.logEvent(
'incident.closed',
actorDaisId,
incidentId,
'city',
{}
);
logger.info(`Incident closed: ${incidentId}`);
return { success: true };
} catch (error) {
logger.error('Failed to close incident', error);
throw error;
}
}
/**
* Add comment to incident
*/
async addComment(
incidentId: string,
actorDaisId: string,
comment: string
): Promise<{ success: boolean; error?: string }> {
try {
await this.addHistory(incidentId, 'comment', actorDaisId, null, null, comment);
logger.info(`Comment added to incident: ${incidentId}`);
return { success: true };
} catch (error) {
logger.error('Failed to add comment to incident', error);
throw error;
}
}
/**
* Get incident history
*/
async getIncidentHistory(incidentId: string): Promise<IncidentHistory[]> {
const result = await db.query<{
id: string;
incident_id: string;
action: IncidentHistory['action'];
actor_dais_id: string;
old_value: Record<string, unknown> | null;
new_value: Record<string, unknown> | null;
comment: string | null;
created_at: Date;
}>(
`SELECT * FROM incident_history WHERE incident_id = $1 ORDER BY created_at ASC`,
[incidentId]
);
return result.rows.map(row => ({
id: row.id,
incidentId: row.incident_id,
action: row.action,
actorDaisId: row.actor_dais_id,
oldValue: row.old_value || undefined,
newValue: row.new_value || undefined,
comment: row.comment || undefined,
createdAt: row.created_at,
}));
}
/**
* Get open incidents count by escalation level
*/
async getOpenIncidentsCount(): Promise<{
microdao: number;
district: number;
city: number;
total: number;
}> {
const result = await db.query<{ escalation_level: EscalationLevel; count: string }>(
`SELECT escalation_level, COUNT(*) as count
FROM incidents
WHERE status IN ('open', 'in_progress')
GROUP BY escalation_level`
);
const counts = { microdao: 0, district: 0, city: 0, total: 0 };
result.rows.forEach(row => {
counts[row.escalation_level] = parseInt(row.count, 10);
counts.total += parseInt(row.count, 10);
});
return counts;
}
/**
* Determine initial escalation level based on target scope
*/
private determineInitialEscalation(targetScopeType: TargetScopeType): EscalationLevel {
switch (targetScopeType) {
case 'city':
return 'city';
case 'district':
return 'district';
default:
return 'microdao';
}
}
/**
* Add entry to incident history
*/
private async addHistory(
incidentId: string,
action: IncidentHistory['action'],
actorDaisId: string,
oldValue: Record<string, unknown> | null,
newValue: Record<string, unknown> | null,
comment?: string
): Promise<void> {
await db.query(
`INSERT INTO incident_history (incident_id, action, actor_dais_id, old_value, new_value, comment)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
incidentId,
action,
actorDaisId,
oldValue ? JSON.stringify(oldValue) : null,
newValue ? JSON.stringify(newValue) : null,
comment || null,
]
);
}
/**
* Map database row to Incident type
*/
private mapRowToIncident(row: IncidentRow): Incident {
return {
id: row.id,
createdByDaisId: row.created_by_dais_id,
targetScopeType: row.target_scope_type,
targetScopeId: row.target_scope_id,
status: row.status,
priority: row.priority,
assignedToDaisId: row.assigned_to_dais_id || undefined,
escalationLevel: row.escalation_level,
title: row.title,
description: row.description || undefined,
resolution: row.resolution || undefined,
metadata: row.metadata,
createdAt: row.created_at,
updatedAt: row.updated_at,
resolvedAt: row.resolved_at || undefined,
closedAt: row.closed_at || undefined,
};
}
}
export const incidentsService = new IncidentsService();

View File

@@ -0,0 +1,621 @@
/**
* Permission Engine
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Implements permission checks for all governance actions
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import {
AgentGovLevel,
AgentStatus,
GovernancePower,
GovernanceScope,
GovernanceContext,
CanDoResult,
GOV_LEVEL_TO_NUM,
POWER_MATRIX,
TargetType,
PermissionAction,
} from '../../domain/governance/types';
// City governance agent IDs
const CITY_AGENTS = ['daarwizz', 'dario', 'daria'];
/**
* Get agent's governance level
*/
export async function getAgentLevel(agentId: string): Promise<AgentGovLevel> {
const result = await db.query<{ gov_level: AgentGovLevel; status: AgentStatus }>(
`SELECT gov_level, status FROM agents WHERE id = $1`,
[agentId]
);
if (result.rows.length === 0) {
return 'guest';
}
if (result.rows[0].status === 'revoked') {
return 'guest';
}
return result.rows[0].gov_level || 'personal';
}
/**
* Get powers for a governance level
*/
export function getPowersForLevel(level: AgentGovLevel): GovernancePower[] {
const entry = POWER_MATRIX.find(p => p.level === level);
return entry?.powers || [];
}
/**
* Build governance context for an actor
*/
export async function buildContext(
actorAgentId: string,
scope: GovernanceScope
): Promise<GovernanceContext> {
const level = await getAgentLevel(actorAgentId);
const powers = getPowersForLevel(level);
// Get DAIS ID for actor
const agent = await db.query<{ dais_identity_id: string }>(
`SELECT dais_identity_id FROM agents WHERE id = $1`,
[actorAgentId]
);
const daisId = agent.rows[0]?.dais_identity_id || actorAgentId;
// Parse scope
let microdaoId: string | undefined;
let districtId: string | undefined;
if (scope.startsWith('microdao:')) {
microdaoId = scope.replace('microdao:', '');
} else if (scope.startsWith('district:')) {
districtId = scope.replace('district:', '');
}
return {
actorDaisId: daisId,
actorAgentId,
actorLevel: level,
actorPowers: powers,
currentScope: scope,
microdaoId,
districtId,
};
}
// ============================================================================
// PERMISSION CHECKS
// ============================================================================
/**
* Check if agent can create a MicroDAO
* Only Orchestrator (Level 5+) with verified DAIS
*/
export async function canCreateMicrodao(
context: GovernanceContext
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
if (levelNum < 5) {
return {
allowed: false,
reason: 'Only Orchestrators (Level 5+) can create MicroDAO',
requiredLevel: 'orchestrator',
};
}
// Check DAIS trust level
const dais = await db.query<{ trust_level: string }>(
`SELECT trust_level FROM dais_identities WHERE id = $1`,
[context.actorDaisId]
);
if (dais.rows.length === 0 || !['orchestrator', 'operator'].includes(dais.rows[0].trust_level)) {
return {
allowed: false,
reason: 'DAIS must have orchestrator or operator trust level',
};
}
return { allowed: true };
}
/**
* Check if agent can create a District
* Only City Governance (Level 7) or approved Orchestrator
*/
export async function canCreateDistrict(
context: GovernanceContext
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
// City governance can always create
if (levelNum === 7) {
return { allowed: true };
}
// Orchestrator needs city approval
if (levelNum >= 5) {
// Check if there's a pending/approved district request
const approval = await db.query(
`SELECT id FROM event_outbox
WHERE event_type = 'district.creation_approved'
AND payload->>'requestedBy' = $1
AND status = 'published'
LIMIT 1`,
[context.actorAgentId]
);
if (approval.rows.length > 0) {
return { allowed: true };
}
return {
allowed: false,
reason: 'Orchestrator needs city approval to create District',
};
}
return {
allowed: false,
reason: 'Only City Governance can create Districts',
requiredLevel: 'city_governance',
};
}
/**
* Check if agent can register a node
* Orchestrator, Core-team DevOps, Node Manager, City Infrastructure
*/
export async function canRegisterNode(
context: GovernanceContext
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
// Core-team and above can register
if (levelNum >= 4) {
return { allowed: true };
}
// Check for Node Manager role in assignments
const assignment = await db.query(
`SELECT id FROM agent_assignments
WHERE agent_id = $1
AND role IN ('devops', 'node-manager')
AND end_ts IS NULL`,
[context.actorAgentId]
);
if (assignment.rows.length > 0) {
return { allowed: true };
}
return {
allowed: false,
reason: 'Only Core-team, DevOps, or Node Managers can register nodes',
requiredLevel: 'core_team',
requiredPower: 'infrastructure',
};
}
/**
* Check if agent can create a room
*/
export async function canCreateRoom(
context: GovernanceContext,
roomType: string
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
switch (roomType) {
case 'personal':
// Personal agents can create personal rooms
if (levelNum >= 1) return { allowed: true };
break;
case 'project':
// Workers can create project rooms
if (levelNum >= 3) return { allowed: true };
break;
case 'dao-room':
case 'dao-wide':
// Core-team can create DAO-wide rooms
if (levelNum >= 4) return { allowed: true };
break;
case 'front-room':
case 'portal':
// Orchestrator can create front-rooms and portals
if (levelNum >= 5) return { allowed: true };
break;
case 'city-room':
// City agents only
if (levelNum === 7) return { allowed: true };
break;
case 'district-room':
// District lead or higher
if (levelNum >= 6) return { allowed: true };
break;
}
return {
allowed: false,
reason: `Insufficient permissions to create ${roomType} room`,
};
}
/**
* Check if agent can create a front-portal in city
*/
export async function canCreateFrontPortal(
context: GovernanceContext,
targetMicrodaoId: string
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
// City agents can create any portal
if (levelNum === 7) {
return { allowed: true };
}
// District lead can create portals for their district
if (levelNum === 6) {
// Check if target is in their district
const district = await db.query(
`SELECT id FROM microdaos
WHERE id = $1
AND parent_microdao_id = (
SELECT id FROM microdaos WHERE primary_orchestrator_agent_id = $2 AND dao_type = 'district'
)`,
[targetMicrodaoId, context.actorAgentId]
);
if (district.rows.length > 0) {
return { allowed: true };
}
}
// Orchestrator can create portal for their own MicroDAO
if (levelNum === 5) {
const microdao = await db.query(
`SELECT id FROM microdaos
WHERE id = $1 AND primary_orchestrator_agent_id = $2`,
[targetMicrodaoId, context.actorAgentId]
);
if (microdao.rows.length > 0) {
return { allowed: true };
}
return {
allowed: false,
reason: 'Orchestrator can only create portal for their own MicroDAO',
};
}
return {
allowed: false,
reason: 'Only Orchestrators and above can create front-portals',
requiredLevel: 'orchestrator',
};
}
/**
* Check if actor can promote target agent
*/
export async function canPromoteAgent(
context: GovernanceContext,
targetId: string,
newLevel: AgentGovLevel
): Promise<CanDoResult> {
const actorLevelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
const newLevelNum = GOV_LEVEL_TO_NUM[newLevel];
const targetLevel = await getAgentLevel(targetId);
const targetLevelNum = GOV_LEVEL_TO_NUM[targetLevel];
// Cannot promote to same or higher level than self
if (newLevelNum >= actorLevelNum) {
return {
allowed: false,
reason: 'Cannot promote agent to same or higher level than yourself',
};
}
// Cannot promote agent already at higher level
if (targetLevelNum >= newLevelNum) {
return {
allowed: false,
reason: 'Target agent is already at this level or higher',
};
}
// Only core-team and above can promote
if (actorLevelNum < 4) {
return {
allowed: false,
reason: 'Only Core-team and above can promote agents',
requiredLevel: 'core_team',
};
}
// Check scope - can only promote in own DAO/District
if (context.currentScope.startsWith('microdao:')) {
const microdaoId = context.currentScope.replace('microdao:', '');
// Check if actor is orchestrator of this DAO
const isOrchestrator = await db.query(
`SELECT id FROM microdaos
WHERE id = $1 AND primary_orchestrator_agent_id = $2`,
[microdaoId, context.actorAgentId]
);
// Check if target is in this DAO
const targetInDao = await db.query(
`SELECT id FROM agent_assignments
WHERE agent_id = $1 AND target_microdao_id = $2 AND end_ts IS NULL`,
[targetId, microdaoId]
);
if (isOrchestrator.rows.length === 0 && actorLevelNum < 6) {
return {
allowed: false,
reason: 'Only the DAO Orchestrator can promote agents in this DAO',
};
}
if (targetInDao.rows.length === 0 && actorLevelNum < 7) {
return {
allowed: false,
reason: 'Target agent is not a member of this DAO',
};
}
}
return { allowed: true };
}
/**
* Check if actor can revoke target agent
*/
export async function canRevokeAgent(
context: GovernanceContext,
targetId: string
): Promise<CanDoResult> {
const actorLevelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
const targetLevel = await getAgentLevel(targetId);
const targetLevelNum = GOV_LEVEL_TO_NUM[targetLevel];
// Cannot revoke same or higher level
if (targetLevelNum >= actorLevelNum) {
return {
allowed: false,
reason: 'Cannot revoke agent at same or higher level',
};
}
// Must have identity power
if (!context.actorPowers.includes('identity')) {
return {
allowed: false,
reason: 'Requires identity power to revoke agents',
requiredPower: 'identity',
};
}
// City governance can revoke anyone
if (actorLevelNum === 7) {
return { allowed: true };
}
// District lead can revoke in their district
if (actorLevelNum === 6) {
// Check if target is in actor's district
const inDistrict = await db.query(
`SELECT a.id FROM agents a
JOIN agent_assignments aa ON a.id = aa.agent_id
JOIN microdaos m ON aa.target_microdao_id = m.id
WHERE a.id = $1
AND m.parent_microdao_id = (
SELECT id FROM microdaos WHERE primary_orchestrator_agent_id = $2 AND dao_type = 'district'
)`,
[targetId, context.actorAgentId]
);
if (inDistrict.rows.length > 0) {
return { allowed: true };
}
}
// Orchestrator can revoke in their DAO
if (actorLevelNum === 5 && context.currentScope.startsWith('microdao:')) {
const microdaoId = context.currentScope.replace('microdao:', '');
const isOrchestrator = await db.query(
`SELECT id FROM microdaos
WHERE id = $1 AND primary_orchestrator_agent_id = $2`,
[microdaoId, context.actorAgentId]
);
const targetInDao = await db.query(
`SELECT id FROM agent_assignments
WHERE agent_id = $1 AND target_microdao_id = $2 AND end_ts IS NULL`,
[targetId, microdaoId]
);
if (isOrchestrator.rows.length > 0 && targetInDao.rows.length > 0) {
return { allowed: true };
}
}
return {
allowed: false,
reason: 'Insufficient permissions to revoke this agent',
};
}
/**
* Check if actor can moderate a room
*/
export async function canModerateRoom(
context: GovernanceContext,
roomId: string
): Promise<CanDoResult> {
const levelNum = GOV_LEVEL_TO_NUM[context.actorLevel];
// Must have moderation power
if (!context.actorPowers.includes('moderation')) {
return {
allowed: false,
reason: 'Requires moderation power',
requiredPower: 'moderation',
};
}
// Get room info
const room = await db.query<{
owner_type: string;
owner_id: string;
type: string;
space_scope: string;
}>(
`SELECT owner_type, owner_id, type, space_scope FROM rooms WHERE id = $1`,
[roomId]
);
if (room.rows.length === 0) {
return { allowed: false, reason: 'Room not found' };
}
const roomData = room.rows[0];
// City governance can moderate any room
if (levelNum === 7) {
return { allowed: true };
}
// City rooms - only city agents
if (roomData.type === 'city-room') {
return {
allowed: false,
reason: 'Only City Governance can moderate city rooms',
requiredLevel: 'city_governance',
};
}
// District rooms - district lead or higher
if (roomData.type === 'district-room') {
if (levelNum >= 6) {
// Check if actor is lead of this district
const isLead = await db.query(
`SELECT id FROM microdaos
WHERE id = $1 AND primary_orchestrator_agent_id = $2 AND dao_type = 'district'`,
[roomData.owner_id, context.actorAgentId]
);
if (isLead.rows.length > 0) {
return { allowed: true };
}
}
return {
allowed: false,
reason: 'Only District Lead can moderate this room',
requiredLevel: 'district_lead',
};
}
// DAO rooms - check if actor has role in this DAO
if (roomData.space_scope === 'microdao') {
const assignment = await db.query(
`SELECT role FROM agent_assignments
WHERE agent_id = $1 AND target_microdao_id = $2 AND end_ts IS NULL`,
[context.actorAgentId, roomData.owner_id]
);
if (assignment.rows.length > 0) {
return { allowed: true };
}
return {
allowed: false,
reason: 'Must be a member of this DAO to moderate its rooms',
};
}
return { allowed: true };
}
/**
* Check explicit permission in database
*/
export async function hasExplicitPermission(
daisId: string,
targetType: TargetType,
targetId: string,
action: PermissionAction
): Promise<boolean> {
const result = await db.query(
`SELECT id FROM permissions
WHERE dais_id = $1
AND target_type = $2
AND target_id = $3
AND action = $4
AND (expires_at IS NULL OR expires_at > now())`,
[daisId, targetType, targetId, action]
);
return result.rows.length > 0;
}
/**
* Check if agent is a city governance agent
*/
export function isCityAgent(agentId: string): boolean {
return CITY_AGENTS.includes(agentId);
}
/**
* Log permission check for audit
*/
export async function logPermissionCheck(
actorId: string,
action: string,
targetId: string,
result: CanDoResult
): Promise<void> {
logger.info(`Permission check: ${actorId}${action}${targetId}: ${result.allowed ? 'ALLOWED' : 'DENIED'}`, {
actorId,
action,
targetId,
allowed: result.allowed,
reason: result.reason,
});
}
export const permissionEngine = {
getAgentLevel,
getPowersForLevel,
buildContext,
canCreateMicrodao,
canCreateDistrict,
canRegisterNode,
canCreateRoom,
canCreateFrontPortal,
canPromoteAgent,
canRevokeAgent,
canModerateRoom,
hasExplicitPermission,
isCityAgent,
logPermissionCheck,
};

View File

@@ -0,0 +1,350 @@
/**
* Revocation Service
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Handles agent revocation, suspension, and reinstatement
*/
import { db } from '../../infra/db/client';
import { logger } from '../../infra/logger/logger';
import { v4 as uuidv4 } from 'uuid';
import {
RevokeAgentRequest,
RevocationType,
RevocationEffect,
AgentRevocation,
GovernanceScope,
} from '../../domain/governance/types';
import { permissionEngine, buildContext } from './permissions';
import { governanceService } from './governance.service';
export class RevocationService {
/**
* Revoke an agent
*/
async revokeAgent(request: RevokeAgentRequest): Promise<{
success: boolean;
revocationId?: string;
error?: string;
}> {
const context = await buildContext(request.actorId, request.scope);
// Check permission
const canRevoke = await permissionEngine.canRevokeAgent(context, request.targetId);
if (!canRevoke.allowed) {
logger.warn(`Revocation denied: ${request.actorId}${request.targetId}`, {
reason: canRevoke.reason,
});
return { success: false, error: canRevoke.reason };
}
try {
const revocationId = uuidv4();
// Get target agent DAIS ID
const agent = await db.query<{ dais_identity_id: string }>(
`SELECT dais_identity_id FROM agents WHERE id = $1`,
[request.targetId]
);
const daisId = agent.rows[0]?.dais_identity_id;
// Determine what to revoke based on type
const effect = this.getRevocationEffect(request.revocationType);
// Start transaction
await db.query('BEGIN');
try {
// Update agent status
const status = request.revocationType === 'shadow' ? 'active' : 'revoked';
await db.query(
`UPDATE agents
SET status = $1,
revoked_at = now(),
revoked_by = $2,
revocation_reason = $3,
revocation_type = $4,
updated_at = now()
WHERE id = $5`,
[status, request.actorId, request.reason, request.revocationType, request.targetId]
);
// Invalidate DAIS keys if applicable
if (effect.daisKeysInvalidated && daisId) {
await db.query(
`UPDATE dais_keys
SET revoked = true,
revoked_at = now(),
revoked_by = $1,
revoked_reason = $2
WHERE dais_id = $3 AND revoked = false`,
[request.actorId, request.reason, daisId]
);
}
// Terminate assignments if applicable
if (effect.assignmentsTerminated) {
await db.query(
`UPDATE agent_assignments
SET end_ts = now()
WHERE agent_id = $1 AND end_ts IS NULL`,
[request.targetId]
);
}
// Create revocation record
await db.query(
`INSERT INTO agent_revocations (
id, agent_id, dais_id, revoked_by, revocation_type, reason, scope,
keys_invalidated, wallet_disabled, room_access_revoked,
node_privileges_removed, assignments_terminated, reversible
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
[
revocationId,
request.targetId,
daisId,
request.actorId,
request.revocationType,
request.reason,
request.scope,
effect.daisKeysInvalidated,
effect.walletSigningDisabled,
effect.roomAccessRevoked,
effect.nodePrivilegesRemoved,
effect.assignmentsTerminated,
request.revocationType !== 'hard',
]
);
await db.query('COMMIT');
// Log governance event
await governanceService.logEvent('agent.revoked', request.actorId, request.targetId, request.scope, {
revocationId,
revocationType: request.revocationType,
reason: request.reason,
effect,
});
logger.info(`Agent revoked: ${request.targetId}`, {
actorId: request.actorId,
revocationType: request.revocationType,
});
return { success: true, revocationId };
} catch (error) {
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
logger.error('Failed to revoke agent', error);
throw error;
}
}
/**
* Suspend an agent temporarily
*/
async suspendAgent(
actorId: string,
targetId: string,
reason: string,
scope: GovernanceScope,
durationHours?: number
): Promise<{ success: boolean; error?: string }> {
const context = await buildContext(actorId, scope);
// Check permission
const canRevoke = await permissionEngine.canRevokeAgent(context, targetId);
if (!canRevoke.allowed) {
return { success: false, error: canRevoke.reason };
}
try {
await db.query(
`UPDATE agents
SET status = 'suspended',
revoked_at = now(),
revoked_by = $1,
revocation_reason = $2,
revocation_type = 'soft',
updated_at = now()
WHERE id = $3`,
[actorId, reason, targetId]
);
await governanceService.logEvent('agent.revoked', actorId, targetId, scope, {
action: 'suspended',
reason,
durationHours,
});
logger.info(`Agent suspended: ${targetId}`, { actorId, durationHours });
return { success: true };
} catch (error) {
logger.error('Failed to suspend agent', error);
throw error;
}
}
/**
* Reinstate a revoked/suspended agent
*/
async reinstateAgent(
actorId: string,
targetId: string,
scope: GovernanceScope,
reason?: string
): Promise<{ success: boolean; error?: string }> {
const context = await buildContext(actorId, scope);
// Check actor has sufficient level
if (context.actorLevel !== 'city_governance' && context.actorLevel !== 'district_lead') {
return { success: false, error: 'Only City Governance or District Lead can reinstate agents' };
}
try {
// Check if revocation is reversible
const revocation = await db.query<{ reversible: boolean; revocation_type: RevocationType }>(
`SELECT reversible, revocation_type FROM agent_revocations
WHERE agent_id = $1 AND reversed_at IS NULL
ORDER BY created_at DESC LIMIT 1`,
[targetId]
);
if (revocation.rows.length === 0) {
return { success: false, error: 'No active revocation found' };
}
if (!revocation.rows[0].reversible) {
return { success: false, error: 'This revocation is not reversible (hard revocation)' };
}
await db.query('BEGIN');
try {
// Restore agent status
await db.query(
`UPDATE agents
SET status = 'active',
revoked_at = NULL,
revoked_by = NULL,
revocation_reason = NULL,
revocation_type = NULL,
updated_at = now()
WHERE id = $1`,
[targetId]
);
// Mark revocation as reversed
await db.query(
`UPDATE agent_revocations
SET reversed_at = now(), reversed_by = $1
WHERE agent_id = $2 AND reversed_at IS NULL`,
[actorId, targetId]
);
await db.query('COMMIT');
await governanceService.logEvent('agent.reinstated', actorId, targetId, scope, {
reason,
});
logger.info(`Agent reinstated: ${targetId}`, { actorId });
return { success: true };
} catch (error) {
await db.query('ROLLBACK');
throw error;
}
} catch (error) {
logger.error('Failed to reinstate agent', error);
throw error;
}
}
/**
* Revoke DAIS keys
*/
async revokeDaisKeys(
actorId: string,
daisId: string,
reason: string
): Promise<{ success: boolean; keysRevoked: number; error?: string }> {
try {
// Get actor level
const actorLevel = await permissionEngine.getAgentLevel(actorId);
// Only City Governance, District Lead, or Orchestrator can revoke keys
if (!['city_governance', 'district_lead', 'orchestrator'].includes(actorLevel)) {
return { success: false, keysRevoked: 0, error: 'Insufficient permissions to revoke keys' };
}
const result = await db.query(
`UPDATE dais_keys
SET revoked = true, revoked_at = now(), revoked_by = $1, revoked_reason = $2
WHERE dais_id = $3 AND revoked = false
RETURNING id`,
[actorId, reason, daisId]
);
logger.info(`DAIS keys revoked: ${daisId}`, { actorId, count: result.rowCount });
return { success: true, keysRevoked: result.rowCount || 0 };
} catch (error) {
logger.error('Failed to revoke DAIS keys', error);
throw error;
}
}
/**
* Get revocation history for an agent
*/
async getRevocationHistory(agentId: string): Promise<AgentRevocation[]> {
const result = await db.query<AgentRevocation>(
`SELECT * FROM agent_revocations WHERE agent_id = $1 ORDER BY created_at DESC`,
[agentId]
);
return result.rows;
}
/**
* Get revocation effect based on type
*/
private getRevocationEffect(type: RevocationType): RevocationEffect {
switch (type) {
case 'hard':
return {
daisKeysInvalidated: true,
walletSigningDisabled: true,
roomAccessRevoked: true,
nodePrivilegesRemoved: true,
assignmentsTerminated: true,
};
case 'soft':
return {
daisKeysInvalidated: false,
walletSigningDisabled: true,
roomAccessRevoked: true,
nodePrivilegesRemoved: true,
assignmentsTerminated: true,
};
case 'shadow':
return {
daisKeysInvalidated: false,
walletSigningDisabled: false,
roomAccessRevoked: false,
nodePrivilegesRemoved: false,
assignmentsTerminated: false,
};
}
}
}
export const revocationService = new RevocationService();