Skip to content

Трастовые тиры конфигурации

Реализация: scripts/config_trust.py. SPEC: sec-config-trust-tiers (SEC, v1). Задача: l26-config-trust-tiers.

Проблема

.tausik/config.json — обычный файл внутри репозитория, и в нём жили выключатели принуждения:

КлючЧто выключал
qg0.scope_hard_gateзапрет правок вне заявленного scope задачи
risk.l3_block_on_highтребование внешнего L3-ревью при закрытии с тонким доказательством
task_done.auto_verifyзакрытие задачи inline-прогоном в обход подписанного чека
gates.<имя>.enabledсам гейт
gates.<имя>.severityпонижение blockwarn
gates.<имя>.triggerотвязка от событий: гейт включён и не срабатывает никогда
gates.<имя>.file_extensionsсужение фильтра входов до непопадания

Одной записи в отслеживаемый git-ом файл хватало, чтобы агент снял с себя надзор. Та же премиса уже была зафиксирована в дереве точечно — bootstrap_opencode.py: «.tausik/config.json едет вместе с репозиторием, поэтому переопределение НЕДОВЕРЕННОЕ». Этот модуль обобщает её.

Правило

Конфиг читается из трёх тиров, от наименее доверенного к наиболее:

ТирГдеЕдет с репозиторием
project.tausik/config.jsonда
user~/.tausik/config.jsonнет
managed$TAUSIK_MANAGED_CONFIGнет

Старшие тиры выигрывают при слиянии. Сверх этого: охраняемый ключ из project-тира применяется, только если он не слабее того, что уже установили доверенные тиры (а если они молчат — фреймворковый дефолт).

  • Ужесточение из project-тира проходит без изменений. Живой пример из конфига этого репозитория: gates.mypy.enabled: true при дефолте false — остаётся.
  • Ослабление отбрасывается, а на его место подставляется применённое значение. Не удаляется: удаление работало бы только потому, что каждый потребитель случайно подставляет строгий дефолт — неявная связность, которая ломается от первого cfg["gates"]["x"]["enabled"] без дефолта.
  • Ужесточение выигрывает и у доверенного тира. Если оператор в ~/.tausik/config.json просто повторил дефолт (mypy.enabled: false), а проект ставит true, побеждает true: правило «project может только ужесточать» действует и в эту сторону, иначе одобренное политикой ужесточение молча терялось бы при слиянии. На НЕохраняемых ключах обычный приоритет тиров сохраняется.
  • Отбрасывание не молчаливое: каждое отклонение уходит в лог и в tausik doctor.
$ tausik doctor
  WARN  Config trust tier    qg0.scope_hard_gate: project value False rejected
                             (project scope may only tighten scope hard gate
                             (blocks edits outside a task's declared scope));
                             True applied

Что охраняется, а что нет

Охраняются семь ключей из таблицы выше. Критерий отбора сформулирован явно: ключ охраняется, только если он ОТКЛЮЧАЕТ надзор — не сужает его область и не настраивает его параметр.

Сознательно вне периметра (решение #137):

  • gates.filesize.exempt_files — сужает область гейта. Легитимные значения (сгенерированные каталоги, дампы исследований) по природе специфичны для проекта, и осмысленного дома в машинном user-тире у них нет. Отдельно разбирается в l26-filesize-gate-revisit.

    С задачи filesize-mro-exempt-mcp сужение приходит из ДВУХ источников, и оба надо держать в голове: gitignored project-scope config.json (как раньше) и закоммиченный tausik/gates.json, который читает gate_filesize._resolve_exempt_files. Второй источник едет с веткой, то есть переживает свежий клон и виден в code review — это усиление, а не ослабление вывода выше. Две особенности механизма стоит знать до того, как менять исключения: (1) закоммиченный конфиг ОБЪЕДИНЯЕТСЯ поверх хардкодных умолчаний, поэтому из него можно исключение ДОБАВИТЬ, но нельзя УБРАТЬ — снятие требует осознанной правки исходника; (2) поиск tausik/gates.json останавливается на границе .git, чтобы файл постороннего предка (родителя монорепо, забытого клона) не расширил молча то, что обходит лимит в ЭТОМ проекте.

  • verify_cache_ttl_seconds — параметр, а не выключатель. Настоящий контроль свежести кэша это files_hash: кэш инвалидируется при изменении файлов, а у разных задач разные relevant_files, значит разные хеши. TTL ограничивает только неизменившееся дерево, где получасовой green — свидетельство о том же самом дереве. Легитимное значение определяется длительностью тест-сьюты конкретного проекта.

Выключить гейт можно другим написанием

Охрана одного enabled была бы декоративной. Гейт, оставшийся включённым, но потерявший все триггеры, не срабатывает никогда; гейт с сужённым до непопадания file_extensions не получает входов. Оба — то же «выключено», записанное иначе. Поэтому trigger и file_extensions охраняются как МНОЖЕСТВА: проект может их расширять, но не сокращать. Значение не-списком там, где ожидается список, тоже отклоняется — это не читаемое ужесточение.

Команды гейтов

_validate_custom_gate исторически вызывался только для гейтов, отсутствующих в DEFAULT_GATES. Значит, переопределение gates.ruff.command не проходило проверку по списку разрешённых исполняемых файлов вовсе — клон репозитория мог направить встроенный гейт на произвольный бинарник, и безшелловый раннер его запускал. Теперь валидируется любая команда из переопределения; отклонённая откатывается к дефолту, остальные ключи переопределения продолжают действовать.

Остаётся открытым: команду можно свести к РАЗРЕШЁННОЙ, но инертной (gates.ruff.command = "python -c pass"python в списке, гейт формально работает и всегда зелёный). Вектор заведён как l26-gate-command-neutering.

Поверхность угроз — честные границы

Закрыто: репозиторий не может выдать сам себе полномочия..tausik/config.json приезжает с каждым клоном, форком и PR; ~/.tausik/config.json — нет. Участник больше не может прислать ветку, которая тихо выключит гейты на машине ревьюера, а агент, правящий конфиг проекта, больше не может понизить собственную планку.

Не закрыто, и это записано, а не замолчано: агент, который может выполнять произвольные shell-команды, способен сам записать ~/.tausik/config.json или экспортировать TAUSIK_MANAGED_CONFIG. Тиры — не песочница. Они дают поднятую планку и, что важнее, видимость: ослабление теперь обязано происходить вне репозитория и больше не может спрятаться в диффе, выглядящем как обычная правка конфига. Настоящая изоляция требует точки принуждения, до которой агент не дотягивается вовсе, — это за рамками задачи.

Контракт чтения и записи

Разделение обязательное, а не косметическое:

ФункцияЧто возвращаетКому
load_config()эффективный конфиг (слияние + политика)читателям
load_config_with_rejections()то же + список отклоненийdoctor, интроспекция
load_project_config()сырой project-слойписателям

save_config() сохраняет то, что ему дали. Писатель, прочитавший эффективный конфиг, скопировал бы настройки пользователя и оператора в файл репозитория. Все round-trip-писатели (ProjectService.gate_enable/gate_disable, MCP _handle_gate_toggle, brain _ConfigOps) читают сырой слой.

Поведение gates disable

Отключение охраняемого гейта из project-тира записывает ключ, но не меняет поведение. Рапортовать успех здесь было бы ровно тем тихим враньём, ради устранения которого задача и делалась, поэтому вызывающему говорят правду:

$ tausik gates disable filesize
Gate 'filesize' NOT disabled — project scope may only tighten gate on/off
switch. The key was written to .tausik/config.json but the effective config
keeps True. To disable it for real, set it in the user tier
(~/.tausik/config.json) or in $TAUSIK_MANAGED_CONFIG.

Переменные окружения

ПеременнаяНазначение
TAUSIK_MANAGED_CONFIGпуть к managed-тиру; не задана — тир отсутствует
TAUSIK_USER_CONFIGпереопределение пути user-тира (тесты, машины с несколькими аккаунтами)

Миграция существующих проектов

Проект, отключавший гейт в .tausik/config.json, после обновления обнаружит гейт включённым, а ключ — названным в tausik doctor. Это намеренно. Если отключение легитимно (песочница, CI-образ, неприменимый к проекту гейт), перенесите его в ~/.tausik/config.json либо в файл из $TAUSIK_MANAGED_CONFIG. Ужесточение из project-тира работает без изменений.

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