Хост-бридж
Хост-бридж позволяет выполнять команды dwe изнутри dev-контейнеров — git-хуки (exec dwe commands lint), проектные команды и read-only диагностика работают одинаково на хосте и в терминале контейнера (VS Code Dev Containers и аналоги). DWE монтирует небольшой статический shim-бинарник в контейнеры bridge-включённых сервисов под именем dwe; shim пересылает каждый вызов хостовому демону, который запускает настоящий dwe на хосте и транслирует вывод и код выхода обратно.
Бридж по умолчанию выключен для всех типов сервисов — сервис подключается явно через bridge.enabled: true. Всё ниже — справочные детали: схема bridge:, транспорты, политика команд внутри контейнера, генерируемый compose-оверлей, жизненный цикл демона и устранение неполадок.
Содержание
Заголовок раздела «Содержание»- Как это работает
- Включение бриджа:
bridge:в service.yml - Транспорты
- Политика команд внутри контейнеров
- Генерируемый compose-оверлей
- Содержимое
.dwe/bridge/ - Жизненный цикл демона
- Подкоманды
dwe bridge - Контракт окружения
- Ограничения
- Устранение неполадок
- См. также
Как это работает
Заголовок раздела «Как это работает»Суть бриджа проста: внутри bridge-включённого контейнера можно просто запускать dwe — набрав в шелле, из git-хука или из команды проекта — и он ведёт себя так же, как на хосте. Это обеспечивают два процесса: крошечный shim подменяет dwe внутри контейнера, а хостовый демон запускает настоящий dwe. Контейнер никогда не исполняет логику DWE — он только пересылает.
flowchart LR
subgraph container["dev-контейнер (bridge-включённый сервис)"]
direction TB
Caller["dwe <команда><br/>шелл · git-хук · команда проекта"]
Shim["shim dwe<br/>статический бинарник ~3 МБ"]
Caller --> Shim
end
subgraph host["хост"]
direction TB
Daemon["демон бриджа<br/>stateless-форвардер"]
RealDwe["настоящий dwe<br/>форк на соединение"]
Daemon --> RealDwe
end
Shim -->|"unix-сокет — нативный Linux"| Daemon
Shim -.->|"TCP + токен — Docker Desktop / OrbStack / WSL2"| Daemon
Любой вызов транслируется из конца в конец и возвращает хостовый код выхода:
sequenceDiagram participant H as вызывающий в контейнере participant S as shim dwe participant D as хостовый демон participant R as настоящий dwe (хост) H->>S: dwe … Note over S: выбор транспорта —<br/>unix-сокет, иначе TCP + токен S->>D: команда + argv + cwd + env Note over D: трансляция cwd в хостовый путь,<br/>strip/force env, политика команд D->>R: форк "dwe …" R-->>D: stdout / stderr транслируются D-->>S: поток вывода + код выхода S-->>H: тот же код выхода
- Shim — статический Linux-бинарник ~3 МБ (сборки amd64 и arm64 встроены в хостовый
dweи материализуются в.dwe/bridge/). Он не несёт бизнес-логики: выбирает транспорт, отправляет команду, прокачивает stdio и завершается с хостовым кодом выхода. - Демон (
dwe bridge daemon, скрытая команда) — stateless-форвардер: одно соединение на команду, форкdwe <argv>на соединение, без модели проекта, без кэша. Он запускается и останавливается автоматически lifecycle-командами. - Рабочая директория shim транслируется из контейнерного пути в хостовый (см. Контракт окружения), поэтому относительные пути в хуках и проектных командах просто работают.
Включение бриджа: bridge: в service.yml
Заголовок раздела «Включение бриджа: bridge: в service.yml»Опт-ин на уровне сервиса в workspace/services/<name>/service.yml:
type: appdir: ./services/maindir_internal: /var/www/htmlbridge: enabled: true # по умолчанию: false — бридж строго опт-ин # shim_path: /usr/local/bin/dwe # переопределение точки монтирования (коллизия с базовым образом) # on_unreachable: fail # fail | warn — политика shim при недоступном демоне| Поле | По умолчанию | Значение |
|---|---|---|
enabled | false (для всех типов) | монтировать ли shim-бинарник и bridge-маунты в контейнер этого сервиса |
shim_path | /usr/local/bin/dwe | абсолютный контейнерный путь, по которому монтируется shim; переопределите, если базовый образ уже содержит файл по этому пути |
on_unreachable | fail | fail — shim печатает ошибку и выходит с кодом 1, когда хостовый демон недоступен (хук блокирует коммит); warn — печатает предупреждение и выходит с кодом 0 |
bridge.enabled — тристейт и наследуется через extends: сервиса так же, как render.git.enabled: явное значение в потомке побеждает, незаданное наследуется от родителя, а дефолт «выключено» применяется только когда не задано нигде. shim_path и on_unreachable наследуются, когда у потомка они пусты.
Bridge-включённому сервису следует объявить пару dir / dir_internal — именно по ней shim транслирует рабочие директории. Без неё трансляции рабочих директорий нет, и каждый вызов из контейнера выполняется из корня проекта, а не из текущей директории (dwe validate предупреждает об этом).
Транспорты
Заголовок раздела «Транспорты»Shim и демон всегда поддерживают оба транспорта; детекции платформы нет ни на одной стороне.
| Платформа | Рабочий транспорт | Аутентификация |
|---|---|---|
| Linux (нативный Docker) | unix-сокет .dwe/bridge/host.sock | uid пира должен совпадать с uid демона (SO_PEERCRED) |
| Linux rootless | unix-сокет (host-gateway там сломан) | uid пира |
| Docker Desktop (macOS / Windows) | TCP host.docker.internal:<port> (Desktop проксирует на loopback-листенер) | 256-битный пер-проектный токен |
| OrbStack / Colima | TCP | токен |
| WSL2 (dwe внутри дистрибутива) | TCP | токен |
Выбор, на каждый вызов:
- Если в примонтированной bridge-директории есть
host.sock— подключение к нему с таймаутом 300 мс. На Docker Desktop и подобных рантаймах инода сокета видна, но мертва (file sharing не пробрасываетconnect()через границу ВМ), поэтому отказ мгновенный — так и задумано. - Фолбэк на TCP: чтение файлов
portиtokenиз bridge-директории (с короткими повторами — рестарт демона оставляет небольшое окно) и подключение кhost.docker.internal:<port>с предъявлением токена. - Оба не сработали → применяется политика недоступности.
flowchart TD
Start["shim'у нужен демон"] --> Sock{"host.sock есть<br/>и подключается за 300 мс?"}
Sock -->|да| Unix["unix-сокет<br/>auth: peer uid (SO_PEERCRED)"]
Sock -->|"нет / мёртвая инода"| TCP{"файлы port + token<br/>читаются?"}
TCP -->|да| Conn["TCP к host.docker.internal:port<br/>auth: 256-битный токен"]
TCP -->|нет| Unreach["политика недоступности<br/>fail → exit 1 · warn → exit 0"]
Демон всегда слушает unix-сокет и 127.0.0.1 с эфемерным портом, выданным ОС (коллизии портов невозможны, нет экспозиции в LAN — никогда 0.0.0.0). На нативном Linux он дополнительно биндит IP docker-шлюза (обычно 172.17.0.1), чтобы контейнеры могли его достичь; генерируемый оверлей добавляет extra_hosts: host.docker.internal:host-gateway каждому bridged-сервису — это обязательно на Linux и безвредно в остальных случаях. Для экзотических конфигураций адреса прослушивания переопределяются переменной окружения DWE_BRIDGE_BIND (список адресов через запятую или пробелы) при запуске демона. Wildcard-адреса (0.0.0.0, ::) в переопределении отклоняются — привязка ко всем интерфейсам нарушила бы гарантию отсутствия экспозиции в LAN, поэтому такая запись игнорируется с предупреждением, и демон возвращается к loopback по умолчанию.
Изоляция — попроектная: у каждого проекта свои сокет, порт и токен в собственной .dwe/bridge/; контейнер одного проекта, подключившийся к порту другого, не пройдёт аутентификацию, а демон никогда не выполняет команды вне корня своего проекта — внепроектная рабочая директория заменяется корнем проекта.
Политика команд внутри контейнеров
Заголовок раздела «Политика команд внутри контейнеров»Командная поверхность в контейнере сознательно урезана — allowlist, default-deny. Два мотива: dwe stop изнутри контейнера остановил бы контейнер самого вызывающего (сессия терминала/IDE умрёт до получения результата), а токен в скомпрометированном контейнере не должен позволять уничтожать данные.
| Разрешено из контейнера | Почему |
|---|---|
dwe commands <cmd> / dwe cmd <cmd> | основной сценарий — хуки и проектные команды; только команды с opt-in через bridge: |
dwe commands / dwe cmd без аргументов | через бридж нет TTY → печатается вывод commands list (отфильтрованный до bridged-команд) вместо интерактивного браузера |
status, info, logs | read-only диагностика |
docs (включая llms-txt) | read-only; полезно AI-агентам, работающим в devcontainer — dwe docs без аргументов печатает вывод docs list (нет TTY для браузера) |
prompt | сегмент промпта терминала контейнера |
vars (get/list/inspect; set ограничен bridge.vars_writable) | чтение песочницы vars; запись из контейнера запрещена по умолчанию согласно allowlist верхнего уровня bridge.vars_writable |
render config | перегенерация конфиг-файлов после vars set на стороне контейнера (--harvest остаётся только хостовым); прочие подкоманды render (env/ide/ai/git) — только хостовые |
bridge status | самодиагностика бриджа |
version, help | служебные команды |
| Заблокировано | Почему |
|---|---|
stop, restart, reset | суицидально: останавливают / пересоздают контейнер, из которого вызваны |
deploy, run, services | стек управляется с хоста |
snapshot | restore останавливает стек; create тяжёлая и берёт проектные блокировки |
render (кроме render config), setup, init, shell | мутируют файлы воркспейса или интерактивны — выделена только render config (см. выше) |
bridge (всё, кроме status) | bridge stop — суицид для самого бриджа |
validate, completion | хостовые задачи: валидация относится к хостовому воркспейсу, completion-скрипты устанавливаются на хосте (уже запечённый в образ completion-скрипт продолжит молча деградировать — скрытая completion-механика остаётся доступной) |
Per-command opt-in
Заголовок раздела «Per-command opt-in»Внутри разрешённой поверхности dwe cmd / dwe commands есть второй, покомандный гейт: пользовательская команда достижима из контейнера, только если её определение явно включено блоком bridge: (на команде или в заголовке group: её файла) — см. директивы команд § Видимость через bridge. Без него команда остаётся host-only: отфильтрована из контейнерных листингов и completion, а прямой вызов отклоняется ошибкой command_not_bridged с подсказкой, как исправить.
commands: cs.all: type: service_exec cmd: composer cs bridge: enabled: true # доступна из любого bridged-контейнера services: [main] # …или только из этих (ключи сервисов воркспейса)bridge.services сверяется с идентичностью вызывающего контейнера, которую overlay инжектит как DWE_BRIDGE_SERVICE=<ключ сервиса>, а shim пересылает дальше. Идентичность сообщается контейнером и потому advisory — это UX-граница (например, не показывать php-команды в листинге nginx-контейнера), а не граница безопасности; границей безопасности остаётся верхнеуровневый allowlist выше плюс env-хардненинг демона. Матчинг учитывает extends:: сервис, расширяющий другой, наследует права родителя на команды — services: [main] пускает и контейнеры сервисов, расширяющих main (но не наоборот). Исполнение сценариев не гейтится никогда: bridged-сценарий, как обычно, выполняет свои не-bridged подкоманды на хосте.
Механика, которую стоит знать:
- Заблокированная команда завершается ошибкой
bridge_command_blockedс подсказкой запустить на хосте; суицидальные lifecycle-команды дополнительно объясняют почему (например, «она остановила бы контейнер, из которого вызвана»). - Заблокированные команды невидимы в списках
--helpи в shell-комплишене внутри контейнеров — а не «видимы, но падают». - Политика наследуется вложенными вызовами: пользовательская команда
type: dweпорождает дочернийdweс тем же окружением, так что хук, прячущийdwe stopвнутри проектной команды, не сможет убить контейнер. - Каждая bridged-команда выполняется неинтерактивно (плоский прогресс-вывод, без промптов). Интерактивные TUI заблокированы полностью; сам протокол бриджа никогда не выделяет псевдотерминал. Цвета при этом сохраняются: когда stdout у shim — терминал, он пересылает
CLICOLOR_FORCE=1иCOLORTERM=truecolor, и хостовыйdweоставляет ANSI-цвета и полную хостовую палитру поверх пайпа бриджа (help и длинный вывод на пайпах всегда рендерятся тёмной палитрой — тот же дефолт, что у lipgloss). А когда stdin И stdout контейнера — терминалы, shim добавляетDWE_BRIDGE_STDIN_TTY=1, и хостовые раннеры дают детям пользовательских команд локальный PTY —docker compose execтогда выделяет TTY в контейнере, и isatty-инструменты (phpcs, PHPUnit, ripgrep, …) красятся ровно как при запуске на хосте, со stderr, слитым в stdout, как в любой терминальной сессии.NO_COLORв контейнере отключает это; пайп вывода shim (dwe cmd foo | grep …илиcat dump.sql | dwe cmd db.import) автоматически остаётся плоским и без PTY.
Генерируемый compose-оверлей
Заголовок раздела «Генерируемый compose-оверлей».dwe/compose.bridge.yml — машинное состояние, принадлежащее dwe — та же модель владения, что у генерируемого .env и отрендеренных конфигов сервисов. Каждая команда, выполняющая compose up (dwe deploy run, dwe run, dwe services … --apply, рестарт всего стека), атомарно регенерирует его перед стартом стека; когда ни один включённый сервис не имеет включённого бриджа, файл удаляется. Ручные правки не поддерживаются и перезаписываются — об этом сказано в заголовке файла.
# GENERATED by dwe — do not edit; customize via workspace/local.ymlservices: app-main: volumes: - type: bind source: /Users/foo/projects/my-proj/.dwe/bridge target: /dwe-bridge read_only: true - type: bind source: /Users/foo/projects/my-proj/.dwe/bridge/shim-linux-arm64 target: /usr/local/bin/dwe read_only: true environment: DWE_BRIDGE_DIR: /dwe-bridge DWE_HOST_WORKSPACE: /Users/foo/projects/my-proj/services/main DWE_CONTAINER_WORKSPACE: /var/www/html DWE_BRIDGE_PROJECT: my-proj DWE_BRIDGE_SERVICE: main extra_hosts: - host.docker.internal:host-gateway- Позиция в цепочке: оверлей стоит после собственных compose-файлов проекта и до оверлеев
workspace/local.yml— заlocal.ymlостаётся последнее слово поверх всего, что задаёт бридж (в compose побеждает более поздний файл), так что попроектные/персональные кастомизации идут черезlocal.yml, а не через правку генерируемого файла. - Самовосстановление: файл всегда содержит ровно текущие включённые и bridge-включённые сервисы, поэтому отключение сервиса не может оставить устаревший фрагмент, ломающий
compose up. Перемещённая директория проекта, сменившаяся архитектура образа и отредактированные настройкиbridge:подхватываются при следующем старте. - Архитектура shim выбирается на сервис при каждой регенерации по архитектуре образа (
docker inspectпо контейнеру<compose-имя проекта>-<container>с учётомproject_nameизdocker.yml); когда контейнер ещё не существует (первый деплой, после reset), молча используется архитектура хоста (решение фиксируется строкой при-v), а любая другая ошибка резолва даёт тот же фолбэк с предупреждением — оба случая самовосстанавливаются при следующей регенерации. - TCP-порт и токен сознательно не входят в оверлей — shim читает их из примонтированных файлов
/dwe-bridge, поэтому рестарт демона никогда не требует регенерации оверлея или пересоздания контейнеров. - Все маунты read-only; подключение к unix-сокету работает на read-only bind-маунте (тот же механизм, благодаря которому работает
docker.sock:ro).
Содержимое .dwe/bridge/
Заголовок раздела «Содержимое .dwe/bridge/»Runtime-директория бриджа на хосте (.dwe/ в gitignore):
| Файл | Кто пишет | Назначение |
|---|---|---|
host.sock | демон, при старте | unix-сокет-транспорт (нативный Linux) |
port | демон, после bind | фактический эфемерный TCP-порт |
token | демон, при первом старте (режим 0600) | 256-битный токен аутентификации; стабильная идентичность проекта, переживает рестарты демона |
daemon.pid | демон | PID + держатель flock — проба живости |
daemon.log | демон | stderr демона (append-only; без ротации) |
shim-linux-amd64, shim-linux-arm64 | хостовый dwe, при каждом prepare | материализованные shim-бинарники (источники bind-маунтов) |
Сам оверлей живёт уровнем выше — .dwe/compose.bridge.yml.
Жизненный цикл демона
Заголовок раздела «Жизненный цикл демона»Демон управляется автоматически — каждая lifecycle-команда, утверждающая «стек жив», сначала обеспечивает его наличие:
| Команда | Действие с демоном |
|---|---|
dwe deploy run | cycle (стоп → старт) — гарантирует, что демон не от старой сборки dwe |
dwe run | ensure (запустить, если не работает) |
dwe restart (весь стек) | cycle |
| `dwe services enable | disable |
dwe status (верхнего уровня) | best-effort ensure |
dwe bridge start | ensure (вручную) |
dwe stop / dwe reset run (весь стек) | стоп |
dwe stop <svc> / reset run --service <svc> / restart <svc> | не трогают |
Демон также автоостанавливается, когда стек действительно погашен: он следит за docker-событиями контейнеров проекта (с poll-фолбэком раз в 60 с) и завершается, когда их не остаётся, удаляя host.sock и port, но сохраняя token. Idle-таймаута сознательно нет — мёртвого демона нельзя оживить изнутри контейнера, поэтому его убийство под активным разработчиком сломало бы хуки.
Когда в проекте нет bridge-включённого сервиса, демон никогда не запускается и оверлей не генерируется.
Подкоманды dwe bridge
Заголовок раздела «Подкоманды dwe bridge»Ручное управление и диагностика; в повседневной работе lifecycle-команды делают их ненужными. Все поддерживают --output json.
| Команда | Поведение |
|---|---|
dwe bridge start | запустить демон (идемпотентно); отказывает с bridge_not_enabled, когда ни у одного включённого сервиса бридж не включён |
dwe bridge stop | SIGTERM демону по pidfile (идемпотентно); dwe в контейнере перестаёт работать до повторного запуска |
dwe bridge status | живость (pid, аптайм), эндпоинты транспортов (путь сокета, TCP-порт), состояние материализации shim; единственная подкоманда bridge, разрешённая изнутри контейнеров |
dwe bridge logs [--tail N] | показать .dwe/bridge/daemon.log (по умолчанию последние 50 строк, --tail 0 = все) |
Контракт окружения
Заголовок раздела «Контракт окружения»Внутри bridged-контейнера оверлей задаёт:
| Переменная | Значение |
|---|---|
DWE_BRIDGE_DIR | точка монтирования хостовой .dwe/bridge в контейнере (/dwe-bridge) |
DWE_HOST_WORKSPACE / DWE_CONTAINER_WORKSPACE | хостовая hub-директория сервиса (<root>/<dir>) и её маунт в контейнере (dir_internal) — пара префиксов, по которой shim переписывает рабочие директории |
DWE_BRIDGE_PROJECT | имя проекта, используется в диагностике shim |
DWE_BRIDGE_SERVICE | ключ сервиса в воркспейсе — идентичность вызывающего контейнера, с которой сверяются покомандные списки bridge.services. Единственная bridge-переменная, которая ПЕРЕСЫЛАЕТСЯ на хост (её потребитель — хостовый dwe); сообщается контейнером, поэтому advisory |
DWE_BRIDGE_UNREACHABLE | присутствует только при on_unreachable: warn |
Shim вычищает их — кроме DWE_BRIDGE_SERVICE — (плюс любые DWE_PROJECT_ROOT*) из пересылаемого окружения, а демон повторно фильтрует тот же набор на приёме. Демон также отбрасывает переменные, способные перехватить запуск процессов, перед форком — семейства динамического загрузчика (LD_*, DYLD_*), хуки запуска оболочки (BASH_ENV, ENV, SHELLOPTS, BASHOPTS), IFS и PATH — и принудительно задаёт PATH равным собственному значению хостового демона, чтобы контейнер не мог подменить бинарники docker/git/sh, которые хостовый dwe вызывает по короткому имени. Переменные хостовой идентичности заменяются так же: контейнерные HOME, USER, LOGNAME, TMPDIR, SSH_AUTH_SOCK и семейства DOCKER_* / COMPOSE_* / XDG_* отбрасываются, вместо них пересылаются собственные значения демона — контейнерный HOME иначе ломал бы резолв docker-контекста на хосте (CLI откатился бы на /var/run/docker.sock, которого нет на маках с Docker Desktop / OrbStack). Затем демон принудительно задаёт форкаемому dwe две контролируемые хостом переменные: DWE_INVOKED_FROM=container (активирует политику команд — присланные клиентом значения отбрасываются, подделать из контейнера нельзя) и DWE_NONINTERACTIVE=1. Полезные нагрузки --output json идентичны в обоих контекстах.
Вектор аргументов команды передаётся без трансляции — переписывается только рабочая директория. Поэтому относительные пути работают везде; абсолютные контейнерные пути в аргументах не разрешатся на хосте (документированное ограничение).
Ограничения
Заголовок раздела «Ограничения»- Никаких интерактивных команд. Бридж никогда не выделяет псевдотерминал; политика блокирует интерактивные команды, а всё остальное работает в том же неинтерактивном режиме, что и в CI.
- Абсолютные контейнерные пути в аргументах не транслируются — используйте относительные (рабочая директория транслируется автоматически).
- Сервисы без пары
dir/dir_internalне получают трансляции рабочих директорий; вызовы из контейнера тогда выполняются из корня проекта, а не из текущей директории.dwe validateэто подсвечивает. - Windows-контейнеры и dwe на стороне Windows-хоста вне области применения; WSL2 с dwe внутри дистрибутива работает как Linux-случай.
Устранение неполадок
Заголовок раздела «Устранение неполадок»«host daemon is not running for project …» — хостовый стек погашен или демон остановлен вручную. Запустите dwe run (или dwe bridge start) на хосте. С дефолтным on_unreachable: fail shim выходит с кодом 1 и хуки блокируют; задайте сервису on_unreachable: warn, чтобы превратить это в предупреждение + код 0 (например, для рекомендательных хуков).
«shim outdated, re-run dwe deploy to refresh shim binaries» — материализованный shim и хостовый dwe говорят на разных версиях протокола (обычно после обновления dwe при работающем стеке). dwe deploy run обновляет shim’ы и перезапускает демон, даже когда самому деплою нечего делать.
Bridged-команда зависает или падает только на нативном Linux — UFW/firewalld могут резать трафик, приходящий из docker-bridge-сети на IP шлюза. Unix-сокет-транспорт не затронут; если нужен TCP — разрешите input с docker-bridge-интерфейса.
Rootless Docker на Linux — host-gateway там сломан, поэтому TCP недоступен; rootless-конфигурации покрывает unix-сокет-путь. Учтите: ремаппинг user namespace может сместить uid пира, который видит демон; если peercred-аутентификация не проходит, запускайте стек не-rootless или сверьтесь с трекером проекта о статусе token-фолбэка на unix-пути.
Bridged-команда выполняется из корня проекта, а не из текущей директории — shim не смог транслировать контейнерный cwd в хостовый путь (транслируется только маппинг dir / dir_internal сервиса), и демон откатился на корень проекта. Типичный триггер: git-хук или скрипт, который сделал cd за пределы контейнерного монтирования перед вызовом dwe (например, хостовая раскладка cd "$(git rev-parse --show-toplevel)/../../.." приводит в /). Команды продолжают работать — фолбэк фиксируется в dwe bridge logs. Хукам cd вообще не нужен: dwe находит проект, поднимаясь вверх из любой директории внутри него.
Базовый образ уже содержит /usr/local/bin/dwe — задайте bridge.shim_path на другой абсолютный путь, выигрывающий в PATH контейнера.
dwe в контейнере падает с «exec format error» сразу после первого старта — архитектура shim резолвится по существующему контейнеру, поэтому самый первый старт эмулируемого образа чужой архитектуры (например, amd64-only образ на arm64-маке) откатывается на архитектуру хоста и монтирует не тот shim. Запустите стек ещё раз: контейнер уже существует, архитектура резолвится по образу, и оверлей самовосстанавливается. Образы родной архитектуры не затронуты.
Куда смотреть — dwe bridge status (живость демона, транспорты, состояние shim), dwe bridge logs (stderr демона, включая паники при старте) и dwe validate (домен bridge проверяет значения on_unreachable, абсолютность shim_path и маппинг воркспейса; dwe validate bridge ограничивает прогон этим доменом).
См. также
Заголовок раздела «См. также»- Конфигурация сервисов — поля
service.yml, наследование черезextends: - Воркспейс и локальные оверлеи —
workspace/local.yml, цепочка compose-файлов - Пользовательские команды — система
dwe commands, которую хуки вызывают через бридж - Валидация — проверки готовности проекта; домен валидации бриджа