13 KiB
ADR-001: Убрать мёртвый frontmatter model: + валидация имени модели через формат-чек claude-*
Work Item ID: ORCH-074
Эпик: ORCH-052 (слой 3), под-задача ORCH-52a
Связан с: ORCH-041 (каркас resolve_agent_model/resolve_agent_effort), src/config.py, src/agents/launcher.py
Статус
Accepted
Контекст
Каркас выбора модели агентов (ORCH-041) работает корректно: launcher.resolve_agent_model(agent, project_id)
резолвит модель по приоритету project-override → ORCH_AGENT_MODEL_<AGENT> → agent_model_default
→ CLI-дефолт. Все 6 агентов резолвятся в claude-opus-4-8 (через agent_model_default).
Аудит кода (08.06) выявил два дефекта данных/валидации (не дефект механизма):
- P1 — лживый/мёртвый
model:во frontmatter. Все 6 промптов.openclaw/agents/*.mdсодержатmodel:(claude-sonnet-4-6у analyst/developer/tester/deployer,claude-opus-4-7у architect/reviewer). launcher не читает frontmattermodel:— это мёртвая декларация, которая лжёт о реально используемой модели и нарушает принцип «документация = golden source». Мина: если кто-то «починит» launcher читать frontmatter → все агенты молча уедут на устаревшие модели. - P2 — нет валидации имени модели. В отличие от effort (
VALID_EFFORTS-гард вresolve_agent_effort), имя модели не валидируется. Опечатка вagent_model_*/ project-override →--model <мусор>→ CLI падает или тихо деградирует. Нарушение принципа never-break.
Скоуп зафиксирован стейкхолдером (Слава, 08.06): G1 + G2 + опц. G4. G3 routing НЕ включаем
(все 6 агентов остаются claude-opus-4-8). Эффорт не трогаем. rev.2 BRD подтвердила скоуп
без изменений. Код-факт (TRZ §4): agent_fallback_model читается напрямую на launcher.py:374,
минуя resolve_agent_model.
Архитектор должен зафиксировать три решения: (а) форма G1, (б) предикат валидации G2, (в) судьба G4 (fallback) и G3 (routing).
Решение
Решение 1 (G1): убрать model: из frontmatter, НЕ учить launcher его читать
Из YAML-frontmatter всех 6 файлов .openclaw/agents/*.md удаляется только строка model: ….
Ключи name/description/tools сохраняются; frontmatter остаётся валидным YAML и описательным.
config (agent_model_* / agent_model_default) остаётся единственным источником правды о модели.
Отвергнутая альтернатива — научить launcher читать frontmatter model: — отвергнута: она вводит
второй источник правды (frontmatter ⊕ config), усложняет резолв, и моментально активировала бы
устаревшие значения (sonnet-4-6 / opus-4-7) для всех агентов. «Убрать» проще, безопаснее и
устраняет мину раз и навсегда.
Решение 2 (G2): предикат валидации — формат-чек claude-*, оформленный отдельным helper
Добавляется чистый helper is_valid_model(name: str) -> bool рядом с VALID_EFFORTS в
src/agents/launcher.py. Предикат — формат-чек, а не allowlist имён:
strip → непустая строка → соответствует ^claude-[a-z0-9.-]+$
То есть: имя после strip() непусто, начинается с claude- и состоит только из строчных
букв/цифр/точек/дефисов. Регэксп оформляется модульной константой (напр. _MODEL_NAME_RE).
Почему формат-чек, а не allowlist VALID_MODELS:
allowlist (по образцу VALID_EFFORTS) воссоздаёт ровно ту мину, которую мы убиваем в G1 — статичный
список имён, который врёт при устаревании. Когда Anthropic выпустит claude-opus-4-9, оператор,
корректно прописавший новую модель, получит её молчаливый дроп на устаревший default (never-break
сработает против пользователя). Это хуже, чем пропустить структурно-корректное, но опечатанное имя:
финальный авторитет о существовании модели — сам Claude CLI, а не наш код. Формат-чек
forward-compatible (новые версии проходят без правки кода) и ловит реальные классы отказов:
чужой провайдер (gpt-4), пустая строка/пробелы, мусор с недопустимыми символами, неверный префикс
(claud-opus-typo). Признанное ограничение: формат-чек НЕ ловит опечатку, которая всё ещё выглядит
как валидное claude-имя (claude-opus-typo) — такие отсекает CLI на запуске (контракт never-break
- exit-code обработка в
_monitor_agentэто покрывают). Задача валидатора — не быть реестром моделей, а не дать структурному мусору уехать в--model.
Применение (контракт never-break):
- В
resolve_agent_model: резолвенное имя валидируется перед возвратом. Невалидное →logger.warning(...)+ откат на следующий валидный уровень. Реализация: helper применяется внутри каскада приоритетов так, что невалидный уровень пропускается (project-override невалиден → пробуем env → default), а если итог всё равно невалиден → возврат""(без флага--model, CLI-дефолт). Никогда не возвращается мусор и никогда не бросается исключение. - Контракт уровней резолва ORCH-041 сохраняется: валидация добавляется поверх, порядок приоритетов
и сигнатуры не меняются. Все ныне используемые валидные имена (
claude-opus-4-8, валидный enduro per-project override) проходят без изменения поведения. - Поведенческая аналогия с
resolve_agent_effort(VALID_EFFORTS): валидный → как есть, невалидный → лог + дроп. Разница только в форме предиката (формат-чек vs множество) по причинам выше.
Решение 3 (G4): fallback НЕ включаем; но валидатор применяем к точке чтения fallback
agent_fallback_model остаётся "" (флаг --fallback-model не прокидывается). AC-5 помечается
N/A. Обоснование отказа:
- G3 выключен ради детерминизма: все агенты на
claude-opus-4-8. Fallback вернул бы скрытую вариативность модели под нагрузкой (агент молча отработал бы на другой модели) — это противоречит духу зафиксированного скоупа. - Нет наблюдаемой проблемы доступности, мотивирующей fallback. Принцип минимального изменения.
- Self-hosting: новое рантайм-поведение под нагрузкой трудно наблюдать; не вводим без нужды.
При этом helper is_valid_model применяется ТАКЖЕ на месте чтения fallback (launcher.py:374,
fb = settings.agent_fallback_model) — независимо от того, что значение сейчас пустое. Причина —
код-факт TRZ §4: fallback читается напрямую, мимо resolve_agent_model, поэтому валидация только
внутри резолва его НЕ покрывает. Защитный гард на месте чтения навсегда закрывает дыру never-break:
если кто-то позже задаст ORCH_AGENT_FALLBACK_MODEL с опечаткой, мусор будет залогирован и
сброшен (fb_flag = ""), а не уедет в --fallback-model. Для текущего пустого значения регрессии нет:
is_valid_model("") == False → fb_flag = "" — то же поведение, что и сейчас (if fb). Это делает
TC-11 проверяемым (мусорный fallback дропается) при выключенном G4.
Решение 4 (G3): routing НЕ включаем
Подтверждается отказ от model-routing как осознанное решение стейкхолдера (Слава, 08.06). Все 6
агентов резолвятся в claude-opus-4-8. AC-4 = N/A.
Размещение и форма (для разработчика)
is_valid_model(name)+_MODEL_NAME_RE— вsrc/agents/launcher.pyрядом сVALID_EFFORTS(один валидатор, два места вызова: резолв модели и чтение fallback — оба в этом модуле, без кросс-модульного импорта).- Префикс
claude-хардкодится в launcher: оркестратор привязан к Claude CLI (CLAUDE_BIN), конфигурировать предикат не нужно (не over-engineering). Каноничная версия модели по-прежнему живёт ТОЛЬКО вconfig.py::agent_model_default— в launcher версия не хардкодится. - frontmatter: удалить только
model:-строку; не вносить генератор, возвращающий её обратно.
Последствия
Плюсы:
- frontmatter перестаёт лгать; config — единственный источник правды о модели (golden source цел).
- Опечатка/чужой провайдер/мусор в имени модели больше не роняет и не деградирует запуск агента (never-break соблюдён в обеих точках: резолв и fallback).
- Forward-compatible: будущие модели Claude не требуют правки кода (в отличие от allowlist).
- Минимальное изменение: механизм ORCH-041, API, схема БД, структура CLI-команды не меняются.
Минусы / ограничения:
- Формат-чек пропускает структурно-валидную опечатку вида
claude-opus-typo(отсекается CLI на запуске + never-break обработкой exit-code). Принятый компромисс ради forward-compat. - Префикс
claude-зашит — при гипотетической смене CLI-провайдера потребуется правка (приемлемо: оркестратор Claude-специфичен по дизайну).
Не затрагивается:
- API (HTTP) — нет. Схема БД — нет миграций. Стадии/QG — без изменений (это runtime-гард в launcher, не quality-gate). Топология/инфра — без изменений (07/08 артефакты не требуются).
- Эффорт (
agent_effort_*) иVALID_EFFORTS-гард — не трогаются (регрессия покрыта TC-10). - enduro per-project override — валидные имена проходят без изменения поведения (AC-8 / TC-08).
Соответствие принципам
Всё в Docker / один сервер — да. Минимум зависимостей — новых нет. Без ORM/очередей/облака — да. Self-hosting: изменение применяется к БУДУЩИМ запускам агентов, прод-контейнер не перезапускается в рамках задачи; прод-деплой орка — только через staging-гейт (8501) и Plane-статус «Confirm Deploy».