- 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
622 lines
16 KiB
TypeScript
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,
|
|
};
|
|
|