mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
fix(gateway): gate quick_commands through slash access policy
Config-backed quick_commands bypassed the admin-only slash gate. The early gate in _handle_message only fires for registry-known commands (is_gateway_known_command), but quick_commands are never in the gateway registry, so they reached the type:exec dispatch sink unchecked. An allowlisted non-admin gateway user could invoke admin-only quick commands — including shell exec in the gateway process — even when the operator set allow_admin_from / user_allowed_commands to lock them out. Apply _check_slash_access(source, command) at the quick_commands dispatch site (the single exec chokepoint, cold-path only) using the raw typed name. Admins and users with the command in user_allowed_commands still run it; backward-compat (no policy set) is unaffected. Fixes #44727. Co-authored-by: maxpetrusenko <max.petrusenko.agent@gmail.com> Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
This commit is contained in:
parent
6d879d486b
commit
9844243b18
2 changed files with 78 additions and 0 deletions
|
|
@ -8931,6 +8931,16 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if not isinstance(quick_commands, dict):
|
||||
quick_commands = {}
|
||||
if command in quick_commands:
|
||||
# Quick commands are slash capabilities too — and type:exec
|
||||
# ones run a shell command in the gateway process. The early
|
||||
# gate above only fires for registry-known commands, so quick
|
||||
# commands (never in the registry) would otherwise reach this
|
||||
# dispatch sink unchecked. Apply the same admin/user policy to
|
||||
# the raw typed name here so non-admins can't invoke admin-only
|
||||
# quick commands. (#44727)
|
||||
_denied = self._check_slash_access(source, command)
|
||||
if _denied is not None:
|
||||
return _denied
|
||||
qcmd = quick_commands[command]
|
||||
if qcmd.get("type") == "exec":
|
||||
exec_cmd = qcmd.get("command", "")
|
||||
|
|
|
|||
|
|
@ -315,6 +315,74 @@ async def test_plugin_registered_command_is_gated(monkeypatch):
|
|||
assert "/myplugin is admin-only here" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_admin_denied_for_unlisted_quick_command_exec():
|
||||
"""A non-admin must not reach the quick_commands exec sink for a command
|
||||
that isn't in user_allowed_commands. Regression for #44727 — quick
|
||||
commands are never in the gateway registry, so the early gate skips them;
|
||||
the sink gate must catch them."""
|
||||
runner = _make_runner(
|
||||
platform_extra={
|
||||
"allow_admin_from": ["111"],
|
||||
"user_allowed_commands": [],
|
||||
}
|
||||
)
|
||||
runner.config.quick_commands = {
|
||||
"limits": {"type": "exec", "command": "printf quick-command-bypass-confirmed"}
|
||||
}
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event("/limits", _make_source(user_id="999"))
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert "⛔" in result
|
||||
assert "/limits is admin-only here" in result
|
||||
assert "quick-command-bypass-confirmed" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_listed_quick_command_runs_for_non_admin():
|
||||
"""When the operator lists the quick command in user_allowed_commands, a
|
||||
non-admin can run it — the gate must allow, not blanket-deny."""
|
||||
runner = _make_runner(
|
||||
platform_extra={
|
||||
"allow_admin_from": ["111"],
|
||||
"user_allowed_commands": ["limits"],
|
||||
}
|
||||
)
|
||||
runner.config.quick_commands = {
|
||||
"limits": {"type": "exec", "command": "printf quick-command-allowed"}
|
||||
}
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event("/limits", _make_source(user_id="999"))
|
||||
)
|
||||
|
||||
assert result == "quick-command-allowed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_runs_quick_command_when_gating_enabled():
|
||||
"""An admin runs the quick command even under an enabled gate with an
|
||||
empty user_allowed_commands list."""
|
||||
runner = _make_runner(
|
||||
platform_extra={
|
||||
"allow_admin_from": ["111"],
|
||||
"user_allowed_commands": [],
|
||||
}
|
||||
)
|
||||
runner.config.quick_commands = {
|
||||
"limits": {"type": "exec", "command": "printf quick-command-admin"}
|
||||
}
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event("/limits", _make_source(user_id="111"))
|
||||
)
|
||||
|
||||
assert result == "quick-command-admin"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Running-agent fast-path gating — admin/user split must hold even when an
|
||||
# agent is already running. The fast-path block in _handle_message dispatches
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue