Перейти к основному содержимому

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» — до этой сессии она нигде не была зафиксирована как формальное расхождение.

Решение​

  1. execution_mode заменяет workspace_mode и имеет три значения: container (по умолчанию, привычное поведение — всё внутри Docker), mounted (бывший local — bind-mount, RN-170 продолжает действовать, теперь при условии этого значения) и runner (папка существует только на машине пользователя, без какого-либо bind-mount). Имя меняется, потому что local уже несло двусмысленность с GitProviderName и должно было бы нести ДВЕ несовместимые физические семантики (смонтировано vs. не смонтировано) под одной меткой — execution_mode называет реальную ось, которую решает поле (ГДЕ выполняется команда), а не «где живёт код», на что два новых режима отвечают способами, которые не могут разделять проверку.

  2. RN-170 становится условной при execution_mode = 'mounted'. Для execution_mode = 'runner' создание по-прежнему проверяет ЛЕКСИЧЕСКУЮ часть пути (абсолютный, без .. ни в одном сегменте, вне корня и системных папок, без пересечения с checkout Brabo в обоих направлениях) — ничто из этого не зависит от I/O контейнера, и это продолжает действовать, потому что это та же область, которая авторизует терминал агента (ADR 0055). Меняется ДИСКОВАЯ часть (существует, является папкой, доступна для записи): для mounted она по-прежнему выполняется при создании, внутри контейнера api, как сегодня; для runner эта проверка ОТКЛАДЫВАЕТСЯ — api никак не может подтвердить то, что существует только на машине пользователя. Полномочия выполнить этот access() есть у runner, работающего на настоящем хосте.

  3. Проект с execution_mode = 'runner' рождается с workspace: unverified и повышается до verified, когда первый runner подключается и подтверждает путь на хосте. Тот же дух ожидающего состояния с доказательством через событие, что docs/gates.yml уже использует для gate status: planned + evidencia: event_log — объявленная папка принимается сразу (создание не блокируется в ожидании runner, которого ещё нет), но продукт не утверждает «путь действителен», пока нет доказательства того, что кто-то на нужной машине это подтвердил. Точный МЕХАНИЗМ — имя события, маршрут, который вызывает runner для подтверждения, где хранится состояние, — оставлен на сессию реализации: этот ADR объявляет СУЩЕСТВОВАНИЕ состояния и КРИТЕРИЙ повышения, а не механику.

  4. Следствие, а не отдельный запрос: преобразование 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.ts best-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) — меняется лишь то, где/когда проверяется диск при создании проекта.