---
title: Policy
description: Versioned Rego documents evaluated by OPA inside the STS at token-exchange time.
pageType: concept
keywords: [policy, Rego, OPA, policy set, evaluation, builtins, manifest, SHA, versioned]
---

A policy is a versioned Rego document that the STS evaluates at token-exchange time to determine whether to issue a mandate. This page covers the policy structure, the OPA evaluation model, policy sets, versioning, and the runtime loading behavior.

## What a policy is

A policy is a named Rego document with a fixed entry point. It receives structured input describing the principal, resource, session, and context, and must return a decision object at `data.caracal.authz.result`.

Policies are stored in the `policies` table, versioned in `policy_versions` (each version is immutable), and activated through a `policy_set_binding` on the zone.

## The required entry point

Every Caracal-compatible Rego policy must produce a result at `data.caracal.authz.result`:

```text
package caracal.authz

import future.keywords.if
import future.keywords.in

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

result := {
  "decision": "allow",
  "evaluation_status": "complete",
  "determining_policies": ["my-policy"],
  "diagnostics": [],
} if {
  input.principal.type == "application"
  input.resource.identifier == "resource://example"
  "read" in input.resource.scopes
}
```

**Required result fields:**

| Field | Type | Description |
| ----- | ---- | ----------- |
| `decision` | string | `"allow"` or `"deny"` |
| `evaluation_status` | string | Must be `"complete"` for the STS to accept the result |
| `determining_policies` | string[] | IDs or names of the rules that determined the decision |
| `diagnostics` | object[] | Arbitrary metadata; used for step-up signaling (see below) |

## The OPA input object

The full input available to every policy:

```json
{
  "principal": {
    "type": "application",
    "id": "<application_id>",
    "zone_id": "<zone_id>",
    "credential_type": "token",
    "agent_session_id": "<agent_session_id>"
  },
  "resource": {
    "type": "resource",
    "id": "<resource_id>",
    "identifier": "resource://payments",
    "scopes": ["transfer", "read"]
  },
  "action": {
    "id": "TokenExchange"
  },
  "session": {
    "id": "<session_id>"
  },
  "delegation_edge": {
    "id": "<edge_id>",
    "source_session_id": "...",
    "target_session_id": "...",
    "issuer_application_id": "...",
    "receiver_application_id": "...",
    "resource_id": "...",
    "scopes": ["read"],
    "edge_version": 1,
    "path": ["<session_id_1>", "<session_id_2>"],
    "graph_epoch": 42,
    "constraints_json": {
      "max_hops": 1,
      "ttl_seconds": 300,
      "budget": 1
    }
  },
  "context": {
    "actor_claims": { "sub": "...", "zone_id": "...", "scope": "..." },
    "subject_claims": { "sub": "...", "zone_id": "...", "scope": "..." },
    "trace_id": "...",
    "session_id": "...",
    "agent_session_id": "...",
    "delegation_edge_id": "...",
    "challenge_resolved": false,
    "requested_scopes": ["read"]
  }
}
```

`delegation_edge` is present only when the exchange carries a delegation edge ID. `session` is present only when a session ID is provided.

## Blocked builtins

The following Rego builtins are disabled in all Caracal policies:

| Builtin | Reason |
| ------- | ------ |
| `http.send` | Prevents network calls from policy evaluation |
| `net.lookup_ip_addr`, `net.cidr_*`, `net.cidr_expand` | Prevents network inspection |
| `opa.runtime` | Prevents runtime introspection |
| `rand.intn` | Prevents nondeterministic decisions |
| `time.now_ns` | Prevents clock-dependent decisions (use `input.context` timestamps if needed) |

Policies that reference any blocked builtin fail validation when submitted to the API.

## Policy versioning

Each time a policy's Rego source changes, a new version is created in `policy_versions`. Versions are immutable and identified by a content-addressable SHA-256 hash. The API validates Rego syntax and blocked builtins before accepting a new version.

Creating a policy version:

```
POST /zones/{zoneId}/policies/{policyId}/versions
{ "content": "package caracal.authz\n...", "schema_version": "2026-03-16" }
```

The `schema_version` field (`"2026-03-16"`) identifies the input schema this policy was written for.

## Policy sets and manifests

A **policy set** groups one or more policy versions into a bundle evaluated together. The bundle is described by a **manifest** — an ordered list of `policy_version_id` entries. The STS computes a `manifest_sha256` over the manifest content to cache invalidation.

Creating a policy set version:

```
POST /zones/{zoneId}/policy-sets/{id}/versions
{ "manifest": [{ "policy_version_id": "<id>" }, ...] }
```

**Activation**: A zone has one active policy set binding at a time. Activating a policy set version makes it the live enforcement bundle for all subsequent token exchanges in that zone.

```
POST /zones/{zoneId}/policy-sets/{id}/activate
{ "version_id": "<version_id>", "shadow_version_id": "<optional_id>" }
```

The optional `shadow_version_id` registers a shadow binding alongside the active one. The active binding determines all enforcement decisions; the shadow binding can be inspected for testing purposes.

## Runtime loading

The STS loads policies lazily per zone:

1. On first exchange for a zone, the STS queries the active policy set binding and compiles the Rego bundle into memory.
2. Compiled bundles are cached in an atomic `opaZoneState` per zone. Cache hits are identified by `manifest_sha256` — a bundle with the same SHA is not recompiled.
3. A background poll refreshes all known zones every 60 seconds as a safety net.
4. The Redis stream `caracal.policy.invalidate` is the fast path: the STS subscribes to this stream and reloads the affected zone immediately when a new binding is activated, bypassing the 60-second poll.

If policy compilation fails (syntax error, unsatisfiable rule, etc.), the STS installs the deny-all fallback for that zone. It does not crash. The error is visible in STS logs.

## Signaling step-up from policy

A policy can signal that it requires elevated authentication before allowing exchange. It does this by returning a `step_up_required` entry in `diagnostics`:

```text
result := {
  "decision": "deny",
  "evaluation_status": "complete",
  "determining_policies": ["payment-policy"],
  "diagnostics": [{"step_up_required": "mfa"}],
} if {
  input.resource.identifier == "resource://high-value-payments"
  not input.context.challenge_resolved
}
```

When the STS detects `step_up_required` in diagnostics and the exchange has not already resolved a challenge, it creates a step-up challenge and returns HTTP 401 with a challenge object. See [Step-Up Challenge](./step-up/) for the full flow.

## Next steps

- [Authority Model](./authority-model/) — how the STS uses policy results to make decisions
- [Principal and Application](./principal/) — what `input.principal` contains
- [Resource and Grant](./resource-grant/) — what `input.resource` contains
- [Step-Up Challenge](./step-up/) — signaling elevated authentication from a policy
