Getting Started With dogwood-py#

This page walks through a first Dogwood authorization with dogwood-py: a Cedar action schema, a Dogwood event schema, a policy, and Python code that decides a request. It starts with a single-request policy and then adds a decision that depends on history.

The Shape Of The Workflow#

Every schema-backed Dogwood workflow follows the same pipeline:

  • Build the two schema halves: a ServiceSchema for Dogwood service inputs such as the event-schema DSL, and a PolicySchema for your Cedar action schema.

  • Parse and lower policy source into a LoweredPolicySet.

  • Validate the lowered policy set.

  • Build an authorizer and feed it events or request-like tool calls.

The native Python binding also exposes a convenience NativeAuthorizer. It parses and lowers once, then handles repeated authorization calls.

Note

If you use Codex, Claude Code, or another agentic coding assistant, you can copy the Dogwood authoring skills from the Dogwood source repository and use them while building schemas and policies.

The source skills live under https://github.com/dogwood-policy/dogwood/tree/main/.claude/skills. Copy these skill folders into the skills directory used by your assistant, preserving each folder name and its SKILL.md contents:

  • dogwood

  • authoring-action-schema

  • authoring-service-schema

  • autoformalize-policies

With Codex, that usually means copying them into $CODEX_HOME/skills or ~/.codex/skills:

git clone https://github.com/dogwood-policy/dogwood.git /tmp/dogwood
mkdir -p ~/.codex/skills
cp -R /tmp/dogwood/.claude/skills/dogwood ~/.codex/skills/
cp -R /tmp/dogwood/.claude/skills/authoring-action-schema ~/.codex/skills/
cp -R /tmp/dogwood/.claude/skills/authoring-service-schema ~/.codex/skills/
cp -R /tmp/dogwood/.claude/skills/autoformalize-policies ~/.codex/skills/

Then invoke the skills in this order: $authoring-action-schema for the Cedar .cedarschema action model, $authoring-service-schema when you need a custom Dogwood .dwschema event model or providers, and $autoformalize-policies to turn prose requirements into validated .dw policies. Start with $dogwood when you are unsure which step applies.

Step 1 - Install#

Install from PyPI:

pip install dogwood-py

For local development from this repository:

make develop

The package installs as dogwood:

from dogwood import native

assert native.available()

Step 2 - A Cedar Action Schema#

The action schema declares the entity types and actions in your application. It is standard Cedar .cedarschema text. This minimal schema has Login and Read actions. Each action input lives under context.input.

namespace Agent {
  type LoginInput = { user: String };
  type LoginOutput = { success: Bool };
  type ReadInput = { user: String };

  entity Gateway;
  entity OAuthUser;

  action "Login" appliesTo {
    principal: [OAuthUser],
    resource: [Gateway],
    context: { input: LoginInput, output?: LoginOutput }
  };

  action "Read" appliesTo {
    principal: [OAuthUser],
    resource: [Gateway],
    context: { input: ReadInput }
  };
}

Step 3 - A Dogwood Event Schema#

Dogwood has two schema layers:

  • The policy/action schema is the Cedar .cedarschema file. It defines entities, actions, and request context types such as context.input.amount.

  • The event schema tells Dogwood how actions become historical events: which event kinds exist, which fields are recorded in temporal history, and which event kinds produce authorization decisions.

The default event schema is usable for many cases, but an explicit .dwschema makes the event model visible and portable.

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,
}

The decision prefix means request events produce authorization decisions. The callerPrincipal pin keeps temporal history local to the requesting principal.

Under Dogwood’s default event schema, request events are decision points, and the event history records request input fields plus reserved fields like callerPrincipal, callerResource, and requestId. That is why a temporal policy can ask about past events such as:

Agent::Action::"Transfer"::request{ input.user: context.input.user }

In other words, the Cedar schema says what a Transfer request looks like; the event schema says that Transfer::request is both authorizable and stored in history for later temporal checks.

To supply an explicit event schema through the high-level API, pass ServiceSchema(event_schema=...):

from pathlib import Path

service = ServiceSchema(event_schema=Path("event.dwschema").read_text())
policies = LoweredPolicySet.from_str(policy, service, policy_schema)

Step 4 - A Policy#

A policy is a permit or forbid rule. This one permits every Read request.

@id("permit_read_anyone")
permit (
    principal,
    action == Agent::Action::"Read",
    resource
);

Step 5 - Decide From Python#

Use the native authorizer for schema-backed workflows. It is persistent, so the policy is parsed and lowered once.

from dogwood import native

authorizer = native.NativeAuthorizer(
    policy_source,
    cedar_schema_source,
    event_schema_source,
)

decision = authorizer.authorize_request(
    "Agent::Action::Read",
    'Agent::OAuthUser::"alice"',
    'Agent::Gateway::"gw1"',
    {"user": "alice"},
)

assert decision == "Allow"

Use the higher-level SDK objects when you want the full parse/lower/validate shape in Python:

from dogwood import LoweredPolicySet, PolicySchema, ServiceSchema, Validator

service = ServiceSchema(event_schema=event_schema_source)
policy_schema = PolicySchema.from_cedarschema_str(cedar_schema_source)
policies = LoweredPolicySet.from_str(policy_source, service, policy_schema)

assert Validator().validate(policies).validation_passed()

Step 6 - A Decision That Depends On History#

Now change the requirement: permit Read only if the same user logged in within the last hour.

@id("read_after_login")
permit (
    principal,
    action == Agent::Action::"Read",
    resource
)
when temporal {
    formerly within 1h Agent::Action::"Login"::response{
        input.user: context.input.user
    }
};

Read the temporal clause as: there was formerly, within the last hour, a successful Login whose input.user matches this Read request.

Replay a trace with the native binding:

trace = '''
@0 scope(principal: Agent::OAuthUser::"alice", resource: Agent::Gateway::"gw1") request_context(input: { user: "alice" }) Agent::Action::"Login"::request(input: { user: "alice" }, callerPrincipal: Agent::OAuthUser::"alice", callerResource: Agent::Gateway::"gw1", requestId: "r1", sessionId: "s1")
@5 scope(principal: Agent::OAuthUser::"alice", resource: Agent::Gateway::"gw1") request_context(input: { user: "alice" }) Agent::Action::"Login"::response(input: { user: "alice" }, output: { success: true }, callerPrincipal: Agent::OAuthUser::"alice", callerResource: Agent::Gateway::"gw1", requestId: "r1", sessionId: "s1")
@10 scope(principal: Agent::OAuthUser::"alice", resource: Agent::Gateway::"gw1") request_context(input: { user: "alice" }) Agent::Action::"Read"::request(input: { user: "alice" }, callerPrincipal: Agent::OAuthUser::"alice", callerResource: Agent::Gateway::"gw1", requestId: "r2", sessionId: "s1")
@7200 scope(principal: Agent::OAuthUser::"alice", resource: Agent::Gateway::"gw1") request_context(input: { user: "alice" }) Agent::Action::"Read"::request(input: { user: "alice" }, callerPrincipal: Agent::OAuthUser::"alice", callerResource: Agent::Gateway::"gw1", requestId: "r3", sessionId: "s1")
'''

print(native.replay(
    temporal_policy_source,
    cedar_schema_source,
    trace,
    event_schema_source,
))

Output:

@0 (time point 0): false
@10 (time point 2): true
@7200 (time point 3): false

The login request at @0 is denied because the policy only permits Read. The login response at @5 is history-only. The read at @10 is allowed because the login response is within one hour. The read at @7200 is denied because the login has expired.

Run It From The CLI#

Save the policy, Cedar action schema, Dogwood event schema, and trace to files, then run:

dogwood validate policy.dw \
  --policy-schema schema.cedarschema \
  --event-schema event.dwschema

dogwood replay policy.dw \
  --policy-schema schema.cedarschema \
  --event-schema event.dwschema \
  --trace trace.log

Runnable examples are packaged under examples/. After installing dogwood-py and any optional dependencies, run:

python -m examples.api_usage

python -m examples.cli

python -m uvicorn examples.fastapi_simple.app:app --host 127.0.0.1 --port 8000

python -m examples.strands_shopping_agent.agent --user alice

Where To Go Next#