odysseus-dev/odysseus · error · HTTPException

API token missing required scope: {' and '.join(sorted(missi

Error message

API token missing required scope: {' and '.join(sorted(missing))}

What it means

Raised as HTTP 403 by _scope_owner_all when an API-token caller is missing one or more of a route's required scopes — this variant demands ALL scopes (set difference), and the message interpolates the missing ones joined by 'and'. It appears on routes combining multiple integrations, such as POST /api/codex/emails/draft-document which needs both email-draft and documents-write scopes.

Source

Thrown at routes/codex_routes.py:105

    if getattr(request.state, "api_token", False):
        scopes = set(getattr(request.state, "api_token_scopes", []) or [])
        if not scopes.intersection(allowed):
            required = " or ".join(sorted(allowed))
            raise HTTPException(403, f"API token missing required scope: {required}")
        owner = getattr(request.state, "api_token_owner", None)
        if not owner:
            raise HTTPException(403, "API token has no owner")
        return owner
    return require_user(request)


def _scope_owner_all(request: Request, required: set[str]) -> str:
    """Return owner only when an API token has every required scope."""
    if getattr(request.state, "api_token", False):
        scopes = set(getattr(request.state, "api_token_scopes", []) or [])
        missing = required - scopes
        if missing:
            raise HTTPException(403, f"API token missing required scope: {' and '.join(sorted(missing))}")
        owner = getattr(request.state, "api_token_owner", None)
        if not owner:
            raise HTTPException(403, "API token has no owner")
        return owner
    return require_user(request)


def _require_cookbook_scope(request: Request, allowed: set[str]) -> str:
    """Authorize a Codex cookbook route.

    For API-token callers, enforce the given scope set.
    For cookie-session callers, additionally require admin privileges
    because cookbook surfaces expose host topology, task logs, tmux
    commands, and model-serving controls.
    """
    owner = _scope_owner(request, allowed)
    if not getattr(request.state, "api_token", False):
        require_admin(request)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Parse the missing list from the message and re-issue the token adding exactly those scopes.
  2. Verify with the token introspection endpoint that all required scopes are present before calling combined routes.
  3. Keep tokens per-purpose: mint a dedicated token with the full scope set for cross-integration automation rather than reusing a narrow one.

Example fix

# before
curl -H 'Authorization: Bearer $EMAIL_ONLY_TOKEN' -X POST .../api/codex/emails/draft-document  # 403

# after
TOKEN=$(create_token --scope email:write --scope documents:write)
curl -H "Authorization: Bearer $TOKEN" -X POST .../api/codex/emails/draft-document
Defensive patterns

Strategy: validation

Validate before calling

const REQUIRED = ['email:write', 'documents:write']; // draft-document route
const {scopes} = await introspect(token);
const missing = REQUIRED.filter(s => !scopes.includes(s));
if (missing.length) await reissueToken([...scopes, ...missing]);

Type guard

function hasAllScopes(have: string[], need: string[]): boolean {
  return need.every(s => have.includes(s));
}

Try / catch

try { r = await draftDocument() } catch (e) { if (e.status === 403 && e.detail.includes('missing required scope')) { reissue with missing scopes; retry once } else throw }

Prevention

When it happens

Trigger: Calling a multi-scope route (e.g. codex_email_draft_document) with a token that has email:write but not the DOCS_WRITE_SCOPES entries; a token provisioned before a route gained an additional required scope.

Common situations: Route's scope requirements expanded in a new release so previously-working tokens now 403; token minted from a scope template that omits one integration.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/cfdb28674ddd9edb. Report an issue: GitHub.