feat: TASK 038 - Node Guardian & Steward Integration
- Backend: Added dynamic discovery of node agents in repo_city.py - Backend: Created seed SQL for agent types - Frontend: Added NodeGuardianCard component - Frontend: Integrated NodeGuardianCard into Node Dashboard / Profile
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
ModulesCard,
|
ModulesCard,
|
||||||
NodeStandardComplianceCard
|
NodeStandardComplianceCard
|
||||||
} from '@/components/node-dashboard';
|
} from '@/components/node-dashboard';
|
||||||
import { NodeAgentsPanel } from '@/components/nodes/NodeAgentsPanel';
|
import { NodeGuardianCard } from '@/components/nodes/NodeGuardianCard';
|
||||||
|
|
||||||
function getNodeLabel(nodeId: string): string {
|
function getNodeLabel(nodeId: string): string {
|
||||||
if (nodeId.includes('node-1')) return 'НОДА1';
|
if (nodeId.includes('node-1')) return 'НОДА1';
|
||||||
@@ -124,7 +124,7 @@ export default function NodeCabinetPage() {
|
|||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Node Guardian & Steward Agents */}
|
{/* Node Guardian & Steward Agents */}
|
||||||
<NodeAgentsPanel
|
<NodeGuardianCard
|
||||||
guardian={nodeProfile?.guardian_agent}
|
guardian={nodeProfile?.guardian_agent}
|
||||||
steward={nodeProfile?.steward_agent}
|
steward={nodeProfile?.steward_agent}
|
||||||
/>
|
/>
|
||||||
@@ -279,7 +279,7 @@ export default function NodeCabinetPage() {
|
|||||||
|
|
||||||
{/* Node Guardian & Steward Agents */}
|
{/* Node Guardian & Steward Agents */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<NodeAgentsPanel
|
<NodeGuardianCard
|
||||||
guardian={nodeProfile?.guardian_agent}
|
guardian={nodeProfile?.guardian_agent}
|
||||||
steward={nodeProfile?.steward_agent}
|
steward={nodeProfile?.steward_agent}
|
||||||
/>
|
/>
|
||||||
|
|||||||
115
apps/web/src/components/nodes/NodeGuardianCard.tsx
Normal file
115
apps/web/src/components/nodes/NodeGuardianCard.tsx
Normal file
@@ -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 (
|
||||||
|
<section className="bg-white/5 border border-emerald-400/30 rounded-2xl p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4 text-emerald-400" />
|
||||||
|
Node Guardian & Steward
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-white/60">
|
||||||
|
Агенти, які відповідають за цю ноду: техніка + взаємодія.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
{guardian && (
|
||||||
|
<AgentMiniCard
|
||||||
|
title="Node Guardian"
|
||||||
|
description="Слідкує за інфраструктурою, метриками та безпекою ноди."
|
||||||
|
agent={guardian}
|
||||||
|
accentClass="border-emerald-400/40 bg-emerald-500/5"
|
||||||
|
icon={<Shield className="w-4 h-4 text-emerald-400" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{steward && (
|
||||||
|
<AgentMiniCard
|
||||||
|
title="Node Steward"
|
||||||
|
description="Представляє ноду як громадянина міста, відповідає за комунікацію."
|
||||||
|
agent={steward}
|
||||||
|
accentClass="border-cyan-400/40 bg-cyan-500/5"
|
||||||
|
icon={<MessageSquare className="w-4 h-4 text-cyan-400" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentMiniCardProps = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
agent: NodeAgentSummary;
|
||||||
|
accentClass?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AgentMiniCard({ title, description, agent, accentClass, icon }: AgentMiniCardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`rounded-xl border border-white/10 ${accentClass ?? ""} p-3 space-y-2`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] uppercase tracking-wide text-white/50 font-semibold">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-white">
|
||||||
|
{agent.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{agent.kind && (
|
||||||
|
<div className="text-[10px] text-white/30 px-2 py-0.5 rounded bg-black/20 border border-white/5">
|
||||||
|
{agent.kind}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-white/60 leading-relaxed">{description}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs pt-1">
|
||||||
|
{agent.slug && (
|
||||||
|
<Link
|
||||||
|
href={`/citizens/${agent.slug}`}
|
||||||
|
className="px-2 py-1 rounded-lg border border-white/10 bg-white/5 text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Публічний профіль
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
href={`/agents/${agent.id}`}
|
||||||
|
className="px-2 py-1 rounded-lg border border-white/10 bg-white/5 text-white/70 hover:bg-white/10 hover:text-white transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Кабінет
|
||||||
|
<ExternalLink className="w-3 h-3 opacity-50" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
23
db/sql/038_node_guardian_seed.sql
Normal file
23
db/sql/038_node_guardian_seed.sql
Normal file
@@ -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));
|
||||||
|
|
||||||
70
docs/users/nodes/NODE_GUARDIAN_AND_STEWARD.md
Normal file
70
docs/users/nodes/NODE_GUARDIAN_AND_STEWARD.md
Normal file
@@ -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]`).
|
||||||
|
|
||||||
|
Це дозволяє оператору швидко перейти до налаштувань агента або почати діалог з ним.
|
||||||
|
|
||||||
@@ -1665,6 +1665,40 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
|||||||
else:
|
else:
|
||||||
data["steward_agent"] = None
|
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
|
# Clean up intermediate fields
|
||||||
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
||||||
"steward_name", "steward_kind", "steward_slug"]:
|
"steward_name", "steward_kind", "steward_slug"]:
|
||||||
|
|||||||
Reference in New Issue
Block a user