Policy Engine¶
The ARX policy engine is the central enforcement point for all agent governance. Every connector call made by every agent passes through the policy engine before execution. The engine evaluates the call against permission bindings, declared intent manifests, risk scores, and organization-defined policy rules, then returns one of three verdicts: PERMIT, ESCALATE, or DENY.
This page covers the policy data model, evaluation algorithm, rule configuration, and default behaviors.
Core Concepts¶
Policy Verdicts¶
Every evaluation produces exactly one of three verdicts:
| Verdict | Meaning | Effect |
|---|---|---|
PERMIT |
Action is allowed | Connector call proceeds immediately |
ESCALATE |
Action requires human approval | Approval request is created; agent blocks until a reviewer responds |
DENY |
Action is forbidden | Connector call is rejected; PermissionDeniedError is raised |
Verdicts are enforced at the connector level through the BaseConnector.execute() intercept pattern. There is no bypass mechanism.
Policy Rules¶
A policy rule is a named, versioned governance directive that maps a combination of connector, action pattern, and risk threshold to a verdict. Policy rules are scoped to an organization and bound to a specific agent.
PolicyRule Schema¶
Policy rules are defined using the following schema:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Human-readable rule name. 1-255 characters. |
rule_type |
string |
Yes | One of allow, deny, or escalate. |
agent_id |
UUID |
Yes | The agent this rule applies to. |
connector |
string |
No | Connector identifier (e.g., crowdstrike, okta). null means the rule applies to all connectors. |
action_pattern |
string |
No | Glob pattern matched against the operation string. Defaults to * (match all). |
risk_threshold |
integer |
No | Score threshold (0-100) above which the engine escalates even if rule_type is allow. Defaults to 70. |
approval_channel |
string |
No | Slack/Teams channel ID for routing escalation notifications. |
Rule Types¶
allow-- Permits the action, subject to risk threshold. If the computed risk score meets or exceedsrisk_threshold, the verdict is upgraded toESCALATEregardless of theallowdesignation.deny-- Unconditionally blocks the action. Risk score is still computed for audit purposes but does not change the verdict.escalate-- Always routes to human approval, regardless of risk score.
Action Pattern Matching¶
The action_pattern field supports Python fnmatch glob syntax:
| Pattern | Matches | Does Not Match |
|---|---|---|
* |
All operations | -- |
host:* |
host:read, host:isolate, host:contain |
detection:list |
*:delete |
ticket:delete, user:delete |
ticket:update |
host:isolate |
host:isolate only |
host:contain |
detection:* |
detection:list, detection:update |
host:isolate |
Patterns are evaluated using Python's fnmatch.fnmatch(). Case sensitivity follows the default behavior of the underlying OS, but by convention all operation names in ARX are lowercase.
Evaluation Algorithm¶
The policy engine evaluates every connector call through a four-step pipeline. The steps execute in strict order; the first step that produces a terminal verdict short-circuits the rest.
Step 1: Permission Binding Check (INV-005)¶
The engine queries the connector_configs table for a binding between the agent and the target connector. The binding includes a permitted_operations list.
- If no binding exists for the agent/connector pair, the verdict is DENY with a risk score of 100.
- If the requested operation is not in
permitted_operations, the verdict is DENY with a risk score of 100.
This step enforces the principle that agents have no implicit permissions. An agent cannot interact with a connector unless explicitly bound to it.
Step 2: Declared Intent Validation (INV-003)¶
If the agent has a declared intent manifest, the engine validates the connector call against it:
- Permitted systems check: If
permitted_systemsis non-empty and the target connector is not listed, the verdict is DENY. - Permitted actions check: If
permitted_actionsis non-empty and the operation does not match any entry (glob matching viafnmatch), the verdict is DENY.
If no declared intent is set on the agent, this step is skipped.
Step 3: Risk Scoring¶
The engine computes a dynamic risk score (0-100) based on four factors:
- Operation type: Read/list operations score low (10); write/update operations score moderate (30); delete/remove operations score high (50).
- Target sensitivity: The
target_sensitivityfield from session context adds 0 (low), 15 (medium), 30 (high), or 50 (critical) points. - Session frequency: If the agent has performed more than 50 actions in the current session, 20 points are added. More than 20 actions adds 10 points.
The score is capped at 100.
Step 4: Policy Rule Matching¶
The engine retrieves all policy rules for the organization/agent/connector combination. Rules are evaluated in order. For each rule:
- The engine checks if the rule's
connectormatches (anullconnector is a wildcard that matches any connector). - The engine checks if the rule's
action_patternmatches the operation viafnmatch. - If both match:
- If
rule_typeisdeny, return DENY. - If
rule_typeisescalate, or the computed risk score is >= the rule'srisk_threshold, return ESCALATE. - Otherwise, the rule permits the action and evaluation continues.
The first matching rule with a deny or escalate outcome wins. This is the "most restrictive match wins" principle.
Default Behavior (No Policy Match)¶
If no policy rule matches, the engine applies built-in thresholds based on the risk score:
| Risk Score | Default Verdict |
|---|---|
| 0-49 | PERMIT |
| 50-79 | ESCALATE |
| 80-100 | DENY |
These defaults ensure that high-risk operations are never silently permitted, even if the organization has not yet configured explicit policy rules.
Creating Policies via API¶
Create a Policy Rule¶
POST /v1/policies
Authorization: Bearer <token>
Content-Type: application/json
{
"agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Block CrowdStrike host isolation",
"rule_type": "deny",
"connector": "crowdstrike",
"action_pattern": "host:isolate",
"risk_threshold": 70
}
Response: 201 Created with the full PolicyResponse object.
Only users with the admin role can create policy rules. The org_id is automatically set from the authenticated user's organization.
Update a Policy Rule¶
PATCH /v1/policies/{policy_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"risk_threshold": 50,
"rule_type": "escalate"
}
All fields in the update payload are optional. Only the fields provided are changed. Returns the updated PolicyResponse.
List Policy Rules¶
GET /v1/policies
GET /v1/policies?agent_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
Returns a PolicyListResponse containing an array of policies and a total count. Results are ordered by created_at descending.
Delete a Policy Rule¶
DELETE /v1/policies/{policy_id}
Returns 204 No Content. The deletion is permanent and immediate.
Audit Trail¶
Every policy lifecycle event is recorded in the audit log:
| Event | action_type |
Metadata |
|---|---|---|
| Policy created | policy.created |
rule_type, action_pattern |
| Policy updated | policy.updated |
List of updated field names |
| Policy deleted | policy.deleted |
policy_name |
| Policy evaluated (per call) | connector.called |
risk_score, policy_id, verdict |
All policy mutations include the user_id of the admin who made the change. Evaluation events include the agent_id and the matched policy_id (if any).
Common Policy Configurations¶
Read-Only Agent¶
Allow an agent to read from all connectors but deny all write and delete operations:
[
{
"name": "Permit all reads",
"rule_type": "allow",
"connector": null,
"action_pattern": "*:read",
"risk_threshold": 90
},
{
"name": "Permit all list operations",
"rule_type": "allow",
"connector": null,
"action_pattern": "*:list",
"risk_threshold": 90
},
{
"name": "Deny all writes",
"rule_type": "deny",
"connector": null,
"action_pattern": "*:write"
},
{
"name": "Deny all deletes",
"rule_type": "deny",
"connector": null,
"action_pattern": "*:delete"
}
]
Escalate All CrowdStrike Containment Actions¶
{
"name": "Escalate CrowdStrike containment",
"rule_type": "escalate",
"connector": "crowdstrike",
"action_pattern": "host:*",
"approval_channel": "#security-approvals"
}
Low-Threshold Monitoring for Okta¶
Force escalation for any Okta operation with a risk score above 40:
{
"name": "Strict Okta oversight",
"rule_type": "allow",
"connector": "okta",
"action_pattern": "*",
"risk_threshold": 40
}
Deny Specific Destructive Operations¶
{
"name": "Block user deletion",
"rule_type": "deny",
"connector": "okta",
"action_pattern": "user:delete"
}
Versioning and Change Management¶
Policy rules are stored in the policies table with created_at timestamps and created_by user references. The audit log provides a complete history of every policy creation, modification, and deletion. To reconstruct the policy state at any point in time, query the audit log for policy.created, policy.updated, and policy.deleted events filtered by timestamp.
Organizations that require formal change management should use the audit log entries as evidence artifacts for compliance reviews. Each audit entry includes the authenticated user ID, the organization ID, and a structured metadata payload describing what changed.
Integration with the Intercept Layer¶
The policy engine is invoked by two code paths:
BaseConnector.execute()-- Every connector subclass inherits this method. It callsPolicyEngine.evaluate()before dispatching to the connector-specific_execute_impl().intercept_connector_call()-- The SDK intercept layer calls the policy engine as part of a broader pipeline that also includes drift detection and webhook notifications.
Both paths produce identical policy verdicts. The intercept layer adds drift detection as a pre-check before policy evaluation; if drift detection suspends the agent, the policy engine is never reached.
Troubleshooting¶
Agent is denied but no deny policy exists. Check the permission binding. If the agent has no connector_configs entry for the target connector, or the operation is not listed in permitted_operations, the engine returns DENY before policy rules are evaluated.
Agent is escalated on low-risk reads. A policy rule with a low risk_threshold (e.g., 30) on a high-sensitivity connector (e.g., Okta at 35 base points) will trigger escalation even for read operations. Raise the risk_threshold or make the action_pattern more specific.
Policy changes are not taking effect. Policies are queried from the database on every evaluation. There is no cache. Verify the policy's org_id and agent_id match the agent being tested. Use GET /v1/policies?agent_id=<id> to confirm the rule is visible.