| 1 | # ADR 0149: Точечные привязки `settings.toml` к переменным окружения (`*_env`) |
| 2 | |
| 3 | **Статус:** Accepted · Implemented |
| 4 | **Дата:** 2026-05-24 |
| 5 | |
| 6 | ## Связанные ADR |
| 7 | |
| 8 | | ADR | Роль | |
| 9 | |-----|------| |
| 10 | | [0028](0028-user-settings-toml-localappdata-and-secrets.md) | `settings.toml`, snake_case, секреты отдельно | |
| 11 | | [0040](0040-lsp-launch-line-settings-toml-presets-and-environment.md) | LSP: `executable_env` / `arguments_env` в профилях | |
| 12 | | [0023](0023-environment-readiness-glance.md) | readiness: какие env читаются, без дампа `environ` | |
| 13 | | [0144](0144-intercom-team-transport-cide-sync-and-reference-service.md) | `intercom.transport` | |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Контекст |
| 18 | |
| 19 | Пользовательский `settings.toml` часто копируется между машинами или хранится в общем профиле разработчика. Абсолютные пути к бинарникам (LSP, Intercom service, cursor-agent) не должны быть единственным способом конфигурации. При этом подстановки `$VAR` внутри строк TOML **не** используем — хуже валидировать и показывать в Environment readiness ([0040](0040-lsp-launch-line-settings-toml-presets-and-environment.md) §4). |
| 20 | |
| 21 | Нужен **явный** парный ключ: литерал в TOML + опциональное имя переменной окружения. |
| 22 | |
| 23 | --- |
| 24 | |
| 25 | ## Решение |
| 26 | |
| 27 | ### 1. Соглашение имён |
| 28 | |
| 29 | Для строкового поля `<field>` в TOML добавляется опциональный ключ **`<field>_env`** (snake_case), в модели C# — **`<Field>Env`** (`JsonPropertyName` через общий snake_case сериализатор). |
| 30 | |
| 31 | Пример: |
| 32 | |
| 33 | ```toml |
| 34 | [intercom.transport] |
| 35 | local_server_path = "tools/intercom-service/IntercomService.exe" |
| 36 | local_server_path_env = "CASCADE_INTERCOM_SERVER_EXE" |
| 37 | ``` |
| 38 | |
| 39 | ### 2. Приоритет резолва (`SettingsEnvResolver`) |
| 40 | |
| 41 | На **чтении** (runtime), не при сохранении файла: |
| 42 | |
| 43 | 1. Если `<field>_env` — sentinel **`PATH`** (регистронезависимо) → для **запуска процесса** (LSP `executable`, ACP `cursor_acp_path`): эффективный путь **пустой**; дальше пресет + поиск команды в **PATH** ([0040](0040-lsp-launch-line-settings-toml-presets-and-environment.md)). Это **не** чтение переменной окружения `PATH`. |
| 44 | 2. Иначе, если `<field>_env` — **имя переменной** и `GetEnvironmentVariable` возвращает непустую строку → использовать её. |
| 45 | 3. Иначе → литерал `<field>` из TOML. |
| 46 | 4. Дальше по домену — пресеты LSP, legacy defaults, fallback intercom-service и т.д. |
| 47 | |
| 48 | Пустой `*_env` = привязки нет (для LSP с пресетом эквивалентно п.1–3 с пустым `executable` и поиском в PATH). Пустая **именованная** переменная **не** перекрывает литерал. |
| 49 | |
| 50 | `Environment.ExpandEnvironmentVariables` для `%VAR%` внутри литерала **не** смешиваем с `*_env`; при необходимости — только в отдельных резолверах путей (как MCP JSON path). |
| 51 | |
| 52 | ### 3. Первая волна полей (v1) |
| 53 | |
| 54 | | Секция | Поле | `*_env` | Типичное значение | |
| 55 | |--------|------|---------|-------------------| |
| 56 | | `[languages.*.*]` профиль | `executable` | `executable_env` | **`PATH`** (дефолт для OmniSharp/Marksman) или имя переменной с абсолютным путём | |
| 57 | | то же | `arguments` | `arguments_env` | имя переменной (sentinel `PATH` не используется) | |
| 58 | | `[intercom.transport]` | `local_server_path` | `local_server_path_env` | `CASCADE_INTERCOM_SERVER_EXE` | |
| 59 | | `[intercom.transport]` | `base_url` | `base_url_env` | `CASCADE_INTERCOM_BASE_URL` | |
| 60 | | `[ai.acp]` | `cursor_acp_path` | `cursor_acp_path_env` | `CURSOR_ACP_AGENT_PATH` | |
| 61 | | `[agent_notes]` | `config_path` | `config_path_env` | `CASCADE_AGENT_NOTES_CONFIG` | |
| 62 | | `[agent_notes]` | `kb_base_overlay_path` | `kb_base_overlay_path_env` | `CASCADE_KB_BASE_OVERLAY` | |
| 63 | |
| 64 | Секреты (API keys, JWT) — по-прежнему только `*-secrets.toml` / [0028](0028-user-settings-toml-localappdata-and-secrets.md), **не** через `*_env` в основном файле. |
| 65 | |
| 66 | Уже существующие process-wide переменные (`AGENT_NOTES_FILE`, `NETCOREDBG_PATH`) сохраняют приоритет там, где код читает их **напрямую**; `config_path_env` дополняет путь из `[agent_notes]`, не заменяет `AGENT_NOTES_FILE`. |
| 67 | |
| 68 | ### 4. Отклонено |
| 69 | |
| 70 | - `launch_from_environment` (глобальный bool) — заменён точечными `*_env` ([0040](0040-lsp-launch-line-settings-toml-presets-and-environment.md) §3 обновлён). |
| 71 | - Inline-таблица `{ env = "...", fallback = "..." }` — усложняет десериализацию и UI. |
| 72 | - Универсальный `lookup_environment_variable_name` на все поля `CascadeIdeSettings` — вне scope v1. |
| 73 | |
| 74 | --- |
| 75 | |
| 76 | ## Последствия |
| 77 | |
| 78 | - `SettingsEnvResolver` — единая точка резолва. |
| 79 | - `ResolveForRuntime()` LSP, `IntercomTransportSettings` helpers, `AgentNotesRuntimeLoader`, `KbBaseOverlayPathResolver`, `CursorAcpAgentPath` получают уже разрешённые строки или вызывают resolver в месте использования. |
| 80 | - Environment readiness: при заданном `*_env` и пустом env — явная подсказка в LSP-строках (имя переменной без значения). |
| 81 | - Расширение на новые поля — новый ADR-подпункт или дополнение таблицы §3. |
| 82 | |
| 83 | --- |
| 84 | |
| 85 | ## Статус реализации |
| 86 | |
| 87 | | Элемент | Артефакт | |
| 88 | |---------|----------| |
| 89 | | Резолвер | `Services/SettingsEnvResolver.cs` | |
| 90 | | LSP | `LanguageServerLaunchProfile`, [0040](0040-lsp-launch-line-settings-toml-presets-and-environment.md) | |
| 91 | | Intercom / ACP / agent_notes | модели + call sites | |
| 92 | | Тесты | `SettingsEnvResolverTests`, deserialize samples | |
| 93 | | Дефолты | комментарии в `Settings/defaults-settings.toml` | |
| 94 | |