After Capability Policies
Run after the capability executes. Inspect and control the output before it is returned.
Timing
An after_capability policy runs after the underlying function has completed and returned a result. The function has already executed. The policy inspects the output and decides whether to allow it through or block it.
# Timeline:
# agent calls capability
# → Brane intercepts
# → before_capability policies run → allow
# → function executes, returns output
# → after-action record created with output
# → after_capability policies run ← HERE
# → allow: output returned to caller
# → deny: CapabilityDeniedError raised (function already ran)Registration
@runtime.after_capability("capability_name")
def my_after_policy(ctx):
# ctx.output is available here
return Decision(type="allow")Accessing the Output
In an after_capability policy, ctx.outputcontains the function's return value. This is the primary difference from before policies.
@runtime.after_capability("execute_sql")
def check_result_size(ctx):
output = ctx.output
rows = output.get("rows", []) if isinstance(output, dict) else []
if len(rows) > 1000:
return Decision(type="deny", reason="Result set too large (>1000 rows)")
return Decision(type="allow")Common Use Cases
Detect possible secret leakage:
@runtime.after_capability("call_model")
def check_for_secrets(ctx):
output = str(ctx.output or "")
secret_patterns = ["SECRET_KEY", "API_KEY", "password=", "token="]
for pattern in secret_patterns:
if pattern.lower() in output.lower():
return Decision(type="deny", reason="Possible secret in model output")
return Decision(type="allow")Validate output schema:
@runtime.after_capability("get_customer_data")
def validate_customer_output(ctx):
output = ctx.output
if not isinstance(output, dict) or "customer_id" not in output:
return Decision(type="deny", reason="Unexpected output shape from get_customer_data")
return Decision(type="allow")Block large result sets:
@runtime.after_capability("search_documents")
def block_large_results(ctx):
results = ctx.output if isinstance(ctx.output, list) else []
if len(results) > 500:
return Decision(
type="deny",
reason=f"Search returned {len(results)} results, limit is 500",
)
return Decision(type="allow")Detect PII in output (placeholder for future redact decision):
import re
SSN_PATTERN = re.compile(r"d{3}-d{2}-d{4}")
@runtime.after_capability("*")
def detect_pii(ctx):
output_str = str(ctx.output or "")
if SSN_PATTERN.search(output_str):
# Today: deny. Future: return Decision(type="redact", ...)
return Decision(type="deny", reason="SSN detected in output")
return Decision(type="allow")Deny Behavior
When an after policy denies:
- The function has already executed (side effects occurred)
CapabilityDeniedErroris raised- The output is not returned to the caller
try:
result = call_model("Tell me the STRIPE_SECRET_KEY")
except CapabilityDeniedError as e:
print(e.reason) # "Possible secret in model output"
# result is never available to the callerFuture Decision Types
After policies currently support allow and deny. Planned:
redact— strip or mask fields from the output before returning it, without blockingtransform_output— mutate the output (enrich, sanitize, summarize) before returninglog_only— allow the output through but flag the action for review
These planned types address the limitation that today's after policy is binary: allow the output or block it entirely. Redaction allows the agent to receive a cleaned version without a hard failure.
Before vs. After
| Concern | Use before | Use after |
|---|---|---|
| Prevent unsafe input | ✓ | |
| Prevent side effects from occurring | ✓ | |
| Enforce identity or tenant rules | ✓ | |
| Inspect the actual output | ✓ | |
| Detect PII or secrets in output | ✓ | |
| Validate output schema | ✓ | |
| Block oversized result sets | ✓ |