Skip to Content
デベロッパーガイドDaemonワークスペースファイルシステム境界

ワークスペースファイルシステム境界

概要

デーモンは、HTTP ルートや ACP 側エージェントの呼び出しがホストファイルシステムに直接アクセスすることを許可しません。すべての読み取り、書き込み、一覧取得、glob、stat は WorkspaceFileSystem 境界(packages/cli/src/serve/fs/)を経由して実行され、以下の機能を提供します。

  • パス解決 — パスを正規化し、シンボリックリンク経由のものも含め、バインドされたワークスペース外へのアクセスを拒否する。
  • トラスト制御 — ワークスペースが信頼されていない場合(untrusted_workspace)は書き込みを拒否する。
  • サイズ・コンテンツポリシー — 読み取り上限(MAX_READ_BYTES = 256 KiB)、書き込み上限(MAX_WRITE_BYTES = 5 MiB)、バイナリ検出。
  • アトミック性 — 書き込み後リネームによるアトミック操作。対象ファイルのモードを保持し、新規ファイルのデフォルトは 0o600
  • 監査 — すべてのアクセス・拒否は PermissionAuditRing / モニタリング向けの構造化イベントとして送出される。
  • 型付きエラー — 閉じた FsErrorKind ユニオンを HTTP ステータスにマッピング。

HTTP ファイルルート(GET /fileGET /file/bytesPOST /file/writePOST /file/editGET /listGET /globGET /stat)と、ACP 側の BridgeFileSystem アダプター(エージェント駆動の readTextFile / writeTextFile 呼び出しが同じゲートを通るようにする)は、いずれもこの境界を経由します。

責務

  • ユーザーが指定したパスをブランド付き ResolvedPath 値に解決し、境界内の残りのコードが安全に利用できるようにする。
  • バインドされたワークスペース外のパス(path_outside_workspace)やシンボリックリンクを対象とするパス(symlink_escape)を拒否する。
  • MAX_READ_BYTES を超える読み取り、MAX_WRITE_BYTES を超える書き込み、バイナリファイル(binary_file)を拒否する。
  • ワークスペースが信頼されていない場合(untrusted_workspace)は書き込み・編集を拒否する — assertTrustedForIntent(trusted, intent) によるゲート制御。
  • shouldIgnore を通じて .gitignore / .qwenignore パターンを尊重する。
  • 対象ファイルのモードを保持しながら書き込み後リネームによるアトミック操作を実行する。新規ファイルのデフォルトモードは 0o600
  • すべての操作で fs.access / fs.denied 監査イベントを送出する。
  • すべての失敗を kind と HTTP ステータスを持つ FsError にマッピングし、ルートハンドラが統一的にシリアライズする。

アーキテクチャ

モジュール構成

ファイル役割
paths.tscanonicalizeWorkspaceresolveWithinWorkspacehasSuspiciousPathPattern、ブランド付き ResolvedPathIntent ユニオン(read | write | list | stat | glob)。
policy.tsMAX_READ_BYTESMAX_WRITE_BYTESBINARY_PROBE_BYTESassertTrustedForIntentdetectBinaryenforceReadBytesSizeenforceReadSizeenforceWriteSizeshouldIgnore
audit.tsFS_ACCESS_EVENT_TYPEFS_DENIED_EVENT_TYPEcreateAuditPublisher、監査ペイロード型。
errors.tsFsError クラス、isFsErrorFsErrorKind ユニオン(14 種類)、FsErrorStatus ユニオン(400 / 403 / 404 / 409 / 413 / 422 / 500 / 503)。
workspace-file-system.tscreateWorkspaceFileSystemFactoryWorkspaceFileSystem(読み取り・書き込み・一覧取得のオーケストレーター)、WriteModeContentHashFsEntryFsStatListOptionsGlobOptionsReadTextOptionsReadBytesOptionsWriteTextAtomicOptions

FsErrorKind 分類

Kindデフォルト HTTP意味
path_outside_workspace400解決されたパスがバインドされたワークスペース外にある。
symlink_escape400対象がシンボリックリンクである(PR 18 + PR 20 の保守的な方針により拒否)。
path_not_found404ENOENT
binary_file422テキストルートでバイナリとして検出されたコンテンツ。
file_too_large413MAX_READ_BYTES または MAX_WRITE_BYTES を超えている。
hash_mismatch409楽観的並行制御の expectedSha256 が一致しなかった。
file_already_exists409既存のファイルに対して mode: 'create' を指定した。
text_not_found422POST /file/edit の検索文字列がファイル内に存在しない。
ambiguous_text_match4221 件のみ必要な箇所で複数の一致が見つかった。
untrusted_workspace403信頼されていないワークスペースで書き込みを試みた。
permission_denied403OS レベルの EACCES / EPERM
io_error503ENOSPC / EIO / EBUSY / ETXTBSY / ENAMETOOLONG / EMFILE / ENFILEpermission_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 の注入ポイントです。ブリッジのテストや Mode A の組み込み呼び出し元は BridgeOptions で省略できます。その場合、BridgeClient はインラインの fs.readFile / fs.writeFile プロキシにフォールバックします(F1 以前の動作を維持)。本番の qwen servecreateBridgeFileSystemAdapter(fsFactory)packages/cli/src/serve/bridge-file-system-adapter.ts)経由で BridgeFileSystem を接続するため、エージェント側の ACP 書き込みは HTTP ルートと同じ TOCTOU、シンボリックリンク、トラストゲート、監査のゲートを経由します。

アダプターが複製しなければならない防御的ゲート(アダプターが注入されるとインラインプロキシは完全にバイパスされるため):

  1. 非通常ファイルを拒否する — ソケット・パイプ・文字デバイス・procfs・sysfs エントリは stats.size === 0 であっても無制限のデータをストリームできる。インラインパスはメッセージに describeStatKind(stats) を含む例外をスローする。
  2. バッファサイズを READ_FILE_SIZE_CAP = 100 MiB で制限する — 500 MB のログに対して { line: 1, limit: 10 } という小さなリクエストを送ると、10 行を返すだけのために 500 MB の RSS を消費してしまう。

アダプターはさらに踏み込んで、アトミックな一時ファイル&リネーム書き込み(モード保持、0o600 デフォルト、パスごとのロック内でのシンボリックリンク拒否)に WorkspaceFileSystem.writeTextOverwrite(PR 18 プリミティブ)を使用します。これはシンボリックリンクを解決してその対象に書き込んでいたF1 以前のインラインプロキシからの変更点であり、シンボリックリンクを経由したドットファイルへの書き込みに依存していたエージェントは、解決済みパスを直接指定する必要があります。

ACP ワイヤー越しの FsError 保持

BridgeFileSystem アダプターが FsErrorkind: 'untrusted_workspace' / 'symlink_escape' / 'file_too_large' など)をスローすると、ACP SDK のデフォルト RPC エラーパスは error.message のみを汎用の -32603 "Internal error" としてシリアライズし、kind / status / hint は消えてしまいます。その結果、下流のエージェント RPC クライアントは型付き UI(認証再試行 vs ファイルピッカー vs プロキシヒント)のディスパッチに人間可読メッセージへの正規表現マッチが必要になります。

BridgeClient.writeTextFileBridgeClient.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.hintdata.status を受け取り、SDK の利用者はメッセージへの正規表現マッチではなく型付き列挙値で分岐できます。

設計上の注意点が 2 点あります。

  • import ではなくダックタイピングFsErrorpackages/cli/src/serve/fs/errors.ts にあり、BridgeClientpackages/acp-bridge にあります。import { FsError } を直接行うと依存関係が逆転してしまいます。ダックチェック(name === 'FsError' + kind: string)は、同じクロスパッケージバンドルの理由から mapDomainErrorToErrorKindstatus.ts)が TrustGateError / SkillError に対して行っていることと同じです。
  • JSON-RPC コードは -32603 のまま — ブリッジは FsError.kind を JSON-RPC エラーコード形式に確実にマッピングできないため、SDK 利用者向けのセマンティック情報は構造化 data フィールドに格納されます。ワイヤーステータスコード(-32603 “internal error”)は変更されず、クライアントは data.errorKind でルーティングします。

トラストゲート

assertTrustedForIntent(trusted, intent) は呼び出し元から注入されたトラスト boolean を消費します。ポリシー層は Config.isTrustedFolder() を直接読み取りません。読み取り・一覧取得・stat・glob は常に許可されます(トラストは書き込みのみに適用)。信頼されていないワークスペースでの書き込みインテントは FsError('untrusted_workspace', ..., status: 403) をスローします。トラストシグナルは WorkspaceFileSystemFactoryDeps.trusted: boolean 経由で渡されます — runQwenServe はオペレーターが暗黙的に信頼するワークスペースに対してデーモンを起動するため true を渡し、createServeApprunQwenServe なしの直接組み込み)はデフォルト false でプロセスごとに一度警告を出します(02-serve-runtime.md を参照)。

ワークフロー

読み取り

readText は無視ルールに基づいて読み取りをスキップしたり拒否したりしません。通常通りファイルを読み取り、一致した無視分類を meta.matchedIgnore に記録します。listglobincludeIgnored が有効でない場合にのみ無視された結果をフィルタリングします。

書き込み

書き込み後リネームによるアトミック操作により、書き込み途中の SIGKILL / OOM が発生しても対象ファイルが切り詰められた状態にはなりません。mode: 'create' は lstat で既存ファイルが見つかると file_already_exists で中断し、mode: 'overwrite' は続行します。expectedSha256 は楽観的並行制御を有効化します(不一致の場合は hash_mismatch)。

POST /file/edit(単一テキスト置換)

書き込みに加えて 2 つの失敗モードがあります。

  • text_not_found(422)— 検索文字列がファイル内に存在しない。
  • ambiguous_text_match(422)— ルートの契約上 1 件のみが要求される箇所で複数の一致が見つかった。

監査ファンアウト

FS_ACCESS_EVENT_TYPE / FS_DENIED_EVENT_TYPE はコンテキスト(ctx)、パス、インテント、結果、errorKind?、bytesRead/written、sha256? を持ちます。

状態とライフサイクル

  • ファクトリーはデーモン起動時に一度だけ構築されます(runQwenServeresolveBridgeFsFactory → アダプター)。
  • 各リクエストは RequestContext を構築し、その呼び出しのみのためにファクトリーのオーケストレーターを呼び出します — ファイルごとの長命な状態はありません。
  • パスごとのロックは書き込み操作の期間中のみ存在します(クロスコールロックはなく、同一パスへの並行書き込みはロックで直列化されます)。
  • 監査リングは runQwenServe が所有し、パーミッション監査パブリッシャーと共有されます。

依存関係

  • @qwen-code/qwen-code-coreIgnoreisBinaryFileConfig.isTrustedFolder()
  • node:fsnode:pathnode:crypto
  • @qwen-code/acp-bridge — ACP 側の BridgeFileSystem コントラクト。
  • HTTP ルート: packages/cli/src/serve/routes/workspace-file-read.tsworkspace-file-write.ts

設定

ソース設定値効果
WorkspaceFileSystemFactoryDeps.trusted: booleanコンストラクター入力書き込みを許可するか。runQwenServe からはデフォルト truecreateServeApp からは false(警告あり)。
定数MAX_READ_BYTES = 256 KiB読み取り上限。超えると file_too_large
定数MAX_WRITE_BYTES = 5 MiB書き込み上限。express.json({ limit: '10mb' }) より小さく設定。
定数BINARY_PROBE_BYTES = 4096コンテンツベースのバイナリ検出のサンプルサイズ。
ケイパビリティタグworkspace_file_readworkspace_file_bytesworkspace_file_write11-capabilities-versioning.md を参照。
ワークスペースファイル.gitignore.qwenignore無視されたパスは shouldIgnore から ignored: true として返される。

注意事項と既知の制限

  • シンボリックリンクはフォローせず拒否します。 これは、シンボリックリンクを解決してその対象に書き込んでいた F1 以前のインラインの BridgeClient.writeTextFile プロキシからの変更点です。シンボリックリンク経由でドットファイルに書き込んでいたエージェントは、解決済みパスを直接指定する必要があります。
  • io_errorpermission_denied は別の種類です。 混同しないでください。監視パイプラインはアラートの errorKind をキーにしており、ENOSPC を permission_denied に統合すると df -h の問題でセキュリティ担当者にアラートが送られてしまいます。
  • 新規ファイルのモードはデフォルト 0o600 であり、umask のデフォルトではありません。 書き込みシステムコールの mode 引数は umask をバイパスします。公開ファイルを書き込むエージェントは明示的にモードオーバーライドを指定する必要があります。
  • createServeApp のデフォルト trusted: false は、カスタムの fsFactorybridge を注入しない埋め込み利用者に対して、untrusted_workspace で ACP 書き込みを暗黙的に拒否します。最初の呼び出し時に一度 stderr 警告が出力され、以降の呼び出し元にはリマインダーは表示されません。02-serve-runtime.md を参照してください。
  • 読み取り上限はデコード前に適用されます。 MAX_READ_BYTES + 1 のファイルはリクエストが 10 行のみを要求していても拒否されます — これは、内部の readFileWithLineAndLimit がスライスする前にファイル全体をメモリに読み込むためです。
  • BridgeFileSystem アダプターはインラインプロキシの両方のゲートを複製しなければなりません(非通常ファイル拒否 + バッファサイズ上限)。アダプターが注入されるとインラインパスは完全にバイパスされます。

参照

  • packages/cli/src/serve/fs/index.ts(バレル)
  • 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 ルートリファレンス: ../qwen-serve-protocol.md
Last updated on