mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 02:01:47 +00:00
fix(slack): exclude reserved Slack commands from native slash manifest
Slack has built-in slash commands (e.g. /status, /me, /join) that apps cannot register. When running `hermes slack manifest --write`, the generated manifest included /status, causing Slack to reject the entire manifest with a reserved-command error. Add _SLACK_RESERVED_COMMANDS frozenset of all known Slack built-ins and skip them in slack_native_slashes(). Affected commands remain reachable via /hermes <command>. Tests updated: - New test_excludes_slack_reserved_commands validates no leaks - test_includes_canonical_commands no longer asserts /status - test_telegram_parity accounts for expected Slack-only exclusions
This commit is contained in:
parent
8fcc160f6b
commit
a717199bbf
2 changed files with 30 additions and 2 deletions
|
|
@ -838,6 +838,13 @@ def discord_skill_commands_by_category(
|
||||||
_SLACK_MAX_SLASH_COMMANDS = 50
|
_SLACK_MAX_SLASH_COMMANDS = 50
|
||||||
_SLACK_NAME_LIMIT = 32
|
_SLACK_NAME_LIMIT = 32
|
||||||
_SLACK_INVALID_CHARS = re.compile(r"[^a-z0-9_\-]")
|
_SLACK_INVALID_CHARS = re.compile(r"[^a-z0-9_\-]")
|
||||||
|
_SLACK_RESERVED_COMMANDS = frozenset({
|
||||||
|
# Built-in Slack slash commands that cannot be registered by apps.
|
||||||
|
# https://slack.com/help/articles/201259356-Use-built-in-slash-commands
|
||||||
|
"me", "status", "away", "dnd", "shrug", "remind", "msg", "feed",
|
||||||
|
"who", "collapse", "expand", "leave", "join", "open", "search",
|
||||||
|
"topic", "mute", "pro", "shortcuts",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_slack_name(raw: str) -> str:
|
def _sanitize_slack_name(raw: str) -> str:
|
||||||
|
|
@ -864,6 +871,10 @@ def slack_native_slashes() -> list[tuple[str, str, str]]:
|
||||||
documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work).
|
documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work).
|
||||||
Plugin-registered slash commands are included too.
|
Plugin-registered slash commands are included too.
|
||||||
|
|
||||||
|
Commands whose sanitized name collides with a Slack built-in
|
||||||
|
(e.g. ``/status``, ``/me``, ``/join``) are silently skipped. Users
|
||||||
|
can still reach them via ``/hermes <command>``.
|
||||||
|
|
||||||
Results are clamped to Slack's 50-command limit with duplicate-name
|
Results are clamped to Slack's 50-command limit with duplicate-name
|
||||||
avoidance. ``/hermes`` is always reserved as the first entry so the
|
avoidance. ``/hermes`` is always reserved as the first entry so the
|
||||||
legacy ``/hermes <subcommand>`` form keeps working for anything that
|
legacy ``/hermes <subcommand>`` form keeps working for anything that
|
||||||
|
|
@ -881,6 +892,8 @@ def slack_native_slashes() -> list[tuple[str, str, str]]:
|
||||||
slack_name = _sanitize_slack_name(name)
|
slack_name = _sanitize_slack_name(name)
|
||||||
if not slack_name or slack_name in seen:
|
if not slack_name or slack_name in seen:
|
||||||
return
|
return
|
||||||
|
if slack_name in _SLACK_RESERVED_COMMANDS:
|
||||||
|
return
|
||||||
if len(entries) >= _SLACK_MAX_SLASH_COMMANDS:
|
if len(entries) >= _SLACK_MAX_SLASH_COMMANDS:
|
||||||
return
|
return
|
||||||
# Slack description cap is 2000 chars; keep it short.
|
# Slack description cap is 2000 chars; keep it short.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from hermes_cli.commands import (
|
||||||
SlashCommandAutoSuggest,
|
SlashCommandAutoSuggest,
|
||||||
SlashCommandCompleter,
|
SlashCommandCompleter,
|
||||||
_CMD_NAME_LIMIT,
|
_CMD_NAME_LIMIT,
|
||||||
|
_SLACK_RESERVED_COMMANDS,
|
||||||
_TG_NAME_LIMIT,
|
_TG_NAME_LIMIT,
|
||||||
_clamp_command_names,
|
_clamp_command_names,
|
||||||
_clamp_telegram_names,
|
_clamp_telegram_names,
|
||||||
|
|
@ -299,9 +300,19 @@ class TestSlackNativeSlashes:
|
||||||
def test_includes_canonical_commands(self):
|
def test_includes_canonical_commands(self):
|
||||||
names = {n for n, _d, _h in slack_native_slashes()}
|
names = {n for n, _d, _h in slack_native_slashes()}
|
||||||
# Sample of gateway-available canonical commands
|
# Sample of gateway-available canonical commands
|
||||||
for expected in ("new", "stop", "background", "model", "help", "status"):
|
for expected in ("new", "stop", "background", "model", "help"):
|
||||||
assert expected in names, f"missing canonical /{expected}"
|
assert expected in names, f"missing canonical /{expected}"
|
||||||
|
|
||||||
|
def test_excludes_slack_reserved_commands(self):
|
||||||
|
"""Slack built-in commands (e.g. /status, /me, /join) cannot be
|
||||||
|
registered by apps and must be excluded from the manifest.
|
||||||
|
Users can still reach them via /hermes <command>."""
|
||||||
|
names = {n for n, _d, _h in slack_native_slashes()}
|
||||||
|
for reserved in _SLACK_RESERVED_COMMANDS:
|
||||||
|
assert reserved not in names, (
|
||||||
|
f"/{reserved} is a Slack built-in and must not appear in the manifest"
|
||||||
|
)
|
||||||
|
|
||||||
def test_includes_aliases_as_first_class_slashes(self):
|
def test_includes_aliases_as_first_class_slashes(self):
|
||||||
"""Aliases (/btw, /bg, /reset, /q) must be registered as standalone
|
"""Aliases (/btw, /bg, /reset, /q) must be registered as standalone
|
||||||
slashes — this is the whole point of native-slashes parity."""
|
slashes — this is the whole point of native-slashes parity."""
|
||||||
|
|
@ -319,6 +330,9 @@ class TestSlackNativeSlashes:
|
||||||
Telegram but not Slack (because of Slack's 50-slash cap), this
|
Telegram but not Slack (because of Slack's 50-slash cap), this
|
||||||
test fails loudly so we can curate the list rather than silently
|
test fails loudly so we can curate the list rather than silently
|
||||||
dropping parity.
|
dropping parity.
|
||||||
|
|
||||||
|
Slack-reserved built-in commands (e.g. /status) are excluded
|
||||||
|
from parity checks since they cannot be registered on Slack.
|
||||||
"""
|
"""
|
||||||
slack_names = {n for n, _d, _h in slack_native_slashes()}
|
slack_names = {n for n, _d, _h in slack_native_slashes()}
|
||||||
tg_names = {n for n, _d in telegram_bot_commands()}
|
tg_names = {n for n, _d in telegram_bot_commands()}
|
||||||
|
|
@ -329,7 +343,8 @@ class TestSlackNativeSlashes:
|
||||||
|
|
||||||
slack_norm = {_norm(n) for n in slack_names}
|
slack_norm = {_norm(n) for n in slack_names}
|
||||||
tg_norm = {_norm(n) for n in tg_names}
|
tg_norm = {_norm(n) for n in tg_names}
|
||||||
missing = tg_norm - slack_norm
|
reserved_norm = {_norm(n) for n in _SLACK_RESERVED_COMMANDS}
|
||||||
|
missing = (tg_norm - slack_norm) - reserved_norm
|
||||||
assert not missing, (
|
assert not missing, (
|
||||||
f"commands on Telegram but missing from Slack native slashes: {sorted(missing)}"
|
f"commands on Telegram but missing from Slack native slashes: {sorted(missing)}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue