# Рельсы плагинов: установка, Marketplace, сборка, MCP

ADR-0008 решил, что плагин — единица расширения, и оставил три расхождения кода со
спекой. Это решение закрывает их и задаёт, как плагин попадает в инсталляцию, как
живёт в ней и как её покидает. Строится сразу целиком, без промежуточных схем: рельсы
переделывать у сотен чужих инсталляций дороже, чем построить один раз (решение
владельца, 17.08.2026).

## Что решено

**Папка плагинов одна — `data/custom/plugins/<name>/`.** Плагин лежит в Custom layer как
git-checkout на запиненном sha и переживает `iva update`. Рядом, отдельными файлами:
`<name>.config.json` (конфиг eve Extension, читается в рантайме) и `<name>.env` (env
для MCP-серверов). `PLUGIN_DATA` плагина — `data/plugin-data/<name>/`, переживает
апдейт плагина (требование спеки §9.1). Общую папку `~/.agents/plugins/` не читаем:
второе место, откуда берётся плагин, — второе место, где он теряется.

**Состояние — один файл `data/custom/plugins.json`.** Запись на плагин: `name`,
`source`, `ref` (что отслеживаем), `sha` (что стоит), `digest` (что лежит), `enabled`,
`trusted`, `installedAt`. Плюс список маркетплейсов. Намерение и факт лежат рядом, отдельного
lock-файла нет. `enabled` включает скиллы и код плагина; `trusted` отдельно
разрешает поднимать его MCP-серверы — текст в промпте и процесс на машине не
включаются одним тумблером (так у pi). `iva plugin sync` доставляет всё из этого
файла: снёс папку — восстановится.

**Источники: `owner/repo[/подпапка][@ref]`, git-URL, локальная папка, имя из
Marketplace.** Установка: `fetch --depth 1 --filter=blob:none origin <sha>` →
`checkout FETCH_HEAD` → сверка `rev-parse HEAD`; sha по `^[a-f0-9]{40,64}$`. При установке
записывается digest дерева плагина; `update` любого вида источника отказывает, если дерево в
`data/custom/plugins/` разошлось с записанным digest (правки владельца не затираются молча),
`--force` снимает отказ. Так делают Codex и `plugins.sh`, только они проверяют git status.

**Marketplace — конвенция Codex.** Читаем `.agents/plugins/marketplace.json` в
git-репо: `name`, `plugins[{name, source, description}]`, `source` — строка-путь или
объект `local | url | git-subdir` с `ref`/`sha`. `npm` и `policy` игнорируем.
Дефолтный маркетплейс — `smixs/iva-plugins`; свои добавляются `iva plugin marketplace
add`. Спека Agent Plugins дистрибуцию не описывает; три существующих формата
маркетплейса несовместимы, а конвенция Codex — единственная у полного open-source
конформного клиента, и наш список смогут прочитать его пользователи.

**Клиент выполняет спеку буквально.** Закрытая схема манифеста, валидация локально
без сети, `name` по регэкспу спеки, только два нефатальных нарушения (лишнее поле
верхнего уровня, `extensions` не объект). `skills/` — только непосредственные дети с
`SKILL.md`, без рекурсии. `mcp.json` — закрытый документ, версия `$schema` совпадает
с манифестом. Containment всех путей; симлинки отвергаем; лимиты 2000 файлов / 50 МБ
/ 16 уровней (строже спеки, как NanoClaw — спека это разрешает). Битая компонента не
валит остальные.

**Скиллы плагина — живьём.** Резолвер custom-скиллов читает `data/custom/agent/skills/`
и `plugins/*/skills/` включённых плагинов; сборка и рестарт не нужны. Одноимённый
скилл плагина побеждает ядровой (ближний слой, как у eve). Два плагина с одноимённым
скиллом — `add` отказывает.

**Код плагина — eve Extension под `sh.iva/`, собирает Ива.** При сборке версии:
копия плагина в `<version>/plugins/<name>/` → `npm ci --omit=dev` в ней → `npx eve
extension build` нашим eve → сгенерированный mount `agent/extensions/<name>.ts` с
относительным спецификатором (eve его резолвит; `package.json` ядра не трогаем) →
`npm run build` → probe → flip → рестарт. Всё по рельсам апдейтера v2. Плагин с кодом
всегда совместим с текущим eve; апдейт eve пересобирает плагины сам.

**Отказ сборки не роняет коробку (ADR-0003).** `iva plugin add`: установка
отменяется, текущая версия не тронута. `iva update`, где плагин сломался о новый eve:
версия собирается без него, плагин получает `enabled: false`, владелец — Alert
(ADR-0007). Один плагин не валит агента (в отличие от fail-loud DSH).

**MCP — оба транспорта.** `streamable-http`/`sse` → сгенерированный
`agent/connections/mcp-<name>--<server>.ts` с `defineMcpClientConnection` (имя файла
складывается в алфавит, который eve принимает под connection — `[a-z][a-z0-9-]`, без
подчёркиваний и точек; файл с другим именем eve не регистрирует вовсе). `stdio` →
systemd-юнит `iva-mcp-<name>-<server>.service` с **MCP proxy** — своим сервисом на
`@modelcontextprotocol/sdk` (`services/mcp-proxy/`), loopback-порт из `plugins.json` +
bearer, `Restart=on-failure`, тот же connection-файл. Env сервера — только из
`mcp.json` и `<name>.env`, не весь env агента; `PLUGIN_ROOT`/`PLUGIN_DATA` заданы;
плейсхолдеры раскрываются один проход в `args`/`env`/`cwd`. Тот же механизм юнитов
служит долгоживущим сервисам плагина, объявленным в `sh.iva/`.

**CLI; код и MCP — только по тапу владельца** (правка 04.10.2026). `iva plugin add|remove|
update|enable|disable|trust|untrust|list [--available]|sync|propose`, `iva plugin marketplace
add|remove|list`. Тула модели для установки нет. Плагин из одних скиллов модель ставит сама
(`iva plugin add` из `bash`): это текст, который и так читается из Custom layer. Плагин с
`mcp.json` или `sh.iva/` — код в процессе с токенами инсталляции и процессы на машине,
поэтому его ставит Bridge по тапу владельца на предложении, собранном кодом: `iva plugin
propose` копирует папку в `data/plugin-proposals/<name>-<digest12>/` и шлёт в личный чат
владельца, что плагин будет запускать и сколько в нём файлов, с кнопкой «Установить»; тап
забирает копию, сверяет её хеш дерева и запускает `iva plugin install-proposal` вне хода модели;
установщик сверяет хеш ещё раз у копии в своём staging и ставит только её. Предложения и забранные копии старше суток удаляет следующий `propose` по времени папки — явное
исключение из запрета «удаление по mtime» (`docs/philosophy.md`): удаляется только копия, исходная
папка плагина остаётся у модели, худший исход — «предложи заново». Тап ставит забранной копии
время тапа, поэтому идущая установка свою копию не теряет; копия, возвращённая после
неудачного запуска установщика, получает обратно время propose, и срок предложения не продлевается.
`add` такого плагина без TTY и не из установщика отказывает и называет `propose`;
`plugin trust|enable|update|sync|install-proposal` при `IVA_AGENT_TURN=1` отклоняет CLI с указанием `iva plugin propose <папка>`; эти команды выполняет Bridge по тапу владельца вне хода модели. Честная оговорка:
пока ступень 2 ADR-0005 отложена, модель с `bash` может поставить что угодно мимо CLI; тап —
граница намерения владельца на штатном пути, не защита от взломанной модели. При первом
`add` печатается принятый риск из ADR-0008; при `mcp.json` со `stdio` — команды серверов и
запрос доверия. `iva doctor` проверяет манифесты,
`plugins.json`, сборку, юниты MCP proxy.

**`sh.iva/` — двух видов.** eve Extension — это `sh.iva/package.json` (собирается в
версию); сервисы — папки `sh.iva/services/<svc>/` с `service.json` (поднимаются юнитом
прямо из Custom layer и версию не пересобирают никогда). Ключ `extensions["sh.iva"]`
обязателен для обоих: без него папка игнорируется.

**Namespace — `sh.iva`.** Ключ в `extensions` и папка `sh.iva/` в корне плагина.
Домен iva.sh проекту не принадлежит; спека требует reverse-domain как SHOULD и не
проверяет владение. Свободен среди занятых (`com.openai`, `dev.kiro`, `ai.openclaw`,
`ai.nanoco.nanoclaw`, `dev.pi.agent`, `dev.opencode`). Принятый риск, не менять.

## Владение службами плагинов

Установка плагинов определяется canonical `dataRoot`: `plugins.json`, порты и замок
общие для всех копий кода над этим каталогом. Имена новых служб содержат полный SHA-256
JSON-кортежа `["iva-plugin-data", 1, dataRoot]`. Прежняя диагностика коллизии
`a-b/c` и `a/b-c` сохраняется. Метка владельца кодирует обратимый JSON-кортеж в UTF-8 hex
и связывает scope, ключ плагина, вид имени и хеш точного содержимого службы.

Один inspector устанавливает фактическое имя для reconcile, перезапуска и doctor.
Старое имя сохраняется при доказанном каталоге данных в файле и загруженной службе:
WorkingDirectory для сервиса, аргументы data/token/port для MCP. Добавление одной метки
работающей службе сохраняет её процесс. Перед управлением scoped-службой также сверяется
загруженная папка данных. Чужое содержимое, ссылки и неизвестное владение сохраняются;
конфликт под ожидаемым именем останавливает изменение этого ключа.

Это защита штатных установок одного пользователя, а не граница против кода того же
пользователя. Старые CLI, включая откат на прежнюю версию, этих правил владения не знают;
их глобальный reconcile остаётся принятым риском совместимости. Отдельный файл владельцев,
дополнительный замок и ремонт отката сюда не входят.

## Что отвергнуто

- npm-пакет как форма плагина: правит `package.json` ядра, конфликтует на каждом
  апдейте, и это не папка Agent Plugins.
- Слот `agent/extensions/` в Custom layer руками: это eve Extension, не плагин.
- Автор коммитит `dist/`: после бампа eve чужой плагин отваливается по контракту.
- Свой формат маркетплейса и GitHub-топик вместо Marketplace: первый никто чужой не прочтёт,
  второй — имя займёт кто угодно.
- Только http-транспорт MCP: половина серверов в дикой природе — stdio.
- Готовые прокси (`supergateway`, `mcp-proxy`): чужой релиз-цикл, лишние
  зависимости или второй рантайм ради одного процесса.
- Тул модели для установки и установка командой из Telegram: обе двери обходят сообщение,
  собранное кодом, и тап владельца на нём. Плагин из одних скиллов модель ставит сама,
  плагин с кодом или MCP — только через предложение (правка 04.10.2026).
- Cordis/DI, fail-loud, `node:vm`-песочница, YAML с кодом (DSH): второй слой поверх
  eve и лишние трущиеся детали.
- `schedules/` внутри плагина: eve сам запрещает (`DISCOVER_EXTENSION_SCHEDULE_UNSUPPORTED`).

## Принятый риск

Плагин с кодом работает в процессе агента и видит его env (ADR-0008). Рельсы это не
меняют: доверие — решение владельца при `add`. MCP-серверы получают меньше: только
свой env, `PLUGIN_ROOT` и `PLUGIN_DATA`.

Изоляция здесь одна и узкая: от env агента. Друг от друга и от каталога данных плагины
не изолированы — все юниты идут от того же пользователя, что и Ива, а токен прокси
лежит в `PLUGIN_DATA`, то есть доступен любому процессу этого пользователя. Песочница
за это не отвечает (ADR-0005 ступень 2 отложена владельцем).

Статус: решение принято 17.08.2026, реализация — волна «плагины», первый плагин —
`trace` (ADR-0010).
