AOC-2: Blast Radius Limits
Shipped: March 2026 (Sprint Y) Severity: High — prevents disproportionate impact from individually-allowed actions
The problem
Even if each individual action an agent takes is policy-allowed, the cumulative scope of those actions may be disproportionate. An agent with write access to a filesystem should not be able to delete /home in a single call. An agent with email access should not be able to message 500 recipients without human approval.
Traditional RBAC answers "is this agent allowed to delete files?" — but not "is this delete dangerously broad?" Blast radius controls close that gap.
How it works
Blast radius evaluation runs as step 4d in the proxy engine, before the main OPA policy evaluation. This means disproportionate actions are caught early and never reach the policy engine, reducing unnecessary OPA calls.
The evaluation has two layers:
- Python pre-check (
check_blast_radius()) — fast, in-process evaluation against configurable thresholds - OPA policy (
blast_radius.rego) — Rego rules that mirror the Python logic for defense-in-depth
The five rules
| Rule | Action | Trigger |
|---|---|---|
| Shallow delete | Deny | Delete operation on a path with fewer components than min_delete_depth (default: 3) |
| Recipient limit | Escalate | Email/message action targeting more recipients than email_recipient_limit (default: 10) |
| Bulk threshold | Escalate | Any operation affecting more items than bulk_action_threshold (default: 50) |
| Config path write | Escalate | Write to a system config path (/etc, /root, ~/.ssh, ~/.aws, ~/.config) |
| Protected file pattern | Escalate | Any action on files matching protected patterns (MEMORY, SOUL, IDENTITY, .env) |
Shallow delete is a hard deny — it cannot be overridden by exceptions. All other rules trigger HITL escalation, requiring a human administrator to approve or reject the action before it proceeds.
OPA policy
The Rego policy at policies/base/blast_radius.rego implements the same five rules with configurable thresholds. Defaults are built into the policy and can be overridden by pushing per-tenant configuration via the OPA data API:
package behavry.blast_radius
import rego.v1
# Configurable thresholds — defaults apply when no per-tenant config is pushed
_email_recipient_limit := data.behavry.blast_radius_config.email_recipient_limit if {
data.behavry.blast_radius_config.email_recipient_limit
} else := 10
_min_delete_depth := data.behavry.blast_radius_config.min_delete_depth if {
data.behavry.blast_radius_config.min_delete_depth
} else := 3
# DENY: shallow delete path (structural — cannot be overridden)
decision := {
"result": "deny",
"reason": sprintf("Delete path too shallow (depth %d, minimum %d)", [_delete_path_depth, _min_delete_depth]),
"policy": "blast_radius.shallow_delete",
} if {
_is_delete_action
_delete_path_depth < _min_delete_depth
_delete_path_depth > 0
}
# ESCALATE: too many email/message recipients
decision := {
"result": "escalate",
"reason": sprintf("Too many recipients (%d, limit %d)", [input.request.resource_count, _email_recipient_limit]),
"policy": "blast_radius.recipient_limit",
} if {
input.request.resource_count > _email_recipient_limit
_is_message_action
}
The proxy engine passes resource_count into the OPA input via build_opa_input(), so both the Python pre-check and the Rego policy evaluate against the same extracted context.
Context extraction
The extract_blast_radius_context() function inspects every tool call to determine its scope:
- Recipients — extracted from
to,recipients,cc,bcc,addressesparameters - Delete path depth — computed by counting path components (e.g.,
/home/user/docs= depth 3) - Bulk count — largest list parameter among
files,items,records,ids,paths,targets,messages
The extracted resource_count is the maximum of recipient count and bulk count, ensuring the broadest scope indicator is used.
Per-tenant configuration
Thresholds are stored as blast_radius_config (JSONB) on tenant_configs and can be managed via the admin API:
Read current configuration
curl -H "Authorization: Bearer $TOKEN" \
GET /api/v1/admin/blast-radius
{
"email_recipient_limit": 10,
"min_delete_depth": 3,
"bulk_action_threshold": 50,
"config_path_prefixes": ["/etc", "/root", "~/.config", "~/.ssh", "~/.aws"],
"protected_file_patterns": ["MEMORY", "SOUL", "IDENTITY", ".env"]
}
Update thresholds
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
/api/v1/admin/blast-radius \
-d '{
"email_recipient_limit": 25,
"bulk_action_threshold": 100,
"protected_file_patterns": ["MEMORY", "SOUL", "IDENTITY", ".env", ".credentials"]
}'
Updates are applied immediately and synced to OPA. Only the fields you include are changed; omitted fields retain their current values.
Dashboard
The Policy Limits section in Settings provides a UI for viewing and editing all blast radius thresholds.
Event types
| Event | Fired when |
|---|---|
BLAST_RADIUS_ESCALATION | An action exceeds a blast radius threshold and is escalated for human review |
BASELINE_POISONING_SUSPECTED | More than 3 baseline approvals detected for the same agent within 24 hours |
Both events appear in the Alerts tab and in the SSE stream with blast_radius_count and blast_radius_threshold fields.
Exception hardening
AOC-2 also hardens the policy exception system to prevent gradual policy erosion:
Mandatory justification
Creating a manual exception now requires a justification (minimum 10 characters) and an expiration window:
curl -X POST -H "Authorization: Bearer $TOKEN" \
/api/v1/exceptions \
-d '{
"agent_id": "agent-uuid",
"tool_name": "delete_file",
"justification": "Cleaning up temp directory during scheduled maintenance",
"expires_in_hours": 4
}'
Exceptions without justification or expires_in_hours are rejected with a 422 error. The expires_in_hours field accepts values from 1 to 8760 (one year).
Exception frequency monitoring
An alert fires when more than 5 exceptions per hour are created for the same agent. This pattern suggests that an operator is using exceptions to bypass policy rather than updating the policy itself.
Baseline poisoning detection
When an administrator approves more than 3 baseline resets within 24 hours for the same agent, Behavry fires a BASELINE_POISONING_SUSPECTED event. This detects a scenario where an adversary (or a compromised admin account) repeatedly resets behavioral baselines to normalize anomalous behavior.
Architecture
Tool call arrives at proxy engine
|
v
Step 4d: extract_blast_radius_context()
|
v
check_blast_radius() — Python pre-check
|
+-- "deny" --> return blocked response immediately
+-- "escalate" --> create HITL escalation, wait for admin resolution
+-- "allow" --> continue to Step 5 (OPA policy evaluation)
The Python pre-check and the OPA policy evaluate the same thresholds (defense-in-depth). The Python check runs first for efficiency, but the OPA policy provides a second enforcement point that cannot be bypassed by modifying backend code alone.
Related
- Agentic Security Overview
- AOC-4: Trust Reset Detection — behavioral baselines that blast radius protects