Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions agent/skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,9 +701,19 @@ def iter_skill_index_files(skills_dir: Path, filename: str):
scripts) can contain arbitrary markdown and even archived package
``SKILL.md`` files, but they are progressive-disclosure data loaded through
``skill_view(..., file_path=...)`` rather than active skill roots.

Symlink cycles (e.g. ``skills/productivity/productivity -> ..``) are
detected via a *visited* set of resolved real paths. When a cycle is
encountered the branch is pruned instead of recursing infinitely.
"""
matches = []
visited: set = set()
for root, dirs, files in os.walk(skills_dir, followlinks=True):
real_root = Path(root).resolve()
if real_root in visited:
dirs[:] = [] # prune this branch β€” already traversed
continue
visited.add(real_root)
has_skill_md = "SKILL.md" in files
dirs[:] = [
d
Expand Down
32 changes: 31 additions & 1 deletion tests/agent/test_skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,37 @@ def test_iter_skill_index_files_keeps_support_named_categories(tmp_path):
assert is_excluded_skill_path(scripts_skill / "SKILL.md") is False


# ── skill_matches_platform on Termux ──────────────────────────────────────
def test_iter_skill_index_files_prunes_self_referencing_symlinks(tmp_path):
"""Self-referencing symlinks must not cause infinite recursion.

Regression test for <https://github.com/NousResearch/hermes-agent/issues/47659>.
A symlink inside a skill directory that points back to its own parent
(e.g. ``productivity/productivity -> productivity/``) must be detected
as a cycle and pruned instead of recursing infinitely.
"""
# Create a skill directory with a self-referencing symlink.
skill_dir = tmp_path / "productivity"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"---\nname: productivity\n---\n", encoding="utf-8"
)
# Symlink: productivity/productivity -> productivity/
(skill_dir / "productivity").symlink_to(skill_dir)

# Create a second skill without a cycle (control).
normal_skill = tmp_path / "research"
normal_skill.mkdir()
(normal_skill / "SKILL.md").write_text(
"---\nname: research\n---\n", encoding="utf-8"
)

found = list(iter_skill_index_files(tmp_path, "SKILL.md"))

# Each skill should appear exactly once β€” no duplicated entries from
# the cycle.
assert len(found) == 2
names = sorted(p.parent.name for p in found)
assert names == ["productivity", "research"]


class TestSkillMatchesPlatformTermux:
Expand Down
Loading