Strands Agents Integration

Strands Agents Integration#

Dogwood policies should usually guard Strands tool calls through interventions. Plugins and direct hooks are also available. All three surfaces support Strands lifecycle events; before-tool-call authorization remains the default so existing CallTool schemas do not accidentally deny invocation or model events.

The implementation follows the Strands extension model:

  • DogwoodIntervention subclasses Strands’ intervention handler and returns typed actions.

  • DogwoodPlugin subclasses Strands’ plugin base class and uses @hook so Strands can auto-register lifecycle handlers from their event types.

  • lifecycle_hook exposes lower-level lifecycle hooks when you need direct event mutation.

Supported lifecycle names:

  • before_invocation

  • after_invocation

  • message_added

  • before_model_call

  • after_model_call

  • before_tool_call

  • after_tool_call

Before-events can block execution when Dogwood denies. After-events are observational by default because the work has already happened; use them to record outcomes into Dogwood temporal history or guide the next model step.

Enforcement modes#

Dogwood Strands integrations use the SDK-level enforcement modes provided by dogwood.PolicyEnforcer, following the common gateway rollout pattern:

  • mode="enforce" evaluates the Dogwood policy and enforces denials. This is the default.

  • mode="log_only" evaluates the Dogwood policy and records what would have happened, but proceeds without blocking the operation.

Use log-only mode when introducing a new policy or validating its effect before turning on enforcement:

dogwood_policy = DogwoodIntervention(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
    mode="log_only",
)

After every evaluated event, the integration records audit fields on the event:

  • event.dogwood_decision is the raw Dogwood decision, such as "Allow" or "Deny".

  • event.dogwood_enforcement_mode is "enforce" or "log_only".

  • event.dogwood_would_have_denied is True only when log-only mode saw a denied decision.

Intervention handler#

Use DogwoodIntervention when you want Strands’ typed control flow. It returns Proceed for allowed tool calls, Deny for hard denials, and can return Confirm for human step-up approval.

from strands import Agent
from dogwood.integrations.strands import DogwoodIntervention

dogwood_policy = DogwoodIntervention(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
)

agent = Agent(
    tools=[search_tool, write_file],
    interventions=[dogwood_policy],
)

To evaluate more lifecycle stages, pass lifecycle_events:

dogwood_policy = DogwoodIntervention(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
    lifecycle_events=(
        "before_invocation",
        "before_tool_call",
        "after_tool_call",
        "before_model_call",
        "after_model_call",
    ),
)

Use lifecycle_events="all" to evaluate every supported lifecycle event.

Structural checks that should happen before Dogwood policy evaluation belong in agent code as a separate intervention before DogwoodIntervention. This is where you can normalize model-generated tool input with transform(...) or give the model corrective feedback with guide(...).

from strands.interventions import InterventionHandler
from dogwood.integrations.strands import DogwoodIntervention, guide, proceed

class ToolInputPrecheck(InterventionHandler):
    name = "tool-input-precheck"

    def before_tool_call(self, event, **kwargs):
        tool_input = event.tool_use["input"]
        if event.tool_use["name"] == "send_email" and not tool_input.get("subject"):
            return guide("All emails must include a subject line.")
        return proceed()

dogwood_policy = DogwoodIntervention(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
)

agent = Agent(
    tools=[send_email],
    interventions=[ToolInputPrecheck(), dogwood_policy],
)

For step-up approval, pass confirm_when. It can be True, a prompt string, or a function that returns False, True, or a custom prompt.

high_risk_policy = DogwoodIntervention(
    policy_source=item_risk_policy,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
    confirm_when=lambda event: (
        "Approve this high-risk item?"
        if event.tool_use["name"] == "add_to_cart"
        else False
    ),
)

Plugin#

Use DogwoodPlugin when you want Strands to discover and register decorated plugin hooks automatically. The plugin exposes hook methods for invocation, message, model, and tool lifecycle events.

from strands import Agent
from dogwood.integrations.strands import DogwoodPlugin

agent = Agent(
    tools=[search_tool, write_file],
    plugins=[
        DogwoodPlugin(
            policy_source=policy_source,
            policy_schema_source=cedar_schema_source,
            event_schema_source=event_schema_source,
            lifecycle_events="all",
        )
    ],
)

Direct hook#

Use before_tool_call_hook if you need the lower-level hook object directly for tool authorization. Denied calls set event.cancel_tool.

from strands import Agent
from dogwood.integrations.strands import before_tool_call_hook

agent = Agent(
    tools=[search_tool, write_file],
    hooks=[
        before_tool_call_hook(
            policy_source=policy_source,
            policy_schema_source=cedar_schema_source,
            event_schema_source=event_schema_source,
        )
    ],
)

The default hook maps a Strands tool call into Dogwood input like this:

{
    "tool": event.tool_use["name"],
    "input": event.tool_use["input"],
    "toolUseId": event.tool_use.get("toolUseId", ""),
}

The default request action is Agent::Action::CallTool. The Dogwood policy source still refers to the Cedar action as Agent::Action::"CallTool". The principal and resource can be supplied through Strands invocation_state:

agent(
    "research Dogwood policy examples",
    principal='Agent::OAuthUser::"alice"',
    resource='Agent::Gateway::"agent"',
)

For framework-specific semantics, pass custom principal, resource, or input_mapper callbacks when constructing the hook.

Prefer one Cedar action per agent tool for production schemas. action may be a fixed string or a callback that maps the Strands event to the concrete Dogwood action:

TOOL_ACTIONS = {
    "add_to_cart": "Agent::Action::AddToCart",
    "checkout_cart": "Agent::Action::CheckoutCart",
}

def shopping_action(event):
    return TOOL_ACTIONS[event.tool_use["name"]]

dogwood_policy = DogwoodIntervention(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
    action=shopping_action,
)

Use lifecycle_hook for direct access to all lifecycle stages:

from dogwood.integrations.strands import lifecycle_hook

dogwood_lifecycle = lifecycle_hook(
    policy_source=policy_source,
    policy_schema_source=cedar_schema_source,
    event_schema_source=event_schema_source,
    lifecycle_events="all",
)

dogwood_lifecycle.handle("before_invocation", event)
dogwood_lifecycle.handle("after_tool_call", event)

Example#

See Strands Shopping Agent for the runnable shopping-agent example. It uses framework-neutral policies from examples/shopping_agent_policies and Strands-specific wiring from examples/strands_shopping_agent.