# 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`](https://docs.diagrid.io/develop/agents/enterprise-identity/oauth-helper) — an agent without the middleware never returns an `oauth.*` code.
- **MCP identity enforcement** runs on the way out, when your agent calls a server that [requires a user identity](https://docs.diagrid.io/develop/agents/enterprise-identity/require-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](https://docs.diagrid.io/operate/project-operations/idp-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](https://docs.diagrid.io/develop/agents/enterprise-identity/custom-idp-tokens).

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`](https://docs.diagrid.io/develop/agents/enterprise-identity/oauth-helper#configure) 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](https://docs.diagrid.io/operate/project-operations/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](https://docs.diagrid.io/develop/agents/enterprise-identity/require-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](https://docs.diagrid.io/develop/agents/enterprise-identity/require-user-identity#serve-both-user-and-service-callers).
- `oauth.missing_scope` (403) — the user's token lacks a scope the server requires. Grant it in your identity provider, or re-run `diagrid mcpserver access user-identity require` with 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:

1. **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`](https://docs.diagrid.io/develop/agents/enterprise-identity/require-user-identity#turn-on-enforcement); there is no setting that forwards a user opportunistically when one happens to be present.

2. **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](https://docs.diagrid.io/develop/agents/enterprise-identity/on-behalf-of#send-the-callers-identity) — 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.
