Skip to content

Let two prefixless connections share an agent when one names its issuer - #56

Merged
haruotsu merged 2 commits into
mainfrom
declare-jwt-issued-tokens
Sep 3, 2026
Merged

Let two prefixless connections share an agent when one names its issuer#56
haruotsu merged 2 commits into
mainfrom
declare-jwt-issued-tokens

Conversation

@haruotsu

@haruotsu haruotsu commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Why

An agent that reads one service and writes to another is refused today when both connections declare token_prefixes: []: elimination_problems allows at most one connection per agent to accept tokens by elimination. The rule is right — two fully anonymous connections cannot be told apart, and a token from either authorization would pass as the other's — but it is blunter than the runtime it protects. Since tokens are judged by the issuer their claims name (accepts_tokenissued_here), a provider whose access tokens are JWTs issued by its own host announces itself in every token. It is not anonymous at all; validate just had no way to know the tokens would arrive as JWTs.

What

  • tokens: {format: jwt} on a connection says its tokens are JWTs whose iss claim names the connection's own service (its hosts and its OAuth endpoints, the same set issued_here already matched). Schema, README, and gete connections know about it.
  • The runtime holds the connection to it. accepts_token refuses anything that is not such a JWT — an opaque token, a JWT whose claims cannot be read, a JWT that names no issuer or names someone else's — whatever else would have taken it. Without that, the declaration would loosen validate while promising nothing where it counts. A format this gete cannot judge (a resolved declaration outlives the gete that wrote it) accepts nothing rather than falling back to elimination.
  • elimination_problems counts only connections with neither prefixes nor a declared format. One anonymous connection per agent stays the ceiling; a jwt-declaring one no longer occupies it. The remaining anonymous one is already protected from the JWT side: a JWT whose issuer names the other service is refused by accepts_token today.
  • Two declaring connections that name one issuer host are refused together, for the same reason the anonymous pair is — each would accept a JWT naming the shared issuer, so neither could tell the other's token from its own. Not in the issue, but the declaration opens that route and the function exists to close it.
  • Prefixes next to a declared format are reported. The format decides on its own, so the prefixes would never be read, and a reader would have to know that to see which rule the connection is held to.
  • gete connections prints (none: jwt issued by this service) where it printed (none: by elimination). That difference is what decides whether an agent may hold the connection beside another, so it must not read as the elimination it replaces.
  • A refusal names the promise the token missednot the jwt this connection declares, a JWT that names no issuer — because an operator who turned the provider's setting off has every token refused at once and nothing else in the log would say why.

Zendesk

Whether a provider issues JWTs can be a per-client setting — Zendesk does so only with token expiry turned on — so the catalog must not promise it for every installation. The entry keeps token_prefixes: []; its setup says how to declare the format and what it costs, next to the setting itself: turn expiry off again and every Zendesk token is refused until the declaration goes with it.

Its examples.accepts had to go. Which of the two shapes arrives is that same setting, so an example fixing one contradicts an installation that has the other — an installation declaring the format would have seen examples.accepts: '...' is not accepted from validate for a catalog entry it never wrote. What is refused is refused either way, and the rejects list stays.

# gete.yaml
connections:
  zendesk:
    base_url: https://acme.zendesk.com
    tokens:
      format: jwt      # tokens are JWTs issued by acme.zendesk.com, nothing else

Tests

  • schema: the block is accepted; an unknown format, an unknown key under it, and an empty tokens are refused
  • accepts_token: a JWT from the service's own hosts, OAuth endpoints and installation root is accepted; an opaque token, a foreign issuer, a missing issuer, unreadable claims, Google's access tokens and ID tokens are refused; a foreign prefix no longer decides; an unknown format accepts nothing
  • elimination_problems: a declaring connection does not occupy the elimination slot, two from different services are fine, two sharing an issuer host are reported as one pairing; the anonymous pair is unchanged
  • connection_problems: prefixes beside a format are reported
  • validate, end to end: zendesk + internal-api on one agent passes with the declaration and still fails without it
  • resolve: the declaration travels to the runtime and refuses there
  • runtime: describe_token names the unmet promise, usable_token refuses the opaque token without logging it
  • catalog conformance: Zendesk promises no format for everyone, records no accepted shape, and an installation may declare the format on top of it

uv run pytest (863 passed, e2e included), ruff check, ruff format --check and mypy all pass.

An agent that reads one service and writes to another was refused when
both connections declared token_prefixes: [], because only one of an
agent's connections may accept tokens by elimination. The rule is right
- two anonymous connections cannot be told apart, and a token from
either authorization would pass as the other's - but it was blunter
than the runtime it protects. Since tokens are judged by the issuer
their claims name, a service whose access tokens are JWTs issued by its
own host announces itself in every token; validate simply had no way to
know the tokens would arrive as JWTs.

tokens.format: jwt is the word for it. The runtime holds the connection
to it: a token that is not a JWT its own service issued is refused,
whatever else would have taken it, so the declaration cannot loosen
validate while promising nothing where it counts. A format this gete
cannot judge accepts nothing rather than falling back to elimination -
a resolved declaration outlives the gete that wrote it. Only connections
with neither prefixes nor a format are now counted as accepting by
elimination, so one anonymous connection per agent stays the ceiling
and a declaring one no longer occupies it. Two declaring connections
that name one issuer host are refused together for the same reason the
anonymous pair is: neither could tell the other's token from its own.

Whether a provider issues such tokens can be a per-client setting -
Zendesk does only with token expiry turned on - so the catalog must not
promise it for every installation, and the declaration belongs in
gete.yaml. Zendesk's setup says so next to the setting, including what
it costs: turn expiry off again and every token is refused until the
declaration goes with it. Its accepted example had to go, because which
of the two shapes arrives is that setting and an example fixing one
would contradict the other; what is refused is refused either way.

gete connections prints the declared format where it printed "(none: by
elimination)", which is the difference that decides what an agent may
hold, and a refusal now names the promise the token missed rather than
leaving an operator to guess at a log full of them.

🤖 Generated with Claude Code
The declaration was let out of the elimination count on the grounds that
its tokens name their issuer, and two declaring connections naming one
issuer were refused together. An anonymous connection judges the same
way: it takes a JWT whose iss names its own authorization server and
refuses one naming anything else. So a single authorization server in
front of two services confuses a declaring connection and an anonymous
one exactly as it confuses two declaring ones - and that pairing is the
one the declaration was written to allow, so nothing else was left to
refuse it. Two services behind one authorization server is not an exotic
shape, and validate took them.

The issuer check now runs over every connection that judges a token by
who issued it, declaring or not. The anonymous pair is skipped there
because it is already refused for saying nothing about itself at all,
and a connection held to a prefix stays out entirely: the prefix decides
before any issuer does, so no token of another's reaches its issuer.

describe_token read the issuer before the format, so a connection
declaring a format this gete cannot judge answered "a JWT whose issuer
does not name this service" about a token its own service had issued.
What refused it is the declaration being newer than the gete reading it,
and that is where the operator has to be sent - not after a token that
may be exactly right.

token_format was read as a truth value in five places and against None
in accepts_token. Against None everywhere now, so the site that decides
what a token is judged by cannot drift from the ones that decide whether
an agent may hold the connection at all.

🤖 Generated with Claude Code
@haruotsu
haruotsu merged commit 625a031 into main Sep 3, 2026
3 checks passed
@haruotsu
haruotsu deleted the declare-jwt-issued-tokens branch September 3, 2026 02:06
@github-actions github-actions Bot mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant