Files
microdao-daarion/backend/services/governance/revocation.service.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

351 lines
10 KiB
TypeScript

/**
* 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();