clawdai.ru
Решения продукта

Запись по умолчанию — репетиция: почему mode=preview, а dryRun отвергается

Почему запись в mcpd по умолчанию — предпросмотр: один переключатель mode, отвергаемый dryRun и мутируемость, объявленная по каждому действию каталога.

· 5 мин чтения

Опубликовано
Раздел
Решения продукта
Время чтения
≈ 5 мин

Ассистент не знает заранее, какой инструмент пишет в модель, а какой только читает. Он выбирает по русскому описанию — и иногда ошибается. Если инструмент с записью по умолчанию выполняет её сразу, один неверный выбор оставляет след в живой конфигурации 1C:EDT, а откатывать его нечем: правки идут поверх in-memory-моделей открытого проекта, а не файлов на диске. Поэтому запись по умолчанию в mcpd — репетиция, а не применение.

Почему репетиция, а не применение

Решение записано в ADR-004 со статусом accepted и датой 2026-08. Контекст там короткий и жесткий: модель угадывает инструменты записи, а применение по умолчанию против живой конфигурации — невосстановимая ловушка. Отсюда правило: предпросмотр — значение по умолчанию для каждого маршрута, где он возможен; применение — явное (mode=apply либо вызов маршрута, у которого предпросмотра вообще нет). Явное применение сохраняет прежние защёлки — подтверждение, ожидаемые строки и allowlist.

Главная мысль ADR формулируется как запрет, а не как рекомендация:

A preview that cannot prove it did not mutate must not call the mutating core.

Если предпросмотр не может доказать, что ничего не изменил, ему не места рядом с изменяющим ядром — он просто не вправе его вызывать. Это развилка, которая и определила форму поверхности: сначала отдельный гейт capability-preview, где все изменяющие действия прогоняются в предпросмотре без изменения рабочей области, и лишь затем — включаемый вручную capability-mutation с временными объектами и откатом.

Один переключатель — mode

На проводе у записи ровно один переключатель — mode с двумя значениями: preview (по умолчанию) и apply. Режим не хранится как настройка: право писать выбирается на каждый вызов отдельно, флагом mode или самим фактом вызова маршрута, у которого предпросмотра нет. Мультидейственные инструменты берут ещё и параметр action, но он выбирает операцию, а не режим записи.

Старое имя dryRun не принимается. Вызов с ним не «починят молча» и не проигнорируют — его отвергают, и отказ обязан назвать канонический параметр, иначе модель повторит тот же вызов с тем же dryRun и застрянет. Реальный текст отказа таков:

error.code              = "UNKNOWN_PARAMETER"
error.retryable         = false
data.canonicalParameter = "mode"
error.message = "Параметр dryRun не принимается: канонический переключатель режима — mode."
error.hint    = "Ничего не выполнялось. Передайте mode=preview для предпросмотра или
                 mode=apply для выполнения; dryRun в mcpd больше не существует.
                 Уберите dryRun и повторите вызов."

Фраза «ничего не выполнялось» здесь не вежливость, а обещание: раз переключатель не распознан, ядро не вызывалось, и предпросмотр не успел ничего задеть.

Отказ приходит в том же конверте, что и успех, — { "ok": …, "data": …, "warnings": …, "error": …, "meta": … }, плюс структурированное содержимое MCP и короткое русское резюме. На проводе нет слова «legacy»: отказ и успех не различаются формой ответа, чтобы вызывающая сторона не завела два обработчика и не начала путаться между ними.

Право писать живёт в группах инструментов: группа либо читает, либо пишет, но никогда не делает и то и другое, а режим выводится из включённых групп и не хранится отдельной настройкой. Административный флаг -Dmcpd.admin=true включает только диагностику — /admin и поля ошибок — и не расширяет набор инструментов: админка не превращает читающий профиль в пишущий, и применение по-прежнему остаётся явным выбором на каждый вызов.

Почему dryRun не стал алиасом

Соблазн принять dryRun как алиас mode=preview велик: одна строчка в разборе параметров, и никто не сломается. Инвариант «легаси не оставляем» запрещает именно это. Меняя публичное имя или конвенцию, старую форму удаляют, а не поддерживают: ни алиасов приёма, ни внутренних легаси-флагов «на переходный период», ни разных правил для провода и внутренностей. Двойной стандарт сам становится источником ошибок, и живой пример — та самая пара mode/dryRun (#558).

Логика отказа в том, что безопаснее дважды ошибиться на понятном правиле, чем один раз положиться на тихую догадку. Алиас молча принимает устаревшее имя, и через полгода в логе невозможно отличить, кто что имел в виду. Отказ с указанием на mode неудобен для вызова один раз — но он не оставляет второго, невидимого пути.

Держать старый флаг «на переходный период» дороже, чем кажется. Любое переименование публичного инструмента уже требует синхронной правки каталога, групп инструментов со списком легаси-алиасов, перекрёстных русских описаний, справок и золотых тестов: потянуть за одно имя — значит перебрать шесть мест. Дополнительный невидимый алиас — седьмое место, где каталог и приёмка способны разойтись.

Мутируемость видна по каждому действию

Слово «безопасно по умолчанию» ничего не значит, если его нельзя прочитать до вызова. Поэтому изменяемость объявлена в каталоге — у маршрута целиком и у отдельных действий, — и живёт в одном перечислении.

public enum Mutability {
    READ_ONLY,
    PREVIEWABLE,
    /** Legacy-исполнитель показывает флаг preview, а не режим. */
    PREVIEW_PARAM,
    APPLY_ONLY
}

READ_ONLY — только чтение, PREVIEWABLE — репетиция с возможным применением, APPLY_ONLY — маршрут, где предпросмотра нет и всякий вызов сразу действие (metadata_export, workspace_rebuild, database_sync). Отдельно стоит PREVIEW_PARAM: под ним спрятаны legacy-исполнители, которые показывают булев флаг предпросмотра, а не режим, и это честно названо классом, а не растворено в общем PREVIEWABLE.

Важная деталь: по умолчанию PREVIEWABLE у маршрута не распространяют молча на все его действия. У metadata_edit из 143 действий персональная политика названа для 11 операций, у configuration_update — для всех 11 действий сразу. Комментарий в каталоге объясняет, зачем это раздувание:

унаследованный от маршрута PREVIEWABLE делает обещание безопасного предпросмотра неявным, и соседняя правка способна отменить его молча.

Когда каждое действие перечислено по имени, соседняя правка не может отменить обещание предпросмотра незаметно — её придётся писать явно напротив конкретного action.

Этот же класс вызова и та же политика попадают в порождаемый public-contract.json: машиночитаемое описание поверхности, собранное из каталога, где у каждого действия перечислены политика и класс вызова. Файл руками не правится — его перезаписывает генератор, а гейт краснеет и на новый маршрут без пересборки, и на ручную правку. Так обещание «по умолчанию репетиция» перестаёт быть комментарием в коде и становится полем, которое читает и модель, и приёмка, прежде чем выбрать инструмент.

Обещание доходит даже до русского описания, которое модель читает при выборе: library_manage формулирует обновление как «обновление с правдивым предпросмотром (mode)». А изменяемость связана с правами: отдельная проверка требует, чтобы у каждого изменяющего действия была объявлена область доступа, и свежая рабочая область получает профиль без права на изменение (#710). Вызов без выданной области отвергается кодом PERMISSION_REQUIRED и называет область, профиль и место включения. Предпросмотр по умолчанию здесь — не вежливость к ассистенту, а свойство самой поверхности, которое проверяется раньше, чем вызов дойдёт до изменяющего ядра.