Skip to main content

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:

  1. Python pre-check (check_blast_radius()) — fast, in-process evaluation against configurable thresholds
  2. OPA policy (blast_radius.rego) — Rego rules that mirror the Python logic for defense-in-depth

The five rules

RuleActionTrigger
Shallow deleteDenyDelete operation on a path with fewer components than min_delete_depth (default: 3)
Recipient limitEscalateEmail/message action targeting more recipients than email_recipient_limit (default: 10)
Bulk thresholdEscalateAny operation affecting more items than bulk_action_threshold (default: 50)
Config path writeEscalateWrite to a system config path (/etc, /root, ~/.ssh, ~/.aws, ~/.config)
Protected file patternEscalateAny 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, addresses parameters
  • 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

EventFired when
BLAST_RADIUS_ESCALATIONAn action exceeds a blast radius threshold and is escalated for human review
BASELINE_POISONING_SUSPECTEDMore 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.