From a717199bbf31a0900a99b06153d3ba5803cd9012 Mon Sep 17 00:00:00 2001 From: Prive FE Coder <280484231+prive-fe-bot@users.noreply.github.com> Date: Fri, 1 May 2026 09:49:14 -0600 Subject: [PATCH] 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 . 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 --- hermes_cli/commands.py | 13 +++++++++++++ tests/hermes_cli/test_commands.py | 19 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index ce2d9eaaa2..41b1dad500 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -838,6 +838,13 @@ def discord_skill_commands_by_category( _SLACK_MAX_SLASH_COMMANDS = 50 _SLACK_NAME_LIMIT = 32 _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: @@ -864,6 +871,10 @@ def slack_native_slashes() -> list[tuple[str, str, str]]: documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work). 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 ``. + Results are clamped to Slack's 50-command limit with duplicate-name avoidance. ``/hermes`` is always reserved as the first entry so the legacy ``/hermes `` 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) if not slack_name or slack_name in seen: return + if slack_name in _SLACK_RESERVED_COMMANDS: + return if len(entries) >= _SLACK_MAX_SLASH_COMMANDS: return # Slack description cap is 2000 chars; keep it short. diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index adafe58c64..a35adbe4cc 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -13,6 +13,7 @@ from hermes_cli.commands import ( SlashCommandAutoSuggest, SlashCommandCompleter, _CMD_NAME_LIMIT, + _SLACK_RESERVED_COMMANDS, _TG_NAME_LIMIT, _clamp_command_names, _clamp_telegram_names, @@ -299,9 +300,19 @@ class TestSlackNativeSlashes: def test_includes_canonical_commands(self): names = {n for n, _d, _h in slack_native_slashes()} # 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}" + 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 .""" + 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): """Aliases (/btw, /bg, /reset, /q) must be registered as standalone 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 test fails loudly so we can curate the list rather than silently 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()} 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} 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, ( f"commands on Telegram but missing from Slack native slashes: {sorted(missing)}" )