Relates to: #850, #914, #1059
Problem:
An agent that calls a tool its OAuth grant does not cover is told to re-authenticate, does so, gets the identical 403, and loops. The failure it hit is unfixable by re-running the same grant, but nothing in the tool result says so.
Every upstream 401 and 403 during invocation maps to the single AuthToolFailureCode value connection_rejected, with a message reading "Re-authenticate or update the connection". authRecovery in packages/core/sdk/src/auth-tool-failure.ts:40 then attaches startOAuthTool: "executor.coreTools.oauth.start" to every auth failure, regardless of code. The upstream body that would distinguish the two cases survives only as opaque details, never reaching the branch that picks the code:
// packages/plugins/openapi/src/sdk/backing.ts:698
if (result.status === 401 || result.status === 403) {
return openApiAuthToolFailure({
code: "connection_rejected",
status: result.status,
message: `Upstream rejected credentials for "${integration}" with HTTP ${result.status}. Re-authenticate or update the connection "${input.credential.connection}" before retrying this tool.`,
...
details: result.error,
});
}
The enum has no variant that could express the scope case:
// packages/core/sdk/src/auth-tool-failure.ts:3
export type AuthToolFailureCode =
| "connection_value_missing"
| "connection_rejected"
| "oauth_connection_missing"
| "oauth_refresh_failed"
| "oauth_reauth_required";
The GraphQL plugin repeats the branch at packages/plugins/graphql/src/sdk/plugin.ts:1147, and the MCP plugin at packages/plugins/mcp/src/sdk/plugin.ts:291. These are three independent copies, not one shared helper. The MCP copy already says "may lack access or required scope" in its 403 prose, but still emits connection_rejected and the same recovery block, so a caller can act on the prose only by parsing English.
Line numbers are against 738628132. The deployed cloud build's revision is not observable from the client.
Reproduction:
Needs an upstream that returns a distinguishable scope-insufficient 403. A Google connection works: authorize it for one product, then call an operation from another.
- Create an OAuth connection whose grant omits a scope one operation requires, while other operations keep working (so it is visibly a scope problem, not a revoked token).
- Invoke the tool for that operation.
- The upstream returns 403 with
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT" (Google) or insufficient_scope (RFC 6750).
- Run
oauth.start for the same connection against the same client, retry, and observe the identical failure.
Expected vs actual:
Expected: a 403 indicating a scope shortfall carries a distinct code, so a caller knows re-authenticating the same grant will not help.
Actual: every 401 and every 403 from all three plugins returns connection_rejected, the same re-authenticate message, and the same oauth.start hint, with no branch on the upstream body.
Proposed solution:
Add an oauth_scope_insufficient variant to AuthToolFailureCode, and make authRecovery branch on the code so that variant drops startOAuthTool and oauthInstructions in favour of guidance to reconnect with a broader scope.
At each of the three collapse sites, inspect the upstream body before defaulting to connection_rejected. The OpenAPI plugin backs many providers, so it should recognise both Google's nested reason and the generic RFC 6750 insufficient_scope. Leave 401 and any 403 without a recognised reason on the existing code, so the change is purely additive.
One shared code with a per-plugin detector seems right, given the three upstream error shapes differ. Whether the message should name the specific missing scope when the body provides it is worth deciding before implementing.
packages/plugins/mcp/src/sdk/plugin.test.ts:622 currently pins the old behaviour, asserting connection_rejected uniformly across a [401, 403] parametrization, and would need a scope-insufficient fixture alongside it.
Relates to: #850, #914, #1059
Problem:
An agent that calls a tool its OAuth grant does not cover is told to re-authenticate, does so, gets the identical 403, and loops. The failure it hit is unfixable by re-running the same grant, but nothing in the tool result says so.
Every upstream 401 and 403 during invocation maps to the single
AuthToolFailureCodevalueconnection_rejected, with a message reading "Re-authenticate or update the connection".authRecoveryinpackages/core/sdk/src/auth-tool-failure.ts:40then attachesstartOAuthTool: "executor.coreTools.oauth.start"to every auth failure, regardless of code. The upstream body that would distinguish the two cases survives only as opaquedetails, never reaching the branch that picks the code:The enum has no variant that could express the scope case:
The GraphQL plugin repeats the branch at
packages/plugins/graphql/src/sdk/plugin.ts:1147, and the MCP plugin atpackages/plugins/mcp/src/sdk/plugin.ts:291. These are three independent copies, not one shared helper. The MCP copy already says "may lack access or required scope" in its 403 prose, but still emitsconnection_rejectedand the same recovery block, so a caller can act on the prose only by parsing English.Line numbers are against
738628132. The deployed cloud build's revision is not observable from the client.Reproduction:
Needs an upstream that returns a distinguishable scope-insufficient 403. A Google connection works: authorize it for one product, then call an operation from another.
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT"(Google) orinsufficient_scope(RFC 6750).oauth.startfor the same connection against the same client, retry, and observe the identical failure.Expected vs actual:
Expected: a 403 indicating a scope shortfall carries a distinct code, so a caller knows re-authenticating the same grant will not help.
Actual: every 401 and every 403 from all three plugins returns
connection_rejected, the same re-authenticate message, and the sameoauth.starthint, with no branch on the upstream body.Proposed solution:
Add an
oauth_scope_insufficientvariant toAuthToolFailureCode, and makeauthRecoverybranch on the code so that variant dropsstartOAuthToolandoauthInstructionsin favour of guidance to reconnect with a broader scope.At each of the three collapse sites, inspect the upstream body before defaulting to
connection_rejected. The OpenAPI plugin backs many providers, so it should recognise both Google's nestedreasonand the generic RFC 6750insufficient_scope. Leave 401 and any 403 without a recognised reason on the existing code, so the change is purely additive.One shared code with a per-plugin detector seems right, given the three upstream error shapes differ. Whether the message should name the specific missing scope when the body provides it is worth deciding before implementing.
packages/plugins/mcp/src/sdk/plugin.test.ts:622currently pins the old behaviour, assertingconnection_rejecteduniformly across a[401, 403]parametrization, and would need a scope-insufficient fixture alongside it.