Skip to content

feat(skills): add dependency declaration and auto-install for skills#1805

Open
mvanhorn wants to merge 1 commit intobytedance:mainfrom
mvanhorn:feat/skill-deps-auto-install
Open

feat(skills): add dependency declaration and auto-install for skills#1805
mvanhorn wants to merge 1 commit intobytedance:mainfrom
mvanhorn:feat/skill-deps-auto-install

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Apr 3, 2026

Summary

Add a dependencies field 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.

Source Evidence
CrewAI Agents declare pip requirements per task
skill-creator template Has TODO items for dependency management

Changes

  • types.py: SkillDependencies dataclass with pip and npm fields, added to Skill
  • parser.py: Secondary pass using yaml.safe_load() to extract nested dependencies from frontmatter
  • loader.py: ensure_skill_dependencies() checks missing pip packages via importlib.util.find_spec(), installs via uv pip install with fallback to pip. Caches per skill to avoid redundant installs.
  • skill-creator template: Commented example of dependencies field

SKILL.md format

dependencies:
  pip:
    - pandas>=2.0
    - matplotlib

Testing

3 new tests + all 19 existing skill tests pass.

  • test_parse_skill_with_dependencies - parses pip and npm deps from frontmatter
  • test_parse_skill_without_dependencies_is_backward_compatible - no deps = None
  • test_ensure_skill_dependencies_installs_missing_pip_packages - mocked install with caching

This contribution was developed with AI assistance (Codex).

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>
@WillemJiang WillemJiang requested a review from Copilot April 3, 2026 11:43
@WillemJiang WillemJiang added the question Further information is requested label Apr 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SkillDependencies and adds optional dependencies to the Skill model.
  • Extends SKILL.md parsing to read nested dependencies from frontmatter (via yaml.safe_load).
  • Adds runtime dependency enforcement in load_skills() (pip install via uv/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.

Comment on lines +22 to +31
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
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +41
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)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +50
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])
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +139
for skill in skills:
if skill.enabled:
ensure_skill_dependencies(skill)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +122
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)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +26
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"]

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants