# Конфигурация Конвенция: *как* устроена и грузится конфигурация transcriber (TOML). Правила оформления кода (How), не спецификация поведения. **Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту. Главные: комментариями снабжена половина полей; единого места проверки на старте нет: у секций `[auth]`, `[pipeline]` и `[storage]` свой `Validate()` в точке входа, а пустые ключи `[yandex]` ловит конструктор распознавателя. **Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml` ([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки приезжают из TOML»: наш рабочий код окружение не читает вовсе — читателя `.env` в `cmd/transcriber` сняли 2026-08-23 вместе с зависимостью. Окружение остаётся у границы SDK: `aws-sdk-go-v2` в `internal/adapter/recognizer/yandex/s3.go` зовёт `config.LoadDefaultConfig`, а тот читает `AWS_PROFILE`, `AWS_CA_BUNDLE`, `AWS_ENDPOINT_URL` и `AWS_ENDPOINT_URL_S3` и смотрит `~/.aws/config`. ## Принципы - **Конфигурация — только TOML.** Переменные окружения для конфигурации **не используем**: окружение наследуется дочерними процессами и видно через `/proc//environ` — для секретов это слабее файла под `0600`. - Грузим **один раз при старте** в одну типизированную структуру `Config` (под-структуры по секциям). Дальше по коду читаем только её — чтения файла в прикладном коде нет, только загрузчик `internal/config`. - Конфиг **неизменяем** после старта; смена параметров — перезапуск процесса. ## Файл и поиск - Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочем каталоге** процесса. - Путь переопределяется опцией **`-c path`** или **`--config=path`**. - Образец в репозитории — **`config.example.toml`** (см. ниже); реальный `config.toml` не коммитится. ## config.example.toml — самодокументируемый образец `config.example.toml` коммитим как единый справочник по конфигу: все секции и все поля. **Каждое поле снабжаем комментарием**, из которого ясно: - **зачем** поле — что оно меняет в поведении; - **допустимые значения** — перечисление или границы; - **единицы измерения**, если применимы — секунды, байты, доля `0–1`. ```toml [server] port = # порт HTTP-сервера shutdown_timeout = # ждать мягкой остановки сервера, секунды force_shutdown_timeout = # ждать остановки воркеров, секунды ``` Значения намеренно заменены плейсхолдерами: предмет конвенции — форма комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут одним домом — таблица «Настройки с числовым значением» в [../database.md](../database.md); `config.example.toml` — источник истины по составу полей. Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»). *Расхождение:* перечень доверенных адресов в секции `[auth]` образца заполнен примером — подсетью docker, — а не оставлен пустым: пустое значение не говорит, какой формы значение здесь ждут, а сервис с пустым перечнем не поднимается вовсе. Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным входом. Там же, комментарием под секцией, стоит рецепт локального входа **одним связным блоком**, а не тремя комментариями по месту: правки связаны между собой, и применённая порознь любая из них роняет старт либо оставляет сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение — перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована целиком: образец описывает боевую выкладку, а локальный вход — способ до неё дойти, и два рабочих значения в одном файле читались бы как выбор без указания, какое из них чьё. *Расхождение:* петлевые адреса в рецепте названы **парой** — `127.0.0.1` и `::1`, — а не одним значением, хотя правило секции требует от образца только формы значения. Причина в цене: браузер разрешает `localhost` в IPv6 не реже, чем в IPv4, и перечень без `::1` даёт неузнанный запрос там, где человек ждёт входа. ## Поля по дискриминатору `type` Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из бекендов или внешних сервисов), обязательность и опциональность полей определяет значение `type`, а не фиксированный список секции. - **Проверка — по `type`.** Для каждого поддерживаемого значения свой набор обязательных полей; поля других значений не требуются. Неизвестное значение — ошибка на старте с перечислением поддерживаемых. - **Образец — по `type`.** В `config.example.toml`: - основное (умолчательное) значение **предзаполнено** рабочими значениями; - альтернативные — **блоками-комментариями ниже**, каждый со своим описанием полей (зачем, границы, единицы — как у обычных полей); - так из примера видны все варианты и поля каждого, не открывая код. Дискриминатора в transcriber пока нет; правило записано на случай второго распознавателя. ## Секреты Секреты доставляет **выкладка**, рендеря их прямо в `config.toml` (transcriber: Ansible из `pet-project-server`). Приложение просто читает TOML — отдельного слоя секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не репозиторий и не окружение. - Секретные поля transcriber: `yandex.speech_kit_api_key`, `yandex.object_storage_access_key_id`, `yandex.object_storage_secret_access_key`. Секрет клиента OIDC отсюда ушёл 2026-08-22 вместе с собственным входом. - Отрендеренный `config.toml` (с секретами) **не коммитится**; права `0600`, владелец — пользователь процесса (`1000:1000`). - В `config.example.toml` секретные поля — пустые строки. *Расхождение:* сейчас там стоят подсказки вида `your_..._here`, а не пустые строки, и загрузчик их не отличает от настоящего значения. - Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Проверка и остановка на старте». - В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность». - **Отказ загрузки настроек не несёт содержимого файла.** Текст такого отказа собирает библиотека разбора, и собирает она его из разбираемого куска: `toml.ParseError` кладёт в сообщение само значение («Invalid float value: %q»). Оборванная кавычка в строке секретного ключа — типовая поломка криво отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ разбора пересобирается своими словами: путь, строка, столбец и последний ключ, без сообщения библиотеки. Прочие отказы декодера (несовпадение типов, неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их текст остаётся как есть — иначе за разборчивость отказа платили бы там, где платить не за что. **Файл, способный нести секрет, называется в `.gitignore` и в `.dockerignore`.** Путей наружу у такого файла два, и закрывает их разное: git держит `.gitignore`, а контекст сборки образа — `.dockerignore`, потому что docker `.gitignore` не читает. Сегодня в обоих названы `config.toml` и `.env`. Правило записано прозой и держится чтением: сверка двух списков стала бы проверкой над проверкой, а такие проект не заводит ([../../CLAUDE.md](../../CLAUDE.md), «Запреты»). До 2026-08-23 парность не называл ни один документ, и прогон, снимавший мёртвого читателя `.env`, снял строку с одной стороны — вернуло её ревью. ## Проверка и остановка на старте Конфиг проверяем **на старте, до приёма трафика**. Негодный конфиг — лог `ERROR` и выход с ненулевым кодом, не стартуем наполовину. Что проверяем: - обязательные поля заданы; - каталоги хранилища существуют и доступны на запись; - границы числовых полей соблюдены; - ключи внешних сервисов не пусты. *Расхождение:* `LoadConfig` проверяет только существование файла и разбирает TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс выходит с кодом 1. Единого места проверки нет. Два ключа секции `[telegram]`, стоявшие здесь исключением, ушли вместе с самим входом 2026-08-14: секции больше нет, и своей проверки у неё тоже. **Проверка, охватывающая две секции разом, живёт методом на корневой `Config`.** Такая сегодня одна — `ValidateTestHeaders`: она судит `[server] debug` против `[auth.test_headers]`, и ни в `Validate()` секции сервера, ни в `Validate()` секции входа не помещается — секция начала бы знать о чужой секции. Зовётся она из `cmd/transcriber` рядом с остальными. Имена принимаемых заголовков приходят ей **доводом**, а не читаются из пакета настроек: дом у них один — константы транспорта, — а `internal/config` транспорта не знает и знать не должен, иначе `cmd/devtools`, которому нужен один разбор конфига, линковал бы всю поверхность HTTP. Секции `[auth]`, `[pipeline]` и `[storage]` проверяют себя сами, и проверка стоит на старте: `Validate()` каждой зовётся из `cmd/transcriber` сразу после загрузки и роняет процесс с именем незаполненного ключа. У `[storage]` это ожидание занятой базы и число соединений читающего пула: ноль у первого отдаёт «база занята» первому же воркеру, ноль у второго означает пул без предела — то есть настройку, которой не управляют. Причина в цене умолчания: поднявшись с пустым перечнем доверенных адресов, сервис не узнавал бы никого, а узнать об этом было бы неоткуда — все адреса приложения просто отвечали бы отказом. Сообщение называет **имя ключа**; правило «значения в отказ не идут» остаётся в силе для прочих секций, где секреты есть. ## Структура в коде - Весь разбор и проверка — в `internal/config`; наружу отдаётся готовая `Config`. - Одна корневая структура `Config` с под-структурами по секциям. Перечень секций и полей здесь не повторяем: источник истины по составу — `config.example.toml`, действующие числа — [../database.md](../database.md), «Настройки с числовым значением». Каталог данных задаётся одним ключом `[storage] data_dir` ([ADR](../adr/ADR-2026-08-12-single-data-dir-config-key.md)). - Умолчания задаются в `defaultConfig()`, файл их перекрывает. Новое поле требует правки обоих мест. - **Обязательное поле — поле, у которого умолчания нет намеренно.** Умолчание у такого поля было бы угаданным намерением, и одна из двух ошибок стала бы тихой. Форма записи: умолчания нет ни в `defaultConfig()` (причина — строкой комментария у самого поля), ни по нулевому значению типа; присутствие ключа судит **разбор** — `MetaData.IsDefined` из `toml.DecodeFile`, — потому что значение отличить «не задано» от «задано нулём» не позволяет. В `config.example.toml` у поля стоит значение свежей установки. Первым таким полем был `telegram.enabled`; секция убрана 2026-08-14, и живого примера у правила сейчас нет.