fix(gateway): include external_dirs skills in Telegram/Discord slash commands (#18741)

Skills configured through `skills.external_dirs` in config.yaml were
visible via `hermes skills list`, `get_skill_commands()`, and the
agent's `/skill-name` dispatch, but silently excluded from the
Telegram and Discord slash-command menus. The filter in
`_collect_gateway_skill_entries` only accepted skills whose
`skill_md_path` started with `SKILLS_DIR`, so anything under an
external directory fell through.

Widen the accepted-prefix set to include all configured external
dirs alongside the local skills dir. Every prefix is now
slash-terminated so `/my-skills` cannot also admit
`/my-skills-extra`. Also guard against empty `skill_md_path`
values so they can't accidentally match.

Fixes #8110

Salvages #8790 by luyao618.

Co-authored-by: Yao <34041715+luyao618@users.noreply.github.com>
This commit is contained in:
Teknium 2026-05-02 01:36:57 -07:00 committed by GitHub
parent c73594fe41
commit e2cea6eeba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 2 deletions

View file

@ -899,6 +899,73 @@ class TestTelegramMenuCommands:
assert "my_enabled_skill" in menu_names
assert "my_disabled_skill" not in menu_names
def test_external_dir_skills_included_in_telegram_menu(self, tmp_path, monkeypatch):
"""External skills (``skills.external_dirs``) must appear in the Telegram menu.
Regression test for #8110 — external skills were visible to the
agent and CLI but silently excluded from gateway slash menus
because ``_collect_gateway_skill_entries`` only accepted skills
whose path started with ``SKILLS_DIR``.
Also verifies the trailing-slash boundary: a directory that
simply shares a prefix with a configured ``external_dirs`` entry
(``/tmp/my-skills-extra`` vs ``/tmp/my-skills``) must NOT be
admitted.
"""
from unittest.mock import patch
local_dir = tmp_path / "skills"
local_dir.mkdir()
external_dir = tmp_path / "my-skills"
external_dir.mkdir()
lookalike_dir = tmp_path / "my-skills-extra"
lookalike_dir.mkdir()
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
f"skills:\n external_dirs:\n - {external_dir}\n"
)
fake_cmds = {
"/local-one": {
"name": "local-one",
"description": "Local",
"skill_md_path": f"{local_dir}/local-one/SKILL.md",
"skill_dir": f"{local_dir}/local-one",
},
"/morning-briefing": {
"name": "morning-briefing",
"description": "External skill",
"skill_md_path": f"{external_dir}/morning-briefing/SKILL.md",
"skill_dir": f"{external_dir}/morning-briefing",
},
"/lookalike-skill": {
"name": "lookalike-skill",
"description": "Lives in a sibling dir that shares a prefix",
"skill_md_path": f"{lookalike_dir}/lookalike-skill/SKILL.md",
"skill_dir": f"{lookalike_dir}/lookalike-skill",
},
}
with (
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch(
"agent.skill_utils.get_external_skills_dirs",
return_value=[external_dir],
),
):
menu, _ = telegram_menu_commands(max_commands=100)
menu_names = {n for n, _ in menu}
assert "local_one" in menu_names, "local skill must appear"
assert "morning_briefing" in menu_names, (
"external skill from skills.external_dirs must appear (fixes #8110)"
)
assert "lookalike_skill" not in menu_names, (
"prefix-match sibling directories must not be admitted"
)
def test_special_chars_in_skill_names_sanitized(self, tmp_path, monkeypatch):
"""Skills with +, /, or other special chars produce valid Telegram names."""
from unittest.mock import patch