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

ADR 0113 — Локальная папка, приложенная через браузер, становится chunks.scope = 'local', читаемым БРАУЗЕРОМ, никогда путём хоста

Статус​

Принят.

Ссылается (без правки): ADR 0072, ADR 0079, ADR 0080, ADR 0107, ADR 0108.

Контекст​

Запрос​

Позволить пользователю приложить к проекту локальную папку со своей машины как СПРАВОЧНЫЙ материал ТОЛЬКО ДЛЯ ЧТЕНИЯ для агентов — явно БЕЗ запуска CLI brabo-runner (ADR 0103). Это НЕ исполнение проекта и не workspace: execution_mode: runner (ADR 0104) существует именно потому, что исполнению нужен РЕАЛЬНЫЙ путь ХОСТА, куда контейнер/engine может маршрутизировать команды, подтверждённый runner, который подключается и его проверяет (RN-423). Эта фича ближе к «приложить файлы к разговору», чем к «смонтировать workspace».

Что на самом деле отвергли ADR 0072 и ADR 0107, и почему здесь иначе​

Оба решения касаются разрешения абсолютного ПУТИ ХОСТА из браузера. ADR 0072 отверг селектор папки при создании проекта, потому что «селектор требовал бы, чтобы api перечисляла браузеру файловую систему КОНТЕЙНЕРА», — новая поверхность лишь ради эргономики, да ещё и файловая система не того контекста. ADR 0107 решил навигацию по папке, маршрутизируя чтение через Runner (процесс с привилегиями самого пользователя на его собственной машине, а не api), именно потому, что api никогда не видит реальную файловую систему пользователя.

Оба касаются того, ОТКУДА БЕРЁТСЯ ПУТЬ НА ДИСКЕ ПОЛЬЗОВАТЕЛЯ, и оба важны, потому что путь питает либо создание проекта (workspace_path), либо выполнение команды (cwd у TerminalExecutor) — контексты, где НЕВЕРНЫЙ путь — проблема безопасности и корректности: он решает, против чего агент может выполнять.

Эта фича никогда не разрешает путь хоста. <input webkitdirectory> браузера возвращает объекты File; код читает их содержимое (File.text()) и относительный путь внутри выбранной папки (File.webkitRelativePath) — никогда абсолютный путь, потому что File API браузера не раскрывает его, по замыслу, ни одному сайту. По сети идёт ТЕКСТ, который браузер уже имел законное право читать, — ровно та же модель доверия, что у любой обычной загрузки через <input type="file"> на любом сайте. Нет пути, который нужно проверять, изолировать или маршрутизировать на runner, потому что пути нет вовсе. Строить для этого ретранслятор в стиле runner значило бы решать проблему, которой у этой фичи нет.

Почему переиспользовать конвейер RAG, а не параллельный механизм​

У продукта уже есть работающий конвейер chunking/embedding/гибридного поиска (ADR 0079/0080): таблица chunks, RagEmbeddingService, chunkText/chunkMarkdownDocument, ChunkRepository.searchByVector/ searchByLexicalQuery и отрисовка цитат вкладки Chat RAG (RagCitationCard, origemDoChunk). Локально приложенный файл структурно имеет ту же форму, что и проиндексированный файл документации/ADR: путь плюс содержимое, которое должно быть доступно поиску и цитированию. Построение второго параллельного хранилища «приложенных файлов» и второго UI цитат продублировало бы конвейер, который уже делает ровно эту работу, без иной причины, кроме того, что источник байтов — загрузка из браузера, а не blob git.

Решение​

chunks.scope получает четвёртое значение, 'local', добавленное через ALTER TYPE ... ADD VALUE (миграция 0052) — аддитивно, без миграции данных, без нового CHECK: существующие ограничения chunks_session_id_casa_com_escopo/chunks_source_path_casa_com_escopo (миграция 0045) уже записаны как «это session или нет», поэтому local попадает на ту же сторону, что docs/adr (есть source_path, нет session_id), с нулём изменений схемы, кроме значения enum.

IndexLocalFolderUseCase близко отражает IndexProjectDocsUseCase: делит каждый файл на chunks (с учётом Markdown через chunkMarkdownDocument для .md/.mdx, chunkText для прочих), вызывает тот же RagEmbeddingService, записывает chunks с embedding: null, когда embedding падает (тот же образец честности, что уже используют docs/adr — RN-233), и делает полный deleteByScope(projectId, 'local') + пересоздание при каждом вызове. origemDoChunk потребовал НОЛЬ изменений: chunks local несут sourcePath ровно как docs/adr, поэтому попадают в существующую ветку kind: 'file' — UI цитат их уже обрабатывает.

Отказ сразу, никогда молчаливое усечение. В отличие от docs/adr (фоновое сканирование, на которое никто не смотрит), приложение папки — это однократный жест пользователя с селектором папки перед ним. IndexLocalFolderUseCase ОТКЛОНЯЕТ (400) всю загрузку целиком, когда совокупные потолки превышены (число файлов, сумма байт), — тот, кто нажал «Anexar», смотрит на экран и может выбрать папку поменьше. Отдельный слишком большой файл или файл с нераспознанным расширением лишь ПРОПУСКАЕТСЯ (filesSkipped), никогда не роняет партию — то же различие, которое IndexProjectDocsUseCase уже делает неявно, фильтруя только .md.

Никогда не связан с общим «Reindexar agora». ReindexProjectUseCase перечитывает из источника, к которому СЕРВЕР может вернуться, — репозитория проекта (docs/adr) и event log (session). У области local такого источника нет: текст живёт только в браузере того, кто его приложил, а сервер никогда не сохранял путь хоста (его не существует). Вызов deleteByScope(projectId, 'local') общей кнопкой переиндексации стёр бы приложенный пользователем справочный материал без возможности его воссоздать — повторная загрузка папки — ЕДИНСТВЕННЫЙ механизм ресинхронизации, и это намеренно отдельная кнопка (POST .../rag/local, maintainer, та же роль, что у reindex, потому что он тоже задействует provider embedding и заменяет проиндексированное). Оба use case несут явный комментарий в коде, указывающий друг на друга и на этот ADR, чтобы будущее «исправление» не связало их и не стёрло молча справочный материал пользователей одним несвязанным кликом.

Покрытие говорит правду о том, чем local на самом деле является. RagCoverage.local — НОВАЯ форма (RagLocalCoverage), а не переиспользование RagFileCoverage: нет «всего файлов в репозитории», с чем сравнивать, — у загрузки из браузера нет репозитория для повторного сканирования. Она сообщает, что проиндексировано СЕЙЧАС (filesIndexed, folderName, lastAttachedAt). lastAttachedAt — ЕДИНСТВЕННОЕ объявленное исключение из правила продукта «никогда не показывать угаданную метку времени» (RN-237/ADR 0080): это реальный MAX(chunks.created_at) по области, а не вычисленное или оценочное значение, — та же дисциплина честности, применённая к области, где реальная метка времени случайно имеет ответ.

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

  • Не трогает execution_mode/workspace_path/runner. Они остаются ровно такими, как описывают ADR 0072/0104/0107: как ИСПОЛНЯЕТСЯ код проекта. Эта фича добавляет справочный материал в индекс RAG; у неё нет мнения о том, где живёт или работает код проекта.
  • Не добавляет пятое понятие «приложенных файлов». Никакой новой таблицы, никакого параллельного формата цитат — local — значение того же ChunkScope, на котором уже говорит остальной конвейер.
  • Не индексирует репозиторий самого проекта второй раз. docs/adr по-прежнему читаются из git-репозитория проекта через ReadProjectCodeUseCase; local — намеренно ОТДЕЛЬНЫЙ источник, для материала, не имеющего ничего общего с репозиторием самого проекта (спецификация, эталонная реализация, документация конкурента — что угодно, что пользователь хочет сделать доступным поиску агента).

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

  • Вторая загрузка заменяет первую целиком. Нет «добавить к приложенному» — IndexLocalFolderUseCase стирает всю область local перед записью новой. Это ТА ЖЕ дисциплина полной идемпотентной пересборки, которой уже следуют docs/adr/session (RN-231), применённая согласованно, а не изобретённая специально для этой области.
  • Бинарные и слишком большие файлы молча отсутствуют в поиске, объявленные через filesSkipped. Allowlist расширений (RAG_LOCAL_ALLOWED_EXTENSIONS) — реальное ограничение содержимого, а не просто отделка UX — сервер навязывает его независимо от того, что предварительно отфильтровал браузер, потому что фильтр на стороне клиента — удобство, а не гарантия.
  • Граница доверия — ровно граница обычной загрузки файла. Без изоляции, без allowlist путей, без тикета runner — потому что в этой фиче пути нет вовсе, только байты, которые у браузера уже были. Кто может вызвать POST .../rag/local (роль maintainer), может положить произвольный текст в поисковый индекс проекта; это та же власть, которую маршрут уже требует для reindex.

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

  • Маршрутизировать чтение через Runner (ADR 0103/0107), как навигацию по папке. Отвергнуто: Runner существует, чтобы дать ENGINE реальный путь хоста, против которого выполнять команды, на машине, которую пользователь явно настроил с CLI. Эта фича явно задумана работать БЕЗ CLI, для справочного материала только для чтения — маршрутизация через Runner сделала бы CLI требованием, которого эта фича задумана не иметь.
  • Новая таблица attachments, независимая от chunks. Отвергнуто: продублировала бы chunking, embedding, гибридный поиск и отрисовку цитат без какой-либо пользы — форма «путь + содержимое, доступно поиску, цитируемо» — ровно то, что уже моделирует chunks.
  • Связать local с ReindexProjectUseCase. Отвергнуто: для этой области на стороне сервера нечего перечитывать; связывание заставило бы общую кнопку переиндексации молча стирать приложенный пользователями справочный материал, без возможности его вернуть.