Skip to content
Ayhan Sipahi Ayhan Sipahi

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 into execute_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

DimensionCedar (point in time)Dogwood (over time)
PurposeAccess-control decisions on a single requestDecisions that depend on patterns of events over time
What a policy seesOnly the current requestThe current request plus an event history
Key constructwhen { ... } over principal, action, resource, contextAdds when temporal { ... }; also usable inline as a temporal { ... } expression
OperatorsCedar’s expression languagesince, formerly, once, plus aggregations: count_within, count_distinct_within, sum_within, bind
Extra factsContext attributes supplied by the callerInformation providers: computed guardrail facts (Rhai) injected at evaluation time
Theoretical basisAttribute-based authorization, built for automated reasoningMetric First-Order Temporal Logic, from runtime verification
EvaluationDirect, stateless Cedar evaluationLowers to Cedar; each temporal clause becomes a context.* slot filled from event history
Cost profileIdentical requests yield identical decisions, whatever the order or stateStateful: 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 30s or within 24h.
  • An action name and a type, either request or response.
  • The associated data: input arguments reachable as context.input.* and output values as context.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 operator forms below are the attested surface syntax. Prefer them; the lower-level forms that appear in some enforcement examples are more verbose and are better described in prose.

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.

Dogwood policy (.dw)

dogwood lower

Cedar policy + context.* slots

Runtime fills slots from event history

Cedar evaluator: ALLOW or DENY

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.

ToolTrajectory StoreGateway PolicyAgentToolTrajectory StoreGateway PolicyAgentalt[Denied][Allowed]Agent never reads or writes the trajectory storeTool call + session idFetch prior events for sessionOrdered trajectoryEvaluate temporal policy (Dogwood)DENY (logged)Forward requestResponseResult

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 forbid overrides a permit.
  • 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 background on MCP, the external authorization systems post covers how a dedicated policy layer fits into a broader architecture.

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

Related posts