Before Capability Policies
Run before the capability executes. The most important policy stage.
Timing
A before_capability policy runs after the call is intercepted but before the underlying function executes. If the policy denies, CapabilityDeniedError is raised and the function never runs. The consequence does not occur.
# Timeline:
# agent calls capability
# → Brane intercepts
# → AgentAction created
# → PolicyContext built
# → before_capability policies run ← HERE
# → allow: function executes
# → deny: CapabilityDeniedError raised, function does NOT runRegistration
@runtime.before_capability("capability_name")
def my_policy(ctx):
# inspect ctx
return Decision(type="allow") # or denyWhat Is Available
In a before_capability policy, ctx.output is always None — the function has not run yet. Everything else on the context is available:
ctx.capability— the capability being attemptedctx.arg("name")— input argumentsctx.agent_id,ctx.principal_id,ctx.tenant_idctx.is_prod,ctx.is_high_riskctx.agent_has_scope("scope")
Common Use Cases
Block unsafe input:
@runtime.before_capability("execute_sql")
def read_only_sql(ctx):
query = ctx.arg("query", "").lower().strip()
if not query.startswith("select"):
return Decision(type="deny", reason="Only SELECT queries are allowed")
return Decision(type="allow")Block high-risk tools in production:
@runtime.before_capability("*")
def block_high_risk_in_prod(ctx):
if ctx.is_prod and ctx.is_high_risk:
return Decision(
type="deny",
reason="High-risk capability use requires an approved workflow in prod",
)
return Decision(type="allow")Enforce a financial limit:
@runtime.before_capability("refund_customer")
def refund_amount_limit(ctx):
amount = ctx.arg("amount_usd", 0)
limit = get_tenant_limit(ctx.tenant_id)
if amount > limit:
return Decision(type="deny", reason=f"Refund ${amount} exceeds limit of ${limit}")
return Decision(type="allow")Enforce tenant boundaries:
@runtime.before_capability("*")
def tenant_isolation(ctx):
if ctx.tenant_id not in ALLOWED_TENANTS:
return Decision(type="deny", reason="Tenant not authorized")
return Decision(type="allow")Check scopes:
@runtime.before_capability("refund_customer")
def require_refund_scope(ctx):
if not ctx.agent_has_scope("refunds:create"):
return Decision(type="deny", reason="Agent requires refunds:create scope")
return Decision(type="allow")Require prod environment:
@runtime.before_capability("send_production_email")
def require_prod(ctx):
if not ctx.is_prod:
return Decision(type="deny", reason="This capability is only allowed in prod")
return Decision(type="allow")Deny Behavior
When any before policy returns Decision(type="deny"):
- The runtime stops evaluating remaining before policies
- The underlying function is not called
CapabilityDeniedErroris raised with the denial reason
from brane import CapabilityDeniedError
try:
execute_sql("delete from customers")
except CapabilityDeniedError as e:
print(e.reason) # "Only SELECT queries are allowed"
print(e.policy_name) # name of the policy that denied
print(e.action_id) # ID of the action that was deniedPolicy Order
Multiple before policies on the same capability run in descending priority order (higher number runs first). Deny-wins: if any policy denies, the action is blocked regardless of other policies.
Wildcard policies ("*") run alongside exact-match policies.
Future Decision Types
Before policies currently support allow and deny. Planned additional types:
approval_required— pause until a human approvestransform_input— mutate the input before executionroute— redirect to a different capability or providersandbox— execute with restricted accesslog_only— allow but flag for review