fix(skills): stop integration discovery at package boundaries (#5890)

This commit is contained in:
Daoyuan Li 2026-09-26 00:56:54 -07:00 committed by GitHub
parent b5c5cdd6bc
commit 1b0b27ff43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 1 deletions

View File

@ -1072,7 +1072,7 @@ Skills are loaded progressively — only when the task needs them, not all at on
When deferred skill discovery is enabled, `describe_skill` ranks installed skills by bounded, Unicode-normalized intent-term coverage across names and descriptions. Natural multi-term requests can therefore find a relevant skill without requiring one exact phrase, while exact `select:` and required-name `+prefix` lookups remain available. Ranked searches use up to 256 characters and return up to five results; exact `select:` lists are not truncated and return all requested catalog matches.
A skill directory is a package boundary: once DeerFlow finds its `SKILL.md`, nested `SKILL.md` files under that package (for example evaluation fixtures) remain supporting data and are not registered as runtime skills. Namespace directories without their own `SKILL.md` can still group nested skills.
A skill directory is a package boundary: once DeerFlow finds its `SKILL.md`, nested `SKILL.md` files under that package (for example evaluation fixtures) remain supporting data and are not registered as runtime skills. This applies to managed integration packs as well as public and custom skills. Namespace directories without their own `SKILL.md` can still group nested skills.
Skill Markdown and bundled text resources use UTF-8. Skill-creator CLI and review utilities read and write text explicitly as UTF-8 so localized skills behave consistently across operating systems.

View File

@ -1,5 +1,6 @@
### Skills System (`packages/harness/deerflow/skills/`)
- **Integration package boundaries**: recurse through provider and namespace directories, but stop at each package's `SKILL.md`. Nested fixtures must neither become catalog entries nor shadow real public skills; regression coverage lives in `tests/test_user_scoped_skill_storage.py`.
- **Location**: global public skills live under `deer-flow/skills/public/`; user-authored custom skills live under `{DEER_FLOW_HOME}/users/{user_id}/skills/custom/`; globally managed integration skills live under `{DEER_FLOW_HOME}/integrations/skills/{provider}/`; per-user integration credentials remain under `{DEER_FLOW_HOME}/users/{user_id}/integrations/{provider}/{config,data}`
- **Format**: Directory with `SKILL.md` (YAML frontmatter: name, description, license, allowed-tools as a spec-compatible string or YAML list, argument-hint, required-secrets). Exact portable spellings such as `Bash`, `WebFetch`, `WebSearch`, `Glob`, `Grep`, `Read`, `Write`, and `Edit` map to `bash`, `web_fetch`, `web_search`, `glob`, `grep`, `read_file`, `write_file`, and `str_replace`; lowercase or otherwise unknown scalar names and YAML-list entries preserve their exact runtime spelling. Argument-scoped entries remain literal and inactive because the tool policy does not inspect arguments; the scalar tokenizer keeps spaces, quotes, and escaped parentheses inside patterns intact.
- **Loading**: `load_skills()` recursively scans public, per-user custom, global integration, and legacy custom locations for `SKILL.md`, parses metadata, and reads enabled state from extensions_config.json plus per-user skill state for non-public categories; that directory is a package boundary, so no nested `SKILL.md` is registered as a runtime skill. A custom skill directory may be a one-level symlink to an external directory for compatibility with operator-managed skill trees; activation still rejects a symlinked `SKILL.md` or deeper path escape. Deleting such a skill unlinks the entry and leaves the external tree untouched. SkillScan has a deliberately narrower packaging rule: known eval fixtures are permitted as support data, while other nested `SKILL.md` files are reported as package defects. It parses runtime metadata and reads enabled state from extensions_config.json.

View File

@ -289,6 +289,7 @@ class UserScopedSkillStorage(LocalSkillStorage):
dir_names[:] = sorted(name for name in dir_names if not name.startswith("."))
if SKILL_MD_FILE not in file_names:
continue
dir_names.clear()
yield SkillCategory.INTEGRATION, integration_path, Path(current_root) / SKILL_MD_FILE
# 3. Custom skills: prefer user-level directory

View File

@ -165,6 +165,32 @@ class TestSkillLoading:
assert alice_skill.enabled is False
assert bob_skill.enabled is True
@pytest.mark.parametrize("nested_name", ["fixture-example", "public-skill"])
def test_integration_skill_package_children_are_not_registered(self, user_storage: UserScopedSkillStorage, skills_root: Path, nested_name: str):
public_dir = skills_root / "public" / "public-skill"
public_dir.mkdir(parents=True)
(public_dir / "SKILL.md").write_text(_skill_content("public-skill", "Public version"), encoding="utf-8")
provider_dir = user_storage.get_integrations_root() / "lark-cli"
package_dir = provider_dir / "lark-doc"
fixture_dir = package_dir / "evals" / "fixtures" / "example"
fixture_dir.mkdir(parents=True)
(package_dir / "SKILL.md").write_text(_skill_content("lark-doc"), encoding="utf-8")
(fixture_dir / "SKILL.md").write_text(_skill_content(nested_name, "Support data"), encoding="utf-8")
sibling_dir = provider_dir / "tools" / "lark-sheet"
sibling_dir.mkdir(parents=True)
(sibling_dir / "SKILL.md").write_text(_skill_content("lark-sheet"), encoding="utf-8")
by_name = {skill.name: skill for skill in user_storage.load_skills(enabled_only=False)}
assert set(by_name) == {"public-skill", "lark-doc", "lark-sheet"}
assert by_name["public-skill"].category == SkillCategory.PUBLIC
assert by_name["public-skill"].description == "Public version"
assert by_name["lark-doc"].category == SkillCategory.INTEGRATION
assert by_name["lark-doc"].get_container_file_path() == "/mnt/skills/integrations/lark-cli/lark-doc/SKILL.md"
assert by_name["lark-sheet"].get_container_file_path() == "/mnt/skills/integrations/lark-cli/tools/lark-sheet/SKILL.md"
def test_public_skill_package_children_are_not_registered(self, user_storage: UserScopedSkillStorage, skills_root: Path):
public_dir = skills_root / "public" / "reviewer"
fixture_dir = public_dir / "evals" / "fixtures" / "injection"