Deny-overrides
Warrant combines every can and cannot — across all rules, including
implicit rules — with deny-overrides.
For a given ability the compiled predicate is:
predicate(ability) = ( OR of each `can` rule's if-expression ) AND ( AND of NOT(each `cannot` rule's if-expression) )Because it’s a symmetric AND/OR combination, rule order never matters. You can merge rules from a resolver, implicit rules, and multiple clauses in any order.
The hard edges
Section titled “The hard edges”| Situation | Compiles to | Meaning |
|---|---|---|
Unconditional cannot |
AND NOT(true) → 1 = 0 |
This user can never have the ability, on any row |
No can rule for the ability |
1 = 0 |
Denied by default — silence is not permission |
Unconditional can |
1 = 1 term |
This user has the ability on every row |
Conditional cannot |
AND NOT(condition) |
Subtracts matching rows from the grant |
A cannot is an absolute veto
Section titled “A cannot is an absolute veto”No can rule can bring back an ability a cannot denies. This is what makes the
“kill switch” idiom work:
they can * # grant everything...if is_suspendedthey cannot * # ...but a suspended user loses all of itSince the combination is order-independent, an implicit cannot beats any
resolver-supplied can:
protected function implicitRules(): array{ return [ WarrantRule::fromSyntax('if is_suspended they cannot *'), ];}“Unless” is just and not
Section titled ““Unless” is just and not”Exceptions read naturally as a guard on the deny:
if is_self or manages_team they can updateif is_locked and not is_admin they cannot updateif is_admin they can *The middle line — “never once it’s locked, unless they’re an admin” — is a
cannot update guarded by and not is_admin. No early returns, no ordering
tricks.
Silence denies — a common surprise
Section titled “Silence denies — a common surprise”If no rule mentions an ability with a can, that ability is denied, full stop.
This trips people who expect an unlisted ability to be “allowed by default.” It
isn’t. Every ability a user should have needs a matching can.
Interaction with missing context
Section titled “Interaction with missing context”There’s one way deny-overrides can surprise you: an optional @context key
that’s absent at check time makes its condition unevaluable → treated as false.
On a can that’s safe (no key, no grant). But on a cannot, a false condition
means the veto doesn’t apply — the deny silently lifts (fail-open). That’s
exactly why any context key gating a cannot should be declared required. See
Check-time context.
