fix(skills): load symlinked skill slash commands
This commit is contained in:
@@ -58,13 +58,35 @@ def _load_skill_payload(skill_identifier: str, task_id: str | None = None) -> tu
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from tools.skills_tool import SKILLS_DIR, skill_view
|
from tools.skills_tool import SKILLS_DIR, skill_view
|
||||||
|
from agent.skill_utils import get_external_skills_dirs
|
||||||
|
|
||||||
identifier_path = Path(raw_identifier).expanduser()
|
identifier_path = Path(raw_identifier).expanduser()
|
||||||
if identifier_path.is_absolute():
|
if identifier_path.is_absolute():
|
||||||
|
normalized = None
|
||||||
|
trusted_roots = [SKILLS_DIR]
|
||||||
try:
|
try:
|
||||||
normalized = str(identifier_path.resolve().relative_to(SKILLS_DIR.resolve()))
|
trusted_roots.extend(get_external_skills_dirs())
|
||||||
except Exception:
|
except Exception:
|
||||||
normalized = raw_identifier
|
pass
|
||||||
|
|
||||||
|
# Prefer the lexical path under a trusted skill root before
|
||||||
|
# resolving symlinks. Slash-command discovery can legitimately
|
||||||
|
# find a skill via ~/.hermes/skills/<name> where <name> is a
|
||||||
|
# symlink to a checked-out skill elsewhere. Resolving first turns
|
||||||
|
# that trusted visible path into an arbitrary absolute path that
|
||||||
|
# skill_view() refuses to load.
|
||||||
|
for root in trusted_roots:
|
||||||
|
try:
|
||||||
|
normalized = str(identifier_path.relative_to(root))
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if normalized is None:
|
||||||
|
try:
|
||||||
|
normalized = str(identifier_path.resolve().relative_to(SKILLS_DIR.resolve()))
|
||||||
|
except Exception:
|
||||||
|
normalized = raw_identifier
|
||||||
else:
|
else:
|
||||||
normalized = raw_identifier.lstrip("/")
|
normalized = raw_identifier.lstrip("/")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import tools.skills_tool as skills_tool_module
|
import tools.skills_tool as skills_tool_module
|
||||||
from agent.skill_commands import (
|
from agent.skill_commands import (
|
||||||
build_preloaded_skills_prompt,
|
build_preloaded_skills_prompt,
|
||||||
@@ -125,6 +127,30 @@ class TestScanSkillCommands:
|
|||||||
assert "/knowledge-brain" in result
|
assert "/knowledge-brain" in result
|
||||||
assert result["/knowledge-brain"]["name"] == "knowledge-brain"
|
assert result["/knowledge-brain"]["name"] == "knowledge-brain"
|
||||||
|
|
||||||
|
def test_loads_skill_invocation_from_symlinked_skill_dir(self, tmp_path):
|
||||||
|
"""Slash commands should load skills symlinked under the local skills dir."""
|
||||||
|
external_root = tmp_path / "external"
|
||||||
|
skills_root = tmp_path / "skills"
|
||||||
|
skills_root.mkdir()
|
||||||
|
real_skill_dir = _make_skill(
|
||||||
|
external_root,
|
||||||
|
"impeccable",
|
||||||
|
body="Apply impeccable design craft.",
|
||||||
|
)
|
||||||
|
symlink_path = skills_root / "impeccable"
|
||||||
|
try:
|
||||||
|
symlink_path.symlink_to(real_skill_dir, target_is_directory=True)
|
||||||
|
except (OSError, NotImplementedError) as exc:
|
||||||
|
pytest.skip(f"symlinks unavailable in test environment: {exc}")
|
||||||
|
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", skills_root):
|
||||||
|
result = scan_skill_commands()
|
||||||
|
message = build_skill_invocation_message("/impeccable")
|
||||||
|
|
||||||
|
assert "/impeccable" in result
|
||||||
|
assert message is not None
|
||||||
|
assert "Apply impeccable design craft." in message
|
||||||
|
|
||||||
def test_get_skill_commands_rescans_when_platform_scope_changes(self, tmp_path):
|
def test_get_skill_commands_rescans_when_platform_scope_changes(self, tmp_path):
|
||||||
"""Platform-specific disabled-skill caches must not leak across platforms.
|
"""Platform-specific disabled-skill caches must not leak across platforms.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user