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 resultView on GitHub (pinned to f9235ebbf1)
Solutions
- Send only one credential: drop the cookie session when calling with an API token (or vice versa).
- Re-issue the API token under the same user that owns the documents workspace.
- 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
- Send exactly one credential per request: API token XOR cookie session.
- Never mix a shared/organizational token with a personal cookie session on combined routes.
- Assert token owner equals the active workspace owner in client startup checks.
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
- API token missing required scope: {required}
- API token has no owner
- API token missing required scope: {' and '.join(sorted(missi
- API token requires chat scope
- Admin only
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/ad0fd99eabdd052e.
Report an issue: GitHub.