mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(discord): include approval command in message content
This commit is contained in:
parent
3c63ed3a3c
commit
2acbdd1848
2 changed files with 103 additions and 6 deletions
|
|
@ -5453,15 +5453,44 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
if not channel:
|
||||
channel = await self._client.fetch_channel(int(target_id))
|
||||
|
||||
# Discord embed description limit is 4096; show full command up to that
|
||||
max_desc = 4088
|
||||
cmd_display = command if len(command) <= max_desc else command[: max_desc - 3] + "..."
|
||||
# Keep the approval request self-contained in plain message content.
|
||||
# Discord embeds can be invisible or visually separated from the
|
||||
# component row on some clients (notably web/mobile), so the actual
|
||||
# command and reason must be visible in the same content block as
|
||||
# the approval buttons.
|
||||
reason_budget = 300
|
||||
reason_display = str(description or "dangerous command")
|
||||
if len(reason_display) > reason_budget:
|
||||
reason_display = reason_display[: reason_budget - 15] + "... [truncated]"
|
||||
|
||||
prompt_prefix = (
|
||||
"⚠️ **Command Approval Required**\n\n"
|
||||
"Do you want Hermes to run this command?\n\n"
|
||||
"**Requested command:**\n```bash\n"
|
||||
)
|
||||
prompt_tail = f"\n```\n**Reason:** {reason_display}"
|
||||
truncated_suffix = "\n... [truncated]"
|
||||
command_budget = max(0, self.MAX_MESSAGE_LENGTH - len(prompt_prefix) - len(prompt_tail))
|
||||
content_cmd_display = str(command or "")
|
||||
if len(content_cmd_display) > command_budget:
|
||||
content_cmd_display = (
|
||||
content_cmd_display[: max(0, command_budget - len(truncated_suffix))]
|
||||
+ truncated_suffix
|
||||
)
|
||||
content = f"{prompt_prefix}{content_cmd_display}{prompt_tail}"
|
||||
|
||||
# Preserve the richer embed path and its larger description budget
|
||||
# for clients where embeds render correctly.
|
||||
max_embed_desc = 4088
|
||||
embed_cmd_display = str(command or "")
|
||||
if len(embed_cmd_display) > max_embed_desc:
|
||||
embed_cmd_display = embed_cmd_display[: max_embed_desc - 3] + "..."
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Command Approval Required",
|
||||
description=f"```\n{cmd_display}\n```",
|
||||
description=f"```\n{embed_cmd_display}\n```",
|
||||
color=discord.Color.orange(),
|
||||
)
|
||||
embed.add_field(name="Reason", value=description, inline=False)
|
||||
embed.add_field(name="Reason", value=reason_display, inline=False)
|
||||
|
||||
require_admin, admin_user_ids = _resolve_exec_approval_admin_gate(
|
||||
getattr(self.config, "extra", None)
|
||||
|
|
@ -5474,7 +5503,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
admin_user_ids=admin_user_ids,
|
||||
)
|
||||
|
||||
msg = await channel.send(embed=embed, view=view)
|
||||
msg = await channel.send(content=content, embed=embed, view=view)
|
||||
view._message = msg # store for on_timeout expiration editing
|
||||
return SendResult(success=True, message_id=str(msg.id))
|
||||
|
||||
|
|
|
|||
68
tests/gateway/test_discord_exec_approval_content.py
Normal file
68
tests/gateway/test_discord_exec_approval_content.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import PlatformConfig
|
||||
from plugins.platforms.discord.adapter import DiscordAdapter
|
||||
|
||||
|
||||
def _capture_channel(adapter):
|
||||
sent = {}
|
||||
|
||||
async def fake_send(**kwargs):
|
||||
sent.update(kwargs)
|
||||
return SimpleNamespace(id=1234)
|
||||
|
||||
channel = SimpleNamespace(send=AsyncMock(side_effect=fake_send))
|
||||
adapter._client = SimpleNamespace(
|
||||
get_channel=lambda _chat_id: channel,
|
||||
fetch_channel=AsyncMock(),
|
||||
)
|
||||
return sent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_approval_prompt_uses_visible_content_with_command_and_reason():
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
sent = _capture_channel(adapter)
|
||||
|
||||
command = "python scripts/deploy.py --env prod --force"
|
||||
result = await adapter.send_exec_approval(
|
||||
chat_id="555",
|
||||
command=command,
|
||||
session_key="discord:555",
|
||||
description="script execution via -c flag",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert sent["view"] is not None
|
||||
assert sent["embed"] is not None
|
||||
|
||||
prompt_text = sent["content"]
|
||||
assert "Command Approval Required" in prompt_text
|
||||
assert "Do you want Hermes to run this command?" in prompt_text
|
||||
assert "Requested command" in prompt_text
|
||||
assert command in prompt_text
|
||||
assert "Reason" in prompt_text
|
||||
assert "script execution via -c flag" in prompt_text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_approval_prompt_truncates_long_command_in_content():
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
sent = _capture_channel(adapter)
|
||||
|
||||
long_command = "python -c '" + ("x" * 5000) + "'"
|
||||
result = await adapter.send_exec_approval(
|
||||
chat_id="555",
|
||||
command=long_command,
|
||||
session_key="discord:555",
|
||||
description="long generated shell command",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert len(sent["content"]) <= adapter.MAX_MESSAGE_LENGTH
|
||||
assert "... [truncated]" in sent["content"]
|
||||
assert "long generated shell command" in sent["content"]
|
||||
assert len(sent["embed"].description) > len(sent["content"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue