Amazon Bedrock AgentCore で Dogwood ポリシーによる AI エージェントの制御を検証
本文の状態
日本語全文を表示中
詳細モードで約19分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
ExaWizards Tech Blog
ExaWizards は AWS の Dogwood ポリシー言語を Amazon Bedrock AgentCore で実証し、イベント履歴に基づく動的なツール呼び出し制御の実用性を示した。
AI深層分析を開く2026年8月21日 13:56
AI深層分析
キーポイント
Dogwood と AgentCore の連携実装
ExaWizards は AWS が発表した Dogwood ポリシー言語の参照実装を Amazon Bedrock AgentCore Gateway に適用し、実際の MCP ツール呼び出しでポリシーが機能する様子を確認した。
イベント履歴に基づく動的制御
「機密ドキュメントを読んだ後にメール送信を禁止する」という条件を、過去のイベント履歴(コンテキスト)に基づいて評価する temporal policy の動作を検証した結果、リクエストの文脈によって許可・拒否が切り替わることを確認した。
強制モードとデフォルト拒否の仕組み
ENFORCE モードではポリシーに明示的に許可されないリクエストはすべて拒否され、forbid が permit より優先されるという Cedar 準拠の評価規則が動作することを示した。
評価対象範囲の明確化
このポリシー機能は MCP のツール呼び出し(tools/call)と一覧取得(tools/list)に限定され、プロンプトやリソースの取得は対象外であることが確認された。
Gateway 認可の仕組み
インバウンド認可には AWS_IAM(SigV4 署名)を採用し、セッション ID と認証済み呼び出し元の単位で管理する必要があるため、認可なしでは呼び出し元の区別がつかない。
重要な引用
「機密ドキュメントを読んだ後はメール送信を禁止する」というガードレールの判定が、同じリクエストでもイベント履歴だけで変わる様子を <code>replay</code> コマンドで確認しました。
ENFORCE モードで engine を関連付けた Gateway はデフォルト拒否になります。
評価されるのは MCP のツール呼び出し(<code>tools/call</code>)と一覧(<code>tools/list</code>)で、プロンプトやリソースの取得(<code>prompts/*</code>、<code>resources/*</code>)はこのポリシー機能の認可対象ではありません。
temporal policy の履歴は「セッション ID × 認証済みの呼び出し元」の単位で管理されるため、認可なし(NONE)の Gateway では呼び出し元の区別がつかないからです。
編集コメントを表示
編集コメント
ExaWizards の検証により、AWS が提案する Dogwood ポリシー言語が実際のエージェント運用でどのように機能するかという具体的な知見が得られる。特にイベント履歴を条件とする動的制御の実装事例は、実務でのセキュリティ設計に大きな示唆を与える内容である。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
先端技術開発グループ(WAND)の伊賀です。**
前回の記事では、AWS のポリシー言語 Dogwood の参照実装をローカルで動かし、「機密ドキュメントを読んだ後はメール送信を禁止する」というガードレールの判定が、同じリクエストでもイベント履歴だけで変わる様子を replay コマンドで確認しました。
そのとき結びに書いた AgentCore Gateway で temporal policy を設定して試すことを今回実施します。
前回と同じポリシーを Amazon Bedrock AgentCore のポリシー機能(Policy in AgentCore)として AgentCore Gateway に適用し、実際の MCP ツール呼び出しが履歴で止まるところまで確認します。
発表ブログ(Securing AI agents with temporal policies in Amazon Bedrock AgentCore)と開発者ガイドを参照しながら進めました。
検証はすべて東京リージョン(ap-northeast-1)で行っています。
ポリシーが適用される仕組み
AgentCore のポリシー機能は、エージェントのツール呼び出しを Gateway の入り口で評価して許可または拒否します。
ポリシーは policy engine** という入れ物に登録し、その engine を Gateway に関連付けると適用されます。
Gateway 1つに関連付けられる engine は1つで、同じ engine は複数の Gateway で使い回せます。
関連付けるときに強制モードを選びます。
- LOG_ONLY:評価はするが遮断せず、決定をログに残す(試験運用向け)
- ENFORCE:評価の結果に基づいて実際に許可または拒否する
ENFORCE モードで engine を関連付けた Gateway はデフォルト拒否になります。
どのポリシーにも許可されないリクエストは拒否され、forbid は permit より常に優先されます。
この評価規則は Cedar と同じで、ポリシーには通常の Cedar と、temporal 条件を持つ Dogwood の両方を登録できます。
評価されるのは MCP のツール呼び出し(tools/call)と一覧(tools/list)で、プロンプトやリソースの取得(prompts/*、resources/*)はこのポリシー機能の認可対象ではありません(Gateway のインバウンド認可など、別の層の制御は残ります)。
試す構成
前回と同じ2つのツールを用意します。
read_document:ドキュメント ID を受け取って内容を返す
send_email:宛先を受け取ってメールを送信する
実体はどちらもダミー応答を返す1つの Lambda 関数です。
Gateway の Lambda ターゲットでは、呼ばれたツール名が Lambda の client context に入ってくるので、そこで分岐します。
def lambda_handler(event, context):
tool_name = ""
if context.client_context and context.client_context.custom:
tool_name = context.client_context.custom.get("bedrockAgentCoreToolName", "")
name = tool_name.split("___")[-1]
if name == "read_document":
doc = event.get("doc", "")
return {"doc": doc, "content": f"(dummy) content of {doc}"}
if name == "send_email":
to = event.get("to", "")
return {"status": "sent", "to": to}
return {"error": f"unknown tool: {tool_name}"}
Gateway のインバウンド認可は AWS_IAM(SigV4 署名)にしました。
temporal policy の履歴は「セッション ID × 認証済みの呼び出し元」の単位で管理されるため、認可なし(NONE)の Gateway では呼び出し元の区別がつかないからです。
ツールの入力の形は、ターゲット登録時に inlinePayload としてスキーマで渡します(tools.json)。
[
{
"name": "read_document",
"description": "Read a document by its ID",
"inputSchema": {
"type": "object",
"properties": {
"doc": { "type": "string", "description": "The document ID to read" }
},
"required": ["doc"]
}
},
{
"name": "send_email",
"description": "Send an email to the given address",
"inputSchema": {
"type": "object",
"properties": {
"to": { "type": "string", "description": "The recipient email address" }
},
"required": ["to"]
}
}
]
リソースの作成は AWS CLI で行います。
Gateway、Lambda ターゲット、policy engine を作り、engine を ENFORCE モードで関連付けます。
# Gateway(インバウンド認可は AWS_IAM)
aws bedrock-agentcore-control create-gateway \
--name dogwood-demo-gateway \
--role-arn "arn:aws:iam::<ACCOUNT_ID>:role/dogwood-demo-gateway-role" \
--protocol-type MCP \
--authorizer-type AWS_IAM \
--exception-level DEBUG
# 2つのツールを持つ Lambda ターゲット
aws bedrock-agentcore-control create-gateway-target \
--gateway-identifier <GATEWAY_ID> \
--name DocTools \
--target-configuration '{"mcp": {"lambda": {"lambdaArn": "<LAMBDA_ARN>", "toolSchema": {"inlinePayload": <tools.jsonの中身>}}}}' \
--credential-provider-configurations '[{"credentialProviderType": "GATEWAY_IAM_ROLE"}]'
# policy engine を作成し、ACTIVE を待ってから ENFORCE モードで Gateway に関連付け
aws bedrock-agentcore-control create-policy-engine --name dogwood_demo_engine
aws bedrock-agentcore-control wait policy-engine-active \
--policy-engine-id <POLICY_ENGINE_ID>
aws bedrock-agentcore-control update-gateway \
--gateway-identifier <GATEWAY_ID> \
--name dogwood-demo-gateway \
--role-arn "arn:aws:iam::<ACCOUNT_ID>:role/dogwood-demo-gateway-role" \
--protocol-type MCP \
--authorizer-type AWS_IAM \
--exception-level DEBUG \
--policy-engine-configuration '{"arn": "<POLICY_ENGINE_ARN>", "mode": "ENFORCE"}'
リソースの作成と更新はどれも非同期です。
policy engine とポリシーには CLI の waiter(wait policy-engine-active / policy-active / policy-deleted)があるので、後続のコマンドの前に挟みます。
Gateway とターゲットには waiter がないため、get-gateway と get-gateway-target で status が READY になったことを確認してから次に進みます。
--exception-level DEBUG は、拒否の理由を応答に含める検証用の設定です(後の実験で効いてきます)。
詳細エラーにはポリシー ID やロール、リソースの ARN まで含まれるため、本番に出す前に UpdateGateway で外しておくことになります。
つまずいたのは Gateway のサービスロールの権限です。
ポリシー機能そのものに必要なのは、engine の取得に使う bedrock-agentcore:GetPolicyEngine と、リクエスト評価に使う bedrock-agentcore:AuthorizeAction / PartiallyAuthorizeActions の3つで、ポリシー用の IAM 権限としてドキュメントにまとまっています。
GetPolicyEngine が不足していると engine の関連付けが失敗します(ドキュメントの説明は InternalServerException ですが、2026年8月の東京リージョンで試したところ、足りない権限を名指しする ValidationException が返りました)。
temporal policy を使う場合は、これに加えて bedrock-agentcore:GetWorkloadAccessToken が必要です。
セッションと呼び出し元の紐付けをおこなう WAT(Workload Access Token)を Gateway が取得するための権限で、temporal policy の必須権限として明記されています。
私はこの記載を見落としたまま進めてしまい、セッション ID 付きの呼び出しに対するツール応答の「not authorized to perform: bedrock-agentcore:GetWorkloadAccessToken」で気付いて、Gateway のサービスロールに追加しました。
サービスロールに付ける4アクションの最小ポリシーは、公式の例に沿うとこの形になります(ターゲットの Lambda を呼ぶ lambda:InvokeFunction は別のステートメントで付けます)。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetPolicyEngine"],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:policy-engine/<POLICY_ENGINE_ID>"
},
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:AuthorizeAction",
"bedrock-agentcore:PartiallyAuthorizeActions"
],
"Resource": [
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:policy-engine/<POLICY_ENGINE_ID>",
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:gateway/<GATEWAY_ID>"
]
},
{
"Effect": "Allow",
"Action": "bedrock-agentcore:GetWorkloadAccessToken",
"Resource": [
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:workload-identity-directory/default/workload-identity/<GATEWAY_ID>*"
]
}
]
}
ポリシーを登録する
先にデフォルト拒否を確かめておきます。
engine を関連付けた直後、ポリシーが0本の状態では tools/list が空になり、ツール呼び出しは拒否されます。
{
"code": -32002,
"message": "Tool Execution Denied: Tool call not allowed due to policy enforcement [No policy applies to the request (denied by default).]"
}
そこでまず、Gateway 上の呼び出しを全部許可する permit を登録しようとしました。
permit (
principal,
action,
resource == AgentCore::Gateway::"<GATEWAY_ARN>"
);
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name permit_all_tools \
--definition '{"cedar": {"statement": "<上のポリシー文>"}}'
ところが、ステータスは CREATE_FAILED になります。
ポリシー作成時には Cedar アナライザによる検証が走り、既定(FAIL_ON_ANY_FINDINGS)では指摘が1つでもあると作成が失敗します。
この permit への指摘は「Overly Permissive」で、statusReasons には、どの principal(IAM エンティティと OAuth ユーザー)にどのアクション(今後ターゲットに追加されるツールも含む)が全許可になるかが列挙されていました。
書きかけの緩いポリシーが既定で止まるのは行儀のよい振る舞いですが、今回はデモなので指摘を承知のうえで作成しました。
その前に後始末が1つあり、CREATE_FAILED になったポリシーもリソースとして残って名前を占有し続けるため、同名で作り直す前に削除し、waiter で削除完了を待ちました。
# 失敗したポリシーを削除し、完了を待つ
aws bedrock-agentcore-control delete-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <CREATE_FAILEDになったポリシーのID>
aws bedrock-agentcore-control wait policy-deleted \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <CREATE_FAILEDになったポリシーのID>
# アナライザの指摘を承知のうえで作り直し、ACTIVE を待つ
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name permit_all_tools \
--validation-mode IGNORE_ALL_FINDINGS \
--definition '{"cedar": {"statement": "<上のポリシー文>"}}'
aws bedrock-agentcore-control wait policy-active \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <作成したポリシーのID>
次が本命の temporal です。
前回ローカルで再生したポリシーを、AgentCore に合わせて書き換えます。
アクション名は「ターゲット名___ツール名」(アンダースコア3つ)、リソースは Gateway の ARN になります。
forbid (
principal,
action == AgentCore::Action::"DocTools___send_email",
resource == AgentCore::Gateway::"<GATEWAY_ARN>"
)
when temporal {
formerly within 24h AgentCore::Action::"DocTools___read_document"::response{ eventResource: resource, input.doc: "customer-secrets" }
};
前回のポリシー文と見比べると、temporal に eventResource: resource が増えています。
最初はこれを付けずに登録して CREATE_FAILED になりました。
temporal predicates do not constrain the matched event to the current request's resource:
`AgentCore::Action::"DocTools___read_document"::response`
each temporal predicate must include `eventResource: resource`
参照する過去イベントを、いま判定しているリクエストと同じ Gateway のものに限定する制約で、AgentCore ではすべての temporal に必須です。
なお、このポリシーが照合しているのは「入力に customer-secrets という ID を指定した読み取りが成功した」という事実であって、読み取った内容の機密性を検出しているわけではありません。
以降の「機密ドキュメントを読んだ後」は、この ID 指定の読み取りのことを指すとします。
実運用で機密性そのものに反応させたい場合は、ツール側が信頼できる分類ラベルをレスポンスに含め、output.classification のような出力フィールドを temporal 条件で照合する設計が考えられます(発表ブログのポリシー例も output.approved: true のように出力を照合しています)。
その場合、レスポンスに値を足すだけでは条件を書けません。
出力フィールドはツール定義の outputSchema で宣言されている必要があり、宣言がないと output.classification を参照するポリシーは作成時の検証で「field output.classification ... is not declared on that event」と弾かれます。
read_document に次の宣言を足してターゲットを更新したところ、同じポリシーがそのまま作成できました。
"outputSchema": {
"type": "object",
"properties": { "classification": { "type": "string" } },
"required": ["classification"]
}
登録には、--definition の cedar タイプではなく、Dogwood のポリシー文を受け付ける policy タイプを使います。
cedar タイプに temporal 構文を渡すと「unexpected token temporal」で弾かれます。
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name forbid_send_after_secret_read \
--definition '{"policy": {"statement": "<上のポリシー文>"}}'
aws bedrock-agentcore-control wait policy-active \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <作成したポリシーのID>
1つ引っかかったのは、この policy タイプが新しい定義タイプで、検証に使っていた AWS CLI 2.33.30 と boto3 1.42.76 のモデルには存在しなかったことです。
このときは CreatePolicy API を SigV4 署名で直接呼んで回避しました[[1]](#fn-b589-1)。
その後、最新の AWS CLI 2.36.25 と botocore 1.43.73 で上の形の登録がそのまま通ることを確認できたので、試す前に CLI と SDK を最新にしておくのがよさそうです。
細かい点ですが、ポリシー名は engine 内ではなくアカウント内で一意です。
別の engine に同名のポリシーがあると 409 で弾かれます。
履歴で判定が変わるところを見る
呼び出し側は、SigV4 で署名した JSON-RPC を Gateway の /mcp エンドポイントに送ります。
temporal policy を効かせるには、セッション ID を専用ヘッダーである x-amzn-bedrock-agentcore-policy-session-id で毎回送ります。
req = AWSRequest(method="POST", url=gateway_url, data=body, headers={
"Content-Type": "application/json",
"x-amzn-bedrock-agentcore-policy-session-id": session_id,
})
SigV4Auth(creds, "bedrock-agentcore", "ap-northeast-1").add_auth(req)
セッション ID は Gateway が生成してくれるものではなく、呼び出し側が生成して最初のリクエストから同じ値を送り続けます[[2]](#fn-b589-2)。
ではヘッダーを送らないとどうなるのでしょうか。
temporal policy がある engine では、セッション ID なしのツール呼び出しはエラーになります。
{
"code": -32006,
"message": "Tool Execution Denied: Policy Evaluation rejected the request as invalid [sessionId is required when temporal policies are enabled]"
}
準備が整ったので、セッション sess-alpha で5回呼び出します。
1. send_email {"to": "partner@example.com"} → {"status": "sent", ...}
2. read_document {"doc": "public-faq"} → 成功
3. send_email {"to": "partner@example.com"} → {"status": "sent", ...}
4. read_document {"doc": "customer-secrets"} → 成功
5. send_email {"to": "partner@example.com"} → 拒否
5回目の応答はこうなりました。
{
"code": -32002,
"message": "Tool Execution Denied: Tool call not allowed due to policy enforcement [Policy evaluation denied due to forbid_send_after_secret_read-vtkj5fubpn]"
}
3回目と5回目は宛先まで同じリクエストで、間に挟まった読み取りの履歴だけが判定を変えています。
前回 replay の出力で見た「公開ドキュメントの後は許可、機密ドキュメントの後は拒否」と同じ結果が、今度は AgentCore Gateway で再現できました。
どのポリシーによる拒否かまで応答に出るのは --exception-level DEBUG の効果です。
別のセッション ID(sess-beta)で send_email を呼ぶと成功しました。
履歴はセッションごとに独立していることがわかります。
逆に、同じセッション ID を使っても、認証された呼び出し元が違えば履歴は混ざりません。
セッションのキーがセッション ID と呼び出し元の組になっているためで、別の IAM ロールから同じ ID で send_email を呼んでも拒否されず、そのロール自身が機密を読んだ後で初めて拒否されました。
最後に tools/list です。
機密を読んだ後のセッションでも、send_email は一覧に出続けます。
一覧の評価は入力パラメータなどの文脈を持たないメタアクションとして扱われるためです。
コンソールでどう見えるか
ここまで CLI と API で作ってきたものを、コンソールで確認してみます。
engine の詳細画面には、関連付けた Gateway と強制モード、登録したポリシーの一覧が表示されます。

ポリシーの詳細画面では、temporal 構文が表示されるだけでなく、影響(forbid)、対象アクション、対象リソースが表示されます。
ステータスは「アクティブ(検証済み)」となっていました。

ポリシーの作成画面には、プロンプト、フォーム、Code の3つの入力モードがあります。
プロンプトモードで「customer-secrets というドキュメントを read_document で読み取った後は、send_email によるメール送信を禁止する」と日本語で書いて生成させたところ、手で書いたものとほぼ同じ temporal ポリシーがプレビューに出てきました。

Gateway 側の詳細画面でも、強制モードと関連付けられた engine を確認できます。

engine 詳細の「トレースを表示」からは CloudWatch の生成 AI オブザーバビリティに飛べて、ポリシー決定をトレースとして追う導線になっています(今回の環境ではトレース送信を設定していないため、中身までは確認していません)。
利用前に知っておきたいこと
動かすだけなら以上ですが、実際の設計に組み込むときに効いてくる挙動がいくつかあります。
設計にいちばん響くのは response イベントの記録タイミングです。
記録が行われるのはツール呼び出しの完了後で、呼び出し側が応答を受け取った時点で必ず記録済みであることを保証する記載は見当たりません。
実際の検証では、読み取りの応答を受けてから発行した送信は3回とも拒否されました。
一方、同一の IAM プリンシパルと同一セッション ID のまま、読み取りと送信を2スレッドから同時に発行する(発行時刻差は数ミリ秒)と、送信は3回とも許可されました。
応答はどちらも約1秒後にほぼ同時に返り、読み取りより先に送信の応答が返る回もあったので、読み取りの response が記録される前に送信が評価されたと考えられます。
発表ブログには「セッションあたりの同時認可リクエストは1件まで(no more than one concurrent authorization request per session)」という記述があります。
直列化の対象は認可の評価であって、先行するツールの完了(response の記録)を待つとは書かれていないため、この結果と矛盾するわけではありませんが、どこまでが保証された仕様なのかは注意する必要がありそうです。
今回の検証の範囲で有効だった緩和策は、順序に依存する呼び出しを前の応答を受け取ってから発行することです(ドキュメントの案内も同じです)。
記録の完了が保証されない以上、より厳密に実装するのであれば、要求時点で記録される ::request イベントを条件にする手も考えられそうです。
ただし ::request には、読み取りの実行が失敗した場合でも要求の事実だけで送信が止まり続ける(成功した読み取りに限定できない)というトレードオフがあります。
ポリシーの履歴だけでは要件を満たさない場合は、承認や実行の状態をアプリケーション側で管理するか、ツール側の実装で制御することになります。
タイミングのほかにも、運用の前提になる制約があります。
クォータの数値は執筆時点のものです。
- temporal policy を追加または変更すると、その engine の進行中の temporal セッションは無効化されます。無効化されたセッション ID での次のリクエストはエラーになるので(ドキュメントの記載は 409 ConflictException、MCP 呼び出しの検証では「Policy session is stale. Policies were updated after the session started.」)、新しいセッション ID で再開する必要があります
- temporal policy は engine あたり25本まで、1ポリシーあたり temporal 演算子3つ、時間ウィンドウは最大24時間という上限があります(ポリシー全体の上限は engine あたり1,000本です)
- セッション ID は呼び出し側が保有しています。ID を付け替えれば履歴はリセットされます。回数や累積額の上限として使う場合、この付け替えで制限を逃れられる形になっていないか(誰がセッション ID を決めるか)はセキュリティ設計として確認が必要です
- 今回インバウンド認可を AWS_IAM にした理由でもありますが、認可なし(authorizerType NONE)の Gateway には呼び出し元単位の隔離がなく、同じセッション ID を送ってきた呼び出し同士が1つのイベント履歴を共有します。他者の呼び出しが自分のレート制限や順序条件にカウントされるため、ドキュメントも認可なしの Gateway での temporal policy を advisory(参考程度)と位置付けています。呼び出し元ごとに隔離するには AWS_IAM か CUSTOM_JWT の認可を付けることになります
まとめ
前回ローカルの replay で実施した認可処理を、AgentCore Gateway 上の temporal policy として動かしました。
- 「
customer-secretsという ID の読み取りに成功した後のメール送信禁止」が、実際の MCP 呼び出しに対して履歴どおりに効いた。呼び出し側の変更はセッション ID ヘッダーを毎回送ることだけで、履歴の記録や判定のロジックはエージェントのコードに入れていない
- ポリシー本文の違いは temporal の
eventResource: resourceが必須になる点だけで、言語としては前回のものがそのまま通じた
- 検証を実施する上で3点のつまずきがあった。Gateway サービスロールの権限(ポリシー用の3つに加えて temporal では
GetWorkloadAccessTokenも要る)、temporal を受け付けるpolicy定義タイプに検証時の CLI と SDK が追いついていなかったこと、ポリシー名がアカウント内で一意であること
- セッション ID は呼び出し側が生成して送る。temporal policy がある engine では、セッションなしのリクエストは失敗となる
ツール一覧には temporal の状態が反映されないこと、ツールを並行発行すると response 条件をすり抜けうること、セッション ID の設計が実質的にガードレールの強度を決めうることなど、エージェント実装時に注意が必要な点も見えました。
脚注
- CreatePolicy の REST は
POST /policy-engines//policiesで、リクエストボディのdefinitionに{"policy": {"statement": "..."}}を渡します。 ↩︎
原文を表示
先端技術開発グループ(WAND)の伊賀です。**
前回の記事では、AWS のポリシー言語 Dogwood の参照実装をローカルで動かし、「機密ドキュメントを読んだ後はメール送信を禁止する」というガードレールの判定が、同じリクエストでもイベント履歴だけで変わる様子を replay コマンドで確認しました。
そのとき結びに書いた AgentCore Gateway で temporal policy を設定して試すことを今回実施します。
前回と同じポリシーを Amazon Bedrock AgentCore のポリシー機能(Policy in AgentCore)として AgentCore Gateway に適用し、実際の MCP ツール呼び出しが履歴で止まるところまで確認します。
発表ブログ(Securing AI agents with temporal policies in Amazon Bedrock AgentCore)と開発者ガイドを参照しながら進めました。
検証はすべて東京リージョン(ap-northeast-1)で行っています。
ポリシーが適用される仕組み
AgentCore のポリシー機能は、エージェントのツール呼び出しを Gateway の入り口で評価して許可または拒否します。
ポリシーは policy engine** という入れ物に登録し、その engine を Gateway に関連付けると適用されます。
Gateway 1つに関連付けられる engine は1つで、同じ engine は複数の Gateway で使い回せます。
関連付けるときに強制モードを選びます。
- LOG_ONLY:評価はするが遮断せず、決定をログに残す(試験運用向け)
- ENFORCE:評価の結果に基づいて実際に許可または拒否する
ENFORCE モードで engine を関連付けた Gateway はデフォルト拒否になります。
どのポリシーにも許可されないリクエストは拒否され、forbid は permit より常に優先されます。
この評価規則は Cedar と同じで、ポリシーには通常の Cedar と、temporal 条件を持つ Dogwood の両方を登録できます。
評価されるのは MCP のツール呼び出し(tools/call)と一覧(tools/list)で、プロンプトやリソースの取得(prompts/*、resources/*)はこのポリシー機能の認可対象ではありません(Gateway のインバウンド認可など、別の層の制御は残ります)。
試す構成
前回と同じ2つのツールを用意します。
- read_document:ドキュメント ID を受け取って内容を返す
- send_email:宛先を受け取ってメールを送信する
実体はどちらもダミー応答を返す1つの Lambda 関数です。
Gateway の Lambda ターゲットでは、呼ばれたツール名が Lambda の client context に入ってくるので、そこで分岐します。
def lambda_handler(event, context):
tool_name = ""
if context.client_context and context.client_context.custom:
tool_name = context.client_context.custom.get("bedrockAgentCoreToolName", "")
name = tool_name.split("___")[-1]
if name == "read_document":
doc = event.get("doc", "")
return {"doc": doc, "content": f"(dummy) content of {doc}"}
if name == "send_email":
to = event.get("to", "")
return {"status": "sent", "to": to}
return {"error": f"unknown tool: {tool_name}"}
Gateway のインバウンド認可は AWS_IAM(SigV4 署名)にしました。
temporal policy の履歴は「セッション ID × 認証済みの呼び出し元」の単位で管理されるため、認可なし(NONE)の Gateway では呼び出し元の区別がつかないからです。
ツールの入力の形は、ターゲット登録時に inlinePayload としてスキーマで渡します(tools.json)。
[
{
"name": "read_document",
"description": "Read a document by its ID",
"inputSchema": {
"type": "object",
"properties": {
"doc": { "type": "string", "description": "The document ID to read" }
},
"required": ["doc"]
}
},
{
"name": "send_email",
"description": "Send an email to the given address",
"inputSchema": {
"type": "object",
"properties": {
"to": { "type": "string", "description": "The recipient email address" }
},
"required": ["to"]
}
}
]
リソースの作成は AWS CLI で行います。
Gateway、Lambda ターゲット、policy engine を作り、engine を ENFORCE モードで関連付けます。
# Gateway(インバウンド認可は AWS_IAM)
aws bedrock-agentcore-control create-gateway \
--name dogwood-demo-gateway \
--role-arn "arn:aws:iam::<ACCOUNT_ID>:role/dogwood-demo-gateway-role" \
--protocol-type MCP \
--authorizer-type AWS_IAM \
--exception-level DEBUG
# 2つのツールを持つ Lambda ターゲット
aws bedrock-agentcore-control create-gateway-target \
--gateway-identifier <GATEWAY_ID> \
--name DocTools \
--target-configuration '{"mcp": {"lambda": {"lambdaArn": "<LAMBDA_ARN>", "toolSchema": {"inlinePayload": <tools.jsonの中身>}}}}' \
--credential-provider-configurations '[{"credentialProviderType": "GATEWAY_IAM_ROLE"}]'
# policy engine を作成し、ACTIVE を待ってから ENFORCE モードで Gateway に関連付け
aws bedrock-agentcore-control create-policy-engine --name dogwood_demo_engine
aws bedrock-agentcore-control wait policy-engine-active \
--policy-engine-id <POLICY_ENGINE_ID>
aws bedrock-agentcore-control update-gateway \
--gateway-identifier <GATEWAY_ID> \
--name dogwood-demo-gateway \
--role-arn "arn:aws:iam::<ACCOUNT_ID>:role/dogwood-demo-gateway-role" \
--protocol-type MCP \
--authorizer-type AWS_IAM \
--exception-level DEBUG \
--policy-engine-configuration '{"arn": "<POLICY_ENGINE_ARN>", "mode": "ENFORCE"}'
リソースの作成と更新はどれも非同期です。
policy engine とポリシーには CLI の waiter(wait policy-engine-active / policy-active / policy-deleted)があるので、後続のコマンドの前に挟みます。
Gateway とターゲットには waiter がないため、get-gateway と get-gateway-target で status が READY になったことを確認してから次に進みます。
--exception-level DEBUG は、拒否の理由を応答に含める検証用の設定です(後の実験で効いてきます)。
詳細エラーにはポリシー ID やロール、リソースの ARN まで含まれるため、本番に出す前に UpdateGateway で外しておくことになります。
つまずいたのは Gateway のサービスロールの権限です。
ポリシー機能そのものに必要なのは、engine の取得に使う bedrock-agentcore:GetPolicyEngine と、リクエスト評価に使う bedrock-agentcore:AuthorizeAction / PartiallyAuthorizeActions の3つで、ポリシー用の IAM 権限としてドキュメントにまとまっています。
GetPolicyEngine が不足していると engine の関連付けが失敗します(ドキュメントの説明は InternalServerException ですが、2026年8月の東京リージョンで試したところ、足りない権限を名指しする ValidationException が返りました)。
temporal policy を使う場合は、これに加えて bedrock-agentcore:GetWorkloadAccessToken が必要です。
セッションと呼び出し元の紐付けをおこなう WAT(Workload Access Token)を Gateway が取得するための権限で、temporal policy の必須権限として明記されています。
私はこの記載を見落としたまま進めてしまい、セッション ID 付きの呼び出しに対するツール応答の「not authorized to perform: bedrock-agentcore:GetWorkloadAccessToken」で気付いて、Gateway のサービスロールに追加しました。
サービスロールに付ける4アクションの最小ポリシーは、公式の例に沿うとこの形になります(ターゲットの Lambda を呼ぶ lambda:InvokeFunction は別のステートメントで付けます)。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetPolicyEngine"],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:policy-engine/<POLICY_ENGINE_ID>"
},
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:AuthorizeAction",
"bedrock-agentcore:PartiallyAuthorizeActions"
],
"Resource": [
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:policy-engine/<POLICY_ENGINE_ID>",
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:gateway/<GATEWAY_ID>"
]
},
{
"Effect": "Allow",
"Action": "bedrock-agentcore:GetWorkloadAccessToken",
"Resource": [
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:workload-identity-directory/default/workload-identity/<GATEWAY_ID>*"
]
}
]
}
ポリシーを登録する
先にデフォルト拒否を確かめておきます。
engine を関連付けた直後、ポリシーが0本の状態では tools/list が空になり、ツール呼び出しは拒否されます。
{
"code": -32002,
"message": "Tool Execution Denied: Tool call not allowed due to policy enforcement [No policy applies to the request (denied by default).]"
}
そこでまず、Gateway 上の呼び出しを全部許可する permit を登録しようとしました。
permit (
principal,
action,
resource == AgentCore::Gateway::"<GATEWAY_ARN>"
);
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name permit_all_tools \
--definition '{"cedar": {"statement": "<上のポリシー文>"}}'
ところが、ステータスは CREATE_FAILED になります。
ポリシー作成時には Cedar アナライザによる検証が走り、既定(FAIL_ON_ANY_FINDINGS)では指摘が1つでもあると作成が失敗します。
この permit への指摘は「Overly Permissive」で、statusReasons には、どの principal(IAM エンティティと OAuth ユーザー)にどのアクション(今後ターゲットに追加されるツールも含む)が全許可になるかが列挙されていました。
書きかけの緩いポリシーが既定で止まるのは行儀のよい振る舞いですが、今回はデモなので指摘を承知のうえで作成しました。
その前に後始末が1つあり、CREATE_FAILED になったポリシーもリソースとして残って名前を占有し続けるため、同名で作り直す前に削除し、waiter で削除完了を待ちました。
# 失敗したポリシーを削除し、完了を待つ
aws bedrock-agentcore-control delete-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <CREATE_FAILEDになったポリシーのID>
aws bedrock-agentcore-control wait policy-deleted \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <CREATE_FAILEDになったポリシーのID>
# アナライザの指摘を承知のうえで作り直し、ACTIVE を待つ
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name permit_all_tools \
--validation-mode IGNORE_ALL_FINDINGS \
--definition '{"cedar": {"statement": "<上のポリシー文>"}}'
aws bedrock-agentcore-control wait policy-active \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <作成したポリシーのID>
次が本命の temporal です。
前回ローカルで再生したポリシーを、AgentCore に合わせて書き換えます。
アクション名は「ターゲット名___ツール名」(アンダースコア3つ)、リソースは Gateway の ARN になります。
forbid (
principal,
action == AgentCore::Action::"DocTools___send_email",
resource == AgentCore::Gateway::"<GATEWAY_ARN>"
)
when temporal {
formerly within 24h AgentCore::Action::"DocTools___read_document"::response{ eventResource: resource, input.doc: "customer-secrets" }
};
前回のポリシー文と見比べると、temporal に eventResource: resource が増えています。
最初はこれを付けずに登録して CREATE_FAILED になりました。
temporal predicates do not constrain the matched event to the current request's resource:
`AgentCore::Action::"DocTools___read_document"::response`
each temporal predicate must include `eventResource: resource`
参照する過去イベントを、いま判定しているリクエストと同じ Gateway のものに限定する制約で、AgentCore ではすべての temporal に必須です。
なお、このポリシーが照合しているのは「入力に customer-secrets という ID を指定した読み取りが成功した」という事実であって、読み取った内容の機密性を検出しているわけではありません。
以降の「機密ドキュメントを読んだ後」は、この ID 指定の読み取りのことを指すとします。
実運用で機密性そのものに反応させたい場合は、ツール側が信頼できる分類ラベルをレスポンスに含め、output.classification のような出力フィールドを temporal 条件で照合する設計が考えられます(発表ブログのポリシー例も output.approved: true のように出力を照合しています)。
その場合、レスポンスに値を足すだけでは条件を書けません。
出力フィールドはツール定義の outputSchema で宣言されている必要があり、宣言がないと output.classification を参照するポリシーは作成時の検証で「field output.classification ... is not declared on that event」と弾かれます。
read_document に次の宣言を足してターゲットを更新したところ、同じポリシーがそのまま作成できました。
"outputSchema": {
"type": "object",
"properties": { "classification": { "type": "string" } },
"required": ["classification"]
}
登録には、--definition の cedar タイプではなく、Dogwood のポリシー文を受け付ける policy タイプを使います。
cedar タイプに temporal 構文を渡すと「unexpected token temporal」で弾かれます。
aws bedrock-agentcore-control create-policy \
--policy-engine-id <POLICY_ENGINE_ID> \
--name forbid_send_after_secret_read \
--definition '{"policy": {"statement": "<上のポリシー文>"}}'
aws bedrock-agentcore-control wait policy-active \
--policy-engine-id <POLICY_ENGINE_ID> \
--policy-id <作成したポリシーのID>
1つ引っかかったのは、この policy タイプが新しい定義タイプで、検証に使っていた AWS CLI 2.33.30 と boto3 1.42.76 のモデルには存在しなかったことです。
このときは CreatePolicy API を SigV4 署名で直接呼んで回避しました[[1]](#fn-b589-1)。
その後、最新の AWS CLI 2.36.25 と botocore 1.43.73 で上の形の登録がそのまま通ることを確認できたので、試す前に CLI と SDK を最新にしておくのがよさそうです。
細かい点ですが、ポリシー名は engine 内ではなくアカウント内で一意です。
別の engine に同名のポリシーがあると 409 で弾かれます。
履歴で判定が変わるところを見る
呼び出し側は、SigV4 で署名した JSON-RPC を Gateway の /mcp エンドポイントに送ります。
temporal policy を効かせるには、セッション ID を専用ヘッダーである x-amzn-bedrock-agentcore-policy-session-id で毎回送ります。
req = AWSRequest(method="POST", url=gateway_url, data=body, headers={
"Content-Type": "application/json",
"x-amzn-bedrock-agentcore-policy-session-id": session_id,
})
SigV4Auth(creds, "bedrock-agentcore", "ap-northeast-1").add_auth(req)
セッション ID は Gateway が生成してくれるものではなく、呼び出し側が生成して最初のリクエストから同じ値を送り続けます[[2]](#fn-b589-2)。
ではヘッダーを送らないとどうなるのでしょうか。
temporal policy がある engine では、セッション ID なしのツール呼び出しはエラーになります。
{
"code": -32006,
"message": "Tool Execution Denied: Policy Evaluation rejected the request as invalid [sessionId is required when temporal policies are enabled]"
}
準備が整ったので、セッション sess-alpha で5回呼び出します。
1. send_email {"to": "partner@example.com"} → {"status": "sent", ...}
2. read_document {"doc": "public-faq"} → 成功
3. send_email {"to": "partner@example.com"} → {"status": "sent", ...}
4. read_document {"doc": "customer-secrets"} → 成功
5. send_email {"to": "partner@example.com"} → 拒否
5回目の応答はこうなりました。
{
"code": -32002,
"message": "Tool Execution Denied: Tool call not allowed due to policy enforcement [Policy evaluation denied due to forbid_send_after_secret_read-vtkj5fubpn]"
}
3回目と5回目は宛先まで同じリクエストで、間に挟まった読み取りの履歴だけが判定を変えています。
前回 replay の出力で見た「公開ドキュメントの後は許可、機密ドキュメントの後は拒否」と同じ結果が、今度は AgentCore Gateway で再現できました。
どのポリシーによる拒否かまで応答に出るのは --exception-level DEBUG の効果です。
別のセッション ID(sess-beta)で send_email を呼ぶと成功しました。
履歴はセッションごとに独立していることがわかります。
逆に、同じセッション ID を使っても、認証された呼び出し元が違えば履歴は混ざりません。
セッションのキーがセッション ID と呼び出し元の組になっているためで、別の IAM ロールから同じ ID で send_email を呼んでも拒否されず、そのロール自身が機密を読んだ後で初めて拒否されました。
最後に tools/list です。
機密を読んだ後のセッションでも、send_email は一覧に出続けます。
一覧の評価は入力パラメータなどの文脈を持たないメタアクションとして扱われるためです。
コンソールでどう見えるか
ここまで CLI と API で作ってきたものを、コンソールで確認してみます。
engine の詳細画面には、関連付けた Gateway と強制モード、登録したポリシーの一覧が表示されます。

ポリシーの詳細画面では、temporal 構文が表示されるだけでなく、影響(forbid)、対象アクション、対象リソースが表示されます。
ステータスは「アクティブ(検証済み)」となっていました。

ポリシーの作成画面には、プロンプト、フォーム、Code の3つの入力モードがあります。
プロンプトモードで「customer-secrets というドキュメントを read_document で読み取った後は、send_email によるメール送信を禁止する」と日本語で書いて生成させたところ、手で書いたものとほぼ同じ temporal ポリシーがプレビューに出てきました。

Gateway 側の詳細画面でも、強制モードと関連付けられた engine を確認できます。

engine 詳細の「トレースを表示」からは CloudWatch の生成 AI オブザーバビリティに飛べて、ポリシー決定をトレースとして追う導線になっています(今回の環境ではトレース送信を設定していないため、中身までは確認していません)。
利用前に知っておきたいこと
動かすだけなら以上ですが、実際の設計に組み込むときに効いてくる挙動がいくつかあります。
設計にいちばん響くのは response イベントの記録タイミングです。
記録が行われるのはツール呼び出しの完了後で、呼び出し側が応答を受け取った時点で必ず記録済みであることを保証する記載は見当たりません。
実際の検証では、読み取りの応答を受けてから発行した送信は3回とも拒否されました。
一方、同一の IAM プリンシパルと同一セッション ID のまま、読み取りと送信を2スレッドから同時に発行する(発行時刻差は数ミリ秒)と、送信は3回とも許可されました。
応答はどちらも約1秒後にほぼ同時に返り、読み取りより先に送信の応答が返る回もあったので、読み取りの response が記録される前に送信が評価されたと考えられます。
発表ブログには「セッションあたりの同時認可リクエストは1件まで(no more than one concurrent authorization request per session)」という記述があります。
直列化の対象は認可の評価であって、先行するツールの完了(response の記録)を待つとは書かれていないため、この結果と矛盾するわけではありませんが、どこまでが保証された仕様なのかは注意する必要がありそうです。
今回の検証の範囲で有効だった緩和策は、順序に依存する呼び出しを前の応答を受け取ってから発行することです(ドキュメントの案内も同じです)。
記録の完了が保証されない以上、より厳密に実装するのであれば、要求時点で記録される ::request イベントを条件にする手も考えられそうです。
ただし ::request には、読み取りの実行が失敗した場合でも要求の事実だけで送信が止まり続ける(成功した読み取りに限定できない)というトレードオフがあります。
ポリシーの履歴だけでは要件を満たさない場合は、承認や実行の状態をアプリケーション側で管理するか、ツール側の実装で制御することになります。
タイミングのほかにも、運用の前提になる制約があります。
クォータの数値は執筆時点のものです。
- temporal policy を追加または変更すると、その engine の進行中の temporal セッションは無効化されます。無効化されたセッション ID での次のリクエストはエラーになるので(ドキュメントの記載は 409 ConflictException、MCP 呼び出しの検証では「Policy session is stale. Policies were updated after the session started.」)、新しいセッション ID で再開する必要があります
- temporal policy は engine あたり25本まで、1ポリシーあたり temporal 演算子3つ、時間ウィンドウは最大24時間という上限があります(ポリシー全体の上限は engine あたり1,000本です)
- セッション ID は呼び出し側が保有しています。ID を付け替えれば履歴はリセットされます。回数や累積額の上限として使う場合、この付け替えで制限を逃れられる形になっていないか(誰がセッション ID を決めるか)はセキュリティ設計として確認が必要です
- 今回インバウンド認可を AWS_IAM にした理由でもありますが、認可なし(authorizerType NONE)の Gateway には呼び出し元単位の隔離がなく、同じセッション ID を送ってきた呼び出し同士が1つのイベント履歴を共有します。他者の呼び出しが自分のレート制限や順序条件にカウントされるため、ドキュメントも認可なしの Gateway での temporal policy を advisory(参考程度)と位置付けています。呼び出し元ごとに隔離するには AWS_IAM か CUSTOM_JWT の認可を付けることになります
まとめ
前回ローカルの replay で実施した認可処理を、AgentCore Gateway 上の temporal policy として動かしました。
- 「customer-secrets という ID の読み取りに成功した後のメール送信禁止」が、実際の MCP 呼び出しに対して履歴どおりに効いた。呼び出し側の変更はセッション ID ヘッダーを毎回送ることだけで、履歴の記録や判定のロジックはエージェントのコードに入れていない
- ポリシー本文の違いは temporal の eventResource: resource が必須になる点だけで、言語としては前回のものがそのまま通じた
- 検証を実施する上で3点のつまずきがあった。Gateway サービスロールの権限(ポリシー用の3つに加えて temporal では GetWorkloadAccessToken も要る)、temporal を受け付ける policy 定義タイプに検証時の CLI と SDK が追いついていなかったこと、ポリシー名がアカウント内で一意であること
- セッション ID は呼び出し側が生成して送る。temporal policy がある engine では、セッションなしのリクエストは失敗となる
ツール一覧には temporal の状態が反映されないこと、ツールを並行発行すると response 条件をすり抜けうること、セッション ID の設計が実質的にガードレールの強度を決めうることなど、エージェント実装時に注意が必要な点も見えました。
脚注
- CreatePolicy の REST は POST /policy-engines/<エンジンID>/policies で、リクエストボディの definition に {"policy": {"statement": "..."}} を渡します。 ↩︎
- 発表ブログには、ヘッダーを渡さない場合は Gateway がセッション ID を生成するという記述があります。ただし執筆時点の開発者ガイドは「Gateway は生成しない。temporal policy がある engine ではセッション ID なしのリクエストは validation error になる」と明記しており、東京リージョンでの検証結果も開発者ガイドのとおりでした。 ↩︎
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み