Coverage for src / openenv / core / skills.py: 95.83%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 13:36 +0000

1"""Shared skill defaults and normalization helpers.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Iterable 

6 

7from openenv.core.models import SkillConfig 

8from openenv.core.utils import slugify_name 

9 

10 

11FREERIDE_SKILL_SOURCE = "freeride" 

12AGENT_BROWSER_SKILL_SOURCE = "agent-browser-clawdbot" 

13SKILL_SOURCE_NAME_OVERRIDES: dict[str, str] = { 

14 FREERIDE_SKILL_SOURCE: "free-ride", 

15} 

16FREERIDE_SKILL_NAME = SKILL_SOURCE_NAME_OVERRIDES[FREERIDE_SKILL_SOURCE] 

17MANDATORY_SKILL_SOURCES: tuple[str, ...] = ( 

18 "deus-context-engine", 

19 "self-improving-agent", 

20 "skill-security-review", 

21 FREERIDE_SKILL_SOURCE, 

22 AGENT_BROWSER_SKILL_SOURCE, 

23) 

24 

25 

26def mandatory_skill_names() -> tuple[str, ...]: 

27 """Return the canonical skill directory names for mandatory skill sources.""" 

28 return tuple(skill_name_for_source(source) for source in MANDATORY_SKILL_SOURCES) 

29 

30 

31def catalog_install_dir_name(source: str) -> str: 

32 """Return the default directory name created by ClawHub for a catalog source.""" 

33 source_name = source.rsplit("/", 1)[-1] 

34 return slugify_name(source_name) 

35 

36 

37def skill_name_for_source(source: str) -> str: 

38 """Convert a catalog source into the local skill directory name.""" 

39 source_name = source.rsplit("/", 1)[-1] 

40 return SKILL_SOURCE_NAME_OVERRIDES.get(source_name, catalog_install_dir_name(source)) 

41 

42 

43def build_catalog_skill(source: str, *, mandatory: bool = False) -> SkillConfig: 

44 """Build a manifest skill entry for an externally referenced catalog skill.""" 

45 descriptor = "Always-installed skill" if mandatory else "Skill" 

46 return SkillConfig( 

47 name=skill_name_for_source(source), 

48 description=f"{descriptor} referenced from catalog source {source}", 

49 source=source, 

50 ) 

51 

52 

53def merge_mandatory_skill_sources(skill_sources: Iterable[str]) -> list[str]: 

54 """Merge extra skill sources with the mandatory skill set without duplicates.""" 

55 ordered_sources = list(MANDATORY_SKILL_SOURCES) 

56 seen = set(ordered_sources) 

57 for source in skill_sources: 

58 if source not in seen: 

59 seen.add(source) 

60 ordered_sources.append(source) 

61 return ordered_sources 

62 

63 

64def ensure_mandatory_skills(skills: Iterable[SkillConfig]) -> list[SkillConfig]: 

65 """Append mandatory skills when they are missing from the manifest.""" 

66 normalized = list(skills) 

67 present_sources = {skill.source for skill in normalized if skill.source is not None} 

68 present_names = {skill.name for skill in normalized} 

69 for source in MANDATORY_SKILL_SOURCES: 

70 skill_name = skill_name_for_source(source) 

71 if source in present_sources or skill_name in present_names: 

72 continue 

73 normalized.append(build_catalog_skill(source, mandatory=True)) 

74 return normalized 

75 

76 

77def catalog_skill_specs(skills: Iterable[SkillConfig]) -> list[tuple[str, str]]: 

78 """Return ordered `(name, source)` pairs for skills referenced from an external catalog.""" 

79 ordered: list[tuple[str, str]] = [] 

80 seen: set[tuple[str, str]] = set() 

81 for skill in skills: 

82 if skill.source is None: 

83 continue 

84 spec = (skill.name, skill.source) 

85 if spec in seen: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true

86 continue 

87 seen.add(spec) 

88 ordered.append(spec) 

89 return ordered 

90 

91 

92def is_mandatory_skill_reference(reference: str) -> bool: 

93 """Return whether a source or local skill name belongs to the mandatory set.""" 

94 return reference in MANDATORY_SKILL_SOURCES or reference in mandatory_skill_names() 

95 

96 

97def is_mandatory_skill(skill: SkillConfig) -> bool: 

98 """Return whether a skill entry belongs to the mandatory skill set.""" 

99 if skill.source is not None and skill.source in MANDATORY_SKILL_SOURCES: 

100 return True 

101 return skill.name in mandatory_skill_names()