fix(gateway): honor configured slash-command refusal

(cherry picked from commit 8f052e0db1b33a5472473484d0a85aa72d476f73)
This commit is contained in:
funky-xamarin
2026-09-20 21:27:08 +08:00
committed by Teknium
parent d90c3ad40a
commit 72ba7ae3d6
3 changed files with 64 additions and 0 deletions

View File

@@ -1090,6 +1090,12 @@ class GatewayBusySessionMixin:
"Slash command /%s denied for %s:%s (not admin, not in user_allowed_commands)",
canonical_cmd, source.platform.value if source.platform else "?", source.user_id,
)
platform_config = self.config.platforms.get(source.platform)
extra = getattr(platform_config, "extra", platform_config)
message = extra.get("command_denied_message") if isinstance(extra, dict) else None
if isinstance(message, str):
# Empty string is a silent denial, not permission: callers check ``is not None``.
return message
allowed_preview = sorted(policy.user_allowed_commands)
if allowed_preview:
suffix = (

View File

@@ -155,6 +155,44 @@ async def test_non_admin_with_empty_user_commands_gets_floor_only():
assert "Tier: user" in whoami_result
@pytest.mark.asyncio
@pytest.mark.parametrize("message", ["Please contact support.", ""])
@pytest.mark.parametrize("entry", ["idle", "busy", "quick"])
async def test_configured_denial_preserves_dispatch_gate(message, entry):
"""Custom/empty refusals never grant execution (regression for #117217)."""
runner = _make_runner(platform_extra={
"allow_admin_from": ["111"], "command_denied_message": message,
})
source = _make_source(user_id="999")
runner._handle_restart_command = AsyncMock(side_effect=AssertionError("denied command ran"))
command = "/restart"
if entry == "busy":
key = build_session_key(source)
runner._running_agents[key] = MagicMock()
runner._running_agents_ts[key] = 0
elif entry == "quick":
runner.config.quick_commands = {"restricted": {"type": "alias", "target": "/restart"}}
command = "/restricted"
result = await runner._handle_message(_make_event(command, source))
assert result == message
runner._handle_restart_command.assert_not_awaited()
@pytest.mark.parametrize("override", [None, False, 42, [], {}])
def test_denial_override_keeps_default_and_authorization(override):
runner = _make_runner(platform_extra={"allow_admin_from": ["111"]})
source = _make_source(user_id="999")
original = runner._check_slash_access(source, "restart")
extra = runner.config.platforms[Platform.DISCORD].extra
extra["command_denied_message"] = override
assert runner._check_slash_access(source, "restart") == original
extra["command_denied_message"] = "custom"
assert runner._check_slash_access(_make_source(user_id="111"), "restart") is None
assert runner._check_slash_access(source, "help") is None
extra["user_allowed_commands"] = ["restart"]
assert runner._check_slash_access(source, "restart") is None
# ---------------------------------------------------------------------------
# Gate ALLOW — admin and listed user
# ---------------------------------------------------------------------------

View File

@@ -414,6 +414,26 @@ gateway:
**Backward compat:** if `allow_admin_from` is not set for a scope, the tier split is disabled for that scope and every allowed user has full access. Existing installs keep working with no changes — opt in when you want the distinction.
#### Customizing command refusals
For customer-facing channels, set `command_denied_message` in the platform's
`extra` block to replace the admin-only refusal with your own text:
```yaml
platforms:
telegram:
extra:
allow_admin_from: ["111"]
command_denied_message: "Please contact support for this request."
```
An empty string (`command_denied_message: ""`) silently rejects gated commands.
Omitting the setting, using `null`, or providing a non-string retains the default
refusal. Text is used literally, without placeholder expansion. This changes only
the reply: denied commands remain blocked, including while an agent is busy.
It does not hide `/help` or `/whoami`, change their always-allowed status, or
suppress unrelated notices.
#### Inspecting your access
Use `/whoami` from any platform to see the active scope, your tier (admin / user / unrestricted), and which slash commands you can run. See the [Telegram](./telegram.md#slash-command-access-control) and [Discord](./discord.md#slash-command-access-control) pages for platform-specific examples.