Skip to Content
Руководство для разработчиковDaemonГраница файловой системы рабочей области

Граница файловой системы рабочей области

Обзор

HTTP-маршруты файлов демона и делегированные вызовы ACP readTextFile / writeTextFile проходят через границу WorkspaceFileSystem (packages/cli/src/serve/fs/), которая предоставляет:

  • Разрешение путей — канонизация путей и отклонение всего, что выходит за пределы привязанной рабочей области, включая символические ссылки.
  • Шлюз доверия — отказ в записи, если рабочая область не является доверенной (untrusted_workspace).
  • Политика размера и содержимого — лимит на полный снимок/вывод (MAX_READ_BYTES = 256 КиБ), окна для большого текста с ограничением и вывода, и стоимости сканирования (MAX_TEXT_SCAN_BYTES = 8 МиБ), лимит на запись (MAX_WRITE_BYTES = 5 МиБ), обнаружение двоичных файлов.
  • Атомарность — запись с последующим переименованием, с сохранением режима целевого файла и режимом по умолчанию 0o600 для новых файлов, или следует umask процесса при политике режима новых файлов system фабрики (QWEN_SERVE_NEW_FILE_MODE).
  • Аудит — каждый доступ / отказ генерирует структурированное событие для PermissionAuditRing / мониторинга.
  • Типизированные ошибки — замкнутый объединенный тип FsErrorKind, сопоставленный с HTTP-статусами.

HTTP-маршруты файлов (GET /file, GET /file/bytes, POST /file/write, POST /file/edit, GET /list, GET /glob, GET /stat) используют эту границу и никогда не получают исключение для одного хоста. В продакшен-демоне делегированные вызовы ACP достигают WFS через внедрённый адаптер моста; общие вызывающие моста используют WFS только когда они внедряют такой адаптер. Продакшен-среды выполнения qwen serve на одном хосте объявляют readTextFile: false, поэтому все потребители FileSystemService.readTextFile в дочернем процессе используют обычный сервис файловой системы CLI. Итоговые записи текста ACP writeTextFile остаются делегированными: цели рабочего пространства используют WFS, тогда как строгий маркер встроенного инструмента может выбрать эквивалентный хост-писатель для внешнего пути только на адаптерах для одного хоста, созданных демоном. См. дизайн внешних записей.

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

  • Он намеренно принимает обычное поведение чтения CLI, а не гарантии чтения WFS. Документ по дизайну содержит точный список того, от чего отказываются.
  • Тот же документ записывает ограниченное чувство, в котором сохранённый путь чтения адаптера “fail closed”; отдельный дизайн внешних записей записывает, как завершается сбой одобренной итоговой записи.
  • Прямой внешний read_file сохраняет обычные правила разрешений CLI и телеметрию основных файловых операций.
  • HTTP-маршруты файловой системы остаются ограниченными рабочим пространством, и поведение инструмента обнаружения агента не изменяется этой возможностью.
  • Вспомогательные действия, такие как создание родительского каталога и shell-команды, — это отдельные существующие пути, не охваченные этой границей.
  • qwen serve предполагает принципал безопасности на одном хосте с одним UID и не является песочницей ОС.

Обязанности

  • Разрешать пути, предоставленные пользователем, в типизированные значения ResolvedPath, которые остальная часть границы может безопасно использовать.
  • Отказывать в работе с путями за пределами привязанной рабочей области (path_outside_workspace) и путями, цель которых — символическая ссылка (symlink_escape).
  • Отказывать в чтении полного снимка, если размер превышает MAX_READ_BYTES, при этом разрешать явные окна с выводом, ограниченным MAX_READ_BYTES, и стоимостью сканирования, ограниченной MAX_TEXT_SCAN_BYTES; отказывать в записи, если размер превышает MAX_WRITE_BYTES, а также в работе с двоичными файлами (binary_file).
  • Отказывать в записи/редактировании, если рабочая область не доверена (untrusted_workspace) — проверка через assertTrustedForIntent(trusted, intent).
  • Учитывать шаблоны .gitignore / .qwenignore через shouldIgnore.
  • Выполнять атомарные операции записи с последующим переименованием с сохранением режима целевого файла; режим нового файла по умолчанию — 0o600 (или 0o666 & ~umask при политике режима новых файлов system).
  • Генерировать события аудита fs.access / fs.denied при каждой операции.
  • Сопоставлять каждый сбой с FsError с указанием вида и HTTP-статуса; обработчики маршрутов сериализуют их единообразно.

Архитектура

Структура модулей

ФайлНазначение
index.tsБаррель: реэкспортирует WorkspaceFileSystem, WorkspaceFileSystemFactory, FsError, FsErrorKind, ResolvedPath, RequestContext.
workspace-file-system.tsОсновной оркестратор: разрешение путей, проверки политик, атомарные записи, аудит.
paths.tsКанонизация путей, resolveWithinWorkspace, отклонение symlink.
policy.tsПроверки размера (enforceReadSize, enforceWriteSize), доверия (assertTrustedForIntent), бинарности (detectBinary).
audit.tscreateAuditPublisher, типы событий аудита.
errors.tsFsError, FsErrorKind, FsErrorStatus.
types.tsResolvedPath, RequestContext, ReadOptions, WriteOptions.

WorkspaceFileSystemFactory

Фабрика строится один раз при запуске демона (runQwenServeresolveBridgeFsFactory → адаптер). Она владеет:

  • trusted: boolean — сигнал доверия для этой рабочей области.
  • workspaceRoot: string — канонизированный корень рабочей области.
  • auditRing: PermissionAuditRing — общее кольцо аудита.
  • newFileMode: 'owner' | 'system' — политика режима новых файлов (по умолчанию owner; 0o600 для новых файлов, независимо от umask). При system новые файлы следуют 0o666 & ~umask. Существующие файлы всегда сохраняют свой режим.

Каждый запрос создает RequestContext и вызывает оркестратор фабрики только для этого вызова — долгоживущего состояния на файл нет.

FsError и FsErrorKind

FsError — это типизированная ошибка с { kind, message, status, cause? }. FsErrorKind — замкнутый объединенный тип:

ВидHTTPПричина
path_outside_workspace400Разрешенный путь выходит за пределы привязанной рабочей области.
symlink_escape400Цель является символической ссылкой.
path_not_found404ENOENT.
binary_file422Содержимое определено как двоичное при запросе через текстовый маршрут, либо большой текст в кодировке, которую текстовый маршрут не может декодировать.
file_too_large413Текст без окна/полного снимка выше MAX_READ_BYTES, смещение строк за пределами MAX_TEXT_SCAN_BYTES или запись выше MAX_WRITE_BYTES.
hash_mismatch409Ошибка оптимистичной конкурентности expectedSha256, или файл изменился во время стабильного чтения.
file_already_exists409Режим 'create' для существующего файла.
text_not_found422Строка поиска в POST /file/edit не найдена в файле.
ambiguous_text_match422Несколько совпадений, когда требовалось ровно одно.
untrusted_workspace403Попытка записи в недоверенной рабочей области.
permission_denied403Ошибка ОС EACCES / EPERM.
io_error503ENOSPC / EIO / EBUSY / ETXTBSY / ENAMETOOLONG / EMFILE / ENFILE. Отличается от permission_denied, чтобы пайплайны мониторинга не вызывали команду реагирования на инциденты безопасности из-за “диск заполнен”.
internal_error500Ошибка, не связанная с errno, достигшая границы (TypeError, программная ошибка).
parse_error400 / 422Ошибка разбора тела запроса (400) или нарушение инварианта уровня сервиса (422).

BridgeFileSystem (ACP-адаптер)

packages/acp-bridge/src/bridgeFileSystem.ts определяет:

interface BridgeFileSystem { readText(params: ReadTextFileRequest): Promise<ReadTextFileResponse>; writeText(params: WriteTextFileRequest): Promise<WriteTextFileResponse>; }

Это точка внедрения для ACP readTextFile / writeTextFile. Тесты моста и встроенные вызывающие в режиме A могут опустить его в BridgeOptions; BridgeClient использует свой встроенный прокси fs.readFile / fs.writeFile (сохраняет поведение до F1). Продакшен qwen serve подключает BridgeFileSystem через createBridgeFileSystemAdapter(fsFactory) (packages/cli/src/serve/bridge-file-system-adapter.ts) и устанавливает delegateReadTextFileToClient: false. Соответствующие возможности дочерние процессы поэтому читают текст локально и делегируют итоговые записи текста ACP. Адаптер сохраняет свою реализацию чтения, чтобы неожиданные или нарушающие возможности делегированные чтения всё равно сталкивались с границей рабочего пространства WFS. Его внешний путь хост-писателя отключён по умолчанию и выбирается только по точному версионному происхождению на адаптерах для одного хоста, принадлежащих демону; внедрённые bridge, реестры и фабрики рабочего пространства, общий ACP и HTTP сохраняют обычную границу.

Два защитных шлюза, которые адаптер ОБЯЗАН воспроизвести (потому что встроенный прокси полностью обходится при внедрении адаптера):

  1. Отклонение нерегулярных файлов — сокеты/каналы/символьные устройства/файлы procfs/sysfs могут передавать неограниченные потоки данных, несмотря на stats.size === 0. Встроенный путь выбрасывает исключение с describeStatKind(stats) в сообщении.
  2. Избегать неограниченной буферизации всего файла. Встроенный фолбэк ограничивает буферизованное чтение READ_FILE_SIZE_CAP = 100 МиБ. Внедрённый адаптер вместо этого применяет более строгий контракт WorkspaceFileSystem: полные снимки останавливаются на 256 КиБ, тогда как для больших UTF-8 файлов требуется конечный limit, и они потоково читаются из привязанного к inode handles с возвратом не более 256 КиБ. Адаптер не должен читать весь 500-мегабайтный лог только для того, чтобы вернуть { line: 1, limit: 10 }.

Адаптер идет дальше: он использует WorkspaceFileSystem.writeTextOverwrite (примитив PR 18) для записей рабочего пространства и эквивалент, принадлежащий фабрике, для строго помеченных внешних записей встроенных инструментов. Оба используют атомарные записи во временный файл с переименованием с сохранением режима, значением по умолчанию 0o600 и отклонением символьных ссылок в рамках общей блокировки канонического пути. Это отличие от встроенного прокси до F1, который разрешал символьные ссылки и записывал через них в целевой файл — агенты, которые полагались на запись через символьные ссылки в dot-файлы, теперь должны обращаться непосредственно к разрешенному пути.

Сохранение FsError через ACP-проводку

Когда адаптер BridgeFileSystem выбрасывает FsError (kind: 'untrusted_workspace' / 'symlink_escape' / 'file_too_large' / и т.д.), стандартный RPC-путь ошибок ACP SDK сериализует только error.message как общую ошибку -32603 "Internal error"kind / status / hint отбрасываются. Клиенту RPC агента пришлось бы сопоставлять человекочитаемое сообщение с регулярным выражением, чтобы диспетчеризовать типизированные действия (повтор аутентификации, выбор файла, подсказка прокси).

BridgeClient.writeTextFile и BridgeClient.readTextFile устанавливают тонкую защиту (packages/acp-bridge/src/bridgeClient.ts), которая перехватывает исключения, похожие на FsError, и пробрасывает их как ACP RequestError:

function isFsErrorShape(err: unknown): err is FsErrorShape { return ( err instanceof Error && err.name === 'FsError' && typeof (err as { kind?: unknown }).kind === 'string' ); } function preserveFsErrorOverAcp(err: unknown): never { if (isFsErrorShape(err)) { throw new RequestError(-32603, err.message, { errorKind: err.kind, ...(err.hint !== undefined ? { hint: err.hint } : {}), ...(err.status !== undefined ? { status: err.status } : {}), }); } throw err; }

Теперь клиент RPC агента получает data.errorKind (замкнутое значение FsErrorKind) и опциональные data.hint и data.status, так что потребители SDK могут ветвиться по типизированному перечислению вместо сопоставления с регулярным выражением по сообщению.

Два замечания по дизайну:

  • Утиная типизация вместо импортаFsError находится в packages/cli/src/serve/fs/errors.ts, а BridgeClient — в packages/acp-bridge. Прямой import { FsError } инвертировал бы зависимость. Проверка через «утку» (name === 'FsError' + kind: string) повторяет то, что mapDomainErrorToErrorKind (status.ts) уже делает для TrustGateError / SkillError по той же причине кросс-пакетной сборки.
  • Код JSON-RPC остается -32603 — мост не может надежно сопоставить FsError.kind с формой кода ошибки JSON-RPC, поэтому структурированное поле data несет семантическую информацию для потребителей SDK. Код статуса проводки (-32603 “internal error”) не изменяется; клиенты маршрутизируют на основе data.errorKind.

Шлюз доверия

assertTrustedForIntent(trusted, intent) потребляет булево значение доверия, переданное вызывающей стороной; уровень политики не читает Config.isTrustedFolder() напрямую. Чтение/вывод списка/статистика/поиск по шаблону всегда разрешены (доверие требуется только для записи). Попытки записи в недоверенных рабочих областях выбрасывают FsError('untrusted_workspace', ..., status: 403). Сигнал доверия поступает через WorkspaceFileSystemFactoryDeps.trusted: booleanrunQwenServe передает true, потому что оператор запустил демона для рабочей области, которой неявно доверяет; createServeApp (прямое встраивание без runQwenServe) по умолчанию устанавливает false и выводит предупреждение один раз на процесс (см. 02-serve-runtime.md).

Рабочий процесс

Чтение

readText не пропускает и не отклоняет чтение из-за правил игнорирования. Он читает файл в обычном режиме и записывает соответствующую классификацию игнорирования в meta.matchedIgnore. list и glob фильтруют игнорируемые результаты только если includeIgnored не включено.

Запись

Атомарная запись с переименованием гарантирует, что SIGKILL / OOM в середине записи НЕ оставит целевой файл обрезанным. Режим 'create' прерывается с file_already_exists при lstat; режим 'overwrite' продолжается; expectedSha256 включает оптимистичную блокировку (hash_mismatch при несовпадении).

POST /file/edit (однократная замена текста)

Добавляет два дополнительных режима сбоя поверх записи:

  • text_not_found (422) — строка поиска не найдена в файле.
  • ambiguous_text_match (422) — несколько совпадений, когда требовалось ровно одно (контракт маршрута).

Разветвление аудита

FS_ACCESS_EVENT_TYPE / FS_DENIED_EVENT_TYPE содержат контекст (ctx), путь, намерение, результат, errorKind?, прочитано/записано байт, sha256?.

Состояние и жизненный цикл

  • Фабрика создается один раз при запуске демона (runQwenServeresolveBridgeFsFactory → адаптер).
  • Каждый запрос создает RequestContext и вызывает оркестратор фабрики только для этого вызова — долгоживущего состояния на файл нет.
  • Блокировки на путь живут только на время операции записи (без блокировок между вызовами; одновременные записи в один и тот же путь конкурируют за блокировку и сериализуются).
  • Кольцо аудита принадлежит runQwenServe и используется совместно с издателем аудита разрешений.

Зависимости

  • @qwen-code/qwen-code-coreIgnore, isBinaryFile, Config.isTrustedFolder().
  • node:fs, node:path, node:crypto.
  • @qwen-code/acp-bridge — контракт BridgeFileSystem на стороне ACP.
  • HTTP-маршруты: packages/cli/src/serve/routes/workspace-file-read.ts, workspace-file-write.ts.

Конфигурация

ИсточникПараметрЭффект
WorkspaceFileSystemFactoryDeps.trusted: booleanВходной параметр конструктораРазрешена ли запись; по умолчанию true от runQwenServe, false от createServeApp (с предупреждением).
КонстантаMAX_READ_BYTES = 256 КиБЛимит полного снимка и возвращаемого текста; больший текст требует явного аргумента окна.
КонстантаMAX_TEXT_SCAN_BYTES = 8 МиБКоличество байт, которое может просканировать чтение большого текста для поиска смещения строки; при превышении — file_too_large.
КонстантаMAX_WRITE_BYTES = 5 МиБЛимит записи; размер меньше express.json({ limit: '10mb' }).
КонстантаMAX_UPLOAD_BYTES = 50 МиБЛимит загрузки двоичных файлов для POST /file/upload; загрузки никогда не перезаписывают и автоматически нумеруют занятые имена.
КонстантаBINARY_PROBE_BYTES = 4096Размер выборки для определения двоичного содержимого.
Теги возможностейworkspace_file_read, workspace_file_bytes, workspace_file_write, workspace_file_uploadСм. 11-capabilities-versioning.md.
Файлы рабочей области.gitignore, .qwenignoreИгнорируемые пути отмечаются как ignored: true от shouldIgnore.

Оговорки и известные ограничения

  • Символические ссылки отклоняются, а не обрабатываются. Это отличие от встроенного прокси BridgeClient.writeTextFile до F1, который разрешал символьные ссылки. Агентам, записывающим данные через символические ссылки в dot-файлы, необходимо обращаться непосредственно к разрешенному пути.
  • io_error и permission_denied различимы. Не путайте их. Пайплайны мониторинга используют errorKind для оповещения — сведение ENOSPC к permission_denied вызовет вызов команды реагирования на проблемы с df -h.
  • Режим нового файла по умолчанию — 0o600, а не по умолчанию umask. Аргумент mode системного вызова записи обходит umask. Агенты не могут передать переопределение режима для каждой записи. Операторы, которые хотят, чтобы файлы, созданные агентом, следовали umask демона, могут включить это для каждого демона с помощью QWEN_SERVE_NEW_FILE_MODE=system (существующие файлы всё ещё сохраняют свой режим); см. 17-configuration.md.
  • createServeApp по умолчанию trusted: false молча отклоняет ACP-записи с untrusted_workspace для встраивающих систем, которые не внедряют собственную fsFactory или bridge. Одноразовое предупреждение в stderr выводится при первом вызове; последующие вызывающие не увидят напоминания. См. 02-serve-runtime.md.
  • Большой текст требует явного аргумента окна — любого из line / limit / maxBytes. Чтение без них остаётся file_too_large, потому что вызывающий, который считает, что владеет всем файлом, может записать его обратно обрезанным. Окна потоково читаются из привязанного к inode handle и никогда не возвращают больше MAX_READ_BYTES.
  • MAX_READ_BYTES ограничивает то, что возвращает чтение; MAX_TEXT_SCAN_BYTES ограничивает то, что оно стоит. Смещения строк разрешаются сканированием от байта 0, поэтому { line: 900_000_000, limit: 20 } возвращает почти ничего и всё равно проходит по файлу. После 8 МиБ сканирования чтение отклоняется с file_too_large, указывающим на readBytes, который достигает любого смещения за O(1).
  • Потоковые окна допускают добавления, но не усечение. Путь полного снимка может требовать побайтовую стабильность, потому что он возвращает весь файл; окно префикса не может, иначе каждое чтение живого лога завершается сбоем. Потоковый путь утверждает идентичность inode плюс “не уменьшался”, поэтому добавления проходят, а усечение / замена всё ещё отклоняются. sizeBytes сообщает размер при open, описывая снимок, из которого было вырезано окно.
  • Большие частичные чтения опускают хэш полного файла. originalLineCount опускается, когда потоковая передача останавливается до EOF.
  • Пейджинг выполняется по байтовому курсору, а не по строке. Чтение, которое оставляет содержимое, возвращает hasMore и, где байтовое смещение выводимо, непрозрачный nextCursor. Возобновление от него — O(1); возобновление по line повторно сканирует от байта 0 и отклоняется после MAX_TEXT_SCAN_BYTES. Курсор несет {dev, ino, size}, поэтому заменённый или усечённый файл даёт hash_mismatch вместо байт из неправильного места, тогда как добавление оставляет его действительным. Чтения снимков не-UTF-8 сообщают hasMore, но без курсора — их декодированный текст является перекодировкой UTF-8, длины которой не отображаются обратно в файловые смещения.
  • BridgeFileSystem адаптер ДОЛЖЕН воспроизвести оба шлюза встроенного прокси (отказ от нерегулярных файлов + ограниченная буферизация/потоковая передача). Встроенный путь полностью обходится при внедрении адаптера.

Ссылки

  • packages/cli/src/serve/fs/index.ts (barrel)
  • packages/cli/src/serve/fs/paths.ts
  • packages/cli/src/serve/fs/policy.ts
  • packages/cli/src/serve/fs/errors.ts
  • packages/cli/src/serve/fs/audit.ts
  • packages/cli/src/serve/fs/workspace-file-system.ts
  • packages/cli/src/serve/bridge-file-system-adapter.ts
  • packages/acp-bridge/src/bridgeFileSystem.ts
  • HTTP route reference: ../qwen-serve-protocol.md.
Last updated on