Skip to content

Composites

llms.txt lists every page for an agent

Cedar has no functions, modules or loops, so a repeated policy shape is copy-paste in .cedar text. A TypeScript factory writes it once.

These follow chant’s general composite resources pattern: a function returning declared resources, discovered like any other export.

import { OwnerCanManage, DenyByDefaultSet } from "@intentius/chant-lexicon-cedar";

“The owner of a thing may act on it” is the most-repeated shape in any policy set, and where the two classic mistakes get made: the when guard names an attribute the schema spells differently, or the grant is written wide and the scoping clause is forgotten.

import { ReadAction, WriteAction } from "@intentius/chant-lexicon-cedar";
export const docOwner = OwnerCanManage({
entityType: "App::Document",
actions: [ReadAction, WriteAction],
principal: "App::User",
});

Emits:

@id("doc-owner")
@composite("OwnerCanManage")
@scopedTo("App::Document")
permit (
principal is App::User,
action in [App::Action::"read", App::Action::"write"],
resource is App::Document
)
when { resource.owner == principal };
OptionDefaultNotes
entityTyperequiredEntityTypeName — schema-checked
actionsunconstrainedOne action emits ==, several emit in [ … ]
ownerAttribute"owner"The attribute holding the owner
principalunconstrainedA bare type string becomes is T; a full scope passes through
when—Appended after the ownership test
unless—Omitted entirely when not asked for
annotations—Merged over the generated ones; an explicit id wins

Leaving actions off produces a wide grant, so it has to be asked for by omitting the field rather than arriving by accident.

A guarded forbid and the permits it governs, returned from one call. The pattern teams write by hand is a forbid at the top of a file and a pile of permits under it with nothing tying the two together. Delete the forbid and the permits keep working, wider than anyone intended.

import { DeleteAction } from "@intentius/chant-lexicon-cedar";
const guarded = DenyByDefaultSet({
policies: [docOwner],
entityType: "App::Document",
actions: DeleteAction,
when: ['resource.classification == "confidential"'],
unless: ['principal == App::User::"archivist"'],
});
export const confidentialFloor = guarded.floor;
export const documentOwnerGrant = guarded.members[0];
ReturnedWhat it is
floorThe forbid policy
membersThe permits, unchanged, in the order given
all[floor, ...members]

when is required. An unguarded forbid overrides every permit in the set, so the result would authorize nothing, so the composite throws rather than emit it.

They return Declarable values like any other resource, so exporting them from a discovered file is all that is needed:

export const [floor, grant] = DenyByDefaultSet({ /* … */ }).all;

The floor is emitted first. Cedar’s evaluation is order-independent (a forbid wins wherever it sits), but a file that reads floor-first matches how the set is reasoned about.

chant