# Dogwood adds temporal conditions to Cedar, then lowers them back into context slots

> A governance language for AI agent actions that keeps Cedar's permit and forbid and adds conditions that read backwards over an event history. The repository ships a reference interpreter, which is the honest reason to read it and the honest reason not to deploy it.

**dogwood-policy/dogwood** — Reference parser and interpreter for the Dogwood policy language

- Repository: https://github.com/dogwood-policy/dogwood
- Website: https://dogwood-policy.github.io/dogwood/
- Stars: 419 · Forks: 29
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/dogwood-policy-dogwood

## Temporal blocks sit beside Cedar's when and unless clauses

Dogwood is a Cedar derivative for governing what an AI agent is allowed to do, and the difference from plain Cedar is a set of conditions that read backwards over recent events. `permit` and `forbid` with `when` and `unless` are kept as they are. What gets added is `since`, `formerly`, `once`, and aggregations over an event history, so a rule can ask what happened in the last hour instead of only what holds right now.

The worked example in the README is a read after login policy:

```text
@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 }
};
```

Drupe is the schema namespace used by that example. The read passes only when a login by the same user sits inside a one hour window, which is a rule Cedar has no way to express on its own.

## Lowering swaps each temporal block for a numbered context slot

Lowering is where Dogwood gives up ownership. A policy that mixes a temporal block with a Cedar condition cannot be expressed in Cedar, so the compiler replaces the temporal block with a slot in the `context` object and hands the rest to a standard Cedar engine:

```text
@id("read_after_login")
permit(principal, action == Drupe::Action::"Read", resource) when { context.policy_0__temporal_0 };
```

The generated field name encodes its position, so `policy_0__temporal_0` is the first temporal condition of the first policy. At evaluation time Dogwood computes the truth value from the event history and writes it into that slot. The consequence matters more than the syntax: whatever engine consumes the lowered policies is looking at ordinary Cedar, and Dogwood keeps only the part Cedar cannot express.

## validate, lower and replay cover the whole command line surface

The CLI has three verbs, and the README's quick start is the whole of it.

```bash
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
```

`validate` checks a policy against its schemas and prints `OK: validation passed with no errors or warnings.` on a clean run. `lower` writes the Cedar output, and `--emit both` is what asks for the original form and the lowered form together. `replay` needs a trace file, and that is where the temporal conditions actually get exercised. Starter action schemas and event schemas live in `dogwood-language/configuration/`, and the policies under `dogwood-docs/examples/` ship with the traces and expected output they need.

## A replay trace shows why a rule fired, not only that it fired

Replay is the part of the tool that makes a temporal decision visible. The `read_after_login` example carries three events: a login at t=0, a read at t=10 seconds, and another read at t=2 hours.

```text
@0 (time point 0): DENY
@10 (time point 1): ALLOW  [rules: 0]
@7200 (time point 2): DENY
```

The first line is the login itself, where no read was requested, so there is nothing to permit. Ten seconds later the read passes because the login is inside the window. Two hours on the same rule denies, because the login has aged out of it. Without a trace file the temporal condition is only syntax, and this is the part of Dogwood that cannot be checked by reading a policy.

## Information providers are Rhai scripts injected as guardrail fields

Computed facts enter through information providers. A provider is a Rhai script, injected at evaluation time, and its results become context fields that guardrail expressions read. That is the extension point for a value the policy has to compute rather than look up.

The README does not enumerate the providers that ship in the box, so the example bundles under `dogwood-docs/examples/` are where the available set is discoverable. Providers become `context.*` fields on the Cedar side as well, which means they travel the same lowering path as the temporal slots and are filled the same way.

Temporal conditions and providers are therefore two answers to the same problem. One asks about time, the other asks about anything the host can compute, and both end up as context fields in the policy that reaches the engine.

## Both engines are swappable, but the default trace is neither capped nor durable

The two halves of the engine can be replaced, and they can be replaced independently. The policy backend is either a local Cedar engine or a remote policy store. The temporal backend is either in-memory or database-backed. Swapping one leaves the other alone, which suits a team that wants evaluation to stay local while policy distribution moves somewhere central, or the reverse.

What cannot be swapped is the default trace. The built-in `InMemoryTemporalEngine` has no eviction and no size cap, so sustained event volume grows it without limit, and because the reference interpreter is purely in memory the trace is gone after a crash or restart. A deployment that depends on windows an hour wide has to bring its own temporal engine before it has a durability story. The README's section on production concerns stops partway through the trace management item, so whatever guidance follows it is not written down here.

## Five ways a Dogwood deployment can fail without printing an error

The longest section of the README covers ways a deployment can be quietly wrong, and none of them raise anything.

Timestamps are accepted as given and never validated, so a production system has to take them from a trusted time source or check them before ingestion. Events carry no authentication at all, which leaves the authenticated caller identity to be bound to the principal before an event reaches the engine. Fields needed by both a temporal predicate and a Cedar condition must be supplied twice, once to the `logged` bag through `.field()` and once to the `request_context` bag through `.request_context()`, and supplying only one silently weakens whichever check missed the field.

The sharpest edge is action naming. Events must use the same qualified format the policies do, `"{ServiceName}::Action::Transfer"` rather than `"Transfer"`. Get that wrong and a temporal predicate quietly stops matching while the Cedar side still authorizes the action, so the rule appears to be working and is enforcing nothing.

## One crate for the frontend, and doc examples that break the build

The workspace holds three crates. `dogwood-language` is the frontend, and Cargo.toml explains why it is one crate instead of a public API paired with a private implementation: a published API would force its implementation dependency to be published too, and the implementation's `pub` items would then be reachable directly. A single crate with crate-private modules keeps that enforced by the compiler. The workspace is version 1.0.0, edition 2024, and publishes to crates.io.

`dogwood-cli` is a thin shell that drives the frontend's curated public surface, schema and policy in, verdicts and diagnostics out. `dogwood-docs` is the documentation crate, and its test harness runs every example bundle through the `dogwood` binary, so an example that stops compiling is a build failure.

The same repository ships agent skills that let assistants author Dogwood policies from natural language requirements, with setup in `AGENTS-README.md` covering Claude Code, the Codex CLI, Cursor, Copilot and others. A top level `book.toml` and a `generate-examples.sh` script sit alongside them.

## Conclusion

Dogwood is worth reading if you are designing authorization for agent actions and want to watch a temporal rule decide a real request rather than infer its behaviour from the grammar. The reference interpreter is candid about what it is, so it is not the place to run production traffic: timestamps arrive unvalidated, events arrive unauthenticated, the default temporal engine grows without a cap and loses its trace on restart. Start by replaying the read_after_login example and reading the DENY, ALLOW, DENY verdicts against a window of your own, then confirm that any events you generate use the qualified ServiceName::Action::Name form, since a mismatch there is the failure that looks like success.

## FAQ

### What does Dogwood add to Cedar?

Dogwood keeps Cedar's permit and forbid with when and unless, and adds temporal conditions built from since, formerly, once and aggregations over an event history. Policies lower back to standard Cedar, with each temporal block replaced by a context slot filled at runtime.

### Can the Dogwood reference interpreter act as a production authorization engine?

No, and the README says so directly. It accepts unvalidated timestamps, offers no authentication on events, and its InMemoryTemporalEngine has no eviction or size cap, so the trace is also lost after a crash or restart.

### How do I check what a temporal Dogwood policy actually decides?

Use replay with a trace file. The read_after_login example runs three events and prints DENY at the login, ALLOW for the read ten seconds later, and DENY once the login has aged past the one hour window.

### How do I add the Dogwood library to a Rust project?

Add the git dependency to Cargo.toml under the dependencies table, pointing at the dogwood-policy/dogwood repository. The library README covers the API and the API and Workflow guide covers detailed usage.

## Sources

- [dogwood-policy/dogwood on GitHub](https://github.com/dogwood-policy/dogwood)
- [Issues](https://github.com/dogwood-policy/dogwood/issues)
- [License: Apache-2.0](https://github.com/dogwood-policy/dogwood/blob/main/LICENSE)
- [Project website](https://dogwood-policy.github.io/dogwood/)
- [README](https://github.com/dogwood-policy/dogwood/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dogwood-policy-dogwood
