読み取りだけを任せていたバックグラウンドのエージェントが、ある日ファイル削除のツールを握っていることに気づいた、という出来事がありました。実際に消したわけではありません。ただ、必要のない権限がいつの間にか手の届くところに置かれていた、というその一点が、しばらく頭から離れませんでした。
Antigravity 2.0 に MCP サーバーを足していくと、こういう状態は自然に生まれます。サーバーを一つ繋ぐたびに、そのサーバーが公開するツールが「使えるツール」の山に積み上がります。山は全エージェントから同じように見えていて、誰がどれを使ってよいかの線引きは、どこにも書かれていません。
ここで扱うのは、その線引きをコードに落とす設計です。鍵になる考え方は最小権限、つまり「各エージェントには、その仕事に要るツールだけを見せる」という一点に尽きます。
「全部入り」のツールセットが事故を生む構造
まず、なぜ放っておくと危ないのかを具体的に置いておきます。
MCP の便利さは、サーバーを繋げば公開ツールがそのまま使える点にあります。ファイル操作、シェル実行、外部 API、データベース、課金。繋いだ数だけツールは増えます。問題は、その増えたツールが「役割で仕分けされないまま」全エージェントに見えてしまうところにあります。
夜間に走らせる集計エージェントに必要なのは、読み取りと集計だけです。けれども同じ環境にデプロイ系や削除系のツールが同居していると、モデルが文脈を読み違えた瞬間に、本来触るはずのないツールへ手が伸びる余地が残ります。レビューのないバックグラウンド実行では、その余地がそのまま事故の入り口になります。
権限を絞らない設計は、二つの理由で外れます。読み取りだけのエージェントに破壊的ツールを見せれば、誤爆の確率がゼロより大きくなります。逆に全エージェントの権限を一律で狭めれば、本当に必要なエージェントまで仕事ができなくなります。役割ごとに線を引く以外に、両方を満たす道はありません。
許可リストはエージェント単位で持つ
設計の中心に置くのは、エージェントの役割ごとに「見せてよいツール」を列挙した許可リストです。ツール側ではなくエージェント側に権限を持たせるのが要点になります。
エージェントの役割 許可するツール 禁止するツール
集計・レポート(読み取り専用) read_file, list_dir, query_db_readonly write_file, run_shell, deploy, delete_*
記事ビルド(書き込みあり) read_file, write_file, run_build run_shell, deploy, charge_*, delete_*
デプロイ(破壊的・要承認) read_file, deploy(二段階) delete_db, charge_*
表の縦軸は役割、横軸は許可と禁止です。同じツール群でも、役割が違えば見える範囲が変わる のがこの設計の眼目になります。デフォルトは「禁止」とし、明示的に並べたものだけを通す——いわゆる deny by default を貫くと、新しいツールを繋いだ瞬間に全エージェントへ漏れる事故を防げます。
呼び出しの直前で弾く — ラッパー実装
許可リストは、定義しただけでは効きません。ツール呼び出しの直前で必ず参照する一点を作る必要があります。MCP のツール実行を一段ラップして、そこで判定を挟みます。
from dataclasses import dataclass, field
# 役割ごとの許可リスト(deny by default)
ALLOWLISTS : dict[ str , set[ str ]] = {
"reporter" : { "read_file" , "list_dir" , "query_db_readonly" },
"builder" : { "read_file" , "write_file" , "run_build" },
"deployer" : { "read_file" , "deploy" },
}
# 破壊的ツール — 許可されていても二段階承認を必須にする
DESTRUCTIVE = { "deploy" , "delete_file" , "delete_db" , "charge_card" , "run_shell" }
class PermissionError_ ( Exception ):
pass
@dataclass
class CallContext :
role: str
approved: set[ str ] = field( default_factory = set ) # 承認済みの破壊的操作
def guard (ctx: CallContext, tool: str ) -> None :
allowed = ALLOWLISTS .get(ctx.role, set ())
if tool not in allowed:
raise PermissionError_(
f "[deny] role=' { ctx.role } ' はツール ' { tool } ' を許可されていません"
)
if tool in DESTRUCTIVE and tool not in ctx.approved:
raise PermissionError_(
f "[hold] ' { tool } ' は破壊的です。承認フラグが要ります"
)
async def call_mcp_tool (ctx: CallContext, tool: str , ** args):
guard(ctx, tool) # ① 許可判定(呼び出し前)
result = await _raw_mcp_call(tool, ** args) # ② 実際の呼び出し
audit(ctx.role, tool, args) # ③ 監査ログ
return result
判定の順序が大切です。先に役割の許可リストで弾き、通った後にだけ破壊的かどうかを見ます。逆にすると、許可されていないツールに対しても「承認すれば通る」抜け道が開いてしまいます。許可が先、承認が後、という優先順位を崩さないことが肝心になります。
破壊的ツールは二段階にする
許可リストを通ったツールでも、消す・課金する・デプロイするといった取り返しのつかない操作は、それだけで実行させない設計にしておくと安心です。approved フラグを別の経路で立てる、という一手間を挟みます。
# 破壊的操作は「計画」と「実行」を分ける
async def plan_then_execute (ctx: CallContext, tool: str , ** args):
if tool in DESTRUCTIVE :
preview = await dry_run(tool, ** args) # まず影響範囲を出す
if not human_or_policy_approves(preview): # 人 or ポリシーが承認
raise PermissionError_( f "[abort] ' { tool } ' は未承認のため中止しました" )
ctx.approved.add(tool) # この実行に限り承認
return await call_mcp_tool(ctx, tool, ** args)
ここでの human_or_policy_approves は、対話中なら人の確認、完全無人なら「対象が想定の範囲に収まっているか」を機械的に検査する関数に置き換えます。私自身は無人運用のデプロイで、変更ファイル数が一定を超えたら自動で止める、という素朴なポリシーから始めました。素朴でも、止まる仕組みがあるだけで夜間の安心感がまるで違います。
監査ログで「何が許可されたか」を追える状態にする
許可リストは育てていくものなので、後から「いつ・誰が・何を許された/拒まれたか」を見返せる記録が要ります。監査ログは凝らずに、構造化された一行で十分です。
import json, time
def audit (role: str , tool: str , args: dict , verdict: str = "allow" ):
line = json.dumps({
"ts" : int (time.time()),
"role" : role,
"tool" : tool,
"verdict" : verdict, # allow / deny / hold
"arg_keys" : sorted (args.keys()), # 値は残さない(秘密混入を避ける)
}, ensure_ascii = False )
with open ( "mcp_audit.jsonl" , "a" ) as f:
f.write(line + " \n " )
引数の値そのものは残さず、キーだけを記録しているのが小さな工夫です。ログに秘密が混ざるのを避けつつ、「どのツールがどれだけ拒まれたか」は集計できます。拒否が頻発する役割は、許可リストが現実に追いついていない兆候として読めます。
運用してみて分かった、許可リストの育て方
最後に、机上で完成させようとしないことをお勧めします。最初から完璧な許可リストを書こうとすると、たいてい広すぎるか狭すぎるかのどちらかに振れます。
私が落ち着いた手順は、次の三段です。
新しいエージェントは、まず読み取り系だけの最小許可で動かします。
監査ログで deny が出たツールを一つずつ確認し、本当に必要なものだけを許可リストに足します。
破壊的ツールは、足すと同時に二段階承認の対象へ必ず入れます。
個人開発で Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab の4サイトを自動運用していると、エージェントの種類は静かに増えていきます。AdMob のレポートを読むだけのもの、App Store 向けの素材を組むもの、本番へ反映するもの——役割が違えば、見えてよいツールも違います。その違いを許可リストという一つの場所に集めておくと、新しいサーバーを繋ぐときの判断が「このエージェントに見せるか」の一問に縮みます。
権限は、足すより削るほうがずっと難しいものです。だからこそ最初を狭く始めて、必要に応じて少しずつ開けていく。そのほうが、夜のあいだ静かに走り続けるエージェントを、安心して任せられるようになると考えています。