{"record":{"id":"81c4606f7912dbb3","repo":"BerriAI/litellm","slug":"unable-to-record-skill-ownership-caller-has-no-id","errorCode":null,"errorMessage":"Unable to record skill ownership: caller has no identity scope.","messagePattern":"Unable to record skill ownership: caller has no identity scope\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/litellm_proxy/skills/handler.py","lineNumber":77,"sourceCode":"        return prisma_client\n\n    @staticmethod\n    async def create_skill(\n        data: NewSkillRequest,\n        user_id: str | None = None,\n        user_api_key_dict: UserAPIKeyAuth | None = None,\n    ) -> LiteLLM_SkillsTable:\n        prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()\n\n        skill_id: Final = f\"{LITELLM_SKILL_ID_PREFIX}{uuid.uuid4()}\"\n        owner: Final = get_primary_resource_owner_scope(user_api_key_dict) or user_id\n        if owner is None:\n            # Identity-less callers (no user_id / team_id / org_id /\n            # api_key / token) can't be uniquely stamped on the row.\n            # Stamping a placeholder would let any two such callers see\n            # each other's skills via the shared owner. ValueError keeps\n            # this module FastAPI-free per the project layering rule.\n            raise ValueError(\"Unable to record skill ownership: caller has no identity scope.\")\n\n        skill_data: Final[dict[str, Any]] = {\n            \"skill_id\": skill_id,\n            \"display_title\": data.display_title,\n            \"description\": data.description,\n            \"instructions\": data.instructions,\n            \"source\": \"custom\",\n            \"created_by\": owner,\n            \"updated_by\": owner,\n        }\n\n        if data.metadata is not None:\n            from litellm.litellm_core_utils.safe_json_dumps import safe_dumps\n\n            skill_data[\"metadata\"] = safe_dumps(data.metadata)\n\n        if data.file_content is not None:\n            from prisma.fields import Base64","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/litellm_proxy/skills/handler.py#L59-L95","documentation":"When creating a skill, the handler derives the owner from the authenticated caller (get_primary_resource_owner_scope on the UserAPIKeyAuth) with user_id as fallback. If neither yields an identity, it refuses with ValueError instead of stamping a placeholder: a NULL or shared owner would let any identity-less caller read everyone's skills, so creation is rejected outright.","triggerScenarios":"create_skill invoked with user_api_key_dict=None (or a dict lacking any user/team/org/api_key scope) and user_id=None — typically internal scripts, background jobs, or tests calling the handler directly instead of through an authenticated proxy route.","commonSituations":"Custom routes that bypass the proxy's auth middleware; test fixtures constructing NewSkillRequest without an auth context; refactoring code that previously relied on implicit identity.","solutions":["Call through an authenticated proxy route so a populated UserAPIKeyAuth is available","Or pass the request's user_api_key_dict explicitly: await LiteLLMSkillsHandler.create_skill(data, user_api_key_dict=auth)","Or pass an explicit user_id when no auth dict exists","Ensure the virtual key/user carries a usable scope (user, team, org, or key id)"],"exampleFix":"# before\nskill = await LiteLLMSkillsHandler.create_skill(data)  # ValueError: caller has no identity scope\n\n# after\nskill = await LiteLLMSkillsHandler.create_skill(\n    data,\n    user_api_key_dict=user_api_key_dict,  # from the authenticated request\n)\n# or, for trusted internal callers:\nskill = await LiteLLMSkillsHandler.create_skill(data, user_id=\"user-123\")","handlingStrategy":"validation","validationCode":"from litellm.proxy.auth.auth_checks import get_primary_resource_owner_scope\n\ndef has_identity_scope(user_api_key_dict, user_id: str | None) -> bool:\n    return bool(get_primary_resource_owner_scope(user_api_key_dict) or user_id)\n\nif not has_identity_scope(auth_dict, user_id):\n    raise HTTPException(status_code=401, detail=\"Authenticated caller required to create skills\")","typeGuard":null,"tryCatchPattern":"try:\n    skill = await LiteLLMSkillsHandler.create_skill(data, user_api_key_dict=auth, user_id=user_id)\nexcept ValueError as e:\n    if \"no identity scope\" in str(e):\n        raise HTTPException(status_code=401, detail=\"Skill creation requires an authenticated caller\") from e\n    raise","preventionTips":["Only expose skill creation through authenticated proxy routes so UserAPIKeyAuth is always populated","Always pass the request's auth dict (or an explicit user_id) when calling the handler programmatically","Never rely on placeholder owners; the guard exists to prevent cross-tenant skill leakage"],"tags":["litellm-proxy","skills","authorization","ownership","authentication"],"backgroundTag":"missing-auth-context","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}