Troubleshooting
A rejected agent invocation carries a machine-readable code. Three different layers can raise one, and which layer it was tells you where to look:
- Catalyst inbound auth runs in the sidecar, in front of your agent. It verifies the token the caller presented and exchanges it for a Catalyst-issued user token. Its codes are the ones a plain HTTP caller hits, because they are returned before any of your code runs.
- The helper library runs inside your agent, once the request reaches it. Its codes only appear if you attached
OAuthMiddleware— an agent without the middleware never returns anoauth.*code. - MCP identity enforcement runs on the way out, when your agent calls a server that requires a user identity. Its codes are returned to your agent on the MCP call, not to whoever invoked the agent.
Rejection codes
| Code | HTTP status | Raised by | Meaning |
|---|---|---|---|
malformed_jwt | 401 | Catalyst inbound auth | The presented token is not a well-formed JWT, or is missing the iss claim. |
invalid_grant | 401 | Catalyst inbound auth | The token was refused — most often because its issuer is not federated. |
temporarily_unavailable | 503 | Catalyst inbound auth | Catalyst could not verify or exchange the token right now. Retryable. |
oauth.missing_token | 401 | Helper library | No X-Diagrid-User-Token header on the request. |
oauth.invalid_signature | 401 | Helper library | The token's signature did not verify. |
oauth.invalid_issuer | 401 | Helper library | The token's iss claim does not match the expected issuer. |
oauth.invalid_audience | 401 | Helper library | The token's aud claim does not match the expected audience. |
oauth.expired | 401 | Helper library | The token is past its expiry. |
oauth.decode_error | 401 | Helper library | The token could not be decoded. |
oauth.invalid_token | 401 | Helper library | The token failed validation for another reason. |
oauth.missing_scope | 403 | Helper library | The user authenticated but lacks a required scope. |
oauth.not_configured | 503 | Helper library | The middleware has no issuer to validate against. |
oauth.verifier_unavailable | 503 | Helper library | The middleware has not yet loaded the issuer's signing keys. Retryable. |
obo.exchange_failed | 502 | MCP identity enforcement | Catalyst could not issue a delegated token for the outbound MCP call. |
Catalyst inbound auth codes
malformed_jwt
What you sent in X-Diagrid-User-Token is not a parseable JWT, or it parses but carries no iss claim. Check that you sent the token itself and not a wrapper around it, that the Bearer prefix is present, and that nothing truncated the header in transit.
invalid_grant
Catalyst verified the token's shape but refused it. The most common cause is an issuer that has not been federated: your token comes from a provider Catalyst does not trust for this deployment. Confirm a federation exists for the token's iss claim and is delivered to the region serving the project. This is the failure to expect first when using a custom identity provider token.
A token that is well-formed and federated but expired also lands here. For CLI callers, refresh credentials with diagrid login. For a custom identity provider token, re-acquire a fresh JWT from your provider's tooling.
temporarily_unavailable
Catalyst could not reach the service that verifies and exchanges tokens. This is a transient platform condition, not a problem with your token — retry with backoff.
Helper library codes
These are raised by OAuthMiddleware inside your agent and returned to the caller as the response body, {"error": "<code>"}. Read the rejected response to get the code — the middleware does not log a failed validation. It logs only when it cannot build a verifier at all, so diagrid agent logs <agent>, or diagrid app logs <app> if it runs as an app, is where you find oauth.not_configured and discovery warnings, not the codes above it.
Because Catalyst has already verified and exchanged the caller's token by the time the middleware runs, an oauth.* code usually points at the middleware's own configuration rather than at the caller.
oauth.missing_token
The request reached your agent with no X-Diagrid-User-Token header. If the route is meant to be reachable without a user — a health or readiness probe, for example — set require_auth=False on the OAuthConfig so unauthenticated routes can share the app.
oauth.invalid_signature
The signature on the presented JWT did not verify against the issuer's keys. This normally means the middleware resolved the wrong issuer. Leave issuer and jwks_uri unset so they are discovered from the sidecar, or confirm the values you set explicitly.
oauth.invalid_issuer and oauth.invalid_audience
The token's iss or aud claim does not match what the middleware expects. As above, prefer discovery over setting issuer and audience by hand. If you must set them, they have to match what Catalyst mints — not your upstream identity provider, whose token is exchanged before it reaches you.
oauth.expired
The token is past its expiry. Long-running invocations can outlive a short-lived token — check the gap between when the caller acquired the token and when the middleware saw it.
oauth.decode_error and oauth.invalid_token
The token could not be decoded, or failed validation for a reason with no more specific code. The middleware does not surface the underlying parser message, so decode the token you sent and confirm it is the Catalyst-issued one your sidecar set on X-Diagrid-User-Token, rather than your upstream provider's token or another credential entirely.
oauth.missing_scope
The user authenticated but the token does not carry a scope your policy requires. Confirm the scopes you declared in OAuthConfig(scopes={...}) and check that the identity provider issues them for this user — scope conventions per provider are covered in IdP federation.
oauth.not_configured and oauth.verifier_unavailable
The middleware could not verify the token at all: it either resolved no issuer, or has not yet fetched the issuer's signing keys. Confirm the agent runs with a Catalyst sidecar, since that is where the middleware discovers its issuer. Discovery runs on the first authenticated request rather than at startup, so a misconfigured agent starts cleanly and fails only once traffic arrives. oauth.verifier_unavailable is retryable and typically clears once the key fetch succeeds.
MCP identity enforcement codes
When an MCP server requires a user identity, the outbound call is checked before it is forwarded, and a rejected call never reaches the server. Your agent sees {"error": "<code>"} on the MCP call itself:
oauth.missing_token(401) — the invocation that reached your agent carried no user. A scheduled, pub/sub, or agent-to-agent trigger has none; route those callers through a server without enforcement.oauth.missing_scope(403) — the user's token lacks a scope the server requires. Grant it in your identity provider, or re-rundiagrid mcpserver access user-identity requirewith the scope dropped.oauth.decode_error(401) — the user token could not be read. Confirm the caller forwards the token Catalyst issued, unmodified.oauth.verifier_unavailable(503) — the identity could not be verified right now. Retryable.obo.exchange_failed(502) — Catalyst could not issue the delegated token. This is not a configuration problem on your side. Retry, and open a support case if it persists.
These four oauth.* codes are the same strings the helper library returns. Which layer raised one is settled by where you saw it: on the response to the agent's invocation it came from the middleware, on the response to an MCP call it came from enforcement.
The tool ran, but saw no user
The call succeeded and reached the MCP server, but the server reports no end user. Two causes, in the order worth checking:
-
The server does not require a user identity. This is the default, and it is the common answer. A server that does not require one never receives one — Catalyst removes the user's token before forwarding. Turn the requirement on with
diagrid mcpserver access user-identity require; there is no setting that forwards a user opportunistically when one happens to be present. -
The invocation carried no user in the first place. A scheduled, pub/sub, or cron trigger has no person behind it, so there is nothing to propagate. Confirm the agent was invoked with a user token, and that it builds its MCP client on the identity-aware HTTP client — a plain client sends no identity.
Once the requirement is on, this failure mode becomes visible instead of silent: a call with no user is rejected with oauth.missing_token rather than quietly running unattributed. That is the main reason to turn it on for any tool where attribution matters.