- 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
317 lines
8.5 KiB
TypeScript
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();
|
|
|