Read OSS

設計によるセキュリティ: ポリシーエンジン、サンドボックス、セーフティチェッカー

上級

前提知識

  • 第3回: ツールとスケジューラー
  • セキュリティサンドボックスの基本概念
  • TOML 設定フォーマットの基礎知識

設計によるセキュリティ: ポリシーエンジン、サンドボックス、セーフティチェッカー

シェルコマンドを実行したりファイルを編集したりできるエージェント型コーディングアシスタントには、堅牢なセキュリティが欠かせません。Gemini CLI はこの課題に対して、三層の防御機構を用意しています。ツールの動作を制御するルールベースのポリシーエンジン、実行環境そのものを制約するプラットフォーム固有のサンドボックス、そしてコンテキストを踏まえた判断を加えるプラガブルなセーフティチェッカーです。この記事では、各レイヤーを順に掘り下げていきます。

PolicyEngine: ルールベースのアクセス制御

PolicyEngine はセキュリティの第一関門です。ツール呼び出しを優先度順に並んだルールリストと照合し、ALLOWDENYASK_USER のいずれかを返します。

ルールは TOML ファイルから読み込まれ、優先度の高い順にソートされます。ツール呼び出しが来ると、エンジンは最初にマッチしたルールの決定を返します。設定の例を示します。

[[rules]]
toolName = "shell"
decision = "allow"
commandPrefix = "git status"
priority = 100

[[rules]]
toolName = "shell"
decision = "deny"
commandPrefix = "git push --force"
priority = 200

[[rules]]
toolName = "mcp_myserver_*"
decision = "ask_user"
priority = 50

エントリーポイントとなる check() メソッド (line 488) はいくつかの複雑な処理を担っています。

  • MCP サーバー名の解決 — ツールのアノテーションからサーバー名を取り出すか、mcp_serverName_toolName 形式の FQN 文字列をパースします
  • Args パターンマッチング — キーをソートした安定した JSON 文字列化で argsPattern の正規表現を照合します
  • ツールエイリアスの解決getToolAliases() を使って現在の名前とレガシーな名前の両方をチェックします
  • シェルコマンドの特別処理 — シェルツールの呼び出しを検出し、コマンドレベルのパースに委譲します
flowchart TD
    A[PolicyEngine.check(toolCall)] --> B[Resolve serverName]
    B --> C[Compute stringified args]
    C --> D{Is shell command?}
    D -- Yes --> E[Parse command for<br/>per-command policy]
    D -- No --> F[Match against rules]
    E --> F
    F --> G{First matching rule?}
    G -- Found --> H[Return rule.decision]
    G -- None --> I{Safety checkers match?}
    I -- Yes --> J[Run checker]
    I -- No --> K[Return default decision]
    J --> L[Return checker result]

line 79ruleMatches() によるルールマッチングは、次の条件をサポートしています。

  • 承認モードのフィルタリング — ルールに適用するモード(DEFAULTYOLOAUTO_EDITPLAN)を指定できます
  • サブエージェントのスコープ指定 — 特定のサブエージェントを対象にしたルールを定義できます
  • MCP サーバーのマッチングmcpName フィールドまたはワイルドカードパターンで指定します
  • アノテーションのマッチング — 特定のツールアノテーションを持つ呼び出しだけにルールを適用できます
  • インタラクティブ/非インタラクティブのフィルタリング — パイプ経由か端末からの実行かで異なるルールを設定できます

引数レベルのポリシーのためのシェルコマンドパース

git statusgit push --force は同じ shell ツールを使うコマンドですが、リスクプロファイルは大きく異なります。ポリシーエンジンはシェルコマンドを専用にパースし、引数レベルでポリシーを評価することでこの問題に対応しています。

シェルツールの呼び出しが検出されると、エンジンは次の手順を踏みます。

  1. ツール呼び出しから command 引数を取り出す
  2. splitCommands() を使って複合コマンド(cmd1 && cmd2 など)を分割する
  3. shell-quote ライブラリの shellParse() でサブコマンドをパースする
  4. commandPrefix パターンに従って各サブコマンドをルールと照合する

これにより、シェルコマンド全体を一律に許可・拒否するのではなく、「npm test は許可するが npm publish はブロックする」といった精度の高いポリシーが実現できます。ルールの commandPrefix フィールドには、文字列と配列のどちらでも指定できます。

flowchart TD
    A["shell tool call:<br/>npm test && npm publish"] --> B[splitCommands]
    B --> C["Sub-command 1: npm test"]
    B --> D["Sub-command 2: npm publish"]
    C --> E{Match rules}
    D --> F{Match rules}
    E --> G["Rule: allow npm test → ALLOW"]
    F --> H["Rule: deny npm publish → DENY"]
    G --> I{Both sub-commands}
    H --> I
    I --> J["Final: DENY<br/>(most restrictive wins)"]

ヒント: シェルコマンド向けの TOML ポリシーを書くときは、argsPattern ではなく commandPrefix を使いましょう。パイプチェーンや &&|| 演算子を含む複雑なシェル構文も、commandPrefix であれば正しく処理できます。文字列化した args への生の正規表現マッチングでは、複雑なシェル構文に対応できません。

プラットフォーム固有のサンドボックス

サンドボックスレイヤーは OS レベルで機能し、プロセス全体がアクセスできる範囲を制限します。SandboxManager インターフェースが契約を定義し、プラットフォームごとに実装が用意されています。

MacOsSandboxManager は macOS の seatbelt プロファイル(sandbox-exec システム)を使い、ファイルシステムへのアクセス、ネットワーク通信、システムコールを制限します。主なメソッドを見ていきましょう。

  • isKnownSafeCommand(args)lscat など安全と判明しているコマンドを高速にパスします。モード設定の approvedTools リストも参照します。
  • isDangerousCommand(args) — 明らかに危険な操作を検出します。
  • prepareCommand(req) — コアとなるメソッドです。環境変数のサニタイズ、パーミッション(読み取り専用モード、ワークスペースへの書き込み、ネットワークアクセス)に基づく seatbelt プロファイルの構築、そして sandbox-exec を使ったコマンドのラッピングを行います。
graph TD
    subgraph "Platform Sandbox Managers"
        MAC[MacOsSandboxManager<br/>seatbelt profiles]
        DOCKER[Docker/Podman<br/>container isolation]
        WIN[WindowsSandboxManager<br/>Windows sandbox]
        NOOP[NoopSandboxManager<br/>no restrictions]
    end
    
    SI[SandboxManager Interface]
    SI --> MAC
    SI --> DOCKER
    SI --> WIN
    SI --> NOOP
    
    POLICY[SandboxPolicyManager<br/>persistent permissions]
    MAC --> POLICY
    DOCKER --> POLICY

macOS 実装では仮想コマンドの変換も行います。__read/bin/cat に、__writecat > "$1" パイプラインに変換され、制御されたプリミティブを通じてサンドボックス内のファイルアクセスを実現します。

Linux では Docker または Podman コンテナが隔離環境を提供します。サンドボックスの判断は起動シーケンスの早い段階で行われます(第1回で触れたとおりです)。サンドボックスが有効で、かつまだサンドボックス内にいない場合、プロセスは自分自身をコンテナ内で再起動します。

セーフティチェッカー: CheckerRunner と Conseca

ルールベースのポリシーに加えて、Gemini CLI はプラガブルなセーフティチェッカーをサポートしており、ツール呼び出しに対してより細かい判断を加えられます。CheckerRunner は二種類のチェッカーを調整します。

インプロセスチェッカー — Node.js プロセス内で直接実行されます。CheckerRegistry が名前からチェッカーを解決し、ContextBuilder が必要なコンテキストを組み立て、チェッカーが ALLOWDENYASK_USER のいずれかを返します。

外部チェッカー — タイムアウト付き(デフォルト5秒)で別プロセスとして実行されます。ツール呼び出しの詳細が JSON として標準入力に渡され、標準出力が Zod スキーマに対してパースされます。

const SafetyCheckResultSchema = z.discriminatedUnion('decision', [
  z.object({ decision: z.literal(SafetyCheckDecision.ALLOW), reason: z.string().optional() }),
  z.object({ decision: z.literal(SafetyCheckDecision.DENY), reason: z.string().min(1) }),
  z.object({ decision: z.literal(SafetyCheckDecision.ASK_USER), reason: z.string().min(1) }),
]);

セーフティチェッカーのルールは TOML 設定ファイル内にポリシールールと並べて定義します。独自の優先度ソートとマッチングロジック(ツール名のワイルドカードや args パターンを含む)を持ちますが、返す値はポリシーパイプラインにフィードバックされる SafetyCheckDecision です。

ConsecaSafetyChecker はインプロセス実装の一例で、ヒューリスティクスと会話のコンテキストを使ってツール呼び出しを評価します。有効化フラグ(enableConseca)は Config から伝播します。

承認モードと確認フロー

Gemini CLI にはツールの自動承認の積極性を変える四つの承認モードがあります。

モード 動作
DEFAULT 破壊的なツールはすべてユーザー確認が必要
YOLO すべてを自動承認(信頼できる環境向け)
AUTO_EDIT ファイル編集は自動承認、シェルコマンドは確認
PLAN 読み取り専用モード。書き込みと実行をすべてブロック

これらのモードはポリシールールの modes フィールドを通じて連携します。あるルールを特定のモードのみに適用することが可能です。たとえば YOLO モード用のルールには、すべてを上書きするために priority: PRIORITY_YOLO_ALLOW_ALL を指定できます。

確認フローはすべてのレイヤーを統合しています。

stateDiagram-v2
    [*] --> PolicyCheck: Tool call arrives
    PolicyCheck --> AutoAllow: ALLOW
    PolicyCheck --> AutoDeny: DENY
    PolicyCheck --> CheckerPhase: ASK_USER
    
    CheckerPhase --> AutoAllow: Checker says ALLOW
    CheckerPhase --> AutoDeny: Checker says DENY
    CheckerPhase --> HookPhase: Checker says ASK_USER
    
    HookPhase --> AutoAllow: Hook says proceed
    HookPhase --> AutoDeny: Hook says block
    HookPhase --> UIConfirmation: Hook says ask user
    
    UIConfirmation --> Executed: User approves
    UIConfirmation --> Cancelled: User denies
    UIConfirmation --> PolicyUpdate: User says "always allow"
    
    PolicyUpdate --> Executed: Update rules, then execute
    
    AutoAllow --> Executed
    AutoDeny --> Cancelled

ユーザーがツールに対して「常に許可」を選択すると、スケジューラーの updatePolicy() が適切な粒度で新しいルールを作成します。シェルコマンドなら commandPrefix を、MCP ツールなら mcpName をキャプチャします。「常に許可して保存」を選べば、このルールをディスクに永続化することもできます。

ヒント: PolicyEngine の disableAlwaysAllow フラグを有効にすると、「常に許可」ルールが効かなくなります。管理者がポリシーの制御を維持したい環境で役立ちます。このフラグをはじめとするセキュリティ関連の設定は PolicyEngineConfig を確認してみましょう。

このセキュリティアーキテクチャの強みは多層構造にあります。ポリシールールが粗粒度の制御を担い、サンドボックスが実行環境を制約し、セーフティチェッカーがコンテキストを踏まえた判断を加えます。各レイヤーが独立してツール呼び出しをブロックでき、自動判断だけでは不十分なときに確認フローがユーザーへ制御を戻します。

次回は、このセキュリティ基盤の上に構築された拡張性の仕組みを探ります。フック、スキル、MCP インテグレーション、そして拡張機能のパッケージングシステムを取り上げる予定です。