---
title: Author a Rego Policy
description: Write a Caracal-compatible Rego policy, understand the full OPA input schema, output the required result structure, and avoid forbidden builtins.
pageType: workflow
---

Caracal policies are Rego modules evaluated by OPA. Every token exchange calls OPA once per requested resource and expects a single `result` object back. The policy decides whether to allow or deny the exchange and may attach diagnostic data that the SDK and STS use for step-up challenge signaling.

## Prerequisites

- OPA installed locally for testing (`brew install opa` or the [OPA releases page](https://www.openpolicyagent.org/docs/latest/#running-opa)).
- A Caracal zone with an active policy set (see [Activate a Policy Set](./activate-policy-set/)).

## Package and rule requirements

Every policy must:

1. Declare `package caracal.authz`.
2. Import `rego.v1`.
3. Define a `result` rule that produces the required output object.

```text
package caracal.authz
import rego.v1

result := { ... }
```

## The `result` object

The `result` rule must produce an object with exactly these four fields:

```text
result := {
  "decision":              "allow",    # or "deny"
  "evaluation_status":    "complete",
  "determining_policies": [{"policy": "my-policy-name"}],
  "diagnostics":          [],
}
```

| Field | Type | Description |
| ----- | ---- | ----------- |
| `decision` | `"allow"` \| `"deny"` | The access decision |
| `evaluation_status` | `"complete"` | Any value other than `"complete"` is treated as an evaluation error |
| `determining_policies` | `array` | Objects identifying which policy rules drove the decision |
| `diagnostics` | `array` | Arbitrary objects surfaced to the SDK; used for step-up signaling |

## The OPA `input` object

The STS populates `input` on every evaluation:

```json
{
  "schema_version": "2026-05-20",
  "principal": {
    "type": "application",
    "id": "app-abc123",
    "zone_id": "my-zone",
    "credential_type": "token",
    "agent_session_id": "agent-sess-xyz",
    "agent_kind": "ephemeral",
    "capabilities": ["browser", "code"]
  },
  "resource": {
    "type": "resource",
    "id": "res-def456",
    "identifier": "resource://payments",
    "scopes": ["payment:read", "payment:submit"]
  },
  "action": {
    "id": "token_exchange"
  },
  "session": {
    "id": "sess-xyz"
  },
  "delegation_edge": {
    "id": "edge-ghi789",
    "source_session_id": "sess-parent",
    "target_session_id": "sess-xyz",
    "issuer_application_id": "app-orchestrator",
    "receiver_application_id": "app-abc123",
    "resource_id": "res-def456",
    "scopes": ["payment:submit"],
    "edge_version": 1,
    "path": ["edge-ghi789"],
    "graph_epoch": 42,
    "constraints_json": {}
  },
  "context": {
    "actor_claims": {
      "roles": ["analyst"],
      "department": "finance",
      "clearance_level": 3
    },
    "subject_claims": {},
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
    "session_id": "sess-xyz",
    "agent_session_id": "sess-xyz",
    "delegation_edge_id": "edge-ghi789",
    "challenge_resolved": false,
    "requested_scopes": ["payment:submit"]
  }
}
```

`schema_version` identifies the STS policy input contract. The current contract is `2026-05-20`; policy creation, policy set creation, and activation reject unsupported or mixed schema versions. Policies should check it explicitly if they depend on fields introduced after their original authoring. `principal.agent_kind` and `principal.capabilities` are present when the exchange asserts an active Coordinator agent session. `delegation_edge` is present only when the exchange involves a delegation edge. `context.actor_claims` carries custom claims injected by the credential provider (e.g., from an OIDC userinfo endpoint).

Use `POST /v1/zones/{zone_id}/policy-sets/{policy_set_id}/simulate` before activation to dry-run the rollout contract. The simulation endpoint validates the policy set version, schema compatibility, manifest integrity, and optional OPA input shape without mutating active bindings. It does not replace STS runtime evaluation; it is a deployment safety check for operators.

## Forbidden builtins

The following OPA builtins are blocked and cause an evaluation error if used:

- `http.send`
- `net.lookup_ip_addr`
- `net.cidr_contains`, `net.cidr_intersects`, `net.cidr_merge`, `net.cidr_expand`
- `opa.runtime`
- `rand.intn`
- `time.now_ns`

Policies must be deterministic and must not make network calls. Use `data.*` for reference material.

## Default deny with explicit allow

This pattern ensures the policy always produces a valid `result`, with allow overriding deny:

```text
package caracal.authz
import rego.v1

default result := {
  "decision":           "deny",
  "evaluation_status":  "complete",
  "determining_policies": [],
  "diagnostics":        [],
}

result := {
  "decision":           "allow",
  "evaluation_status":  "complete",
  "determining_policies": [{"policy": "allow-rule"}],
  "diagnostics":        [],
} if {
  allow
}

default allow := false

allow if {
  "analyst" in input.context.actor_claims.roles
  input.resource.identifier == "resource://reports"
}
```

## Scope-based access

Check that requested scopes are covered by what the principal is allowed:

```text
package caracal.authz
import rego.v1

default result := {
  "decision": "deny",
  "evaluation_status": "complete",
  "determining_policies": [],
  "diagnostics": [],
}

# Allowed scopes per resource — load from data for real policies
allowed_scopes := {"resource://inventory": {"inventory:read", "inventory:write"}}

result := {
  "decision": "allow",
  "evaluation_status": "complete",
  "determining_policies": [{"policy": "scope-check"}],
  "diagnostics": [],
} if {
  permitted := allowed_scopes[input.resource.identifier]
  every scope in input.context.requested_scopes {
    scope in permitted
  }
}
```

## Step-up trigger

Emit a `step_up_required` diagnostic to signal that the STS should create a step-up challenge:

```text
package caracal.authz
import rego.v1

sensitive_resources := {"resource://payments", "resource://transfers"}

# Allow once challenge is resolved
result := {
  "decision": "allow",
  "evaluation_status": "complete",
  "determining_policies": [{"policy": "payments-policy"}],
  "diagnostics": [],
} if {
  input.resource.identifier in sensitive_resources
  input.context.challenge_resolved == true
}

# Deny and request step-up when challenge not yet resolved
result := {
  "decision": "deny",
  "evaluation_status": "complete",
  "determining_policies": [{"policy": "payments-policy"}],
  "diagnostics": [{"step_up_required": "mfa"}],
} if {
  input.resource.identifier in sensitive_resources
  not input.context.challenge_resolved
}

# Default deny for everything else
default result := {
  "decision": "deny",
  "evaluation_status": "complete",
  "determining_policies": [],
  "diagnostics": [],
}
```

The `step_up_required` value (`"mfa"`, `"human_approval"`, or `"software_attestation"`) determines the challenge type the STS creates.

## Reference data with `data.*`

Store reference tables outside policy logic by loading them as OPA data documents. When you version a policy set, bundle the policy and a data document together. Access via `data.my_table`:

```text
# Policy references data.approved_vendors
result := {
  "decision": "allow",
  ...
} if {
  input.principal.id in data.approved_vendors
}
```

## Test locally with OPA

Save a policy to `policy.rego` and an input to `input.json`, then evaluate:

```bash
opa eval \
  --input input.json \
  --data policy.rego \
  'data.caracal.authz.result'
```

A well-formed allow result looks like:

```json
{
  "result": [
    {
      "expressions": [
        {
          "value": {
            "decision": "allow",
            "evaluation_status": "complete",
            "determining_policies": [{"policy": "allow-rule"}],
            "diagnostics": []
          }
        }
      ]
    }
  ]
}
```

## What to read next

- [Activate a Policy Set](./activate-policy-set/) — upload the policy, bundle it into a policy set, and make it active
- [Concepts: Policy](../concepts/policy/) — how policies are versioned, bundled, and evaluated
