{"record":{"id":"a3cb0afbeefbccbb","repo":"odysseus-dev/odysseus","slug":"api-token-requires-chat-scope","errorCode":null,"errorMessage":"API token requires chat scope","messagePattern":"API token requires chat scope","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"companion/routes.py","lineNumber":65,"sourceCode":"    A caller sees a row when it is their own, or when it is a legacy null-owner\n    (\"shared\") row. A caller must NEVER see another owner's row. Mirrors the\n    `owner_filter` rule used elsewhere, expressed as a pure predicate so it can\n    be tested directly and used as a defensive in-Python check alongside the\n    SQL filter.\n    \"\"\"\n    return row_owner is None or row_owner == owner\n\n\ndef require_models_scope(request: Request) -> None:\n    \"\"\"Require the companion chat scope for bearer-token model inventory.\"\"\"\n    if not getattr(request.state, \"api_token\", False):\n        return\n    scopes = getattr(request.state, \"api_token_scopes\", None) or []\n    if isinstance(scopes, str):\n        scopes = [scope.strip() for scope in scopes.split(\",\")]\n    scope_set = {str(scope).strip() for scope in scopes if str(scope).strip()}\n    if _pairing.COMPANION_SCOPE not in scope_set:\n        raise HTTPException(403, \"API token requires chat scope\")\n\n\ndef mint_pairing_token(owner: str, invalidate=None) -> tuple[str, str]:\n    \"\"\"Mint a pairing token AND invalidate the auth middleware's in-memory token\n    cache, so the new token is accepted on the very next request without a server\n    restart. Returns (token_id, raw_token); the raw token is shown once.\n\n    `invalidate` is the app's request.app.state.invalidate_token_cache callable\n    (passed in so this stays a pure, testable unit).\n    \"\"\"\n    token_id, raw_token = _pairing.mint_token(owner)\n    if callable(invalidate):\n        invalidate()\n    return token_id, raw_token\n\n\ndef setup_companion_routes() -> APIRouter:\n    router = APIRouter(prefix=\"/api/companion\", tags=[\"companion\"])","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/companion/routes.py#L47-L83","documentation":"HTTP 403 raised by require_models_scope for companion model-inventory routes. When the request authenticates with a bearer API token (request.state.api_token truthy), the token's scopes are checked and the 'chat' scope (companion.pairing.COMPANION_SCOPE) must be present. Session-authenticated requests skip the check entirely.","triggerScenarios":"Calling a companion route guarded by require_models_scope with a pairing/API token that was minted without the 'chat' scope, or whose scope string is malformed so 'chat' is not in the parsed set.","commonSituations":"Using an old token minted before scopes were introduced; minting a token with only other scopes; passing a custom Authorization header where a session cookie would have sufficed.","solutions":["Re-mint the pairing token including the chat scope (mint_pairing_token mints the companion scope)","Or call the endpoint with the browser session (cookie auth) instead of the bearer token","Verify the stored scopes string for the token contains 'chat' (comma/space separated entries are parsed)"],"exampleFix":"# before\ncurl -H 'Authorization: Bearer <token-without-chat-scope>' http://host/companion/models\n# after — mint a token with the chat scope, then use it\ntoken_id, raw = mint_pairing_token(owner, invalidate=app.state.invalidate_token_cache)\ncurl -H f'Authorization: Bearer {raw}' http://host/companion/models","handlingStrategy":"validation","validationCode":"scopes = (token_scopes or '').split(',') if isinstance(token_scopes, str) else (token_scopes or [])\nif 'chat' not in {s.strip() for s in scopes if s.strip()}:\n    raise PermissionError('Re-mint token with chat scope')","typeGuard":null,"tryCatchPattern":"try:\n    require_models_scope(request)\nexcept HTTPException as e:\n    if e.status_code == 403:\n        return JSONResponse({'error': 'token lacks chat scope'}, status_code=403)\n    raise","preventionTips":["Mint pairing tokens via mint_pairing_token so scopes are correct","Store the scope list with the token and display it in token management UI","Prefer session auth for browser flows to avoid scope issues"],"tags":["authorization","api-token","scopes","companion"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}