DEV Community

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

Projetxana on October 09, 2026

Imagine an AI agent receives a valid, signed delegation to perform an action. The user later revokes that delegation in another system. The signa...
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.

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.