Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_skill_access.py: 83%

40 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2Claude Code marketplace visibility: enabled plugins are public, disabled plugins 

3are private and resolve only for proxy admins or keys granted them via 

4``object_permission.skills``. 

5""" 

6 

7from dataclasses import dataclass 

8from typing import TYPE_CHECKING, Final, Protocol 

9 

10from litellm.proxy._types import LiteLLM_ObjectPermissionTable, UserAPIKeyAuth 

11from litellm.proxy.common_utils.resource_ownership import is_proxy_admin 

12 

13if TYPE_CHECKING: 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true

14 from prisma.types import LiteLLM_ClaudeCodePluginTableWhereInput 

15 

16 

17class _SkillRecord(Protocol): 

18 name: str 

19 enabled: bool 

20 

21 

22def _skills_of(permission: LiteLLM_ObjectPermissionTable | None) -> frozenset[str]: 

23 return frozenset(permission.skills or ()) if permission is not None else frozenset() 

24 

25 

26def granted_skills(user_api_key_dict: UserAPIKeyAuth) -> frozenset[str]: 

27 """Key grant intersected with the team grant when both are non-empty; either alone applies as is. 

28 

29 An empty list is the Prisma column default for every object-permission row, so it means 

30 "no private grants configured here" and defers to the other scope, same as the agents check. 

31 """ 

32 key_skills: Final = _skills_of(user_api_key_dict.object_permission) 

33 team_skills: Final = _skills_of(user_api_key_dict.team_object_permission) 

34 match (bool(key_skills), bool(team_skills)): 

35 case (True, True): 35 ↛ 36line 35 didn't jump to line 36 because the pattern on line 35 never matched

36 return key_skills & team_skills 

37 case (True, False): 37 ↛ 38line 37 didn't jump to line 38 because the pattern on line 37 never matched

38 return key_skills 

39 case _: 

40 return team_skills 

41 

42 

43@dataclass(frozen=True, slots=True) 

44class SkillVisibility: 

45 granted: frozenset[str] 

46 sees_private: bool 

47 

48 def allows(self, skill: _SkillRecord) -> bool: 

49 return skill.enabled or self.sees_private or skill.name in self.granted 

50 

51 def where(self) -> "LiteLLM_ClaudeCodePluginTableWhereInput": 

52 if self.sees_private: 

53 return {} 

54 if not self.granted: 54 ↛ 56line 54 didn't jump to line 56 because the condition on line 54 was always true

55 return {"enabled": True} 

56 return {"OR": [{"enabled": True}, {"name": {"in": sorted(self.granted)}}]} 

57 

58 

59PUBLIC_ONLY: Final = SkillVisibility(granted=frozenset(), sees_private=False) 

60 

61 

62def skill_visibility(user_api_key_dict: UserAPIKeyAuth | None) -> SkillVisibility: 

63 if user_api_key_dict is None: 

64 return PUBLIC_ONLY 

65 if is_proxy_admin(user_api_key_dict): 

66 return SkillVisibility(granted=frozenset(), sees_private=True) 

67 return SkillVisibility(granted=granted_skills(user_api_key_dict), sees_private=False)