Skip to content

Author Policy Data

Use this guide after resources and applications exist and before activating production access.

Authorization logic - delegation narrowing, role and grant checks, label confinement, bootstrap isolation - is identical for every adopter and is the part most dangerous to get wrong. A single typo in a hand-written rule (if { false }if { true }) silently turns a deny into an allow-all. Caracal removes that footgun by owning the logic in a signed, versioned platform decision contract and letting you supply only data.

Every policy you author is a data document marked with # caracal:data-document on its first line. It carries only data tables and is forbidden from defining result, so it can never decide an authorization on its own. restrict entries can only subtract authority and confinement can only narrow it - a careless data change fails closed.

  • A zone, application, and resource.
  • Access to the web console or Admin API.
  • The resource identifier and scopes you want to grant.
  • The real application ID and the Session labels the application emits.
  • Representative allow and deny inputs for simulation.

The core document is grants: it names the application that owns a resource view and the scopes each role may hold. Pair it with app_ids, which binds the application key you use in grants to the control-plane id the STS sees as input.principal.id.

# caracal:data-document
package caracal.authz
import rego.v1
app_ids := {
"pipernet": "app-pipernet",
}
grants := {
"resource://pipernet": {
"application": "pipernet",
"roles": {"reader": ["pipernet:read", "pipernet:write"]},
},
}

This is the canonical “application A may call resource B with scopes C” pattern, expressed as data. The platform contract allows a mint only when the acting application owns the view, the agent’s role label grants the scope, and the Delegation narrows to it. You declare the grant; the contract enforces the narrowing. Several app_ids keys may bind to one application id - the contract treats them as one identity - but one binding key per application keeps grant review straightforward, and the Admin SDK’s grant helpers author exactly that.

Two further documents add an optional human gate on top of a grant. risk names a tier for each sensitive scope, and approval_tiers declares which tiers hold the mint until a person decides it:

# caracal:data-document
package caracal.authz
import rego.v1
risk := [
{"scope": "pipernet:refund", "tier": "high"},
]
approval_tiers := [
{"tier": "high", "approver": "operator", "ttl_seconds": 1800, "privacy": "identified"},
]

Like restrict and confinement, an approval declaration can only add a gate, never widen authority, and a malformed declaration fails the gated mint closed. Human Approval covers the tier fields, both decision planes, and the agent-side wait-and-retry flow.

You do not have to write each document by hand. Caracal ships a built-in catalog of data-document starters, served at /v1/policy-templates and through the Admin SDK:

import { AdminClient } from '@caracalai/admin'
const admin = new AdminClient({
apiUrl: process.env.CARACAL_API_URL!,
adminToken: process.env.CARACAL_ADMIN_TOKEN!,
})
const templates = await admin.policyTemplates.list()
const starter = await admin.policyTemplates.get('resource-grants')
TemplateUse it for
application-bindingsMap each application key used in grants to its control-plane application id.
resource-grantsDeclare the owning application and per-role scope sets for a resource view.
label-confinementCap every session carrying a label prefix to a fixed scope set.
zone-restrictionA deny overlay that freezes the zone while an entry is present.

For assisted authoring, describe the outcome to the Caracal Operator. Its policy author models the use case as grant, binding, and confinement data, validates and previews each document against the platform contract, and proposes a governed create you review and approve - so the policy that lands is already contract-valid.

The decision contract evaluates a fixed input contract and resolves it against your data. The acting application is the principal - there is no input.application or input.grant object. See the full Policy Input Contract for every field.

Input the contract readsResolved against
input.principal.idapp_ids - to find the application key used in grants.
input.principal.labelsgrants[...].roles and confinement label prefixes.
input.resource.identifierthe top-level key in grants.
input.context.requested_scopesthe role’s scope set and any matching confinement rule.
input.delegation_edge.scopesthe narrowing floor every requested scope must sit inside.

Use the web console policy workflow to paste the document and run validation. For automation, validate through the Admin API or @caracalai/admin:

import { AdminClient } from '@caracalai/admin'
const admin = new AdminClient({
apiUrl: process.env.CARACAL_API_URL!,
adminToken: process.env.CARACAL_ADMIN_TOKEN!,
})
const validation = await admin.policies.validate(policySource)
if (!validation.valid) {
throw new Error('policy failed validation')
}

Validation enforces the data-document contract: the package must be caracal.authz, the first line must carry the # caracal:data-document directive, the document must define at least one data rule, and it must not define result. Validation also checks the schema version, balanced syntax, and forbidden built-ins. Because the platform contract owns the decision, a data document can never authorize on its own.

A successful validation returns a preview describing exactly what the engine parsed, so you can confirm the backend reads your data the way you intend before activating it:

const { preview } = await admin.policies.validate(policySource)
// preview = {
// package: "caracal.authz",
// rules: ["app_ids", "grants"], // the data documents you defined
// default_result: false, // data documents never define result
// decisions: [], // the platform contract owns every decision
// inputs_referenced: [],
// data_referenced: [],
// }

Use rules to confirm the document defines the data tables you intended. The preview is a static read of the source; for an end-to-end decision run a simulation with representative input against the platform decision contract.

When a real request is denied, you do not have to guess the input: the audit explain endpoint reconstructs a redaction-safe policy input for every denied decision, and you replay it against a candidate policy-set version before activating the fix. The workflow, snippet, and caveats live in Iterate from real denials; Iterate Policy Safely automates the whole loop.

  • Default to deny.
  • Keep resource identifiers stable and scopes action-oriented.
  • Keep grant data normalized: one grants entry per resource view, rather than duplicating the same scope sets across documents.
  • Split policies by ownership only when separate review or activation is useful.

Next, activate the policy in a policy set.

Validation must return valid: true; preview must list only intended data rules; simulation must allow the intended role and deny a missing role, extra scope, wrong resource, and confinement escape. Syntax validity alone is not production readiness.

Activate a Policy Set with the allow and deny simulations used here.