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

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.

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:

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:

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:

  1. The engine checks if the rule's connector matches (a null connector is a wildcard that matches any connector).
  2. The engine checks if the rule's action_pattern matches the operation via fnmatch.
  3. If both match:
  4. If rule_type is deny, return DENY.
  5. If rule_type is escalate, or the computed risk score is >= the rule's risk_threshold, return ESCALATE.
  6. 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:

  1. BaseConnector.execute() -- Every connector subclass inherits this method. It calls PolicyEngine.evaluate() before dispatching to the connector-specific _execute_impl().
  2. 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.