Skip to main content

Decision Types

Reference

Every audit event records a decision — the outcome of evaluating the request against your policies.

Decision types

DecisionMeaning
AllowRequest passed all policy checks and was forwarded to the upstream
DenyRequest 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
AuditRequest was allowed but flagged — typically by an audit-mode rule or anomaly detection
WarnRequest 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).

note

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.

DecisionRecorded when
exception_requestedA 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_grantedA request was approved and the grant became live
exception_partial_approvalA multi-approver request escalated to co-sign — the grant is active while the remaining approvers review it
exception_deniedA request was denied, by a single approver or any co-signer
exception_revokedAn admin ended a still-active grant early
exception_expiredA grant's expiry time passed
allowed_by_exceptionA 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:

SetDecisions
Quiet (no human involved)Allow, Mutated, Cache Hit, Audit, Allowed by Exception
Human-involvedDeny, 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 bothWorkflow 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 nameDecisionFires when
unverified-identityDenyidentity.requireVerifiedIdentity is enabled and the request's identity was not cryptographically or credential-verified
mcp-ssrf-guardDenyThe always-on SSRF floor blocked an MCP tool call whose URL argument resolved to a private, loopback, or internal address
egress-warn-fallbackDenynetworkFilter.warnFallback: deny is set and an outbound HTTP(S) call matched no egress policy that gave a definitive allow or deny
upstream-format-mismatchDenyThe 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-capDenyAn 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_exceededDenyA 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)
note

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).

See also