feat(skills): add dependency declaration and auto-install for skills#1805
feat(skills): add dependency declaration and auto-install for skills#1805mvanhorn wants to merge 1 commit intobytedance:mainfrom
Conversation
Add a `dependencies` field to SKILL.md frontmatter that declares pip (and npm) packages a skill needs. The skill loader checks for missing packages on first load and installs them via uv pip install. - SkillDependencies dataclass in types.py - Parser extracts dependencies via yaml.safe_load on frontmatter - Loader caches checked skills to avoid redundant installs - skill-creator template updated with commented example - 3 new tests covering parse, backward compat, and install logic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class dependency declarations to skills so SKILL.md frontmatter can specify required packages, and the loader will attempt to auto-install missing pip dependencies when enabled skills are loaded.
Changes:
- Introduces
SkillDependenciesand adds optionaldependenciesto theSkillmodel. - Extends SKILL.md parsing to read nested
dependenciesfrom frontmatter (viayaml.safe_load). - Adds runtime dependency enforcement in
load_skills()(pip install viauv/pip) plus new tests and a template example.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/public/skill-creator/scripts/init_skill.py | Documents the new dependencies frontmatter field via commented example. |
| backend/tests/test_skill_dependencies.py | Adds tests for dependency parsing and auto-install behavior with caching. |
| backend/packages/harness/deerflow/skills/types.py | Adds SkillDependencies and attaches it to the Skill dataclass. |
| backend/packages/harness/deerflow/skills/parser.py | Parses dependencies from SKILL.md frontmatter using a YAML pass. |
| backend/packages/harness/deerflow/skills/loader.py | Implements ensure_skill_dependencies() and invokes it for enabled skills during load. |
| def ensure_skill_dependencies(skill: Skill) -> None: | ||
| """Ensure declared skill dependencies are installed (pip only).""" | ||
| skill_key = f"{skill.category}:{skill.name}" | ||
| if skill_key in _CHECKED_SKILL_DEPENDENCIES: | ||
| return | ||
| _CHECKED_SKILL_DEPENDENCIES.add(skill_key) | ||
|
|
||
| dependencies = skill.dependencies | ||
| if not dependencies or not dependencies.pip: | ||
| return |
There was a problem hiding this comment.
ensure_skill_dependencies() adds skill_key to _CHECKED_SKILL_DEPENDENCIES before it has verified dependencies are present and before any install attempt succeeds. If the dependency list changes later (or an install fails transiently), the skill will be permanently skipped for the lifetime of the process. Consider only caching after a successful check/install, or include a hash of the declared dependencies in the cache key and avoid marking as checked on failures.
| missing_packages: list[str] = [] | ||
| for package in dependencies.pip: | ||
| if not isinstance(package, str) or not package.strip(): | ||
| continue | ||
|
|
||
| normalized_name = package.replace("-", "_") | ||
| if not _has_module_spec(package) and not _has_module_spec(normalized_name): | ||
| missing_packages.append(package) | ||
|
|
There was a problem hiding this comment.
The missing-package detection uses importlib.util.find_spec() on the raw requirement string (e.g. the documented example pandas>=2.0). find_spec('pandas>=2.0') will always fail, causing unnecessary reinstalls/forced upgrades on every process start. Parse PEP 508 requirement strings (extras/version markers) to extract the distribution name, and check installation via importlib.metadata (or packaging.requirements.Requirement) rather than module specs.
| command = ["uv", "pip", "install", *missing_packages] if shutil.which("uv") else ["pip", "install", *missing_packages] | ||
| try: | ||
| subprocess.run(command) | ||
| except FileNotFoundError: | ||
| if command[0] == "uv": | ||
| subprocess.run(["pip", "install", *missing_packages]) |
There was a problem hiding this comment.
subprocess.run(command) is invoked without check=True, timeout, or logging; if the install fails, the loader will continue and the skill will still fail later with ImportError (and is currently cached as “checked”). Also, calling pip as a bare executable can install into the wrong environment (or be missing entirely). Consider running installs via the current interpreter (sys.executable -m pip) and for uv explicitly targeting the same Python, and treat non-zero exit codes as errors with a helpful message.
| for skill in skills: | ||
| if skill.enabled: | ||
| ensure_skill_dependencies(skill) | ||
|
|
There was a problem hiding this comment.
load_skills() now auto-installs declared dependencies for every enabled skill. Because ExtensionsConfig.is_skill_enabled() defaults to enabled for custom skills too, a newly installed custom skill can trigger arbitrary pip install execution as soon as skills are listed/loaded (e.g. via /api/skills or prompt generation). This is a significant security risk; consider gating auto-install behind an explicit config flag (default false) and/or restricting installs to trusted admins/allowlisted packages.
| dependencies: SkillDependencies | None = None | ||
| try: | ||
| yaml_front_matter = yaml.safe_load(front_matter) | ||
| except Exception: | ||
| yaml_front_matter = None | ||
|
|
||
| if isinstance(yaml_front_matter, dict): | ||
| yaml_dependencies = yaml_front_matter.get("dependencies") | ||
| if isinstance(yaml_dependencies, dict): | ||
| pip = yaml_dependencies.get("pip") | ||
| npm = yaml_dependencies.get("npm") | ||
|
|
||
| pip_deps = [pkg for pkg in pip if isinstance(pkg, str)] if isinstance(pip, list) else [] | ||
| npm_deps = [pkg for pkg in npm if isinstance(pkg, str)] if isinstance(npm, list) else [] | ||
|
|
||
| if pip_deps or npm_deps: | ||
| dependencies = SkillDependencies(pip=pip_deps, npm=npm_deps) | ||
|
|
There was a problem hiding this comment.
parse_skill_file() now accepts a dependencies key in SKILL.md frontmatter, but deerflow.skills.validation.ALLOWED_FRONTMATTER_PROPERTIES currently does not include dependencies (and install_skill_from_archive() validates frontmatter). This means skills that declare dependencies will likely be rejected during installation/validation. Update the validation allowlist and validation rules to include the new field so the feature works end-to-end.
| def test_parse_skill_with_dependencies(tmp_path: Path) -> None: | ||
| skill_file = _write_skill( | ||
| tmp_path, | ||
| "---\nname: deps-skill\ndescription: Skill with deps\ndependencies:\n pip:\n - requests\n - pydantic-core\n npm:\n - lodash\n---\n\n# Deps Skill\n", | ||
| ) | ||
|
|
||
| result = parse_skill_file(skill_file, "public") | ||
|
|
||
| assert result is not None | ||
| assert result.dependencies is not None | ||
| assert result.dependencies.pip == ["requests", "pydantic-core"] | ||
| assert result.dependencies.npm == ["lodash"] | ||
|
|
There was a problem hiding this comment.
The PR description documents version-pinned requirements (e.g. pandas>=2.0), but the current tests only cover bare names. Add coverage for specifiers/extras/markers (e.g. requests>=2, pydantic-core>=2; python_version>='3.12') to ensure ensure_skill_dependencies() correctly detects “already installed” without attempting installs due to find_spec() failing on non-module strings.
Summary
Add a
dependenciesfield to SKILL.md frontmatter so skills can declare pip packages they need. The skill loader checks for missing packages on first load and installs them automatically.Why this matters
Skills that need Python packages (pandas for data-analysis, matplotlib for chart-visualization) fail silently at runtime with ImportError. There is no way to declare what a skill needs.
Changes
SkillDependenciesdataclass withpipandnpmfields, added toSkillyaml.safe_load()to extract nesteddependenciesfrom frontmatterensure_skill_dependencies()checks missing pip packages viaimportlib.util.find_spec(), installs viauv pip installwith fallback to pip. Caches per skill to avoid redundant installs.SKILL.md format
Testing
3 new tests + all 19 existing skill tests pass.
test_parse_skill_with_dependencies- parses pip and npm deps from frontmattertest_parse_skill_without_dependencies_is_backward_compatible- no deps = Nonetest_ensure_skill_dependencies_installs_missing_pip_packages- mocked install with cachingThis contribution was developed with AI assistance (Codex).