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

622 lines
16 KiB
TypeScript

/**
* 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,
};