エージェントのツールアクセスを保護する
エージェントのツール権限に最小権限の原則を適用し、破壊的な操作の前に確認ステップを設け、異常な動作を検出するためにエージェントの操作ログを監査します。
「エージェントのツールアクセスを保護する」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
ツールアクセスにおけるセキュリティ上の問題
AIエージェントにツールを与えると、現実世界でアクションを実行する能力も与えることになります。メールの送信、SQLクエリの実行、決済APIの呼び出し、ファイルの変更を行うツールは、プロンプトインジェクションによってエージェントが侵害された場合や、単にエージェントがミスをした場合に、大きな被害を引き起こす可能性があります。エージェントによるツールアクセスの保護とは、個々のツール使用による被害を制限し、監査可能にし、可能な限り元に戻せるようにツール層を設計することです。
最小権限の原則
すべてのツールに最小権限の原則を適用します。エージェントには、割り当てられたタスクを完了するために必要な最小限のアクセス権だけを与え、それ以上は与えません。エージェントが顧客情報を読み取る必要がある場合は、保守に使用する完全な管理者接続ではなく、読み取り専用のデータベース接続を与えます。通知を送信する場合は、APIキーの権限を通知エンドポイントだけに限定します。最小権限によって、セキュリティインシデントが発生した場合の被害範囲を抑えられます。
from enum import Enum
from dataclasses import dataclass
class Permission(Enum):
READ_CUSTOMER_PROFILE = 'read_customer_profile'
SEND_NOTIFICATION = 'send_notification'
READ_ORDER_HISTORY = 'read_order_history'
WRITE_CUSTOMER_PROFILE = 'write_customer_profile' # higher privilege
PROCESS_REFUND = 'process_refund' # highest risk
@dataclass
class AgentIdentity:
agent_id: str
allowed_permissions: set[Permission]
# Support chat agent: read-only + send notification only
SUPPORT_AGENT = AgentIdentity(
agent_id='support-agent-v1',
allowed_permissions={
Permission.READ_CUSTOMER_PROFILE,
Permission.READ_ORDER_HISTORY,
Permission.SEND_NOTIFICATION # can notify, but NOT write or refund
}
)
# NOT: give every agent all permissions for convenience権限で制御されたツール実行
最小権限は設計時だけでなく、実行層でも適用します。すべてのツール呼び出しは、呼び出し元のエージェントがその特定のアクションに必要な権限を持っていることを確認する権限チェックを通過しなければなりません。これはLLMのプロンプトではなくコードによって適用されるため、エージェントやプロンプトインジェクションによって回避されることはありません。
class ToolGateway:
def __init__(self, agent: AgentIdentity):
self.agent = agent
self.audit_log = []
def execute_tool(self, tool_name: str, required_permission: Permission, tool_fn, **kwargs) -> dict:
# Permission check - enforced in code, not in the LLM prompt
if required_permission not in self.agent.allowed_permissions:
self.log_denied(tool_name, required_permission, kwargs)
raise PermissionError(
f'Agent {self.agent.agent_id} does not have permission: {required_permission.value}'
)
# Execute tool
result = tool_fn(**kwargs)
self.log_allowed(tool_name, kwargs, result)
return result
def log_denied(self, tool: str, permission: Permission, args: dict):
entry = {'type': 'DENIED', 'agent': self.agent.agent_id, 'tool': tool, 'permission': permission.value, 'args': args}
self.audit_log.append(entry)
print(f'SECURITY: Permission denied - {entry}')
def log_allowed(self, tool: str, args: dict, result):
entry = {'type': 'ALLOWED', 'agent': self.agent.agent_id, 'tool': tool, 'args': args}
self.audit_log.append(entry)
gateway = ToolGateway(SUPPORT_AGENT)
# This will succeed (agent has READ permission)
gateway.execute_tool('read_profile', Permission.READ_CUSTOMER_PROFILE, read_customer_profile, user_id='123')
# This will raise PermissionError (agent lacks PROCESS_REFUND permission)
gateway.execute_tool('process_refund', Permission.PROCESS_REFUND, process_refund, order_id='456', amount=50.00)破壊的なアクションに対する確認手順
レコードの削除、一括メールの送信、金融取引の処理など、一部のアクションは取り消せなかったり、大きな影響を及ぼしたりします。そのようなアクションをエージェントが実行する前に、人間による明示的な確認を必須にします。エージェントがアクション(何をしたいのか、なぜそうしたいのか)を提案し、人間が承認または拒否してから、初めて実行に進みます。この確認ゲートは、エージェントのエラーやインジェクションによる悪用に対する、最も効果的な防御策です。
HIGH_RISK_ACTIONS = {
'delete_customer_record',
'send_bulk_email',
'process_refund_over_100',
'deploy_code',
'revoke_user_access'
}
def execute_with_confirmation(tool_name: str, tool_args: dict, agent_reasoning: str) -> dict:
if tool_name in HIGH_RISK_ACTIONS:
# Pause and request human approval
approval_request = {
'action': tool_name,
'arguments': tool_args,
'agent_reasoning': agent_reasoning,
'risk_level': 'HIGH'
}
approval = request_human_approval(approval_request) # blocks until human responds
if not approval.approved:
return {'status': 'rejected', 'reason': approval.rejection_reason}
# Log the approval for audit trail
log_approval(tool_name, tool_args, approved_by=approval.approver_id)
# Execute only after confirmation
return execute_tool(tool_name, **tool_args)ツールの範囲設定:時間、ユーザー、データの境界
権限の種類に加えて、ツールアクセスを3つの観点で限定します。時間の境界:エージェントのセッション終了後に期限切れになるAPIキーまたはトークン。ユーザーの境界:ユーザーAを支援するエージェントが、指示されたとしてもユーザーBのデータにアクセスできないようにすること。データの境界:カスタマーサポートエージェントが、すべての顧客ではなく、現在対応中の顧客のデータだけにアクセスすること。これらの境界は、エージェントのプロンプトではなく、ツールの実装に組み込みます。
class ScopedCustomerTool:
def __init__(self, current_user_id: str, session_token: str):
self.user_id = current_user_id # agent can only access THIS user's data
self.token = session_token # expires at end of session
def get_customer_profile(self) -> dict:
# Hardcoded to current user - agent cannot change this via prompt
return db.query(
'SELECT * FROM customers WHERE id = %s',
(self.user_id,) # parameterized, always this user's ID only
)
def get_order_history(self, limit: int = 10) -> list:
# Even if the agent says 'get orders for user 999', it gets self.user_id
return db.query(
'SELECT * FROM orders WHERE customer_id = %s ORDER BY date DESC LIMIT %s',
(self.user_id, min(limit, 50)) # cap limit too
)
# Inject scoped tool into agent - cannot be overridden by prompt
def create_support_agent(user_id: str, session_token: str):
scoped_tools = ScopedCustomerTool(user_id, session_token)
return AgentExecutor(llm=llm, tools=[scoped_tools.get_customer_profile, scoped_tools.get_order_history])ツール引数の入力検証
エージェントはツールの引数をテキストとして生成します。ツールを実行する前に、すべての引数を厳格なスキーマに照らして検証します。これにより、ツールパラメーターを介したSQLインジェクション、パス・トラバーサル攻撃(エージェントがファイルパスとして '../../../etc/passwd' を渡す攻撃)、SSRF攻撃(エージェントが「リモートURL」パラメーターとして内部サービスのURLを渡す攻撃)、予期しない動作を引き起こす可能性がある整数オーバーフローや範囲外の値を防止できます。
from pydantic import BaseModel, validator, constr, confloat
import re
class SendEmailArgs(BaseModel):
to: str
subject: constr(max_length=200)
body: constr(max_length=10000)
@validator('to')
def must_be_company_email(cls, v):
if not re.match(r'^[^@]+@(?:yourcorp\.com|partner\.com)$', v):
raise ValueError('Email must be sent to yourcorp.com or partner.com domains only')
return v
class ReadFileArgs(BaseModel):
filename: constr(pattern=r'^[a-zA-Z0-9_\-\.]+$') # alphanumeric only, no path traversal
@validator('filename')
def no_parent_directory(cls, v):
if '..' in v or '/' in v or '\\' in v:
raise ValueError('Path traversal detected')
return v
# Validate before execution
def validated_send_email(args_dict: dict) -> dict:
args = SendEmailArgs(**args_dict) # raises ValueError on invalid input
return send_email(args.to, args.subject, args.body)変更不可能な監査ログ
エージェントが行ったすべてのツール呼び出しを、変更不可能な監査ログに記録する必要があります。ログエントリには、エージェントIDとセッションID、ツール名とすべての引数、戻り値、タイムスタンプ、呼び出しを行った理由としてエージェントが示した説明を含めます。変更不可能とは、エージェント(または攻撃者)がログエントリを削除したり変更したりできないという意味です。追記専用ストレージ(クラウドロギングサービス、ワンスライトデータベース、WORMストレージなど)を使用します。
import hashlib
import json
import time
class ImmutableAuditLog:
def __init__(self, log_backend):
self.backend = log_backend # e.g., CloudWatch, BigQuery, or append-only file
self.previous_hash = '0' * 64 # genesis hash
def record(self, agent_id: str, tool_name: str, args: dict, result, reasoning: str):
entry = {
'agent_id': agent_id,
'tool': tool_name,
'args': args,
'result_summary': str(result)[:500], # truncate large results
'reasoning': reasoning[:1000],
'timestamp': time.time(),
'previous_hash': self.previous_hash # chain entries like a blockchain
}
entry_json = json.dumps(entry, sort_keys=True)
entry['hash'] = hashlib.sha256(entry_json.encode()).hexdigest()
self.backend.append(entry) # append-only, never update
self.previous_hash = entry['hash']
return entry['hash']エージェントの異常な動作の検出
権限ゲートや監査ログを導入した場合でも、エージェントが侵害された、または予期せず動作していることを示す異常な動作パターンを監視します。異常の兆候には、サポートエージェントが突然、これまで一度も呼び出したことのないツールを呼び出すこと、1つのエージェントからのツール呼び出し頻度が異常に急増すること、通常とは異なる不審な引数(異常に長い文字列や奇妙な文字の並びなど)、通常とは異なる時間帯に行われる呼び出しなどがあります。
from collections import Counter
class AgentBehaviorMonitor:
def __init__(self):
self.tool_call_counts = Counter() # tracks historical tool usage
self.arg_length_history = {} # tracks typical argument lengths
def record_and_check(self, agent_id: str, tool_name: str, args: dict) -> list[str]:
key = f'{agent_id}:{tool_name}'
anomalies = []
self.tool_call_counts[key] += 1
# Flag tools never called before by this agent
if self.tool_call_counts[key] == 1 and tool_name not in COMMON_TOOLS:
anomalies.append(f'First time agent {agent_id} called unusual tool: {tool_name}')
# Flag unusually long arguments (potential injection payload)
total_arg_length = sum(len(str(v)) for v in args.values())
if tool_name not in self.arg_length_history:
self.arg_length_history[tool_name] = []
self.arg_length_history[tool_name].append(total_arg_length)
if len(self.arg_length_history[tool_name]) > 10:
avg = sum(self.arg_length_history[tool_name]) / len(self.arg_length_history[tool_name])
if total_arg_length > avg * 5:
anomalies.append(f'Unusually long arguments for {tool_name}: {total_arg_length} chars (avg: {avg:.0f})')
return anomaliesトークンの有効期限とセッションの範囲設定
エージェントのセッションには、明確な有効期間を設定する必要があります。セッション開始時に短期間だけ有効なトークンまたは認証情報をエージェントに発行し、セッション終了時に無効化します。エージェントのセッションが実行中に侵害された場合でも、攻撃者がそのセッションを悪用できる範囲は、トークンの有効期限によって制限されます。セッションの範囲を限定することで、1つの侵害されたエージェントが数日後に追撃攻撃へ利用されることも防げます。
import secrets
import time
class SessionTokenManager:
def __init__(self, ttl_seconds=3600):
self.tokens = {} # token -> (agent_id, user_id, expires_at)
self.ttl = ttl_seconds
def issue_token(self, agent_id: str, user_id: str) -> str:
token = secrets.token_urlsafe(32)
expires_at = time.time() + self.ttl
self.tokens[token] = (agent_id, user_id, expires_at)
return token
def validate_token(self, token: str) -> dict | None:
if token not in self.tokens:
return None
agent_id, user_id, expires_at = self.tokens[token]
if time.time() > expires_at:
del self.tokens[token] # clean up expired token
return None
return {'agent_id': agent_id, 'user_id': user_id}
def revoke_token(self, token: str):
self.tokens.pop(token, None)
token_manager = SessionTokenManager(ttl_seconds=1800) # 30-minute sessionsツールのセキュリティテスト
攻撃をシミュレートして、ツールのセキュリティ層をテストします。権限のないエージェントで制限されたツールの呼び出しを試みる、パス・トラバーサル用の引数を渡す、ツールパラメーターに命令文をインジェクションする、期限切れのトークンでツールを呼び出そうとする、といったテストを行います。包括的なツールセキュリティテストスイートでは、すべての権限境界、すべての検証ルール、すべての異常検出トリガーを対象にします。
可逆性を考慮した設計
可能な場合は、ツールを元に戻せる形、または段階的に実行できる形で設計します。レコードをすぐに削除する代わりに、削除済みであることを示すトゥームストーンを付けて復元できるようにします。メールをすぐに送信する代わりに、「送信待ち」キューに入れ、5分間の確認期間が経過してから実行します。金融取引は、決済前に保留状態にします。可逆性によってセキュリティリスクがなくなるわけではありませんが、攻撃が成功した場合の影響を大幅に抑えられます。
クイックチェック
このレッスンで学んだ、エージェントによるツールアクセスの保護についての理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、最小権限の原則によって各エージェントの役割に実際に必要な範囲までツール権限を制限できること、コードレベルの権限ゲートによってプロンプトインジェクションでは回避できない形でこれらの権限を適用できること、そして高リスクのアクションに対する人間による確認手順が、取り消し不能な操作を実行する前の最後の安全策になることを学びました。次は、LLMアプリケーションに対して体系的なレッドチーム演習を実施します。
よくある質問
「エージェントのツールアクセスを保護する」レッスンは無料ですか?
はい。「エージェントのツールアクセスを保護する」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「エージェントのツールアクセスを保護する」で何を学びますか?
エージェントのツール権限に最小権限の原則を適用し、破壊的な操作の前に確認ステップを設け、異常な動作を検出するためにエージェントの操作ログを監査します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「エージェントのツールアクセスを保護する」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- プロンプトインジェクション攻撃の分類
- RAGシステムのインジェクション対策
- エージェントのツールアクセスを保護する
- LLMアプリケーションをレッドチームで検証する