chore: cleanup dead code, docs

This commit is contained in:
Greyson Lalonde
2026-03-05 20:37:32 -05:00
parent bb126d54e5
commit a940623672
8 changed files with 95 additions and 158 deletions

View File

@@ -8,10 +8,18 @@ from __future__ import annotations
from enum import IntEnum
from pathlib import Path
from typing import Any
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, model_validator
from crewai.skills.validation import validate_skill_name
from crewai.skills.validation import (
MAX_SKILL_NAME_LENGTH,
MIN_SKILL_NAME_LENGTH,
SKILL_NAME_PATTERN,
)
MAX_DESCRIPTION_LENGTH: int = 1024
class DisclosureLevel(IntEnum):
@@ -28,30 +36,41 @@ class DisclosureLevel(IntEnum):
RESOURCES = 3
class SkillFrontmatter(BaseModel, frozen=True):
class SkillFrontmatter(BaseModel):
"""YAML frontmatter from a SKILL.md file.
Attributes:
name: Unique skill identifier (1-64 chars, lowercase alphanumeric + hyphens).
description: Human-readable description of the skill.
license: Optional SPDX license identifier.
compatibility: Optional compatibility information.
description: Human-readable description (1-1024 chars).
license: Optional license name or reference.
compatibility: Optional compatibility information (max 500 chars).
metadata: Optional additional metadata as string key-value pairs.
allowed_tools: Optional list of tools the skill may use.
allowed_tools: Optional space-delimited list of pre-approved tools.
"""
name: str
description: str
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str] | None = None
allowed_tools: list[str] | None = None
model_config = {"frozen": True, "populate_by_name": True}
@field_validator("name")
name: str = Field(
min_length=MIN_SKILL_NAME_LENGTH,
max_length=MAX_SKILL_NAME_LENGTH,
pattern=SKILL_NAME_PATTERN,
)
description: str = Field(min_length=1, max_length=MAX_DESCRIPTION_LENGTH)
license: str | None = None
compatibility: str | None = Field(default=None, max_length=500)
metadata: dict[str, str] | None = None
allowed_tools: list[str] | None = Field(default=None, alias="allowed-tools")
@model_validator(mode="before")
@classmethod
def check_name(cls, v: str) -> str:
"""Validate skill name against spec constraints."""
return validate_skill_name(v)
def parse_allowed_tools(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Parse space-delimited allowed-tools string into a list."""
key = "allowed-tools"
alt_key = "allowed_tools"
raw = values.get(key) or values.get(alt_key)
if isinstance(raw, str):
values[key] = raw.split()
return values
class Skill(BaseModel):
@@ -96,18 +115,6 @@ class Skill(BaseModel):
"""Path to the assets directory."""
return self.path / "assets"
def has_scripts(self) -> bool:
"""Check if the skill has a scripts directory."""
return self.scripts_dir.is_dir()
def has_references(self) -> bool:
"""Check if the skill has a references directory."""
return self.references_dir.is_dir()
def has_assets(self) -> bool:
"""Check if the skill has an assets directory."""
return self.assets_dir.is_dir()
def with_disclosure_level(
self,
level: DisclosureLevel,

View File

@@ -146,8 +146,11 @@ def load_skill_resources(skill: Skill) -> Skill:
skill = load_skill_instructions(skill)
resource_files: dict[str, list[str]] = {}
for dir_name in ("scripts", "references", "assets"):
resource_dir = skill.path / dir_name
for dir_name, resource_dir in (
("scripts", skill.scripts_dir),
("references", skill.references_dir),
("assets", skill.assets_dir),
):
if resource_dir.is_dir():
resource_files[dir_name] = sorted(
str(f.relative_to(resource_dir))

View File

@@ -6,49 +6,11 @@ Validates skill names and directory structures per the Agent Skills standard.
from __future__ import annotations
from pathlib import Path
import re
MAX_SKILL_NAME_LENGTH: int = 64
MIN_SKILL_NAME_LENGTH: int = 1
_SKILL_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def validate_skill_name(name: str) -> str:
"""Validate a skill name against the Agent Skills specification.
Names must be 1-64 characters, lowercase alphanumeric with hyphens,
no leading/trailing hyphens, and no consecutive hyphens.
Args:
name: The skill name to validate.
Returns:
The validated skill name.
Raises:
ValueError: If the name violates any constraint.
"""
if len(name) < MIN_SKILL_NAME_LENGTH:
msg = "Skill name must not be empty"
raise ValueError(msg)
if len(name) > MAX_SKILL_NAME_LENGTH:
msg = (
f"Skill name must be at most {MAX_SKILL_NAME_LENGTH} characters, "
f"got {len(name)}"
)
raise ValueError(msg)
if not _SKILL_NAME_PATTERN.match(name):
msg = (
f"Invalid skill name '{name}'. Names must be lowercase alphanumeric "
f"with single hyphens, no leading/trailing hyphens."
)
raise ValueError(msg)
return name
SKILL_NAME_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
def validate_directory_name(skill_dir: Path, skill_name: str) -> None:

View File

@@ -6,9 +6,7 @@ compatibility: crewai>=0.1.0
metadata:
author: test
version: "1.0"
allowed_tools:
- web-search
- file-read
allowed-tools: web-search file-read
---
## Instructions

View File

@@ -73,27 +73,6 @@ class TestSkill:
assert skill.references_dir == skill_dir / "references"
assert skill.assets_dir == skill_dir / "assets"
def test_has_dirs_false(self, tmp_path: Path) -> None:
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
fm = SkillFrontmatter(name="test-skill", description="desc")
skill = Skill(frontmatter=fm, path=skill_dir)
assert not skill.has_scripts()
assert not skill.has_references()
assert not skill.has_assets()
def test_has_dirs_true(self, tmp_path: Path) -> None:
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
(skill_dir / "scripts").mkdir()
(skill_dir / "references").mkdir()
(skill_dir / "assets").mkdir()
fm = SkillFrontmatter(name="test-skill", description="desc")
skill = Skill(frontmatter=fm, path=skill_dir)
assert skill.has_scripts()
assert skill.has_references()
assert skill.has_assets()
def test_with_disclosure_level(self, tmp_path: Path) -> None:
fm = SkillFrontmatter(name="test-skill", description="desc")
skill = Skill(frontmatter=fm, path=tmp_path)

View File

@@ -1,77 +1,81 @@
"""Tests for skills/validation.py."""
"""Tests for skills validation."""
from pathlib import Path
import pytest
from crewai.skills.models import SkillFrontmatter
from crewai.skills.validation import (
MAX_SKILL_NAME_LENGTH,
validate_directory_name,
validate_skill_name,
)
class TestValidateSkillName:
"""Tests for validate_skill_name."""
def _make(name: str) -> SkillFrontmatter:
"""Create a SkillFrontmatter with the given name."""
return SkillFrontmatter(name=name, description="desc")
class TestSkillNameValidation:
"""Tests for skill name constraints via SkillFrontmatter."""
def test_simple_name(self) -> None:
assert validate_skill_name("web-search") == "web-search"
assert _make("web-search").name == "web-search"
def test_single_word(self) -> None:
assert validate_skill_name("search") == "search"
assert _make("search").name == "search"
def test_numeric(self) -> None:
assert validate_skill_name("tool3") == "tool3"
assert _make("tool3").name == "tool3"
def test_all_digits(self) -> None:
assert validate_skill_name("123") == "123"
assert _make("123").name == "123"
def test_single_char(self) -> None:
assert validate_skill_name("a") == "a"
assert _make("a").name == "a"
def test_max_length(self) -> None:
name = "a" * MAX_SKILL_NAME_LENGTH
assert validate_skill_name(name) == name
assert _make(name).name == name
def test_multi_hyphen_segments(self) -> None:
assert validate_skill_name("my-cool-skill") == "my-cool-skill"
assert _make("my-cool-skill").name == "my-cool-skill"
def test_empty_raises(self) -> None:
with pytest.raises(ValueError, match="must not be empty"):
validate_skill_name("")
with pytest.raises(ValueError):
_make("")
def test_too_long_raises(self) -> None:
name = "a" * (MAX_SKILL_NAME_LENGTH + 1)
with pytest.raises(ValueError, match="at most"):
validate_skill_name(name)
with pytest.raises(ValueError):
_make("a" * (MAX_SKILL_NAME_LENGTH + 1))
def test_uppercase_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("MySkill")
with pytest.raises(ValueError):
_make("MySkill")
def test_leading_hyphen_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("-skill")
with pytest.raises(ValueError):
_make("-skill")
def test_trailing_hyphen_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("skill-")
with pytest.raises(ValueError):
_make("skill-")
def test_consecutive_hyphens_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("my--skill")
with pytest.raises(ValueError):
_make("my--skill")
def test_underscore_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("my_skill")
with pytest.raises(ValueError):
_make("my_skill")
def test_space_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("my skill")
with pytest.raises(ValueError):
_make("my skill")
def test_special_chars_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
validate_skill_name("skill@v1")
with pytest.raises(ValueError):
_make("skill@v1")
class TestValidateDirectoryName: