ADR 0104 — execution_mode с тремя значениями, и workspace рождается unverified, когда проверяет его runner
- Статус: Принят
- Дата: 2026-08-22
- Контекст: ADR 0072 и 0103 так и не были согласованы между собой
- Пересматривает (не смягчая) область: ADR 0072, ADR 0103
Контекст
Два ADR этого продукта описывают физически несовместимые способы выполнения под ОДНИМ И ТЕМ ЖЕ именем поля, и до сих пор никто их не согласовал.
ADR 0072 создал
projects.workspace_mode ('container'|'local') + workspace_path. В
режиме local RN-170 при СОЗДАНИИ проверяет, что
папка смонтирована через bind-mount внутри контейнеров api И
engine — apps/api/src/infrastructure/filesystem/project-workspaces-root.ts
(validarCaminhoDeWorkspaceLocal) выполняет access(W_OK|X_OK) внутри
процесса контейнера api и отказывает с 400, если не находит папку. Веб-мастер
(apps/web/src/routes/NewProjectWizard.tsx:71,462-468) учит только
редактировать docker/docker-compose.yml строкой bind-mount — другой
инструкции нет.
ADR 0103 создал
apps/runner: CLI, который работает на МАШИНЕ пользователя, вообще без
bind-mount, подключаясь к engine по каналу Phoenix (/runner, топик
terminal:<projectId>), аутентифицированному одноразовым тикетом, выданным
самим engine. Он выполняет команду агента, уже одобренную обычным
pipeline, и открывает настоящий интерактивный PTY-терминал — вкладка Терминал
(apps/web/src/routes/code/TerminalPanel.tsx) уже по-настоящему использует для этого
@xterm/xterm. RN-420, которая решает,
когда направить команду в runner вместо привычного System.cmd, повторно использует
ТО ЖЕ условие workspace_mode == "local" (плюс подключённый runner) —
ADR 0103 так и не создал третье значение поля.
Практический результат: сегодня, чтобы ИСПОЛЬЗОВАТЬ runner — который существует именно для того,
чтобы обойтись без bind-mount, — пользователь всё равно обязан пройти проверку
bind-mount из ADR 0072 только ради создания проекта. Обе половины
продукта ссылаются на один и тот же enum из 2 значений, описывая два способа выполнения, у которых
физически нет ничего общего: папка, смонтированная в оба контейнера, и
папка, существующая только на машине пользователя. Подтверждено прямым чтением
кода: apps/api/src/db/schema.ts:238-240
(projectWorkspaceModeEnum = pgEnum('project_workspace_mode', ['container', 'local'])) и apps/api/src/domain/iam/project.entity.ts
(PROJECT_WORKSPACE_MODES = ['container', 'local'], существующий комментарий которого
уже предупреждает о риске спутать омоним с GitProviderName
'local' — риск, который этот ADR наследует и должен сохранить при переименовании
поля).
Эта находка возникла при исследовании вопроса «чего не хватает продукту, чтобы действительно получить доступ к папке и терминалу пользователя вне рамок Docker» — до этой сессии она нигде не была зафиксирована как формальное расхождение.
Решение
-
execution_modeзаменяетworkspace_modeи имеет три значения:container(по умолчанию, привычное поведение — всё внутри Docker),mounted(бывшийlocal— bind-mount, RN-170 продолжает действовать, теперь при условии этого значения) иrunner(папка существует только на машине пользователя, без какого-либо bind-mount). Имя меняется, потому чтоlocalуже несло двусмысленность сGitProviderNameи должно было бы нести ДВЕ несовместимые физические семантики (смонтировано vs. не смонтировано) под одной меткой —execution_modeназывает реальную ось, которую решает поле (ГДЕ выполняется команда), а не «где живёт код», на что два новых режима отвечают способами, которые не могут разделять проверку. -
RN-170 становится условной при
execution_mode = 'mounted'. Дляexecution_mode = 'runner'создание по-прежнему проверяет ЛЕКСИЧЕСКУЮ часть пути (абсолютный, без..ни в одном сегменте, вне корня и системных папок, без пересечения с checkout Brabo в обоих направлениях) — ничто из этого не зависит от I/O контейнера, и это продолжает действовать, потому что это та же область, которая авторизует терминал агента (ADR 0055). Меняется ДИСКОВАЯ часть (существует, является папкой, доступна для записи): дляmountedона по-прежнему выполняется при создании, внутри контейнера api, как сегодня; дляrunnerэта проверка ОТКЛАДЫВАЕТСЯ — api никак не может подтвердить то, что существует только на машине пользователя. Полномочия выполнить этотaccess()есть у runner, работающего на настоящем хосте. -
Проект с
execution_mode = 'runner'рождается сworkspace: unverifiedи повышается доverified, когда первый runner подключается и подтверждает путь на хосте. Тот же дух ожидающего состояния с доказательством через событие, чтоdocs/gates.ymlуже использует для gatestatus: planned+evidencia: event_log— объявленная папка принимается сразу (создание не блокируется в ожидании runner, которого ещё нет), но продукт не утверждает «путь действителен», пока нет доказательства того, что кто-то на нужной машине это подтвердил. Точный МЕХАНИЗМ — имя события, маршрут, который вызывает runner для подтверждения, где хранится состояние, — оставлен на сессию реализации: этот ADR объявляет СУЩЕСТВОВАНИЕ состояния и КРИТЕРИЙ повышения, а не механику. -
Следствие, а не отдельный запрос: преобразование
execution_modeмежду тремя значениями больше не требует пересоздания проекта. Сегодня режим выбирается только при создании (ADR 0072, пункт 1) — когда разница между значениями сведена к одному столбцу (плюс состояние проверки из пункта 3), это перестаёт быть структурным ограничением. Разрешаются все направления преобразования, без собственных ограничений, объявленных здесь, —container ⇄ mounted ⇄ runner, в любую сторону. Механика каждого перехода (что происходит с worktree/состоянием при смене, есть ли отдельное подтверждение для каждого направления) оставлена на сессию реализации; этот ADR лишь объявляет, что преобразование разрешено в любую сторону.
Порядок поставки того, что отложено
Это решение (пункты 1–3 выше) — P1: без него runner существует, но никто не может до него добраться, не смонтировав сначала папку, без которой он был задуман обходиться. Отложенное, в порядке, который владелец продукта уже определил (полная детализация в backlog.md):
- Распространение runner (
tsup→ единый пакет +npm publish @brabo/runner) — сегодня это"private": trueсbin, указывающим на сырой.ts, доступный только при клонировании всего монорепозитория. - Долгоживущий токен учётной записи (PAT), заменяющий повтор входа
из
apps/runner/src/auth.ts, — должен появиться ДО распространения, потому что публикация сегодня распространила бы поток пароль+cookie, сохранённые на диске. - Эксклюзивность runner по
{project_id, machine_id}, а не только поproject_id— ОТЛОЖЕНА, пока не появится реальный критерий включения (второй разработчик, действительно одновременно работающий в том же проекте). guard.tsbest-effort — это не пробел, который нужно закрыть, см. Последствия.
Последствия
Что болит и не решается этим ADR: ALTER TYPE ... ADD VALUE
имеет транзакционные ограничения в нескольких версиях PostgreSQL (его нельзя
использовать в той же транзакции, в которой упоминается новое значение). Migration,
вводящая третье значение enum, должна явно это учесть
(два шага или пересоздание типа) — зафиксировано здесь как технический риск для
сессии реализации, не решённый в этом документе.
Что ОБЪЯВЛЕНО как инвариант, а не как пробел:
apps/runner/src/guard.ts остаётся и будет оставаться ЛЕКСИЧЕСКОЙ
проверкой best-effort — уязвимой к TOCTOU и к symlink, созданному после
проверки, без sandbox, без отдельного пользователя, без реального технического ограничения. Это
уже было объявлено в ADR 0103; этот ADR явно ПОДТВЕРЖДАЕТ это, потому что
execution_mode = 'runner' перестаёт быть условным бонусом (режим local
- случайно подключённый runner) и становится путём ПЕРВОГО КЛАССА
при создании проекта — а путь первого класса рискует быть
прочитанным тем, кто не следил за ADR 0103, как обещание
изоляции, которой никогда не было. Реальная граница безопасности runner
по-прежнему состоит только из: аутентификации (CLI идентифицируется токеном учётной записи
пользователя) + привычного pipeline одобрения (каждая команда агента
рождается как
proposed_action, включая абсолютные потолки ADR 0102) + согласия пользователя запускать бинарник на своей машине.
Чего этот ADR НЕ делает:
- Не реализует ни строки кода, migration, маршрута или UI — это зафиксированное
решение, сессия реализации выполняет его поверх
origin/dev. - Не проектирует точный механизм состояния
unverified/verified(событие, маршрут, где хранится) — лишь объявляет, что оно существует, и критерий повышения. - Не реализует никакого преобразования между значениями
execution_mode— лишь объявляет, что разрешены все направления. - Не меняет границу внешнего эффекта (RN-106/RN-418):
git push, открытие PR и deploy остаются безусловнымrequire_approval, в области или вне её, при любомexecution_mode. - Не меняет политику области пути и allowlist терминала (ADR 0055) — меняется лишь то, где/когда проверяется диск при создании проекта.