> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brane.membranelabs.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing Policies

> How to write before_capability and after_capability policies in brane-core.

Policies are functions. They take a [PolicyContext](/concepts/policy-context) and return a [Decision](/concepts/decision).

## Policy Function Shape

Every policy is a plain Python function:

```python theme={null}
from brane import Decision, PolicyContext

def my_policy(ctx: PolicyContext) -> Decision:
    return Decision(type="allow")
```

No inheritance, no framework, no configuration file. Just a function that receives context and returns a decision.

## Registering Policies

Use `@runtime.before_capability` or `@runtime.after_capability` to register:

```python theme={null}
@runtime.before_capability("execute_sql")
def read_only_sql(ctx):
    if not ctx.arg("query").lower().strip().startswith("select"):
        return Decision(type="deny", reason="Only SELECT queries are allowed")
    return Decision(type="allow")
```

The decorator registers the function as a `Policy` targeting `"execute_sql"` at the `before_capability` stage. The original function is returned unchanged, so you can call it directly in tests.

## Policy Targets

Exact match:

```python theme={null}
@runtime.before_capability("refund_customer")
def refund_policy(ctx):
    ...
```

Wildcard:

```python theme={null}
@runtime.before_capability("*")
def global_policy(ctx):
    if ctx.is_prod and ctx.is_high_risk:
        return Decision(type="deny", reason="High-risk actions blocked in prod")
    return Decision(type="allow")
```

Wildcard policies run alongside exact-match policies. Both are evaluated; deny wins.

## Policy Metadata

Provide a name and version to make decisions traceable:

```python theme={null}
@runtime.before_capability(
    "refund_customer",
    name="refund_amount_limit",
    version="1.0",
    description="Blocks refunds above the tenant limit",
    priority=10,
)
def refund_limit_policy(ctx):
    amount = ctx.arg("amount_usd", 0)
    tenant_limit = get_tenant_limit(ctx.tenant_id)
    if amount > tenant_limit:
        return Decision(type="deny", reason=f"Amount exceeds limit of ${tenant_limit}")
    return Decision(type="allow")
```

The name and version are annotated onto the Decision and appear in `CapabilityDeniedError.policy_name`.

## Policy Metadata Fields

* `name`: human-readable identifier. Defaults to the function name.
* `version`: policy version string.
* `description`: what this policy enforces.
* `priority`: higher priority runs first. Default is `0`.
* `enabled`: set `False` to disable without removing. Default is `True`.
* `source`: where this policy came from.

## Multiple Policies On The Same Capability

Multiple policies can target the same capability. They all run. Deny wins: if any policy returns deny, the action is denied regardless of other policies returning allow.

```python theme={null}
@runtime.before_capability("refund_customer", priority=10)
def refund_amount_limit(ctx):
    if ctx.arg("amount_usd", 0) > 100:
        return Decision(type="deny", reason="Amount too high")
    return Decision(type="allow")

@runtime.before_capability("refund_customer", priority=5)
def refund_scope_check(ctx):
    if not ctx.agent_has_scope("refunds:create"):
        return Decision(type="deny", reason="Missing refunds:create scope")
    return Decision(type="allow")
```

## Testing Policies

Because policies are plain functions, you can test them by constructing a [PolicyContext](/concepts/policy-context) directly:

```python theme={null}
from brane import AgentAction, Capability, PolicyContext
from my_policies import refund_amount_limit

def test_refund_limit_deny():
    cap = Capability(name="refund_customer", type="tool", risk="high")
    action = AgentAction(
        action_type="tool_call",
        capability=cap,
        input={"customer_id": "cust_1", "amount_usd": 250.0},
    )
    ctx = PolicyContext(action=action, args=action.input)
    decision = refund_amount_limit(ctx)
    assert decision.denied
    assert "Amount too high" in decision.reason
```

## Where Policy Lives

Policy should be a separate control layer: not scattered across tool implementations, not hidden in prompts, not inside ad hoc if-statements in the agent logic.

Start with a single policy file per agent. Once policies are explicit, they can be tested, audited, reused, and eventually moved to a central policy system or cloud bundle.

<Warning>
  Write explicit deny rules for high-risk capabilities rather than relying on allow-by-default.
</Warning>
