Развёртывание deepseek-harness в отдельном LXC 116 на proxmox-pve3, подключённого к omniroute как LLM-провайдеру, доступного снаружи по harness.skud24.ru через traefik-ha.
Содержание
Хост: LXC 116 на pve3 (198.51.100.1), Debian 12, unprivileged, статический IP 198.51.100.2/24. Изначально 2 CPU/2GB RAM/16GB rootfs — память пришлось поднять до 4096MB + 2048MB swap (pct set 116 --memory 4096 --swap 2048 + pct reboot), сборка падала в OOM при 2GB.
Установка: git clone репозитория в /opt/harness, pnpm install && pnpm run build. Два нетривиальных препятствия по пути: 1. Node.js 20 не годится. pnpm install падал ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite — pnpm-версия репозитория (11.7.0, задана через packageManager в package.json) требует Node ≥22.13. Попытка вручную пинить corepack prepare pnpm@9.15.0 --activate не сработала — corepack всё равно уважает packageManager внутри самого проекта и переставляет обратно на 11.7.0. Единственный рабочий фикс — поднять Node.js до 22.x (NodeSource setup_22.x). 2. OOM на сборке. pnpm run build (tsc -b + tsdown/vite по ~246 пакетам монорепо) падал FATAL ERROR: Reached heap limit даже с NODE_OPTIONS=--max-old-space-size=3584 — реальным потолком был cgroup-лимит LXC (2GB), не V8 heap-size. Исправлено увеличением RAM контейнера (см. выше).
Backend: провайдер omniroute в /root/.dsh/settings.yaml (api: openai-completions, baseURL: https://omniroute.skud24.ru/v1). Изначально модели-алиасы auto/best-coding, auto/coding:fast, auto/best-reasoning, auto/best-chat — заменены 2026-08-22 на конкретные пиненные модели (kr/claude-sonnet-4.5, kr/claude-haiku-4.5, kr/deepseek-3.2, kr/glm-5, kr/qwen3-coder-next, дефолт kr/claude-sonnet-4.5) — см. находку про "Invalid model" ниже. Ключ через OMNIROUTE_API_KEY в systemd-юните.
Сетевая экспозиция — двухслойная, из-за собственных ограничений dsh:
dshв рантайме блокирует--host 198.51.100.3(см. deepseek-harness) — слушает только198.51.100.4:3080.- Локальный TCP-форвард
dsh-proxy.service(socat TCP-LISTEN:3081,fork,reuseaddr,bind=198.51.100.2 TCP:198.51.100.4:3080) пробрасывает на LAN-IP без изменения поведения самого dsh. - traefik-ha (198.51.100.5) терминирует TLS и роутит
harness.skud24.ru → 198.51.100.2:3081через file-provider паттерн (/opt/skud24-traefik/dynamic/harness.yml). Изначально (2026-08-21) — двойной middlewareipWhiteList+basicAuth;ipWhiteListснят 2026-08-22 по явному запросу пользователя — сервис открыт в интернет (198.51.100.3/0), единственный барьер теперьbasicAuth(mix:<пароль>). Подробности — см. traefik-ha.
Найденная и исправленная 2026-08-21 проблема — 403 на /api/agentPreset.list после того, как basicAuth уже пройден. Симптом: страница настроек открывается, но список моделей не грузится, DevTools показывает HTTP 403 на /api/* вызовах. Диагностика через прямые curl-запросы показала: 198.51.100.4:3080 напрямую (Host-заголовок 198.51.100.4) → 404 (обычный, метод/маршрут не тот); тот же путь через LAN-прокси (198.51.100.2:3081, Host-заголовок иной) → 403; через сам домен без авторизации → 401 (basicAuth Traefik, ожидаемо); с валидной basicAuth-парой → 403 (то есть Traefik пропустил, само приложение отбило). Причина — второй защитный механизм dsh, не связанный с --host: allowlist доверенных Host-заголовков (защита от DNS rebinding), см. deepseek-harness. Исправлено добавлением флага --trusted-host harness.skud24.ru в ExecStart юнита dsh.service + systemctl daemon-reload && systemctl restart dsh. После фикса /api/agentPreset.list через домен с авторизацией отдаёт 404 (тот же безобидный ответ, что и локально на GET) вместо 403 — модели должны загружаться штатно из браузера (реальный клиент шлёт правильный метод/RPC-конверт).
systemd-юниты (LXC 116):
dsh.service—ExecStart=/bin/pnpm dsh web --host 198.51.100.4 --port 3080 --no-open --trusted-host harness.skud24.ru, envDSH_HOME=/root/.dsh,OMNIROUTE_API_KEY=demo-secret-123.dsh-proxy.service—socat TCP-LISTEN:3081,fork,reuseaddr,bind=198.51.100.2 TCP:198.51.100.4:3080,Requires=dsh.service.
Доступ: https://harness.skud24.ru, basicAuth mix:<пароль> — единственный барьер после снятия ipWhiteList (см. ниже). TLS Let's Encrypt, действителен 2026-08-21 → 2026-11-19.
Найденная и исправленная 2026-08-21/22 проблема — "Invalid model" при попытке чата. Корень — не в dsh, а в omniroute: auto/ комбо-роутеры (auto/best-coding и другие) пробуют первым кандидатом kiro/auto-kiro, который падает HTTP 400 от бэкенда kiro с текстом Invalid model. Please select a different model to continue. — и OmniRoute абортит весь комбо целиком вместо перехода к оставшимся ~17 кандидатам ("skipping fallback to other targets to prevent infinite loop"). Отдельно кандидат github-models/openai/gpt-4.1 падает независимо HTTP 410 (внешний retirement brownout GitHub Models, не связан с нашей конфигурацией). Это баг/недонастройка самого OmniRoute — не чинится со стороны dsh/LXC 116, требует внимания в OmniRoute-конфиге на 198.51.100.6. Воркараунд: settings.yaml перепинен на конкретные модели (kr/, см. выше) — не решает первопричину, но чат снова работает.
Открытие в интернет (2026-08-22). По явному запросу пользователя (подтверждён выбор "полностью открыть 198.51.100.3/0" из двух вариантов) middleware ipWhiteList удалён из harness.yml на traefik-ha — Traefik подхватил без рестарта (~2 сек), проверено live curl (401 без пароля → 200 с паролем). basicAuth остаётся единственным барьером. ⚠️ dsh даёт модели произвольное выполнение кода на хосте — стоит следить за стойкостью пароля.
harness → n8n: MCP-клиент (2026-08-23). Направление, обратное SSH-мосту через n8n-skud24 (там Claude/n8n управляют лабой по SSH) — здесь сам dsh подключается к n8n как MCP-клиент и получает его тулы в свой собственный список. Механизм — встроенный плагин @deepseek-ai/dsh-mcp-client (пакет уже в node_modules профиля), конфигурируется per-профиль в /root/.dsh/profiles/<profile>/cordis.patch.yml (пустой YAML-массив по умолчанию — редактировать его, не cordis.yml). n8n отдаёт instance-level MCP endpoint http://198.51.100.7:5678/mcp-server/http (LAN-адрес, не через Traefik/домен) — требует отдельного MCP-токена (не n8n API key), генерируется в n8n UI: Settings → Instance-level MCP → Connect a client. Патч:
- insert:
- id: mcp-n8n
name: "@deepseek-ai/dsh-mcp-client"
config:
serverName: n8n
transport: streamable-http
url: http://198.51.100.7:5678/mcp-server/http
headers:
Authorization: "Bearer <MCP-токен>"
Грабля: официальный пример дока dsh-плагина использует !!js YAML-тег для интерполяции process.env. в значении — не работает в per-profile cordis.patch.yml (парсер оверлеев кидает YAMLException: cannot resolve a node with !<tag:yaml.org,2002:js> explicit tag; похоже !!js поддерживается только в конфигах верхнего уровня сборки, не в profile-патчах). Решение — токен прописан прямой строкой, не через env-переменную. Применено к обоим профилям (web — боевой за harness.skud24.ru, и headless). После systemctl restart dsh проверено: 29 тулов mcp__n8n__ (search_workflows, execute_workflow, create_workflow_from_code, get_node_types, validate_workflow, list_credentials и т.д.) доступны модели — подтверждено через dsh --profile headless "list all tools whose name contains n8n".
Связано с
- deepseek-harness — общая архитектура инструмента, откуда взяты оба защитных механизма (
--host,--trusted-host) - omniroute — LLM-backend этого инстанса
- proxmox-pve3 — физический хост (LXC 116)
- traefik-ha — реверс-прокси, TLS, basicAuth/ipWhiteList
- n8n-skud24 — двусторонняя связь: инструмент, через который шло всё провижининг и диагностика (SSH Bridge (Chat)), и с 2026-08-23 — сервер, к которому сам dsh подключается как MCP-клиент
- claude-code-lab-deployment — аналогичный по духу деплой другого агентного харнесса в этой же лабе
Источник
- Рабочая сессия Cowork 2026-08-21 (полное развёртывание + диагностика 403 через прямое чтение исходников dsh по SSH)
- Рабочая сессия Cowork 2026-08-22 (диагностика "Invalid model" через прямые curl к OmniRoute +
docker logs, воркараунд наkr/*модели; открытие в интернет по запросу пользователя) - Рабочая сессия Cowork 2026-08-23 (harness → n8n MCP-клиент: чтение
.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.mdиз репозитория dsh, конфигурацияcordis.patch.yml, проверка черезdsh --profile headless)
Комментарии