Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/auth-missing-invalid-split.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 4 additions & 2 deletions docs/building/operating/transport-errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion docs/media-buy/specification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
3 changes: 2 additions & 1 deletion docs/media-buy/task-reference/get_media_buys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/media-buy/task-reference/get_products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 2 additions & 1 deletion docs/protocol/get_adcp_capabilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
4 changes: 3 additions & 1 deletion scripts/error-code-aliases.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
8 changes: 6 additions & 2 deletions server/src/routes/helpers/oauth-error-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
* - Transport 401 with WWW-Authenticate: Bearer → SDK's
* `NeedsAuthorizationError` message begins with
* "Agent <url> 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);
}
14 changes: 13 additions & 1 deletion server/tests/unit/oauth-error-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@ 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
expect(isOAuthRequiredErrorMessage('Request failed: AUTH_REQUIRED.')).toBe(true);
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.',
Expand Down
Loading
Loading