An agent is most of the way through a task when it tries to reach a destination its egress proxy does not allow. What comes back is an opaque failure: a connection reset, a timeout, a 403 with no body. The agent cannot tell whether the destination is wrong, down, or simply not approved yet. There is no machine-actionable way for it to ask for access, and no human standing by at the moment of the block. So the task stalls, or the agent improvises around the wall, which is worse.
This is the egress version of a problem I wrote about for policy decisions in Authorization Denied Is No Longer Enough. A deny is increasingly not a full stop. It is the beginning of an escalation. At the network egress boundary the same thing is true, and the enforcement point is the proxy.
Egress Allowlists Cannot Be Enumerated Upfront
Agent runtimes commonly route outbound traffic through an egress proxy that enforces a destination allowlist, and this is already standard practice rather than a hypothetical. Anthropic’s sandbox-runtime puts a proxy outside the sandbox that allows no domains by default and permits only the hostnames you configure, and the same allowlist model sits behind Claude Code’s managed sandbox (secure deployment guidance). Open-source agent egress proxies like Brex’s CrabTrap sit between the agent and external APIs and evaluate every outbound request against allow and deny policies, returning a 403 on a block, and current guidance for running autonomous agents treats a network egress allowlist as a baseline control. They share one behavior: when a destination is not on the list, the agent gets a failure, not a way forward. That model assumes you can enumerate the destinations ahead of time. Agents increasingly do not work that way.
As I described in OAuth for Open-World Ecosystems, modern agents discover APIs dynamically, interact with previously unknown systems, and expand workflows during execution. A renewal assistant that started with a known set of endpoints discovers mid-task that the answer lives behind a partner API it has never called. A host-level allowlist provisioned at startup cannot accommodate that. The deployment is left with two bad options: over-provision egress so broadly that the proxy stops being a control, or block the destination and let the task stall.
The block is the honest behavior. The problem is what the block does not carry.
The Block Is a Requestable Denial, and the Proxy Is a PEP
Reframe the egress proxy as a policy enforcement point. The dropped connection is a decision, and that decision could be a requestable denial: access is currently denied, but an escalation path is available. The agent could ask for the destination, a governance event could occur, and the proxy could re-evaluate and let the traffic through.
What is missing is not the decision. It is the transport. A headless agent has no browser, no consent screen, and no human in the loop at the moment of the block. So how does it discover that a block is requestable, learn where to ask, and find out when it has been let through?
RFC 8908 Already Standardizes This
There is already a standard for telling a blocked client why it is blocked and how to get out. RFC 8908, the Captive Portal API, is the JSON API behind the coffee-shop and hotel wifi experience. A client queries it and learns whether it is captive, where the portal is (user-portal-url), and how long any granted access lasts (seconds-remaining). It was designed so a device could discover its network restriction without scraping an HTML interstitial.
A blocked agent is just another captive client. It is on a network, behind an enforcement point, denied egress, and it needs a machine-readable answer to “why am I blocked and how do I get out.” That is exactly what the Captive Portal API returns. We do not need a new protocol for agents to discover egress blocks. We need to put the requestable denial inside the captive portal response.
That is the move both proposals make. This is not a claim that per-destination agent egress is what RFC 8908 originally standardized. It is a profile of the same recovery shape: a blocked client queries a well-known API, receives captive: true, and learns the machine-actionable path to remediation. The egress proxy is both the enforcement point and an RFC 8908-style Captive Portal API server, and the blocked response carries the recovery context alongside captive: true: an access-request member in the AuthZEN profile, an opaque resource-token in the AAuth profile.
Repurposing a human and browser protocol for headless agents has three costs, and they are worth naming up front because they are the price of the insight, not an afterthought.
Granularity mismatch. RFC 8908 signals captivity at the network level: you are on this network, you are captive or you are not. Egress control is per-destination: this agent may reach destination A but not destination B. Both proposals resolve this by scoping a distinct Captive Portal API URL per blocked destination, so the per-denial URL carries one destination’s requestable denial rather than a network-wide state.
Discovery into TLS tunnels. An agent learns the captive portal URL through the network layer (RFC 8910 provisioning for managed clients), through an HTTP 511 at the proxy hop, or through direct configuration. A proxy cannot inject a 511 into an opaque TLS tunnel without terminating it, so discovery for fully tunneled HTTPS egress remains an open question rather than a solved one.
Untrusted URLs. The user-portal-url and venue-info-url in a captive portal response are human-facing and attacker-influenceable. An agent must not navigate them or, worse, feed them to an LLM as instructions. The machine-actionable path is the structured member the response carries, the access-request member or the resource-token, not the portal URL. This matters more for agents than for humans, because the client here is a model that will tend to act on text it reads.
The safe MVP discovery model is direct configuration. The agent runtime is launched with the egress proxy’s captive API endpoint and knows to query that endpoint when a connect attempt is blocked. HTTP 511 and RFC 8910 provisioning are useful deployment options, especially for managed environments, but they are not the baseline for fully tunneled HTTPS. The minimum profile should assume the runtime already knows its proxy and captive API, then standardize what comes back from that API when a destination or operation is captive.
The Minimum Shape
A useful profile does not need to standardize the whole approval system behind the proxy. It needs to standardize the recovery contract between the blocked agent and the enforcement point:
- how the agent discovers or is configured with the captive API endpoint;
- how the proxy scopes a captive response to the blocked destination or operation;
- which response member carries the machine-actionable recovery path;
- how the denial is cryptographically bound to the thing that was blocked;
- how long the denial binding and any resulting approval remain valid;
- how the agent polls or is notified that the request has completed;
- how the proxy re-evaluates before allowing egress; and
- what the agent must ignore, especially human-facing portal URLs and untrusted text.
Everything else can be deployment-specific: approver routing, ticketing systems, user interface, risk scoring, and the internal policy language. The standardization point is the blocked-client handshake, not the governance workflow behind it.
Two Ways to Wire It Up
There are two early proposals for this, both mine, both reusing RFC 8908 as the transport. They are siblings, not a stack. Neither composes on top of the other. They sit at different altitudes on different authorization substrates, and the interesting axis between them is how fine-grained the control is and how much human judgment each block deserves.
Destination-Level, on AuthZEN
The Captive Portal Egress Profile is a strict extension of the AuthZEN Access Request profile from the requestable-denial work. The egress proxy is both an AuthZEN policy enforcement point and a Captive Portal API server. The agent is the AuthZEN Subject, the blocked destination is the Resource, and connect is the Action. When the proxy blocks, the captive portal response carries the requestable denial in the access-request member. The denied subject, resource, and action are reconstructable from the denial binding, so the member can carry just the context:
| |
The agent submits a binding-only access request to the advertised endpoint, presenting just the denial-binding material (evaluation_id or binding_token), and the Access Request Service reconstructs the denied subject, resource, and action from it. It polls the resulting Task Handle, and on approval it retries the egress. The proxy re-evaluates against current policy using the base profile’s reevaluate completion mode, and only then does the destination become reachable. Approval resolution is server-side by default: the proxy reads the approval from PDP state at re-evaluation rather than trusting an approval the agent injects into a header. When the connection is allowed, a re-query returns captive: false with seconds-remaining derived from the approval’s expiry, and at expiry the destination returns to captive.
This is the policy-decision shape of the problem. What gets approved is “may this subject connect to this destination,” and the PDP stays authoritative end to end. It advertises support through a capability URN, urn:openid:authzen:capability:access-request:captive-portal-egress, and serves the response as application/captive+json.
Operation-Level, on AAuth and Mission
The AAuth Agent Egress Governance proposal sits at a finer altitude. It builds on Dick Hardt’s AAuth protocol and Rich Resource Requests (R3) drafts, with the Mission layer I have written about in Mission Architecture on AAuth and AAuth Now Has a Mission Layer. Control is at the operation level, not the host level: the question is not “may I reach this host” but “may this mission perform this operation, with these parameters.”
The simple version is this: the proxy is not only deciding whether the agent may reach api.partner.example; it is deciding whether the current Mission permits a specific operation exposed by that API. Routine operations can be pre-authorized in the token. Risky operations can be marked conditional, so the proxy stops the call and asks the Authorization Server for a fresh decision before the side effect leaves the network.
The Mission-bound token carries a two-tier model. r3_granted operations are served immediately by the proxy with no per-call backend call, the fast path where authority travels with the agent. r3_conditional operations are authorized in principle but validated per call against the Authorization Server at enforcement time. Routine reads stay granted. High-risk or parameter-sensitive operations route to conditional, so the expensive evaluation happens only where it earns its cost. The token also carries r3_uri and r3_s256, the location and hash of the R3 document, and a constraints_hash that versions the Mission authority.
Operation-level control assumes the proxy can see the operation, which depends on its vantage point. Behind an opaque CONNECT tunnel the proxy can only govern the host or connection. A TLS-terminating proxy can match on HTTP method and URL. An API-aware gateway can govern named operations like invoice.read. The finer the visibility, the finer the grant, which is the same reason the deepest control comes with TLS termination rather than pass-through, and why fully tunneled egress falls back to host-level rules.
So when the renewal agent tries to issue a refund through the same partner API, the block is not “you may not reach this host.” It is “this mission is not yet authorized for payment.refund on this destination.” The proxy blocks the call and, in the captive portal response, hands back an opaque resource-token that captures the blocked call, the attempted operation and parameters bound to the destination’s r3_uri and r3_s256, plus a user-portal-url pointing at the Person Server:
| |
The agent presents that resource-token to the Authorization Server in an authorization request that names the r3_operations it needs, here payment.refund. The AS verifies the token, fetches and validates the R3 document for the destination (served only to signed, AS-only requests), and routes the parameter-sensitive parts to a human at the Person Server, who approves against the R3 display text (summary, implications, data_accessed, and an irreversible marker), not the agent’s own rationale. On approval, the AS issues a refreshed Mission-bound token with a new constraints_hash. The proposal specifies these as token claims rather than wire JSON, so the values below are illustrative:
POST /aauth/token → 200 OK
{
"token_type": "DPoP",
"expires_in": 900,
"access_token": "<AAuth-Mission JWT>",
"constraints_hash": "9f2c8e1b...",
"r3_uri": "https://api.partner.example/.well-known/r3",
"r3_s256": "Yt4Qk9a1xK2v...",
"r3_granted": ["invoice.read"],
"r3_conditional": ["payment.refund"]
}
The agent presents this token to the egress proxy and retries. invoice.read flows straight through. payment.refund is now authorized, but as a conditional operation it stops at the proxy on every call: the proxy returns an AAuth-Requirement challenge carrying the concrete parameters, the agent round-trips to the AS, and the AS issues a per-call token or denies before the refund leaves the network. The new constraints_hash supersedes the prior token, so authority that was broadened can also be narrowed or revoked by issuing the next version.
Two properties are worth calling out. The agent holds only the opaque r3_s256 hash, never the R3 document itself, so prompt injection cannot rewrite the operation set through the agent: it never carries the semantics, only a hash of them. And non-AAuth destinations are handled by R3 derivation: a TLS-terminating proxy derives an R3 document from a published API description (OpenAPI, MCP, gRPC) to govern named operations, or falls back to method-and-URL rules when it only sees HTTP, standing in for the resource in the conditional handshake so the third-party service stays unchanged and unaware.
Same Transport, Different Altitude
| AuthZEN Access Request profile | AAuth and R3 Mission egress | |
|---|---|---|
| Granularity | Destination (host) | Operation (or host, by proxy visibility) |
| What is approved | May this subject connect to this destination | May this mission perform this operation, with these parameters |
| Consent model | Policy re-evaluation at the PDP, human approval optional behind the request endpoint | Human approval at the Person Server against resource-authored display |
| Substrate | AuthZEN Authorization API (PDP and PEP) | Dick Hardt’s AAuth and R3 drafts, plus the Mission layer |
| Enforcement-time check | Re-evaluate against current policy | r3_conditional validated per call |
Both reuse RFC 8908 as the transport, though they carry the recovery differently: the AuthZEN profile puts the requestable denial in an access-request member, while the AAuth profile returns an opaque resource-token that the agent takes to its Authorization Server. The choice between them is altitude: how fine-grained the control needs to be, and how much human judgment a given block deserves. A coarse “may I reach this partner API” is well served by destination-level policy re-evaluation. A consequential, parameter-shaped action like a refund, which a person should look at before it happens, belongs at the operation level with curated consent. They are tiers for different risk, not competitors.
The Principles That Keep This Honest
Reusing the captive portal API is the easy insight. The discipline is in what the proxy is allowed to conclude from a block.
Approval is not a grant. A captive: true response is a requestable denial, not a grant, whether it carries an access-request member or a resource-token. The proxy keeps the destination blocked until re-evaluation, or a re-projected token, actually permits it. This is the same invariant as the policy-decision case: the enforcement layer stays authoritative at the moment of access, and the workflow that produced the approval does not get to declare the connection open. An approval signed earlier is an input to that decision, not a substitute for it. It is the same reason a session is not a mission and the reason governing the stay matters more than governing the entry.
Binding prevents confused-deputy egress. The denial binding ties the approval to the exact thing denied: an evaluation_id or binding_token for the destination in the AuthZEN profile, the r3_uri and r3_s256 for the operation set in the AAuth profile. An approval for one destination cannot be replayed to open another. Without that binding, the egress proxy becomes a confused deputy.
Consent is curated, not narrated. When a human is in the loop, they approve against resource-authored, structured descriptions of impact, and the agent’s own rationale is untrusted and sanitized before display. The person approving an agent’s refund needs to see what the operation will actually do, not what the agent says it will do. This is the credential-side argument from From Passports to Power of Attorney and the Power of Attorney series, applied at the network boundary: authority is granted against the work, not against the asker’s self-description.
Failure Modes to Design Against
The profile has to be strict about failure because the blocked client is an agent, not a person reading a hotel wifi page.
Malicious portal content. The agent must treat user-portal-url, venue-info-url, and any human-readable display as untrusted. It can surface them to a human or governance system, but it must not browse them, summarize them into its plan, or follow instructions found there.
Approval replay. An approval for one destination or operation must not open another. The denial binding has to cover subject, destination, operation where applicable, proxy identity, expiry, and the policy decision context.
Stale approval. Approval is time-bounded. The proxy must re-evaluate at the moment of egress and must return to captive: true when the approval expires, the Mission changes, or the policy state changes.
DNS and wildcard ambiguity. Destination-level approval needs precise matching rules. api.partner.example, *.partner.example, IP literals, CNAME chains, SNI names, and CONNECT targets are not interchangeable unless policy explicitly says they are.
Proxy impersonation. The agent must know which egress proxy and captive API it is allowed to trust. A random captive response from the network is not enough. Direct configuration, mTLS, signed metadata, or an equivalent trust mechanism is required.
Privacy leakage. A requestable egress denial reveals intent: where the agent wanted to go, what operation it wanted to perform, and sometimes why it was blocked. The proxy, PDP, AS, and approval workflow all become observers of task state. Profiles should minimize what is carried in the captive response and keep sensitive prompt or plan evidence out of the agent-visible payload.
The Wall Is a State, Not a Dead End
The egress proxy was always a policy enforcement point. We just treated the block as a failure instead of a question. RFC 8908 gives a headless agent the same thing a laptop on hotel wifi already has: a machine-readable answer to “why am I blocked, and how do I get out.” The two proposals differ on how much authority and judgment a given block deserves, one at the destination level on AuthZEN policy, one at the operation level on AAuth and Mission-bound authority. What they share is the move that matters.
A blocked agent is not stuck. It is captive, and captivity is a state with a defined way out.