Permit / defer / deny / modify / step-up
The five decision outcomes: permit, defer, deny, modify, step-up.
Every governed tool call resolves to exactly one outcome. What you see at the terminal:
>>> agent calls calc({'expression': '21 * 2'})
<<< result: 42
[raxit PENDING] 'post_to_webhook' deferred — waiting for human approval (up to 120s).
Token: dt_1373bb3e0bcf139d
Approve: raxit approvals approve dt_1373bb3e0bcf139d
Deny: raxit approvals deny dt_1373bb3e0bcf139d
<<< [raxit BLOCKED] 'post_to_webhook' denied (reason: deny.approval_expired). The operation was
not executed. Details: raxit explain d867c63661454e8497369a08ebaec35dThe first call ran silently — that's permit. The second suspended and printed a token — that's
defer. Left unapproved for 120s, it resolved to a hard [raxit BLOCKED] — that's deny.
The vocabulary is permit | defer | deny — one, not two
Cedar's native decision is binary: Allow or Deny. raxit needs a third state — a call that's
neither permitted nor denied, but suspended pending a human. It produces that third state with a
Cedar policy annotation, not a second policy language: a contributing permit can carry
@effect("defer") or @effect("step_up"), and the engine reads that annotation to resolve the
final decision. There is exactly one decision vocabulary end to end — permit | defer | deny —
never a second "outcome/status" split.
The resolution chain
1. Authorize the normalized tool call with Cedar → (Decision, Diagnostics)
2. Cedar errors → deny (fail closed, never fail open)
3. Decision == Deny (forbid matched, or nothing matched)
→ deny
4. Decision == Allow:
contributing permit has @effect("step_up") → defer (STEP_UP)
contributing permit has @effect("defer") → defer (DEFER)
contributing permit has @effect("modify") → permit + transform, or deny if the
transform doesn't parse
otherwise → permitThe five outcomes
security.yaml | Cedar annotation | Decision.Effect | Decision.Type | Reason code |
|---|---|---|---|---|
effect: allow | (none) | Permit | ALLOW | permit |
effect: review | defer (step_up only with a when: guard) | Defer | DEFER (STEP_UP with when:) | defer.policy (defer.step_up with when:) |
effect: block | forbid | Deny | DENY | deny.default_or_forbid |
modify: {...} | modify + transform | Permit (or Deny if transform invalid) | MODIFY (or DENY) | modify.policy (or deny.modify_error) |
| Cedar eval error | — | Deny | DENY | deny.eval_error |
effect: review is generic defer by default — step_up only with a when: guard. A plain
effect: review rule (no when:) compiles to a Sensitive tool, @effect("defer"), reason
defer.policy — the same generic DEFER/defer.policy type used for non-rule defers (budget,
cost, and context-completeness suspensions — e.g. defer.budget_exceeded,
defer.context_incomplete). Add a when: condition to a review rule and it compiles instead to
@effect("step_up"), reason defer.step_up, Decision.Type == STEP_UP: "a step-up rule requires
explicit human approval for this call." Both land in the same coarse defer bucket when you
only need expect: defer in a test; use expect_type: STEP_UP vs
expect_type: DEFER when you need to tell them apart.
modify is permit-with-transformation, not a fourth effect
- resource: send_email
modify:
redact: [body, subject]A modify: rule still permits the call — the tool runs — but with the declared transform
applied to the named argument paths first (redact, hash, or drop). If the transform can't
be applied, the call fails closed to deny.modify_error rather than silently forwarding the
original, untransformed arguments. See Rules & effects for the full
modify: grammar.
Fail-closed at every step
Notice step 2 in the resolution chain above: a Cedar evaluation error is a deny, never a
silent pass-through. This is one instance of a rule that holds everywhere in raxit — see
Fail-closed & degrade-closed for the general principle.
Demo: automatic PII redaction
A send_email carrying a raw card number is rewritten in-flight: the engine returns MODIFY with redacted args, the tool runs on those, and the PII never leaves — a real, executed demo.
Taint & the lethal trifecta
Cross-call taint tracking: how raxit breaks the lethal trifecta and stops prompt-injection-driven data exfiltration.

