# forge-tools-proxmox MCP-сервер для **гипервизора Proxmox VE** (MCP по stdio) — управление кластером/нодами, QEMU-ВМ, LXC-контейнерами, снапшотами, бэкапами, задачами, хранилищами и сетью. Зеркало архитектуры `forge-tools/ssh` + `postgres`: **stdio-only, одиночный потокобезопасный Manager, ручные JSON-Schema, `--health`, graceful shutdown, `isolation=pooled`** (per-agent конфиг приходит на каждый вызов через `_tenant_config`). > **Границы (принцип «дополнять, а не дублировать»).** Домен — только > гипервизор. Запуск кода *внутри* гостя — не здесь: это `ssh__run` > (remote exec); локальные файлы — `filesystem`; БД — `postgres`. > Управление Ceph/ZFS/SDN/firewall/HA/ACL и восстановление бэкапов — > кандидаты в отдельные узкие `forge-tools` (см. «Roadmap»). ## Инструменты (54) | Домен | Инструменты | |---|---| | **cluster** (4) | `clusters_list`, `cluster_status`, `nodes_list`, `node_status` (read) | | **node** (1) | `node_network` (read) | | **VM** (13) | `vms_list`, `vm_describe`, `vm_config`, `vm_next_id` (read) · `vm_start`, `vm_stop`, `vm_reboot`, `vm_shutdown`, `vm_suspend`, `vm_resume`, `vm_clone`, `vm_delete`, `vm_convert_template` (мутации) | | **VM config/disk/net** (10) | `vm_config_update`, `vm_resize_disk`, `vm_add_disk`, `vm_remove_disk`, `vm_move_disk`, `vm_add_network`, `vm_update_network`, `vm_remove_network`, `vm_set_cloudinit` (мутации) | | **LXC** (12) | `containers_list`, `container_describe`, `container_config` (read) · `container_start`, `container_stop`, `container_reboot`, `container_shutdown`, `container_clone`, `container_delete`, `container_config_update`, `container_resize` (мутации) | | **snapshot** (4) | `snapshot_list` (read) · `snapshot_create`, `snapshot_delete`, `snapshot_rollback` (мутации) | | **backup** (2) | `backup_list` (read) · `backup_create` (async UPID) | | **task** (4) | `tasks_list`, `task_status` (+wait), `task_log` (read) · `task_stop` (мутация) | | **guest** (1) | `guest_ips` (read, QEMU guest-agent) | | **storage** (4) | `storage_list`, `storage_status`, `templates_list`, `isos_list` (read) | ## Конфигурация Путь per-agent `pve.json` (pooled → на каждый вызов ядро инжектит `_tenant_config`); либо статический `-config` для одиночного режима. ```jsonc { "hosts": [ { "alias": "pve1", "url": "https://10.0.0.10:8006/api2/json", "token_id": "root@pam!forge-dev", "token_secret": "${PROXMOX_TOKEN_SECRET}", // из env / gitignored *.local.json "ca_file": "${PROXMOX_CA_FILE}", // предпочтительно; insecure=true только for dev-lab "insecure": false, "allow_nodes": ["pve1"], "allow_vmids": [500] } ], "default": "pve1", "read_only": true, // false включает мутации (в доп. к confirm+approval) "timeout_sec": 15, "task_poll_max_sec": 600, "max_output_bytes": 4194304 } ``` - **Секреты никогда в git**: `token_secret`/пароль — только `${VAR}` или gitignored `*.local.json`. В репозитории хранится лишь шаблон `pve.json.example`; рабочие `pve.json`/`config.json` и `*.local.json` закрыты локальным [`.gitignore`](.gitignore). - **`read_only`** (дефолт `true`) и **`allow_nodes`/`allow_vmids`** (per-host) — fail-closed: без явных `allow_vmids` мутации недостижимы. Это второй слой поверх least-privilege токена. - **`ca_file`** — кастомный CA для self-signed кластера; `insecure` — только для локального dev-lab. - **Live-reload** через `configreload`: правки `pve.json` применяются без рестарта (по контент-хэшу). ### Переменные окружения | Переменная | Назначение | |---|---| | `FORGE_TENANT_CONFIG` | Каталог с per-agent `pve.json` (режим `isolation=pooled`). | | `PROXMOX_HOST` | Адрес гипервизора (подставляется в `url`). | | `PROXMOX_TOKEN_ID` | ID API-токена PVE, напр. `root@pam!forge-dev`. | | `PROXMOX_TOKEN_SECRET` | Секрет API-токена. | | `PROXMOX_CA_FILE` | Путь к кастомному CA self-signed кластера. | Значения в конфиге поддерживают подстановку `${VAR}` из окружения — реальные адреса и токены в репозиторий не попадают. ## Несколько гипервизоров и коллизии VMID Каждый `hosts[]` — отдельный кластер со **своими** кредами/TLS и **своими** правами. **VMID/ноды на разных гипервизорах могут пересекаться**, поэтому идентичность ресурса — всегда **`(host, node, vmid)`**, а не «голый vmid». - **`host`** выбирает целевой гипервизор (aliасс, дефолт = `default`). - **`allow_vmids`/`allow_nodes` — per-host**: права изолированы между кластерами. Одни и те же `vmid=100` на pve1 и pve2 дают **разный** вердикт авторизации (никакого протекания прав). - **При `len(hosts) > 1` `host` обязателен** для всех инструментов, кроме `clusters_list` (флот-обзор). Не указан → отказ (fail-closed на неоднозначность), чтобы случайно не задеть чужой кластер. - **Глобальный `allowlist` при мульти-гипервизоре — запрещён** (ошибка конфигурации): права обязаны быть per-host. Для одиночного гипервизора глобальный `allowlist` остаётся допустимым фолбэком. - **Probe-паттерны** несут `host=` первым — permission-правила могут различать кластеры: ```yaml patterns: - match: "proxmox__vm_delete" pattern: "host=pve1 node=pve vmid=500" then: ask ``` - **Нет флот-агрегатов** по VMID (`clusters_list` даёт карту алиасов, дальше — вызов с `host`), чтобы не терять принадлежность хоста. ## Безопасность (§9.5) | Против чего | Защита в модуле | Внешний барьер | |---|---|---| | Случайное удаление/rollback/migrate | `confirm="true"`, `read_only`, **per-host** `allow_vmids`/`allow_nodes`, path-валидация | `require_approval` в agent.yaml | | Коллизия VMID между гипервизорами | `host` обязателен при мульти; авторизация **per-host**; глобальный allowlist запрещён | — | | Инъекция в URL (node/vmid/snapname/storage/upid) | guard-валидация (`ValidateIdentifier`/`ValidateUPID`) + `url.PathEscape` | least-privilege токен в PVE | | Изменение конфига вслепую | `DenyConfigKey` (delete/revert/hotplug/spice/...) для update-инструментов | — | | Внешний доступ | URL объявляет оператор (модель не задаёт хост), кастомный CA, таймаут | — | | Долгие/необратимые операции | async UPID + `task_status(wait)` (bounded `task_poll_max_sec`) | — | | Запуск кода в госте | **не реализуется** (зона `ssh__run`) | — | **Главный барьер — на стороне PVE**: токен с минимальными правами (для чтения достаточно `Sys.Audit, VM.Audit, Datastore.Audit`; для мутаций — только нужное). Модуль держит guard как второй слой (стандарт двухслойности в §9.5). **Probe/approval**: мутации регистрируются через `registerPatternTool` (эмитятся `host= node= vmid=`), оператор может задать `patterns`-правила по ресурсу; дефолт permission — `ask`, и всё деструктивное ещё и в `require_approval`. `always` не эмитим — каждый опасный вызов отдельный ask. ## Подключение к агенту ```jsonc // config.json ядра "mcp": { "servers": { "proxmox": { "command": "./forge-tools/proxmox/forge-tools-proxmox", "args": [], "isolation": "pooled" } }} ``` ```yaml # agent.yaml mcp_servers: - proxmox tools: require_approval: - "proxmox__vm_start" # и прочие lifecycle - "proxmox__vm_delete" - "proxmox__vm_config_update" - "proxmox__vm_add_disk" ... "proxmox__vm_move_disk" - "proxmox__vm_add_network" ... "proxmox__vm_remove_network" - "proxmox__container_*" - "proxmox__snapshot_rollback" # + snapshot_delete - "proxmox__backup_create" - "proxmox__task_stop" blocked: [] # при желании тонкие правила по ресурсу: permission: default: ask patterns: - match: "proxmox__vm_delete" pattern: "node=pve vmid=500" then: ask ``` ## Сборка и ручной тест (T1) ```bash make build # CGO_ENABLED=0, статический бинарник ./forge-tools-proxmox --health # ok ``` T1 по stdio: ```bash printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}\n' \ | ./forge-tools-proxmox -config /abs/path/pve.json ``` `tools/list` → 54 инструмента (27 мутаций с probe-флагом); `node_status` в `read_only` → данные; `vm_delete` без `confirm`/вне `allow_vmids` → понятный отказ без обращения к API. ## Безопасный тест на homelab (не трогая существующие ВМ) - Single-node `pve` (10.0.0.10, PVE 8.4.21). Существующие ВМ — 100…304. - **Тестовая ВМ 500** — клон шаблона **106** (`template.example.com`, server: OVMF, `scsi0` 16G на `local-nc`, `net0`→`vmbr0`, `agent:1`), через `proxmox__vm_clone` (newid=500) ИЛИ вручную. - Dev-конфиг: `"hosts":[{"alias":"pve","allow_nodes":["pve"],"allow_vmids":[500]}]` → модуль физически не может задеть 100–304. - `guest_ips` тестируется на клоне 500 (нужен guest-agent, в шаблоне включён). ## Roadmap (отдельные узкие модули, не сюда) - `proxmox-backup` — restore/delete/prune vzdump (необратимые, нужна выверенная семантика), расписания, retention. - `proxmox-storage` — Ceph/ZFS-pools, ISO/template upload, disk import. - `proxmox-net` — bridges/bonds/SDN-VXLAN, HA, firewall, ACL/users. - Node reboot/shutdown — сознательно НЕ включено (однонодовый blast-radius). ## Тесты ```bash go test ./... -count=1 # unit: политика/guard (без сети и без PVE) ``` Integration-кейсы (`-run Integration`) — против реального PVE, только на VMID 500. --- ## Разработка ```bash make lint # go vet + gofmt -l (пусто = ок) make test # go test -race ./... make build ``` Требования: Go 1.27+ и доступ к приватному Go-модулю [`forge-toolkit`](https://git.totmin.ru/en2zmax/forge-toolkit) (`export GOPRIVATE=git.totmin.ru`). --- ## Лицензия Apache-2.0 — см. [`LICENSE`](LICENSE).