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.
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:
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 returned unchanged, so you can call it directly in tests.
Policy Targets
Exact match:
@runtime.before_capability("refund_customer")
def refund_policy(ctx):
...
Wildcard:
@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.
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.
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.
@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
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.
Write explicit deny rules for high-risk capabilities rather than relying on allow-by-default.