社内向けの業務エージェントを作っていて、どうしても割り切れなかった問題がある。ツール呼び出しの1回1回はすべて権限的に正しいのに、並びが間違っているというケースだ。
具体的にはこうだった。承認ワークフローを持つエージェントに approve_request と execute_transfer という2つのMCPツールを持たせた。IAMポリシー的にはどちらもエージェントの実行ロールで叩ける。ところがLLMが承認をスキップして先に送金を呼んでしまう。プロンプトで「必ず承認を先に取ること」と書いても、コンテキストが長くなると平気で無視される。仕方なくエージェント側のPythonコードに if not state.approved: raise という状態チェックを埋め込んだのだが、これがエージェントを増やすたびに書き直しになる。しかもモデルを差し替えると挙動が変わる。
2026年8月6日にAWSが公開した Dogwood は、まさにこの問題に対する言語レベルの回答だった。Cedar を拡張して「過去に何が起きたか」をポリシーの条件に書けるようにした、オープンソースのガバナンス言語だ。
実際に仕様を読んで手元で試してみたので、何ができて何ができないのか、そして本番投入するとしたら何を自前で用意する必要があるのかをまとめる。
Dogwood とは何か
一言でいうと、Cedar に時相条件(temporal conditions)を足した言語だ。
Cedar は AWS が2023年にオープンソース化したポリシー言語で、Amazon Verified Permissions の裏側で動いている。permit(principal, action, resource) when { ... } という形式で認可ルールを書く。特徴は「ある1回のリクエストが許可されるか」を型安全に、しかも形式検証可能な形で判定できること。
Dogwood はこの Cedar を包含する。仕様上、構文的に妥当な Cedar ポリシーは、そのまま構文的に妥当な Dogwood ポリシーになる。既存の Cedar 資産を書き換える必要がない。その上に「直近1時間以内に同じユーザーがログインしていたか」「このセッションで累計いくら送金したか」といった、イベント履歴を参照する条件を追加している。
| 項目 | 内容 |
|---|---|
| 公開日 | 2026年8月6日 |
| ライセンス | Apache-2.0 |
| リポジトリ | dogwood-policy/dogwood |
| 実装言語 | Rust(dogwood-language クレート) |
| 理論的基礎 | Metric First-Order Temporal Logic (MFOTL) |
| ベース | Cedar(後方互換) |
| マネージド提供 | Amazon Bedrock AgentCore Policy |
重要な但し書きがひとつ。リポジトリで配布されているのはリファレンスインタプリタであり、AWS自身が「本番の認可エンジンとして使うことは意図していない」と明記している。言語のセマンティクスを理解・検証するためのものだ。実運用でマネージドに使いたい場合は Bedrock AgentCore Policy 側を見ることになる。
なぜ Cedar だけでは足りないのか
Cedar の評価モデルは徹底して「一点在時(point-in-time)」だ。認可リクエストが来たら、principal / action / resource / context の4つだけを見て許可・拒否を返す。ステートレスであることが Cedar の強みで、だからこそ形式検証ができる。
しかしエージェントのツール呼び出しでは、この前提が崩れる場面が3つある。
1. 前提ステップの強制
「送金する前に人間の承認が記録されていること」。承認イベントは別のリクエストなので、送金リクエストの context には存在しない。
2. セッション横断の累積制限
「1回の送金は1,000ドル未満」は Cedar で書ける。だが「セッション合計で5,000ドルを超えない」は書けない。個々のリクエストは全部1,000ドル未満で通ってしまう。
3. 並行リクエストによる上限回避
ここが一番厄介で、私が最初に踏んだ落とし穴でもある。「合計5,000ドルまで」をレスポンスイベントで数えるように書くと、2,000ドルの送金を3本同時に投げられたときに全部通る。3本ともまだレスポンスが返っていないので、累計は0のままだからだ。
Dogwood のドキュメントはこの点を明示していて、レート制限や累計制限は ::response ではなく ::request イベントで数えろと書いている。地味だが、これを知らずに書くと静かに破られる類のバグになる。
まず動かしてみる
理屈より先に動かした方が早い。リポジトリの read_after_login という例が一番わかりやすいので、これをそのまま追いかける。
やりたいことは「同じユーザーが直近1時間以内にログインに成功している場合だけ、Read を許可する」。
アクションスキーマ
Dogwood は Cedar のスキーマファイル(.cedarschema)をそのまま使う。
namespace Drupe {
type LoginInput = { user: String };
type ReadInput = { user: String };
entity Gateway;
entity OAuthUser = { id: String } tags String;
action "Login" appliesTo {
principal: [OAuthUser],
resource: [Gateway],
context: { input: LoginInput }
};
action "Read" appliesTo {
principal: [OAuthUser],
resource: [Gateway],
context: { input: ReadInput }
};
}
Cedar を書いたことがある人なら見慣れた形のはずだ。Dogwood 固有の約束は context の構造にあって、input(ツールの引数)、output(ツールの戻り値、任意)、system(現在時刻など)という3つのキーを持つ規約になっている。MCPツールの入出力スキーマをそのままマッピングすることを想定した設計だ。
ポリシー
// policy.dw
@id("read_after_login")
permit (
principal,
action == Drupe::Action::"Read",
resource
)
when temporal {
formerly within 1h Drupe::Action::"Login"::response{ input.user: context.input.user }
};
when temporal { ... } が Dogwood の中核だ。この中には Cedar の式ではなく、時相式を書く。
formerly within 1h X は「直近1時間以内に、Xにマッチするイベントが少なくとも1回あったか」を意味する。
そして { input.user: context.input.user } の部分が地味に大事なところで、これは**ピン留め(pin)**と呼ばれる相関条件だ。「過去のログインイベントの input.user が、いま来ている Read リクエストの context.input.user と一致すること」を要求している。これを書き忘れると「誰かがログインしていれば誰でも Read できる」というザルなポリシーになる。最初に自分で書いたときは見事にこれをやらかした。
イベントトレース
Dogwood はイベントの列(トレース)に対してポリシーを再生できる。トレースの1行はこうなっている。
@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1")
request_context(input: { user: "alice" })
Drupe::Action::"Login"::request(input: { user: "alice" },
callerPrincipal: Drupe::OAuthUser::"alice",
callerResource: Drupe::Gateway::"gw1",
requestId: "u1")
@0 は相対秒のタイムスタンプ。request_context(...) はそのリクエスト自身の context(Cedar 条件から context.* で参照される)、末尾の Drupe::Action::"Login"::request(...) が履歴に積まれるイベント本体だ。
この2つが別々のバッグである点は運用上の落とし穴になる。時相述語と Cedar 条件の両方で使うフィールドは、両方に入れておく必要がある。片方だけだと、エラーにならずに黙って条件が弱まる。
トレース全体は4イベント。alice がログイン(@0 request / @5 response)、10秒後に Read、2時間後にもう一度 Read。
再生する
dogwood validate policy.dw --policy-schema schema.cedarschema
dogwood replay policy.dw \
--policy-schema schema.cedarschema \
--trace trace.log
出力はこうなる。
@0 (time point 0): DENY
@10 (time point 1): ALLOW [rules: 0]
@7200 (time point 2): DENY
読み方はこうだ。
-
@0— ログインリクエスト自体。このポリシーはReadしか許可していないので、マッチする permit がなく DENY。ただしイベントは履歴に積まれる -
@5— ログインのレスポンス。デフォルトのイベントスキーマではresponseは判定を伴わない「履歴専用」イベントなので、判定行そのものが出ない -
@10— 10秒後の Read。1時間ウィンドウ内にログイン成功があるので ALLOW -
@7200— 2時間後の Read。7200秒 > 3600秒でウィンドウから外れたので DENY
ステートレスな Cedar では絶対に表現できない挙動が、宣言的に14行で書けている。ここで「これは使える」と思った。
内部で何が起きているか ── Cedar への lowering
Dogwood の設計で一番うまいと感じたのは、時相条件を Cedar の式として評価していないことだ。
dogwood lower を実行すると、先ほどのポリシーがどう変換されるかが見える。
dogwood lower policy.dw --policy-schema schema.cedarschema --emit cedar-policies
@id("read_after_login")
permit(principal, action == Drupe::Action::"Read", resource)
when { context.policy_0__temporal_0 };
時相条件がまるごと context.policy_0__temporal_0 というただのブール値スロットに置き換わっている。
つまり実行時の流れはこうなる。
Dogwood は「時相モニタ」と「Cedar 認可エンジン」という2つの評価器を持ち、前者の結果を後者の context に注入している。Information Providers(後述)も同じ仕組みで context.providers.<id> に降ろされる。
この分離が効いてくるのは、バックエンドを差し替えられる点だ。ポリシーエンジン側はローカルの Cedar でも外部のポリシーストアでもよく、時相エンジン側はインメモリでも DB バックエンドでもいい。両者を独立に選べる。認可基盤を自社で組む立場だと、この境界の切り方はかなり実用的に見える。
一方で、失うものもはっきりしている。Cedar が提供する自動推論(automated reasoning)による解析ツールは、時相条件には適用できない。Cedar の売りのひとつだった「このポリシー集合は矛盾していないか」「このポリシーは常に拒否するのではないか」という形式的な検証が、時相部分については効かない。ここはトレードオフとして理解しておく必要がある。
時相演算子は3つだけ
Dogwood のコア言語が持つ時相演算子は、過去方向の3つだけだ。少ない。だが組み合わせで大抵のことは書ける。
formerly ── 「ウィンドウ内に少なくとも1回」
formerly within 1h Drupe::Action::"ApproveSale"::response{
input.stock: context.input.stock,
output.approved: true
}
一番よく使う。承認ゲート、事前ステップの強制はほぼこれで済む。output.approved: true のようにレスポンスの中身まで条件にできるのがポイントで、「承認アクションが呼ばれた」ではなく「承認が実際に通った」を要求できる。
previous ── 「直前の1時点だけ」
previous within 1h Drupe::Action::"Login"::response{
input.user: context.input.user,
output.result: true
}
formerly より厳しく、直前のタイムポイント(i-1)だけを見る。「ウィンドウ内のどこかで」ではなく「直前が必ずこれ」を要求したいときに使う。最初のタイムポイントでは常に false。
since ── 「ずっと成り立ち続けている」
中置演算子で、左 since within W 右 は「右が起きた時点から現在まで、左が継続して成り立っている」を意味する。
実務で一番よく出るのは左辺を否定するイディオムだ。
@id("access_not_revoked_since_grant")
permit (
principal,
action == Drupe::Action::"Access",
resource
)
when temporal {
!Drupe::Action::"Revoke"::request{
input.user: context.input.user,
input.resource: context.input.resource
}
since within 1h
Drupe::Action::"Grant"::request{
input.user: context.input.user,
input.resource: context.input.resource
}
};
「1時間以内に権限が付与されて、それ以降 revoke されていないなら Access を許可」。! は since より結合が強いので、!A since within W B は左辺 A だけを否定する。
「セッションが開いている間だけ有効」「人間が離席したら権限を絞る」といった要件が、この形にきれいに落ちる。個人的には Dogwood で一番好きな演算子だ。ログイン/ログアウト、承認/取消、開始/終了といった対になるイベントを扱う場面が実務では圧倒的に多い。
時間ウィンドウの書き方
within には s / m / h / d が使える(30m、1h、7d など)。境界は閉区間で、ちょうど W 秒前のイベントもウィンドウ内に含まれる。
デフォルトの上限は24時間で、イベントスキーマの max_window で変更できる。無制限にしないのは、履歴の保持コストと評価コストが窓幅に比例するからだ。
max_window = 24h
集約 ── 累計とレート制限
コア言語の集約は count と sum の2つだけ。min / max / avg はない。
生の構文はやや重い。「Alert を許可するのは累計送金額が200を超えているときだけ」を書くとこうなる。
@id("alert_total_transfer_over_200")
permit (
principal,
action == Drupe::Action::"Alert",
resource
)
when temporal {
exists (total: Long). (
(sum a for (a: Long). where Drupe::Action::"Transfer"::request{ input.amount: a }) == total
&& total > 200
)
};
集約は必ず比較の直接のオペランドとして書かなければならず、その比較変数は exists (x: T). ... で束縛する必要がある。ネストした集約は書けない。
最初にハマったこと ── 集約の中身に時相演算子がない
上のポリシー、実は永遠に発火しない。リポジトリの例にも「every verdict in the trace is therefore a deny」と書いてある。
理由は、sum の中身が formerly などで包まれていない裸の述語だからだ。裸の述語は現在のタイムポイントしか走査しない。そして permit のスコープは「現在のアクションが Alert であること」を要求している。Alert の判定時点に Transfer イベントは存在しないので、集約対象は常に空で total は 0。
正しくはこう書く。
def temporal sum_formerly(?a, ?w, ?body) {
sum ?a for (?a: Long), ($t: Timepoint).
where (formerly within ?w (?body && tp($t)))
};
@id("transfer_sum_over_100")
permit (principal, action == Drupe::Action::"Alert", resource)
when temporal {
exists (total: Long). (
(sum_formerly(a, 1h, Drupe::Action::"Transfer"::request{
input.user: _, input.amount: a
})) == total
&& total > 100
)
};
集約の本体を formerly within ?w (... && tp($t)) で包むことで、ウィンドウ内の各タイムポイントを走査するようになる。tp($t) は「現在のタイムポイント番号」を束縛する演算子で、これがないと異なる時点の同一値が重複除去されて数が合わなくなる。
この「裸の述語は現在時点だけ」という挙動はエラーにならずに黙って通るので、必ず replay で検算するべきところだ。バリデーションが通ったからといって意図通りとは限らない。
標準ライブラリのマクロを使う
毎回この定型を書くのは辛い。Dogwood はマクロを持っていて、標準ライブラリに主要なパターンが用意されている。
| マクロ | 用途 |
|---|---|
count_within(?w, ?s) |
ウィンドウ ?w 内で条件 ?s が成立した回数 |
sum_within(?a, ?w, ?body) |
ウィンドウ内の数値 ?a の合計 |
count_distinct_within(?k, ?w, ?s) |
キー ?k のユニーク数 |
bind(?n, ?A, ?B) |
exists (n: Long). (?A == n && ?B) の定型を生成 |
これを使うと、実務で書きたい形にかなり近くなる。
// 1時間に5回を超える送金リクエストを禁止する
@id("rate_limit_transfer")
forbid (principal, action == AgentCore::Action::"Transfer", resource)
when temporal {
count_within(1h, AgentCore::Action::"Transfer"::request{ input.amount: _ }) > 5
};
// セッション累計が5000を超える送金を禁止する
@id("budget_cap_transfer")
forbid (principal, action == AgentCore::Action::"Transfer", resource)
when temporal {
bind(prior,
sum_within(a, 1h, AgentCore::Action::"Transfer"::request{ input.amount: a }),
context.input.amount + prior > 5000)
};
マクロは lowering 時に展開されるだけのテンプレートで、再帰もコールスタックもない。?p が値パラメータ、$t が衝突しない新しい束縛変数を作るシジル。自前のマクロライブラリを --macros で差し込むこともでき、ポリシー内の def が同名のライブラリ定義を上書きする。
チーム運用を考えると、時相式の生構文をアプリ開発者に書かせず、プラットフォームチームがマクロライブラリを整備して配るのが現実的だと思う。生の MFOTL 構文は正直とっつきにくい。
ここでも ::request か ::response か
冒頭で触れた並行リクエスト問題は、まさにこの集約で顕在化する。上の例で ::response を数えていたら、同時に投げられた送金は全部素通りする。制限系のポリシーは ::request を数える。これは覚えておく価値のあるルールだ。
イベントスキーマと pin ── マルチテナントの落とし穴
Dogwood にはもうひとつスキーマがある。.dwschema というイベントスキーマで、「アクションからどうイベントを導出するか」のテンプレートを書く。
デフォルトはこうなっている(抜粋)。
decision event <A>::request {
...inputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
sessionId: String,
}
event <A>::response {
...inputs(A),
...outputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
sessionId: String,
}
<A> は「任意のアクション」を表すバインダで、1つのイベントスキーマが全アクションに適用される。decision 修飾子が付いたイベント種別だけが認可判定を発生させる。デフォルトでは request のみが判定対象で、response と error は履歴専用。
注目すべきは pin だ。pin callerPrincipal: principalType(A) = principal は、そのイベント種別を使うすべての述語に対して、暗黙に「呼び出し元プリンシパルが現在のプリンシパルと一致すること」を強制する。alice の履歴が bob の判定に混ざらないようにするための仕組みだ。
ただし注意点があって、ドキュメントは「pin はポリシーをあたかもキーで分割されているかのように解釈させる書き換えパスであって、イベント履歴が実際に分割保存されることを意味しない」と明記している。マルチテナントで運用するなら、authorizer インスタンスとイベントストレージそのものを分離することを検討しろ、と。
さらに厄介なのは「全イベント種別に宣言されていない pin は、エラーにならずに黙ってグローバルセマンティクスのまま」という仕様だ。片方の種別に付け忘れると、意図した分離が効かないまま動く。ここはレビュー観点として明文化しておいた方がいい。
Information Providers ── 計算が必要な事実
「認可したい事実がリクエストの中になく、計算しないと出てこない」ケースがある。正規表現マッチ、denylist 照会、分類器によるリスクスコアなど。
Dogwood はこれを Information Providers として外に出している。プロバイダは Rhai スクリプトで書かれ、lowering 時に呼び出しが context.providers.<id> への参照に書き換えられる。
@id("sell_after_approval_valid_ticker")
permit ( principal, action == Drupe::Action::"SellShares", resource )
when temporal {
formerly within 1h Drupe::Action::"ApproveSale"::response{
input.stock: context.input.stock
}
}
when guardrails {
Strings::Matches(context.input.stock, "^[A-Z]+$").matched == true
};
when guardrails { ... } は when { ... } の透過的なシンタックスシュガーで、意味論的な違いはない。プロバイダ呼び出しであることを明示するタグだと思えばいい。この例では「同じ銘柄の承認が1時間以内にある」かつ「ティッカーが大文字のみ」の両方を満たすときだけ売却を許可している。
プロバイダを使う上で押さえておくべき契約が3つある。
1. プロバイダは無条件に実行される。 ルールのスコープや条件にかかわらず、すべての判定イベントで評価される。重い処理を書くとレイテンシに直撃する。
2. 実装は純粋関数でなければならない。 副作用を持たず、null 引数に対しても防御的に振る舞う必要がある。
if type_of(text) == "()" {
return #{ length: -1 };
}
3. エラー時の挙動は未定義。 リファレンス実装はプロバイダがエラーになったらリクエストを拒否するが、これは仕様として保証されていない。
そしてセキュリティ上、最も注意すべきは net フィーチャだ。有効にするとプロバイダスクリプトから http_get で外部HTTPリクエストが飛ばせるようになるが、この関数はホストやIPの検証を一切行わない。169.254.169.254(インスタンスメタデータ)、127.0.0.1、RFC-1918 のプライベートアドレス、どこへでも繋がる。
信頼できないイベントフィールドから URL の authority 部分を組み立てるのは絶対にやってはいけない。典型的な SSRF になる。認可基盤がメタデータエンドポイントを叩けるというのは、被害の大きさを考えると相当まずい。
加えて、Rhai スクリプトはデフォルトで CPU / メモリ制限が設定されていない。無限ループするプロバイダひとつで認可エンジンが止まる。本番運用するなら max_operations / max_call_levels の設定かタイムアウトが必須になる。
MCP マニフェストからスキーマを生成する
エージェント基盤を作っている立場だと、ツール定義は MCP のマニフェストに既にある。それを手でCedarスキーマに書き写すのは無駄だし、ずれる。
Dogwood には変換コマンドが用意されている。
dogwood schema mcp --manifest tools.json -o schema.cedarschema
MCPツール1つにつき Cedar アクション1つが生成され、各ツールの inputSchema が context.input、outputSchema が context.output のレコード型になる。生成されるアクションは Drupe::Action::"ToolName" という命名で、in [Action::"CallTool"] として基底アクションの階層に入る。
JSON Schema からの型マッピングはシンプルだ。
| JSON Schema | Cedar |
|---|---|
integer |
Long |
string |
String |
boolean |
Bool |
number + format: decimal
|
decimal |
Bedrock AgentCore Policy でも、MCP 経由で公開済みのツールからアクションスキーマが自動生成される。CI で MCP マニフェストからスキーマを再生成し、既存ポリシーを dogwood validate にかけて壊れていないか検証する、というパイプラインは組みやすいはずだ。ツール定義の変更がポリシーを黙って壊すのを防げる。
本番で使うなら何を自前で用意するか
ここが実務者として一番気になるところだと思う。リファレンス実装の README には、本番の認可エンジンが扱うべきでこの実装が扱っていない項目が列挙されている。かなり正直な記述で、好感が持てる。実務目線で整理するとこうなる。
| 論点 | リファレンス実装の状態 | 本番で必要になること |
|---|---|---|
| タイムスタンプ完全性 | 与えられた値をそのまま信頼 | 信頼できる時刻ソース、または投入前の検証 |
| イベント認証 | なし | 認証済み呼び出し元IDを principal としてバインド |
| トレース永続化 | インメモリのみ、再起動で消失 | 永続化・耐障害性。機密データの保護と消去 |
| トレースのサイズ | eviction なし、上限なし | 保持期間とサイズの制御 |
| マルチテナント分離 | 1インスタンス1履歴 | authorizer とストレージの分離 |
| 監査ログ | 判定を返すのみ |
is_authorized() 周辺でのロギング |
| Rhai サンドボックス | CPU/メモリ制限なし |
max_operations 等の設定、タイムアウト |
| エラーメッセージ | ポリシー内容・型情報が露出 | マルチテナント環境ではサニタイズ |
| ポリシー検証 | 任意 |
Validator::validate() を必ず実行 |
特に重い順に3つ挙げるなら、イベント履歴の永続化、マルチテナント分離、アクション名の一貫性だと思う。
3つ目は README で警告されている静かな失敗モードで、イベントはポリシーと同じ完全修飾形式("{ServiceName}::Action::Transfer" であって単なる "Transfer" ではない)で記録されなければならない。ずれると時相述語が黙ってマッチしなくなり、しかも Cedar 側は普通に許可を出す。つまり時相ガードだけが外れた状態で通る。これが本番で起きたら気づくのは相当遅れる。イベント投入層でアクション名を正規化し、テストで固定するべきところだ。
要するに、Dogwood が提供するのは言語とセマンティクスであって、信頼できるイベントログは自分で作る必要がある。ここを軽く見ると成立しない。
Bedrock AgentCore Policy との関係
自前で全部を作りたくないなら、マネージド側の選択肢がある。Amazon Bedrock AgentCore Policy が Dogwood をサポートしていて、AgentCore Gateway レイヤーでポリシーを強制する。
Gateway で止めることの意味は大きい。エージェントのコードの外側で判定するので、プロンプトインジェクションでもモデルの不具合でも迂回できない。エージェント側に if not state.approved を書いていた頃と比べると、信頼境界の位置が根本的に違う。判定は決定論的で、デフォルト拒否、フルコンテキスト付きでログに残る。
AgentCore 側は Dogwood による時相ポリシーに加えて、OAuth / IAM の既存IDを軸にしたレート制限も持っている。リクエスト数、モデルごとのトークン処理量、接続時間を、秒単位・分単位のウィンドウで制限できる。これはエージェントのコスト暴走対策としてわかりやすい。
住み分けとしては、「使いすぎ」の抑制は AgentCore のレート制限、「順番」「前提」「累計」の強制は Dogwood ポリシーという整理になる。
導入判断 ── 使うべき場面、使わなくていい場面
一通り触ってみた上での所感を書く。
向いている場面:
- 承認ゲートが要件にある(送金、本番デプロイ、データ削除)
- セッション単位の予算・数量の上限を守らせたい
- 「機微データを読んだ後は外部送信ツールを使わせない」といった順序制約がある
- MCP ツールを多数持つエージェントを複数チームで運用していて、ガードをアプリコードから追い出したい
- 監査で「なぜ許可/拒否したか」の説明責任がある
現時点では見送っていい場面:
- ツールが数個で、順序制約が1〜2個。素直にアプリ側の状態チェックで足りる
- リクエスト単位の認可しか必要ない。それは Cedar / Verified Permissions で完結する
- 履歴を永続化・分離する基盤を作る余力がない。中途半端な導入は「守っているつもり」を作るだけで危険
もうひとつ。Dogwood はまだコントリビューションを受け付けていない。AWS は「言語が安定したら開放する」としている。仕様が動く可能性は織り込んでおくべきで、ポリシーを大量に書き溜める前に、マクロ層で構文を抽象化しておくと将来の移行コストを下げられる。
将来の拡張として、カレンダー基準の絶対時刻演算子(日次クォータなど)、liveness 検証(必要なアクションが必ず起きることの保証)、マルチエージェントのオーケストレーションポリシーが予告されている。現状は過去方向の演算子しかないので、「30分以内に必ずレポートを提出すること」のような未来方向の制約は書けない。
まとめ
Dogwood を一言で表すなら、認可の判定単位を「1リクエスト」から「イベント列」へ引き上げた言語だ。
- Cedar の上位互換なので、既存ポリシーの書き換えは不要
- 時相演算子は
formerly/previous/sinceの3つ、集約はcount/sumの2つ。マクロで実用的な形に包む - 時相条件は Cedar の
contextスロットに降ろされる。評価器が分離しているのでバックエンドを差し替えられる - その代償として、Cedar の自動推論による形式検証は時相部分に効かない
- 制限系ポリシーは
::responseではなく::requestを数える - 集約の本体を
formerlyで包み忘れると、黙って常に拒否になる。replayで必ず検算する - リファレンス実装は本番用ではない。信頼できるイベントログの構築が実質的な導入コスト
エージェントに実世界の操作をさせる以上、「1回1回は正しいが並びが間違っている」問題からは逃げられない。それを実装ではなくポリシーとして書けるようになったのは、素直に前進だと思う。
まずは dogwood replay で手元のワークフローを再生してみるのがいい。自分たちのエージェントが本当は何をやっているのか、イベント列として眺めるだけでも発見がある。私の場合、承認をスキップしていたのは3回に1回程度だと思っていたが、実際にはもっと多かった。
参考リンク
- Introducing Dogwood: runtime verification for AI agents | AWS Open Source Blog
- dogwood-policy/dogwood (GitHub)
- The Dogwood Guide
- Control agent behaviors and cost beyond a single action: new capabilities in Amazon Bedrock AgentCore | AWS ML Blog
- AWS Open-Sources Dogwood, Extending Cedar to Govern Sequences of Agent Tool Calls | InfoQ
- Your AI agent's next tool call may be valid but wrong. AWS's Dogwood promises to fix that. | The New Stack
- Cedar Policy Language