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
21 changes: 18 additions & 3 deletions agent/skill_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
logger = logging.getLogger(__name__)

_skill_commands: Dict[str, Dict[str, Any]] = {}
# Per-platform cache — prevents disabled-skill views from one platform
# leaking into another when the gateway serves multiple adapters (#14536).
_skill_commands_by_platform: Dict[str, Dict[str, Dict[str, Any]]] = {}
# Patterns for sanitizing skill names into clean hyphen-separated slugs.
_SKILL_INVALID_CHARS = re.compile(r"[^a-z0-9-]")
_SKILL_MULTI_HYPHEN = re.compile(r"-{2,}")
Expand Down Expand Up @@ -376,11 +379,23 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
return _skill_commands


def get_skill_commands() -> Dict[str, Dict[str, Any]]:
"""Return the current skill commands mapping (scan first if empty)."""
def get_skill_commands(platform: str = "") -> Dict[str, Dict[str, Any]]:
"""Return the current skill commands mapping (scan first if empty).

When *platform* is provided, returns a platform-specific view that
may exclude skills disabled for that platform. The base (unfiltered)
scan result is cached globally; per-platform views are cached
separately to prevent cross-platform cache leakage (#14536).
"""
if not _skill_commands:
scan_skill_commands()
return _skill_commands
if not platform:
return _skill_commands
if platform in _skill_commands_by_platform:
return _skill_commands_by_platform[platform]
# Build per-platform view — callers can filter further
_skill_commands_by_platform[platform] = dict(_skill_commands)
return _skill_commands_by_platform[platform]


def resolve_skill_command_key(command: str) -> Optional[str]:
Expand Down
40 changes: 40 additions & 0 deletions tests/agent/test_skill_commands_platform_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Tests for skill_commands per-platform cache isolation (#14536)."""
import pytest


class TestSkillCommandsPlatformCache:
"""Per-platform skill caches must be isolated (#14536)."""

def test_get_skill_commands_accepts_platform(self):
from agent.skill_commands import get_skill_commands
import inspect
sig = inspect.signature(get_skill_commands)
assert "platform" in sig.parameters

def test_per_platform_cache_exists(self):
import agent.skill_commands as mod
assert hasattr(mod, '_skill_commands_by_platform')

def test_platform_isolation(self):
"""Different platforms get independent cache entries."""
import agent.skill_commands as mod
mod._skill_commands_by_platform.clear()
mod._skill_commands = {"/test-skill": {"name": "test"}}

telegram_cmds = mod.get_skill_commands(platform="telegram")
discord_cmds = mod.get_skill_commands(platform="discord")

# Remove a skill from telegram's view
telegram_cmds.pop("/test-skill", None)

# Discord should still have it
assert "/test-skill" in discord_cmds, (
"Removing skill from telegram cache leaked into discord cache"
)

def test_no_platform_returns_global(self):
"""Calling without platform returns the global cache."""
import agent.skill_commands as mod
mod._skill_commands = {"/global": {"name": "global"}}
result = mod.get_skill_commands()
assert "/global" in result
Loading