Skip to content

feat(go): declare the runtime security scheme on the agent card - #483

Merged
edenreich merged 1 commit into
mainfrom
feat/declare-security-schemes
Oct 4, 2026
Merged

edenreich merged 1 commit into
mainfrom
feat/declare-security-schemes

Conversation

@edenreich

Copy link
Copy Markdown
Contributor

Summary

A generated Go agent now declares on its agent card how /a2a is secured, for both auth modes the ADK supports and from runtime config alone.

Closes #482.

Before, main.go.tmpl emitted the server.OIDCSecuritySchemes block only when the manifest set spec.server.auth.enabled, and nothing declared the static bearer-token mode. An agent started with A2A_AUTH_ENABLED=true or A2A_AUTH_TOKEN could protect /a2a while its card had securitySchemes: null, and the ADK logged authentication is enabled but the agent card declares no securitySchemes.

Changes

  • main.go.tmpl always generates advertiseAuthScheme, called after the server is built. It reads cfg.A2A.AuthConfig at startup and adds the matching scheme and requirement to the served card: openIdConnect when A2A_AUTH_ENABLED is true, http bearer when A2A_AUTH_TOKEN is set. With no auth it does nothing.
  • A scheme the manifest already declares under the same name (spec.card.securitySchemes) is kept as written, and no duplicate requirement is added. The old block overwrote it.
  • A2A_AUTH_TOKEN is listed in the generated CONFIGURATIONS.md, and as an optional commented entry in .env.example for manifests without spec.server.auth.enabled.

Testing

  • TestGoMainTemplate_AdvertisesRuntimeAuthScheme renders main.go with and without spec.server.auth.enabled.
  • Generated go-agent-builtin-tools (auth off in the manifest) and go-agent-auth, built them and read the served card:
Manifest Runtime Card
auth off no auth no securitySchemes
auth off A2A_AUTH_TOKEN bearer http scheme and requirement
auth off A2A_AUTH_ENABLED against Keycloak openId scheme with the issuer's discovery URL and requirement
static bearer and apiKey declared A2A_AUTH_TOKEN the two static schemes, unchanged, no duplicate

Follow-ups

  • inference-gateway/mock-agent: its main.go is in .adl-ignore and synced with the template by hand, so it needs a re-sync after this is released. Not filed yet.
  • inference-gateway/cli: examples/a2a-auth can then drop auth.oidc.issuer_url.
  • TypeScript and Rust templates are untouched.

The generated server declared its OIDC scheme only when the manifest set
spec.server.auth.enabled, and never declared the static bearer-token mode.
An agent started with A2A_AUTH_ENABLED or A2A_AUTH_TOKEN could protect /a2a
while its card said nothing about how to authenticate.

main.go now always carries advertiseAuthScheme, which reads the auth mode
from runtime config at startup and adds the matching scheme and requirement
to the served card: openIdConnect for A2A_AUTH_ENABLED, http bearer for
A2A_AUTH_TOKEN. A scheme the manifest declares under the same name is kept.

A2A_AUTH_TOKEN is listed in the generated CONFIGURATIONS.md and .env.example.

Closes #482
@edenreich
edenreich requested a review from a team as a code owner October 4, 2026 23:44
@edenreich
edenreich merged commit c068fc3 into main Oct 4, 2026
5 checks passed
@edenreich
edenreich deleted the feat/declare-security-schemes branch October 4, 2026 23:49
inference-gateway-releaser Bot added a commit that referenced this pull request Oct 4, 2026
## [0.67.0](v0.66.4...v0.67.0) (2026-10-04)

### ✨ Features

* **go:** declare the runtime security scheme on the agent card ([#483](#483)) ([c068fc3](c068fc3)), closes [#482](#482)
@inference-gateway-releaser

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.67.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

edenreich added a commit to inference-gateway/cli that referenced this pull request Oct 5, 2026
… a2a-auth example (#1542)

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

```yaml
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

- `inference-gateway/docs` needs the new `auth` block documented,
tracked in inference-gateway/docs#985.
- `inference-gateway/adl-cli`: generated Go agents did not declare their
runtime auth mode in the card's `securitySchemes`. Fixed in
inference-gateway/adl-cli#483 (closes inference-gateway/adl-cli#482),
released in v0.67.0.
- `inference-gateway/mock-agent`: its hand-maintained `main.go` was
re-synced with the template in inference-gateway/mock-agent#172,
released in 0.5.0, which the example pins.
- inference-gateway/adl-cli#485 fixes an older template bug found during
that sync, a doc comment glued onto the closing brace above it. It does
not affect this PR.
- `inference-gateway/adk` could expose a typed HTTP status error
(optional follow-up, see above).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Declare securitySchemes on the agent card for every runtime auth mode

1 participant