Python - TECH PLAY - TECH PLAY

TECH PLAY

Python

Pythonは明確で読みやすい構文を持っているため、プログラミング初心者にもおすすめの言語です。また多くのコミュニティがあり、それぞれがライブラリ開発やフレームワーク開発に貢献しています。

イベント

マガジン

技術ブログ

こんにちは、サイオステクノロジー武井です。 AI コーディングエージェントを使い始めて、ふと不安になったことはないでしょうか。 「このAI、プロジェクトの中を自由に読めるけど…… .env   みたいな機密情報も読めてしまうのでは?」 そうなんです。読めちゃうんです。しかも、ただ読めるだけでは終わりません。読んだ内容がドキュメントに紛れ込んだり、コミットに含まれたりして、 GitHub リポジトリ経由で外部に漏れる ところまで想像すると、なかなか怖い話です((((;゚Д゚))))ガクガクブルブル この記事では、その対策として用意されている Claude Code の権限機能とサンドボックス機能を取り上げます。ただ「こう設定すれば安全」という話で終わらせるのではなく、 なぜその機能が必要なのか、どこまで守れて、どこに穴が残るのか を、OS の仕組みまで降りて整理していきます。途中で Linux カーネルの   fork / exec   という基本機能まで出てきますが、そこが腑に落ちると、サンドボックスが「なぜそういう形をしているのか」まで見通せるようになります。 なお、以降は Claude Code を題材に話を進めますが、 基本的な仕組みは他のコーディングエージェントでも多分同じ   です。設定ファイルの名前や項目名は製品ごとに違いますが、どれも「エージェント本体のプロセスがファイルを読む」「シェルコマンドを子プロセスとして起動する」という同じ形で動いていて、OS が用意している道具立て(macOS の Seatbelt、Linux の namespace など)も共通です。この記事で扱うのは、その   共通部分の構造   です。「アプリが自分で我慢しているのか、OS が強制しているのか」という見分け方を持っておけば、お使いのツールがどちらの層で守ってくれているのかも、同じ物差しで判断できるようになります。 何も気をつけないと、AIは機密情報を読んでしまう まず、問題の全体像を確認します。 多くのプロジェクトでは、API キーやデータベースの接続文字列といった秘密情報を   .env   ファイルに書いて、プロジェクト直下に置いています。ローカルで開発する分には便利な運用です。 ところが、AI コーディングエージェントはプロジェクト内のファイルを読んで文脈を理解します。つまり、 放っておくと   .env   も読める ということです。そして厄介なのは、読んだ後に何が起きるか予測しづらい点です。 「README を書いて」と頼んだら、環境変数の設定例として本物のキーが埋め込まれる コード生成の過程で、秘密情報を含むファイルを参照した内容がコメントに残る それらをコミットして   git push   した瞬間、 公開リポジトリに秘密が載る 「AIが勝手に漏らした」というより、「AIが読めたものが、生成物を経由して、いつの間にか外に出ていく」という流れです。悪意がなくても起きるからこそ、仕組みで防いでおく必要があります。 まず思いつく対策 ―― でも、これには穴がある Claude Code には、こうした事故を防ぐための権限機能があります。 settings.json   の   permissions.deny   に、読ませたくないファイルを書いておく方法です。 { "permissions" : { "deny" : [ "Read(./.env)" , "Read(./.env.*)" ] } } これで   Read   ツールから   .env   へのアクセスがブロックされます。 Glob   や   Edit   など、ファイルに触れる他のツールについても同様に指定できます。 一見これで安心に見えますが、 大きな穴があります 。 permissions.deny   がブロックしているのは、あくまで   Claude Code というプログラム自身が持つ機能(Read ツールなど)   です。言い換えると、これは   アプリケーションが自分で「このファイルは読まない」と自制しているだけ   なのです。 では、その自制を回避する経路があったらどうなるでしょうか。あります。そう、それは、、、、なんと!! Bash ツール です。 cat .env   も   diff .env other   も、Read ツールを使っていません。 別プロセスとして起動されるシェルコマンド です。だから「Read ツールを塞ぐ」というアプリレベルの自制は、ここには効きません。正面玄関に鍵をかけても、裏口が開いている状態です。 この「裏口」を塞ぐために登場するのが、サンドボックスです。 サンドボックスとは何か ―― システムコールそのものを止める アプリの自制ではなく、OSの強制 permissions.deny   が「アプリの自制」だったのに対し、サンドボックスは   OS レベルの強制   です。 Claude Code が Bash コマンドを実行するとき、そのコマンドを   サンドボックスで包んで起動   します。すると、そのコマンドは「特定のファイルにアクセスするシステムコール自体を呼べない」状態になります。 cat   だろうが   diff   だろうが Python スクリプトだろうが、ファイルを読むには必ず OS(カーネル)に   open()   というシステムコールを発行しなければなりません。サンドボックスはこの   カーネルへの入り口   で検問を張るので、どんなプログラムを使っても迂回できません。 どのレイヤーで検問しているのか 「アプリの自制」と「OS の強制」は、ソフトウェアの層で見るとまったく違う場所に立っています。図にすると、こうです。 上から順に見ていきます。 アプリケーション層 :   permissions.deny   はここにいます。この層の特徴は、 判定ロジックも判定対象も同じプロセスの中にある   ことです。Claude Code が「 .env   を読もうとしている自分」を、自分のコードで止めている。つまり自主規制です。自主規制の弱点は明確で、 そのルールを持っていないコードに仕事を渡せば、ルールは適用されない   こと。 cat .env   は Claude Code のコードではないので、 deny   の判定ロジックをそもそも通りません。 システムコールの境界 :   プロセスは自分の中では何でもできますが、 ファイルを読む、ネットワークに出る、といった「外の世界に触る操作」だけは自分では実行できません 。必ず   open()   connect()   といったシステムコールでカーネルに依頼する必要がある。ここがプロセスの自由が終わる線です。 そして重要なのは、この線が   プログラムの種類に依存しない   こと。 cat   でも   diff   でも Python でも、自作のバイナリでも、ファイルを読むなら全員この一本の線を越えます。だから、この線の上に検問を置けば、 「別のプログラムを使う」という迂回が原理的に成立しません 。アプリ層の検問が「使うツールを変える」で抜けられたのと、ちょうど対照的です。 OS カーネル層 :   実際の判定はこの層で行われます。 sandbox-exec   が登録したポリシーは   カーネル側にそのプロセスの属性として記録され 、以降そのプロセスがシステムコールを発行するたびにカーネルが照合します。 ここで立場が逆転しています。アプリ層では「自分が自分を止めていた」のに対し、カーネル層では   止める側が、止められる側から手出しできない場所にいる 。だから「自制」ではなく「強制」と呼べるわけです。実装は OS ごとに違いますが、性質は同じです。 macOS (Seatbelt) : カーネル内のフックでシステムコールごとにポリシーを照合し、許可されないアクセスを拒否する Linux (bubblewrap) : namespace を使って、 そのプロセスから見えるファイルシステムの景色自体を作り替える 。拒否する以前に、 .env   が存在しない世界を見せる 前者は「頼んでも断られる」、後者は「頼む対象がない」という違いですが、どちらも   プロセスの外側で決着している   点が共通です。 ハードウェア層 :   ここが「関与しない」になっているのは、単なる余白ではなく   限界の宣言   です。サンドボックスの強制力は「カーネルが正しく動いていること」に全面的に依存しています。カーネルの脆弱性を突かれて特権を取られた場合、判定者そのものが倒れるので、サンドボックスも一緒に倒れます。 つまりサンドボックスは、 「うっかり   cat .env   される」を確実に防ぐ道具 であって、「カーネルまで攻略してくる攻撃者」を想定した壁ではない、という位置づけです。 この「境界の上か下か」という視点は、後半で出てくる抜け穴を理解するときにも効いてきます。 どのプロセスに効いているのか では、その強制は Claude Code のどのプロセスに効いているのでしょうか。中の構造はこうなっています。 ポイントは、 Bash ツールは「起動時に明示的にサンドボックスでラップする」から拘束される   という点です。裏を返せば、ラップされていないものには効きません。ここが後半の伏線になります。 なぜ Read ツールにはサンドボックスが効かないのか 「システムコールを止めるなら、Read ツールも止められるのでは?」と思うかもしれません。ここが理解の勘所です。 サンドボックスは   プロセスに貼り付ける制約   です。プロセスが生成される瞬間に適用され、そのプロセスと、さらにその子プロセスに効きます。 一方、Read ツールは   Claude Code 本体プロセスに組み込まれた機能   で、 子プロセスを作りません 。本体が自分で   open()   を呼んでファイルを読むだけです。つまり、サンドボックスを「貼り付ける対象となる新しいプロセス」がそもそも生まれない。だから、Read ツールを守りたければ本体ごとサンドボックスに入れるしかなく、それをやると本体の正常動作(設定の読み書きや通信)まで壊れてしまいます。 だから役割分担になっているのです。 Read などの本体機能   →   permissions.deny (アプリの自制)で守る Bash が起動する子プロセス   → サンドボックス(OSの強制)で守る 両方揃えて初めて、正面玄関も裏口も塞がる   わけです。 設定はこう書く ―― サンドボックスを有効にする 仕組みの話が続いたので、ここで一度、実際の設定を見ておきましょう。 まずは   /sandbox   で様子を見る 一番手軽なのは、セッション中にスラッシュコマンドを打つ方法です。 /sandbox サンドボックスの設定パネルが開き、モードの選択(サンドボックス内のコマンドを自動承認するか、通常の権限確認を残すか)や、現在の設定内容を確認できます。ここで選んだ内容は、そのプロジェクトの   .claude/settings.local.json   に保存されます。 Linux で必要なパッケージが足りていない場合は Dependencies タブが出て、何が足りないかを教えてくれます。まずはこれを開いてみるのが早いです。 settings.json に書く すべてのプロジェクトで有効にしたいなら、ユーザー設定   ~/.claude/settings.json   に書きます。 { "sandbox" : { "enabled" : true } } これだけです。以降、Bash ツールが起動するコマンドは、次の節で見る「 fork   して、隙間で設定して、 exec 」という流れでラップされて動きます。 なお、動く環境には条件があります。 macOS : OS 内蔵の Seatbelt を使うので、追加インストールは不要 Linux / WSL2 :   bubblewrap (ファイルシステム隔離)と   socat (ネットワーク中継)が必要 sudo apt-get install bubblewrap socat # Ubuntu / Debian ネイティブの Windows は非対応で、WSL2 の中で動かす必要があります。 重要 ―― 有効にするだけでは   .env   は読めたままです そして、この記事のテーマにとって一番大事な注意点です。 "enabled": true   にしただけでは、 .env   の読み取りは止まりません。 デフォルトのポリシーは、読みと書きで非対称になっています。 書き込み : 作業ディレクトリ(と一時ディレクトリ)だけ許可 読み取り :   マシン全体が許可 (一部の拒否ディレクトリを除く) つまり、デフォルトのサンドボックスが主に想定しているのは「作業ディレクトリの外を勝手に書き換えられること」と「知らないドメインに通信されること」の防止です。 読み取りについてはかなり緩く、 ~/.aws/credentials   や   ~/.ssh/   すら読めます 。 ですから、 .env   を守りたければ、読み取り拒否を明示的に書く必要があります。 { "sandbox" : { "enabled" : true , "filesystem" : { "denyRead" : [ "./.env" ] } , "credentials" : { "files" : [ { "path" : "~/.aws/credentials" , "mode" : "deny" } , { "path" : "~/.ssh" , "mode" : "deny" } ] , "envVars" : [ { "name" : "GITHUB_TOKEN" , "mode" : "deny" } ] } } } filesystem.denyRead : サンドボックス内のプロセスからの読み取りを、OS レベルで拒否する credentials.files : 同じことを「秘密情報」としてまとめて書ける枠( "mode": "deny" ) credentials.envVars :   サンドボックス実行前に、その環境変数を消す 。ファイルを塞いでも、同じ秘密が環境変数に入っていては意味がないので、ここも大事です パスの書き方には癖があります。 sandbox.filesystem.*   は一般的な慣習どおりで、 /tmp/build   が絶対パス、 ~/   がホーム、 ./   がプロジェクトルートです。ただし   ./   がプロジェクトルートを指すのは   プロジェクト設定( .claude/settings.json )に書いた場合だけ   で、ユーザー設定に同じものを書くと   ~/.claude   からの相対になってしまいます。上の例のような   ./.env   は、プロジェクト側に置いてください。 2箇所に書くことになる ここで、2章の   permissions.deny   と並べてみると、役割分担がはっきりします。 守りたい経路 書く場所 効いている層 Read / Edit / Glob(本体の機能) permissions.deny アプリケーション層(自制) Bash が起動する子プロセス sandbox.filesystem.denyRead   /   sandbox.credentials OS カーネル層(強制) 同じ   .env   を守るために、 2箇所に書く ことになります。冗長に見えますが、これは重複ではありません。 効いている層が違うので、片方だけでは片方の経路しか塞げない のです。さきほどの「両方揃えて初めて塞がる」を、設定ファイルの言葉に翻訳するとこうなる、というわけです。 サンドボックスの正体 ――   fork   と   exec ここで、サンドボックスが具体的にどう「プロセスに制約を貼る」のかを見ておきます。実はこれは、特別な魔法ではなく   Linux カーネルの基本的な仕組み   をそのまま使っています。 Unix / Linux では、「プロセスを作る」と「プログラムを実行する」が   別々のシステムコール   に分かれています。 fork() → 今のプロセスを複製する。新しいプロセスができる exec() → 今のプロセスの「中身」を、別のプログラムに丸ごと入れ替える exec()   が独特です。新しいプロセスを作るのではなく、 自分自身の中身を捨てて、別のプログラムに変身する   のです。器(プロセス)はそのまま、中の人だけが入れ替わる。プロセス番号(PID)も変わりません。 一番わかりやすい確認方法があります。ターミナルで試してみてください。 echo "PID: $$ " exec bash # 新しい bash に変身する echo "PID: $$ " # ← PIDが変わっていない! 新しいシェルになったはずなのに PID が同じ。「変身」が起きている証拠です。 この仕組みが、サンドボックスの土台になっています。 sandbox-exec (macOS の場合)のようなラッパーは、次の順番で動きます。 1. fork() で子プロセスを作る(この時点では sandbox-exec のコード) 2. カーネルにサンドボックスのポリシーを登録する 3. exec("bash") で自分自身を bash に変身させる ↑ ここで「sandbox-exec プロセス」は消え、同じ器が bash になる 4. 以降、その bash には制約が貼り付いた状態 つまり、 「サンドボックスというプロセスが監視役として残る」わけではありません 。 fork   で複製し、 exec   で目的のコマンドに変身するときに、 制約という「属性」だけがそのプロセスに刻まれて残る   のです。制約はカーネル側でそのプロセスに紐づいて記録されるので、 exec   で中身が入れ替わっても消えません。むしろ、消えずに引き継がれるからこそ、この仕組みが成立します。 なぜわざわざ「複製してから変身」という回りくどいことをするのか。理由は、 fork   と   exec   の間に「設定をするための隙間」ができる   からです。この隙間で「サンドボックスのポリシーを登録する」「作業ディレクトリを変える」「環境変数を設定する」といった細工を、普通のコードとして自由に挟み込めます。 env (環境変数を設定して実行)や   nice (優先度を変えて実行)といったおなじみのコマンドも、実は全部この「隙間で細工してから   exec   する」という同じパターンで作られています。サンドボックスは、その一族の一員にすぎないのです。 そして、この制約は   子孫プロセスに継承されます 。サンドボックス化された Bash が   cat   を呼べば、 cat   も同じ制約を受け継ぐ。だから「Bash 経由なら何を使っても読めない」が成立するわけです。 疑似コードで見る ―― サンドボックスが適用される瞬間 ここまでの話を、コードの形で一度に見てみましょう。ラッパーがやっていることは、だいたいこういう形です。 if ( fork ( ) == 0 ) { // 子プロセスの側 // ここは「まだ自分のコード」なので、好きなことができる chdir ( "/tmp" ) ; // 作業ディレクトリを変える setuid ( 1000 ) ; // 権限を落とす close ( 0 ) ; open ( "input.txt" , . . . ) ; // 標準入力を差し替える setenv ( "LANG" , "C" , 1 ) ; // 環境変数を設定 sandbox_init ( profile , . . . ) ; // サンドボックスを適用 exec ( "/bin/ls" , . . . ) ; // ここで初めて ls に化ける } fork()   の戻り値が   0   になる側が子プロセスです。そして   この   if   の中、 exec()   までの数行が、さきほど言った「隙間」   です。この時点でプロセスの中身はまだラッパー自身のコードなので、特別な仕掛けは何もいりません。普通の関数呼び出しとして準備ができます。 注目してほしいのは、並んでいる5行が   全部同じ性質の操作   だということです。 chdir   でも   setuid   でも   setenv   でも   sandbox_init   でも、設定している相手は共通で、 「これから   ls   になる、いまの自分」   です。 つまり、 サンドボックスの適用は「特殊な起動方法」ではありません 。 chdir()   や   setenv()   と同じ列に、同じ資格で並んでいる   ただの1行   です。3章の冒頭で「サンドボックスは   env   や   nice   の一族にすぎない」と書いたのは、こういう意味です。 そして最後の   exec("/bin/ls") 。ここでプロセスの中身が   ls   に入れ替わりますが、 直前に設定したものは全部残ります 。 カレントディレクトリは   /tmp UID は 1000 標準入力は   input.txt 環境変数は   LANG=C そして、サンドボックスのポリシーも ls   のコードは、自分がこれらを設定された覚えなどありません。それでも、 生まれた瞬間からその条件下にいる 。ここがサンドボックスの本質です。 ls   はサンドボックスの存在を知らないし、協力することも拒否することもできません。制約は「 ls   が守るべきルール」ではなく、 「 ls   が置かれている環境」   だからです。 だからこそ、 ls   を   cat   に変えても、自作のバイナリに変えても意味がない。ここが、アプリケーション層の自制( permissions.deny )との決定的な違いです。 もうひとつ、 順番が命   だという点も押さえておきたいところです。 sandbox_init()   は必ず   exec()   より前になければいけません。「 exec   した後に設定すればいい」は成立しません。 exec   の後には   もう自分のコードが存在しない   からです(中身は   ls   になっている)。設定するチャンスは   fork   と   exec   の間、この隙間しかないのです。 Claude Code が Bash ツールを起動するときも、やっていることはこれと同じです。逆に言えば ――   この隙間を通らずに起動されたプロセスには、当然、何の設定もされていない 。これが次章の話につながります。 さらに残る抜け穴 「Read は   permissions.deny 、Bash はサンドボックス。これで完璧」と言いたいところですが、 まだ抜け穴があります 。 そのひとつが   MCP サーバー   です。MCP サーバーは、AI に外部ツール(ファイル操作、DB アクセスなど)を持たせるための仕組みで、Claude Code とは別のプロセスとして起動します。 「別プロセスなら、サンドボックスで守られるのでは?」と思うところですが、そうはなりません。もう一度、先ほどの図を見てください。 鍵は、図の一番上に書いてある   「Claude Code 本体はサンドボックス未適用」   という点です。 サンドボックスは「制約を持つプロセスから子へ継承される」仕組みでした。ところが本体自身は制約を持っていません。だから、本体が子プロセスを起動するとき、 その起動処理がわざわざサンドボックスでラップしなければ、子は素のまま生まれます 。 Bash ツールは「ラップする」ようにわざわざ実装されているので拘束されます。しかし MCP サーバーは   別のコードパスで起動される   ため、そのパスがラップしていなければ、制約なしで動いてしまう。「子プロセスだから安全」ではなく、「 ラップされた子プロセスだけが安全 」なのです。継承元が無拘束である以上、包む処理を通らない経路はすべて素通りになります。 さきほどのレイヤー図の言葉で言い直すと、こうなります。システムコールの境界は   すべてのプロセスに共通して存在している   のに、そこで照合されるポリシーは   プロセスごとに違う 。MCP サーバーは、境界を越えていないわけではありません。 境界は越えているが、そのプロセスには照らし合わせるルールが登録されていない   ので、カーネルはそのまま通してしまうのです。 MCP サーバーは、ファイルにも DB にもネットワークにもアクセスできる強力な存在です。それが制約の外で動くというのは、 .env   にとっては立派な裏口になり得ます。 MCP サーバーを包む ――   @anthropic-ai/sandbox-runtime では、この裏口は塞げないのでしょうか。塞げます。 @anthropic-ai/sandbox-runtime   という、Anthropic が公開しているサンドボックスツールを使います。 これは Claude Code の内蔵サンドボックスと同じ OS の仕組み(macOS なら Seatbelt、Linux なら bubblewrap)を、 任意のプロセスに対して外から適用できるようにした単体パッケージ   です。 srt   というコマンドを提供していて、公式のドキュメントでも「ローカル MCP サーバーを包むこと」が主要な想定用途として挙げられています。 npm install -g @anthropic-ai/sandbox-runtime やっていることは、3章で見た   sandbox-exec   とまったく同じです。 fork   して、隙間でポリシーを登録して、 exec   で目的のコマンドに変身する。だから、 包まれたプロセスの子孫にも制約が継承されます 。 .mcp.json   の   command   を差し替える やり方はシンプルで、 .mcp.json   の   command   を   srt   に差し替えて、本来のコマンドを引数に回すだけです。 包む前: { "mcpServers" : { "filesystem" : { "command" : "npx" , "args" : [ "-y" , "@modelcontextprotocol/server-filesystem" ] } } } 包んだ後: { "mcpServers" : { "filesystem" : { "command" : "srt" , "args" : [ "npx" , "-y" , "@modelcontextprotocol/server-filesystem" ] } } } 差分は「 command   を   srt   にして、元の   command   を   args   の先頭に押し込む」だけです。これで、この MCP サーバー(と、それが起動する子プロセス)はサンドボックスの中で動くようになります。 MCP サーバー側に手を入れる必要はありません 。サーバーは自分が包まれていることを知りませんが、それでも制約は効く ―― 3章の   ls   の話とまったく同じ構図です。 制約の内容は   ~/.srt-settings.json   に書きます。 { "filesystem" : { "denyRead" : [ "./.env" , "~/.ssh" , "~/.aws" ] , "allowWrite" : [ "." ] , "denyWrite" : [ "./.env" ] } , "network" : { "allowedDomains" : [ ] , "deniedDomains" : [ ] } } srt --settings /path/to/srt-settings.json ...   のように、サーバーごとに別の設定ファイルを渡すこともできます。 デフォルトの挙動には癖があるので、ここは押さえておいてください。 書き込み : デフォルトで   全部拒否 。 allowWrite   に書いたものだけ許可される ネットワーク : デフォルトで   全部拒否 。 allowedDomains   に書いたものだけ許可される 読み取り : デフォルトで   全部許可 。 denyRead   で塞ぐ 内蔵サンドボックスと同じで、ここでも   読み取りだけは緩い   という構図です。 .env   を守りたければ   denyRead   を明示的に書く必要があります。しかも   srt   では   allowRead   が   denyRead   より優先される   ので、広い   allowRead   を書くと   denyRead   が打ち消されてしまいます(内蔵サンドボックスとは優先順位が逆なので、ここは要注意です)。 逆に、書き込みとネットワークはデフォルトで全部閉まっています。 包んだ MCP サーバーが動かなくなったら、まず   allowWrite   と   allowedDomains   の不足を疑う   のが正解です。 ただし注意点もあります。 @anthropic-ai/sandbox-runtime   は   ベータのリサーチプレビュー   という位置づけで、設定フォーマットは今後変わる可能性があると明記されています。運用ルールに組み込む場合は、そのつもりで扱ってください。 じゃあ、結局どうすればいいのか ここまで、「経路を1つ塞いでも、別の経路が出てくる」というモグラ叩きを見てきました。Read を塞げば Bash、Bash を塞げば MCP……。 塞ぐアプローチには、常に「列挙し忘れた経路」が残る という本質的な弱さがあります。 そこで発想を変えます。 そもそも   .env   に本物の秘密を置かなければ、何経路から読まれても平気   です。守るべきものが最初からそこに無ければ、読まれても被害はありません。 理想的なのは、 短命なアクセストークン   を使う運用です。永続的な秘密鍵をファイルに置くのではなく、必要なときにだけ発行される、寿命の短いトークンで認証する。イメージとしては、こういう流れです。 1. 開発者が最初に CLI でログインする(人間の認証) $ my-tool login 2. その権限をもとに、短命なアクセストークンをリクエストする → 数十分で失効するトークンが発行される 3. アプリはそのトークンを使って動く → ファイルには本物の秘密鍵が存在しない → 万一トークンが漏れても、すぐ失効するので被害が限定的 実際のコマンドで見るとこうなる my-tool login   は架空のコマンドですが、実際のクラウドでは、おなじみのコマンドがそのまま当てはまります。 AWS の場合 # 1. 人間の認証(ブラウザが開いて SSO でログインする) aws sso login --profile dev # 2〜3. あとは CLI / SDK が一時credentialを自動で取得して使う aws s3 ls ~/.aws/credentials   に永続的なアクセスキーを置く代わりに、 aws sso login   で得た権限をもとに、裏側で数時間で失効する一時credentialが発行されます。トークンは   ~/.aws/sso/cache/   にキャッシュされるだけなので、期限が切れればそれ以上使えません。 Azure の場合 # 1. 人間の認証 az login # 2. 必要なリソース向けの短命トークンを取得(有効期限は1時間程度) az account get-access-token --resource https://vault.azure.net アプリ側は   DefaultAzureCredential   を使えば、この CLI のログイン状態を自動で拾ってくれます。 Google Cloud の場合 # 1. 人間の認証 gcloud auth application-default login # 2. 短命なアクセストークン(有効期限は1時間程度) gcloud auth print-access-token こちらも ADC(Application Default Credentials)という仕組みがあり、SDK が自動でトークンを取得してくれます。 どれも形はまったく同じです。 「人間が一度ログインする」→「そこから短命なトークンが発行される」→「アプリはそれを使う」 。そして共通して、 .env   に書くべき永続的な秘密がどこにも登場しません 。 クラウドのマネージド ID(実行環境自身の身元でシークレットストアからトークンを取得する仕組み)や、OIDC ベースの一時credential(GitHub Actions からクラウドに、静的なアクセスキーを Secrets に置かずに認証する構成など)が、この考え方の実装にあたります。 秘密は「ファイルに書いて守る」のではなく、「 そもそも書かず、必要な瞬間に短命なものを取りに行く 」。これが、経路を1つずつ塞ぐ発想から抜け出す、根本的な解決策です。 とはいえ、理想はなかなか大変 ただ、正直に言うと、この理想形はすぐに実現できるものではありません。 トークンを発行する仕組み(認証基盤、シークレットストア)を用意する必要がある 既存のアプリを「ファイルから読む」前提で書いている場合、その書き換えが要る ローカル開発、CI、本番で、それぞれトークンの取得経路を整える必要がある 規模の小さいプロジェクトや、とりあえず今動いているものに、いきなりこれを導入するのは腰が重い、というのが現実です。 なので、当面の現実的な立ち回りは、こうなります。 理想を目指しつつ 、本物の秘密を短命トークンに寄せていく(長期的な方向性) それができるまでは 、経路を1つずつ地道に塞ぐ permissions.deny   で Read / Edit / Glob をパス単位でブロック サンドボックスを有効にし、 denyRead   /   credentials   で読み取りも明示的に塞ぐ(有効化だけでは読み取りは止まらない) MCP サーバーは使うものを吟味し、 srt ( @anthropic-ai/sandbox-runtime )で包む .env   にはできる限りダミー値だけを置き、本物は最小限に 結局のところ、 「守るべきものを、そこに置かない」が最強の対策   です。しかしそれが難しい間は、 「経路をひとつずつ調べて防ぐ」しか、今のところ道はありません 。モグラ叩きに聞こえるかもしれませんが、どの経路がどのレイヤーで守られるのか(アプリの自制なのか、OS の強制なのか)を理解していれば、叩き漏らしはずっと減らせます。 まとめ コーディングエージェントを実用化させるためにはセキュリティは欠かせません。でもだからといって、コーディングエージェントならではの知識が必要なわけではなく、 OS の基本的な仕組みと、プロセスの性質を理解していれば十分   です。今回の話は、Claude Code に限らず、コーディングエージェント全般に当てはまります。なので、やっぱりいくらAIを使って業務をしても、やっぱり基本が大事だなと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post コーディングエージェントに機密情報を読ませない基本的な仕組み first appeared on SIOS Tech Lab .
1.はじめに 2.背景・目的:なぜ外側からの統制が必要か 3.検証対象:Agent Governance Toolkit と今回確認した範囲 4.検証の概要:実行条件 5.STEP 1の検証結果:3つの統制機能は何をしているか 5-1.ポリシー適用:何を許可・記録・遮断するか 5-2.ゼロトラストエージェントID:最上位に誰を置くか 5-3.信頼性エンジニアリング:統制の状態をどう観測するか 6.STEP 2の検証結果:実際のAIエージェントで実行前に介入できるか 補足:Claude Code の「ツール」と「フック」 6-1:統制ツールの判定で、ツール使用が確認待ちになる 6-2:危険な操作はAIエージェント自身が先に止めることもある 7.考察:3つの機能から見えてくること 8.まとめ:AIエージェントを統制するために必要なこと 付録:検証に使った手順 執筆者 商標 1.はじめに 本記事では、Microsoft が公開している AIエージェント統制向けのオープンソースツール「Agent Governance Toolkit」を動かし、 AIエージェントの行動を外側から統制する仕組み が、実際にどのように働くかを観察しました。 なお、Agent Governance Toolkit は、2026年6月時点で Public Preview として公開されています。 対象読者 :AIエージェントの企業導入、AIガバナンスの仕組みづくりに関心のある方 ※本記事は2026年6月時点の情報に基づいています。 2.背景・目的:なぜ外側からの統制が必要か AIエージェントの企業導入が進むと、「便利に使えるか」だけでなく、「使わせてはいけない操作をどう止めるか」「誰が最終的に責任を持つか」「期待した範囲で動いているかをどう継続的に観測するか」も課題になります。 前回の記事 では、Claude金融エージェントを題材に、AIエージェントの出力に HITL(Human-in-the-Loop:人間が確認・承認する仕組み) がどのように組み込まれているかを観察しました。HITL は重要な仕組みですが、AIエージェント自身が出力で「承認が必要」と示しても、それだけでは実行時の強制力としては不十分な場合があります。指示の取り違えや不正な入力の影響により、宣言と異なる動作をする可能性があります。 そのため、AIエージェントにすべてを委ねるのではなく、行動が実際に起きる手前で、外側の独立した仕組みによって判定・記録・制御する考え方が必要になります。 本記事では、この「外側からの統制」を実際に動かして確認します。Microsoft の Agent Governance Toolkit が提供する複数の統制機能を試し、それぞれが何をしているのか、どこまで強制力を持つのかを観察しました。 3.検証対象:Agent Governance Toolkit と今回確認した範囲 Agent Governance Toolkit は GitHub 上で公開されており *1 、AIエージェントの統制に関するいくつかの機能を含んでいます。 このツールが扱う主な統制の領域は、README の記載をもとに、本記事では次のように整理しました。 領域 内容 本記事での扱い ポリシー適用 行動をルールに基づいて許可・記録・遮断する 確認する ゼロトラストエージェントID どのAIエージェントの行動かを見分け、信頼関係や権限を扱う 信頼性エンジニアリング AIエージェントの動作状態や異常を継続的に観察する 実行サンドボックス AIエージェントが動作する環境を切り離し、影響の及ぶ範囲を抑える 対象外 本記事では、このうち「 ポリシー適用 」「 ゼロトラストエージェントID 」「 信頼性エンジニアリング 」の3つを実際に動かして確認しました。 一方「実行サンドボックス」は、AIエージェントが動作する環境を切り離す機能ですが、この機能は実行環境の構成に左右される部分があり、今回の検証環境では確認対象外としました。 4.検証の概要:実行条件 検証の考え方 このツールでは、AIエージェントが実行しようとする行動、たとえば「どのツールを使うか」「どのようなコードを実行するか」といった情報を入力すると、あらかじめ定めたルールに従って判定結果を返します。 本検証は、2段階で行いました。 STEP 1では、AIエージェントが「こういう行動をしようとした」という状況をデータとして与え、Agent Governance Toolkit 単体で各機能の判定や出力を確認しました。 STEP 2では、実際のAIエージェント(本検証ではClaude Codeを使用)に統制機能を組み込み、ツール使用の手前で外側の仕組みが介入するかを確認しました。 実行環境 STEP 1は、WindowsのPowerShellからツールを導入し、Python環境で判定の仕組みを単体で確認しました。STEP 2では、Claude Code にAgent Governance ToolkitのClaude Code用サンプルを組み込み、実際のツール使用に介入する様子を確認しました。 STEP 項目 内容 1 実行基盤 PowerShell、Python 3.13.11 ツール Agent Governance Toolkit (本検証では agent-os-kernel を利用) 2 実行基盤 Claude Code 2.1.150 (Node.js 24.18.0 経由のプラグインを使用) ツール Agent Governance Toolkit の Claude Code 用公式プラグイン(agent-governance-claude-code)。 ポリシーは examples/claude-code-agt の例を使用 導入方法 GitHub リポジトリのクローン、pip・npm でのインストール いずれのバージョンも本検証時点のものです。 5.STEP 1の検証結果:3つの統制機能は何をしているか ここからは、実際に動かした3つの機能を順に見ていきます。それぞれ、「 ポリシー適用(行動を判定する) 」「 ゼロトラストエージェントID( 誰の行動かを識別する) 」「 信頼性エンジニアリング( 全体を観測する) 」という、異なる役割を持っています。 5-1.ポリシー適用:何を許可・記録・遮断するか 最初に確認したのは、AIエージェントの行動をルールに従って判定する機能です。判定のルールは、AIエージェントのコードの中ではなく、独立した設定ファイル(YAML形式)として記述しました。制御ルールをAIエージェント本体から切り離して管理する、というこのツールの基本的な考え方に沿ったものです。 動かす前は、ルールによる制御とは「許可」か「遮断」かの二択を返すものだと考えていました。実際に動かしてみると、判定には「記録(監査)」という第3の選択肢があり、さらに判定結果が証跡として残るようになっていました。 判定の種類(許可・記録・遮断) AIエージェントの行動を表すツール名を入力として与え、「破壊的な操作は遮断」「メール送信は記録対象」というルールで判定させた結果です。 入力された行動 判定 効いたルール web_search(Web検索) 許可(allow) なし(初期設定) read_file(ファイル読取) 許可(allow) なし(初期設定) send_email(メール送信) 記録(audit) audit-email-send delete_file(ファイル削除) 遮断(deny) block-destructive execute_code(コード実行) 遮断(deny) block-destructive 注目したいのは、判定が「許可」「遮断」の二択ではなく、その中間に「 記録(audit) 」がある点です。記録は、行動自体は通すものの、後から確認できるよう証跡を残す判定です。メール送信のように、一律に禁止はできないが、誰がいつ送ったかは把握しておきたい操作に対応します。なお、今回のポリシーで確認できたのは許可・記録・遮断の3種類ですが、これはこのツールが扱う判定のすべてではありません。 ルールはYAMLファイルに記述されているため、変えたいときはAIエージェント本体に手を入れるのではなく、ファイルを差し替えるだけで済みます。 監査ログ(何が記録されるか) 判定の内容は、証跡として残ります。ファイル削除を遮断した1件の記録です。 { "policy": "business-agent-policy", "rule": "block-destructive", "action": "deny", "context_snapshot": {"tool_name": "delete_file", "agent_id": "demo-agent"}, "timestamp": "2026-06-25T06:45:24.694632+00:00" } context_snapshot に、判定の対象となった行動の中身(どのAIエージェントが、どのツールを使おうとしたか)が記録されています。これにより、後から「いつ、どのAIエージェントが、何をしようとして、どう判定されたか」をたどれます。遮断した行動だけでなく、許可した行動についても同じ形式で記録が残り、 止めたものだけでなく通したものも含めて1件ずつ記録する 動作を確認できました。 内容ベースの制御(行動の中身で判定する) ツール名だけでなく、渡される中身そのものを見た判定もできます。「危険な文字列を含む場合は遮断」というルールでの結果です。 入力された実行内容 判定 効いたルール SELECT * FROM users 許可 なし(無害な参照) DROP TABLE users 遮断 block-sql-injection ls -la 許可 なし(無害な一覧表示) rm -rf /data 遮断 block-shell 同じ「データベース操作」でも参照( SELECT )は許可され、テーブル削除( DROP TABLE )は遮断されています。行動の種類だけでなく、その中身を見て、危険なものだけを遮断していることが確認できました。一方で、今回遮断できたのは、 DROP TABLE や rm -rf を危険な文字列として、あらかじめルールに書いていたからです。企業では、対象となるシステム、データベース、API、業務操作が多く、危険な操作のパターンも業務ごとに異なります。したがって実運用では、何を危険とみなすかをどう設計し、そのルールを誰が維持するかが大きな課題になります。 5-2.ゼロトラストエージェントID:最上位に誰を置くか 次に確認したのは、AIエージェントを見分け、権限の階層を管理する機能です。ポリシー適用が「どの行動を許すか」だったのに対し、こちらは「誰の行動か」「誰が最終的に判定するか」を扱います。 階層の検証 AIエージェントの統制を階層構造で考えたとき、その最上位(最終的な判定者)に誰を置けるかを確認しました。 設定 階層 AIか 判定 human-controller 最上位(level 0) いいえ 受理 ai-supervisor 最上位(level 0) はい 拒否 team-lead-agent 下位(level 1) はい 受理 注目したいのは、今回確認した階層の設定では、 最上位(level 0)にAIエージェントを置こうとすると拒否され、人間を最上位に置く構成が求められた 点です。AIエージェントは下位(level 1)には置けますが、この設定では最終的な判定をする最上位(level 0)には置けませんでした。 これは、AIエージェントが別のAIエージェントの行動を最終承認してしまう、という状況を構造として防ぐ仕組みと考えられます。前回の HITL が「人間が確認する」という運用だったのに対し、こちらは「最終的な判定は人間側に置く」という考え方を、判定の仕組みそのものに組み込んでいます。 5-3.信頼性エンジニアリング:統制の状態をどう観測するか 3つ目に確認したのは、統制システム全体の状態を、数値として観測する機能です。1件ずつの判定(ポリシー適用)を集計し、全体の状態を把握する役割です。 この機能は、AIエージェントの行動判定が起きるたびに数値を記録する想定で動かしました。今回確認した範囲では、違反の検出数、遮断数、リクエスト数などを、監視でよく使われる形式(Prometheus形式)の数値として出力できました。 agent_os_violations_total{action="delete_file",severity="high"} 1.0 agent_os_violations_total{action="execute_code",severity="high"} 1.0 agent_os_violations_blocked_total{action="delete_file"} 1.0 agent_os_requests_total{action="web_search",status="allow"} 1.0 agent_os_requests_total{action="read_file",status="allow"} 1.0 agent_os_requests_total{action="delete_file",status="deny"} 1.0 agent_os_active_agents 1.0 それぞれの指標の意味は以下のとおりです。 指標 意味 violations_total 検出した違反の総数(行動・深刻度ごと) violations_blocked_total 遮断した違反の数 requests_total 処理したリクエスト数(許可・遮断の状態ごと) active_agents 現在動いているAIエージェントの数 注目したいのは、各指標が行動や状態の ラベル付き で記録される点です。これにより、「どの行動で、どの深刻度の違反が、何件起きたか」「許可と遮断がそれぞれ何件か」を集計できます。こうした指標を継続的に集めれば、統制の状態を時系列で観測することにつなげられると考えられます。 6.STEP 2の検証結果:実際のAIエージェントで実行前に介入できるか STEP 2では、実際のAIエージェントを動かし、その行動が外側の仕組みで実行前に介入されるかを確認しました。題材には、Claude Code を使いました。Agent Governance Toolkit には、Claude Code に統制を組み込むための公式サンプルが用意されています。これを使うと、Claude Code がツールを使おうとする手前で、ポリシーに基づいた判定を挟むことができます。 補足:Claude Code の「ツール」と「フック」 このSTEPの土台となる2つの言葉を整理します。 ツール と フック です。外側からの統制が「いつ、どこで」働くかは、この2つで決まります。 Claude Code のようなAIエージェントは、ファイルを読む・書く、コマンドを実行するといった操作を、 ツール と呼ばれる機能で行います。AIエージェントが「ファイルを書き換えたい」「コマンドを実行したい」と判断すると、対応するツールを呼び出します。 用語 意味 どこが行うか ツール AIエージェントが外部環境に対して行う操作機能 Claude Code Read ファイルを読む Write ファイルに書く Bash シェルコマンドを実行する フック ツールが実行される手前で、外部の処理に橋渡しする組み込み口 ポリシー判定 渡された情報を、許可・確認・遮断に振り分ける Agent Governance Toolkit ここで外側からの統制で最も重要なのが、 いつ・どこで止めるか です。今回の統制は、Claude Code が持つ フック という仕組みに、統制ツール(Agent Governance Toolkit)を組み込みました。フックは、AIエージェントがツールを使おうとする、 その直前 に発動します。下の図のように、AIエージェントがツールを呼び出そうとするたびに、実行の手前でフックがその情報を外側の統制ツールに渡し、統制ツールがポリシーに照らして判定します。 ─────────── Claude Code(AIエージェント) ─────────── 利用者の依頼 → 行動を決める → ツールを呼び出そうとする │ │ フック(組み込み口)がツール使用の情報を渡す ▼ ─────── Agent Governance Toolkit(外側からの統制) ─────── ポリシーで判定する → 許可 / 確認 / 遮断 │ │ 判定結果を返す ▼ ─────────── Claude Code(AIエージェント) ─────────── 判定に従う(止まる / 実行する) 判定そのものは、Claude Code ではなく、 外側のAgent Governance Toolkitが行います 。Claude Code は「このツールを使おうとした」という情報を渡し、返ってきた判定に従うだけです。 行動が起きてからではなく、起きる直前に、外側で止める 。これが、出力に「やめておくべき」と書かせる前回の方法と大きく違う点です。 ただし、フックのような組み込み口は、エージェント実行環境側に必要です。Claude Code はフックの仕組みを備えていますが、すべてのAIエージェントが同じ仕組みを持っているわけではありません。また、フックがあっても、それを誰が設定し、どの範囲に適用するかは運用に依存します。つまり統制は、エージェントの外に置いた独立した仕組みでありながら、エージェント側に組み込み口があり、その設定が確実に適用されて初めて働きます。この点は後で改めて触れます。 STEP 2の実際の操作で現れたのは、 主に「許可(allow)」と「確認(review)」 でした。review は、行動を即座に通すのでも遮断するのでもなく、実行の手前で一度止めて、人間に確認を求める判定です。なお、遮断(deny)にあたる動作は、後述するように今回の操作の範囲では現れませんでした。 6-1:統制ツールの判定で、ツール使用が確認待ちになる Claude Code に、ファイルを1つ作るよう依頼しました。ファイルの作成は、それ自体は危険な操作ではありません。しかし、今回適用したポリシーでは、ファイルへの書き込みを確認の対象に指定していました。 実行しようとした手前で、ファイルを作成してよいかの確認が表示され、人が承認するまで実行は止まりました。ただし、Claude Code は、ファイルの書き込みに対してもともと確認を求める作りになっています。そのため、この確認が表示されたこと自体は、統制ツールが止めたのか、Claude Code がもとから持つ確認なのか、これだけでは区別がつきません。そこで、何がこの確認を判定したのかを、監査ログで確認しました。 { "timestamp": "2026-06-27T07:25:13.935Z", "agentId": "claude-code:e3c691d7-...", "action": "tool.Write", "decision": "review", "previousHash": "09c9c9c5...", "hash": "b686a97b..." } 記録を見ると、この書き込み( tool.Write )が review と判定されていました。この監査ログは統制ツールが残すもので、Claude Code 標準の確認では作られません。つまり先ほどの確認は、統制ツールがポリシーに従って review と判定し、人の確認を挟んだ結果でした。処理されたのは、統制ツールが通したからではなく、確認に対して人が承認したからです。ファイルの作成は、AIエージェント自身が危険とみなす操作ではありません。それでも、外側のポリシーが確認の対象に指定していれば、実行の手前で確認待ちになる、ということです。 さらに、各記録には previousHash と hash があります。 previousHash には、1つ前の記録の hash が入ります。各記録がこのように直前の記録のハッシュ値を抱えるため、途中の記録を後から書き換えると、その先のハッシュ値の連鎖が合わなくなり、改ざんに気づけます。単に記録を残すだけでなく、記録そのものが書き換えられていないことを後から確かめられる作りになっていました。企業の統制では、監査の証跡が後から改ざんされていないことが重要になるため、これはSTEP 1の判定の確認だけでは見えなかった点でした。ただし、これは改ざんを検知できる仕組みであって、改ざんそのものを防ぐものではありません。ログの保管先自体が適切に保護されていることは、別途必要になります。 6-2:危険な操作はAIエージェント自身が先に止めることもある 検証の途中で、Claude Code に、外部から取得したスクリプトをそのまま実行するよう依頼しました。これは、今回のポリシー上は遮断(deny)の対象にあたる操作です。 しかし実際には、外側の仕組みが遮断(deny)と判定する前に、Claude Code 自身が危険な操作と判断し、実行を止めました。そのため、外側の仕組みによる遮断そのものは、今回の操作の範囲では現れませんでした。 今回の検証で使った同梱ポリシーでは、遮断(deny)の対象が、外部から取得したスクリプトの実行のような、明確に危険な操作に向けて書かれていました。こうした操作は、外側の仕組みが判定するより先に、Claude Code 自身も危険とみなして止めます。そのため、「AIエージェント自身は実行しようとするのに、外側のポリシーが遮断する」という、遮断だけを純粋に取り出せる操作が、同梱ポリシーの範囲では用意しにくいものでした。今回の操作で遮断の動作までたどり着かなかったのは、この理由によります。 7.考察:3つの機能から見えてくること 今回動かした3つの機能は、役割は異なりますが、いずれも「 AIエージェントの外側から統制する 」という共通の考え方に基づいています。 ポリシー適用は、行動を許可・記録・遮断に振り分けます。ゼロトラストエージェントIDは、誰の行動かを見分け、最終的な判定を人間側に置きます。信頼性エンジニアリングは、全体の状態を数値で観測します。「何を許すか」「誰が決めるか」「どんな状態か」という、統制に必要な異なる側面を、それぞれが担っていると整理できます。 前回の記事で観察した HITL の制御と並べると、今回の統制の違いがはっきりします。 観点 前回 (AIエージェントの出力に組み込まれた制御) 今回 (AIエージェントの外側の独立した仕組み) 制御の主体 AIエージェント自身 外側の独立した統制層 強制力 出力に「承認が必要」と示す(宣言) 行動の手前で判定する(強制) 判定の仕組み AIエージェントの判断による ルールや実装された判定ロジックによる 最上位の判定者 運用として人間が確認 階層の設定として人間側に置く 記録 出力に含まれる 独立した監査ログ・数値に残る 前回の記事で残った課題は、AIエージェントの出力上の宣言だけでは、実際の行動を強制的に制御できないという点でした。今回確認した Agent Governance Toolkit の各機能は、その課題に対して、行動の手前で判定し、記録し、必要に応じて介入する仕組みを提供するものです。 ただし、外側からの統制は、ツールを導入するだけで自動的に効くものではありません。AIエージェントが外部システムに接続する経路が、この判定の仕組みを必ず通るように設計されている必要があります。 迂回できる経路があれば、統制は機能しません 。 また、今回 Claude Code に介入できたのは、Claude Code 側にフックという組み込み口があり、そこに統制を組み込んだからです。つまり、外側からの統制を企業で機能させるには、個々のエージェントへの組み込みだけでなく、 統制を通らないAIエージェントを作らせない開発ルールや運用ルールも必要 になります。これは、統制に組み込まれていないAIエージェントをどう扱うか、という次の論点につながります。 8.まとめ:AIエージェントを統制するために必要なこと ここまで、Microsoft の Agent Governance Toolkit を題材に、AIエージェントを外側から統制する仕組みを動かしてきました。STEP 1で判定の仕組みを単体で確認し、STEP 2で実際の Claude Code に組み込んで、その判定が実行前のツール使用に介入するところまでを見ました。 これらの検証を通して見えてきたのは、特定のツールの使い方というより、AIエージェントを統制するうえで共通して必要になる要素です。今回の検証をもとに整理すると、次のようにまとめられます。 統制に必要なこと 今回の検証で見えた裏づけ 行動を、通す・記録する・止めるに振り分ける判断 許可・記録・遮断の3種類の判定。単純な可否ではなく、通すが記録は残す、という中間の判定が必要になる 最上位の判定者を人間側に置く構造 権限の階層で、最上位にAIエージェントを置けず、人間を最上位に置く構成が求められた 改ざんを検知できる形で記録を残す仕組み 誰が何をしようとして、どう判定されたかが、ハッシュで連鎖した監査ログに残る 統制をAIエージェントに組み込み、その組み込みを徹底させる仕組み Claude Code のフックに組み込んで初めて統制が働いた。組み込みの有無は設定・運用に依存する すべての経路が統制を通る全体の設計 判定が正しくても、それを迂回できる経路があれば統制は機能しない 統制の外で動くAIエージェントを生ませない仕組み 統制に組み込まれていないAIエージェントが抜け道になる 前半の3つ(振り分ける判断・人間への権限固定・改ざんを検知できる記録)は、統制ツールそのものが提供する機能です。今回 Agent Governance Toolkit で実際に確認できた部分にあたります。一方、後半の3つ(組み込みの徹底・経路の設計・統制外をなくす仕組み)は、ツールだけでは完結せず、組織の設計やルールと組み合わせて初めて成り立つものです。AIエージェントを企業で安全に統制するには、 ツールが提供する判定の仕組みと、それを全体に行き渡らせる組織の仕組みの、両方が必要になります 。 今回の検証結果は一つのツールに基づくものであり、そのまますべてのAIエージェント統制ツールに一般化できるものではありません。それでも、ここで整理した「統制に必要なこと」は、別のツールや基盤を検討する際にも、何が備わっていて何が足りないかを見極めるための土台になると考えます。前回の HITL(内側の制御)と、今回の外側からの統制は対になる関係にあり、実運用では両方を組み合わせる設計が重要になります。そのうえで、統制を組み込まないAIエージェントをどう生ませないかが、その先の課題になります。 付録:検証に使った手順 STEP 1では、WindowsのPowerShellからツールを導入し、Python環境で各機能を確認しました。STEP 2では、Claude Code に公式サンプルの統制を組み込んで確認しました(いずれも2026年6月時点)。 STEP 1:環境準備 GitHub リポジトリをクローンし、Python の仮想環境を作成したうえで、ツールをインストールしました。 git clone https://github.com/microsoft/agent-governance-toolkit.git pip install agent-os-kernel 本検証では、ポリシー適用とゼロトラストエージェントIDの確認に agent-os-kernel を利用しました(本検証時点で利用した手順です)。信頼性エンジニアリングの確認では、これに加えて監視向けのライブラリ( prometheus_client 、 opentelemetry-api 、 opentelemetry-sdk )を導入しました。なお、2026年6月時点の公式READMEでは、統合パッケージである agent-governance-toolkit[full] も案内されています。導入時は公式リポジトリの最新手順を確認してください。ポリシー適用・ゼロトラストエージェントIDの判定そのものは、外部のLLMやAPIキーを必要とせず、ローカルで動作します。 STEP 1:ルールの記述例 制御ルールは、AIエージェント本体とは別のYAMLファイルとして記述しました。破壊的な操作を遮断し、メール送信を記録対象とするポリシーの例です。 name: business-agent-policy version: "1" defaults: action: allow rules: - name: block-destructive condition: { field: tool_name, operator: in, value: [delete_file, drop_table, execute_code] } action: deny priority: 100 - name: audit-email-send condition: { field: tool_name, operator: eq, value: send_email } action: audit priority: 90 STEP 2:Claude Code への組み込み STEP 2では、リポジトリに含まれる Claude Code 用の公式プラグイン(agent-governance-claude-code)を読み込み、同梱のポリシー例(examples/claude-code-agt/config/review-heavy-policy.json)を指定して Claude Code を起動しました。 cd agent-governance-claude-code npm install cd .. $env:AGT_CLAUDE_POLICY_PATH = (Resolve-Path .\examples\claude-code-agt\config\review-heavy-policy.json).Path claude --plugin-dir .\agent-governance-claude-code STEP 2:Windows環境での注意点 今回利用した公式サンプルは、Windows 環境ではそのままでは起動できませんでした。統制を組み込むための起動スクリプトの呼び出しでエラーになるためです。フックの設定ファイル(hooks.json)で、起動コマンドが Windows では実行できない形式を指していたことが原因でした。今回は、この起動コマンドを node に直接置き換えることで動作しました。公式サンプルはmacOS / Linux を前提とした記述になっている部分があり、Windows で動かす場合はこうした調整が必要になる場合があります。 執筆者 倉田 裕紀(NTT西日本 技術革新部 AI技術担当) AIエンジニアとして、社内外のAIプロジェクトを技術支援しています。 趣味は筋トレです。 商標 「Microsoft」は Microsoft Corporation の商標または登録商標です。 「Claude」は Anthropic, PBC の商標または登録商標です。 「GitHub」は GitHub, Inc. の商標または登録商標です。 「Python」は Python Software Foundation の商標または登録商標です。 「Prometheus」は The Linux Foundation の商標です。「OpenTelemetry」は The Linux Foundation のプロジェクトです。 その他、本文中に記載されている会社名、製品名、サービス名などは、各社の商標または登録商標です。 *1 : Microsoft「Agent Governance Toolkit」: https://github.com/microsoft/agent-governance-toolkit
PSSLの佐々木です MCP サーバーを書いていると、ある機能を @mcp.tool で実装すべきか @mcp.resource で実装すべきかで迷う場面が出てきます。公式のコース教材でも「ドキュメントを読む」という同じ処理が tool と resource の両方で実装されていて、最初に読んだときは違いがピンと来ませんでした。 仕様を読み直して整理したところ、判断基準は思っていたよりはっきりしていたので、使い分けの考え方としてまとめます。 この記事では、 tool と resource の違いは「機能」ではなく「誰が呼ぶか」であること どちらで実装するかを決める判断フロー ユースケース別の使い分け resource 側にしかない機能(URI テンプレート、変更通知)の活かし方 迷ったときに両方出しておく実装パターン ハマりどころ(Messages API の MCP connector は tool のみ、など) についてまとめました。 1. 何がわからなかったのか Anthropic のコース教材では、インメモリのドキュメント管理サーバーを題材に、こういう tool が定義されます。 @mcp.tool( name="read_doc_contents", description="Read the contents of a document and return it as a string." ) def read_document( doc_id: str = Field(description="Id of the document to read") ): if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] ところが少し後の章で、 まったく同じことをする resource が出てきます。 @mcp.resource("docs://documents", mime_type="application/json") def list_docs() -> list[str]: return list(docs.keys()) @mcp.resource("docs://documents/{doc_id}", mime_type="text/plain") def fetch_doc(doc_id: str) -> str: if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] やっていることは docs[doc_id] を返すだけで、tool 版と 1 行も違いません。なぜ 2 つあるのか。 2. 違いは「誰が呼ぶか」 答えは機能差ではなく 制御主体 です。MCP の仕様では tool は model-controlled、resource は application-controlled と明確に区別されています。 つまり read_doc_contents は「Claude に自分で判断して使ってほしい機能」、 docs://documents/{doc_id} は「 @ メンション UI のためのデータソース」です。同じ処理でも役割がまったく違う、というのが教材の意図でした。 ここを押さえると、資料でよく見る「resource はデータ、tool はアクション」という説明が、もう一段深く理解できます。読み取り専用かどうかが本質なのではなく、 呼ぶ判断をモデルに委ねるのか、アプリが握るのか が本質です。 3. 判断フロー 実装するときは、この順番で考えるとよさそうです。 Q1 は単純です。書き込み・削除・外部への送信は必ず tool にします。 resources/read はクライアントが再読み込みやキャッシュをする前提の操作なので、ここに副作用を置くと何回呼ばれるか分からず事故ります。「GET に副作用を持たせない」と同じ話です。 Q2 と Q3 が実質的な分かれ目です。読み取り専用でも、 「どのデータが必要かをモデルに判断させたい」なら tool です。ここを「読み取りだから resource」と機械的に決めてしまうと、モデルからは存在しないデータになってしまいます。 仕様側にもこの指針が書かれていて、 モデルに対してデータを自動的に公開したい場合は Tools のような model-controlled なプリミティブを使うべき とされています。 4. ユースケース別の使い分け 具体例に落とすとこうなります。 やりたいこと 選択 理由 @ でドキュメントを参照させる resource ユーザーの UI 操作が引き金。モデルの判断は不要 「report.pdf を要約して」に応える tool どのドキュメントが必要かはモデルが判断する ドキュメントを編集する tool 副作用がある 全文検索して該当箇所を返す tool 検索クエリをモデルが組み立てる プロジェクト規約や DB スキーマを常に文脈に入れる resource アプリが定型的に注入すればよい ログの最新状態を追わせる resource + subscribe 変更通知がプロトコル標準にある チケットを作成する tool 副作用がある ユーザーが選んだファイルを添付する resource 選択したのはユーザー 「検索は tool、指定は resource」と覚えると整理しやすいと感じています。何を取るかが決まっていないなら tool、決まっているなら resource です。 5. resource 側の機能を活かす resource を選んだ場合、tool にはない仕組みが使えます。ここを使わないと resource にした旨味が薄くなります。 5.1 URI テンプレートと補完 docs://documents/{doc_id} のようなテンプレートは RFC 6570 の URI Template 構文で、 resources/templates/list で discovery できます。パラメータは MCP の completion API で自動補完に対応させられるので、 @ メンションの候補表示が標準の枠に乗ります。Python SDK はテンプレートのパラメータを自動でパースして関数のキーワード引数に渡してくれるので、実装側は URI のパースを書く必要がありません。 5.2 変更通知(subscribe) サーバーが subscribe capability を宣言すると、クライアントは resources/subscribe で個別の URI を購読でき、内容が変わると notifications/resources/updated が飛びます。クライアントはそれを受けて再読み込みします。ログやメトリクスのような「更新され続けるデータ」を扱うなら、ポーリングを自作せずに済みます。 5.3 mime_type mime_type はクライアントがレンダリングを決めるヒントになります。JSON を返すのに text/plain を書いておくと、クライアントによっては素の文字列として扱われます。SDK が戻り値のシリアライズはやってくれますが、MIME タイプの正しさは面倒を見てくれないので、ここはサボらないほうがいいです。 6. 迷ったら両方出しておく 判断フローで整理しても、実際には「両方あると便利」というケースが出てきます。そのときは 内部実装を 1 本にして、tool と resource の両方から呼ぶ のが素直です。 理由は、resource の扱いがクライアント実装に委ねられているからです。仕様上、ユーザーに明示的に選択させるクライアント、ヒューリスティクスで自動選択するクライアント、モデル自身に選ばせるクライアント、どれもあり得るとされています。実際にも resources/list は実装済みでも read の UX がまちまち、 subscribe は未対応、といった差があります。resource しか用意していないと、未対応のクライアントからは中身が空っぽに見えます。 from mcp.server.fastmcp import FastMCP from pydantic import Field mcp = FastMCP("DocumentMCP", log_level="ERROR") docs = { "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.", "report.pdf": "The report details the state of a 20m condenser tower.", } # --- 実装は 1 箇所だけ --- def _list_doc_ids() -> list[str]: return list(docs.keys()) def _read_doc(doc_id: str) -> str: if doc_id not in docs: raise ValueError(f"Doc with id {doc_id} not found") return docs[doc_id] # --- resource: アプリの @ メンション UI 用 --- @mcp.resource("docs://documents", mime_type="application/json") def list_docs_resource() -> list[str]: return _list_doc_ids() @mcp.resource("docs://documents/{doc_id}", mime_type="text/plain") def fetch_doc_resource(doc_id: str) -> str: return _read_doc(doc_id) # --- tool: Claude が自分で判断して呼ぶ用 --- @mcp.tool( name="list_documents", description="List the ids of all available documents." ) def list_documents() -> list[str]: return _list_doc_ids() @mcp.tool( name="read_doc_contents", description="Read the contents of a document and return it as a string." ) def read_document( doc_id: str = Field(description="Id of the document to read") ): return _read_doc(doc_id) 追加コストはデコレータ数行なので、割に合うと思っています。ただし tool を増やすとその定義はモデルのコンテキストを常に消費するので、「何でも tool にも出しておく」は避けたほうがいいです。文脈注入で完結するデータは resource だけに留めます。 動作確認は Inspector が楽です。 uv run mcp dev mcp_server.py Resources と Resource Templates が別枠で表示されるので、テンプレートのパラメータ解決まで確認できます。 7. 注意点(ハマりどころ) 7.1 Messages API の MCP connector は tool しか使えない これは事前に知らないと詰みます。公式ドキュメントに明記されていて、MCP 仕様の機能セットのうち 現時点では tool call のみサポート です。さらにサーバーは HTTP で公開されている必要があり、 ローカル STDIO サーバーは直接接続できません 。 mcp_servers パラメータ(MCP connector) ├─ tool call ... OK ├─ resource ... NG ├─ prompt ... NG └─ stdio サーバー ... NG 「resource を作ったのに Messages API から読めない」は仕様どおりの挙動です。 7.2 resource を使いたいならクライアント側ヘルパーに寄せる では API 経由で resource を使う手段が無いのかというと、そうではなく 自分で MCP クライアント接続を管理する 側に回ります。Anthropic SDK にはそのための変換ヘルパーが用意されています。 # pip install "anthropic[mcp]" (Python 3.10 以降) from anthropic.lib.tools.mcp import mcp_resource_to_content resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt") response = await client.beta.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{ "role": "user", "content": [ mcp_resource_to_content(resource), {"type": "text", "text": "Summarize this document"}, ], }], ) mcp_resource_to_file を使えばそのまま Files API にアップロードもできます。公式の使い分けも明快で、 URL で到達できるリモートサーバーで tool だけ使いたいなら mcp_servers パラメータ、ローカルサーバーや prompt / resource を使いたいならクライアント側ヘルパー です。 resource は「Claude が勝手に読むもの」ではなく「アプリが読んでプロンプトに詰めるもの」だという 2 章の話が、SDK の API 設計にそのまま現れています。 7.3 resource link はクライアント側で解決してから渡す 変換ヘルパーは、未対応のコンテンツタイプや MIME タイプ、そして resource link を渡すと例外を投げます(Python なら UnsupportedMCPValueError )。resource link は MCP クライアント側で実体に解決してから変換する必要があります。 7.4 大きい resource はコンテキストを食う resource の中身は最終的にプロンプトに入ります。ログファイル全体のような resource をうっかり注入すると一撃でコンテキストが埋まります。大きいものは resource link で参照させるか、tool 側でフィルタ・要約してから返す設計にしたほうが安全です。 8. まとめ tool と resource の違いは機能ではなく 制御主体 。tool は model-controlled、resource は application-controlled 判断は「副作用があるか」→「呼ぶタイミングをアプリが決められるか」→「モデルに存在を知らせる必要があるか」の順で考える 読み取り専用でも、 何を取るかをモデルに判断させたいなら tool 。読み取りだから resource、と機械的に決めるとモデルから見えないデータになる resource を選んだら URI テンプレート・補完・ subscribe ・ mime_type まで使い切ると効果が出る 両方あると便利なケースは、内部関数を共有して二重提供する。ただし tool 定義はコンテキストを消費する点に注意 Messages API の MCP connector は tool のみ。resource を使うならクライアント側ヘルパー( mcp_resource_to_content / mcp_resource_to_file )に寄せる 「モデルに判断させる」のか「アプリが決め打ちする」のかを設計として先に決めておくと、実装もレビューもぶれなくなります。何でも tool にしてモデルに探索させるとトークンも増えて挙動も揺れますし、逆に何でも resource にするとモデルからは存在しないデータになります。AI エージェントと組み合わせる開発では、この「どこまでを機械的に確定させるか」の線引きが毎回論点になるなと感じています。 参考リンク MCP connector – Claude Platform Docs: https://platform.claude.com/docs/en/agents-and-tools/mcp-connector Resources – Model Context Protocol 仕様: https://modelcontextprotocol.io/specification/draft/server/resources Connect to local MCP servers: https://modelcontextprotocol.io/docs/develop/connect-local-servers Connect Claude Code to tools via MCP: https://code.claude.com/docs/en/mcp MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk AI エージェント導入のご相談 サイオステクノロジーでは、本記事で扱った MCP サーバーの設計・実装を含む AI エージェントの SI サービス を提供しています。既存システムと AI エージェントの接続、RAG の精度評価と改善など、PoC から本番運用までの実装フェーズをまとめてご支援します。 「自社の業務でどこまで自動化できるのか」「どのデータをエージェントに渡すべきか」といった構想段階のご相談も歓迎です。無料相談も承っておりますので、ご興味のある方は下記サービスサイトからお気軽にお問い合わせください。 サイオス ネクストテックソリューション 生成AI導入支援サービス: https://nextech-solutions.sios.jp/genai/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post MCP の tool と resource の使い分け first appeared on SIOS Tech Lab .

動画

書籍