Skip to content

Commit c3bcbf3

Browse files
authored
Restrict agent thread continuations (#379)
1 parent ea4cbc2 commit c3bcbf3

3 files changed

Lines changed: 48 additions & 11 deletions

File tree

apps/discord_bot/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,22 @@ Long-running service changes should be implemented as PR-based workflows rather
6363
than direct production mutations. Task reads require an explicit project filter
6464
to avoid guild-wide task enumeration.
6565

66-
Mention flow is opt-in per message: the bot runs the agent only when directly
67-
mentioned in a server channel or thread. Mention-triggered agent results and
66+
Mention flow is opt-in by default: the bot runs the agent when directly
67+
mentioned in a server channel or thread. The only unmentioned continuation path
68+
is a bot-owned thread named `Agent response`, which is created for public-safe
69+
mention clarifications. Other bot-created threads, including job forum posts,
70+
still require an explicit bot mention. Mention-triggered agent results and
6871
confirmation buttons are sent by DM to avoid leaking task or plan details into
69-
public channels. A follow-up in the same thread should mention the bot again so
70-
the bot has an explicit user trigger and fresh Discord role context for that
71-
request.
72+
public channels.
7273

7374
Production mention handling depends on Discord gateway and channel access:
7475
The bot requests all intents in code, but the production Discord application
7576
should have the Message Content privileged intent enabled or approved in the
7677
Developer Portal. Direct mentions expose message content even without that
77-
intent, but follow-up messages in bot-created threads need it because they do
78-
not mention the bot. The bot also needs channel permissions to view the channel,
79-
send messages, create public threads, and send messages in threads.
78+
intent, but unmentioned follow-up messages in dedicated agent response threads
79+
need it because they do not mention the bot. The bot also needs channel
80+
permissions to view the channel, send messages, create public threads, and send
81+
messages in threads.
8082

8183
Audit writes are best-effort and do not block command execution. If the audit
8284
store is unavailable, treat the agent surface as temporarily untraced until the

apps/discord_bot/src/five08/discord_bot/cogs/agent.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
_GENERIC_UNSUPPORTED_AGENT_MESSAGE = (
3333
"I could not turn that into a supported task action."
3434
)
35+
_AGENT_RESPONSE_THREAD_NAME = "Agent response"
3536
_AGENT_HELP_REQUESTS = frozenset(
3637
{
3738
"help",
@@ -475,7 +476,10 @@ def _audit_agent_mention_response_safe(
475476
def _is_agent_thread(channel: object, bot_user_id: int) -> bool:
476477
if not isinstance(channel, discord.Thread):
477478
return False
478-
return getattr(channel, "owner_id", None) == bot_user_id
479+
if getattr(channel, "owner_id", None) != bot_user_id:
480+
return False
481+
thread_name = str(getattr(channel, "name", "") or "").strip()
482+
return thread_name == _AGENT_RESPONSE_THREAD_NAME
479483

480484
@staticmethod
481485
def _is_agent_help_request(request: str) -> bool:
@@ -719,7 +723,7 @@ async def _mention_response_thread(
719723

720724
@staticmethod
721725
def _mention_thread_name(_request: str) -> str:
722-
return "Agent response"
726+
return _AGENT_RESPONSE_THREAD_NAME
723727

724728
async def _send_mention_response_dm(
725729
self,

tests/unit/test_agent_cog.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,12 +905,17 @@ def test_mention_thread_name_does_not_include_request_content() -> None:
905905
def test_is_agent_thread_requires_bot_owned_thread() -> None:
906906
bot_owned_thread = object.__new__(discord.Thread)
907907
bot_owned_thread.owner_id = 999
908+
bot_owned_thread.name = "Agent response"
908909
user_owned_prefixed_thread = object.__new__(discord.Thread)
909910
user_owned_prefixed_thread.owner_id = 123
910-
user_owned_prefixed_thread.name = "agent: renamed by user"
911+
user_owned_prefixed_thread.name = "Agent response"
912+
bot_owned_job_thread = object.__new__(discord.Thread)
913+
bot_owned_job_thread.owner_id = 999
914+
bot_owned_job_thread.name = "[RECRUITING] Senior Engineer"
911915

912916
assert AgentCog._is_agent_thread(bot_owned_thread, 999) is True
913917
assert AgentCog._is_agent_thread(user_owned_prefixed_thread, 999) is False
918+
assert AgentCog._is_agent_thread(bot_owned_job_thread, 999) is False
914919

915920

916921
@pytest.mark.asyncio
@@ -1167,6 +1172,32 @@ async def test_agent_thread_reply_continues_without_mention() -> None:
11671172
cog._audit_message_safe.assert_not_called()
11681173

11691174

1175+
@pytest.mark.asyncio
1176+
async def test_bot_owned_non_agent_thread_reply_without_mention_is_ignored() -> None:
1177+
cog = AgentCog.__new__(AgentCog)
1178+
cog.bot = SimpleNamespace(user=SimpleNamespace(id=999))
1179+
cog._post_agent_request = AsyncMock()
1180+
cog._audit_message_safe = Mock()
1181+
channel = object.__new__(discord.Thread)
1182+
channel.owner_id = 999
1183+
channel.name = "[RECRUITING] Senior Engineer"
1184+
message = SimpleNamespace(
1185+
id=555,
1186+
content="@Jon Knebel ?",
1187+
author=SimpleNamespace(id=123, bot=False, roles=[]),
1188+
mentions=[SimpleNamespace(id=111)],
1189+
guild=SimpleNamespace(id=456),
1190+
channel=channel,
1191+
reply=AsyncMock(),
1192+
)
1193+
1194+
await cog.agent_mention(message)
1195+
1196+
cog._post_agent_request.assert_not_awaited()
1197+
message.reply.assert_not_awaited()
1198+
cog._audit_message_safe.assert_not_called()
1199+
1200+
11701201
@pytest.mark.asyncio
11711202
async def test_agent_mention_routes_unlinked_member_report_to_ephemeral_command() -> (
11721203
None

0 commit comments

Comments
 (0)