/** * 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 { const result = await db.query( `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();