Are you an LLM? You can read better optimized documentation at /docs/agent-guard/policies/evaluation-and-ordering.md for this page in Markdown format
Evaluation and ordering
One policy on its own is easy to reason about: the rules run, and if any assertion is false the action fires. Load a second policy, or add a pre-built tier alongside your own, and order starts to matter. This page covers how the engine picks policies, what each action does when a rule fails, and the two cases where ordering changes the outcome.
How a request is evaluated
For each request the engine selects every policy of the matching kind whose match block applies, sorts them alphabetically by metadata.name, and evaluates them in that order. Within a policy, every rule is evaluated; if any rule's assertion returns false, the policy's action fires.
Sorting is by name, not by file, directory, or the order sources were added. Two policies from different bundles interleave according to their names.
| Action | All rules pass | A rule fails |
|---|---|---|
Enforce | continue | Violation collected. The request is denied after all policies have run |
Audit | continue | Violation logged. The request continues |
Elicit | continue | Short-circuits immediately. Marked for operator approval |
Redact | continue | Short-circuits immediately. Content masked |
Enforce deliberately does not short-circuit. Every applicable policy runs, and every denial is collected, so an operator sees all the reasons a request failed rather than whichever one happened to sort first.
Fail-closed, including in Audit
An evaluation error denies the request. This holds inside an Audit policy too: an Audit rule that throws does not log and continue, it denies. A rule that crashes never becomes a rule that passes.
The practical consequence is that Audit is safe for deciding nothing, but not automatically safe for touching anything. An Audit rule reading a field that might be absent still blocks the request when that field is missing. Guard fields in audit rules exactly as you would in enforcing ones — see Writing rules.
Why Elicit and Redact stop evaluation
The engine returns one decision per request, and the four actions differ in whether they can still be combined with another one afterwards.
Enforce and Audit can. Both leave the request exactly as it was: a denial adds a reason, an audit entry adds a log line, and neither changes the data any later policy will look at. Collecting more of them only gives the operator a fuller picture, which is why Enforce keeps going and reports every violation rather than the first.
Redact cannot, because it changes the data. Once a redaction is decided, the content that will actually be sent is not the content in the context the engine is holding. A policy evaluated after that point would be judging text that is about to be rewritten — reaching a verdict about a version of the request that will never exist. Judging it against the redacted version instead is not possible either without re-running the whole evaluation against a new context. So the engine stops and returns what it has.
Elicit cannot, for the mirror-image reason: it suspends the request pending a human decision whose outcome is not known yet. There is no verdict to combine with, because whether this request proceeds at all is still open.
That is the whole reason for the ordering convention below. Enforce and Audit are safe to run early precisely because they are additive; Redact and Elicit belong last because they are the two that end the evaluation.
Three consequences worth designing around.
An audit policy that sorts last won't record anything for elicit or redact requests. The calls most worth reviewing are the ones that fire an interactive or content-modifying action, and those are the ones a late-sorting audit policy misses.
An Elicit policy can suppress an Enforce policy from another bundle. If your policy sorts before a tier's Enforce policy and fires, the tier's policy never runs, and its specific message never reaches the operator. You get your generic prompt instead of the tier's precise reason.
A Redact short-circuit returns allowed: true. Redaction returns success even when an Enforce policy earlier in the same request has already failed — the enforce violations are attached to the response, but the decision is allow. Content that should have been blocked is masked and forwarded instead.
The naming convention
Because ordering is alphabetical and the two data-changing actions end evaluation, policy names are load-bearing. Group them by action with a sortable prefix, additive actions first:
| Prefix | Action | Why there |
|---|---|---|
00- | Audit | Runs first, so every request reaches the audit record, including ones later elicited or redacted |
01- … 06- | Enforce | Denials are always collected, and running before the short-circuiting actions keeps them that way |
z1- … z3- | Elicit, Redact | Cannot pre-empt an Enforce policy, including one from a bundle loaded alongside yours |
Getting this wrong fails quietly rather than loudly. Nothing errors; the audit trail is simply missing its most interesting entries, or an operator sees a vague prompt where a specific denial was expected.
The Redact escape clause
Ordering alone does not fix the allowed: true problem, because a Redact policy that sorts last still returns allow when it fires after an Enforce violation was collected.
Every rule in a redaction tier therefore needs an escape clause: if blocking-tier content is also present, the rule does not fire, and the Enforce denial stands.
yaml
apiVersion: gerty.jozu.dev/v1
kind: GuardrailPolicy
metadata:
name: z1-contact-and-demographic-detail
spec:
guardrails:
- "*"
direction: both
action: Redact
rules:
- name: mask-phone-numbers
assert: |
!guardrail.findings.exists(f,
f.category in ["body", "file", "json"] &&
f.message.matches("[(]?[0-9]{3}[)]?[- .][0-9]{3}[- .][0-9]{4}")
) ||
guardrail.findings.exists(f,
f.message.matches("[0-9]{3}[-][0-9]{2}[-][0-9]{4}")
)
message: "Phone number masked before the request left the boundary."The second clause is the escape: when a Social Security number is present too, the assertion passes, this rule does not fire, and whichever Enforce policy matched the SSN denies the request. Without it, a message containing both would be masked and allowed.
Copy the pattern into any Redact policy you write alongside Enforce policies.
Composing bundles
Policy sources are additive. Adding a bundle does not replace what you already have, so a tier plus your own policies means both are loaded and both evaluate.
Before adding a second source, check two things:
- Where its policy names sort relative to yours, especially if either uses
ElicitorRedact - Whether its actions overlap with yours on the same operations, which usually means one of them never gets to speak
agentguard policy list shows the sources you have loaded. The bundle contents are the authoritative list of what is in each.
