Writing Policies
Policies are functions. They take a PolicyContext and return a Decision.
Policy Function Shape
Every policy is a plain Python function:
from brane import Decision, PolicyContext
def my_policy(ctx: PolicyContext) -> Decision:
# inspect ctx
# return a 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:
@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 also returned unchanged, so you can call it directly in tests.
Policy Targets
Exact match — runs only for the named capability:
@runtime.before_capability("refund_customer")
def refund_policy(ctx):
...Wildcard — runs for all capabilities:
@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:
@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— setFalseto disable without removing. Default isTrue.source— where this policy came from (file path, bundle ID, etc.).
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.
@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 directly:
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
def test_refund_limit_allow():
...
action = AgentAction(..., input={"amount_usd": 50.0})
ctx = PolicyContext(action=action, args=action.input)
decision = refund_amount_limit(ctx)
assert decision.allowedWhere 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.
return Decision(type="allow") is always valid — it documents an explicit decision to allow the action under those conditions.