Author Policy Data
Use this guide after resources and applications exist and before activating production access.
Why data, not decisions
Section titled “Why data, not decisions”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.
Prerequisites
Section titled “Prerequisites”- 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.
Start from a grant
Section titled “Start from a grant”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-documentpackage 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.
Gate scopes on human approval
Section titled “Gate scopes on human approval”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-documentpackage 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.
Start from a template
Section titled “Start from a template”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')| Template | Use it for |
|---|---|
application-bindings | Map each application key used in grants to its control-plane application id. |
resource-grants | Declare the owning application and per-role scope sets for a resource view. |
label-confinement | Cap every session carrying a label prefix to a fixed scope set. |
zone-restriction | A 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.
How your data maps to the request
Section titled “How your data maps to the request”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 reads | Resolved against |
|---|---|
input.principal.id | app_ids - to find the application key used in grants. |
input.principal.labels | grants[...].roles and confinement label prefixes. |
input.resource.identifier | the top-level key in grants. |
input.context.requested_scopes | the role’s scope set and any matching confinement rule. |
input.delegation_edge.scopes | the narrowing floor every requested scope must sit inside. |
Validate before versioning
Section titled “Validate before versioning”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.
Preview how the document parses
Section titled “Preview how the document parses”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.
Iterate from a denied request
Section titled “Iterate from a denied request”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.
Keep policies reviewable
Section titled “Keep policies reviewable”- Default to deny.
- Keep resource identifiers stable and scopes action-oriented.
- Keep grant data normalized: one
grantsentry 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.
Validate the authored data
Section titled “Validate the authored data”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.
Next Step
Section titled “Next Step”Activate a Policy Set with the allow and deny simulations used here.

