Skip to content

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.

  • A zone, application, and resource.
  • Access to the Console or Admin API.
  • The resource identifier and scopes you want to 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-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 edge narrows to it. You declare the grant; the contract enforces the narrowing.

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 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.

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-document
package 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.

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:

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.

  • 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.

Next, activate the policy in a policy set.