
Nnenna NdukweAt Qodo, we've publicly described how PR-Agent became an A2A agent for MOSAICO, a research project...
At Qodo, we've publicly described how PR-Agent became an A2A agent for MOSAICO, a research project coordinating software-engineering agents. Other agents can delegate code reviews and receive results. When you expose an agent this way, how do you control who can call it and verify that rejected requests never reach it?
This walkthrough puts a sample agent behind agentgateway, checks caller credentials, and records which requests reach the backend!
Agent2Agent, or A2A, defines how independently deployed agents exchange messages, tasks, and results. An agent card advertises an agent's capabilities and connection details. A client can discover a reviewer, send work, and receive its response without sharing the reviewer's implementation.
Qodo's public PR-Agent integration illustrates that boundary. MOSAICO can delegate a review to PR-Agent and receive review output. That establishes a useful interaction, but operating an endpoint also requires decisions about callers, credentials, and the resources each request may access.
For the access tests, a deterministic Hello World agent is enough. It returns a greeting instead of reviewing code. That keeps model behavior out of the experiment and makes backend execution easy to observe.
Agentgateway's A2A example places a proxy between the client and the agent. The tested setup uses agentgateway 1.6.0, Python 3.13.4, A2A Python SDK 1.1.0, and the A2A 1.0 JSON-RPC binding. Its executor comes from this pinned upstream sample.
Requests go to the gateway on port 18760; the sample backend listens on 127.0.0.1:18761. Request logging and an executor-entry record let the test compare what the gateway returned with what the agent actually received and ran.
The tested request path. The backend remains reachable directly from the local machine; the diagram doesn't imply network isolation.
Fetching the agent card through the gateway returned a supportedInterfaces URL pointing back to port 18760. That helps clients continue through the proxy. It doesn't prevent someone who knows a reachable backend address from calling it directly.
The route combines JWT authentication with an authorization rule. A JSON Web Token carries signed claims about its issuer, intended audience, and caller. The gateway checks the signature against a JSON Web Key Set, or JWKS, containing the issuer's public key.
Here's the route portion of the tested configuration:
binds:
- port: 18760
listeners:
- routes:
- policies:
a2a: {}
jwtAuth:
mode: strict
issuer: https://issuer.example.test
audiences: [a2a-demo]
jwks:
file: ./jwks.json
authorization:
rules:
- require: 'jwt.role == "reviewer"'
backends:
- host: 127.0.0.1:18761
The issuer here is a synthetic test identity, not a service to sign into. The runner creates an RSA key and short-lived tokens locally. In this release, the JWKS file path resolves from the process's working directory.
strict requires a valid token. The issuer and audience constrain which tokens qualify; a separate rule requires the signed role claim to equal reviewer. That role is this demo's convention, not an A2A-defined permission.
Keep those checks explicit. Agentgateway's default optional authentication mode permits requests without a token. A configuration that validates credentials when present can behave differently from one that requires credentials on every request.
Each test sends a real SendMessage request with a unique message ID. The same identifier connects the request to the backend's records. Accepted cases must return the greeting without a JSON-RPC error; an HTTP 200 alone doesn't establish successful A2A execution.
These were the observed outcomes with the configuration above:
| Caller credentials | HTTP response | Backend requests / executor calls |
|---|---|---|
| Valid token, reviewer role | 200 | 1 / 1 |
| No token | 401 | 0 / 0 |
| Wrong audience | 401 | 0 / 0 |
| Wrong role | 403 | 0 / 0 |
| Missing role claim | 403 | 0 / 0 |
| Invalid signature or expired token, tested separately | 401 each | 0 / 0 each |
A valid request after the rejection cases also completed. Removing only the authorization rule let a valid token with the wrong role reach the executor. That control checks whether the role rule actually explains the rejection.
One configuration change exposed a useful failure case. Replacing the requirement with this rule changed the result for a valid token missing role:
authorization:
rules:
- deny: 'jwt.role != "reviewer"'
That request returned 200 and executed once. With require, it returned 403 and never reached the backend.
The documented evaluation rules explain why: accessing a missing claim makes the expression fail. A failed requirement rejects the request. A failed deny expression doesn't match; with this single deny rule and no allow rules, the request proceeds.
JWT authentication stayed strict in both configurations. A missing token still returned 401. The finding concerns a missing application claim in an otherwise valid token, under these exact policies.
The gateway established which tested requests could reach this endpoint. It didn't decide whether a caller may read a particular repository, retrieve a private diff, or write a change. A real reviewer needs those checks where it accesses the repository and invokes its tools.
The bypass control matters too: calling this local backend directly without a token returned 200 and executed once. A deployment needs to restrict direct backend access or enforce equivalent checks there. Rewriting the agent card isn't a network access control.
This reproduction used synthetic credentials, HTTP on a development machine, and a greeting executor. It didn't validate production TLS, identity-provider integration, streaming recovery, or repository permissions. The gateway's standalone listener binds a wildcard address; the backend and administrative endpoints were configured on loopback.
For your own agent, start with an allowed request and specific rejected requests. Check both the response and backend execution. Then remove or change the relevant policy in an isolated test to verify which check caused the rejection. That gives you evidence for the boundary you've configured—and a clear account of what still needs protection.