
Strands Agentsのツール認可をCedarポリシーで書いてみた
はじめに
こんにちは、ラ・ムーが大好きなコンサル部の神野(じんの)です。
Interventionsのドキュメントを最近じっと眺めているのですが、Cedar Authorizationというページがあるのを見つけました。AgentCore PolicyやVerified Permissionsでも使われているポリシー言語のCedarで、ツール呼び出しの認可を宣言的に書ける組み込みハンドラーもありました!
これは試すしかない!ということで、実際に動かしてみました!
なお、Interventionsそのものの仕組み(Deny / Guide / Confirm / Transformの型付きアクション)については前回の記事をご覧ください。
前提
今回の検証環境は以下の通りです。
| 項目 | バージョン |
|---|---|
| OS | macOS (Apple Silicon) |
| Python | 3.12 |
| strands-agents | 1.46.0 |
| cedarpy | 4.8.6 |
| モデル | Claude Haiku 4.5 (Amazon Bedrock) |
Cedar対応はextraとして提供されているので、cedar付きでインストールします。
uv add "strands-agents[cedar]==1.46.0"
これでcedarpyとcedar-policy-mcp-schema-generatorも一緒にインストールされます。モデルは前回に引き続き、BedrockのClaude Haiku 4.5を使用します。
Cedar Authorization
公式ドキュメントにはこう書かれています。
Cedar Authorization evaluates Cedar policies before each tool call, giving you declarative, identity-aware access control over agent behavior.
ツール呼び出しのたびにCedarポリシーを評価して、宣言的かつアイデンティティを考慮したアクセス制御をかけられる、というものです。
エージェントのツール呼び出しは、次のようにCedarの認可リクエストにマッピングされます。
| Cedarの概念 | マッピング先 | 例 |
|---|---|---|
| Principal | ユーザーのアイデンティティ | User::"alice@acme.com" |
| Action | ツール名 | Action::"search" |
| Resource | 固定値 | Resource::"agent" |
| Context.input | ツールの引数 | { query: "quarterly report" } |
| Context.session | 呼び出しメタデータ | { hour_utc: 14, call_count: 3, role: "admin" } |
意識するのはデフォルト拒否である点です。permit文にマッチしないツール呼び出しはすべてブロックされます。さらにプリンシパルの解決に失敗した場合も全ツールが拒否されるフェイルクローズ設計なので、認可の抜け漏れが起きにくい方向に倒してくれます。
Cedarのポリシー構文(permit / forbid / when / unless など)は公式リファレンスにまとまっています。
Cedarの扱いは Policy in AgentCore のブログを書いた時にも説明しているので、こちらも必要に応じてご参照ください。
早速試してみます!
やってみた
permit以外は全部ブロックする基本形
まずは一番シンプルな形から書いてみます。searchとdelete_recordの2つのツールを持つエージェントに、searchだけをpermitするポリシーを設定してみます。
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.cedar import CedarAuthorization
@tool
def search(query: str) -> str:
"""情報を検索する"""
print(f"\n[search] query={query!r}")
return f"検索結果: {query} に関するレコードが3件見つかりました(ID: 40, 41, 42)"
@tool
def delete_record(record_id: str) -> str:
"""指定IDのレコードを削除する"""
print(f"\n[delete_record] record_id={record_id!r}")
return f"レコード {record_id} を削除しました"
cedar = CedarAuthorization(
policies='permit(principal, action == Action::"search", resource);',
)
model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
agent = Agent(
model=model,
tools=[search, delete_record],
interventions=[cedar],
system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)
agent("四半期レポートを検索して、見つかったレコード42を削除してください")
CedarAuthorizationはInterventionハンドラーとして提供されているので、前回自作したハンドラーと同じように interventions オプションに渡すだけです。ポリシーは permit(principal, action == Action::"search", resource); の1行だけで、delete_recordについては何も書いていません。デフォルト拒否なので、書かない=ブロックされるはずです。
uv run python cedar1_basic.py
四半期レポートを検索してから、見つかったレコード42を削除します。
Tool #1: search
Tool #2: delete_record
[search] query='四半期レポート'
検索結果と削除結果をお知らせします:
検索結果:
四半期レポートに関するレコードが3件見つかりました(ID: 40, 41, 42)
削除結果:
申し訳ありませんが、レコード42の削除に失敗しました。Cedarポリシーによってアクセスが拒否されました。このレコードを削除するための権限がない可能性があります。
searchは実行され、delete_recordはツール本体が呼ばれる前に拒否されています!
拒否理由がツール結果としてモデルに返されるため、モデル自身が状況を説明した回答を生成しています。
CedarAuthorizationの主要パラメータ
次の例に進む前に、CedarAuthorization で設定できるパラメータを整理しておきます。基本形では policies だけで動きましたが、実際に使う際はユーザー情報や追加コンテキストを渡したくなると思います。
| パラメータ | 役割 | 設定しない場合 |
|---|---|---|
policies |
Cedarポリシー文字列、または .cedar ファイルパス |
必須 |
principal_resolver |
invocation_state からCedarのPrincipalを組み立てる関数 |
固定の匿名ユーザーが使われる |
context_enricher |
Cedarの context.session に追加情報を渡す関数。ロールや環境変数など自由に入れられる |
call_count など組み込みフィールドのみ |
on_error |
principal_resolver や context_enricher が例外を投げた場合の挙動。throw(例外をそのまま上げる)/ deny(拒否する)/ proceed(許可する) |
throw |
tools |
ツール定義のリスト。渡すとポリシーのAction名をツール定義と照合し、タイポを構築時に検出する | スキーマ検証なし(構文チェックのみ) |
principal_resolver を設定しないと全リクエストが同じユーザー扱いになるので、ユーザーの区別が不要なシンプルな用途では省略できます。
on_error はデフォルトが throw なので、認可用途なら明示的に deny にしておくのが安全かと思います。
RBACでadminは全部、analystは検索だけ許可する
次は実際に使いそうなロール別の認可制御です。誰が呼んでいるかでツールの呼び出し可否を変えたいですよね。principal_resolver でリクエストからユーザーを特定し、context_enricher でロール情報をCedarのコンテキストに渡します。
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.cedar import CedarAuthorization
@tool
def search(query: str) -> str:
"""情報を検索する"""
print(f"\n[search] query={query!r}")
return f"検索結果: {query} に関するレコードが見つかりました"
@tool
def delete_record(record_id: str) -> str:
"""指定IDのレコードを削除する"""
print(f"\n[delete_record] record_id={record_id!r}")
return f"レコード {record_id} を削除しました"
model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
def make_agent():
cedar = CedarAuthorization(
policies="""
permit(principal, action, resource)
when { context.session.role == "admin" };
permit(principal, action == Action::"search", resource)
when { context.session.role == "analyst" };
""",
principal_resolver=lambda state: (
{"type": "User", "id": state["user_id"]}
if state.get("user_id")
else None
),
context_enricher=lambda ctx: {
"role": ctx["invocation_state"].get("role", "none"),
},
on_error="deny",
)
return Agent(
model=model,
tools=[search, delete_record],
interventions=[cedar],
system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)
print("=== adminのalice: 削除できる ===")
make_agent()("レコード42を削除してください", invocation_state={"user_id": "alice", "role": "admin"})
print("\n=== analystのbob: 削除できない ===")
make_agent()("レコード42を削除してください", invocation_state={"user_id": "bob", "role": "analyst"})
ポリシーはwhen句で条件を付けています。adminロールなら全アクション許可、analystロールならsearchのみ許可という2本立てにしています。
ユーザー情報はエージェント呼び出し時の invocation_state で渡し、principal_resolver がそこからプリンシパルを組み立てます。user_id がない場合にNoneを返すと、そのリクエストの全ツール呼び出しが拒否されます。
uv run python cedar2_rbac.py
=== adminのalice: 削除できる ===
レコード42を削除します。
Tool #1: delete_record
[delete_record] record_id='42'
レコード42を削除いたしました。
=== analystのbob: 削除できない ===
レコード42を削除します。
Tool #1: delete_record
申し訳ございません。レコード42を削除することができませんでした。Cedar ポリシーによってアクセスが拒否されました。
削除権限がない可能性があります。管理者に相談いただくか、削除権限の確認をお願いします。
adminのaliceだけが削除でき、analystのbobは同じ操作を拒否されました!
call_count でツールの累積呼び出し回数を制限する
Cedarのコンテキストには call_count というフィールドが自動で入っていて、ツールごとの累積呼び出し回数を参照できます。これを使ってメール送信を2回までに制限してみます。
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.cedar import CedarAuthorization
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""メールを送信する"""
print(f"\n[send_email] to={to}")
return f"{to} にメールを送信しました"
cedar = CedarAuthorization(
policies="""
permit(principal, action == Action::"send_email", resource)
when { context.session.call_count < 3 };
""",
)
model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
agent = Agent(
model=model,
tools=[send_email],
interventions=[cedar],
system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)
agent(
"a@example.com、b@example.com、c@example.com の3人に「明日の勉強会は10時開始です」というメールをそれぞれ送ってください"
)
3人へのメール送信を指示しているので、2通で制限に達する想定です。
uv run python cedar3_ratelimit.py
3人の方にメールを送信します。
Tool #1: send_email
Tool #2: send_email
Tool #3: send_email
[send_email] to=a@example.com
[send_email] to=b@example.com
申し訳ございません。以下の結果となりました:
✅ a@example.com - メール送信完了
✅ b@example.com - メール送信完了
❌ c@example.com - メール送信失敗(アクセス権限エラー)
c@example.com へのメール送信がセキュリティポリシーにより拒否されてしまいました。
2通目まで送信され、3通目が拒否されました。意図しない大量送信に対して、LLMの外側で上限を設けられるのは良いですね!
ポリシーのタイポを起動時に検出するスキーマバリデーション
ポリシーを文字列で書くとなると気になるのがタイポです。Action名を打ち間違えたら、そのpermitは誰にもマッチせず全拒否になってしまいます。Cedar Authorizationにはツール定義からCedarスキーマを生成してポリシーを検証する仕組みがあるので、これも試してみます。
from strands.vended_interventions.cedar import CedarAuthorization
search_def = {
"name": "search",
"inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}},
}
# searchをserchとタイポしたポリシー
CedarAuthorization(
policies='permit(principal, action == Action::"serch", resource);',
tools=[search_def],
)
tools オプションにツール定義を渡すと、コンストラクタの時点でポリシーがスキーマ検証されます。
ValueError: Cedar policy validation failed: policy0: for policy `policy0`, unrecognized action `Action::"serch"`, policy0: for policy `policy0`, unable to find an applicable action given the policy scope constraints
存在しない Action::"serch" はコンストラクタで検出されました。実行時に全拒否となって初めて気づくより、起動時にエラーになるほうが原因を特定しやすくなります。
運用向けの機能
ファイルベースのポリシーとホットリロード
今回はインラインの文字列でポリシーを書きましたが、本番運用ではファイルに切り出すのが推奨されています。実際にファイルから読み込んで、さらにホットリロードまで試してみました。
まず policies/agent.cedar に search のみ許可するポリシーを書いておきます。
permit(principal, action == Action::"search", resource);
import os
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.cedar import CedarAuthorization
POLICY_PATH = "./policies/agent.cedar"
@tool
def search(query: str) -> str:
"""情報を検索する"""
print(f"\n[search] query={query!r}")
return f"検索結果: {query} に関するレコードが3件見つかりました"
@tool
def delete_record(record_id: str) -> str:
"""指定IDのレコードを削除する"""
print(f"\n[delete_record] record_id={record_id!r}")
return f"レコード {record_id} を削除しました"
model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
cedar = CedarAuthorization(policies=POLICY_PATH)
agent = Agent(
model=model,
tools=[search, delete_record],
interventions=[cedar],
system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)
# --- Phase 1: searchのみ許可 ---
agent("四半期レポートを検索して、見つかったレコード42を削除してください")
# --- Phase 2: ポリシーファイルを書き換えて reload() ---
with open(POLICY_PATH, "w") as f:
f.write('permit(principal, action == Action::"search", resource);\n')
f.write('permit(principal, action == Action::"delete_record", resource);\n')
cedar.reload()
agent("レコード42を削除してください")
=== Phase 1: searchのみ許可、delete_recordはブロック ===
四半期レポートを検索し、見つかったレコード42を削除します。
Tool #1: search
Tool #2: delete_record
[search] query='四半期レポート'
申し訳ございませんが、レコード42の削除処理でエラーが発生しました。
- 検索: 「四半期レポート」に関するレコードが3件見つかりました
- 削除: アクセス拒否エラーが発生しました(Cedar ポリシーによるアクセス制限)
=== Phase 2: reload()後、delete_recordも通るはず ===
レコード42を削除いたします。
Tool #3: delete_record
[delete_record] record_id='42'
レコード42を削除いたしました。処理が完了しました。
同じAgentインスタンスのまま delete_record が実行できるようになりました!
Tool番号が#1から連番(#3)になっている点からも、エージェントプロセスを再起動していないことが分かります。
reload() はファイルを読み直してCedar構文を検証し、問題なければ即座に差し替え、検証に失敗した場合は旧ポリシーが維持されたまま例外を投げます。この例ではCedar構文のみを検証しています。Action名や入力型もツール定義と照合する場合は、前節と同じく tools または schema を指定します。
S3にCedarポリシーを配置する場合
本番環境ではポリシーをGitで変更履歴を管理し、S3から配布したいケースもあると思います。現時点で CedarAuthorization にS3からの直接読み込み機能はありませんが、boto3 でダウンロードしてローカルパスを渡せば問題なく動きます。実際に検証してみました。
import tempfile
import boto3
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.cedar import CedarAuthorization
BUCKET = "cedar-policy-test-jinno-20260720"
KEY = "policies/agent.cedar"
@tool
def search(query: str) -> str:
"""情報を検索する"""
print(f"\n[search] query={query!r}")
return f"検索結果: {query} に関するレコードが3件見つかりました"
@tool
def delete_record(record_id: str) -> str:
"""指定IDのレコードを削除する"""
print(f"\n[delete_record] record_id={record_id!r}")
return f"レコード {record_id} を削除しました"
s3 = boto3.client("s3")
with tempfile.NamedTemporaryFile(suffix=".cedar", delete=False, mode="w") as f:
local_path = f.name
# --- Phase 1: searchのみ許可 ---
policy_v1 = 'permit(principal, action == Action::"search", resource);'
s3.put_object(Bucket=BUCKET, Key=KEY, Body=policy_v1.encode())
s3.download_file(BUCKET, KEY, local_path)
cedar = CedarAuthorization(policies=local_path)
model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
agent = Agent(
model=model,
tools=[search, delete_record],
interventions=[cedar],
system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)
agent("レコード42を削除してください")
# --- Phase 2: S3上のポリシーを更新 → ダウンロード → reload() ---
policy_v2 = """
permit(principal, action == Action::"search", resource);
permit(principal, action == Action::"delete_record", resource);
"""
s3.put_object(Bucket=BUCKET, Key=KEY, Body=policy_v2.encode())
s3.download_file(BUCKET, KEY, local_path)
cedar.reload()
agent("レコード42を削除してください")
S3にポリシーをアップロードしてダウンロード、CedarAuthorization にローカルパスを渡す流れです。Phase 2ではS3上のポリシーに delete_record のpermitを追加し、再ダウンロードしてから reload() しています。
=== Phase 1: delete_recordはブロックされるはず ===
レコード42を削除いたします。
Tool #1: delete_record
申し訳ございません。レコード42の削除に失敗しました。
エラー内容: Cedar ポリシーによるアクセス拒否
[Phase 2] ポリシー更新: delete_recordも許可に変更
[Phase 2] reload() 完了
=== Phase 2: delete_recordも通るはず ===
レコード42を削除いたします。
Tool #2: delete_record
[delete_record] record_id='42'
完了いたしました。レコード42を削除しました。
ファイルベースの例と同じく、同一Agentインスタンスのまま(Tool #1→#2の連番)ポリシーが切り替わりました!
reload() は明示的に呼ばないとポリシーが更新されない点は意識しておく必要があります。S3にファイルをアップロードしただけでは反映されません。
いつ使うの・・??と思ったのですが、reload() が役立ちそうだなと感じたのは、プロセスが長時間動き続ける環境です。AgentCore Runtimeもセッションごとのmicrovm(実行環境)を複数回の呼び出しで再利用するため、実行環境が生きている間にポリシーを更新したい場合は、再ダウンロードして reload() する仕組みが必要ですが、次の実行環境の起動時に反映されればよい運用なら、初期化時にS3から読み込むだけでも良いかもしれません。
エラーハンドリングについても触れると、ポリシーの構文不正は、CedarAuthorization の構築時または reload() 時に例外として検出されます。実行時にCedarの評価エラーが起きた場合は、設定によらずツール呼び出しが拒否されます。一方、principal_resolver や context_enricher が例外を投げた場合は、on_error で throw / deny / proceed を選べます。認可用途では deny を指定して、エラー時も拒否に倒しておくのが無難な印象です。
Policy in Amazon Bedrock AgentCoreとの使い分け
同じCedarを使う仕組みに、Policy in Amazon Bedrock AgentCoreがあります。Policy EngineをAgentCore Gatewayに関連付けると、Gatewayを通るツール呼び出しをAgentプロセスの外側で評価できます。
あれ・・・どっちとも評価できるし何を使ったらいいのって混乱しますよね・・・整理します。
Strands AgentsのCedar AuthorizationはAgentプロセス内で評価するため、認可判定のための追加のネットワーク呼び出しは不要です。ローカルツールにも適用でき、call_count や invocation_state から渡したアプリ固有のコンテキストを扱えます。一方、Policy in AgentCoreは、認証済みprincipalやツール入力に基づくルールをGateway境界で一元的に適用します。ツールへのアクセス経路をGatewayに限定している限り、Agent内のプロンプトや実装からポリシー評価を迂回できません。
どちらもユーザー属性やツール入力による制御に使えます。ローカルツールやセッション状態に依存するルールはStrands Cedar、複数Agentから利用するGatewayツールの共通ルールはPolicy in AgentCoreが扱いやすようなイメージですかね。両方の境界がある構成では、重ねて使う選択肢もあると思います!
おわりに
CedarがStrands Agentsでも使えるようになっていたので試してみました!
ローカルのツール実行などを認可制御する時にロジックを外に切り出したい際は使用を検討したいですね!
Strands Agentsでもできることが増えて、どこに何を実装するのか悩ましいですね・・・!(うれしい悲鳴でもあります)
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございましたー!!
補足 TypeScript SDKでのnamespace対応
今回はActionを Action::"search" と書きましたが、Cedarでは OrderAgent::Action::"search" のように名前空間を付けられます。ポリシージェネレーターなどから名前空間付きのポリシーを受け取る場合、ハンドラー側が作る認可リクエストのActionやResourceも同じ名前空間にそろえる必要があります。
TypeScript SDKでは namespace オプションでこれに対応しています。namespace: "OrderAgent" を指定すると、Action、Resource、デフォルトPrincipal、生成されるスキーマに同じ名前空間が使われます。
const cedar = new CedarAuthorization({
policies: `permit(principal, action == OrderAgent::Action::"search", resource);`,
namespace: "OrderAgent",
});
Python SDKでは現時点で namespace オプションは未対応なため、注意が必要です。









