第 5 課:セキュリティ境界:触れられるもの、触れられないもの
一言でいうと:DSH のセキュリティ哲学は「デフォルトで信用しない」——プラグインは何を使うかを事前に宣言し、コマンドはサンドボックス内でラップされ、ファイル変更はポリシーイベントを通過し、シークレットは常に参照の形でしか存在しない——「何に触れられるか」はスローガンではなく、層ごとに検証できる四つの防衛線です。
1. ユーザーストーリー:見知らぬプラグインはなぜ悪さをできないのか?
コミュニティで小さなプラグインを見つけました:「ワンクリックで Markdown の整形を美化」。それはあなた自身のマシン上で動き、ファイルの読み書きができ、コマンドも実行できます。インストールする前に、素朴な疑問が浮かびます:こいつが黙って私のファイルを削除したり、ターミナル内のシークレットをこっそりどこかのサーバーに送ったりしないと、何を根拠に信じられるのか?
従来の答えは「信頼に頼る」でした:作者の評判を見て、ソースコードに一度目を通し、悪さをしないことを祈る。DSH の答えは違います——信頼ではなく、構造に頼る。プラグイン、モデル、コマンドはデフォルトでは「何にも触れられず」、何かに触れたいと思うたびにゲートを通過しなければなりません。四つの防衛線が重なり合います:
| 防衛線 | 何を管理するか | 誰が執行するか |
|---|---|---|
| ① 依存宣言 | プラグインがどのサービスと依存を取得できるか | ローダー + コンテキストプロキシ(Proxy) |
| ② プロセスサンドボックス | コマンドがホストファイルシステムのどの部分を読み書きできるか | ctx.sandbox |
| ③ ファイルポリシーイベント | 書き込みと編集は先に読む必要があり、バージョンが一致しなければならない | fs ポリシーイベントゲート |
| ④ ガードと承認 | 範囲外の操作を許可するか、どのくらいの幅で | 承認ガード / guard プラグイン |
以下の四節で一つずつ分解して見ていきます。まず「信頼の問題」に最も近い第一の防衛線から。
2. 第一の防衛線:宣言は能力であり、未宣言は拒否
第 2 章第 13 課で学んだアクセス制御を覚えていますか?Cordis の論文には重要なメカニズムがあります:コンポーネントは自分が「宣言した」依存にのみアクセスでき、未宣言のものにアクセスするとエラーになる——JavaScript のコンテキストプロキシ(Proxy)がアクセスのたびにキーが宣言リストにあるかをチェックし、なければ拒否します。これが「ケイパビリティベースセキュリティ」(capability-based security)です:権限は参照を保持することから生まれ、「この環境の人間である」ことからは生まれません。
DSH はこの原則をそのままプラグイン体系に持ち込んでいます:
- プラグインはロード時に、どの依存を注入したいかを静的に宣言します——inject 宣言はケイパビリティの要求です;
- 未宣言のアクセスは実行時にプロキシによって拒否されます:コードに書いてあっても、取得できません;
- 宣言は静的なので、ローダーはロード時に審査・承認でき、アクセスが発生してから一つずつ発見する必要がありません。
つまり「見知らぬプラグイン」が手にするのは、自分が宣言した数個のものだけです。範囲外に出たい?第一のゲートがそれを止めます。
3. 第二の防衛線:プロセスサンドボックスとファイルポリシーイベント
言語レベルのチェックでは悪意あるコードを抑えられません——コードがホストランタイムに触れられる限り、下層のオブジェクトを直接操作できてしまいます。そこで DSH は「コマンド」に着手します:プロセスをサンドボックス内で走らせるのです。
3.1 サンドボックスがしていること:argv のラップ、プロセスの制限
プロセスサンドボックスの核心は「argv のラップ」と呼ばれる動作で、リポジトリ内の記述は非常に正確です:
ctx.sandbox.confine(argv, policy)は spawn に使う argv を返し、呼び出し元の元の argv を置き換えるべきものです。戻り値はラップされており、プロセスおよびそれが派生するすべてのプロセスが制限下で動作します……利用可能なバックエンドがない場合、例外をスローし、argv をそのまま渡して制限なしで動作させることは決してありません。—— 出典:packages/sandbox/sandbox/README.zh.md
平易に言い換えると:
- 実行したいコマンドは、起動前にまず「檻」の一層に包まれます;
- そのコマンドだけでなく、それが派生するすべての子プロセスも同じ檻の中にいます——コマンドがサブコマンドを起動しても逃げられません;
- このマシンに利用可能なサンドボックスバックエンドが一つもなければ、DSH は実行を拒否します。裸で走らせることは絶対にありません。
バックエンドはプラットフォームネイティブです:Linux では bubblewrap(bwrap)または Landlock、macOS では Seatbelt(sandbox-exec)を使います。これらはホストとファイルシステムとカーネルを共有しますが、ファイルへの影響はポリシーによって厳格に制約されます。
3.2 呼び出しごとのポリシー:同じサンドボックス、異なる檻
サンドボックスは「このコマンドがどこに触れられるか」をどうやって知るのでしょうか?答えは呼び出しごとのポリシーにあります——ポリシーはサンドボックスプロバイダーに付いた固定設定ではなく、呼び出しのたびに呼び出しと一緒に渡されます。三つのモード:
| モード | できること | 典型的なシナリオ |
|---|---|---|
| read-only | 読み取り専用;書き込みはすべて拒否(/dev/null など必須の出口のみ保持) | デフォルトモード、フェイルセーフ |
| workspace-write | セッションのワークスペースルート + プラットフォームの一時領域(/tmp など)に書き込み可能 | 通常の作業 |
| danger-full-access | 制限なし | 明示的に信頼する呼び出し |
二つの要点:
- デフォルトは read-only でフェイルセーフ:ファイルを書きたければ、まずこの呼び出しが workspace-write の資格を持つことを証明しなければなりません;
- ポリシーは呼び出しと共に移動し、プロバイダーには紐付かない:bash は read-only で実行でき、制限されたサブエージェントは同時にその状態ディレクトリを書き込み可能に保てます;承認された権限昇格の再試行は、より広いポリシーで発行される新しい呼び出しにすぎません。
この三つのモードはファイル操作のみを制約することに注意してください——ネットワークやプロセスの可視性はこの語彙に含まれていません(第 5 節でそれらが誰の管轄かを説明します)。
3.3 ファイルシステムポリシーイベント:書き込み前に読む、バージョンで上書きを防ぐ
依存宣言は「プラグインの勝手な取得」を防ぎ、プロセスサンドボックスは「コマンドの勝手なファイル書き込み」を防ぎます。では、モデルがファイルツールを通じて読み書きするファイルは?もう一層あります:ファイルシステムポリシーです。
まず「サンドボックス化ファイルシステム」バックエンド(fs-sandbox)を見ましょう。これは書き込みにのみ呼び出しごとのモードフェンスを付け、素朴な原則が一つあります:読み取りは常に直接通過する——すべてのモードが読み取りを許可する。具体的には:
- read-only では、すべての変更が構造化された拒否となります(エラーコード
FS_SANDBOX_DENIED、現在のモードを伴う); - workspace-write では、ターゲットを正規化した結果が書き込み可能ルート(ワークスペースルート + プラットフォームの一時領域)の内側にある場合のみ変更が許可されます;
- danger-full-access ではフェンスなしで直接委譲します。
(出典:packages/fs/fs-sandbox/README.zh.md)
さらに一層上にあるのが、ファイルシステムスタックのポリシー層(fs-observation-policy プラグイン)です。これはサービスメソッドを一切提供せず、fs/* イベントゲートを通じてのみ参加し、「編集の衛生」を専門に管理します:
- 編集前に必ず読んでいること:あるファイルを読んでいないのに edit しようとすれば、即座に拒否されます——「edit requires reading the file first」;
- バージョン保護:書き込みと編集は観測されたバージョンに基づく CAS(compare-and-swap)を行い、ファイルが他者によって変更されていればバージョン古いエラー(
FS_STALE_VERSION)を報告し、再読み込みと再試行を促します; - 利点:モデルは古いビューに基づいてファイルを盲目的に変更することがなく、並行する書き込み者が黙って上書きされることもありません。
(出典:packages/fs/fs-observation-policy/README.zh.md)
3.4 ガードと承認:一度止められた範囲外の書き込み
最後のゲートは「権限昇格」の経路上にあります。モデルは拒否に遭った後、一度だけより広い権限での再試行を発行できます——しかしこの再試行はガードと承認を通過しなければなりません。ガード(guard)プラグインはエージェントループのループ衛生(繰り返されるツール呼び出しへの助言的なリマインダー、単一呼び出しの時間予算)を監視する役割で、より広いモードへの切り替え自体は明示的なイベントです:セッションがモードを切り替えるとは、sandbox/mode イベントを一件追加することです;承認された権限昇格の再試行は、より広いポリシーで発行される新しい呼び出しにすぎません。
チェーン全体をたどると、完全な実例になります(ポリシー設計はリポジトリの fs-sandbox と sandbox-policy に由来):
モデルが app/config.json に書き込みたい
↓ 現在のポリシーは read-only
↓ 書き込み拒否:FS_SANDBOX_DENIED、[sandbox: file access denied under read-only mode] としてレンダリング
↓ ツール層が一度きりの権限昇格再試行のガイドを提示
↓ ガードと承認:この呼び出しは資格があるか?
↓ 承認 → workspace-write で新しい呼び出しを発行、ターゲットはワークスペース内 → 書き込み成功
どのステップも裏付けがあります:ポリシー解決の結果、拒否の理由、承認の決定は、すべてセッションログに記録されます。
沙箱 + 策略 + 审批:智能体只碰它被允许碰的东西
4. 第三の防衛線:設定とクレデンシャル
前の二つの防衛線は「コードが何に触れられるか」を管理します。第三は「設定の中に何があるか」を管理します——特にシークレットです。
4.1 設定:プラグインごとの名前空間
DSH のユーザー設定は登録された名前空間を通じて解決されます:各プラグインは settings に自分の名前空間を登録し、互いに干渉しません。settings ファミリーは「名前空間の登録、階層的な解決、コミットを定義する」サービスと、「設定をローカルファイルに保存し外部からの編集を監視する」プロバイダーで構成されます——あるプラグインの設定項目が、別のプラグインの設定項目になりすますことは決してありません。
(出典:packages/settings/README.zh.md)
4.2 クレデンシャル:名前付きシークレット参照、インラインは絶対禁止
設定の中で最も危険なものはシークレットです。DSH のルールはただ一つで、クレデンシャルサービス定義の README の冒頭に書かれています:
設定は機密への参照のみを持ち、機密そのものを決して持ちません。settings のセクションや cordis.yml のエントリには
apiKeyEnv: DEEPSEEK_API_KEYと書き、参照の背後にある値はクレデンシャルプロバイダーが所有します。—— 出典:packages/credentials/credentials/README.zh.md
つまり:設定の中に sk-xxx のような平文が現れることは決してありません。あなたが書くのは名前付き参照(例:apiKeyEnv: DEEPSEEK_API_KEY)です;実際の値はクレデンシャルプロバイダーのところに保管されます(ローカルでは $DSH_HOME/.credentials.yaml で、0700 のディレクトリ内に 0600 パーミッションで保存)。参照は各操作の開始時に解決され、操作をまたいでキャッシュされることは決してありません——変更されたクレデンシャルは、プラグインを一切再起動せずに次のリクエストから有効になります。
これにより三つの素朴な利点が生まれます:
- 設定ドキュメントは安心して同期し、安心して設定画面にレンダリングできる——中に機密が一切ないから;
- キーのローテーションは設定ファイルに一切触れない——変えるのはクレデンシャルプロバイダーであって、設定ではないから;
- 空の保存値は存在しないのと同じ——空白が設定済みのシークレットを偽装することは決してないから。
5. サンドボックスの外:信頼できないコードには分離されたランタイムが必要
前の三つの防衛線はすべて一つのことを前提にしています:コードはホストとファイルシステムとカーネルを共有しており、だから「ポリシー」で管理できる。しかし、コードが根本的に信頼できない場合はどうでしょう?第 2 章第 13 課で論文 5.2 節の結論を学びました:信頼できないコードは、分離境界の外に投げ出さなければならない——言語レベルのチェックでは悪意あるコードを抑えられず、真の分離には言語レベル以外の実行境界が必要です:ソフトウェア故障分離、分離された言語ランタイム、サンドボックス化プロセス、仮想化コンテナ。
DSH の対応する設計も同様に潔いものです。サンドボックスサービスの README には明確に書かれています:
ホストとファイルシステムとカーネルを共有する制限のみをサポートします。コンテナ、microVM、リモートエグゼキューターはこの seam のバックエンドではありません:それらは環境一貫のグループとしてケイパビリティ seam 全体の Service provider(ctx.shell、ctx.fs)を置き換えます。
—— 出典:packages/sandbox/sandbox/README.zh.md
言い換えると:bwrap、Landlock、Seatbelt といった仕組みは、ホストとファイルシステムとカーネルを「共有する」ことを前提としたポリシー制限です;デプロイ側が求めるのが完全な分離(コンテナ、microVM、リモート実行)であるなら、それはサンドボックスにバックエンドを一つ追加する話ではなく、実行ケイパビリティ全体(bash、fs)を別の実装に交換する話です。ファイルシステムバックエンドの README も同じ立場です——「ポリシーのフェンスであって、カーネル境界ではない」:制約を提供しますが、セキュリティ境界ではありません;信頼できないコードのカーネルレベルの分離は、依然として実行ケイパビリティの責任です。
だからセキュリティトポロジー図を見るときは、まず問いかけてください:そのコードは「制限された隣人」か、それとも「別世界に閉じ込められた囚人」か? 前者はサンドボックスポリシーに頼り、後者は実行世界の交換に頼ります——DSH は両方の道を用意しています。
6. 要点の振り返り
- デフォルトで信用しない:ケイパビリティは宣言と参照から生まれ、「環境の中にいる」ことからは生まれません。
- 宣言はケイパビリティの要求:プラグインは宣言した依存にのみアクセスでき、未宣言のアクセスは Proxy に拒否され、ロード時に審査・承認できます。
- サンドボックスは argv をラップする:プロセスとその子孫はすべて制限下で動作します;利用可能なバックエンドがなければ実行を拒否し、裸で走らせることは絶対にありません。
- 呼び出しごとのポリシー:read-only(デフォルト、フェイルセーフ)/workspace-write/danger-full-access、ポリシーは呼び出しと共に移動します。
- ファイルポリシーイベント:書き込み前に必ず読む、バージョン CAS で上書きを防止;拒否は構造化エラーであり、静かな失敗ではありません。
- シークレットはインラインしない:settings はプラグインごとに名前空間化;credentials は名前付き参照で、操作ごとに解決され、値はプロバイダーが所有します。
- サンドボックスは万能ではない:信頼できないコードには実行世界の交換(コンテナ/microVM/リモート実行)が必要であり、ポリシーを追加するだけでは足りません。
🚀 次の課では「知覚とコンテキスト」に入ります:モデルはどうやってコードベース、Web ページ、実行中の環境を「見る」のか——そして、これらの知覚チャネルも同様にポリシーと承認に制約されていること。
セルフテスト · セキュリティ境界
回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。
