Decision Types
Every audit event records a decision — the outcome of evaluating the request against your policies.
Decision types
| Decision | Meaning |
|---|---|
| Allow | Request passed all policy checks and was forwarded to the upstream |
| Deny | Request was blocked by a policy or budget rule; the caller received an error. An outbound network call blocked by the network egress filtering proxy — either the always-on SSRF floor or an egress policy — also records a Deny, distinguished from MCP/LLM denials by a request type of egress in the audit API. A Cross-App Access (XAA) token issuance denied by policy at grant time similarly records a Deny, distinguished by a request type of xaa — this is a decision about minting a token, not about a tool call or LLM request made with one. A denial that offered the developer an inline exception records the same Deny, noting on the event that an exception-requestable block was shown — offering an exception does not soften the decision, and nothing is allowed unless a grant later exists |
| Audit | Request was allowed but flagged — typically by an audit-mode rule or anomaly detection |
| Warn | Request was allowed with a warning appended to the response |
Ask (warn-ask) | Request was paused and the user was asked to confirm inline; on confirmation the request proceeds and the event is recorded (appears as warn_confirm in the audit API). Produced by enforcement-mode: Ask (validationActions: [Warn] + proxy.nirmata.io/require-confirmation: "true", or the annotation value Ask/warn-ask). The confirmation prompt carries a signed, single-use token bound to the policy, the identity it was issued to, and a short window: a "yes" is only honored against the prompt that produced it, and only once, so a confirmation cannot be replayed on a later turn, in another conversation, or against a different policy. A declined confirmation is the one governance event in this flow that is not authenticated: a "no" is taken at face value with no token check — declining can only ever block the request — so the resulting Deny (policy warn-ask, policy confirmation declined by user) records that the client reported a decline, not proof that a prompt was ever issued. A client can therefore write that event, and 403 its own request, at will; it cannot use it to allow anything. |
Approve (warn-approve, shown as Pending) | Request is waiting for human approval in the Approvals queue (appears as require_approval in the audit API). Produced by enforcement-mode: Approve/warn-approve. Nothing is written while a call waits — a pending hold is tracked in Security → Approvals. The call's own disposition is recorded once, when the hold resolves, and what it says differs by call type. For MCP tool calls it is a single require_approval event whether the reviewer approved or not, so an approved tool call appears as require_approval rather than Allow. For LLM requests an approval records the normal event for the completed call, and a rejection or timeout records a Deny carrying the reviewer's reason — the developer sees a message naming the policy, repeating the policy's own text and the reviewer's note, and offering an exception where the block is exception-eligible, rather than a generic error. Token-count requests are blocked outright rather than held — see Human-in-the-loop. |
Approval granted / Approval denied (hitl_approved / hitl_denied) | A reviewer decided a held request. This is a record of the decision, naming the approver — separate from, and additional to, the call's own disposition above, so a held-and-resolved call produces both. Deliberately not recorded as Allow/Deny: an approver working through the queue must not be counted in deny rates, the per-identity risk score, or the admin digest's most-denied-user ranking. A rejected self-approval attempt also records hitl_denied. |
Blocked (response) (response_blocked) | A policy or content scan blocked the reply — after the request itself was already allowed through to the tool or model. Distinct from Deny, which blocks before the call is ever made: here the call ran, and what came back is what got blocked. The event's request-phase fields (Policy/Rule/Message) still describe the request, which was Allow; the matched policy, matched rule, and reason for the block live in the event's response fields instead, alongside what the content scan found and a capped preview of the response body. |
Upstream errors on an allowed call
An Allow decision means governance permitted the call — it does not mean the call actually succeeded. When an allowed MCP tool call or LLM request reaches its target and gets back a failure (a rejected tool result, an expired or invalid upstream API key, a rate limit, a provider outage), the event still records Allow — that was the real governance decision — but carries an Upstream error annotation with the failure detail. It's shown as a warning-colored badge in the Audit Log and Security Events views, separate from the Allow/Deny coloring, so a credential or availability problem downstream doesn't get mistaken for a policy denial (and vice versa).
When the request went through an upstream pool (preview), each failed attempt records its own Allow + Upstream error event naming which pool and which attempt (e.g. "attempt 1 of 3") failed, and the eventual outcome — success or final exhaustion — carries the same pool name and attempt number. A request that fails over is therefore visible as a short run of events for one call, not a single ambiguous row.
The proxy.nirmata.io/enforcement-mode annotation is case-insensitive and accepts either the short user-facing names (Deny, Approve, Ask, Warn, Audit) or their longer-form values (deny, warn-approve, warn-ask, warn, audit).
Policies written with the legacy annotation values enforcement-mode: require-approval or enforcement-mode: warn-confirm are still accepted and behave identically to Approve (warn-approve) and Ask (warn-ask) respectively — no action needed for existing policies.
Exception lifecycle
A second set of values tracks the exception workflow rather than the outcome of a call. They are written alongside call events, so a blocked-then-granted action leaves a trail across both tables.
| Decision | Recorded when |
|---|---|
exception_requested | A developer asked for an exception — from the Builder Portal, the API, or inline in the tool that was blocked (a reply to a denial, an aigov__request_exception tool call, or /request-exception). The event names the requester and the policy or model asked for. Filing a request changes nothing about the call that triggered it, which keeps its own Deny |
exception_granted | A request was approved and the grant became live |
exception_partial_approval | A multi-approver request escalated to co-sign — the grant is active while the remaining approvers review it |
exception_denied | A request was denied, by a single approver or any co-signer |
exception_revoked | An admin ended a still-active grant early |
exception_expired | A grant's expiry time passed |
allowed_by_exception | A call that a policy would otherwise have blocked was allowed because an active grant covered it. This is the payoff event — it is what a granted exception actually looks like in the audit log |
Quiet confidence
The Security Posture dashboard and the weekly admin digest surface Quiet confidence — the percentage of call dispositions resolved with zero human involvement.
Decisions are split into three sets:
| Set | Decisions |
|---|---|
| Quiet (no human involved) | Allow, Mutated, Cache Hit, Audit, Allowed by Exception |
| Human-involved | Deny, Blocked (response) (response_blocked), Warn Approve (require_approval), Warn Ask (warn_confirm). Egress denials are included via Deny — they record a Deny with request type egress, not a separate decision |
| Excluded from both | Workflow rows — the exception lifecycle (exception_requested, exception_granted, exception_partial_approval, exception_denied, exception_revoked, exception_expired) and approval decisions (hitl_approved, hitl_denied) |
Quiet confidence is quiet ÷ (quiet + human-involved), computed over the period window selected on the Security Posture dashboard. Exception-lifecycle rows are workflow bookkeeping, not call dispositions, so they never appear in the denominator — this makes the metric deliberately narrower than total call volume.
Synthetic policy names
Every audit event has a Policy field naming the rule that produced the decision. Most values are the name of a CEL policy you (or the built-in library) authored. A few reserved names come from proxy-level checks that aren't backed by an editable policy:
| Policy name | Decision | Fires when |
|---|---|---|
unverified-identity | Deny | identity.requireVerifiedIdentity is enabled and the request's identity was not cryptographically or credential-verified |
mcp-ssrf-guard | Deny | The always-on SSRF floor blocked an MCP tool call whose URL argument resolved to a private, loopback, or internal address |
egress-warn-fallback | Deny | networkFilter.warnFallback: deny is set and an outbound HTTP(S) call matched no egress policy that gave a definitive allow or deny |
upstream-format-mismatch | Deny | The resolved upstream's wire format is confidently known to differ from the request's (e.g. an Anthropic Messages client routed to an OpenAI- or Gemini-family upstream, or vice versa) and no format-conversion adapter exists for that direction yet |
subscription-concurrency-cap | Deny | An agent tried to establish an MCP resource subscription (resources/subscribe, or one entry in a subscriptions/listen call) while already holding the maximum of 10 concurrent subscriptions |
subscription_rate_exceeded | Deny | A single MCP resource subscription pushed more than 50 update events per second; the individual event over the limit is dropped (the subscription itself stays open) |
Access Binding decisionMode values map onto these same decisions — Allow, Audit, and Warn behave exactly as above; RequireApproval produces Held; Deny blocks the request (invalid_grant at token issuance, or denied at the tool call).