Author Policy Data
The platform decision contract owns every authorization decision inside the STS. You do not write result — you author policy data documents that tell the contract which application owns a resource, which roles hold which scopes, and how to confine or restrict authority. This guide writes those documents and validates them before activation.
Prerequisites
Section titled “Prerequisites”- A zone, application, and resource.
- Access to the Console or Admin API.
- The resource identifier and scopes you want to grant.
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 edge narrows to it. You declare the grant; the contract enforces the narrowing.
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 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.
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 is a data document marked with # caracal:data-document on its first
line. The document carries only data and is forbidden from defining result,
so it can never decide an authorization:
# caracal:data-documentpackage caracal.authz
import rego.v1
restrict := {}A restrict entry can only subtract authority, and confinement can only narrow it.
Neither can widen what the contract already allows, so a careless data change fails
closed.
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:
const trace = await admin.audit.explain(zoneId, requestId);const input = trace.denied[0]?.policy_input;Feed that input straight into simulation against
a candidate policy-set version to confirm your fix before activating it. The
Iterate Policy Safely automates this denial-to-simulation loop.
Actor and subject claims are never written to audit, so add any claim-dependent
fields to the input before simulating.
Keep policies reviewable
Section titled “Keep policies reviewable”- Default to deny.
- Return diagnostics for denies and step-up triggers.
- Keep resource identifiers stable and scopes action-oriented.
- Keep decision logic in policy rather than duplicating allow tables across rules.
- Split policies by ownership only when separate review or activation is useful.

