Gate an agent's changes by role
Ask aontu allow whether a role may change a subtree before aontu model set writes it, with the answer read from a role model that is itself an aontu document.
Rendered from
docs/how-to/gate-changes-by-role.md
in the engine repository, where a correction belongs, and where the
test suite executes every example on this page.
An agent that edits a model under a role (dev, product, an
on-call rotation) has to be told no before it writes, and the rules
that say no should live in a document the same tools read. Write
roles.aon:
roles: dev: {
allow: ["$.services" "$.deploy.*.replicas"]
deny: ["$.services.*.tier"]
}
roles: product: allow: ["$.features" "$.services.*.description"]
Each role names the subtrees it may change and, optionally, the ones
it may not; * stands for any one key. Now ask, with the very
assignment the agent is about to hand set:
$ aontu allow --role dev roles.aon '$.services.auth.tier="standard"'
verdict: refused
role: dev
$.services.auth.tier: refused by $.roles.dev.deny.0 ($.services.*.tier)
$ echo $?
1
The path is what is judged, and the value only has to be one value: a
second pair inside it would write a subtree the gate was not asked
about, so such a call is refused as usage. The answer names the rule
as a path into the role model, so aontu model why '$.roles.dev.deny.0' roles.aon lists who wrote it and where. Ask again for a path the
role does cover:
$ aontu allow --role dev roles.aon '$.services.auth.replicas=4'
verdict: allowed
role: dev
$.services.auth.replicas: allowed by $.roles.dev.allow.0 ($.services)
Exit 0 is the cue to write. Given a model.aon whose replicas are a
default:
services: {
&: { tier: *standard|string replicas: *1|integer description?:string }
auth: tier: premium
billing: {}
}
features: dark_mode: false
the same assignment goes to aontu set unchanged:
$ aontu model set '$.services.auth.replicas=4' --entry model.aon --overlay overlay.aon
verdict: valid
wrote: overlay.aon
The gate and the write take one spelling, so a skill runs the first
and, on exit 0 alone, the second. Two answers are easy to misread. A
deny entry refuses the paths above it as well as the ones below: dev
may not change $.services.auth either, because a change there could
rewrite the tier, and asking about the parent is how an agent that
plans to replace a whole service finds that out. And a call that lost
its paths, or hands an empty one, is a usage error (exit 2), never
allowed, so a script that branches on exit 0 cannot be let through
by an empty question.
The rule in full, the --at and --format json flags, and the shape
every role must satisfy are under aontu allow. The live version is
use-cases/18-role-permissions,
where a closed role vocabulary governs a service model, an agent skill
asks before every write, and the proposals that touch a denied subtree
are refused with the rule named.