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

ADR 0141 — Единая база монтируемых проектов, смонтированная по тождеству

Контекст​

Режим mounted (прежний local, ADR 0072/RN-169, переименованный ADR 0104/RN-421) оставляет код проекта в папке ПОЛЬЗОВАТЕЛЯ, и эта папка должна быть достижима изнутри ДВУХ контейнеров: api, которая читает и пишет permissions.json и обслуживает вкладку Code, и engine, который монтирует worktree и пишет код. Механизм с самого начала — bind-mount.

Что никогда не было решено — кто создаёт этот bind-mount и когда. Существовала ИНСТРУКЦИЯ: docker/docker-compose.yml содержал в сервисах api и engine большой комментарий, обучающий вручную добавлять по строке на проект —

- /home/voce/projetos/loja:/home/voce/projetos/loja

— а создание проекта отказывало с 400 (RN-170/RN-422), когда папки там не было, с сообщением, повторяющим эту строку. Отказ хорош и продолжает существовать; проблема — в цене лекарства, которому он учит.

Правка compose и перезапуск api + engine НА КАЖДЫЙ СОЗДАННЫЙ ПРОЕКТ. Перезапуск этих двух сервисов убивает каждый идущий ход агента, каждый открытый сокет терминала и каждый вызов LLM в полёте во всей установке — ради подключения проекта. Это было терпимо, пока mounted был запасным выходом для тех, кто уже знал, что делает. Перестаёт быть в момент, когда он становится первоклассным выбором, предлагаемым на экране создания с предложенным путём.

И есть вторая проблема, которую предложение пути делает конкретной. Правило «папка проекта не может перекрываться с checkout Brabo», которое является сердцем ADR 0055, применяется caminhoDeWorkspaceLocalValido, сравнивающей с raizDoBrabo(), — а это process.cwd(), и внутри контейнера api это /workspace. НАСТОЯЩИЙ checkout, на диске разработчика, api узнать не может. Кто склонирует Brabo в $HOME/brabo и направит туда папку проекта, проходит всю существующую проверку, и dev agents начинают выполнять команды внутри дерева самого продукта — ровно тот отказ, ради предотвращения которого существует ADR 0055, входящий через дверь, которую он не охраняет.

Решение​

ЕДИНАЯ база, настраиваемая оператором, смонтированная по ТОЖДЕСТВУ в сервисах api и engine.

# .env
BRABO_PROJECTS_BASE=/home/voce/brabo
# docker/docker-compose.yml — serviços api E engine
- ${BRABO_PROJECTS_BASE:-brabo_projects_base}:${BRABO_PROJECTS_BASE:-/data/brabo-projects-base}

Оператор настраивает один раз, делает docker compose up -d api engine, и больше ничего не редактируется по проекту, никогда. Каждый проект mounted живёт внутри базы.

Приём ${VAR:-nome_de_volume} слева, с абсолютным и инертным значением по умолчанию справа, — ТОТ ЖЕ, что уже используется PROJECT_WORKSPACES_HOST_DIR/GIT_LOCAL_REPOS_HOST_DIR в соседних строках: без переменной левая сторона — имя именованного тома, объявленного в конце файла, строка остаётся валидным Compose, и с хоста ничего не монтируется.

Без переменной api сообщает projectsBase: null (GET /workspaces/:workspaceId/projects-base, maintainer — тот же минимум, что у POST .../projects, потому что это значение существует, чтобы решить, что предлагает тот маршрут), и мастер создания не предлагает режим Смонтированная папка. Никогда не предлагать режим, который установка не соблюдает.

Почему НОВАЯ переменная, а не PROJECT_WORKSPACES_HOST_DIR​

Существует переменная, которая уже заменяет управляемый том настоящей папкой хоста, и соблазн её переиспользовать велик. Есть три причины этого не делать, и у первой есть последствие для данных.

1. Столкновение пространств имён, с git init в чужой папке. Управляемый workspace именуется workspace_dir_name (RN-109), колонкой UNIQUE; смонтированный проект именуется ПОЛЬЗОВАТЕЛЕМ, который вводит путь. Когда оба корня указывают на одно место, <base>/loja и проект container, чей workspace_dir_name — loja, попадают в ОДНУ физическую папку — и Workspace.init_from_bare! сделал бы git init внутри проекта другого человека. Ничто в схеме этому не мешает: уникальность — между значениями workspace_dir_name, никогда не против basename workspace_path, и быть не может, потому что одну сторону выбирает продукт, а другую — тот, кто вводит.

2. Противоположная семантика владельца. /data/project-workspaces принадлежит ПРОДУКТУ: одноразовая, непрозрачная, и именно это делает допустимым удаление проекта container вместе с томом. База принадлежит ПОЛЬЗОВАТЕЛЮ — там живёт его код, открытый в его редакторе, версионируемый им. Слияние обеих заставило бы документацию говорить «эта папка одноразовая и в то же время нет».

3. База навигируема; управляемый корень не должен быть. Обозреватель папок, обслуживаемый api (следующий PR), ограничен базой, чтобы пользователь мог выбрать, где будет жить проект. Слияние обеих раскрыло бы внутренности каждого проекта container установки любому, кто создаёт какой-нибудь проект.

Почему ТОЖДЕСТВО ($X:$X), а не фиксированная точка монтирования​

Фиксированное монтирование — ${BRABO_PROJECTS_BASE}:/data/mounted — было бы короче записать, и оно неверно по двум складывающимся причинам.

Строка projects.workspace_path вводится пользователем и показывается ему обратно (AmbienteDoProjeto). При хост ≠ контейнер продукту пришлось бы выбирать, какую из двух хранить, и любой выбор заставляет экран кому-то лгать: хранение внутреннего пути показало бы папку, которой нет на его машине; хранение внешнего потребовало бы перевода в каждой точке использования.

И именно от этого перевода тождество избавляет. projectScopeRoot (api) и Engine.Actions.Workspace.workspace_dir/2 (engine) различают режим по начальной косой черте пути и используют значение колонки как есть. При хост == контейнер оба остаются корректными без единой новой строки кода — и область терминала из ADR 0055, выводимая из этого же корня, продолжает разрешать ровно ту папку, которую видит пользователь.

Broker получает ВТОРОЙ корень​

BRABO_PROJECTS_HOST_BASE входит в сервис broker, выводясь из BRABO_PROJECTS_BASE (которая по определению уже путь хоста — именно её api/engine монтируют по тождеству), рядом с PROJECT_WORKSPACES_HOST_ROOT и по той же причине, что в ADR 0130: -v у docker run разрешается ДЕМОНОМ, против файловой системы хоста, и путь, пришедший изнутри api, смонтировал бы пустую папку. Изоляция по-прежнему настраивается в broker, никогда не утверждается вызывающим — это и есть настоящий инвариант того ADR.

Эту переменную пока ничто не потребляет: разрешать проект mounted против этого корня будет PR, дающий контейнер режиму Смонтированная папка. Она рождается здесь, потому что здесь рождается база, а переменная хоста, появившаяся на полрелиза позже, — это переменная, которую никто не свяжет с файлом, который её объясняет.

Охрана /workspace, в том месте, где она возможна​

scripts/dev/preflight.mjs начинает отказываться подниматься, когда BRABO_PROJECTS_BASE перекрывается с checkout Brabo, в обе стороны (база содержит checkout; checkout содержит базу), используя git rev-parse --show-toplevel.

Preflight выполняется на ХОСТЕ. Он может ответить на то, на что api не может, и поэтому охрана живёт там, а не в домене: api сравнивает с process.cwd(), которое внутри её контейнера — /workspace, и никогда не увидит настоящий путь. Чистая логика живёт в scripts/dev/base-de-projetos.mjs, собственном протестированном модуле — preflight.mjs выполняет await main() на верхнем уровне, и его импорт из теста поднял бы весь preflight.

Последствия​

Что становится лучше. Создание проекта mounted перестаёт требовать правки compose и перезапуска api + engine — перестаёт стоить каждого хода агента, сокета терминала и вызова LLM в полёте в установке. Настройка становится действием ОПЕРАТОРА, один раз, а не действием пользователя, по проекту. А вектор ADR 0055, до которого не дотягивалась ни одна проверка api, получает охрану, с сообщением, объясняющим, почему ничто иное этого бы не поймало.

Требуется действие оператора ДО деплоя, и поэтому ветка рождается breaking/, а версия поднимается MAJOR: у тех, кто уже использует режим mounted, строки bind-mount по проекту записаны в compose вручную. Они продолжают работать — ничто их не удаляет, и ничто в этом PR не начинает отказывать проекту mounted вне базы (правило базы — для СОЗДАНИЯ и КОНВЕРТАЦИИ и входит в следующий PR; лексический предикат, выполняющийся при каждом чтении, не меняется). Но отныне поддерживаемый путь — база, и продолжение построчно-по-проекту — долг, который больше никто не поддерживает.

Объявленная цена 1: symlink под базой разрешается по-разному с двух сторон. Символическая ссылка внутри базы, указывающая наружу, следуется ядром ВНУТРИ контейнера, только если цель тоже смонтирована — а она не смонтирована. Обозреватель папок отказывается спускаться в symlink (и считает, сколько пропустил, чтобы не лгать «папка пуста»); настоящее расхождение ловит проверка диска в момент материализации, как НАЗВАННЫЙ failed. Это лексическая изоляция с известной дырой — той же, что уже объявляет ADR 0055, и по той же причине: разрешение realpath потребовало бы I/O там, где решение должно быть чистым.

Объявленная цена 2: код вне базы требует перемещения папки. Эта v1 поддерживает ОДНУ базу. У кого репозитории разбросаны по разным местам диска, тому нужно переместить проект внутрь неё — и сообщение отказа говорит ровно это, а не намекает, что существует скрытая настройка. Вторая база (или список) — продуктовое решение, которое не принимается, пока кто-нибудь не попросит: каждый лишний корень — лишний корень, который раскрывает обозреватель папок и который должен различать broker.

Чего этот ADR НЕ решает. Не трогает caminhoDeWorkspaceLocalValido и projectScopeRoot — требование, чтобы проект mounted был внутри базы, относится к следующему PR и намеренно идёт в создание/конвертацию, а не в предикат, выполняющийся при каждом чтении (иначе унаследованный смонтированный проект вне базы начал бы взрываться при ЧТЕНИИ). Не меняет схему, не имеет миграции, и CHECK пары (modo, caminho) остаётся нетронутым. И не даёт контейнер проекту mounted: это другой PR, и именно там BRABO_PROJECTS_HOST_BASE получает потребителя.