Imagine an AI agent receives a valid, signed delegation to perform an action.
The user later revokes that delegation in another system.
The signature is still valid. The token may not have expired. But should the agent still be allowed to act?
Cryptographic validity and current delegated authority are two different questions.
OAuth introspection, short-lived credentials and existing authorization systems address important parts of this problem. But we wanted to explore one specific challenge: checking whether a delegated authority path is still active when agents and issuers operate across different trust domains.
We built GATE — Authority Verification Network as an experimental Developer Preview to investigate this.
Try the live demonstration
Our hosted sandbox uses real HTTP requests to demonstrate three decisions:
- Two authority paths are active → VALID / ALLOW
- One path is revoked, but another survives → VALID / ALLOW
- Both paths are revoked → INVALID / DENY
The demo uses a synthetic authority graph, not a third-party identity provider.
Try the interactive demonstration
Explore the source code on GitHub
GATE also exposes an API for registering ES256 issuer keys and submitting signed authority events.
What we're not claiming
GATE currently runs on a single Canadian primary, without production regional replicas or a guaranteed external issuer propagation time. It does not replace authentication, OAuth, application policy enforcement or an independently audited production authorization system.
We're looking for developers who work with AI agents, MCP tools, delegated permissions or cross-domain authorization.
I'd especially like to hear your thoughts on three questions:
- How do you currently handle upstream revocation for delegated agents?
- Is this already adequately solved by your existing stack?
- Would a separate authority-state verification service make integration easier, or just add another dependency?
We're inviting a small group of independent developers to evaluate the API with their own authority sources.
Join the GATE external developer beta
Feedback and criticism are welcome.
Top comments (10)
The ALLOW is over-determined, and that is the one thing I would change before the beta — it falls straight out of your own three rows.
Rows 1 and 2 (two paths active; one revoked, one survives) both print VALID/ALLOW. As a boolean that is correct; as a receipt it drops exactly what the service exists to tell the caller: which path admitted the action. A client reading only the verdict cannot tell "nothing was revoked" from "one of two was revoked and the other carried it" — and the second case is the whole reason a revocation service exists. The verdict answers "is there an active path"; it does not answer "is the authority I am acting under still the authority I was granted", and those differ precisely when it matters.
So name the admitting path(s) in the ALLOW, not just the outcome. Then revoking one of two surviving paths shows up as a change in the witness while the verdict stays ALLOW — visible, diffable, auditable. Without the name, a healthy allow and a stale allow are the same byte, and no re-check can separate them after the fact.
That is also the better answer to your third question than "dependency or not": a service returning a boolean is a dependency that adds a call; a service returning which authority survived adds something the caller cannot compute locally — the cross-domain state. The value is in the second column, not the first.
And it is what makes the failure mode on your first question observable at all. The dangerous case for a delegated agent is not a denied action, it is a stale ALLOW: the delegation was revoked upstream and the edge is still answering from a cache. If the reason for the ALLOW is not in the response, you find that class only by hand.
That's a very good observation, and I agree that the distinction matters.
The current GATE v7 API actually returns
selectedPath,selectedEdges,validPathCount, and the evaluatedpathsalongside the decision. So the underlying resolver already exposes which authority path survived.However, you're right that our public demo currently flattens this into ALLOW/DENY and hides the most interesting part of the response. That's a presentation gap worth fixing.
There's also an important distinction between a path witness and proof of upstream freshness. The current API evaluates the state known to GATE's canonical primary; it cannot guarantee that an external revocation has already arrived.
Making the surviving path and its freshness limitations explicit would make the demonstration more useful and more auditable.
Thanks for identifying this — it's precisely the kind of technical feedback we're looking for.
Agreed on all three — and the reason row 2 is worth fixing is that your own design rules already forbid it.
docs/ARCHITECTURE.md, "Design rules" §4: preserve alternate valid authority paths after a partial revocation. Row 2 of the demo is that case, and it prints the same byte as row 1. So it is not a missing capability — the rule says the surviving path must be preserved, the resolver already enumerates it (selectedPath, validPathCount), and the demo is the one place it stops being observable. Two rows that must differ by construction rendering identically is the smallest reproduction of it you could ask for.
On upstream freshness: I think your own contract has already answered it, and the answer is that it is not a service-side choice between fail-closed and a lease — it is a call-site choice you have already exposed.
gate.verify({ …, consistency, maxStalenessMs })with the two data-plane lanes (bounded → signed local snapshot + freshness lease; strict → control-plane confirmation) puts the trade-off where the caller is. What is left is the receipt: if the decision does not echo which lane served it and the state it was evaluated at, then "nothing was revoked" and "the revocation has not arrived yet" come back as the same ALLOW — which is precisely the certainty rule 3 says not to fake. The edge already exposes revision and fresh on /v1/replica/status; carrying the age it granted onto the decision itself is what lets a client log staleness instead of inferring it.On a disconnected stream or a sequence gap, the honest third answer is your UNKNOWN — and it is only usable if the caller can tell it apart from OK, which is the same echo. A verdict naming the surviving path, the lane, and the snapshot age is one a caller can act on; a boolean is one they have to trust.
Thank you for taking the time to examine our architecture and design rules. Your feedback has been genuinely useful.
We've actually made two concrete advances this morning that directly relate to your observations.
1. Authority-path observability — now deployed
We've updated the public GATE demo to expose the selected authority path, surviving alternatives, revoked edges, and evaluated path states rather than only ALLOW/DENY.
We also validated the behavior through real hosted HTTP requests:
You can inspect the updated demo here:
gate-beta-nu.vercel.app/demo.html
2. Local revocation and disconnected-state handling — research prototype
We've also built an isolated prototype using ES256-signed state, monotonic sequence numbers, and fail-closed verification.
It currently passes 29 tests covering revocation, sequence gaps, replay attempts, disconnection, resynchronization, and lease expiration.
In this prototype, a disconnected stream or missing sequence produces UNKNOWN/DENY until trusted state is re-established.
We also measured local in-memory decision latency and hosted HTTP latency. The results suggest that local verification is worth exploring, although the measurements are not directly comparable and do not establish end-to-end revocation freshness.
Importantly, this second component is not deployed in GATE v7.
Your point about decision provenance is particularly valuable. We agree that an authority decision should explicitly identify the witness, the consistency scope, and what freshness has actually been established.
The remaining challenge is ensuring that the freshness of an external issuer's state is not confused with the freshness of GATE's own snapshot.
Thanks again — your review is helping us make the implementation and its guarantees more precise.
Thanks for the detail — and for taking the observation to the demo rather than to a reply. Making the selected path, the surviving alternatives and the revoked edges visible is the repair I would have asked for: your third row (both revoked → DENY, no admitting path) is worth exactly because it used to print the same byte as "denied with a live alternative", and now it does not.
One caveat on my side, so you can weigh the rest correctly: I could not inspect the deployment. From this host
gate-beta-nu.vercel.appresolves to an address that is not Vercel's and then times out on 443 (the apexvercel.comanswers fine), so I am reading your description, not the running demo. If the payload differs from what I assume below, the objection may not apply.The remaining challenge you named — "the freshness of an external issuer's state is not confused with the freshness of GATE's own snapshot" — is a two-clock problem, and it is the same shape as an absence-of-witness one:
Two fields, not one. The response wants (a) GATE's own decision clock, and (b) per authority source, the state it read and when that state was produced (issuer timestamp or sequence), plus whether the read was a fresh fetch or a cached snapshot. With a single
freshness/maxStalenessMs, "the issuer's state is fresh" and "GATE's snapshot is fresh" print the same byte — which is the collapse you are trying to undo.A monotonic sequence number tells you a gap exists, not whether the gap is "not yet" or "never." A disconnected stream and an inbound "nothing revoked since seq N" are different inputs that a counter can look identical on at read time. So the per-source state wants three values — FRESH (witnessed, within bound), STALE (witnessed, outside bound), UNKNOWN (no witness) — collapsed to DENY only at the decision and kept distinct in the provenance. Otherwise "we fail closed" and "we cannot tell" are the same ALLOW/DENY byte again: your rule-3 collapse (UNKNOWN is a real state; do not fake certainty) reproduced at the layer you just made observable.
On latency — you already flagged that local in-memory and hosted HTTP are not comparable and do not establish end-to-end revocation freshness. The comparable pair is one quantity: time from "issuer revokes" to "GATE's decision reflects it", origin on the issuer's clock. Two different quantities under one name will keep reading as two.
Fail-closed until re-synchronised is the right default; the thing I would pin before deploying is that the provenance keeps (1) and (2) apart, because that is the part the demo cannot reconstruct after the fact.
In my own agent workflows, upstream revocation is where short-lived tokens hit a practical wall. When an agent delegates subtasks across separate execution environments, five-minute expiration windows keep the blast radius bounded locally, but synchronous introspection on every sub-action adds heavy latency to an already slow loop.
The biggest failure mode in practice is the cache window during an active run. If authority checks require a blocking external HTTP call, any transient network hiccup turns into either an unearned execution failure or a fallback to stale state. Having the execution harness subscribe to an invalidation stream and check a monotonic sequence number at the tool boundary has been far more dependable than making an external round-trip for every tool call.
That's an important trade-off, especially for agents executing many short-lived subtasks.
I agree that a synchronous external check at every tool boundary introduces latency and availability concerns. Your approach using an invalidation stream and monotonic sequence numbers is particularly interesting because it moves much of the decision work closer to the execution harness.
GATE's current Developer Preview uses HTTP verification against a canonical primary. We haven't yet implemented or validated the stream-based local enforcement model you're describing, so I wouldn't claim that capability today.
One question I'd be interested in exploring: how do you handle a disconnected invalidation stream or a gap in sequence numbers? Do you fail closed until the local state is resynchronized, or use a bounded lease?
That recovery behavior seems central to making either approach reliable.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.