diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 33d001b6f2..571c15ab7b 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -58,12 +58,13 @@ ) # Matches resource file references in skill markdown. Group 1 = relative file path. -# Supports two forms: -# 1. Markdown links: [text](path/file.ext) -# 2. Backtick-quoted paths: `path/file.ext` +# Only matches markdown links: [text](path/file.ext) +# Backtick-quoted paths are intentionally excluded because backticks are standard +# inline code formatting in markdown and do not imply the referenced file exists +# in the skill directory. # Supports optional ./ or ../ prefixes; excludes URLs (no ":" in the path character class). _RESOURCE_LINK_RE = re.compile( - r"(?:\[.*?\]\(|`)(\.?\.?/?[\w][\w\-./]*\.\w+)(?:\)|`)", + r"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", ) # Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value, diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index a77f214718..2fbcec608d 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -117,15 +117,17 @@ def test_ignores_urls(self) -> None: def test_empty_content(self) -> None: assert _extract_resource_paths("") == [] - def test_extracts_backtick_quoted_paths(self) -> None: + def test_ignores_backtick_quoted_paths(self) -> None: + """Backtick-quoted filenames are standard markdown code formatting, not resource references.""" content = "Use the template at `assets/template.md` and the script `./scripts/run.py`." paths = _extract_resource_paths(content) - assert paths == ["assets/template.md", "scripts/run.py"] + assert paths == [] - def test_deduplicates_across_link_and_backtick(self) -> None: + def test_backtick_filenames_do_not_shadow_markdown_links(self) -> None: + """Markdown links are still extracted even when backtick filenames are present.""" content = "See [doc](refs/FAQ.md) and also `refs/FAQ.md`." paths = _extract_resource_paths(content) - assert len(paths) == 1 + assert paths == ["refs/FAQ.md"] class TestTryParseSkillDocument: @@ -288,6 +290,18 @@ def test_excludes_skill_with_missing_resource(self, tmp_path: Path) -> None: skills = _discover_and_load_skills([str(tmp_path)]) assert len(skills) == 0 + def test_loads_skill_with_backtick_filenames_that_do_not_exist(self, tmp_path: Path) -> None: + """Backtick-quoted filenames should not be treated as resource references (issue #4369).""" + _write_skill( + tmp_path, + "my-skill", + body="Configure using `config.json` and `deploy.sh`.\nSee [prompt](prompt.jinja2) for the template.", + resources={"prompt.jinja2": "template content"}, + ) + skills = _discover_and_load_skills([str(tmp_path)]) + assert "my-skill" in skills + assert skills["my-skill"].resource_names == ["prompt.jinja2"] + def test_excludes_skill_with_path_traversal_resource(self, tmp_path: Path) -> None: _write_skill( tmp_path,