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
This commit is contained in:
Apple
2025-11-29 16:02:06 -08:00
parent 2627205663
commit e233d32ae7
20 changed files with 5837 additions and 0 deletions

82
src/api/audit.ts Normal file
View File

@@ -0,0 +1,82 @@
/**
* Audit API Client
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
import { apiClient } from './client';
import type {
GovernanceEvent,
GovernanceEventType,
GovernanceScope,
AuditEventFilter,
AuditStats,
} from '../types/governance';
// ============================================================================
// AUDIT EVENTS
// ============================================================================
export async function getAuditEvents(filter: AuditEventFilter = {}): Promise<{
events: GovernanceEvent[];
total: number;
}> {
const response = await apiClient.get('/audit/events', { params: filter });
return response.data;
}
export async function getAuditEvent(eventId: string): Promise<GovernanceEvent | null> {
const response = await apiClient.get(`/audit/events/${eventId}`);
return response.data;
}
export async function getEventsByActor(actorId: string, limit?: number): Promise<GovernanceEvent[]> {
const response = await apiClient.get(`/audit/actor/${actorId}`, {
params: limit ? { limit } : undefined,
});
return response.data;
}
export async function getEventsByTarget(targetId: string, limit?: number): Promise<GovernanceEvent[]> {
const response = await apiClient.get(`/audit/target/${targetId}`, {
params: limit ? { limit } : undefined,
});
return response.data;
}
export async function getEventsByScope(scope: GovernanceScope, limit?: number): Promise<GovernanceEvent[]> {
const response = await apiClient.get(`/audit/scope/${scope}`, {
params: limit ? { limit } : undefined,
});
return response.data;
}
export async function getAuditStats(fromDate?: Date, toDate?: Date): Promise<AuditStats> {
const params: Record<string, string> = {};
if (fromDate) params.fromDate = fromDate.toISOString();
if (toDate) params.toDate = toDate.toISOString();
const response = await apiClient.get('/audit/stats', { params });
return response.data;
}
export async function getEntityHistory(
entityType: 'agent' | 'microdao' | 'district' | 'node' | 'room',
entityId: string,
limit?: number
): Promise<GovernanceEvent[]> {
const response = await apiClient.get(`/audit/entity/${entityType}/${entityId}`, {
params: limit ? { limit } : undefined,
});
return response.data;
}
export const auditApi = {
getAuditEvents,
getAuditEvent,
getEventsByActor,
getEventsByTarget,
getEventsByScope,
getAuditStats,
getEntityHistory,
};

187
src/api/governance.ts Normal file
View File

@@ -0,0 +1,187 @@
/**
* Governance API Client
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
import { apiClient } from './client';
import type {
AgentGovLevel,
AgentRolesResponse,
GovernanceScope,
RevocationType,
AgentRevocation,
GovernancePower,
} from '../types/governance';
// ============================================================================
// AGENT PROMOTION/DEMOTION
// ============================================================================
export async function promoteAgent(params: {
actorId: string;
targetId: string;
newLevel: AgentGovLevel;
scope: GovernanceScope;
reason?: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post('/governance/agent/promote', params);
return response.data;
}
export async function demoteAgent(params: {
actorId: string;
targetId: string;
newLevel: AgentGovLevel;
scope: GovernanceScope;
reason?: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post('/governance/agent/demote', params);
return response.data;
}
// ============================================================================
// AGENT REVOCATION
// ============================================================================
export async function revokeAgent(params: {
actorId: string;
targetId: string;
reason: string;
scope: GovernanceScope;
revocationType?: RevocationType;
}): Promise<{ success: boolean; revocationId?: string; error?: string }> {
const response = await apiClient.post('/governance/agent/revoke', params);
return response.data;
}
export async function suspendAgent(params: {
actorId: string;
targetId: string;
reason: string;
scope: GovernanceScope;
durationHours?: number;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post('/governance/agent/suspend', params);
return response.data;
}
export async function reinstateAgent(params: {
actorId: string;
targetId: string;
scope: GovernanceScope;
reason?: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post('/governance/agent/reinstate', params);
return response.data;
}
export async function getRevocationHistory(agentId: string): Promise<AgentRevocation[]> {
const response = await apiClient.get(`/governance/agent/${agentId}/revocations`);
return response.data;
}
// ============================================================================
// AGENT ROLES & PERMISSIONS
// ============================================================================
export async function getAgentRoles(agentId: string): Promise<AgentRolesResponse> {
const response = await apiClient.get(`/governance/agent/${agentId}/roles`);
return response.data;
}
export async function getAgentPermissions(
agentId: string,
targetType?: string,
targetId?: string,
action?: string
): Promise<{ level: AgentGovLevel; powers: GovernancePower[] } | { hasPermission: boolean }> {
const params: Record<string, string> = {};
if (targetType) params.targetType = targetType;
if (targetId) params.targetId = targetId;
if (action) params.action = action;
const response = await apiClient.get(`/governance/agent/${agentId}/permissions`, { params });
return response.data;
}
// ============================================================================
// PERMISSION CHECKS
// ============================================================================
export async function checkPermission(params: {
actorId: string;
action: string;
targetId?: string;
scope?: GovernanceScope;
roomType?: string;
}): Promise<{ allowed: boolean; reason?: string; requiredLevel?: AgentGovLevel; requiredPower?: GovernancePower }> {
const response = await apiClient.post('/governance/check', params);
return response.data;
}
// ============================================================================
// GOVERNANCE AGENTS
// ============================================================================
export async function getCityGovernanceAgents(): Promise<Array<{ id: string; name: string; role: string }>> {
const response = await apiClient.get('/governance/agents/city');
return response.data;
}
export async function getDistrictLeadAgents(districtId?: string): Promise<Array<{
agentId: string;
agentName: string;
districtId: string;
districtName: string;
}>> {
const response = await apiClient.get('/governance/agents/district-leads', {
params: districtId ? { districtId } : undefined,
});
return response.data;
}
export async function getAgentsByLevel(
level: AgentGovLevel,
limit?: number
): Promise<Array<{
id: string;
name: string;
level: AgentGovLevel;
status: string;
homeMicrodaoId?: string;
}>> {
const response = await apiClient.get(`/governance/agents/by-level/${level}`, {
params: limit ? { limit } : undefined,
});
return response.data;
}
// ============================================================================
// DAIS KEY REVOCATION
// ============================================================================
export async function revokeDaisKeys(params: {
actorId: string;
daisId: string;
reason: string;
}): Promise<{ success: boolean; keysRevoked?: number; error?: string }> {
const response = await apiClient.post('/governance/dais/keys/revoke', params);
return response.data;
}
export const governanceApi = {
promoteAgent,
demoteAgent,
revokeAgent,
suspendAgent,
reinstateAgent,
getRevocationHistory,
getAgentRoles,
getAgentPermissions,
checkPermission,
getCityGovernanceAgents,
getDistrictLeadAgents,
getAgentsByLevel,
revokeDaisKeys,
};

143
src/api/incidents.ts Normal file
View File

@@ -0,0 +1,143 @@
/**
* Incidents API Client
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
import { apiClient } from './client';
import type {
Incident,
IncidentHistory,
IncidentStatus,
IncidentPriority,
EscalationLevel,
TargetScopeType,
IncidentsCount,
} from '../types/governance';
// ============================================================================
// INCIDENTS CRUD
// ============================================================================
export async function createIncident(params: {
createdByDaisId: string;
targetScopeType: TargetScopeType;
targetScopeId: string;
priority?: IncidentPriority;
title: string;
description?: string;
metadata?: Record<string, unknown>;
}): Promise<Incident> {
const response = await apiClient.post('/incidents', params);
return response.data;
}
export async function getIncident(incidentId: string): Promise<Incident | null> {
const response = await apiClient.get(`/incidents/${incidentId}`);
return response.data;
}
export async function listIncidents(filters: {
status?: IncidentStatus;
priority?: IncidentPriority;
escalationLevel?: EscalationLevel;
targetScopeType?: TargetScopeType;
targetScopeId?: string;
assignedToDaisId?: string;
limit?: number;
offset?: number;
} = {}): Promise<{ incidents: Incident[]; total: number }> {
const response = await apiClient.get('/incidents', { params: filters });
return response.data;
}
export async function getIncidentsCount(): Promise<IncidentsCount> {
const response = await apiClient.get('/incidents/count');
return response.data;
}
// ============================================================================
// INCIDENT ACTIONS
// ============================================================================
export async function assignIncident(params: {
incidentId: string;
assignedToDaisId: string;
actorDaisId: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post(`/incidents/${params.incidentId}/assign`, {
assignedToDaisId: params.assignedToDaisId,
actorDaisId: params.actorDaisId,
});
return response.data;
}
export async function escalateIncident(params: {
incidentId: string;
newLevel: EscalationLevel;
actorDaisId: string;
reason?: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post(`/incidents/${params.incidentId}/escalate`, {
newLevel: params.newLevel,
actorDaisId: params.actorDaisId,
reason: params.reason,
});
return response.data;
}
export async function resolveIncident(params: {
incidentId: string;
resolution: string;
actorDaisId: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post(`/incidents/${params.incidentId}/resolve`, {
resolution: params.resolution,
actorDaisId: params.actorDaisId,
});
return response.data;
}
export async function closeIncident(params: {
incidentId: string;
actorDaisId: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post(`/incidents/${params.incidentId}/close`, {
actorDaisId: params.actorDaisId,
});
return response.data;
}
export async function addIncidentComment(params: {
incidentId: string;
actorDaisId: string;
comment: string;
}): Promise<{ success: boolean; error?: string }> {
const response = await apiClient.post(`/incidents/${params.incidentId}/comment`, {
actorDaisId: params.actorDaisId,
comment: params.comment,
});
return response.data;
}
// ============================================================================
// INCIDENT HISTORY
// ============================================================================
export async function getIncidentHistory(incidentId: string): Promise<IncidentHistory[]> {
const response = await apiClient.get(`/incidents/${incidentId}/history`);
return response.data;
}
export const incidentsApi = {
createIncident,
getIncident,
listIncidents,
getIncidentsCount,
assignIncident,
escalateIncident,
resolveIncident,
closeIncident,
addIncidentComment,
getIncidentHistory,
};

View File

@@ -0,0 +1,307 @@
/**
* Audit Dashboard
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Shows governance events from event_outbox with filters and statistics
*/
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { auditApi } from '../../../api/audit';
import type { GovernanceEvent, GovernanceEventType, AuditEventFilter } from '../../../types/governance';
const EVENT_TYPE_LABELS: Partial<Record<GovernanceEventType, string>> = {
'agent.promoted': '⬆️ Просування агента',
'agent.demoted': '⬇️ Зниження агента',
'agent.revoked': '🚫 Відкликання агента',
'agent.reinstated': '✅ Відновлення агента',
'agent.assigned': '📋 Призначення',
'incident.created': '⚠️ Створено інцидент',
'incident.escalated': '📈 Ескалація',
'incident.resolved': '✅ Вирішено',
'microdao.created': '🏢 Створено MicroDAO',
'district.created': '🏘️ Створено District',
'node.registered': '🖥️ Зареєстровано Node',
'room.created': '💬 Створено Room',
};
export const AuditDashboard: React.FC = () => {
const [filter, setFilter] = useState<AuditEventFilter>({
limit: 50,
offset: 0,
});
const [selectedEvent, setSelectedEvent] = useState<GovernanceEvent | null>(null);
// Fetch events
const { data: eventsData, isLoading: loadingEvents } = useQuery({
queryKey: ['audit', 'events', filter],
queryFn: () => auditApi.getAuditEvents(filter),
});
// Fetch stats
const { data: stats, isLoading: loadingStats } = useQuery({
queryKey: ['audit', 'stats'],
queryFn: () => auditApi.getAuditStats(),
});
const handleFilterChange = (key: keyof AuditEventFilter, value: string | undefined) => {
setFilter(prev => ({
...prev,
[key]: value || undefined,
offset: 0, // Reset pagination on filter change
}));
};
return (
<div className="bg-slate-900 rounded-xl border border-slate-700 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-900/50 to-cyan-900/50 px-6 py-4 border-b border-slate-700">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
📊 Audit Dashboard
</h2>
<p className="text-sm text-slate-400 mt-1">
Перегляд governance-подій та аудит дій
</p>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 p-6 border-b border-slate-700">
<StatCard
label="Всього подій"
value={stats.totalEvents}
icon="📈"
/>
<StatCard
label="Типів подій"
value={Object.keys(stats.eventsByType).length}
icon="🏷️"
/>
<StatCard
label="Активних учасників"
value={stats.topActors.length}
icon="👥"
/>
<StatCard
label="Подій за 30 днів"
value={stats.eventsByDay.reduce((sum, d) => sum + d.count, 0)}
icon="📅"
/>
</div>
)}
{/* Filters */}
<div className="p-6 border-b border-slate-700 bg-slate-800/50">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Тип події</label>
<select
value={filter.eventType || ''}
onChange={(e) => handleFilterChange('eventType', e.target.value as GovernanceEventType)}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
>
<option value="">Всі типи</option>
{Object.entries(EVENT_TYPE_LABELS).map(([type, label]) => (
<option key={type} value={type}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Actor ID</label>
<input
type="text"
value={filter.actorId || ''}
onChange={(e) => handleFilterChange('actorId', e.target.value)}
placeholder="Фільтр по актору..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Target ID</label>
<input
type="text"
value={filter.targetId || ''}
onChange={(e) => handleFilterChange('targetId', e.target.value)}
placeholder="Фільтр по цілі..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Scope</label>
<input
type="text"
value={filter.scope || ''}
onChange={(e) => handleFilterChange('scope', e.target.value)}
placeholder="city, district:..., microdao:..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
/>
</div>
</div>
</div>
{/* Events List */}
<div className="p-6">
{loadingEvents ? (
<div className="text-slate-400 text-center py-8">Завантаження...</div>
) : eventsData?.events.length === 0 ? (
<div className="text-slate-400 text-center py-8">Немає подій за заданими фільтрами</div>
) : (
<div className="space-y-3">
{eventsData?.events.map((event) => (
<EventRow
key={event.id}
event={event}
onClick={() => setSelectedEvent(event)}
/>
))}
</div>
)}
{/* Pagination */}
{eventsData && eventsData.total > filter.limit! && (
<div className="flex justify-center gap-2 mt-6">
<button
onClick={() => setFilter(prev => ({ ...prev, offset: Math.max(0, (prev.offset || 0) - prev.limit!) }))}
disabled={!filter.offset}
className="px-4 py-2 bg-slate-700 rounded-lg text-white disabled:opacity-50"
>
Попередні
</button>
<span className="px-4 py-2 text-slate-400">
{(filter.offset || 0) + 1} - {Math.min((filter.offset || 0) + filter.limit!, eventsData.total)} з {eventsData.total}
</span>
<button
onClick={() => setFilter(prev => ({ ...prev, offset: (prev.offset || 0) + prev.limit! }))}
disabled={(filter.offset || 0) + filter.limit! >= eventsData.total}
className="px-4 py-2 bg-slate-700 rounded-lg text-white disabled:opacity-50"
>
Наступні
</button>
</div>
)}
</div>
{/* Event Detail Modal */}
{selectedEvent && (
<EventDetailModal
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
/>
)}
</div>
);
};
// Stat Card Component
const StatCard: React.FC<{ label: string; value: number; icon: string }> = ({ label, value, icon }) => (
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
<div className="flex items-center gap-2 text-slate-400 text-sm mb-1">
<span>{icon}</span>
<span>{label}</span>
</div>
<div className="text-2xl font-bold text-white">{value.toLocaleString()}</div>
</div>
);
// Event Row Component
const EventRow: React.FC<{ event: GovernanceEvent; onClick: () => void }> = ({ event, onClick }) => {
const label = EVENT_TYPE_LABELS[event.eventType] || event.eventType;
const statusColors = {
pending: 'bg-yellow-500/20 text-yellow-400',
published: 'bg-green-500/20 text-green-400',
failed: 'bg-red-500/20 text-red-400',
};
return (
<div
onClick={onClick}
className="bg-slate-800/50 rounded-lg p-4 border border-slate-700 hover:border-slate-500 cursor-pointer transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-lg">{label.split(' ')[0]}</span>
<div>
<div className="font-medium text-white">{label.split(' ').slice(1).join(' ')}</div>
<div className="text-sm text-slate-400">
{event.actorId} {event.targetId}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs ${statusColors[event.status]}`}>
{event.status}
</span>
<span className="text-xs text-slate-500">
{new Date(event.createdAt).toLocaleString('uk-UA')}
</span>
</div>
</div>
</div>
);
};
// Event Detail Modal
const EventDetailModal: React.FC<{ event: GovernanceEvent; onClose: () => void }> = ({ event, onClose }) => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl border border-slate-600 max-w-2xl w-full max-h-[80vh] overflow-auto">
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<h3 className="font-bold text-white">Деталі події</h3>
<button onClick={onClose} className="text-slate-400 hover:text-white"></button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-slate-400">ID</div>
<div className="text-white font-mono text-sm">{event.id}</div>
</div>
<div>
<div className="text-sm text-slate-400">Тип</div>
<div className="text-white">{event.eventType}</div>
</div>
<div>
<div className="text-sm text-slate-400">Actor</div>
<div className="text-white">{event.actorId}</div>
</div>
<div>
<div className="text-sm text-slate-400">Target</div>
<div className="text-white">{event.targetId}</div>
</div>
<div>
<div className="text-sm text-slate-400">Scope</div>
<div className="text-white">{event.scope}</div>
</div>
<div>
<div className="text-sm text-slate-400">Статус</div>
<div className="text-white">{event.status}</div>
</div>
<div>
<div className="text-sm text-slate-400">Створено</div>
<div className="text-white">{new Date(event.createdAt).toLocaleString('uk-UA')}</div>
</div>
{event.publishedAt && (
<div>
<div className="text-sm text-slate-400">Опубліковано</div>
<div className="text-white">{new Date(event.publishedAt).toLocaleString('uk-UA')}</div>
</div>
)}
</div>
<div>
<div className="text-sm text-slate-400 mb-2">Payload</div>
<pre className="bg-slate-900 rounded-lg p-4 text-sm text-green-400 overflow-auto">
{JSON.stringify(event.payload, null, 2)}
</pre>
</div>
</div>
</div>
</div>
);
export default AuditDashboard;

View File

@@ -0,0 +1,246 @@
/**
* City Governance Panel
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Shows city-level governance: DAARWIZZ, DARIO, DARIA, districts, city incidents
*/
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { governanceApi } from '../../../api/governance';
import { incidentsApi } from '../../../api/incidents';
import { GovernanceLevelBadge } from './GovernanceLevelBadge';
import type { Incident } from '../../../types/governance';
interface CityGovernancePanelProps {
actorId?: string; // Current user's agent ID for actions
}
export const CityGovernancePanel: React.FC<CityGovernancePanelProps> = ({ actorId }) => {
const [activeTab, setActiveTab] = useState<'agents' | 'districts' | 'incidents'>('agents');
// Fetch city governance agents
const { data: cityAgents, isLoading: loadingAgents } = useQuery({
queryKey: ['governance', 'city-agents'],
queryFn: () => governanceApi.getCityGovernanceAgents(),
});
// Fetch district leads
const { data: districtLeads, isLoading: loadingDistricts } = useQuery({
queryKey: ['governance', 'district-leads'],
queryFn: () => governanceApi.getDistrictLeadAgents(),
});
// Fetch city-level incidents
const { data: incidentsData, isLoading: loadingIncidents } = useQuery({
queryKey: ['incidents', 'city'],
queryFn: () => incidentsApi.listIncidents({ escalationLevel: 'city', status: 'open' }),
});
// Fetch incidents count
const { data: incidentsCount } = useQuery({
queryKey: ['incidents', 'count'],
queryFn: () => incidentsApi.getIncidentsCount(),
});
return (
<div className="bg-slate-900 rounded-xl border border-slate-700 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-red-900/50 to-purple-900/50 px-6 py-4 border-b border-slate-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
🏛 City Governance
</h2>
<p className="text-sm text-slate-400 mt-1">
DAARION.city управління на рівні міста
</p>
</div>
{/* Incidents Counter */}
{incidentsCount && incidentsCount.city > 0 && (
<div className="bg-red-500/20 border border-red-500/50 rounded-lg px-4 py-2">
<div className="text-2xl font-bold text-red-400">{incidentsCount.city}</div>
<div className="text-xs text-red-300">відкритих інцидентів</div>
</div>
)}
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-700">
<button
onClick={() => setActiveTab('agents')}
className={`px-6 py-3 text-sm font-medium transition-colors ${
activeTab === 'agents'
? 'text-white bg-slate-800 border-b-2 border-purple-500'
: 'text-slate-400 hover:text-white'
}`}
>
👥 City Agents
</button>
<button
onClick={() => setActiveTab('districts')}
className={`px-6 py-3 text-sm font-medium transition-colors ${
activeTab === 'districts'
? 'text-white bg-slate-800 border-b-2 border-purple-500'
: 'text-slate-400 hover:text-white'
}`}
>
🏘 Districts ({districtLeads?.length || 0})
</button>
<button
onClick={() => setActiveTab('incidents')}
className={`px-6 py-3 text-sm font-medium transition-colors relative ${
activeTab === 'incidents'
? 'text-white bg-slate-800 border-b-2 border-purple-500'
: 'text-slate-400 hover:text-white'
}`}
>
Incidents
{incidentsCount && incidentsCount.city > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{incidentsCount.city}
</span>
)}
</button>
</div>
{/* Content */}
<div className="p-6">
{/* City Agents Tab */}
{activeTab === 'agents' && (
<div className="space-y-4">
{loadingAgents ? (
<div className="text-slate-400 text-center py-8">Завантаження...</div>
) : (
<div className="grid gap-4">
{cityAgents?.map((agent) => (
<div
key={agent.id}
className="bg-slate-800/50 rounded-lg p-4 border border-slate-700 flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-2xl">
{agent.id === 'daarwizz' && '🧙'}
{agent.id === 'dario' && '👋'}
{agent.id === 'daria' && '⚙️'}
</div>
<div>
<div className="font-medium text-white">{agent.name}</div>
<div className="text-sm text-slate-400">{agent.role}</div>
</div>
</div>
<GovernanceLevelBadge level="city_governance" />
</div>
))}
</div>
)}
</div>
)}
{/* Districts Tab */}
{activeTab === 'districts' && (
<div className="space-y-4">
{loadingDistricts ? (
<div className="text-slate-400 text-center py-8">Завантаження...</div>
) : districtLeads?.length === 0 ? (
<div className="text-slate-400 text-center py-8">Немає активних дистриктів</div>
) : (
<div className="grid gap-4">
{districtLeads?.map((district) => (
<div
key={district.districtId}
className="bg-slate-800/50 rounded-lg p-4 border border-slate-700"
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-white">{district.districtName}</div>
<div className="text-sm text-slate-400">
Lead: {district.agentName}
</div>
</div>
<GovernanceLevelBadge level="district_lead" />
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Incidents Tab */}
{activeTab === 'incidents' && (
<div className="space-y-4">
{loadingIncidents ? (
<div className="text-slate-400 text-center py-8">Завантаження...</div>
) : incidentsData?.incidents.length === 0 ? (
<div className="text-green-400 text-center py-8">
Немає відкритих інцидентів на рівні City
</div>
) : (
<div className="space-y-3">
{incidentsData?.incidents.map((incident: Incident) => (
<IncidentCard key={incident.id} incident={incident} />
))}
</div>
)}
</div>
)}
</div>
</div>
);
};
// Incident Card Component
const IncidentCard: React.FC<{ incident: Incident }> = ({ incident }) => {
const priorityColors = {
low: 'border-gray-500 bg-gray-500/10',
medium: 'border-yellow-500 bg-yellow-500/10',
high: 'border-orange-500 bg-orange-500/10',
critical: 'border-red-500 bg-red-500/10',
};
const priorityLabels = {
low: 'Низький',
medium: 'Середній',
high: 'Високий',
critical: 'Критичний',
};
return (
<div className={`rounded-lg p-4 border ${priorityColors[incident.priority]}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="font-medium text-white">{incident.title}</div>
{incident.description && (
<div className="text-sm text-slate-400 mt-1 line-clamp-2">
{incident.description}
</div>
)}
<div className="flex items-center gap-3 mt-2 text-xs text-slate-500">
<span>📍 {incident.targetScopeType}: {incident.targetScopeId}</span>
<span> {new Date(incident.createdAt).toLocaleDateString('uk-UA')}</span>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<span className={`
px-2 py-1 rounded text-xs font-medium
${incident.priority === 'critical' ? 'bg-red-500 text-white' : ''}
${incident.priority === 'high' ? 'bg-orange-500 text-white' : ''}
${incident.priority === 'medium' ? 'bg-yellow-500 text-black' : ''}
${incident.priority === 'low' ? 'bg-gray-500 text-white' : ''}
`}>
{priorityLabels[incident.priority]}
</span>
<span className="text-xs text-slate-400">
{incident.status === 'open' ? '🔴 Відкрито' : '🟡 В роботі'}
</span>
</div>
</div>
</div>
);
};
export default CityGovernancePanel;

View File

@@ -0,0 +1,72 @@
/**
* Governance Level Badge
* Displays agent governance level with color coding
*/
import React from 'react';
import type { AgentGovLevel, AgentStatus } from '../../../types/governance';
import { GOV_LEVEL_LABELS, GOV_LEVEL_COLORS, AGENT_STATUS_LABELS } from '../../../types/governance';
interface GovernanceLevelBadgeProps {
level: AgentGovLevel;
status?: AgentStatus;
showLabel?: boolean;
size?: 'sm' | 'md' | 'lg';
}
const sizeClasses = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-3 py-1',
lg: 'text-base px-4 py-1.5',
};
const colorClasses: Record<string, string> = {
gray: 'bg-gray-100 text-gray-700 border-gray-300',
blue: 'bg-blue-100 text-blue-700 border-blue-300',
green: 'bg-green-100 text-green-700 border-green-300',
yellow: 'bg-yellow-100 text-yellow-700 border-yellow-300',
orange: 'bg-orange-100 text-orange-700 border-orange-300',
purple: 'bg-purple-100 text-purple-700 border-purple-300',
pink: 'bg-pink-100 text-pink-700 border-pink-300',
red: 'bg-red-100 text-red-700 border-red-300',
};
export const GovernanceLevelBadge: React.FC<GovernanceLevelBadgeProps> = ({
level,
status,
showLabel = true,
size = 'md',
}) => {
const color = GOV_LEVEL_COLORS[level] || 'gray';
const label = GOV_LEVEL_LABELS[level] || level;
const isRevoked = status === 'revoked';
const isSuspended = status === 'suspended';
return (
<div className="flex items-center gap-2">
<span
className={`
inline-flex items-center rounded-full border font-medium
${sizeClasses[size]}
${isRevoked ? 'bg-red-100 text-red-700 border-red-300 line-through' : colorClasses[color]}
${isSuspended ? 'opacity-60' : ''}
`}
>
{showLabel && label}
</span>
{status && status !== 'active' && (
<span className={`
inline-flex items-center rounded-full text-xs px-2 py-0.5 font-medium
${isRevoked ? 'bg-red-500 text-white' : 'bg-yellow-500 text-white'}
`}>
{AGENT_STATUS_LABELS[status]}
</span>
)}
</div>
);
};
export default GovernanceLevelBadge;

View File

@@ -0,0 +1,519 @@
/**
* Incidents List
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Shows incidents with filters and actions
*/
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { incidentsApi } from '../../../api/incidents';
import type {
Incident,
IncidentStatus,
IncidentPriority,
EscalationLevel,
IncidentHistory,
} from '../../../types/governance';
import {
INCIDENT_STATUS_LABELS,
INCIDENT_PRIORITY_LABELS,
INCIDENT_PRIORITY_COLORS,
ESCALATION_LABELS,
} from '../../../types/governance';
interface IncidentsListProps {
defaultFilter?: {
status?: IncidentStatus;
escalationLevel?: EscalationLevel;
targetScopeId?: string;
};
actorDaisId?: string;
showCreateButton?: boolean;
}
export const IncidentsList: React.FC<IncidentsListProps> = ({
defaultFilter = {},
actorDaisId,
showCreateButton = false,
}) => {
const queryClient = useQueryClient();
const [filter, setFilter] = useState({
status: defaultFilter.status,
escalationLevel: defaultFilter.escalationLevel,
targetScopeId: defaultFilter.targetScopeId,
limit: 20,
offset: 0,
});
const [selectedIncident, setSelectedIncident] = useState<Incident | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
// Fetch incidents
const { data, isLoading } = useQuery({
queryKey: ['incidents', filter],
queryFn: () => incidentsApi.listIncidents(filter),
});
// Escalate mutation
const escalateMutation = useMutation({
mutationFn: (params: { incidentId: string; newLevel: EscalationLevel }) =>
incidentsApi.escalateIncident({
...params,
actorDaisId: actorDaisId!,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['incidents'] });
},
});
// Resolve mutation
const resolveMutation = useMutation({
mutationFn: (params: { incidentId: string; resolution: string }) =>
incidentsApi.resolveIncident({
...params,
actorDaisId: actorDaisId!,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['incidents'] });
setSelectedIncident(null);
},
});
return (
<div className="bg-slate-900 rounded-xl border border-slate-700 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-orange-900/50 to-red-900/50 px-6 py-4 border-b border-slate-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
Incidents
</h2>
<p className="text-sm text-slate-400 mt-1">
Управління інцидентами та ескалація
</p>
</div>
{showCreateButton && actorDaisId && (
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg text-sm font-medium"
>
+ Створити інцидент
</button>
)}
</div>
</div>
{/* Filters */}
<div className="p-4 border-b border-slate-700 bg-slate-800/50 flex flex-wrap gap-4">
<select
value={filter.status || ''}
onChange={(e) => setFilter(prev => ({ ...prev, status: e.target.value as IncidentStatus || undefined, offset: 0 }))}
className="bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
>
<option value="">Всі статуси</option>
{Object.entries(INCIDENT_STATUS_LABELS).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
<select
value={filter.escalationLevel || ''}
onChange={(e) => setFilter(prev => ({ ...prev, escalationLevel: e.target.value as EscalationLevel || undefined, offset: 0 }))}
className="bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
>
<option value="">Всі рівні</option>
{Object.entries(ESCALATION_LABELS).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
{/* List */}
<div className="p-6">
{isLoading ? (
<div className="text-slate-400 text-center py-8">Завантаження...</div>
) : data?.incidents.length === 0 ? (
<div className="text-green-400 text-center py-8">
Немає інцидентів за заданими фільтрами
</div>
) : (
<div className="space-y-4">
{data?.incidents.map((incident) => (
<IncidentCard
key={incident.id}
incident={incident}
onSelect={() => setSelectedIncident(incident)}
onEscalate={actorDaisId ? (level) => escalateMutation.mutate({ incidentId: incident.id, newLevel: level }) : undefined}
/>
))}
</div>
)}
{/* Pagination */}
{data && data.total > filter.limit && (
<div className="flex justify-center gap-2 mt-6">
<button
onClick={() => setFilter(prev => ({ ...prev, offset: Math.max(0, prev.offset - prev.limit) }))}
disabled={!filter.offset}
className="px-4 py-2 bg-slate-700 rounded-lg text-white disabled:opacity-50"
>
Попередні
</button>
<span className="px-4 py-2 text-slate-400">
{filter.offset + 1} - {Math.min(filter.offset + filter.limit, data.total)} з {data.total}
</span>
<button
onClick={() => setFilter(prev => ({ ...prev, offset: prev.offset + prev.limit }))}
disabled={filter.offset + filter.limit >= data.total}
className="px-4 py-2 bg-slate-700 rounded-lg text-white disabled:opacity-50"
>
Наступні
</button>
</div>
)}
</div>
{/* Detail Modal */}
{selectedIncident && (
<IncidentDetailModal
incident={selectedIncident}
actorDaisId={actorDaisId}
onClose={() => setSelectedIncident(null)}
onResolve={(resolution) => resolveMutation.mutate({ incidentId: selectedIncident.id, resolution })}
/>
)}
{/* Create Modal */}
{showCreateModal && actorDaisId && (
<CreateIncidentModal
actorDaisId={actorDaisId}
onClose={() => setShowCreateModal(false)}
onSuccess={() => {
setShowCreateModal(false);
queryClient.invalidateQueries({ queryKey: ['incidents'] });
}}
/>
)}
</div>
);
};
// Incident Card
const IncidentCard: React.FC<{
incident: Incident;
onSelect: () => void;
onEscalate?: (level: EscalationLevel) => void;
}> = ({ incident, onSelect, onEscalate }) => {
const priorityBg = {
low: 'border-gray-500/30 bg-gray-500/10',
medium: 'border-yellow-500/30 bg-yellow-500/10',
high: 'border-orange-500/30 bg-orange-500/10',
critical: 'border-red-500/30 bg-red-500/10 animate-pulse',
};
const nextLevel: Record<EscalationLevel, EscalationLevel | null> = {
microdao: 'district',
district: 'city',
city: null,
};
return (
<div className={`rounded-lg p-4 border ${priorityBg[incident.priority]} cursor-pointer hover:brightness-110 transition-all`}>
<div className="flex items-start justify-between">
<div className="flex-1" onClick={onSelect}>
<div className="flex items-center gap-2">
<span className={`
px-2 py-0.5 rounded text-xs font-medium
${incident.priority === 'critical' ? 'bg-red-500 text-white' : ''}
${incident.priority === 'high' ? 'bg-orange-500 text-white' : ''}
${incident.priority === 'medium' ? 'bg-yellow-500 text-black' : ''}
${incident.priority === 'low' ? 'bg-gray-500 text-white' : ''}
`}>
{INCIDENT_PRIORITY_LABELS[incident.priority]}
</span>
<span className={`
px-2 py-0.5 rounded text-xs font-medium
${incident.status === 'open' ? 'bg-red-900/50 text-red-300' : ''}
${incident.status === 'in_progress' ? 'bg-yellow-900/50 text-yellow-300' : ''}
${incident.status === 'resolved' ? 'bg-green-900/50 text-green-300' : ''}
${incident.status === 'closed' ? 'bg-gray-900/50 text-gray-300' : ''}
`}>
{INCIDENT_STATUS_LABELS[incident.status]}
</span>
<span className="px-2 py-0.5 rounded text-xs bg-purple-900/50 text-purple-300">
{ESCALATION_LABELS[incident.escalationLevel]}
</span>
</div>
<div className="font-medium text-white mt-2">{incident.title}</div>
{incident.description && (
<div className="text-sm text-slate-400 mt-1 line-clamp-2">
{incident.description}
</div>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
<span>📍 {incident.targetScopeType}: {incident.targetScopeId}</span>
<span> {new Date(incident.createdAt).toLocaleDateString('uk-UA')}</span>
{incident.assignedToDaisId && (
<span>👤 {incident.assignedToDaisId}</span>
)}
</div>
</div>
{/* Actions */}
{onEscalate && nextLevel[incident.escalationLevel] && incident.status !== 'resolved' && incident.status !== 'closed' && (
<button
onClick={(e) => {
e.stopPropagation();
onEscalate(nextLevel[incident.escalationLevel]!);
}}
className="px-3 py-1 bg-orange-600 hover:bg-orange-500 text-white rounded text-xs font-medium"
>
Ескалювати {ESCALATION_LABELS[nextLevel[incident.escalationLevel]!]}
</button>
)}
</div>
</div>
);
};
// Incident Detail Modal
const IncidentDetailModal: React.FC<{
incident: Incident;
actorDaisId?: string;
onClose: () => void;
onResolve: (resolution: string) => void;
}> = ({ incident, actorDaisId, onClose, onResolve }) => {
const [resolution, setResolution] = useState('');
// Fetch history
const { data: history } = useQuery({
queryKey: ['incident', incident.id, 'history'],
queryFn: () => incidentsApi.getIncidentHistory(incident.id),
});
const handleResolve = () => {
if (resolution.trim()) {
onResolve(resolution);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl border border-slate-600 max-w-2xl w-full max-h-[80vh] overflow-auto">
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<h3 className="font-bold text-white">Деталі інциденту</h3>
<button onClick={onClose} className="text-slate-400 hover:text-white"></button>
</div>
<div className="p-6 space-y-6">
{/* Info */}
<div>
<h4 className="font-medium text-white text-lg">{incident.title}</h4>
{incident.description && (
<p className="text-slate-400 mt-2">{incident.description}</p>
)}
</div>
{/* Status badges */}
<div className="flex flex-wrap gap-2">
<span className={`px-3 py-1 rounded-full text-sm ${
incident.priority === 'critical' ? 'bg-red-500 text-white' :
incident.priority === 'high' ? 'bg-orange-500 text-white' :
incident.priority === 'medium' ? 'bg-yellow-500 text-black' :
'bg-gray-500 text-white'
}`}>
{INCIDENT_PRIORITY_LABELS[incident.priority]}
</span>
<span className="px-3 py-1 rounded-full text-sm bg-purple-600 text-white">
{ESCALATION_LABELS[incident.escalationLevel]}
</span>
<span className={`px-3 py-1 rounded-full text-sm ${
incident.status === 'open' ? 'bg-red-600' :
incident.status === 'in_progress' ? 'bg-yellow-600' :
incident.status === 'resolved' ? 'bg-green-600' :
'bg-gray-600'
} text-white`}>
{INCIDENT_STATUS_LABELS[incident.status]}
</span>
</div>
{/* Meta */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-slate-400">Створено</div>
<div className="text-white">{new Date(incident.createdAt).toLocaleString('uk-UA')}</div>
</div>
<div>
<div className="text-slate-400">Ціль</div>
<div className="text-white">{incident.targetScopeType}: {incident.targetScopeId}</div>
</div>
{incident.assignedToDaisId && (
<div>
<div className="text-slate-400">Призначено</div>
<div className="text-white">{incident.assignedToDaisId}</div>
</div>
)}
{incident.resolution && (
<div className="col-span-2">
<div className="text-slate-400">Рішення</div>
<div className="text-green-400">{incident.resolution}</div>
</div>
)}
</div>
{/* History */}
{history && history.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-400 mb-3">Історія</h4>
<div className="space-y-2">
{history.map((h) => (
<div key={h.id} className="bg-slate-700/50 rounded p-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-white font-medium">{h.action}</span>
<span className="text-slate-500 text-xs">
{new Date(h.createdAt).toLocaleString('uk-UA')}
</span>
</div>
{h.comment && <div className="text-slate-400 mt-1">{h.comment}</div>}
</div>
))}
</div>
</div>
)}
{/* Resolve form */}
{actorDaisId && incident.status !== 'resolved' && incident.status !== 'closed' && (
<div className="border-t border-slate-700 pt-4">
<h4 className="text-sm font-medium text-slate-400 mb-2">Вирішити інцидент</h4>
<textarea
value={resolution}
onChange={(e) => setResolution(e.target.value)}
placeholder="Опишіть рішення..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm min-h-[80px]"
/>
<button
onClick={handleResolve}
disabled={!resolution.trim()}
className="mt-2 px-4 py-2 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white rounded-lg text-sm font-medium"
>
Вирішити
</button>
</div>
)}
</div>
</div>
</div>
);
};
// Create Incident Modal
const CreateIncidentModal: React.FC<{
actorDaisId: string;
onClose: () => void;
onSuccess: () => void;
}> = ({ actorDaisId, onClose, onSuccess }) => {
const [form, setForm] = useState({
title: '',
description: '',
priority: 'medium' as IncidentPriority,
targetScopeType: 'microdao' as const,
targetScopeId: '',
});
const createMutation = useMutation({
mutationFn: () => incidentsApi.createIncident({
...form,
createdByDaisId: actorDaisId,
}),
onSuccess,
});
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl border border-slate-600 max-w-lg w-full">
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<h3 className="font-bold text-white">Створити інцидент</h3>
<button onClick={onClose} className="text-slate-400 hover:text-white"></button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Заголовок *</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm(prev => ({ ...prev, title: e.target.value }))}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Опис</label>
<textarea
value={form.description}
onChange={(e) => setForm(prev => ({ ...prev, description: e.target.value }))}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white min-h-[80px]"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Пріоритет</label>
<select
value={form.priority}
onChange={(e) => setForm(prev => ({ ...prev, priority: e.target.value as IncidentPriority }))}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
>
{Object.entries(INCIDENT_PRIORITY_LABELS).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Тип цілі</label>
<select
value={form.targetScopeType}
onChange={(e) => setForm(prev => ({ ...prev, targetScopeType: e.target.value as any }))}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
>
<option value="microdao">MicroDAO</option>
<option value="district">District</option>
<option value="agent">Agent</option>
<option value="room">Room</option>
<option value="node">Node</option>
</select>
</div>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">ID цілі *</label>
<input
type="text"
value={form.targetScopeId}
onChange={(e) => setForm(prev => ({ ...prev, targetScopeId: e.target.value }))}
placeholder="Введіть ID..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
/>
</div>
<button
onClick={() => createMutation.mutate()}
disabled={!form.title || !form.targetScopeId || createMutation.isPending}
className="w-full px-4 py-2 bg-red-600 hover:bg-red-500 disabled:opacity-50 text-white rounded-lg font-medium"
>
{createMutation.isPending ? 'Створення...' : 'Створити інцидент'}
</button>
</div>
</div>
</div>
);
};
export default IncidentsList;

295
src/types/governance.ts Normal file
View File

@@ -0,0 +1,295 @@
/**
* Governance Types
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*/
// ============================================================================
// GOVERNANCE LEVELS
// ============================================================================
export const AGENT_LEVELS = {
GUEST: 0,
PERSONAL: 1,
MEMBER: 2,
WORKER: 3,
CORE_TEAM: 4,
ORCHESTRATOR: 5,
DISTRICT_LEAD: 6,
CITY_GOVERNANCE: 7,
} as const;
export type AgentLevelNum = typeof AGENT_LEVELS[keyof typeof AGENT_LEVELS];
export type AgentGovLevel =
| 'guest'
| 'personal'
| 'member'
| 'worker'
| 'core_team'
| 'orchestrator'
| 'district_lead'
| 'city_governance';
export const GOV_LEVEL_LABELS: Record<AgentGovLevel, string> = {
guest: 'Guest',
personal: 'Personal',
member: 'Member',
worker: 'Worker',
core_team: 'Core Team',
orchestrator: 'Orchestrator',
district_lead: 'District Lead',
city_governance: 'City Governance',
};
export const GOV_LEVEL_COLORS: Record<AgentGovLevel, string> = {
guest: 'gray',
personal: 'blue',
member: 'green',
worker: 'yellow',
core_team: 'orange',
orchestrator: 'purple',
district_lead: 'pink',
city_governance: 'red',
};
// ============================================================================
// AGENT STATUS
// ============================================================================
export type AgentStatus = 'active' | 'suspended' | 'revoked';
export const AGENT_STATUS_LABELS: Record<AgentStatus, string> = {
active: 'Активний',
suspended: 'Призупинено',
revoked: 'Заблоковано',
};
// ============================================================================
// GOVERNANCE POWERS
// ============================================================================
export type GovernancePower =
| 'administrative'
| 'moderation'
| 'execution'
| 'infrastructure'
| 'identity'
| 'protocol'
| 'district';
export const POWER_LABELS: Record<GovernancePower, string> = {
administrative: 'Адміністрування',
moderation: 'Модерація',
execution: 'Виконання',
infrastructure: 'Інфраструктура',
identity: 'Ідентичність',
protocol: 'Протокол',
district: 'Район',
};
// ============================================================================
// GOVERNANCE SCOPE
// ============================================================================
export type GovernanceScope = 'city' | `district:${string}` | `microdao:${string}` | `node:${string}`;
// ============================================================================
// REVOCATION
// ============================================================================
export type RevocationType = 'soft' | 'hard' | 'shadow';
export const REVOCATION_LABELS: Record<RevocationType, string> = {
soft: 'Тимчасове',
hard: 'Постійне',
shadow: 'Тіньове',
};
export interface AgentRevocation {
id: string;
agentId: string;
daisId?: string;
revokedBy: string;
revocationType: RevocationType;
reason: string;
scope: GovernanceScope;
keysInvalidated: boolean;
walletDisabled: boolean;
roomAccessRevoked: boolean;
nodePrivilegesRemoved: boolean;
assignmentsTerminated: boolean;
reversible: boolean;
reversedAt?: Date;
reversedBy?: string;
createdAt: Date;
}
// ============================================================================
// INCIDENTS
// ============================================================================
export type IncidentStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
export type IncidentPriority = 'low' | 'medium' | 'high' | 'critical';
export type EscalationLevel = 'microdao' | 'district' | 'city';
export type TargetScopeType = 'city' | 'district' | 'microdao' | 'room' | 'node' | 'agent';
export const INCIDENT_STATUS_LABELS: Record<IncidentStatus, string> = {
open: 'Відкрито',
in_progress: 'В роботі',
resolved: 'Вирішено',
closed: 'Закрито',
};
export const INCIDENT_PRIORITY_LABELS: Record<IncidentPriority, string> = {
low: 'Низький',
medium: 'Середній',
high: 'Високий',
critical: 'Критичний',
};
export const INCIDENT_PRIORITY_COLORS: Record<IncidentPriority, string> = {
low: 'gray',
medium: 'yellow',
high: 'orange',
critical: 'red',
};
export const ESCALATION_LABELS: Record<EscalationLevel, string> = {
microdao: 'MicroDAO',
district: 'District',
city: 'City',
};
export interface Incident {
id: string;
createdByDaisId: string;
targetScopeType: TargetScopeType;
targetScopeId: string;
status: IncidentStatus;
priority: IncidentPriority;
assignedToDaisId?: string;
escalationLevel: EscalationLevel;
title: string;
description?: string;
resolution?: string;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
resolvedAt?: Date;
closedAt?: Date;
}
export interface IncidentHistory {
id: string;
incidentId: string;
action: 'created' | 'assigned' | 'escalated' | 'resolved' | 'closed' | 'comment';
actorDaisId: string;
oldValue?: Record<string, unknown>;
newValue?: Record<string, unknown>;
comment?: string;
createdAt: Date;
}
// ============================================================================
// AUDIT EVENTS
// ============================================================================
export type GovernanceEventType =
| 'agent.promoted'
| 'agent.demoted'
| 'agent.revoked'
| 'agent.reinstated'
| 'agent.assigned'
| 'agent.unassigned'
| 'permission.granted'
| 'permission.revoked'
| 'incident.created'
| 'incident.assigned'
| 'incident.escalated'
| 'incident.resolved'
| 'incident.closed'
| 'microdao.created'
| 'district.created'
| 'node.registered'
| 'room.created'
| 'room.published_to_city';
export interface GovernanceEvent {
id: string;
eventType: GovernanceEventType;
subject: string;
actorId: string;
targetId: string;
scope: GovernanceScope;
payload: Record<string, unknown>;
version: string;
status: 'pending' | 'published' | 'failed';
createdAt: Date;
publishedAt?: Date;
}
// ============================================================================
// API REQUESTS/RESPONSES
// ============================================================================
export interface AgentRolesResponse {
level: AgentGovLevel;
status: AgentStatus;
powers: GovernancePower[];
assignments: Array<{
microdaoId: string;
role: string;
scope: string;
}>;
}
export interface PromoteAgentRequest {
actorId: string;
targetId: string;
newLevel: AgentGovLevel;
scope: GovernanceScope;
reason?: string;
}
export interface RevokeAgentRequest {
actorId: string;
targetId: string;
reason: string;
scope: GovernanceScope;
revocationType?: RevocationType;
}
export interface CreateIncidentRequest {
createdByDaisId: string;
targetScopeType: TargetScopeType;
targetScopeId: string;
priority?: IncidentPriority;
title: string;
description?: string;
}
export interface AuditEventFilter {
eventType?: GovernanceEventType;
actorId?: string;
targetId?: string;
scope?: GovernanceScope;
createdAtFrom?: string;
createdAtTo?: string;
limit?: number;
offset?: number;
}
export interface AuditStats {
totalEvents: number;
eventsByType: Record<string, number>;
eventsByDay: Array<{ date: string; count: number }>;
topActors: Array<{ actorId: string; count: number }>;
}
export interface IncidentsCount {
microdao: number;
district: number;
city: number;
total: number;
}