diff --git a/apps/web/src/app/nodes/[nodeId]/page.tsx b/apps/web/src/app/nodes/[nodeId]/page.tsx index c2f9ae90..0c9484fc 100644 --- a/apps/web/src/app/nodes/[nodeId]/page.tsx +++ b/apps/web/src/app/nodes/[nodeId]/page.tsx @@ -14,7 +14,7 @@ import { ModulesCard, NodeStandardComplianceCard } from '@/components/node-dashboard'; -import { NodeAgentsPanel } from '@/components/nodes/NodeAgentsPanel'; +import { NodeGuardianCard } from '@/components/nodes/NodeGuardianCard'; function getNodeLabel(nodeId: string): string { if (nodeId.includes('node-1')) return 'НОДА1'; @@ -124,7 +124,7 @@ export default function NodeCabinetPage() {
{/* Node Guardian & Steward Agents */} - @@ -279,7 +279,7 @@ export default function NodeCabinetPage() { {/* Node Guardian & Steward Agents */}
- diff --git a/apps/web/src/components/nodes/NodeGuardianCard.tsx b/apps/web/src/components/nodes/NodeGuardianCard.tsx new file mode 100644 index 00000000..da60ae0f --- /dev/null +++ b/apps/web/src/components/nodes/NodeGuardianCard.tsx @@ -0,0 +1,115 @@ +"use client"; + +import Link from "next/link"; +import { Shield, MessageSquare, ExternalLink } from "lucide-react"; + +// Define locally or import from types/nodes if compatible +// We use a compatible shape that fits both NodeProfile agents and generic summaries +export type NodeAgentSummary = { + id: string; + name: string; + kind?: string | null; + slug?: string | null; // public slug +}; + +type Props = { + guardian?: NodeAgentSummary | null; + steward?: NodeAgentSummary | null; +}; + +export function NodeGuardianCard({ guardian, steward }: Props) { + if (!guardian && !steward) return null; + + return ( +
+
+
+

+ + Node Guardian & Steward +

+

+ Агенти, які відповідають за цю ноду: техніка + взаємодія. +

+
+
+ +
+ {guardian && ( + } + /> + )} + {steward && ( + } + /> + )} +
+
+ ); +} + +type AgentMiniCardProps = { + title: string; + description: string; + agent: NodeAgentSummary; + accentClass?: string; + icon?: React.ReactNode; +}; + +function AgentMiniCard({ title, description, agent, accentClass, icon }: AgentMiniCardProps) { + return ( +
+
+
+ {icon} +
+
+ {title} +
+
+ {agent.name} +
+
+
+ {agent.kind && ( +
+ {agent.kind} +
+ )} +
+ +

{description}

+ +
+ {agent.slug && ( + + Публічний профіль + + )} + + Кабінет + + +
+
+ ); +} + diff --git a/db/sql/038_node_guardian_seed.sql b/db/sql/038_node_guardian_seed.sql new file mode 100644 index 00000000..cf37f858 --- /dev/null +++ b/db/sql/038_node_guardian_seed.sql @@ -0,0 +1,23 @@ +-- Оновлення типів агентів для Node Guardian та Node Steward + +-- NODE 1 +UPDATE agents +SET kind = 'node_guardian' +WHERE id = 'monitor-node1'; + +UPDATE agents +SET kind = 'node_steward' +WHERE id = 'node-steward-node1'; + +-- NODE 2 +UPDATE agents +SET kind = 'node_guardian' +WHERE id = 'monitor-node2'; + +UPDATE agents +SET kind = 'node_steward' +WHERE id = 'node-steward-node2'; + +-- Додати теги (опціонально, якщо колонка tags існує і це масив текстів) +-- UPDATE agents SET tags = array_append(tags, 'role:guardian') WHERE id = 'monitor-node1' AND NOT ('role:guardian' = ANY(tags)); + diff --git a/docs/users/nodes/NODE_GUARDIAN_AND_STEWARD.md b/docs/users/nodes/NODE_GUARDIAN_AND_STEWARD.md new file mode 100644 index 00000000..779d3b7a --- /dev/null +++ b/docs/users/nodes/NODE_GUARDIAN_AND_STEWARD.md @@ -0,0 +1,70 @@ +# Node Guardian & Node Steward + +**Дата:** 29 листопада 2025 +**Статус:** Впроваджено (Task 038) + +У екосистемі DAARION кожна нода має два ключові "обличчя" — спеціалізованих агентів, які відповідають за її функціонування та представлення у мережі. + +--- + +## 1. Ролі + +### 🛡️ Node Guardian (Технічний наглядач) +**Відповідальність:** +* Моніторинг інфраструктури (CPU, RAM, Disk, GPU). +* Стан сервісів (Docker, Systemd). +* Безпека та алерти. +* Автоматична реакція на інциденти (в межах дозволеного). + +**Тип агента:** `node_guardian` (або `infra_monitor`). + +### 🗣️ Node Steward (Хазяїн ноди) +**Відповідальність:** +* Публічне представлення ноди як "громадянина". +* Комунікація з іншими учасниками мережі. +* Управління конфігурацією та правилами (Governance). +* "Human Interface" до ноди. + +**Тип агента:** `node_steward` (або `infra_ops`). + +--- + +## 2. Як призначити + +Агенти автоматично визначаються системою (`city-service`) за наступним алгоритмом: + +1. **Пошук за типом (`kind`):** + * Система шукає агентів, прив'язаних до цієї ноди (`node_id`), які мають `kind = 'node_guardian'` або `'node_steward'`. + +2. **Fallback (сумісність):** + * Якщо спеціалізованих типів не знайдено, система шукає `infra_monitor` (як Guardian) та `infra_ops` (як Steward). + +3. **Node Cache (Legacy):** + * Якщо динамічний пошук не дав результатів, використовується закешоване значення з таблиці `node_registry.nodes` (якщо воно було встановлено вручну). + +### SQL для призначення (приклад) + +```sql +-- Призначити Guardian +UPDATE agents +SET kind = 'node_guardian' +WHERE id = 'my-monitor-agent-id'; + +-- Призначити Steward +UPDATE agents +SET kind = 'node_steward' +WHERE id = 'my-steward-agent-id'; +``` + +--- + +## 3. Відображення в UI + +### Node Dashboard (`/nodes/[nodeId]`) +У кабінеті ноди (як повному, так і базовому профілі) відображається картка **"Node Guardian & Steward"**: +* Показує імена та ролі агентів. +* Посилання на їх **Публічний профіль** (`/citizens/[slug]`). +* Посилання на **Кабінет агента** (`/agents/[id]`). + +Це дозволяє оператору швидко перейти до налаштувань агента або почати діалог з ним. + diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py index 243ede62..dc6c45e6 100644 --- a/services/city-service/repo_city.py +++ b/services/city-service/repo_city.py @@ -1665,6 +1665,40 @@ async def get_node_by_id(node_id: str) -> Optional[dict]: else: data["steward_agent"] = None + # TASK 038: Dynamic discovery of Node Guardian / Steward if cache is empty + if not data["guardian_agent"] or not data["steward_agent"]: + dynamic_agents = await pool.fetch(""" + SELECT id, display_name, kind, public_slug + FROM agents + WHERE node_id = $1 + AND (kind IN ('node_guardian', 'node_steward') OR kind IN ('infra_monitor', 'infra_ops')) + AND COALESCE(is_archived, false) = false + """, node_id) + + if not data["guardian_agent"]: + # Prefer 'node_guardian', fallback to 'infra_monitor' + guardian = next((a for a in dynamic_agents if a['kind'] == 'node_guardian'), + next((a for a in dynamic_agents if a['kind'] == 'infra_monitor'), None)) + if guardian: + data["guardian_agent"] = { + "id": guardian["id"], + "name": guardian["display_name"], + "kind": guardian["kind"], + "slug": guardian["public_slug"] + } + + if not data["steward_agent"]: + # Prefer 'node_steward', fallback to 'infra_ops' + steward = next((a for a in dynamic_agents if a['kind'] == 'node_steward'), + next((a for a in dynamic_agents if a['kind'] == 'infra_ops'), None)) + if steward: + data["steward_agent"] = { + "id": steward["id"], + "name": steward["display_name"], + "kind": steward["kind"], + "slug": steward["public_slug"] + } + # Clean up intermediate fields for key in ["guardian_name", "guardian_kind", "guardian_slug", "steward_name", "steward_kind", "steward_slug"]: