Dogwood: Temporal Authorization for AI Agents
How AWS Dogwood adds temporal conditions to Cedar policies, lowers them back to plain Cedar, and enforces agent guardrails at the Amazon Bedrock AgentCore gateway.
Abstract
Point-in-time authorization answers one question: is this single request allowed? For an autonomous agent that calls tools in sequence, that is necessary but not sufficient. A tool call can be valid on its own and still wrong given what the agent did just before. AWS open-sourced Dogwood on 2026-08-06 under Apache 2.0 to close that gap. Dogwood is a policy language that adds temporal conditions on top of Cedar, then compiles (“lowers”) each temporal clause back to ordinary Cedar. A working knowledge of Cedar is assumed here. The focus is what Dogwood adds on top: the temporal operators, how lowering works, how Amazon Bedrock AgentCore enforces temporal policies at the gateway, and where the honest limits sit. For a refresher on Cedar itself, see the Cedar vs Rego vs OpenFGA comparison.
Why Point-in-Time Authorization Fails for Agents
Cedar decides one request in isolation. It looks at the principal, the action, the resource, and a context supplied by the caller, then returns allow or deny. That statelessness is deliberate. It is exactly what makes Cedar analyzable: identical requests yield identical decisions, and a solver can reason about what a policy set can and cannot permit.
An autonomous, tool-calling agent breaks the assumption that each request stands alone. The agent produces a trajectory: a sequence of tool calls, each shaped by the outputs of earlier ones. A single call can pass every point-in-time check and still be harmful in the context of what came before. The New Stack framed the gap well: the problem is the distance between what an agent is allowed to do and what it should do given the runtime history. Prompt engineering and static access-control lists do not close that distance. A deterministic, out-of-band policy layer can.
The Failure Modes
AWS calls out several patterns that point-in-time authorization cannot catch:
- Untrusted-data contamination. A call is safe in isolation but harmful right after the agent reads from an untrusted source. This is the classic prompt-injection chain. The request looks fine; the sequence is the problem.
- Cross-call substitution. The agent runs a legitimate
get_client_profile, then passes a fabricated account id intoexecute_trade. Each call is well-formed. The id was never returned by the lookup. - Runaway aggregation. The agent fires dozens of transfers. Every one sits under the per-request limit, but the cumulative total exceeds the budget. Point-in-time authorization has no memory to catch this.
- Contradiction. The agent approves a claim, then denies the same claim seconds later; or it buys a security and sells it at a loss in the same trajectory. Individually legal, jointly anomalous.
- Stale input. The agent acts on a market price that was fetched too long ago.
Cedar cannot express any of these, because none can be decided from the current request alone. So the goal is not to fix Cedar. The goal is to add a temporal layer that still lowers to Cedar. That is Dogwood.
What Dogwood Adds
Dogwood evaluates a policy against the history of prior events in a session: their order, the time windows between them, how often they occurred, and running sums over their data. The central design move keeps this practical. Dogwood is not a new engine. Each temporal clause compiles back to ordinary Cedar, where the temporal condition becomes a context.* slot that a runtime fills from the event history before Cedar evaluates. The consequence matters: any syntactically valid Cedar policy is already a valid Dogwood policy. Existing Cedar rules keep working unchanged, so there is no migration.
The theoretical basis is Metric First-Order Temporal Logic (MFOTL), drawn from runtime verification. That is the same math used to check whether a running system stays within a temporal specification over time. Dogwood’s user-facing operators are defined in terms of a small core subset of MFOTL operators.
Cedar vs Dogwood
| Dimension | Cedar (point in time) | Dogwood (over time) |
|---|---|---|
| Purpose | Access-control decisions on a single request | Decisions that depend on patterns of events over time |
| What a policy sees | Only the current request | The current request plus an event history |
| Key construct | when { ... } over principal, action, resource, context | Adds when temporal { ... }; also usable inline as a temporal { ... } expression |
| Operators | Cedar’s expression language | since, formerly, once, plus aggregations: count_within, count_distinct_within, sum_within, bind |
| Extra facts | Context attributes supplied by the caller | Information providers: computed guardrail facts (Rhai) injected at evaluation time |
| Theoretical basis | Attribute-based authorization, built for automated reasoning | Metric First-Order Temporal Logic, from runtime verification |
| Evaluation | Direct, stateless Cedar evaluation | Lowers to Cedar; each temporal clause becomes a context.* slot filled from event history |
| Cost profile | Identical requests yield identical decisions, whatever the order or state | Stateful: retains and searches events, so evaluation time can depend on history length |
Both are Apache 2.0. The bottom row is the trade-off in one line: Dogwood buys memory and pays for it in state.
The Event Model
Dogwood evaluates against an event trace, sometimes called the event log. Each event records four things:
- A timestamp, expressed relative to prior events. This is what enables windows such as
within 30sorwithin 24h. - An action name and a type, either
requestorresponse. - The associated data: input arguments reachable as
context.input.*and output values ascontext.output.*. - The authorization verdict for that event, allow or deny.
Policies match events by action and reach into their data through those input.* and output.* slots. A temporal clause is, in effect, a query over this trace.
The Temporal Operators
The forms below are the surface syntax from the reference documentation. AWS’s AgentCore examples sometimes spell the same conditions in a more verbose, lower-level form; the macros here are the intended day-to-day syntax.
formerly within <window> <Event>{ ... } is backward-looking existence. It asks whether a matching event occurred inside the window:
formerly within 1h AgentCore::Action::"ApproveSale"::response{
input.stock: context.input.stock,
input.shares: context.input.shares,
output.approved: true
}
Notice the field binding. The clause does not just check that an approval happened; it checks that the approval was for the same stock and share count as the current request, and that its output was approved: true.
count_within(<window>, <Event>{ ... }) tallies occurrences in a window:
count_within(1h, AgentCore::Action::"Transfer"::request{ input.amount: _ }) > 5
count_distinct_within(<var>, <window>, <Event>{ ... field: <var> }) counts distinct values of a bound field:
count_distinct_within(u, 1h, AgentCore::Action::"Transfer"::request{ input.user: u }) > 3
sum_within(<var>, <window>, <Event>{ ... field: <var> }) aggregates a numeric field:
sum_within(a, 1h, AgentCore::Action::"Transfer"::request{ input.amount: a }) > 5000
bind(<name>, <aggregate>, <expr using name>) captures an aggregate result, then compares against it. This is a spike guard: block a transfer larger than the sum of recent ones.
bind(prior,
sum_within(a, 1h, AgentCore::Action::"Transfer"::response{ input.amount: a }),
context.input.amount > prior)
Two more operators round out the family. since expresses that a condition has held since some event happened. once expresses that something occurred at least once in scope. The since within <window> form appears in “approval consumed exactly once” patterns, discussed below.
These operators are standard-library macros. They expand into the small MFOTL core, which is what keeps the surface language small while the underlying semantics stay precise.
Lowering to Cedar
A Dogwood policy extends Cedar’s when. You can combine an ordinary Cedar condition with a temporal one in the same policy:
permit ( principal, action == AgentCore::Action::"SellShares", resource )
when {
context.input.shares <= 100
&& temporal {
formerly within 1h AgentCore::Action::"ApproveSale"::response{
input.stock: context.input.stock,
input.shares: context.input.shares,
output.approved: true
}
}
};
The first line is plain Cedar: at most 100 shares. The temporal block adds the history requirement: a matching approval for the same stock and share count within the last hour. Both must hold. You can also attach the temporal clause directly with when temporal { ... }. The reference implementation uses a tree-themed Drupe namespace, since a dogwood produces drupe fruit:
@id("read_after_login")
permit (
principal,
action == Drupe::Action::"Read",
resource
)
when temporal {
formerly within 1h Drupe::Action::"Login"::request{ input.user: context.input.user }
};
The key mechanic is what happens next. The dogwood lower command compiles a Dogwood policy to standard Cedar. The temporal clause is replaced by a synthesized context slot. At evaluation time, a runtime searches the event history, computes whether the temporal condition holds, and fills that slot. A normal Cedar evaluator then decides the resulting plain-Cedar policy. The lowered output looks like this:
permit(principal, action == Drupe::Action::"Read", resource)
when { context.policy_0__temporal_0 };
Cedar stays the evaluation engine. Dogwood is a front-end plus a runtime that computes the temporal facts. This is why backward compatibility is free: there is nothing new for the engine to understand.
The reference toolchain exposes three commands: validate a policy against a schema, lower it to Cedar, and replay it against a recorded trace to see what it would have decided.
dogwood validate policy.dw --policy-schema schema.cedarschema
dogwood lower policy.dw --policy-schema schema.cedarschema --emit both
dogwood replay policy.dw --policy-schema schema.cedarschema --trace events.log
To embed the language in a Rust service, add the library as a Git dependency:
dogwood-language = { git = "https://github.com/dogwood-policy/dogwood.git" }
Enforcement in Amazon Bedrock AgentCore
The reference interpreter is a specification and a testbed, not a production policy decision point. The managed enforcement path is Amazon Bedrock AgentCore. Policy in AgentCore now supports Dogwood’s temporal conditions, and it runs them in the AgentCore Gateway, at the perimeter, outside the agent’s own code.
The flow is straightforward. The gateway intercepts each request that carries a session header (x-amzn-bedrock-agentcore-policy-session-id). The policy engine queries the trajectory, meaning the ordered prior actions for that session, evaluates the current request against it with Dogwood, and returns a deterministic allow or deny, then logs the full context of the decision.
The out-of-band property is the whole security argument. The agent never sees the policy logic, never touches the state store, and cannot alter the controls. You cannot prompt-inject a control the model cannot reach. That is a stronger guarantee than any instruction placed in a system prompt, because the enforcement point does not depend on the model behaving.
A few operational details shape how you design policies:
- Session scope is the session id plus the end-user identity. The maximum look-back window is 24 hours; older events auto-delete. Windows in your policies must fit inside that horizon.
- Deny by default, with Cedar semantics preserved: a
forbidoverrides apermit. - Action schemas are generated from MCP tool manifests. The policy layer governs Model Context Protocol tool calls, agent-to-agent calls, and model inference that passes through the gateway.
- Changing a policy invalidates active sessions, so a rule change does not apply retroactively to a trajectory built under the old policy.
Because schemas come from MCP manifests, the actions your policies reference line up with the tools the agent actually exposes. For the broader architecture question of where a dedicated policy layer sits, see the external authorization systems post.
Use Cases by Operator
Each pattern below maps to an operator, so the payoff is concrete rather than aspirational.
Workflow ordering requires step B only after step A. An agent that skips the load-profile step and jumps straight to rebalancing is denied, regardless of its instructions. This is formerly within with no field binding: did the prerequisite action happen in the window?
Output-to-input integrity is the direct answer to cross-call fabrication. The id passed to execute_trade must equal an id that a prior get_client_profile response actually returned. This is formerly within with a field binding that ties the current input to a past output:
permit ( principal, action == AgentCore::Action::"ExecuteTrade", resource )
when temporal {
formerly within 1h AgentCore::Action::"GetClientProfile"::response{
output.account_id: context.input.account_id
}
};
Data freshness requires a get_market_price response within, say, 30 seconds before a trade. It is formerly within 30s. A stale price no longer satisfies the clause once the window closes.
Cumulative budget and rate caps fall straight out of the aggregation macros. Total value moved in a window stays under a ceiling with sum_within. Frequency stays under N with count_within. Distinct recipients stay under N with count_distinct_within. Together they answer the question point-in-time authorization cannot: has this agent stayed under its daily budget?
A human-approval gate consumed once needs a prior approval, and each approval must authorize exactly one execution. The since within form expresses the approval, and a companion guard checks that no execution has occurred since that approval. This prevents an agent from treating one approval as a blanket grant.
A mutual-exclusion guard blocks contradictory actions in the same trajectory, such as selling at a loss a security bought earlier in the run.
Progressive trust decay revokes write actions after a period with no human interaction, while keeping reads available. Long autonomous runs then cannot accumulate unchecked risk. This is a formerly within check on a recent interaction event, used to gate the write.
Rate limiting, quota enforcement, and usage tracking (for example, how many times a token was used) all reduce to the same aggregation macros. That breadth is the “you can do a lot with a few operators” pitch, and it holds up because the operators compose.
Information Providers
Not every fact lives in the event log. A live risk score, an external allow-list, or a computed classification has to come from somewhere. Dogwood adds information providers for this: computed guardrail facts produced by Rhai scripts and injected at evaluation time as extra context fields. Where Cedar’s context is supplied by the caller, an information provider computes the fact at decision time. An optional net feature enables http_get for external lookups, with explicit warnings about constructing URLs from untrusted input. Treat this as the escape hatch for facts you cannot derive from history, and keep the scripts small and reviewable.
Trade-offs and When Plain Cedar Still Wins
Temporal power has a real cost, and the honest read matters more here than the feature list.
State is the price. Cedar’s guarantee that identical requests yield identical decisions, regardless of order or state, is gone by construction. Dogwood retains and searches events, so evaluation time can depend on history length. That is an operational property to plan for, not a footnote.
No automated reasoning yet. Temporal conditions do not currently support Cedar’s analyzer-style reasoning. You keep it for the point-in-time parts of a policy, but for the temporal parts you lose the ability to prove what the policy set can and cannot permit. If formal analyzability is the reason you chose Cedar in the first place, weigh this carefully. The Cedar comparison post covers that analyzability in depth.
The reference interpreter is explicitly not for production. The README says so, and lists the gaps plainly: no event-timestamp integrity validation, no event authentication, no CPU or memory limits on Rhai scripts (a malicious script can starve resources), in-memory traces that are lost on crash (no durability), error messages that may leak policy structure in multi-tenant setups, and no built-in audit logging. For production enforcement, the managed path is AgentCore. The open-source repository is a specification and a reference, not a drop-in policy decision point.
The project is early. Contributions are not yet being accepted; feedback is welcomed while the language stabilizes. The roadmap points at absolute-time windows (daily quotas that reset at a fixed time, since today’s windows are relative and sliding), liveness operators (future-oriented “must eventually” requirements, where today’s operators are safety and past-oriented), and multi-agent orchestration policies. Frame these as direction, not commitments.
So when does plain Cedar remain the right call? When every decision truly stands alone, when analyzability is a hard requirement across the whole policy set, or when you are not running autonomous agents at all. Reach for Dogwood specifically when correctness depends on sequence, timing, counts, or sums across a session, and when the enforcement point can hold state safely out of the agent’s reach.
Conclusion
Dogwood adds one primitive to authorization: memory of the session, expressed as temporal conditions and compiled back to Cedar so nothing about the evaluation engine changes. That primitive is what agent guardrails were missing. A stochastic actor makes a stream of individually plausible tool calls, and a deterministic layer decides them against the history, out of band, where the model cannot interfere. The trade-off is explicit and worth naming: you gain the ability to catch valid-but-wrong sequences, and you give up statelessness and, for now, analyzability of the temporal parts. If you already run Cedar, adopting Dogwood costs nothing for your existing rules and adds temporal rules only where the sequence is the risk. Start by replaying a recorded trajectory against a draft policy with dogwood replay, before you enforce anything in the gateway.
References
- Introducing Dogwood: runtime verification for AI agents - AWS Open Source blog announcing Dogwood, its design, and the lowering-to-Cedar model.
- dogwood-policy/dogwood on GitHub - Reference parser and interpreter, operator syntax, CLI, and the explicit not-for-production limitations.
- Securing AI agents with temporal policies in Amazon Bedrock AgentCore - AWS ML blog on gateway enforcement, session scope, and the 24-hour look-back window.
- Control agent behaviors and cost beyond a single action - AWS ML blog on cumulative budget and rate-control use cases in AgentCore.
- Your AI agent’s next tool call may be valid but wrong - The New Stack coverage framing the “allowed vs should” gap Dogwood targets.
- Cedar policy language documentation - Official Cedar docs: syntax,
permit/forbidsemantics, and the context model Dogwood builds on. - Cedar: A New Language for Expressive, Fast, Safe, and Analyzable Authorization - Amazon Science paper on Cedar’s design and analyzability guarantees.
- Monitoring Metric First-Order Temporal Properties - Basin et al., the runtime-verification foundation behind MFOTL.
- Model Context Protocol specification - MCP spec; tool manifests become the action schemas the policy layer governs.
- Rhai scripting language - The embedded scripting language used for Dogwood information providers.
Related posts
How Zapier MCP gives AI agents action-level whitelisting, credential isolation, and human-in-the-loop approval, a managed alternative to custom scoped proxies.
Build SaaS authorization with AWS Cognito and Verified Permissions, covering Cedar policies, multi-tenant patterns, JWT flow, and cost in TypeScript.
A vendor-neutral evaluation of AWS Verified Permissions, SpiceDB, OpenFGA, Cerbos, and OPA, with architecture patterns, cost analysis, and a decision framework.
A deep comparison of Cedar, Rego, OpenFGA DSL, and Cerbos YAML/CEL policy languages: syntax, performance, formal verification, tooling, and TypeScript integration.
A deep technical comparison of SpiceDB and Auth0 FGA (OpenFGA), two Zanzibar-inspired systems with different trade-offs in schema, consistency, deployment, and scale.