# NODA1 Full Audit — DAARION.city **Дата:** 2026-02-27 **Сервер:** node1-daarion | 144.76.224.179 | NVIDIA RTX 4000 SFF Ada (20GB VRAM) **Аудитор:** Sofiia — Chief AI Architect --- ## EXECUTIVE SUMMARY | Напрям | Стан | Критичність | |--------|------|-------------| | Фото E2E (Telegram→Vision) | ✅ Працює, але є shortcut (не через NATS) | MEDIUM | | PDF/Документи | ⚠️ render-pdf-worker idle, index-doc DNS fail | HIGH | | Router/Profiles | ✅ OK — DeepSeek top-level, 27B crew, smollm2 CPU | LOW | | STT/TTS | ✅ CPU-only (Whisper), TTS unloaded | LOW | | Swapper | ⚠️ Потрібен — єдина точка Vision/STT/OCR/Document | KEEP | | GPU policy | ✅ 27B GPU, smollm2 CPU, policy_ok=1 | OK | | NODA1↔NODA2 | ⚠️ K3s cluster (flannel), NATS не з'єднані між нодами | HIGH | | CTO Sofiia control plane | ⚠️ control-plane сервіс є, але тільки prompts+policy JWT | MEDIUM | --- ## 1. INVENTORY — Що реально запущено ### Контейнери (48 total, ключові): ``` swapper-service-node1 healthy 8890-8891 dagi-router-node1 healthy 9102→8000 dagi-nats-node1 up 4222 dagi-memory-service-node1 healthy 8000 dagi-qdrant-node1 healthy 6333 dagi-gateway-node1 healthy 9300 parser-pipeline up 8101 ingest-service up 8100 render-pdf-worker-node1 up (no port) render-pptx-worker-node1 up (no port) index-doc-worker-node1 up (no port) presentation-renderer-node1 healthy 9212 rag-service-node1 healthy 9500 dagi-vision-encoder-node1 healthy 8001 control-plane up 9200 dagi-crawl4ai-node1 healthy 11235 oneok-gotenberg-node1 up 3010 plant-vision-node1 healthy 8085 crewai-nats-worker up 9011 dagi-staging-crewai-service up 9010 artifact-registry-node1 healthy 9220 dagi-minio-node1 up 9000-9001 ``` ### Systemd: - `ollama.service` — **active** (GPU, port 11434, qwen3.5:27b-q4_K_M, KEEP_ALIVE=10m) - `ollama-cpu.service` — **active** (CPU, port 11435, smollm2:135m) - `gpu-ollama-exporter.service` — **active** (port 9400) - `ollama-warmup-27b.timer` — **active** (кожні 15хв) --- ## 2. ROUTER — Профілі, моделі, routing ### CURRENT STATE **Env у контейнері dagi-router-node1:** ``` ENABLE_CREW_MODEL_ROUTING=1 CREW_SMALL_MODEL=smollm2:135m CREWAI_WORKER_LLM_PROFILE=crew_local_27b DEEPSEEK_API_KEY=sk-0db94... (production key) NATS_URL=nats://nats:4222 VISION_ENCODER_URL=http://vision-encoder:8001 ``` **Профілі (router-config.yml):** | Profile | Provider | Model | URL | |---------|----------|-------|-----| | `cloud_deepseek` | deepseek | deepseek-chat | api.deepseek.com | | `cloud_mistral` | mistral | mistral-large-latest | api.mistral.ai | | `crew_local_27b` | ollama | qwen3.5:27b-q4_K_M | 172.17.0.1:11434 (GPU) | | `crew_vision_27b` | ollama | qwen3.5:27b-q4_K_M | 172.17.0.1:11434 (GPU) | | `crew_local_small` | ollama | smollm2:135m | host.docker.internal:11435 (CPU) | | `service_local_cpu` | ollama | smollm2:135m | host.docker.internal:11435 (CPU) | | `vision_encoder` | — | — | vision-encoder:8001 (ViT-L-14) | | `crewai` | — | — | localhost:9010 | **Агенти з vision моделлю:** greenfood, druid, eonarch, helion → `qwen3-vl:8b` (через swapper) **Метрики:** `llm_heavy_share_ratio=0.0` — важкі запити ще не логовані (лічильники нульові, нові після restart). ### GAPS - `local_qwen3_8b`, `qwen3_strategist_8b`, ... — **всі вказують на 27B замість 8B** (рядки в config не оновлені після зміни). Назви оманливі. - `crew_local_27b` використовує `172.17.0.1:11434` — не `host.docker.internal`. Inconsistency: CPU профілі через host.docker.internal, GPU — через IP. ### RECOMMENDED PATCHES **Patch 1:** Уніфікувати GPU профілі на `host.docker.internal:11434`: ```yaml # services/router/router-config.yml crew_local_27b: base_url: http://host.docker.internal:11434 # було 172.17.0.1 crew_vision_27b: base_url: http://host.docker.internal:11434 ``` **Patch 2:** Перейменувати оманливі профілі (або залишити as-is якщо вони deprecated): ```yaml # local_qwen3_8b → local_qwen3_27b (або видалити невикористані) ``` --- ## 3. ФОТО E2E — Telegram → Vision → Агент ### CURRENT STATE (Два шляхи!) #### Шлях A: Прямий (основний для більшості агентів) ``` Telegram photo → Gateway (http_api.py:~2085) ↓ download photo via Telegram Bot API → file_url (https://api.telegram.org/file/...) ↓ send_to_router({file_url, images: [file_url], prompt}) ↓ Router (main.py:~2445) → SWAPPER_URL/vision payload: {model: "qwen3-vl-8b", prompt, images: [file_url]} ↓ Swapper /vision → завантажує qwen3-vl:8b (ollama pull) → відповідь ↓ Router повертає text → Gateway → Telegram ``` #### Шлях B: Через NATS ATTACHMENTS (для parser-pipeline) ``` Telegram photo → Gateway ↓ (окремий worker?) → NATS ATTACHMENTS stream ↓ parser-pipeline consumer process_image() → SWAPPER_URL/vision (base64 encode) ↓ result → ??? (не ясно куди іде результат) ``` **КРИТИЧНО:** `parser-pipeline` логи показують **тисячі** `ServiceUnavailableError` між рестартами — NATS stream `ATTACHMENTS` зникає після рестарту `dagi-nats-node1` (нема persistence). Після рестарту parser підключається знову (`Consumer created: parser-pipeline`). ### Vision model flow (Swapper): - Gateway надсилає `file_url` (не base64 завантаження) - Router передає `images: [file_url]` у Swapper - Swapper `/vision` → `qwen3-vl:8b` через Ollama (6.1GB, lazy load) - **qwen3-vl:8b зараз `unloaded`** — cold-start ~30-60s при першому виклику ### GAPS 1. **NATS stream ATTACHMENTS не персистентний** — після `docker restart dagi-nats-node1` stream зникає. Parser спамить `ServiceUnavailableError` поки не перезапустити. 2. **parser-pipeline `SWAPPER_URL=http://swapper-service:8890`** — але контейнер називається `swapper-service-node1`. DNS може не резолвитись. 3. **ingest-service** також має `SWAPPER_URL=http://swapper-service-node1:8890` → `socket.gaierror: Temporary failure in name resolution` — сервіс намагається резолвити щось не те. 4. **Шлях B результат незрозумілий** — куди parser-pipeline відправляє результат обробки зображення після Vision? 5. **qwen3-vl:8b cold-start** — перший запит до vision займе 30-60s (lazy load). ### RECOMMENDED PATCHES **Patch 3:** Виправити `SWAPPER_URL` в parser-pipeline compose: ```yaml # docker-compose.node1.yml, parser-pipeline service environment: - SWAPPER_URL=http://swapper-service-node1:8890 # було: http://swapper-service:8890 ``` **Patch 4:** NATS stream ATTACHMENTS — зробити файловий storage з retention: ```yaml # nats-js-init service (вже є в compose) — перевірити що він запускається після рестарту NATS ``` --- ## 4. PDF/ДОКУМЕНТИ — Обробка ### CURRENT STATE **Сервіси обробки документів:** | Сервіс | Статус | Роль | |--------|--------|------| | `render-pdf-worker-node1` | ✅ up, **idle** | PDF → PNG/зображення (NATS: artifact.job.render_pdf.requested) | | `render-pptx-worker-node1` | ⚠️ DNS fail (`nats`) | PPTX → PNG (NATS: нема з'єднання) | | `index-doc-worker-node1` | ⚠️ DNS fail (RAG service?) | RAG indexing (NATS: artifact.job.*) | | `presentation-renderer-node1` | ✅ healthy (9212) | API сервіс рендерингу | | `oneok-gotenberg-node1` | ✅ up (3010) | HTML/PDF generation (Gotenberg) | | `rag-service-node1` | ✅ healthy (9500) | RAG retrieval | | `artifact-registry-node1` | ✅ healthy (9220) | Артефакт реєстр | | `dagi-minio-node1` | ✅ up (9000-9001) | S3 storage | | `parser-pipeline` | ✅ up (8101) | NATS consumer → Swapper doc+image | **Docling:** НЕ ВСТАНОВЛЕНИЙ як окремий контейнер. Є як модель у Swapper (`granite-docling`, тип `document`, 2.5GB, `unloaded`). **Шлях обробки документа (PDF):** ``` Telegram doc → Gateway → ? → або send_to_router з doc_url → або через NATS → parser-pipeline → Swapper /document Swapper /document → granite-docling (lazy load, 2.5GB) → текст Паралельно: → artifact.job.render_pdf.requested → render-pdf-worker → PNG → artifact-registry → MinIO → artifact.job.index_doc.requested → index-doc-worker → rag-service (RAG indexing) ``` ### GAPS 1. **render-pptx-worker** не може резолвити `nats` DNS — на іншій docker network або compose group. 2. **index-doc-worker** DNS fail (щось не резолвить) — перевірити network config. 3. **granite-docling** у swapper `unloaded` — завантажується lazily, займе час при першому запиті документа. GPU увімкнений для docling? (GPU_ENABLED=false зараз!) 4. **Немає Docling окремим сервісом** — вся обробка документів через Swapper, який зараз CPU-only через наші зміни. ### GAPS — КРИТИЧНО > **Swapper GPU_ENABLED=false** — означає, що granite-docling, got-ocr2, qwen3-vl-8b і whisper будуть завантажуватись в CPU/RAM. При 20GB VRAM це субоптимально для Vision і OCR моделей. ### RECOMMENDED PATCHES **Patch 5:** Виправити network для render-pptx-worker та index-doc-worker: ```yaml # docker-compose.node1.yml — додати network dagi-network до цих сервісів render-pptx-worker: networks: - dagi-network # щоб резолвити 'nats' index-doc-worker: networks: - dagi-network ``` --- ## 5. STT/TTS/SWAPPER — Детальний аналіз ### CURRENT STATE **Swapper /health:** `{"status":"healthy","active_model":"qwen3-8b","mode":"single-active"}` **Swapper конфіг (фактичний):** - `mode: multi-active` в yaml, але ENV `MAX_CONCURRENT_MODELS=1` → single-active режим - `GPU_ENABLED=false` (наша зміна) — але config.yaml каже `gpu_enabled: true` - `WHISPER_DEVICE=cpu, WHISPER_COMPUTE_TYPE=int8` **Моделі в Swapper:** | Модель | Тип | Розмір | Статус | |--------|-----|--------|--------| | qwen3-8b | llm | 5.2GB | **loaded** (Ollama) | | qwen3-vl-8b | vision | 6.1GB | unloaded | | got-ocr2 | ocr | 7.0GB | unloaded | | donut-base | ocr | 3.0GB | unloaded | | donut-cord | ocr | 3.0GB | unloaded | | granite-docling | document | 2.5GB | unloaded | | faster-whisper-large | stt | 3.0GB | unloaded | | whisper-small | stt | 0.5GB | unloaded | | xtts-v2 | tts | 2.0GB | unloaded | | flux-klein-4b | image_gen | 15.4GB | unloaded | **STT:** - STT startup: `[STT-POLICY] WHISPER_DEVICE env='cpu' | actual_device='cpu'` ✅ - Swapper `/stt` ← parser-pipeline (audio processing) - Swapper `/stt` ← router (STT_URL) - Swapper `/stt` ← gateway (STT_SERVICE_URL) - **Whisper завантажується lazily при першому аудіо-запиті** на CPU (int8) **TTS:** xtts-v2 (2GB) — `unloaded`. Не використовується активно. **Висновок по Swapper: ЗАЛИШИТИ (він критичний)** Swapper є єдиним агрегатором для: 1. **Vision** (`/vision`) — qwen3-vl:8b для всіх агентів що аналізують фото 2. **STT** (`/stt`) — Whisper для голосових повідомлень 3. **OCR** (`/ocr`) — got-ocr2 для документів 4. **Document** (`/document`) — granite-docling для PDF/DOCX 5. **TTS** (`/tts`) — xtts-v2 (поки не активований) **Проблема:** `active_model=qwen3-8b` через Ollama — це **дублювання** з основним Ollama GPU. Swapper завантажує qwen3:8b через свій ollama, поки є окремий Ollama на 11434 з 27B. При виклику vision, swapper **swap'ає** qwen3:8b і завантажує qwen3-vl:8b — займає VRAM GPU. > **Але GPU_ENABLED=false!** — Значить qwen3-vl:8b завантажиться в RAM/CPU, що дуже повільно (>30s). ### RECOMMENDED PATCHES **Patch 6 (ВАЖЛИВИЙ):** Вирішити GPU конфлікт Swapper vs Ollama: Варіанти: - **A (рекомендований):** Swapper Vision через Ollama GPU (11434), STT на CPU: ```yaml # docker-compose.node1.yml, swapper-service environment: - GPU_ENABLED=true # дозволити GPU для vision/OCR - WHISPER_DEVICE=cpu # але STT лишається CPU - WHISPER_COMPUTE_TYPE=int8 # Прибрати CUDA_VISIBLE_DEVICES= (empty block GPU) ``` Потрібно додати GPU device back: ```yaml deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ``` Тоді Swapper поверне GPU для vision і OCR. - **B (поточний стан):** GPU_ENABLED=false → all CPU → Vision дуже повільно --- ## 6. GPU POLICY ### CURRENT STATE ✅ ``` VRAM: 18783 MiB / 20475 MiB (qwen3.5:27b-q4_K_M завантажений — warmup timer) GPU Ollama (11434): 1 model — qwen3.5:27b-q4_K_M (17434 MiB) CPU Ollama (11435): 0 models (smollm2:135m unloaded, lazy) gpu_single_model_policy_ok = 1 ✅ ollama_cpu_instance_up = 1 ✅ ``` **Проблема:** Swapper показує `active_model=qwen3-8b` — це qwen3:8b через ollama **всередині swapper**, але Swapper зараз CPU-only. Значить qwen3:8b у свопері не займає GPU VRAM поки GPU_ENABLED=false. Але якщо повернути GPU Swapper — треба перевірити що 27B + qwen3-vl-8b не одночасно в VRAM (20GB максимум). **Потенційний конфлікт:** 27B (17.4GB) + qwen3-vl-8b (6.1GB) = **23.5GB > 20GB VRAM** → OOM! Необхідна координація: коли Swapper завантажує vision модель, Ollama GPU має вивантажити 27B або навпаки. --- ## 7. NODA1 ↔ NODA2 — З'єднання ### CURRENT STATE **Інфраструктура:** - NODA1 і NODA2 (`llm80-che-1-1`, IP 192.168.1.240) — це **K3s cluster** (flannel CNI)! - NODA1: `node1-daarion` — **control-plane, master** (Ready) - NODA2 (`llm80-che-1-1`): `worker node` — **NotReady** (проблема!) - **Flannel:** `10.42.0.0/24` (NODA1), `10.42.1.0/24` (NODA2) — pod overlay network - **WireGuard:** НЕ встановлений - **NATS:** cluster config є (`my_cluster`, port 6222), але `routes = []` — **NATS не з'єднаний між нодами** **K3s pods на NODA2 (llm80-che-1-1):** більшість `Terminating` або `Pending` — NODA2 NotReady! **Що це означає:** - Фізично NODA1 і NODA2 з'єднані через K3s/flannel (LAN, 192.168.x.x) - Але Docker Compose сервіси на NODA2 (memory service, qdrant, neo4j) — **окремі**, не в K3s - NATS між нодами не federatederated — жоден cross-node message bus не налаштований ### GAPS 1. **K3s worker NODA2 NotReady** — pods Terminating/Pending. Не ясно чи це критично для поточного продакшну. 2. **NATS не кластеризований** — немає leafnode/route між NODA1 і NODA2 NATS. 3. **Немає cross-node subjects** для агентів. 4. **NODA2 підключення до NODA1:** NODA2 має свій Docker Compose (окремі memory/qdrant), немає спільного bus. ### RECOMMENDED PATCHES **Patch 7 (NATS federation між нодами):** ```conf # /opt/microdao-daarion/nats/nats-server.conf (NODA1) leafnodes { port: 7422 } # NATS на NODA2 підключається як leafnode: leafnodes { remotes = [{ url: "nats://144.76.224.179:7422" }] } ``` Це дозволить NODA2 публікувати/підписуватись на `node.control.noda2.*` через NODA1. --- ## 8. CTO SOFIIA — Control Plane ### CURRENT STATE **`control-plane` контейнер (порт 9200):** - FastAPI сервіс з JWT auth (`SERVICE_ROLE=controlplane`) - Endpoints: - `GET /prompts/{agent_id}` — версіоновані system prompts з файлів `*_prompt.txt` - `GET /policy/{agent_id}` — RBAC/entitlements (DefaultPolicies) - `GET /prompts/{agent_id}/hash` — hash промпту для drift detection - **401 Unauthorized** при зверненні без JWT — це правильно **Що є:** - ✅ Промпти централізовані та версіоновані - ✅ JWT auth для сервіс-до-сервіс - ✅ Policy/RBAC per agent - ✅ `dagi-vision-encoder-node1` — ViT-L-14 на CPU (embeddings) **Що НЕ реалізовано:** - ❌ Node operations (restart/deploy/health через control-plane) - ❌ Sofiia не має NATS-control topic для публікації команд - ❌ Немає `node-ops-worker` на кожній ноді - ❌ Sofiia добавляє нову ноду тільки через SSH root (bRhfV7uNY9m6er — hardcoded!) - ❌ Немає механізму "додати нову ноду без root" **Поточний механізм керування нодами:** SSH з паролем root. Небезпечно. ### RECOMMENDED PATCHES **Patch 8 (мінімальний control plane extension):** Додати в control-plane endpoints для node ops: ```python # services/control-plane/app/main.py (або новий node_ops.py) # Sofiia публікує на NATS: # node.control.noda1.restart_service → {service_name, reason} # node.control.noda1.health_check → {} # node.control.noda1.get_logs → {service_name, lines} # node-ops-worker (новий мікросервіс) підписується на ці subjects # виконує whitelist commands (docker restart, docker logs tail, health curl) # відповідає на node.control.noda1.reply.* ``` **Мінімальна реалізація (50 рядків Python):** ```python # services/node-ops-worker/main.py ALLOWED_COMMANDS = { "restart_service": lambda s: f"docker restart {s}", "health_check": lambda s: f"curl -sf http://localhost:{PORT_MAP[s]}/health", "logs_tail": lambda s, n: f"docker logs --tail {n} {s}", } # Subscribe to node.control.noda1.> via NATS # Execute only ALLOWED_COMMANDS # Reply to reply subject ``` --- ## VALIDATION CHECKLIST ```bash # 1. Router CPU profiles (host.docker.internal) docker exec dagi-router-node1 curl -s http://host.docker.internal:11435/api/tags | python3 -c 'import sys,json; print("CPU Ollama OK:", len(json.load(sys.stdin).get("models",[])))' # 2. GPU policy curl -s http://localhost:9400/metrics | grep gpu_single_model_policy_ok # 3. Swapper Vision (cold start test — без кешу) # УВАГА: займе 30-60s якщо GPU_ENABLED=false! # curl -s -X POST http://localhost:8890/vision -H 'Content-Type: application/json' \ # -d '{"model":"qwen3-vl-8b","prompt":"що на фото?","images":[""]}' | jq . # 4. Parser pipeline connected docker logs --tail 5 parser-pipeline 2>&1 | grep -E 'Connected|Consumer created' # 5. NATS stream ATTACHMENTS exists curl -s 'http://localhost:8222/jsz?streams=true' | python3 -m json.tool | grep -A3 'ATTACHMENTS' # 6. render-pptx-worker DNS fix check docker logs --tail 5 render-pptx-worker-node1 2>&1 | grep -v 'getaddrinfo' # 7. index-doc-worker DNS fix check docker logs --tail 5 index-doc-worker-node1 2>&1 | grep -v 'getaddrinfo' # 8. Control plane health curl -s http://localhost:9200/health # 9. Swapper STT device docker logs swapper-service-node1 2>&1 | grep STT-POLICY # 10. K3s NODA2 status kubectl get nodes ``` --- ## PRIORITIZED ACTION PLAN ### P0 — Негайно (production impact): | # | Патч | Файл | Вплив | |---|------|------|-------| | 3 | SWAPPER_URL fix в parser-pipeline | docker-compose.node1.yml | Vision через parser | | 5 | Network fix render-pptx + index-doc | docker-compose.node1.yml | Документи | | 6 | GPU повернути Swapper (Vision повільний!) | docker-compose.node1.yml | Vision latency | ### P1 — Цього тижня: | # | Патч | Файл | Вплив | |---|------|------|-------| | 1 | host.docker.internal для GPU профілів | router-config.yml | Stability | | 4 | NATS ATTACHMENTS persistence | nats config | Parser stability | | 7 | NATS leafnode NODA1↔NODA2 | nats-server.conf | Cross-node | ### P2 — Наступний спринт: | # | Патч | Файл | Вплив | |---|------|------|-------| | 8 | node-ops-worker для Sofiia control | нові файли | Security | | 2 | Profile rename в router-config | router-config.yml | Clarity | --- ## ВІДПОВІДІ НА 7 КЛЮЧОВИХ ПИТАНЬ ### 1. Фото E2E **Telegram photo → Gateway** (скачує файл → file_url) → **`send_to_router({images:[file_url]})`** → **Router** перевіряє агента → якщо vision-агент → **`SWAPPER_URL/vision`** → Swapper → Ollama `qwen3-vl:8b` → text опис → Router → Gateway → Telegram. Parser-pipeline — паралельний worker для асинхронної обробки (не основний шлях). Payload: `{model, prompt, images:[url], max_tokens}`. ### 2. Документи/PDF **Немає Docling як сервісу.** Docling вбудований в Swapper як `granite-docling` (lazy, unloaded). Шлях: Gateway → Router → `SWAPPER_URL/document` → Swapper → granite-docling. Паралельно через NATS: `artifact.job.render_pdf.requested` → render-pdf-worker → PNG → MinIO/artifact-registry. `index-doc-worker` індексує в RAG але має DNS fail. ### 3. Router Top-level агенти → **DeepSeek API** (cloud_deepseek). Crew tasks → **qwen3.5:27b-q4_K_M** (crew_local_27b, GPU). Monitoring/small → **smollm2:135m** (crew_local_small, CPU Ollama 11435). `ENABLE_CREW_MODEL_ROUTING=1` активний. Vision агенти отримують `qwen3-vl-8b` через Swapper. ### 4. TTS/STT STT: **Whisper (CPU, int8)** через Swapper `/stt`. `WHISPER_DEVICE=cpu` підтверджено логами. Lazy load при першому аудіо. Підтримується: faster-whisper-large (3GB), whisper-small (0.5GB). TTS: xtts-v2 (2GB) — **not deployed активно** (unloaded). Немає VRAM конкуренції для STT. ### 5. Swapper **Залишити.** Є єдиним агрегатором для Vision (qwen3-vl:8b), STT (Whisper), OCR (got-ocr2), Document (granite-docling), TTS (xtts-v2). Без Swapper треба окремі сервіси для кожного. Але: `active_model=qwen3-8b` — потенційно невикористана ролі (є окремий Ollama). **Слід розглянути видалення qwen3-8b зі Swapper** — він дублює GPU Ollama, залишити тільки Vision/OCR/STT/Document функції. ### 6. NODA1↔NODA2 З'єднані через **K3s cluster** (flannel, 10.42.0.0/24). NODA2 (`llm80-che-1-1`, 192.168.1.240) — K3s worker, зараз **NotReady**. NATS між нодами **не з'єднаний** (routes=[]), немає leafnode. Docker Compose сервіси незалежні. Для cross-node messaging потрібен NATS leafnode або Flannel pod networking. ### 7. CTO Sofiia Control Plane Поточний стан: `control-plane` (9200) — JWT-захищений сервіс з prompts + policy. **Немає node-ops механізму**. Sofiia керує нодами через SSH root (небезпечно). Правильний шлях: NATS-control plane + `node-ops-worker` на кожній ноді з whitelist команд. control-plane вже є основою — треба додати NATS subscription для node operations. --- *Звіт згенеровано автоматично аудитом NODA1 | Sofiia v2.7 | 2026-02-27*