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

ADR 0072 — Проект выбирает, где живёт код: папка пользователя или управляемая папка

Статус​

Принят.

Этот ADR пересматривает часть ADR 0065, который остаётся принятым и не редактируется, и затрагивает территорию, описываемую ADR 0055. Оба выбрали ПРОТИВОПОЛОЖНОЕ этому документу направление — идти к стене контейнера, — и поэтому раздел последствий здесь самый важный: он объявляет, не смягчая, что теряется.

Контекст​

Запрос​

«Пересечь границу, при которой код пишется только в контейнере, и иметь возможность писать код из папки пользователя. При создании проекта выделить Local для этой цели — быть какой-то папкой пользователя — и Container с той опцией, что есть сегодня.»

И о форме пути — вариант, явно выбранный владельцем продукта, осознающим предупреждение, что он работает, только если папка смонтирована в контейнер: свободный путь, вводимый пользователем.

Территория: единственная граница, существующая сегодня​

До этого ADR корнем проекта была буквально одна строка:

join(PROJECT_WORKSPACES_ROOT, workspaceDirName)

с workspaceDirName, проверяемым против ^[A-Za-z0-9_-]{1,64}$ (apps/api/src/infrastructure/filesystem/project-workspaces-root.ts). Сила этой формы не в проверке, а в join. С сегментом без / и без .. результат не может выйти за управляемый корень, что бы ни случилось с колонкой. Это СТРУКТУРНАЯ изоляция, и она держит четырёх потребителей, которые должны согласовываться между собой:

потребительгдечто ломается, если корни разойдутся
permissions.jsonfs-permissions-file-store.tsполитика читается из одного места, применяется к другому
область терминала (ADR 0055)propose-action.use-case.ts → decide.tsобласть, РАЗРЕШАЮЩАЯ команду, указывает не на ту папку
чтение вкладки Code (RN-095)caminhoDeRepositorioContidoизоляция чтения над не тем деревом
рабочее дерево engineapps/engine/lib/engine/actions/workspace.exагент пишет туда, где api не читает

Что меняется​

projects получает пару (workspace_mode, workspace_path). В режиме container — по умолчанию, и это поведение каждого уже существующего проекта — ничего не меняется. В режиме local корнем становится абсолютный путь, введённый пользователем, а join уходит со сцены.

Решение​

  1. Режим принадлежит ПРОЕКТУ, выбирается при создании, по умолчанию container. Миграция 0043. container как значение колонки по умолчанию — то, что не даёт существующему проекту сменить место, — и решение NOT NULL, как story_promotion (ADR 0046), потому что значение И ЕСТЬ решение, а решение о полномочиях не остаётся неявным.

  2. Режим и путь — ОДНО решение, закреплённое в базе через CHECK ((workspace_mode = 'local') = (workspace_path IS NOT NULL)). Замок не остаётся только в use case, потому что колонку читают ДВА процесса (api и engine) и пишут скрипты seed и backfill, не проходящие через него. local без пути — это область терминала, указывающая в никуда; container с путём — это второй источник истины, ждущий расхождения с первым.

  3. Вывод остаётся ЕДИНСТВЕННЫМ. projectScopeRoot стал принимать РАСПОЛОЖЕНИЕ ({workspaceDirName, workspaceMode, workspacePath}) вместо имени папки, и именно он выбирает ветку. Ни один вызывающий не получил собственной проверки: причина, по которой функция существует, — два вывода должны согласовываться, — действует ещё сильнее теперь, когда веток две. Действует правило PÓS-FASE 15: дублировать её в каждом вызывающем значило бы проверку, которая однажды разойдётся.

  4. Охрана создания отказывает, а не даёт сломаться потом (RN-170). Путь, не смонтированный в контейнер, даёт проект, который застревает на первом инструменте первого агента, далеко от экрана, где было принято решение. Создание проверяет и отказывает с 400 и с инструкцией, как смонтировать: абсолютный путь, без .., существующий, папка, доступная процессу на запись (access(W_OK|X_OK) — образы работают non-root, ADR 0024, а папка хоста с другим владельцем на практике приходит только для чтения).

  5. Отказ покрывает корень системы и checkout Brabo, в обе стороны. /, системные папки и всё под ними остаются вне, потому что корень проекта — это область, разрешающая терминал агента: проект с корнем в /etc превращает «агент может писать в свой проект» в «агент может переписать контейнер». Перекрытие с checkout Brabo отклоняется в обоих направлениях — папка, СОДЕРЖАЩАЯ монорепозиторий (буквальный случай запроса), и папка ВНУТРИ него, — это проблема, о которой ADR 0055 сообщает как о реально происходящей. Отклонить одно и разрешить другое значило бы закрыть дверь и оставить окно.

  6. Лексическая часть охраны выполняется и при ЧТЕНИИ. Проверка создания — это затвор, но единственный способ её обойти — писать прямо в базу, — и когда это происходит, выигрыш — область терминала в /. Поэтому projectScopeRoot повторно применяет лексический предикат при каждом выводе и громко падает. Что там НЕ перепроверяется — дисковая часть (существует, доступна на запись): это I/O, а функция на горячем пути.

  7. Затвор образа Arquiteto (RN-105) не действует для проекта Local. Он существует потому, что контейнер придаёт смысл чтению кода; проект Local не поднимает никакого контейнера. Без этого решения вкладка Code вечно отвечала бы 409 в проекте, где решение Arquiteto никогда не произойдёт, — вкладка закрыта побочным эффектом, а не выбором. Освобождение живёт в той же воронке, что и затвор, никогда не разбросано по маршрутам.

  8. Engine читает обе колонки и разрешает локатор в ЗАПРОСЕ. Engine.Projects.Project возвращает ИМЯ папки в режиме container и АБСОЛЮТНЫЙ ПУТЬ в режиме local, а Workspace.workspace_dir/2 различает их по начальной косой черте — которая однозначна, потому что имя папки проверяется regex, не допускающим /. Разрешать в запросе, а не в каждом вызывающем, — тот же довод, что в пункте 3.

Чего этот ADR НЕ делает​

  • Не поднимает контейнер на проект. ФАЗА 25b остаётся отрезанной, а ADR 0065 продолжает действовать как есть. Проект local работает в ТОМ ЖЕ контейнере, что и сегодня; изменилась лишь папка.
  • Не меняет границу внешнего эффекта (RN-106). git push, открытие PR и деплой остаются deny в терминале, внутри области и вне её, в режиме Local, как и в Container.
  • Не меняет политику терминала (ADR 0055). Область пути и узкий allowlist остаются границей; меняется то, где начинается область.
  • Не предлагает селектор папки. Путь вводится, по объявленному решению владельца продукта. Селектор потребовал бы от api перечислять браузеру файловую систему контейнера, а это новая поверхность ради решения эргономики.

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

Болезненное, и это объявленная цена этой поставки​

Структурная изоляция join перестаёт существовать для проектов Local. Там, где раньше ни одна повреждённая колонка не могла дать корень вне PROJECT_WORKSPACES_ROOT, теперь корень — то, что скажет колонка. На его месте остаются список отказов (пункт 5) и повторная проверка при чтении (пункт 6), — а список отказов — тот вид барьера, который, как доказали находки Z и AD, не сходится для глаголов команд. Разница, на которой держится решение, — пространственная: абсолютный путь — закрытое упорядоченное пространство, где «находится под этим корнем» решается по префиксу, тогда как глагол/форма/вызов — три открытых пространства. Это не одна и та же проблема, но и не стена.

Вектор symlink, объявленный в ADR 0055, остаётся открытым и теперь указывает на машину пользователя. Нормализация лексическая, по контракту (decide() чист, ноль IO), поэтому symlink ВНУТРИ папки проекта, указывающий наружу, не обнаруживается, — а в режиме Local «наружу» может быть $HOME того, кто работает. Закрытие этого — изоляция, а не политика, и по-прежнему зависит от ФАЗЫ 25b.

Папка пользователя становится доступной агенту на запись. Это запрос, а не побочный эффект: агент пишет код в папку пользователя. Стоит сказать вслух, что permissions.json этого проекта тоже начинает жить внутри неё и что удаление папки удаляет и политику.

Помогающие​

  • Ничего не меняется для того, кто не выбирает. Значение по умолчанию — привычное поведение, и существующая suite прошла без изменения поведения — только сигнатуры.
  • Отказ датирован и обучает. Наиболее вероятный режим отказа (несмонтированная папка) становится 400 со строкой compose, которую нужно добавить, на экране, где принимается решение, а не застрявшим агентом потом.
  • Внешний контракт gates, RBAC и замок merge (RN-014) не затронуты.

Что требуется от оператора​

Смонтировать папку в ОБА сервиса, по ОДНОМУ абсолютному пути — см. docs/runbook.md, раздел «Projeto no modo Local», и docs/reference/configuration.md. Монтирование только в api даёт проект, который api принимает, а engine не видит: api проверяет то, что видит она, и у неё нет способа узнать, что смонтировано в другом контейнере.

Рассмотренные альтернативы​

  • Корень папок пользователя, где проект выбирает ПОДКАТАЛОГ (LOCAL_WORKSPACES_ROOT + имя). Сохранила бы join и всю структурную изоляцию и была самой сильной альтернативой — но это не запрос: владелец продукта выбрал свободный путь, осознавая предупреждение.
  • Принимать путь без проверки и дать упасть при использовании. Отброшено: это ровно тот режим отказа, предотвратить который существует RN-170, и он проявляется далеко от экрана, где было принято решение.
  • Пометить проект Local как «без вкладки Code» вместо освобождения от затвора RN-105. Это значило бы наказать новый режим правилом, которое о нём не говорит.