fix(gateway): honor configured slash-command refusal
(cherry picked from commit 8f052e0db1b33a5472473484d0a85aa72d476f73)
This commit is contained in:
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user