Skip to content

auth: scope-insufficient 403 collapses into connection_rejected #1381

Description

@Mark-Life

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.

  1. 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).
  2. Invoke the tool for that operation.
  3. The upstream returns 403 with "reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT" (Google) or insufficient_scope (RFC 6750).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions