Docs/Policy Runtime/Before Capability
Implemented

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 run

Registration

@runtime.before_capability("capability_name")
def my_policy(ctx):
    # inspect ctx
    return Decision(type="allow")  # or deny

What 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 attempted
  • ctx.arg("name") — input arguments
  • ctx.agent_id, ctx.principal_id, ctx.tenant_id
  • ctx.is_prod, ctx.is_high_risk
  • ctx.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"):

  1. The runtime stops evaluating remaining before policies
  2. The underlying function is not called
  3. CapabilityDeniedError is 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 denied

Policy 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 approves
  • transform_input — mutate the input before execution
  • route — redirect to a different capability or provider
  • sandbox — execute with restricted access
  • log_only — allow but flag for review