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

317 lines
8.5 KiB
TypeScript

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