mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(gateway): apply platform-disabled skill gate to bundle invocations (#59156)
Skill bundles load their member skills via _load_skill_payload directly, bypassing the scan-time disabled filter in get_skill_commands(). PR #58888 closed this gap for stacked slash-skill invocations, but /<bundle> dispatch in the gateway had the same class of bypass: a skill an operator disabled for a platform via skills.platform_disabled still got its full content injected when referenced by a bundle. build_bundle_invocation_message() now accepts a platform kwarg, filters members against get_disabled_skill_names(platform=...), and reports skipped skills in the bundle header. Gateway dispatch passes the event's platform explicitly (env-var resolution can't be trusted in the multi-platform gateway process, same reasoning as the #58888 gate).
This commit is contained in:
parent
f514132ff3
commit
65117671e3
3 changed files with 83 additions and 1 deletions
|
|
@ -254,6 +254,7 @@ def build_bundle_invocation_message(
|
|||
cmd_key: str,
|
||||
user_instruction: str = "",
|
||||
task_id: str | None = None,
|
||||
platform: str | None = None,
|
||||
) -> Optional[Tuple[str, List[str], List[str]]]:
|
||||
"""Build the user message content for a bundle slash command invocation.
|
||||
|
||||
|
|
@ -264,6 +265,16 @@ def build_bundle_invocation_message(
|
|||
loads — the agent gets a note about which ones were skipped. This is
|
||||
the same forgiving stance ``build_preloaded_skills_prompt`` uses for
|
||||
``-s`` CLI preloading.
|
||||
|
||||
Disabled skills are also skipped: bundles load members via
|
||||
``_load_skill_payload`` directly, bypassing the scan-time disabled
|
||||
filter in ``get_skill_commands()``, so the disabled list must be
|
||||
re-applied here. ``platform`` scopes the check to a specific
|
||||
platform's ``skills.platform_disabled`` config (gateway dispatch
|
||||
passes it explicitly because the gateway handles multiple platforms
|
||||
in one process); when *None*, the platform resolves from session env
|
||||
vars and the global disabled list still applies. Mirrors the
|
||||
stacked-skill gate in gateway dispatch (#58888).
|
||||
"""
|
||||
bundles = get_skill_bundles()
|
||||
info = bundles.get(cmd_key)
|
||||
|
|
@ -274,8 +285,15 @@ def build_bundle_invocation_message(
|
|||
# keep skill_bundles cheap to import in test environments.
|
||||
from agent.skill_commands import _load_skill_payload, _build_skill_message
|
||||
|
||||
try:
|
||||
from agent.skill_utils import get_disabled_skill_names
|
||||
disabled_names = get_disabled_skill_names(platform=platform)
|
||||
except Exception:
|
||||
disabled_names = set()
|
||||
|
||||
loaded_names: List[str] = []
|
||||
missing: List[str] = []
|
||||
disabled: List[str] = []
|
||||
skill_blocks: List[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
|
|
@ -295,6 +313,12 @@ def build_bundle_invocation_message(
|
|||
continue
|
||||
loaded_skill, skill_dir, skill_name = loaded
|
||||
|
||||
# Per-platform / global disabled gate. Checked against the loaded
|
||||
# skill's canonical name (identifiers may be paths or aliases).
|
||||
if skill_name in disabled_names or identifier in disabled_names:
|
||||
disabled.append(skill_name or identifier)
|
||||
continue
|
||||
|
||||
try:
|
||||
from tools.skill_usage import bump_use
|
||||
bump_use(skill_name)
|
||||
|
|
@ -329,6 +353,10 @@ def build_bundle_invocation_message(
|
|||
]
|
||||
if missing:
|
||||
header_lines.append(f"Skills missing (skipped): {', '.join(missing)}")
|
||||
if disabled:
|
||||
header_lines.append(
|
||||
f"Skills disabled for this platform (skipped): {', '.join(disabled)}"
|
||||
)
|
||||
if extra_instruction:
|
||||
header_lines.extend(["", f"Bundle instruction: {extra_instruction}"])
|
||||
if user_instruction:
|
||||
|
|
|
|||
|
|
@ -9845,8 +9845,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
bundle_key = resolve_bundle_command_key(command)
|
||||
if bundle_key is not None:
|
||||
user_instruction = event.get_command_args().strip()
|
||||
# Pass the platform explicitly: bundle skill loading
|
||||
# bypasses get_skill_commands()' scan-time disabled
|
||||
# filter, and the gateway serves multiple platforms in
|
||||
# one process, so env-var platform resolution can't be
|
||||
# trusted here. Mirrors the stacked-skill gate (#58888).
|
||||
_bundle_plat = source.platform.value if source.platform else None
|
||||
bundle_result = build_bundle_invocation_message(
|
||||
bundle_key, user_instruction, task_id=_quick_key
|
||||
bundle_key, user_instruction, task_id=_quick_key,
|
||||
platform=_bundle_plat,
|
||||
)
|
||||
if bundle_result:
|
||||
msg, _loaded, missing = bundle_result
|
||||
|
|
|
|||
|
|
@ -213,6 +213,53 @@ class TestBuildBundleInvocationMessage:
|
|||
assert missing == ["skill-ghost"]
|
||||
assert "skill-ghost" in msg # called out in header
|
||||
|
||||
def test_skips_platform_disabled_skills(self, bundles_env, monkeypatch):
|
||||
"""A skill disabled for the invoking platform must not be injected
|
||||
via a bundle (mirrors the stacked-skill gate, #58888)."""
|
||||
bundles_dir, skills_dir = bundles_env
|
||||
_make_skill(skills_dir, "skill-a", body="Skill A content.")
|
||||
_make_skill(skills_dir, "skill-b", body="SECRET DISABLED CONTENT.")
|
||||
_make_bundle_yaml(bundles_dir, "combo", ["skill-a", "skill-b"])
|
||||
scan_bundles()
|
||||
|
||||
def _fake_disabled(platform=None):
|
||||
return {"skill-b"} if platform == "telegram" else set()
|
||||
|
||||
import agent.skill_utils as su_module
|
||||
monkeypatch.setattr(
|
||||
su_module, "get_disabled_skill_names", _fake_disabled
|
||||
)
|
||||
|
||||
result = build_bundle_invocation_message("/combo", platform="telegram")
|
||||
assert result is not None
|
||||
msg, loaded, missing = result
|
||||
assert loaded == ["skill-a"]
|
||||
assert "SECRET DISABLED CONTENT." not in msg
|
||||
assert "skill-b" in msg # called out in the disabled-skipped header line
|
||||
assert "disabled" in msg.lower()
|
||||
|
||||
# Positive control: without the platform the skill loads normally.
|
||||
result2 = build_bundle_invocation_message("/combo")
|
||||
assert result2 is not None
|
||||
msg2, loaded2, _ = result2
|
||||
assert set(loaded2) == {"skill-a", "skill-b"}
|
||||
assert "SECRET DISABLED CONTENT." in msg2
|
||||
|
||||
def test_all_skills_disabled_returns_none(self, bundles_env, monkeypatch):
|
||||
bundles_dir, skills_dir = bundles_env
|
||||
_make_skill(skills_dir, "skill-a")
|
||||
_make_bundle_yaml(bundles_dir, "solo", ["skill-a"])
|
||||
scan_bundles()
|
||||
|
||||
import agent.skill_utils as su_module
|
||||
monkeypatch.setattr(
|
||||
su_module,
|
||||
"get_disabled_skill_names",
|
||||
lambda platform=None: {"skill-a"},
|
||||
)
|
||||
|
||||
assert build_bundle_invocation_message("/solo", platform="discord") is None
|
||||
|
||||
def test_unknown_bundle_returns_none(self, bundles_env):
|
||||
scan_bundles()
|
||||
assert build_bundle_invocation_message("/nope") is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue