diff --git a/.changeset/auth-missing-invalid-split.md b/.changeset/auth-missing-invalid-split.md new file mode 100644 index 0000000000..ad172fcc80 --- /dev/null +++ b/.changeset/auth-missing-invalid-split.md @@ -0,0 +1,17 @@ +--- +"adcontextprotocol": minor +--- + +spec(errors): split `AUTH_REQUIRED` into `AUTH_MISSING` (correctable) + `AUTH_INVALID` (terminal) + +`AUTH_REQUIRED` conflated two operationally distinct cases: missing credentials (genuinely correctable — agent provides creds and retries) and rejected credentials (terminal — expired/revoked tokens require human rotation, not auto-retry). A buyer agent honoring `correctable` on revoked keys will retry-loop, hammering seller SSO endpoints in a pattern indistinguishable from a brute-force probe. + +**New codes:** +- `AUTH_MISSING` — `Recovery: correctable`. No credentials were presented; agent re-handshakes and retries. +- `AUTH_INVALID` — `Recovery: terminal`. Credentials were presented and rejected (expired / revoked / malformed signature). Requires human-driven credential rotation; auto-retry is counterproductive. + +**Backward compat:** `AUTH_REQUIRED` is retained in the enum as a deprecated alias (recovery: correctable) during the 3.x deprecation window. Sellers MUST migrate to the split codes; agents MUST handle all three. The `error-code-aliases.json` linter registry now maps `AUTH_REQUIRED → AUTH_MISSING` so storyboard references emit warnings. + +**Related:** adcp-client#1135 (TS SDK error-code drift fix that surfaced this spec gap), adcp-client#1147 (typed-error recovery alignment). + +Closes #3730. diff --git a/docs/building/operating/transport-errors.mdx b/docs/building/operating/transport-errors.mdx index caf75bb1bd..14cd28bfe4 100644 --- a/docs/building/operating/transport-errors.mdx +++ b/docs/building/operating/transport-errors.mdx @@ -93,7 +93,7 @@ When infrastructure rejects a request *before* tool dispatch (API gateway, rate- | Code | AdCP Error Code | When | |---|---|---| | `-32029` | `RATE_LIMITED` | Infrastructure rate limit before tool dispatch | -| `-32028` | `AUTH_REQUIRED` | Auth rejected by middleware before tool dispatch | +| `-32028` | `AUTH_MISSING` | No credentials presented; auth rejected by middleware before tool dispatch | | `-32027` | `SERVICE_UNAVAILABLE` | Infra health check fails, upstream down | These codes are in the JSON-RPC server-defined range (`-32000` to `-32099`). All other AdCP error codes use the tool-level path exclusively. @@ -331,7 +331,9 @@ const CODE_RECOVERY = { SERVICE_UNAVAILABLE: 'transient', CONFLICT: 'transient', INVALID_REQUEST: 'correctable', - AUTH_REQUIRED: 'correctable', + AUTH_MISSING: 'correctable', + AUTH_INVALID: 'terminal', + AUTH_REQUIRED: 'correctable', // deprecated alias for AUTH_MISSING POLICY_VIOLATION: 'correctable', PRODUCT_NOT_FOUND: 'correctable', PRODUCT_UNAVAILABLE: 'correctable', diff --git a/docs/media-buy/specification.mdx b/docs/media-buy/specification.mdx index e851c2e3a2..5363826db7 100644 --- a/docs/media-buy/specification.mdx +++ b/docs/media-buy/specification.mdx @@ -453,7 +453,8 @@ Common error codes: - `TERMS_REJECTED`: Buyer-proposed `measurement_terms` were rejected by the seller. Error details SHOULD identify which term failed and the seller's acceptable range or supported vendors. Recovery: adjust the proposed terms and retry, or omit `measurement_terms` to accept the product's defaults. - `REQUOTE_REQUIRED`: An `update_media_buy` request changes the parameter envelope (budget, flight dates, volume, targeting) the original quote was priced against. The `pricing_option` remains locked; the seller is declining the requested shape at that price. Distinct from `TERMS_REJECTED` (measurement) and `POLICY_VIOLATION` (content). Recovery: re-negotiate via `get_products` in `refine` mode against the existing `proposal_id` to obtain a fresh quote reflecting the new parameters, then resubmit the update against the new `proposal_id`. Sellers SHOULD populate `error.details.envelope_field` with the field path(s) that breached the envelope (e.g., `packages[0].budget`, `end_time`) so the buyer's agent can autonomously re-discover. - `VALIDATION_ERROR`: Request format or parameter errors -- `AUTH_REQUIRED`: Authentication is required to access this resource +- `AUTH_MISSING`: No credentials were presented. Recovery: correctable. +- `AUTH_INVALID`: Credentials were presented but rejected (expired / revoked / malformed). Recovery: terminal. ## Security Considerations diff --git a/docs/media-buy/task-reference/get_media_buy_delivery.mdx b/docs/media-buy/task-reference/get_media_buy_delivery.mdx index c9264fe1cb..77b4d44579 100644 --- a/docs/media-buy/task-reference/get_media_buy_delivery.mdx +++ b/docs/media-buy/task-reference/get_media_buy_delivery.mdx @@ -616,7 +616,8 @@ asyncio.run(main()) | Error Code | Description | Resolution | |------------|-------------|------------| -| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `AUTH_MISSING` | No credentials presented | Provide credentials via auth header | +| `AUTH_INVALID` | Credentials rejected (expired / revoked) | Human credential rotation required; do not auto-retry | | `MEDIA_BUY_NOT_FOUND` | Media buy doesn't exist | Verify media_buy_id | | `INVALID_DATE_RANGE` | Invalid start/end dates | Use YYYY-MM-DD format, ensure start < end | | `DATE_RANGE_NOT_SUPPORTED` | Product only supports lifetime reporting | Omit `start_date` and `end_date`. Check `reporting_capabilities.date_range_support` on the product. | diff --git a/docs/media-buy/task-reference/get_media_buys.mdx b/docs/media-buy/task-reference/get_media_buys.mdx index 4d0bc96acd..8e5e0038eb 100644 --- a/docs/media-buy/task-reference/get_media_buys.mdx +++ b/docs/media-buy/task-reference/get_media_buys.mdx @@ -402,7 +402,8 @@ Use cursor pagination for broad status queries to avoid large payloads: |------------|-------------|------------| | `MEDIA_BUY_NOT_FOUND` | Media buy ID does not exist | Verify `media_buy_id` | | `CONTEXT_REQUIRED` | No media buys found for the requested scope | Provide valid IDs/refs or broaden `status_filter`/pagination scope | -| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `AUTH_MISSING` | No credentials presented | Provide credentials via auth header | +| `AUTH_INVALID` | Credentials rejected (expired / revoked) | Human credential rotation required; do not auto-retry | ## Next Steps diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx index 790f6ee8ee..e0b6756539 100644 --- a/docs/media-buy/task-reference/get_products.mdx +++ b/docs/media-buy/task-reference/get_products.mdx @@ -969,7 +969,8 @@ Key rules to know before sending: | Error Code | Description | Resolution | |------------|-------------|------------| -| `AUTH_REQUIRED` | Authentication needed for full catalog | Provide credentials via auth header | +| `AUTH_MISSING` | No credentials presented | Provide credentials via auth header | +| `AUTH_INVALID` | Credentials rejected (expired / revoked) | Human credential rotation required; do not auto-retry | | `INVALID_REQUEST` | Brief too long or malformed filters | Check request parameters | | `PRODUCT_NOT_FOUND` | One or more referenced product IDs are unknown or expired | Remove invalid IDs and retry, or re-discover with a `brief` request | | `PROPOSAL_EXPIRED` | A referenced proposal ID has passed its `expires_at` timestamp | Re-discover with a new `brief` or `wholesale` request | diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx index 66ea481dbd..2c70d8a4cb 100644 --- a/docs/protocol/get_adcp_capabilities.mdx +++ b/docs/protocol/get_adcp_capabilities.mdx @@ -977,7 +977,8 @@ New fields: | Error Code | Description | Resolution | |------------|-------------|------------| -| `AUTH_REQUIRED` | Authentication needed | Provide credentials | +| `AUTH_MISSING` | No credentials presented | Provide credentials via auth header | +| `AUTH_INVALID` | Credentials rejected (expired / revoked) | Human credential rotation required | | `VERSION_UNSUPPORTED` | Declared `adcp_major_version` not in seller's `major_versions` | Call without `adcp_major_version` to discover supported versions, then retry | | `INTERNAL_ERROR` | Server error | Retry with backoff | diff --git a/scripts/error-code-aliases.json b/scripts/error-code-aliases.json index 3f1f442aad..418ed67659 100644 --- a/scripts/error-code-aliases.json +++ b/scripts/error-code-aliases.json @@ -1,4 +1,6 @@ { "_comment": "Registry of deprecated error-code aliases and their replacements. Shape: { aliases: { OLD_CODE: NEW_CODE } }. Both keys and values match /^[A-Z][A-Z0-9_]*$/; values MUST exist in static/schemas/source/enums/error-code.json. scripts/lint-error-codes.cjs reads this file and emits warnings on storyboard references to aliased codes during the deprecation window; once an alias is removed, stale references become lint errors. Intentionally empty at creation — add entries only when actively renaming a code.", - "aliases": {} + "aliases": { + "AUTH_REQUIRED": "AUTH_MISSING" + } } diff --git a/server/src/routes/helpers/oauth-error-detection.ts b/server/src/routes/helpers/oauth-error-detection.ts index e979b411eb..9ed28c9f7e 100644 --- a/server/src/routes/helpers/oauth-error-detection.ts +++ b/server/src/routes/helpers/oauth-error-detection.ts @@ -8,12 +8,16 @@ * - Transport 401 with WWW-Authenticate: Bearer → SDK's * `NeedsAuthorizationError` message begins with * "Agent requires OAuth authorization." - * - Agent returns an AdCP `AUTH_REQUIRED` error payload (200 body) when it + * - Agent returns an AdCP `AUTH_MISSING` error payload (200 body) when it * accepted the token at the transport layer but rejected it at the * application layer — common when a saved token has gone stale. + * `AUTH_REQUIRED` is the deprecated predecessor; matched during the 3.x window. + * `AUTH_INVALID` (terminal — credentials rejected) is intentionally excluded: + * it indicates revoked/expired credentials that need human rotation, not a + * re-authorization prompt. */ export function isOAuthRequiredErrorMessage(error: string | null | undefined): boolean { if (!error) return false; return /requires OAuth authorization/i.test(error) - || /(^|[^A-Z0-9_])AUTH_REQUIRED($|[^A-Z0-9_])/.test(error); + || /(^|[^A-Z0-9_])(AUTH_MISSING|AUTH_REQUIRED)($|[^A-Z0-9_])/.test(error); } diff --git a/server/tests/unit/oauth-error-detection.test.ts b/server/tests/unit/oauth-error-detection.test.ts index e0924665a6..c370aa8add 100644 --- a/server/tests/unit/oauth-error-detection.test.ts +++ b/server/tests/unit/oauth-error-detection.test.ts @@ -10,7 +10,7 @@ describe('isOAuthRequiredErrorMessage', () => { expect(isOAuthRequiredErrorMessage(msg)).toBe(true); }); - it('matches an AdCP AUTH_REQUIRED protocol error payload', () => { + it('matches an AdCP AUTH_REQUIRED protocol error payload (deprecated)', () => { expect(isOAuthRequiredErrorMessage('AUTH_REQUIRED: Unauthorized')).toBe(true); expect(isOAuthRequiredErrorMessage('task failed with AUTH_REQUIRED')).toBe(true); // Embedded in a sentence with trailing punctuation @@ -18,6 +18,18 @@ describe('isOAuthRequiredErrorMessage', () => { expect(isOAuthRequiredErrorMessage('AUTH_REQUIRED, please reauthorize')).toBe(true); }); + it('matches an AdCP AUTH_MISSING protocol error payload', () => { + expect(isOAuthRequiredErrorMessage('AUTH_MISSING: No credentials provided')).toBe(true); + expect(isOAuthRequiredErrorMessage('task failed with AUTH_MISSING')).toBe(true); + expect(isOAuthRequiredErrorMessage('Request failed: AUTH_MISSING.')).toBe(true); + expect(isOAuthRequiredErrorMessage('AUTH_MISSING, please authenticate')).toBe(true); + }); + + it('does not match AUTH_INVALID (terminal — human rotation required, not re-auth prompt)', () => { + expect(isOAuthRequiredErrorMessage('AUTH_INVALID: Token revoked')).toBe(false); + expect(isOAuthRequiredErrorMessage('task failed with AUTH_INVALID')).toBe(false); + }); + it('does not match a transport-level connection failure', () => { expect(isOAuthRequiredErrorMessage( 'Failed to discover MCP endpoint. Tried: https://example.com None responded to MCP protocol.', diff --git a/static/schemas/source/enums/error-code.json b/static/schemas/source/enums/error-code.json index a703bdac61..b40920442a 100644 --- a/static/schemas/source/enums/error-code.json +++ b/static/schemas/source/enums/error-code.json @@ -7,6 +7,8 @@ "enum": [ "INVALID_REQUEST", "AUTH_REQUIRED", + "AUTH_MISSING", + "AUTH_INVALID", "RATE_LIMITED", "SERVICE_UNAVAILABLE", "CONFIGURATION_ERROR", @@ -72,7 +74,9 @@ ], "enumDescriptions": { "INVALID_REQUEST": "Request is malformed, missing required fields, or violates schema constraints. Recovery: correctable (check request parameters and fix).", - "AUTH_REQUIRED": "Authentication is required, or presented credentials were rejected. Recovery: correctable when credentials are missing; agents MUST NOT auto-retry presented-but-rejected credentials — escalate to operator for credential rotation. See `error-handling.mdx#auth_required-sub-cases` for the operational distinction.\n\nSub-cases (full guidance). The wire code carries two operationally distinct cases: (a) credentials missing — agent provides credentials and retries; (b) credentials presented but rejected (expired, revoked, or malformed signature) — agent SHOULD NOT auto-retry, since re-presenting a rejected credential against an SSO endpoint creates retry-storm patterns indistinguishable from brute-force probes. In sub-case (b) the agent SHOULD escalate to operator for credential rotation rather than loop. A future minor release splits this code into `AUTH_MISSING` (correctable) and `AUTH_INVALID` (terminal); until then, agents apply the same distinction at the application layer by branching on whether credentials were attached to the failing request.", + "AUTH_REQUIRED": "**Deprecated** — use `AUTH_MISSING` (no credentials presented) or `AUTH_INVALID` (credentials presented and rejected). Retained as a backward-compatible alias during the 3.x deprecation window. Recovery: correctable.", + "AUTH_MISSING": "No credentials were presented. Sellers MUST return this code when no `Authorization` header was included in the request. Recovery: correctable (provide credentials via the auth header and retry).", + "AUTH_INVALID": "Credentials were presented but rejected — revoked, malformed signature, or a key no longer in the seller's keystore. Sellers MUST return this code when an `Authorization` header was present but verification failed. Recovery: terminal. Exception: agents with a valid OAuth 2.1 refresh grant MAY treat this as correctable when the rejection reason is token expiry — silently refresh and retry once; if the refresh fails or the seller explicitly signals revocation, escalate to human.", "RATE_LIMITED": "Request rate exceeded. Retry after the retry_after interval. Recovery: transient.", "SERVICE_UNAVAILABLE": "Seller service is temporarily unavailable. Retry with exponential backoff. Recovery: transient.", "CONFIGURATION_ERROR": "The seller's deployment is misconfigured in a way that prevents handling the request — the buyer cannot fix it, retrying will not help, and reporting to the seller's operator is the only remediation. Examples: account declared with `mode: 'mock'` but no `mock_upstream_url` populated; platform declared with `mode: 'live'` or `mode: 'sandbox'` but no `upstream_url` declared; required environment variable unset on the seller process. Distinct from `INVALID_REQUEST` (buyer-fixable; the request itself is malformed), `SERVICE_UNAVAILABLE` (transient; retry-with-backoff may succeed), `UNSUPPORTED_FEATURE` (capability mismatch — the seller does not implement the requested specialism), `ACCOUNT_SETUP_REQUIRED` (buyer-side onboarding incomplete; this code is seller-side deployment incomplete), and `GOVERNANCE_UNAVAILABLE` (governance-agent-scoped; transient). Wire placement. The deployment cannot produce a success artifact, so sellers MUST flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`) and populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. The code itself is the discriminator; no `error.details` shape is defined for this code (mirroring the minimal-disclosure precedent of `AGENT_SUSPENDED` / `AGENT_BLOCKED`). Sellers SHOULD populate `error.message` with operator-actionable detail (which metadata key is missing, which env var is unset) and MUST NOT include credentials, connection strings, or stack traces — the message is wire-visible to the buyer. Recovery: terminal — the buyer MUST surface to the seller's operator and MUST NOT auto-retry (retries cannot resolve a misconfigured deployment until the operator intervenes).", @@ -118,7 +122,7 @@ "VERSION_UNSUPPORTED": "The declared adcp_version (release-precision) or adcp_major_version (deprecated) is not supported by this seller. The error details SHOULD follow `error-details/version-unsupported.json` — `supported_versions` (release-precision strings) is authoritative for retry; `supported_majors` is deprecated. Recovery: correctable (re-pin to a release in supported_versions and retry; or call get_adcp_capabilities without a version pin to discover supported_versions).", "CAMPAIGN_SUSPENDED": "Campaign governance has been suspended pending human review; the governance agent MUST reject `check_governance` and `report_plan_outcome` calls on the affected plan until the escalation is resolved. Distinct from `ACCOUNT_SUSPENDED` (account-wide) — this is scoped to a single plan/campaign. Recovery: transient (wait for the escalation to resolve; contact the plan operator if the suspension persists).", "GOVERNANCE_UNAVAILABLE": "A registered governance agent is unreachable. Sellers MUST place this code in `errors[]` + `adcp_error` (never a structured rejection arm) and flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`). Distinct from `GOVERNANCE_DENIED` (agent reachable and explicitly denied — see that code's wire-placement guidance). Recovery: transient (retry with backoff; if the agent remains unreachable, the buyer MUST contact the plan's governance operator — the seller MUST NOT proceed with the media buy without a valid decision).\n\nWire placement (full guidance). Governance unavailability is a system error — the governance call FAILED (timeout, network, config error) and the seller could not get a verdict at all. Always populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Do NOT use a structured rejection arm for unavailability even when the task offers one — the buyer's recovery semantics differ (retry-with-backoff for unavailability vs. restructure-or-escalate for denial), and conflating them masks the system-error signal.", - "PERMISSION_DENIED": "The authenticated caller is not authorized for the requested action under the seller's own policies, or a required signed credential (e.g., a `governance_context` token on a spend-commit) is missing, fails verification, or was issued for a different plan, seller, or phase. Distinct from `AUTH_REQUIRED` (no credentials presented), `GOVERNANCE_DENIED` (governance agent denied), `AGENT_SUSPENDED` (agent's relationship temporarily paused), and `AGENT_BLOCKED` (agent's relationship permanently denied). When the gate that fired is specifically a non-status per-agent provisioning constraint — e.g., the agent is provisioned for sandbox traffic only and the request was against a non-sandbox account — `error.details` SHOULD conform to `error-details/agent-permission-denied.json` (`scope: \"agent\"` plus `reason: \"sandbox_only\"`) so callers can dispatch without parsing prose. Sellers MUST emit `scope: \"agent\"` only when buyer-agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; in all other cases (including bearer credentials not mapped to a specific agent record) sellers MUST return `PERMISSION_DENIED` and MUST omit `error.details.scope` — emitting the per-agent scope without established identity is a cross-tenant onboarding oracle, and the omit MUST be enforced across every observable channel (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity) per the channel-coverage rules in error-handling.mdx Per-Agent Authorization Gate, mirroring the `*_NOT_FOUND` uniform-response rule and `BILLING_NOT_PERMITTED_FOR_AGENT`. The `suspended` and `blocked` per-agent states are NOT carried on this code — sellers MUST emit `AGENT_SUSPENDED` / `AGENT_BLOCKED` instead, each of which is its own discriminator. Recovery: correctable (call `check_governance` to mint a valid token, or contact the seller to resolve the underlying permission); when `details.reason` is present the rejection is terminal-pending-onboarding — the agent MUST surface to a human at the buyer rather than auto-retrying, since the agent cannot unilaterally extend its sandbox-only provisioning.", + "PERMISSION_DENIED": "The authenticated caller is not authorized for the requested action under the seller's own policies, or a required signed credential (e.g., a `governance_context` token on a spend-commit) is missing, fails verification, or was issued for a different plan, seller, or phase. Distinct from `AUTH_MISSING` (no credentials presented), `AUTH_INVALID` (credentials presented but rejected), `GOVERNANCE_DENIED` (governance agent denied), `AGENT_SUSPENDED` (agent's relationship temporarily paused), and `AGENT_BLOCKED` (agent's relationship permanently denied). When the gate that fired is specifically a non-status per-agent provisioning constraint — e.g., the agent is provisioned for sandbox traffic only and the request was against a non-sandbox account — `error.details` SHOULD conform to `error-details/agent-permission-denied.json` (`scope: \"agent\"` plus `reason: \"sandbox_only\"`) so callers can dispatch without parsing prose. Sellers MUST emit `scope: \"agent\"` only when buyer-agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; in all other cases (including bearer credentials not mapped to a specific agent record) sellers MUST return `PERMISSION_DENIED` and MUST omit `error.details.scope` — emitting the per-agent scope without established identity is a cross-tenant onboarding oracle, and the omit MUST be enforced across every observable channel (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity) per the channel-coverage rules in error-handling.mdx Per-Agent Authorization Gate, mirroring the `*_NOT_FOUND` uniform-response rule and `BILLING_NOT_PERMITTED_FOR_AGENT`. The `suspended` and `blocked` per-agent states are NOT carried on this code — sellers MUST emit `AGENT_SUSPENDED` / `AGENT_BLOCKED` instead, each of which is its own discriminator. Recovery: correctable (call `check_governance` to mint a valid token, or contact the seller to resolve the underlying permission); when `details.reason` is present the rejection is terminal-pending-onboarding — the agent MUST surface to a human at the buyer rather than auto-retrying, since the agent cannot unilaterally extend its sandbox-only provisioning.", "SCOPE_INSUFFICIENT": "The authenticated caller is not authorized for the invoked task — the task is not in the caller's `allowed_tasks` for this account (discoverable via the `authorization` object on sync_accounts / list_accounts responses). Distinct from `PERMISSION_DENIED` (generic authz failure, often credential-shaped) by being narrowly about task-level scope. Sellers SHOULD populate `error.details.introspection_hint` pointing at where the caller can re-read its scope (strawman: `{ task: 'list_accounts', account: {...} }`). Recovery: correctable in the sense that the request can be re-sent after the scope is broadened, but the agent cannot broaden its own scope — this requires operator intervention, and agents SHOULD surface rather than auto-retry.", "READ_ONLY_SCOPE": "The caller's scope is read-only; the invoked task would mutate state and was rejected. Distinct from `SCOPE_INSUFFICIENT` (task not in scope at all) — the task is in some scopes this seller supports, just not this caller's. Recovery: correctable but not agent-autonomous — use a non-mutating alternative, or surface to the operator to request a scope that permits mutation.", "FIELD_NOT_PERMITTED": "A request field is not in the caller's `field_scopes` allowlist for this task. Sellers declaring `field_scopes` on the account's `authorization` object MUST reject any request that sets a non-allowlisted field with this code. Distinct from `VALIDATION_ERROR` (schema/business-rule violation) - the field is valid, just not writable by this caller. `error.field` MUST identify the exact offending field path (e.g., `packages[0].budget`); when multiple fields are disallowed, sellers SHOULD return one error per field, or MAY enumerate them in `error.details.fields`. Recovery: correctable and agent-autonomous - agent may drop the disallowed field(s) and retry.", diff --git a/static/test-vectors/transport-error-mapping.json b/static/test-vectors/transport-error-mapping.json index 7c6878e663..15332ce647 100644 --- a/static/test-vectors/transport-error-mapping.json +++ b/static/test-vectors/transport-error-mapping.json @@ -113,13 +113,40 @@ "expected_action": "retry" }, { - "id": "mcp-jsonrpc-auth-required", - "description": "MCP transport-level auth error via JSON-RPC error -32028", + "id": "mcp-jsonrpc-auth-missing", + "description": "MCP transport-level auth error via JSON-RPC error -32028 — no credentials presented", "transport": "mcp", "path": "jsonrpc_error", "response": { "jsonrpc": "2.0", "id": "req-456", + "error": { + "code": -32028, + "message": "Authentication required", + "data": { + "adcp_error": { + "code": "AUTH_MISSING", + "message": "No credentials were presented", + "recovery": "correctable" + } + } + } + }, + "expected_error": { + "code": "AUTH_MISSING", + "message": "No credentials were presented", + "recovery": "correctable" + }, + "expected_action": "surface_to_caller" + }, + { + "id": "mcp-jsonrpc-auth-required-deprecated", + "description": "MCP transport-level auth error via JSON-RPC error -32028 — deprecated AUTH_REQUIRED alias (backward compat)", + "transport": "mcp", + "path": "jsonrpc_error", + "response": { + "jsonrpc": "2.0", + "id": "req-457", "error": { "code": -32028, "message": "Authentication required",