{"record":{"id":"cbfb800e5b75358c","repo":"odysseus-dev/odysseus","slug":"api-token-has-no-owner","errorCode":null,"errorMessage":"API token has no owner","messagePattern":"API token has no owner","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/codex_routes.py","lineNumber":94,"sourceCode":"        if orig_api_token is None:\n            try:\n                delattr(request.state, \"api_token\")\n            except AttributeError:\n                pass\n        else:\n            request.state.api_token = orig_api_token\n\n\ndef _scope_owner(request: Request, allowed: set[str]) -> str:\n    \"\"\"Return the data owner if the caller is allowed for this Codex action.\"\"\"\n    if getattr(request.state, \"api_token\", False):\n        scopes = set(getattr(request.state, \"api_token_scopes\", []) or [])\n        if not scopes.intersection(allowed):\n            required = \" or \".join(sorted(allowed))\n            raise HTTPException(403, f\"API token missing required scope: {required}\")\n        owner = getattr(request.state, \"api_token_owner\", None)\n        if not owner:\n            raise HTTPException(403, \"API token has no owner\")\n        return owner\n    return require_user(request)\n\n\ndef _scope_owner_all(request: Request, required: set[str]) -> str:\n    \"\"\"Return owner only when an API token has every required scope.\"\"\"\n    if getattr(request.state, \"api_token\", False):\n        scopes = set(getattr(request.state, \"api_token_scopes\", []) or [])\n        missing = required - scopes\n        if missing:\n            raise HTTPException(403, f\"API token missing required scope: {' and '.join(sorted(missing))}\")\n        owner = getattr(request.state, \"api_token_owner\", None)\n        if not owner:\n            raise HTTPException(403, \"API token has no owner\")\n        return owner\n    return require_user(request)\n\n","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L76-L112","documentation":"Raised as HTTP 403 by _scope_owner when an API token passes the scope check but request.state.api_token_owner is empty/None. The token is structurally valid and sufficiently scoped, yet the authentication middleware did not attach an owner identity to it — a server-side token-record integrity issue (token row missing an owner field), not a caller mistake.","triggerScenarios":"Any /api/codex/* call with a token whose DB record has a null/blank owner, e.g. a token created by an older version before owner tracking, a migration that left owner NULL, or a token minted through a path that never set the owner.","commonSituations":"Upgrading the app versions where api_token schema added the owner column; tokens created programmatically bypassing the normal creation endpoint; a database restore that dropped the owner association.","solutions":["Delete and re-create the API token through the current token management endpoint so it gets a proper owner.","Inspect the API token DB record and backfill the missing owner value if the token must be kept.","Check the middleware that sets request.state.api_token_owner to confirm it reads the correct column after any schema migration."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const t = await introspectToken(tokenId);\nif (!t.owner) { // token record is broken\n  const fresh = await createToken({scopes: t.scopes});\n  switchToken(fresh.token);\n}","typeGuard":"function tokenHasOwner(t: {owner?: string | null}): boolean {\n  return typeof t.owner === 'string' && t.owner.length > 0;\n}","tryCatchPattern":"try { callCodex() } catch (e) { if (e.status === 403 && e.detail === 'API token has no owner') { reissueToken(); retry once } else throw }","preventionTips":["Always create tokens through the management endpoint so owner is bound automatically.","After schema migrations, run a check that no api_token rows have NULL owner.","Retire legacy tokens during upgrades instead of carrying them forward."],"tags":["http-403","api-token","owner","data-integrity","codex"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}