Skip to content

feat(a2a): authenticate to agents with per-agent credentials, with an a2a-auth example - #1542

Merged
edenreich merged 6 commits into
mainfrom
feat/a2a-agent-auth
Oct 5, 2026
Merged

edenreich merged 6 commits into
mainfrom
feat/a2a-agent-auth

Conversation

@edenreich

@edenreich edenreich commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lets infer authenticate to A2A agents that protect /a2a. An agent entry in agents.yaml can carry an auth block with a static bearer token or an OIDC client-credentials grant.

Closes #1514 and closes #1512, which adds examples/a2a-auth on top of it.

agents:
  - name: research
    url: https://research.example.com
    auth:
      token_env: RESEARCH_AGENT_TOKEN
  - name: billing
    url: https://billing.example.com
    auth:
      oidc:
        client_id: infer
        client_secret_env: BILLING_AGENT_CLIENT_SECRET
        audience: billing-agent

How it works

  • Secrets stay in the environment. The auth block names environment variables and never holds a value. LoadYAML expands ${VAR} over the whole file and every infer agents add/update/remove writes the result back, so a token: ${VAR} field would have ended up on disk in plain text. With variable names, the file, infer agents list and infer agents show cannot leak a secret.
  • Every request is covered. The credentials are added by a transport inside a2ainfra.NewClient, the one constructor behind the agent card fetch, SendMessage, GetTask polling, CancelTask, the liveness probe and infer agents status. ADK Config.Headers does not reach the card fetch, a transport does.
  • Credentials stay on their agent. agent_url comes from the model, so the transport only authenticates requests to the scheme, host and port of the configured agent. A redirect target or another port on the same host gets nothing.
  • The agent card says where tokens come from. As the A2A protocol defines, the agent declares how to authenticate in the securitySchemes of its card. The CLI reads the token endpoint from there: the discovery URL of an openIdConnect scheme, or the token URL of an oauth2 client-credentials flow, with the scopes the card's securityRequirements list. agents.yaml only holds the client id, the secret variable and an optional audience.
  • Optional issuer_url, pin and fallback. The client secret goes to whatever endpoint the card names, and the card is fetched unauthenticated. With auth.oidc.issuer_url set, a card that points outside that issuer is refused before the secret is sent. The same field is the fallback for a card that declares no scheme at all, as agents generated before ADL CLI v0.67.0 do.
  • OIDC tokens are reused. One clientcredentials token source per grant lives for the process, fetches once and refreshes shortly before expiry. This adds golang.org/x/oauth2 as a direct dependency.
  • Auth failures are named. A 401 or 403, a missing variable or a failed token request reaches the model as Authentication failed for A2A agent "<name>": ..., without the response body. Background polling ends the job on it instead of retrying forever.

Example (#1512)

examples/a2a-auth/ runs one mock-agent 0.5.0 behind A2A_AUTH_TOKEN and one behind OIDC against Keycloak, with a tokenless mock model and no provider API key. The credentials are in the example's .infer/agents.yaml, with the secrets read from the environment. Each agent declares its scheme on its card, so the OIDC entry only holds the client and infer reads the token endpoint from the card.

Task Result
task token delegation to the token agent completes
task oidc delegation to the OIDC agent completes with a token from Keycloak
task wrong-token Authentication failed for A2A agent "token-agent": the agent rejected the request with status 401
task missing-token Authentication failed for A2A agent "token-agent": environment variable TOKEN_AGENT_TOKEN is not set
task wrong-secret Authentication failed for A2A agent "oidc-agent": fetching the OIDC token: the token endpoint answered with status 401 unauthorized_client

Keycloak and the OIDC agent share a network namespace, so the issuer is http://localhost:8090/realms/a2a for infer on the host and for the agent in its container. docs/examples.md lists the example and docs/agents-configuration.md links it.

Testing

  • infrastructure/auth_test.go: the header on the card fetch, SendTask, GetTask and CancelTask, no header on another origin or after a cross-origin redirect, the OIDC token endpoint read from an openIdConnect and an oauth2 card scheme, one token fetch across requests plus a refetch once the token expires, a card with no usable scheme or outside the pinned issuer refused before the secret is sent, the auth-failure message for 401 and 403, an unset variable.
  • config/agents_test.go: origin matching and a load and rewrite of agents.yaml that never contains the secret.
  • submit_task_error_handling_test.go: a rejected submission and a rejected poll both end with the named error.
  • Manual: all five example scenarios above and task status run against mock-agent 0.5.0 and Keycloak 26.3.2 with a local build, with no issuer_url configured.
  • task precommit:run passes.

Not in this PR

  • infer agents add/update flags for auth. Edit agents.yaml directly for now.
  • Credentials for an agent that is only in INFER_A2A_AGENTS with no agents.yaml entry.
  • Authentication on artifact downloads.
  • ADK v0.33.0 has no typed HTTP error, so a 401 or 403 is recognised from the error text. A test pins the wording. A typed error in inference-gateway/adk would remove the text match and the roughly 6 second retry before a credential error surfaces on task requests.

Affected repos

An agent entry in agents.yaml can now carry an auth block: a static bearer
token (token_env) or an OIDC client-credentials grant (oidc). The block names
the environment variables that hold the secrets, so the file, the logs and
infer agents list never contain them.

The credentials ride on a transport inside NewClient, so they reach every
request to the agent: the card fetch, SendMessage, GetTask polling,
CancelTask and the liveness probe. They are only sent to the origin of the
configured agent, never to a redirect target or another port on the host.
An OIDC token is fetched once per grant and refreshed before it expires.

A 401 or 403, a missing variable or a failed token request reaches the model
as "Authentication failed for A2A agent <name>". Background polling ends the
job on it instead of retrying forever.

Closes #1514
@edenreich
edenreich requested a review from a team as a code owner October 4, 2026 23:10
@github-project-automation github-project-automation Bot moved this to Todo in 2026 Oct 4, 2026
The A2A protocol has the agent declare how to authenticate in the
securitySchemes of its card, so the issuer does not belong in agents.yaml.
The grant now takes its token endpoint from the card: the discovery URL of an
openIdConnect scheme, or the token URL of an oauth2 client-credentials flow,
with the scopes the card's security requirements list.

auth.oidc keeps the client id, the secret variable and the audience.
issuer_url becomes an optional pin. With it set, a card that points outside
that issuer is refused, so a tampered card cannot collect the client secret.

Refs #1514
…heme

An agent whose card has no securitySchemes cannot say where its tokens are
issued. The mock agent is one of them today. auth.oidc.issuer_url now also
serves as the fallback issuer for such a card, next to pinning the issuer a
declaring card may point at. Without it the request fails with an
authentication error that says to set it.

Refs #1514
Two mock agents protect /a2a, one with a static bearer token and one with
OIDC against Keycloak. infer authenticates to each with the credentials in
the example's agents.yaml, with the secrets read from the environment. The
model is a tokenless mock, so no provider API key is needed.

The Taskfile covers an authorized delegation to each agent, a wrong token, a
missing token and a wrong client secret, the last three reported as
authentication failures.

Closes #1512
@edenreich edenreich changed the title feat(a2a): authenticate to agents with per-agent credentials feat(a2a): authenticate to agents with per-agent credentials, with an a2a-auth example Oct 4, 2026
…2a-auth

mock-agent 0.5.0 declares its runtime auth scheme on the agent card, so the
OIDC agent's card now points at the Keycloak realm. Drop issuer_url from the
example's agents.yaml so it shows the card-driven path, and pin the image to
0.5.0, since an older cached latest has no scheme and needs issuer_url.
@edenreich
edenreich merged commit 658701e into main Oct 5, 2026
13 checks passed
@edenreich
edenreich deleted the feat/a2a-agent-auth branch October 5, 2026 17:08
@inference-gateway-releaser

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.226.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

1 participant