dwe vars — работа с песочницей vars:
vars: — единственный формализованный дом для произвольных, специфичных для
проекта значений в объединённом 3-слойном конфиге (см. workspace.md → Строгий
корень + песочница vars:).
Поскольку теперь каждое пользовательское значение живёт под одним namespace,
dwe vars — это первоклассный инструмент для перечисления, чтения,
редактирования и трассировки этих значений.
Содержание
Заголовок раздела «Содержание»- Модель данных
- Подкоманды
- Зашифрованные значения
- Вывод не маскируется
- Сохраняющая комментарии запись в
local.yml - Статическое сканирование использований
- JSON-вывод
- Поведение в контейнере и
bridge.vars_writable - Связанные команды
Модель данных
Заголовок раздела «Модель данных»Var — это листовой dot-path под vars: — например, vars.db.password.
Вложенные map’ы — это namespace’ы (узлы дерева), а не vars. Для:
vars: db: user: root password: secret app: timeout: 30листья (vars) — это vars.db.user, vars.db.password и vars.app.timeout;
vars.db и vars.app — namespace’ы.
Каждый var разрешается по трём слоям конфига:
| Слой | Источник | Значение |
|---|---|---|
| default | workspace.yml / defaults.yml | Трекаемый, общекомандный дефолт |
| local | workspace/local.yml | Gitignore’нутый, персональный override |
| current | после merge | То, во что реально разрешается ${vars.x} (local побеждает) |
Origin var’а — это наивысший слой, дающий значение, то есть файл, который «выигрывает» merge для этого пути.
Префикс
vars.необязателен. Внутриdwe varsлюбой путь — это var, поэтомуget,set,inspectи фильтр namespace уlistпринимают сокращённую форму:dwe vars get db.hostэквивалентноdwe vars get vars.db.host. Префикс также убирается из вывода (в JSON, автодополнении и хранилище остаётся канонический путь). Голова не изvarsнормализуется внутрь песочницы (project.name→vars.project.name) и никогда не резолвится в реальный конфиг проекта — так что ограничение на чтение и allowlist на запись из контейнера сохраняются.
Подкоманды
Заголовок раздела «Подкоманды»dwe vars list
Заголовок раздела «dwe vars list»dwe vars list [namespace]Плоский список каждого листа vars.* с его эффективным значением и бейджем слоя
(local, когда действует override из local.yml, иначе default).
Необязательный аргумент namespace фильтрует поддерево (например,
dwe vars list db), зеркаля dwe commands list.
dwe vars get
Заголовок раздела «dwe vars get»dwe vars get <var>Вывести одно значение. Листовой путь печатает скаляр; путь-namespace (vars.db)
печатает всё поддерево как YAML. Показывается эффективное (после merge)
значение. Путь, который ни во что не разрешается, — это типизированная ошибка
vars_not_found. Чтение ограничено vars.* — путь, чей первый сегмент не
vars (например, project.name), возвращает vars_not_found, а не
разрешается по остальному конфигу проекта, чтобы доступная из контейнера
поверхность vars не утекала произвольный конфиг хоста.
dwe vars inspect
Заголовок раздела «dwe vars inspect»dwe vars inspect <var>Полная картина по одному var’у:
- Значения по слоям — default (общекомандный), local-override, current (после merge).
- Origin — относительный к проекту файл, выигрывающий merge.
- Каждое статическое использование — каждое место ссылки на var, как
file:lineс текстом совпавшей строки. См. статическое сканирование использований.
Var, который нигде не разрешается и не имеет использований, — это
vars_not_found. Inspect сопоставляет как точный путь, так и префикс namespace:
dwe vars inspect db показывает использования vars.db.host,
vars.db.user и т. д. Как и get, инспекция ограничена vars.* — путь не
из vars даёт vars_not_found.
dwe vars set
Заголовок раздела «dwe vars set»dwe vars set <var> [value]dwe vars set <var> --generate SPEC [--force]Записать override var’а в workspace/local.yml, сохраняя окружающие комментарии
и форматирование (см. сохраняющая комментарии запись).
-
Ограничение пути до
vars.*— любой<var>, чей первый сегмент неvars, отклоняется сvars_path_invalid. Это также граница доверия контейнера (см. поведение в контейнере). -
Приведение значения — аргумент
valueпарсится как один YAML-скаляр. Типизированные скаляры становятся типизированными:true/false→ bool,42→ int,1.5→ float, простой текст → строка. Явное кавычивание оставляет строку:set x '"42"'записывает строку"42". Map’ы ({a: b}) и последовательности ([a]) отклоняются — var это лист. Закреплённые неоднозначные случаи:Ввод Результат ""(пустой аргумент, съеден shell)YAML null '""'(явный пустой литерал)пустая строка null,~YAML null yes/no/on/offстрока (не bool YAML 1.1) 0755,01(ведущий ноль)строка (без потери при octal-реинтерпретации) 1.2.3строка 2024-01-02(голый timestamp)строка -
Без
value(интерактивно) — открывает формуhuhс одним полем ввода и контекстом в стиле inspect (текущие значения по слоям). Submit пишет через тот же путь. В JSON / неинтерактивном режиме опусканиеvalue— это типизированная ошибкаvars_value_required(форма не открывается). -
--generate SPEC(случайное значение) — пишет свежее случайное значение вместо позиционного; см. Генерация случайного значения.
Генерация случайного значения
Заголовок раздела «Генерация случайного значения»--generate заполняет var, которому нужно случайное значение вроде секрета —
ключ приложения, секрет сессии, ключ Fernet, — без однострочника на другом языке:
dwe vars set app.secret_key --generate hex # 64 hex-символаdwe vars set app.fernet_key --generate base64url:32 # валидный ключ Fernetdwe vars set app.instance_id --generate uuidSPEC | Результат |
|---|---|
hex[:N] | N случайных байт в hex нижнего регистра — 2N символов |
base64url[:N] | N случайных байт в URL-safe base64 с паддингом = — 4·⌈N/3⌉ символов |
uuid | UUID версии 4 по RFC 9562 (8-4-4-4-12); N не принимает |
N считает байты энтропии, а не символы вывода, поэтому одинаковый N
даёт одинаковую стойкость в любой кодировке. По умолчанию 32, допустимо целое
от 1 до 1024. Паддинг намеренный: ключ Fernet — это ровно base64url:32, а
Fernet отвергает форму без паддинга. Байты берутся из криптографического
источника случайности операционной системы.
- Всегда строка. Значение минует приведение значения и пишется как YAML-строка, поэтому hex из одних цифр после перезагрузки остаётся строкой.
- Существующие значения сохраняются. Если в
local.ymlпо этому пути уже лежит не-null значение,setотказывает сvars_value_existsи не трогает файл — повторный запуск команды настройки никогда не ротирует ключ, которым приложение уже пользуется. Чтобы сгенерировать заново, передайте--force. Дефолт изworkspace.yml/defaults.ymlили явныйnullвlocal.ymlне мешают. Проверка идёт под проектными локами, так что параллельный writer не вклинится между проверкой и записью. - Ошибки использования — первыми. Позиционное значение вместе с
--generate— этоvars_value_ambiguous; некорректный spec — неизвестный вид, нецелый или вне диапазонаN,Nуuuid, пустой--generate=— этоvars_generate_invalid;--forceбез--generate— этоvars_force_requires_generate. Все три возникают до захвата любого лока. - Без формы.
--generateникогда не открывает интерактивную форму, поэтому одинаково работает в JSON и неинтерактивном режиме.
Сгенерированное значение печатается в подтверждении и в JSON-выводе, как у
любого другого set — dwe vars никогда не маскирует (см.
Вывод не маскируется). Оно также лежит открытым текстом
в gitignore’нутом local.yml. Это подходит для персонального локального ключа;
учётные данные, общие для команды, место в отслеживаемом слое в зашифрованном
виде, через dwe secrets set <vars.path> --stdin (см. secrets.md).
set захватывает проектные локи (симметрия с dwe services enable/disable,
которые делят тот же writer local.yml — set без локов мог бы гонками
пересечься с держащим лок toggle на том же файле). Он не запускает preflight
(это не мутация lifecycle/стека). При любой ошибке после записи прежние байты
local.yml восстанавливаются, затем конфиг перезагружается, чтобы последующий
вывод отражал запись.
dwe vars (без аргументов) — TUI-браузер
Заголовок раздела «dwe vars (без аргументов) — TUI-браузер»Запуск dwe vars без подкоманды открывает интерактивный браузер (тот же виджет,
что и у dwe commands): дерево namespace’ов всех vars, оверлей inspect и
действие edit (Enter на листе).
Редактирование без выхода (терминалы ≥ 80 колонок). Enter открывает форму
set как оверлей поверх браузера — дерево остаётся видимым (затемнённым) под
ней. Введите новое значение (некорректный ввод — map, последовательность,
непреобразуемый скаляр — отклоняется на месте), нажмите Enter для сохранения, и
оверлей закроется: отредактированная строка обновится на месте (новое значение +
бейдж слоя), оверлей inspect отразит новое значение, а в строке статуса на ~2
секунды мигнёт подтверждение ✓ <path> = <value>. esc отменяет и возвращает в
браузер с сохранённым состоянием (курсор, раскрытие, фильтр); ctrl+c завершает
весь браузер. Если блокировки проекта заняты (например, приостановленный
dwe deploy run), сохранение не проходит и в строке статуса мигает ошибка
блокировки — браузер остаётся открытым, а local.yml не изменяется.
Два наблюдаемых поведения отличаются от отдельного dwe vars set:
- Подтверждения — это временная вспышка в строке статуса, а не stdout. После
выхода из браузера в терминале нет следов правок — при выходе ничего не
печатается. (Используйте
dwe vars get <path>или заново откройте браузер, чтобы проверить значение.) - Редактирование без выхода работает только на пути оверлея ≥ 80 колонок. На
более узком терминале браузер использует плоский запасной селектор, который
выходит, чтобы запустить форму
set, и затем открывается заново — старый цикл «выход после коммита», по одной правке за раз.
В неинтерактивном контексте — нет TTY, DWE_NONINTERACTIVE=1 или запуск
внутри контейнера — голая команда откатывается к dwe vars list. Аргумент
namespace (dwe vars vars.db) тоже выводит список, а не браузер.
Зашифрованные значения
Заголовок раздела «Зашифрованные значения»Значение vars.* в отслеживаемом слое может быть зашифрованным маркером
ENC[age:…], а не открытым текстом — именно так общекомандные учётные данные
живут в git. Загрузчик конфига расшифровывает маркеры в памяти, поэтому машина,
на которой есть identity проекта, видит в dwe vars везде обычный открытый
текст и всё написанное ниже к ней не относится. Полная модель — в
secrets.md.
Когда значение не удаётся расшифровать здесь (нет identity, он не тот или
payload повреждён), dwe vars показывает <encrypted> — но никогда шифротекст:
listрендерит<encrypted>вместо значения и добавляет(encrypted)к бейджу слоя. JSON-запись получает"encrypted": true(иначе поле опускается, так что вывод для проекта без секретов побайтно совпадает с прежним). Полеlayerв JSON остаётся чистым (local/default) — суффикс это отображение, аencrypted— машиночитаемый ответ.getна листе печатает<encrypted>и завершается с кодом 0; на namespace он печатает поддерево, рекурсивно заменив каждый нерасшифрованный маркер, — и в тексте, и в JSON.inspectмаскирует каждое послойное поле (default / local / current, в тексте и в JSON), содержащее нерасшифрованный маркер, и добавляет строкуsecret:с источником identity или причиной, по которой его нет.- TUI-браузер показывает тот же плейсхолдер и бейдж, а его форма редактирования никогда не подсовывает маркер как «текущее значение», которое выглядит редактируемым.
Флаг выводится из слоя-origin, а не из «любого слоя»: маркер в
defaults.yml, перекрытый открытым значением в local.yml, отрендерится
открытым текстом, а маркер в local.yml поверх открытого дефолта —
как <encrypted>.
vars set против secrets set
Заголовок раздела «vars set против secrets set»dwe vars set | dwe secrets set | |
|---|---|---|
| Пишет в | workspace/local.yml (в gitignore) | workspace/defaults.yml или workspace.yml (отслеживаются) |
| Хранит как | открытый текст | маркер ENC[age:…] |
| Приведение типов | YAML-скаляр (true → bool, 42 → int) | нет — всегда строка |
Префикс vars. | опционален | обязателен |
| Требует | ничего | recipient проекта |
vars set по-прежнему пишет открытый текст, и открытое значение в local.yml
побеждает зашифрованный дефолт. Когда путь, который вы задаёте, перекрывает
маркер в нижележащем слое, set печатает одну заметку в stderr (подавляется в
JSON-режиме):
note: vars.telegram.token is an encrypted secret in workspace/defaults.yml; this plaintext override wins locallyВывод не маскируется
Заголовок раздела «Вывод не маскируется»dwe vars list, get и inspect печатают эффективные значения дословно —
ничего не маскируется. Некоторые проекты обоснованно держат в
workspace/local.yml / .env настоящие сторонние учётные данные (API-токены,
DSN сервисов), потому что их нельзя подделать локально; такие значения попадают
в вывод целиком. set так же печатает записанное значение, включая созданное
через --generate.
Маскирование не сделано — тот, кто может запустить dwe vars list, и так может
прочитать workspace/local.yml. Что маскирование изменило бы, так это то, куда значения уезжают, и
следить надо именно за этим: перед вставкой в сессию AI-агента, в скриншот или
в демо предпочитайте dwe vars get <path> полному list.
Единственный плейсхолдер в этом выводе — не маскирование: <encrypted>
означает, что значение не удалось расшифровать на этой машине, поэтому печатать
просто нечего. Расшифрованный секрет печатается целиком, как любое другое
значение.
Сохраняющая комментарии запись в local.yml
Заголовок раздела «Сохраняющая комментарии запись в local.yml»dwe vars set пишет workspace/local.yml через round-trip yaml.Node:
загружает файл как дерево узлов, патчит только целевой путь и сериализует
заново. Комментарии и порядок ключей сохраняются; меняется только
отредактированный узел значения. Приведение учитывается на уровне узла —
перезапись закавыченной строки на true/42 выдаёт голый скаляр, чтобы он
перезагрузился типизированным.
Пустые строки не сохраняются: документ перекодируется, а YAML-библиотека
держит пустые строки только внутри блоков комментариев, поэтому правка может
заодно переотступить файл под свой шаг отступа. Для local.yml это допустимо —
это gitignore-состояние разработчика. dwe secrets set / init / rekey пишут
отслеживаемые в git файлы слоёв и поэтому используют другой writer, построчный
splice, который пустые строки сохраняет — см. secrets.md.
Этот writer стоит за dwe vars set, dwe services enable/disable и
мастером настройки, так что комментарии в local.yml переживают любую
правку под управлением DWE. Коллизии map-над-скаляром отклоняются
(чтобы не отбрасывать молча данные разработчика), с единственным
задокументированным исключением — легаси голый-int порт-лист апгрейдится до
map’а {port: N}.
Статическое сканирование использований
Заголовок раздела «Статическое сканирование использований»inspect (и оверлей inspect в браузере) сообщает каждое место, где на var
статически ссылаются. Сканирование field-aware, а не сплошной grep по
файлу — оно обходит каждый конфиг-файл и инспектирует только те поля, которые
runtime реально рендерит, поэтому избегает ложных срабатываний (комментарии,
закавыченные литералы, не рендерящиеся поля) и ложных пропусков (ссылки в
структурных ключах). Отслеживаются два синтаксиса ссылок:
- Шаблонные ссылки
${vars.x}— в полях, рендерящихся движком${...}(декларативные командыcmd/argv/compose_args/argv_append_from/env/with, шаги пайплайнаtimeout/files_gate.command,info.ymltext/value, скалярныйwhen:,docker.ymlproject_name, подтверждающие промпты) и в config-шаблонах рендера подworkspace/templates/config/**(произвольный текст → построчный скан и${vars.x}, и{{ resolve .Raw "vars.x" }}). Это шаблоны, которые материализует подсистема config-рендера; соседние пакеты ide / ai / git используют сырой субстрат{{ }}(без разрешения${...}) и не сканируются. Внутренние пробелы (${ vars.x }) и ведущая цифра не совпадают — сканер переиспользует собственный паттерн рендерера. - Структурные dot-path’ы
vars.x— значенияfrom:/default_from:и ссылки внутри типизированногоwhen.expr(не голый скалярwhen: vars.x).
Сопоставление по точному пути или префиксу namespace: ${vars.db.host}
засчитывается и vars.db.host, и vars.db.
Сам блок vars: верхнего уровня не сканируется: его значения — это данные
конфига, разрешаемые по dot-path и никогда не рендерящиеся повторно, поэтому
${vars.x} или from:, появляющиеся внутри vars:, не являются использованием
во время выполнения.
Оговорка (печатается в выводе): динамически собираемые пути и доступ к полям
через Go-шаблоны (.Vars.x / .Raw.vars.x в шаблонах {{ ... }}) не
отслеживаются. Сканирование покрывает только формы ${...} и
структурный-dot-path.
JSON-вывод
Заголовок раздела «JSON-вывод»Каждая read-only подкоманда поддерживает --output json (с --pretty) и держит
stdout чистым — типизированные ошибки сериализуются в конверт {"error":{…}} на
stderr.
| Команда | Форма |
|---|---|
get | {"var": "...", "value": <any>, "encrypted": <bool>} |
list | {"vars": [{"path": "...", "value": <any>, "layer": "local|default", "encrypted": <bool>}]} |
inspect | {"var": "...", "layers": {"default": <any>, "default_set": <bool>, "local": ..., "local_set": ..., "current": ..., "current_set": ...}, "origin": "...", "encrypted": <bool>, "secret": "...", "usages": [{"file": "...", "line": N, "kind": "...", "text": "..."}]} |
set (со значением или --generate) | {"var": "...", "value": <any>} |
Булевы *_set на слоях inspect отличают явное значение null от
отсутствующего слоя. set без значения в JSON-режиме — это ошибка
vars_value_required (форма не открывается).
Коды ошибок set в конверте:
| Код | Когда |
|---|---|
vars_path_invalid | путь — не лист vars.* |
vars_value_required | значения нет, а формы в этом режиме нет |
vars_value_invalid | значение — не один YAML-скаляр |
vars_value_ambiguous | позиционное значение вместе с --generate |
vars_generate_invalid | некорректный spec --generate |
vars_force_requires_generate | --force без --generate |
vars_value_exists | --generate, в local.yml уже есть значение, --force нет (detail var; hint называет --force) |
vars_not_container_writable | запись из контейнера в var вне bridge.vars_writable |
encrypted и secret помечены omitempty: проект без зашифрованных значений
выдаёт ровно те же поля, что и раньше — см.
Зашифрованные значения.
Поведение в контейнере и bridge.vars_writable
Заголовок раздела «Поведение в контейнере и bridge.vars_writable»dwe vars достижима изнутри bridged-контейнера, но записи запрещены
по умолчанию (deny-by-default):
get/list/inspectдостижимы всегда.setдостижима, но контейнерныйsetуспешен только когда целевой var совпадает с записью в allowlist’е проектаbridge.vars_writable. Несовпадающий var отклоняется сvars_not_container_writable. С хостаsetнеограничен.set --generateподчиняется тому же allowlist’у. Allowlist проверяется до отказа из-за существующего значения, поэтому запрещённый var даётvars_not_container_writable, но никогда неvars_value_exists— контейнер ничего не узнаёт о том, задан ли var.- TUI авто-отключается в контейнере (неинтерактивный откат к
list).
Конфиг-блок bridge.vars_writable
Заголовок раздела «Конфиг-блок bridge.vars_writable»bridge.vars_writable — это top-level конфиг-блок, отличный от
per-service блока services.<name>.bridge: в service.yml, который управляет
включением контейнера. Этот top-level блок — это общепроектная политика
контейнерной записи: в какие vars контейнеризованный dwe vars set может писать
в хостовый local.yml.
# workspace.yml (или любой слой — 3-слойный merge)bridge: vars_writable: - vars.db.password # точный путь - vars.app.* # префиксный wildcard (граница по точке)bridge входит в строгий корневой allowlist; он мёржится по трём слоям, как
любой другой формализованный блок. Семантика паттернов использует реальную
границу по точке, а не наивное префиксное совпадение:
- Точный паттерн (
vars.db.password) совпадает только с этим идентичным путём. - Wildcard
vars.x.*совпадает сtargetтолько когдаtargetначинается сbase + ".". Такvars.db.*разрешаетvars.db.host, но запрещаетvars.db,vars.dbx.hostиvars.database.host.
Пустой или отсутствующий список vars_writable означает отсутствие
контейнерных записей — безопасный дефолт. Некорректные паттерны fail closed
(запрет).
Что на самом деле даёт добавление вара в allowlist. Поля шага пайплайна
(cmd:, when.cmd, check.cmd, timeout:) рендерятся через подстановку
${...}, а отрендеренный cmd: уходит в хостовый sh -c как текст
программы — подстановка текстовая, без shell-квотирования (см.
Шаблоны в полях шага). Поэтому вар, который одновременно
доступен на запись из контейнера и используется в команде пайплайна,
позволяет контейнеру выбрать часть хостовой командной строки: значение
x; some-command выполнит some-command на хосте при следующем
dwe deploy run. Это и есть естественная комбинация — контейнер обычно хочет
записать вар именно потому, что его читает пайплайн, — так что считайте
vars_writable делегированием ввода в хостовый shell тому, кто имеет доступ к
контейнеру, и держите в списке вары, которые потребляются как данные (рендер
конфигов, exports.env), а не подставляются в команды.
render config из контейнера
Заголовок раздела «render config из контейнера»Чтобы контейнер мог регенерировать конфиги сервисов после set, dwe render config тоже достижима из контейнера (остальные подкоманды render — env /
ide / ai / git — остаются только хостовыми). Поскольку render config --harvest мутирует хостовое состояние (.dwe/generated.yml), контейнерный
render config --harvest отклоняется с render_harvest_host_only — границу
пересекает только read-only render.
Связанные команды
Заголовок раздела «Связанные команды»dwe vars get <var>— вывести эффективное значение var’аdwe vars list [namespace]— перечислить листьяvars.*dwe vars inspect <var>— значения по слоям, origin и использованияdwe vars set <var> [value]— записать override вlocal.yml(с сохранением комментариев)dwe vars set <var> --generate SPEC [--force]— записать случайное значениеhex/base64url/uuiddwe secrets set <vars.path> [value]— записать зашифрованное значение в отслеживаемый слой (secrets.md)dwe secrets status— сообщает про каждое зашифрованное значение и можно ли его прочитать здесьdwe render env/dwe render config— регенерировать.env/ конфиги сервисов из объединённого конфигаdwe services enable/disable— переключатели сервисов (с сохранением комментариев)