fix(discord): include approval command in message content

This commit is contained in:
Jake Present 2026-06-25 10:20:22 -04:00 committed by Teknium
parent 3c63ed3a3c
commit 2acbdd1848
2 changed files with 103 additions and 6 deletions

View file

@ -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))

View 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"])