Skip to main content

Policy Engine

Overview

Behavry uses Open Policy Agent (OPA) as its policy engine. Policies are written in Rego and stored in two places:

  1. Base policies (policies/base/): checked into the repo, pushed to OPA at startup. These implement the core governance rules for all MCP server types.
  2. Custom policies (database): created via the Policies UI, stored in PostgreSQL, synced to OPA when activated.

OPA runs as a separate sidecar container. The Behavry backend calls it via REST: POST /v1/data/behavry/authz.

In addition to OPA evaluation, the enforcement engine runs several pre-OPA checks in Python (blast radius limits, DLP scanning, baseline drift) that can deny or escalate requests before they ever reach OPA.


OPA Input Envelope

Every policy evaluation receives this input:

{
"agent": {
"id": "a1b2c3d4-...",
"roles": ["filesystem-reader"],
"permissions": ["filesystem:read", "database:read"],
"risk_tier": "medium"
},
"request": {
"tool_name": "read_file",
"action": "read",
"resource": "/home/projects/report.pdf",
"parameters": {"path": "/home/projects/report.pdf"},
"mcp_server": "filesystem",
"resource_count": 1
},
"context": {},
"requester": {
"id": "user@example.com",
"channel": "dashboard",
"verified": true
},
"workflow_session": null,
"delegation": null
}

These are populated by the enforcement engine from the agent's JWT claims, the parsed MCP tool call, and request headers.

Field Reference

FieldSourceDescription
agent.*JWT claimsAgent identity, roles, permissions, and risk tier
request.*MCP tool callTool name, action, resource path, parameters, and server type
request.resource_countProxy engineNumber of affected resources (used by blast radius rules)
contextRequest metadataAdditional context (reserved for future use)
requesterX-Requester-Id header / delegation tokenIdentity of the human or upstream agent that instructed this agent
requester.verifiedDelegation token chaintrue when requester identity is cryptographically verified via a delegation token; false when caller-asserted only
workflow_sessionX-Workflow-Session JWTWorkflow context including causal depth, max depth, and permission ceiling. null for non-workflow requests
delegationX-Delegation-Token JWTDelegation scope, effective permissions, and verification status. null for non-delegated requests

OPA Output (Decision)

{
"result": "allow",
"policy": "filesystem.read",
"reason": ""
}
resultMeaning
allowForward the tool call to the backend MCP server
denyBlock immediately; return JSON-RPC error -32003 to agent
escalateHold the request; wait for human approval via dashboard

Policy Evaluation Order

The enforcement engine has a layered check sequence. Several checks run before OPA is consulted:

1.  JWT validation (auth)
2. Session active check (DB)
2d. Workflow context validation (X-Workflow-Session JWT)
2e. Delegation token validation (X-Delegation-Token JWT)
3. Baseline cache check (tool drift — blocks if not in manifest)
4. DLP scan (blocks on critical severity)
4c. Delegation scope probe tracking
4d. Blast radius pre-check (Python-native — deny or escalate)
5. Policy exception check (standing allowlist — bypasses OPA if matched)
6. OPA evaluation (behavry/authz package)

The blast radius pre-check (step 4d) is notable because it runs entirely in Python before OPA is called. This avoids an unnecessary OPA round-trip for structurally dangerous operations like shallow-path deletes.

OPA itself uses a first-match-wins model via Rego's complete rule semantics with priority:

  1. Explicit deny rules (highest priority)
  2. Explicit escalate rules
  3. Explicit allow rules
  4. Generic RBAC fallback
  5. Default deny (no policy matched)

Blast Radius Pre-Check

Before OPA evaluation, the proxy engine runs a Python-native blast radius check that evaluates whether an action's scope is disproportionately large. This check applies five rules:

RuleConditionDecision
Shallow deleteDelete action on a path with depth below minimum (default 3)deny
Recipient limitMessage/email action with too many recipients (default 10)escalate
Bulk thresholdNon-delete, non-message action affecting too many resources (default 50)escalate
Config path writeWrite action targeting system config paths (/etc, ~/.ssh, ~/.aws, etc.)escalate
Protected file patternsAny action on files matching protected patterns (MEMORY, SOUL, IDENTITY, .env)escalate

Deny results are returned immediately. Escalate results create a HITL hold via the escalation queue, and the engine waits for human resolution before proceeding.

Per-Tenant Configuration

All blast radius thresholds are configurable per tenant via the Admin API:

  • GET /api/v1/admin/blast-radius -- retrieve current configuration
  • PATCH /api/v1/admin/blast-radius -- update thresholds

Configuration changes are synced to OPA as data at data.behavry.blast_radius_config, so both the Python pre-check and the OPA Rego policy (policies/base/blast_radius.rego) use the same thresholds.

When the blast radius check triggers, a BLAST_RADIUS_ESCALATION event is emitted to the audit log and event bus.


OPA Policy Modules

Beyond the base RBAC authorization rules at behavry/authz, Behavry ships several specialized OPA policy modules:

PackagePathPurpose
behavry.authzpolicies/base/Core RBAC, per-server rules (filesystem, database, Slack, GitHub, web)
behavry.blast_radiuspolicies/base/blast_radius.regoBlast radius limits (shallow delete, recipient cap, bulk threshold, config paths, protected files)
behavry.delegation.policypolicies/delegation/delegation_policy.regoDelegation scope enforcement: deny unverified tokens, escalate out-of-scope tools
behavry.workflows.policypolicies/workflows/workflow_policy.regoWorkflow governance: deny depth exceeding max_depth, escalate tools outside permission ceiling
behavry.inbound.injection_checkpolicies/inbound/injection_check.regoInjection findings evaluation from inbound content scanning
behavry.inbound.domain_trustpolicies/inbound/domain_trust.regoContent source trust tier enforcement
behavry.requesterpolicies/requester/requester_policy.regoRequester verification: block critical actions with unverified/absent requester, escalate high-risk actions
behavry.authz.autogenpolicies/autogen/Auto-generated policies from the Red Team Policy Automation loop

Delegation Policy

The delegation policy evaluates every request carrying an X-Delegation-Token header:

  • Allow when the delegation is verified and the tool is within effective_permissions.tools
  • Deny when the delegation token validation failed (unverified)
  • Escalate when the tool is not in the delegation's effective permissions (scope probe)
  • Skip (allow) when no delegation context is present

Workflow Policy

The workflow policy evaluates requests carrying an X-Workflow-Session header:

  • Deny when causal depth exceeds the workflow's max_depth
  • Escalate when the tool is not in the workflow's permission ceiling
  • Allow when all constraints are satisfied
  • Skip (allow) when no workflow context is present

Requester Policy

The requester policy enforces identity verification based on risk tier:

  • Block critical-tier actions with no requester identity or unverified requester
  • Escalate high-tier actions with no requester identity or unverified requester
  • Verified requesters (via delegation token chain) pass through for high-tier actions

Base Policy Structure

package behavry.authz

import rego.v1

# Default: deny if no rule matches
default decision := {"result": "deny", "reason": "No policy matched"}

# Deny rule (high priority via rule ordering)
decision := {
"result": "deny",
"reason": "Access to sensitive files is not permitted",
"policy": "filesystem.blocked_paths",
} if {
_is_filesystem_server
_fs_sensitive_path
}

# Allow rule
decision := {"result": "allow", "policy": "filesystem.read"} if {
_is_filesystem_server
input.request.tool_name in _fs_read_ops
"filesystem:read" in input.agent.permissions
not _fs_sensitive_path
}

# Escalate rule
decision := {
"result": "escalate",
"reason": "File deletion requires human approval",
"policy": "filesystem.escalate_delete",
} if {
_is_filesystem_server
input.request.tool_name in _fs_delete_ops
"filesystem:write" in input.agent.permissions
not _fs_sensitive_path
}

Built-in Policy Rules (Base Policies)

Filesystem Server

RuleConditionDecision
filesystem.blocked_pathsPath contains .env, .ssh, .aws, id_rsa, credentials, secrets, etc.deny
filesystem.readtool in {list_directory, read_file, search_files}, has filesystem:read, non-sensitive pathallow
filesystem.writetool in {write_file}, has filesystem:write, non-sensitive pathallow
filesystem.escalate_deletetool is delete_file, has filesystem:write, non-sensitive pathescalate
filesystem.deny_deletetool is delete_file, no filesystem:writedeny

Database Server

RuleConditionDecision
database.block_writewrite/delete tool, no database:writedeny
database.sensitive_tablesaccess to PII/financial tables, no database:sensitive_readdeny
database.readread tool, has database:read, non-sensitive tableallow
database.sensitive_readread tool, has both database:read + database:sensitive_readallow
database.writewrite tool, has database:writeallow
database.escalate_deletedelete tool, has database:writeescalate

Sensitive tables: employees_pii, financial_records, health_records, salaries

Slack (via web server)

RuleConditionDecision
slack.deny_deleteslack_delete_message, no slack:deletedeny
slack.protected_channelsposting to admin/security/exec channels, no slack:protected_channelsdeny
slack.escalate_broadcastposting to general/announcements/company-wideescalate
slack.readslack_list_channels, has slack:readallow
slack.postslack_post_message, has slack:post, non-broadcast, non-protectedallow

GitHub (via web server)

RuleConditionDecision
github.escalate_mergegithub_merge_pr, no github:mergeescalate
github.readgithub_list_repos, has github:readallow
github.writegithub_create_issue, has github:writeallow

Generic HTTP (via web server)

RuleConditionDecision
web_api.http_gethttp_get, has web:readallow
web_api.escalate_posthttp_post, has web:writeescalate
web_api.deny_posthttp_post, no web:writedeny

RBAC Fallback (no specific server policy)

RuleConditionDecision
rbacrole permits action on resource (via data.roles)allow
rbac (escalate)no rbac_allow, agent is high/critical risk tier + sensitive actionescalate
defaultnothing matcheddeny

Writing Custom Policies

Custom policies are stored in the database and synced to OPA under policies/{tenant_id}/{policy_id}.rego.

Example: Block specific agent from all writes

package behavry.authz.custom

import rego.v1

# Override: a specific agent is read-only
decision := {
"result": "deny",
"reason": "Agent data-analyst-01 is restricted to read operations only",
"policy": "custom.agent_readonly",
} if {
input.agent.id == "a1b2c3d4-0000-0000-0000-000000000001"
input.request.action in {"write", "delete", "execute"}
}

Example: Escalate all actions for high-risk agents

package behavry.authz.custom

import rego.v1

decision := {
"result": "escalate",
"reason": "High-risk agent requires human approval for all non-read actions",
"policy": "custom.high_risk_escalate",
} if {
input.agent.risk_tier in {"high", "critical"}
input.request.action != "read"
}

Example: Time-based restriction (not yet in OPA data -- future)

package behavry.authz.custom

import rego.v1

# Deny writes outside business hours (UTC)
decision := {
"result": "deny",
"reason": "Write operations only permitted during business hours (09:00-18:00 UTC)",
"policy": "custom.business_hours",
} if {
input.request.action in {"write", "delete"}
hour := time.clock(time.now_ns())[0]
not (hour >= 9; hour < 18)
}

Policy Change Request Workflow

Policy modifications go through a structured approval workflow before taking effect. This prevents accidental or unauthorized policy changes from reaching production.

Lifecycle

  1. Draft a change: An administrator submits a change request against an existing policy, specifying proposed Rego content, name, description, and a summary of the change.
  2. Review: The change request appears in the Policies dashboard with an amber pending badge. Other administrators can review the proposed Rego diff.
  3. Approve or reject:
    • Approve -- the proposed Rego content is applied to the policy record, synced to OPA immediately, and the policy becomes active. An audit event of type policy_change_approved is recorded.
    • Reject -- the change request is closed with reviewer notes. An audit event of type policy_change_rejected is recorded.

API Endpoints

MethodPathDescription
POST/api/v1/policies/{id}/change-requestsSubmit a change request
GET/api/v1/policies/{id}/change-requestsList change requests for a policy
POST/api/v1/policies/{id}/change-requests/{req_id}/approveApprove and apply
POST/api/v1/policies/{id}/change-requests/{req_id}/rejectReject with notes
GET/api/v1/policies/change-requests/pendingCount pending requests across all policies

Dashboard Integration

The Policies page includes a ChangeRequestsPanel below the Rego source editor. The Policies navigation item shows an amber badge with the count of pending change requests (refreshed every 30 seconds).


Red Team Policy Automation (Policy Candidates)

The Policy Generator continuously monitors the event bus for security-relevant detection findings and automatically proposes candidate Rego policies to close observed gaps. This creates a feedback loop: detect a threat pattern, generate a policy to block it, and either auto-activate or present it for human review.

Handled Event Types

The generator responds to six event types:

Event TypeGenerated Template
INBOUND_INJECTION_DETECTEDinjection_block -- block the specific injection pattern
INBOUND_INJECTION_BLOCKEDinjection_block -- reinforce existing block
INJECTION_CONDITIONING_SUSPECTEDconditioning_block -- block gradual conditioning attempts
BEHAVIORAL_DRIFT_DETECTEDdrift_escalate -- escalate drifting agent actions
REQUESTER_SESSION_CYCLINGrequester_validation / rate_ceiling -- restrict session cycling patterns
BEHAVIOR_REVERSALbehavior_reversal -- restrict agents reversing previously blocked actions

Seven Rego templates are available: injection_block, conditioning_block, drift_escalate, resource_restrict, rate_ceiling, requester_validation, and behavior_reversal.

Confidence Scoring

Each candidate receives a confidence score (0.0 to 1.0) computed from three factors:

FactorContribution
Pattern frequency1 occurrence = 0.3, 3+ = 0.6, 10+ = 0.9
SeverityCritical = +0.3, High = +0.2, Medium = +0.1
Corroboration2+ distinct source event types for same pattern = +0.2

The final score is capped at 1.0.

Pattern Deduplication

Each candidate is assigned a normalized fingerprint signature derived from the source event type and key fields (agent ID, tool name, resource pattern, requester ID, etc.). If a candidate with the same (tenant_id, signature) already exists in proposed or auto_activated status, no duplicate is created -- instead, the frequency counter for that pattern is incremented, which may raise the confidence score.

Auto-Activation

When a tenant has auto_activate_enabled = true on their configuration, candidates whose confidence score meets or exceeds auto_activate_threshold are automatically activated: the generator creates a Policy record, pushes the Rego to OPA, and marks the candidate as auto_activated. No human review is required.

Candidates below the threshold remain in proposed status for manual review.

Manual Review

Administrators can review candidates in the Policy Suggestions dashboard:

  • Inspect the generated Rego and the source detection events
  • Edit the Rego inline before approving
  • Test the policy with a dry-run against OPA (pushes to a temporary path and evaluates)
  • Approve to create and activate the policy
  • Reject with notes explaining why the candidate was not adopted

Auto-generated policies are stored under policies/autogen/ and use the behavry.authz.autogen OPA package.

API Endpoints

MethodPathDescription
GET/api/v1/policy-candidatesList candidates (filterable by status)
GET/api/v1/policy-candidates/statsSummary counts by status
GET/api/v1/policy-candidates/{id}Get candidate details
POST/api/v1/policy-candidates/{id}/approveApprove and activate
POST/api/v1/policy-candidates/{id}/rejectReject with notes
PATCH/api/v1/policy-candidates/{id}/regoUpdate candidate Rego before approval
POST/api/v1/policy-candidates/{id}/testDry-run test against OPA

Policy Lifecycle

  1. Create policy via POST /api/v1/policies with rego_content and status: "draft"
  2. Validate manually: opa check policies/ (OPA CLI)
  3. Activate: PATCH /api/v1/policies/{id} with {"status": "active"}
  4. Activation syncs the Rego to OPA via PUT /v1/policies/{id} (REST)
  5. Deactivate: set status to archived -- removed from OPA

For changes to existing active policies, use the Policy Change Request Workflow to ensure review and auditability.

For auto-generated policies, the Red Team Policy Automation loop handles creation and activation automatically when confidence thresholds are met.


Policy Exceptions (Standing Allowlist)

A PolicyException bypasses OPA for specific (tool_name, action, target_pattern) tuples. Checked before OPA on every request. Use for:

  • Approved recurring actions that would always escalate
  • Pre-authorized workflows

Created via "Approve + Exception" in the escalation dialog or POST /api/v1/exceptions.

{
"tool_name": "delete_file",
"action": "delete",
"target_pattern": "/tmp/",
"justification": "Approved for automated temp file cleanup during nightly batch jobs",
"expires_in_hours": 720,
"extension_count": 0,
"max_extensions": 4
}

Exception Hardening

Creating exceptions via POST /api/v1/exceptions requires two mandatory fields:

  • justification: A text explanation of at least 10 characters describing why the exception is needed.
  • expires_in_hours: An integer between 1 and 8760 (one year maximum) specifying the exception's lifetime.

These requirements ensure that every exception has a documented rationale and a defined expiration, preventing the accumulation of unbounded standing exceptions.

Rate Limiting and Anomaly Detection

The system monitors exception creation frequency per agent. If more than 5 exceptions are created within a one-hour window for the same agent, an alert is raised. This guards against scenarios where an attacker or misconfigured automation creates a large number of exceptions to weaken the policy posture.

Similarly, baseline approval frequency is tracked. If more than 3 baseline approvals occur for the same agent within a 24-hour window, a BASELINE_POISONING_SUSPECTED event is emitted. This detects attempts to gradually shift an agent's behavioral baseline by repeatedly triggering and approving small deviations.

Extension and Urgency

Timed exceptions can be extended up to max_extensions times (default 4) via PATCH /api/v1/exceptions/{id}/extend. Each extension adds hours to expires_at and increments extension_count. The dashboard shows escalating urgency colors as extensions accumulate (yellow, orange, red), signaling admins to make a permanent decision -- either create a permanent exception or block the action.