
Failure behavior
Fail-Closed Agent Authorization: When the Decision Can't Be Made
Timeouts, unreachable checks, unknown scopes and missing annotations all produce the same question. OWASP's Agent Control Standard ships a fail-open default, and the reasoning is worth reading closely.
TL;DR
Every authorization system has a third outcome besides allow and deny: no answer. Most agent stacks have never written down what they do with it, and the one open standard that has written it down chose to proceed.
OWASP's Agent Control Standard is explicit. A step suffers a decision failure "when no usable decision
arrives within the negotiated timeout", and all such cases "resolve the same way: the Observed Agent
applies the deployment's failure posture, declared as on_decision_failure in the handshake and defaulting
to proceed (fail-open) so that a slow, erroring, or unreachable Guardian does not halt production."
The usual mistake is not choosing fail-open. It is never choosing, and discovering the default during an outage. ACS at least states the trade in its own text: "an adversary who can disrupt the channel converts control into audit."
We are not arguing every deployment should deny on every ambiguity. A blanket deny on an unreachable check turns a network blip into an outage, which is why ACS defaults the way it does.
What we would argue is narrower. Write the posture down per decision class, make the timeout bounded and short, and make the denial recoverable where the protocol gives you a way to say what was missing.
Overview
Take a benefits administration agent at a hospital group. It handles employee life-event changes: a new dependent, a plan switch, an address correction that changes tax withholding. Every action runs through an authorization check that asks whether this agent, acting for this HR coordinator, may modify this employee's record.
One Tuesday the service behind that check gets slow. Not down, slow, in the way that a connection pool exhaustion makes things slow: most calls return in 40 milliseconds and some take nine seconds. The agent is mid-run on a batch of 300 changes. What does it do with the ones it cannot get an answer for?
There are exactly two answers and both are defensible. What is not defensible is not knowing which one your stack picked. Agent authorization vs. authentication covers the 401 and 403 split, and permission models for autonomous systems covers how much initiative an agent gets once it is allowed in. This post is about the third branch, the one nobody draws: the decision that never arrives, or arrives in a form your code does not understand.
The thing to write down: for each class of decision failure, the posture and who chose it. "We fail closed" is a claim about code, and in our reading of the available specifications it is usually a claim about code that was never written.
Four Ways to End Up Without an Answer
"Fail closed" gets used as though decision failures were one thing. They are at least four, and they do not want the same treatment.
The check times out or is unreachable. The authorization service is slow, the connection is refused, the response is malformed. This is the availability case, and it is the one where fail-open has a real argument: a transient network condition should probably not stop payroll.
A scope or grant is unrecognised. The identity provider added a group, a vendor renamed a scope, your mapping table has an entry the other side has never heard of. Here the system is not broken and nothing is slow. It simply cannot tell whether the action is in the intersection, which is the subject of agent scope and user permission.
Metadata the decision depended on is absent. A tool arrives with no annotation saying whether it writes. A resource carries no classification. The decision procedure has a hole in its inputs rather than a missing output.
The decision arrives but cannot be applied. The authorizer says "allow this, with these arguments rewritten" and the agent framework has no way to rewrite arguments mid-flight.
Those four want different defaults. Collapsing them into one global setting is how a system ends up permissive in exactly the case it least wanted to be, because the setting was chosen with the timeout case in mind.
Figure 1 — The branch most architecture diagrams omit. The posture lookup is per failure class, not global, and the audit record has to name which posture applied or the bypass is invisible afterwards.
What OWASP's Agent Control Standard Actually Defaults To
The Agent Control Standard, an open project of the OWASP GenAI Security Project, is the sharpest public artifact on this that we have found, because instead of recommending a posture it specifies one and then argues with itself about the choice in the normative text. It models a Guardian that rules on each step an Observed Agent proposes, over a negotiated handshake.
Its disposition vocabulary has five values: ALLOW, DENY, MODIFY ("proceed with changes"), ASK
("pause and request approval") and DEFER ("verdict not yet reachable"). DEFER is itself a decision:
the Guardian saying it cannot decide yet, with a reason drawn from a fixed list that includes
insufficient_context and conflicting_policies. Silence is a different thing entirely.
Silence is handled in section 6.4. "A step suffers a decision failure when no usable decision arrives
within the negotiated timeout: the Guardian stays silent, the transport fails (connection refused, TLS
failure, malformed response), or the Guardian returns an error instead of a decision." All three "resolve
the same way: the Observed Agent applies the deployment's failure posture, declared as on_decision_failure
in the handshake (§4) and defaulting to proceed (fail-open) so that a slow, erroring, or unreachable
Guardian does not halt production. A deployment MAY set on_decision_failure: deny (fail-closed)."
The reference implementation makes the default concrete as an environment variable. Its configuration table
lists ACS_ON_DECISION_FAILURE with a default of proceed, described as "the posture the ServerHello
declares. deny fails closed. Any other value stops the Guardian at startup."
Two further details are worth having. First, the standard will not let the bypass be quiet: "Every step that proceeds without a decision MUST be recorded as an audit event, so the bypass is visible rather than silent." Second, it states the cost of its own default in one sentence: "Fail-open trades enforcement for availability under disruption: an adversary who can disrupt the channel converts control into audit."
Read that sentence as a threat model, not a caveat. If disruption is cheap for an attacker and your posture is proceed, your authorization layer is a logging layer with extra latency.
The asymmetry inside the standard is the most instructive part. An expired DEFER defaults the other way:
"timeout_decision defaulting to deny is deliberately the opposite of on_decision_failure defaulting to
proceed (§6.4). An expired DEFER is a Guardian that flagged a concern and failed to resolve it, not one that
never answered: a degraded Guardian that can still emit DEFER fails closed where a silent one fails open."
So the standard already does what we argued for above: it splits decision failure into classes and picks a
different posture per class. A Guardian that said "I have a concern" and then timed out blocks. A Guardian
that said nothing at all does not. There is also a startup posture, because the handshake cannot govern
itself: when handshake/hello fails, the agent applies a posture configured out of band, where "proceed
starts the session unguarded, refuse does not start it", and "the default is proceed". A deployment
that sets on_decision_failure: deny and leaves the startup posture alone has a fail-closed session and a
fail-open way of never having a session.
Approval as a disposition, including what it costs in human attention, is covered separately in human-in-the-loop approval design.
Unknown Scopes and Missing Hints
The second and third failure classes are quieter and, we think, more dangerous, because they produce no error anywhere.
For scopes, the rule follows from the intersection: an unmatched grant contributes nothing. If the acting
user's token carries reports:export:v2 and the agent's configuration knows only reports:export, the
correct answer is that the overlap for that action is empty. The tempting alternative is prefix matching,
which quietly turns every future scope a vendor invents into a grant you already made.
For metadata, MCP's 2026-07-28 specification carries the sharper warning. A tool's annotations field is
listed as "Optional properties describing tool behavior", and the spec attaches a normative caution: "For
trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come
from trusted servers."
That sentence does two things at once, and both matter for failure behavior. It makes a missing annotation and a present annotation equally uninformative when the server is not trusted, which means a decision procedure keyed on annotations has no inputs at all in the untrusted case. And it puts the burden on the client, not the server, so "the tool said it was read-only" is not an answer anyone will accept afterwards. The annotation defaults and what they are good for are covered in permission models for autonomous systems; the point here is narrower. An annotation is a hint for building an interface and prioritising review. It is not an input to an authorization decision, so a decision procedure that stalls when one is absent was reading the wrong field in the first place.
OWASP's general design guidance says the same thing in one line, outside any agent context: "Design systems to fail in a secure state, rather than exposing vulnerabilities when they malfunction."
The Recoverable Denial
Fail-closed has a reputation problem because people imagine the deny as a dead end. The HTTP layer has carried a better pattern for fourteen years, and MCP has now specified how agents should use it.
RFC 6750 defines insufficient_scope: "The request requires higher privileges than provided by the access
token. The resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the
scope attribute with the scope necessary to access the protected resource." A denial that names what was
missing is a different object from a denial that does not. The first one is a protocol message. The second
is a support ticket.
MCP's authorization spec makes that concrete for agents. On an insufficient-scope error at runtime the
server SHOULD return HTTP 403 Forbidden with a WWW-Authenticate header carrying
error="insufficient_scope", the scope parameter "specifying the minimum scopes needed for the
operation", and the resource metadata URI. Clients "SHOULD respond to these errors by requesting a new
access token with an increased set of scopes via a step-up authorization flow", computing "the union of the
client's previously requested scope set and the scopes from the current challenge" so previously granted
permissions survive the re-authorization.
The spec is also clear about who may take that route. "Clients acting on behalf of a user SHOULD
attempt the step-up authorization flow. Clients acting on their own behalf (client_credentials clients)
MAY attempt the step-up authorization flow or abort the request immediately." An autonomous agent with
no user attached has nobody to ask, which is a good reason to prefer the on-behalf-of shape described in
what on-behalf-of authorization actually proves.
RFC 9470 extends the same mechanism one layer deeper, for the case where the problem is not the scope but
the authentication behind it. Its error value is insufficient_user_authentication, defined as: "The
authentication event associated with the access token presented with the request does not meet the
authentication requirements of the protected resource." With acr_values and max_age in the challenge, a
resource server can say "this one needs a stronger or fresher sign-in" rather than just no.
Figure 2 — Two different denials in one run. The first has no information in it and is bounded by a budget; the second names exactly what was missing and is therefore recoverable without a human.
A Fail-Closed Engine, Concretely
Abstract postures are easy to agree with, so here is one in a shipped system. mnemiq is our open-source text-to-SQL engine, and its access behaviour is fail-closed by construction rather than by configuration.
Its own description of the ordering: a model proposes a query, and a deterministic layer then "rules on it
before anything touches the database. The query has to be read-only, may only reference objects the caller
is allowed to see, has to compile in the source's own SQL dialect, and has to survive an EXPLAIN. Fail any
of those and you get a refusal with a stated reason rather than a plausible number."
The part that matters for failure behavior is what happens with no inputs. From the README: "Access is fail-closed. A role is required", and "Without a role the engine grants nothing and defers, which is the correct behaviour and the first thing people mistake for a bug." That last clause is the honest cost of the posture. The first-run experience of a fail-closed system is an error message, and the engineering temptation is always to make the empty case permissive so the demo works.
One design consequence is worth stealing even if you never run mnemiq. "Permissions apply before schema retrieval, so the model is never shown a table the caller may not see", which means naming a forbidden table "is useless rather than refused". A system that filters after generation has to refuse; a system that filters before retrieval has nothing to refuse, and the model cannot hallucinate a permission it was never shown. The broader design is covered in governed data agents.
What Fail-Closed Costs, and How to Pay It
The objection to fail-closed is availability, and it is a real objection, not a rhetorical one. ACS picked its default for exactly that reason. Three things make the bill affordable.
Bound the timeout and make it short. A 250 millisecond budget with one retry inside the remaining window turns "the authorizer is slow" into a few extra denials rather than a stalled run. ACS puts this in the normative text: the negotiated timeout "bounds every failure mode", and "an unambiguous failure (a refused connection) MAY resolve immediately rather than waiting out the clock". A fail-closed posture with an unbounded timeout is the worst of both, because it hangs instead of deciding.
Cache the allow, never the deny-by-failure. A decision that was genuinely made can be reused for a short window keyed on agent, user, action and resource. A non-answer must never be cached, because caching it converts one network blip into a sustained outage for that tuple.
Refuse in a way the caller can act on. This is where the taxonomy's third term earns its place.
aact:failure-behavior/refuse-and-explain is defined as behaviour where "the agent declines, tells the
requester what it could not do and why, and does not substitute a partial or guessed result." The two
postures it sits beside are aact:failure-behavior/fail-open, where "if policy is missing, unreachable or
ambiguous, the action proceeds", and aact:failure-behavior/fail-closed, where the action "is denied and
the denial is recorded". This is the failure behavior dimension of the
Agent Access Control Taxonomy, whose
stated question for that dimension is exactly the one this post started with: what happens when the
decision cannot be made.
Note the second half of the fail-closed definition. The denial is recorded. A deny that leaves no trace is operationally almost as bad as a silent allow, because the people who could fix the mapping never learn it is wrong.
Frequently Asked Questions
What does fail-closed mean for agent authorization? That when the authorization decision cannot be made, the action is denied and the denial is recorded. The decision failing is treated as a denial rather than as a reason to skip the check.
What is the difference between fail-open and fail-closed? Fail-open proceeds when the decision is missing, unreachable or ambiguous. Fail-closed denies. Fail-open preserves availability and converts your authorization layer into an audit layer whenever the channel is disrupted, which is the trade OWASP's Agent Control Standard names in its own text.
Does OWASP's Agent Control Standard fail open or closed by default? Open. The standard defaults
on_decision_failure to proceed, so a slow, erroring or unreachable Guardian does not halt production,
and a deployment MAY set on_decision_failure: deny. The reference implementation exposes this as
ACS_ON_DECISION_FAILURE, also defaulting to proceed. The startup posture, for a handshake that never
completes, defaults to proceed as well, so both need changing to get a fail-closed deployment.
What should an agent do when a tool annotation is missing? Treat the absence as uninformative, not as permission. MCP's specification requires clients to consider tool annotations untrusted unless they come from trusted servers, so a present annotation from an untrusted source is worth no more than a missing one. Annotations are for interface design and review triage, not authorization inputs.
What does a 403 insufficient_scope mean for an agent? That the token is valid but too narrow for this
operation. Under RFC 6750 the response may carry a scope attribute naming what is needed, and MCP's
authorization spec tells clients to step up by requesting a new token for the union of their previous
scopes and the challenged ones, with retry limits so a loop cannot form.
How do I stop fail-closed from causing outages? Bound the decision timeout tightly, retry once inside the remaining budget, cache decisions that were actually made, and never cache a non-answer. Most of the pain attributed to fail-closed comes from an unbounded wait rather than from the denial.
Is denying on an unrecognised scope too strict? It is the only safe reading of an intersection. A grant neither side recognises cannot be shown to be inside both sets. The alternative, prefix or wildcard matching, silently grants every future scope a vendor invents.
Conclusion
The reason this dimension deserves its own page is that it is the one most likely to be undocumented and the one most likely to decide what actually happens in an incident. Identity, delegation and scope models get designed. Failure behavior gets inherited from a library default, and in at least one published standard that default is to proceed.
So the work is small and specific. Enumerate the ways your stack can fail to get an answer, which is more than one. Assign a posture to each, and write down who chose it. Bound the wait. Make the recoverable denials carry their reason, because the protocols already have a field for it and a 403 that names the missing scope costs a round trip instead of a ticket. Then audit the bypasses, which ACS is right to make mandatory: a fail-open path nobody can count is indistinguishable from no authorization at all.
Unplug it and watch. Point your agent at a batch of real actions, make the authorizer unreachable, and see what the run does. Whatever happens next is your actual posture, and it is the only one your auditors will be able to observe.
Sources
All URLs read 2026-10-12.
- Agent Control Standard, Instrument pillar specification — section 6 on the five dispositions, 6.4 on honoring decisions and the definition of a decision failure, the
on_decision_failuredefault ofproceed, the mandatory audit event for every bypass, and the stated fail-open trade. - Agent Control Standard, handshake failure (same specification, section 4.1) — the separate startup posture for a handshake that never completes, also defaulting to
proceed. - Agent Control Standard reference implementation, agt — the configuration table listing
ACS_ON_DECISION_FAILUREwith defaultproceed, and the note that any value other thanproceedordenystops the Guardian at startup. - Agent Control Standard repository — the project, its OWASP GenAI Security Project home, and the v0.1.0 wire schemas.
- MCP specification 2026-07-28: Tools — annotations as optional properties, and the requirement that clients consider tool annotations untrusted unless they come from trusted servers.
- MCP specification 2026-07-28: Authorization — the 403
insufficient_scopechallenge shape, the step-up authorization flow, the scope-union requirement, and the retry limits. - RFC 6750: OAuth 2.0 Bearer Token Usage — the
insufficient_scopeerror value, the 403 recommendation, and the optionalscopeattribute naming what is required. - RFC 9470: OAuth 2.0 Step Up Authentication Challenge — the
insufficient_user_authenticationerror value, and theacr_valuesandmax_agechallenge parameters. - OWASP Secure Product Design Cheat Sheet — failing into a secure state, and secure defaults as a design principle.
- mnemiq — the deterministic decider, refusal with a stated reason, fail-closed access, the required role, and permissions applied before schema retrieval.
- mnemiq product page — the nine-stage pipeline, the two access-scoped read-only MCP tools, and the fail-closed quickstart behaviour.
- Agent Access Control Taxonomy — the failure behavior dimension and its question: what happens when the decision cannot be made.
- Agent access control taxonomy, failure-behavior.yaml — the definitions of
aact:failure-behavior/fail-open,fail-closedandrefuse-and-explainquoted above.