odysseus-dev/odysseus · error · HTTPException

API token owner mismatch

Error message

API token owner mismatch

What it means

Raised as HTTP 403 by POST /api/codex/emails/draft-document when the owner resolved from email-draft scopes differs from the owner resolved under the documents-write scopes. Both scope checks must pass for the SAME principal; this fires when an API token's email authorization and documents authorization map to different owners, which the combined route forbids to prevent cross-owner data leaks.

Source

Thrown at routes/codex_routes.py:343

        ]
        if cc:
            lines.append(f"Cc: {cc}")
        if bcc:
            lines.append(f"Bcc: {bcc}")
        lines.append(f"Subject: {subject}")
        if in_reply_to:
            lines.append(f"In-Reply-To: {in_reply_to}")
        if references:
            lines.append(f"References: {references}")
        lines.extend(["---", body_text])
        return "\n".join(lines).rstrip() + "\n"

    @router.post("/emails/draft-document")
    async def codex_email_draft_document(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
        owner = _scope_owner(request, EMAIL_DRAFT_SCOPES)
        docs_owner = _scope_owner_all(request, DOCS_WRITE_SCOPES)
        if docs_owner != owner:
            raise HTTPException(403, "API token owner mismatch")
        if documents_create_endpoint is None:
            raise HTTPException(503, "Documents integration is not available")
        from routes.document_routes import DocumentCreate

        subject = str(body.get("subject") or "Email draft").strip() or "Email draft"
        title = str(body.get("title") or subject).strip() or "Email draft"
        req = DocumentCreate(
            session_id=body.get("session_id"),
            title=title,
            language="email",
            content=_email_draft_document_content(body),
        )
        result = await _as_owner(request, owner, documents_create_endpoint, request, req)
        if isinstance(result, dict):
            result = dict(result)
            result["draft_type"] = "document"
            result["send_required_confirmation"] = True
        return result

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send only one credential: drop the cookie session when calling with an API token (or vice versa).
  2. Re-issue the API token under the same user that owns the documents workspace.
  3. Verify request.state.api_token_owner and the docs owner resolve to the same string via token introspection before calling combined routes.

Example fix

# before
curl -b cookies.txt -H 'Authorization: Bearer $OTHER_USER_TOKEN' -X POST .../emails/draft-document  # 403

# after
curl -H "Authorization: Bearer $TOKEN" -X POST .../emails/draft-document  # no cookie jar
Defensive patterns

Strategy: validation

Validate before calling

const t = await introspectToken(activeTokenId);
const docsOwner = await getDocsOwner(); // from documents API session
if (t.owner && t.owner !== docsOwner) {
  await reissueTokenUnderOwner(docsOwner);
}

Type guard

function sameOwner(a?: string|null, b?: string|null): boolean {
  return !!a && !!b && a === b;
}

Try / catch

try { r = await draftDocument() } catch (e) { if (e.status === 403 && e.detail === 'API token owner mismatch') { clearCookies(); instruct single-credential auth } else throw }

Prevention

When it happens

Trigger: An API token whose api_token_owner differs from the owner the docs-scope check yields — typically only possible with misprovisioned tokens or middleware that resolves owners inconsistently between scope sets; also reachable if one check falls back to a cookie user while the other uses the token identity.

Common situations: Mixing auth: request carries both a valid API token and a cookie session of a different user, so one path authenticates as the token owner and the other as the cookie user; tokens minted against a shared/organization owner while docs require the personal owner.

Related errors


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