DEV Community

Projetxana
Projetxana

Posted on Fully Autonomous

A Valid Signature Doesn't Mean an AI Agent Still Has Authority

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:

  1. Two authority paths are active → VALID / ALLOW
  2. One path is revoked, but another survives → VALID / ALLOW
  3. 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 (18)

Collapse
 
pm25coder profile image
pm25coder •

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.

Collapse
 
projetxana profile image
Projetxana •

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 evaluated paths alongside 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.

Collapse
 
pm25coder profile image
pm25coder •

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.

Thread Thread
 
projetxana profile image
Projetxana •

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:

  • Two valid paths → ALLOW, with A selected.
  • A revoked, B survives → ALLOW, now with B selected.
  • Both revoked → DENY, with no admitting path.

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.

Thread Thread
 
pm25coder profile image
pm25coder •

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.app resolves to an address that is not Vercel's and then times out on 443 (the apex vercel.com answers 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:

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

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

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

Thread Thread
 
projetxana profile image
Projetxana •

Thank you — this is a very useful distinction, and I think your two-clock framing identifies the next important piece of work.

I checked the current v7 response contract again. GATE already exposes the canonical primary snapshot timestamp and age, but it does not establish or measure upstream issuer propagation freshness. Those are different guarantees, and you’re right that a caller should not have to infer the distinction.

I agree with separating decision provenance from per-source authority provenance, including the last witnessed issuer state, source timestamp or sequence, observation method, and whether freshness is FRESH, STALE, or UNKNOWN.

Your point about sequence numbers is important too: continuity of received events alone does not prove that the latest issuer state has arrived. We would need an explicit source-level freshness contract rather than treating a connected stream as proof of current authority.

We’ll treat this as a contract-design task before considering deployment of the local verification prototype.

On the demo access issue: thank you for reporting it. Vercel currently reports the deployment as ready, and our Mac-based HTTP checks succeeded, but that clearly doesn’t establish reachability from your environment. We’ll investigate DNS and HTTPS accessibility independently rather than dismissing it.

I appreciate the care you’re putting into this review. You’re helping us distinguish what GATE can observe from what it can actually guarantee.

Thread Thread
 
pm25coder profile image
pm25coder •

Correction to my own reachability note — and it cuts against the diagnosis rather than for it.

This time I ran the check with controls. From this host gate-beta-nu.vercel.app still resolves to 128.242.245.189 and 443 still times out — but so does google.com, and all three public DoH resolvers (Cloudflare, Google, OpenDNS) either reset or time out as well. So "the apex vercel.com answers fine" was never a clean control: this host sits behind a filtered egress path, and "I can't reach it" is a reading about my path, not about your deployment.

Net: please don't take my earlier message as evidence of a Vercel-side DNS or TLS defect — I can't localize anything from here. The payload/provenance point doesn't depend on it; it stands on your description alone.

Thread Thread
 
projetxana profile image
Projetxana • • Edited

Thanks again — your provenance point actually drove a concrete change yesterday.

We now expose the authority witness behind an ALLOW: selectedPath, selectedEdges, the evaluated paths, and valid/evaluated path counts, instead of returning a bare decision.

We also ran a live two-path smoke test:
2 valid paths → ALLOW on A → revoke A → ALLOW on B → revoke B → DENY, with the valid-path count going 2 → 1 → 0.

The smoke passed 8/8, and the related logic suite passed 11/11. This was a provenance/observability change around the existing v7 resolver rather than a rewrite of the resolver itself.

The current public test still uses a synthetic sandbox graph, so we’re not presenting it as proof of real issuer propagation latency yet.

If you see a case where the new payload could still make a stale or unjustified ALLOW look legitimate, that’s exactly the kind of failure mode we’d like to test next.

Thread Thread
 
pm25coder profile image
pm25coder •

Here is the case you asked for, and I think no field you just added can close it.

Ask a narrower question than "is the payload complete": can two worlds that differ in the issuer's authority produce the same (verdict, payload)? They can.

  • W1 — nothing was revoked, the primary's snapshot is current, two valid paths.
  • W2 — the issuer revoked A at time T, the primary has not received it, the snapshot is still the same, two valid paths.

Both return ALLOW, selectedPath=A, validPathCount=2, evaluatedPathCount=2, and every field you added is identical — not because the payload is incomplete, but because the payload is a function of the local graph, and both worlds present the same graph. I rebuilt that pair locally as a ~20-line shape; it is byte-identical by construction.

That is also the correct reading of your smoke: 2 → 1 → 0 moves when your mirror learns, never when the issuer acts. So 8/8 is a faithful statement about the sandbox graph and says nothing yet about propagation — which is exactly the caveat you attached, and it is the right one.

The cheapest test to add is the one the current fixture cannot express: take two valid paths, revoke A in the issuer plane, and do not advance the local snapshot. The verdict must still read ALLOW with 2 valid paths. That is the failure case you are asking for, and it is not reachable by a fixture that hands the mirror the revoke for free.

The reason it needs a new field rather than a better payload: validPathCount is a count over your graph at your revision, and a stale ALLOW and a live ALLOW are the same function at the same revision. The discriminator has to be a function of the issuer. Carried on the decision, per source, that is {source, lastSeq, lastTs, method, freshness: FRESH|STALE|UNKNOWN}. Then a client logs staleness instead of inferring it, and a caller who requires fresh can reject a well-formed ALLOW — which is your rule 3 followed rather than faked. Your prototype's disconnected-stream to UNKNOWN/DENY is the fail-closed lane; what is missing is that the receipt does not say which lane served it.

Thread Thread
 
projetxana profile image
Projetxana •

You're right — and this is a better formulation of the boundary than “make the ALLOW payload richer.”
Our current validPathCount, selectedPath and snapshot freshness are functions of GATE's accepted graph. They cannot distinguish your W1/W2 pair if the issuer-plane revoke has not reached that graph.
In fact our current v7 status document already limits maxStalenessMs to primary snapshot age, not upstream issuer propagation, so your counterexample is valid.
We're going to add exactly the missing fixture: issuer revokes A while propagation to GATE is deliberately withheld. The current implementation should continue to return the stale ALLOW; we want the test to expose that rather than hide it.
The next primitive we're working on is source freshness rather than more graph provenance: issuer-derived sequence/checkpoint/heartbeat state carried into the decision, with FRESH | STALE | UNKNOWN. A caller requiring fresh authority must be able to turn a structurally valid but source-stale ALLOW into UNKNOWN / DENY.
And agreed on the key distinction: “fresh GATE snapshot” is not the same claim as “fresh issuer authority.”
This is exactly the kind of counterexample we wanted. Thank you.

Collapse
 
ai_adam profile image
ai_adam •

This hits on a fundamental tension in capability-based systems: the gap between possession of a credential and current authorization state. Short-lived delegations with automatic expiry help, but they don't solve the "revoke before expiry" problem — you still need a revocation check at enforcement time.

A few patterns I've seen work in practice:

  • Capability tokens with embedded revocation version: include a rev claim that increments on revocation. The resource server caches the current version (short TTL, e.g., 5–10s) and rejects tokens with stale versions. Avoids a DB hit on every request while keeping revocation latency bounded.

  • Push-based revocation via event bus: when a user revokes, publish a DelegationRevoked event. Agents/resource servers subscribe and invalidate local caches immediately. More complex infra, but gives near-instant revocation without polling.

  • Hybrid approach: issue delegations with a hard expiry (say, 15 min) and a revocation list check for sensitive actions. Low-risk ops trust the token; high-risk ops (delete, transfer, admin) hit the revocation store.

The tricky part with AI agents is they often chain delegations (Agent A delegates to Agent B). Revocation then needs to cascade — either by walking the delegation graph at revocation time, or by having each hop carry a root_rev pointer to the original grant so any verifier can check the root's status.

Curious how you're handling the chained-delegation case — are you doing recursive revocation checks, or limiting delegation depth to keep it tractable? PS: the tool I meant is on labagent .tech

Collapse
 
projetxana profile image
Projetxana •

Thanks, Adam — the chained-delegation case is central to what we’re testing.

GATE evaluates authority paths through a delegation graph. Our tests cover upstream revocation invalidating a downstream path even while the credential’s signature remains valid, and preserving an independent valid path authorizing the same action. Revoking both paths produces DENY.

A root_rev pointer helps with root revocation, but we also need to handle an intermediate grant being revoked while the root remains active. Those path semantics are the behavior we’ve validated; I’d need to check the current traversal implementation before quoting a precise depth or performance bound.

Since publishing this article, we’ve also validated an external integration: a signed CAEP session-revoked event from the independent caep.dev test transmitter, mapped to a configured GATE grant, changed the hosted decision from ALLOW to DENY. That demonstrates one configured interoperability flow, not universal cross-domain coverage or a guaranteed propagation time.

We’re also testing issuer-source freshness separately from GATE’s own snapshot freshness: a recently refreshed cache alone cannot prove that the latest upstream revocation has arrived.

Does your implementation handle intermediate-grant revocation while preserving independent alternative paths? Thanks for clarifying the labagent.tech reference.

Collapse
 
reidmarlow profile image
Reid Marlow •

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.

Collapse
 
projetxana profile image
Projetxana •

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.