Skip to content

fix(discord): make always-on auto replies more robust#73046

Merged
steipete merged 8 commits into
openclaw:mainfrom
scoootscooob:codex/discord-tool-only-visible-replies
Apr 28, 2026
Merged

fix(discord): make always-on auto replies more robust#73046
steipete merged 8 commits into
openclaw:mainfrom
scoootscooob:codex/discord-tool-only-visible-replies

Conversation

@scoootscooob

@scoootscooob scoootscooob commented Apr 27, 2026

Copy link
Copy Markdown
Member

Summary

  • make Discord always-on guild replies safer by requiring explicit message tool sends for visible channel output
  • keep the reply-selection guidance persistent every turn: mostly lurk, send only when directly addressed or clearly useful, and avoid message-tool sends for unrelated chatter
  • keep the change internal to Discord auto-reply handling: no new config field, no config migration, and no docs/schema surface
  • let Discord message(action=send) fall back to the current channel target when the agent chooses to send visibly

Behavior

  • Existing config does not need to change. requireMention: false remains the switch for always-on Discord guild channels.
  • Mention-gated guild replies and DMs keep automatic final-reply behavior.
  • Always-on guild messages are now guarded so normal final answers stay private; Discord only receives explicit message tool sends.

Validation

  • pnpm test extensions/discord/src/monitor/message-handler.process.test.ts extensions/discord/src/config-schema.test.ts extensions/discord/src/monitor.test.ts src/auto-reply/reply/groups.test.ts src/agents/prompt-composition.test.ts
  • pnpm test src/auto-reply/reply/groups.test.ts src/agents/prompt-composition.test.ts
  • pnpm test src/auto-reply/reply/groups.test.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply.triggers.test.ts extensions/discord/src/monitor.test.ts extensions/discord/src/config-schema.test.ts extensions/discord/src/monitor/message-handler.process.test.ts extensions/slack/src/monitor.tool-result.test.ts
  • pnpm config:channels:check
  • pnpm config:docs:check
  • pnpm build

Notes

  • pnpm check:changed was attempted and reached all-lanes because this local branch has broad comparison-base divergence; typecheck, lint, and import-cycle checks passed, then unrelated pre-existing test failures appeared in src/infra/diagnostic-events.test.ts, Windows/Jiti loader tests, and plugin ledger tests.
  • A standalone pnpm test src/auto-reply/reply.triggers.test.ts selector did not map to a test file under this repo's project routing; the broader earlier run selected that suite successfully.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord agents Agent runtime and tooling size: M maintainer Maintainer-authored PR labels Apr 27, 2026
@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds visibleReplyMode plumbing across Discord (and the shared runtime), defaulting Discord to "auto" so only explicit message(action=send) tool calls produce visible channel messages — normal final/block replies are suppressed. The message action handler gains a fallback to the current channel target, and the system prompt, group/direct chat context builders, and config schema are all wired accordingly.

  • The suppressDelivery flag now covers toolOnlyVisibleReplies, but the plugin-binding skip block retains a stale comment and log message that say "under sendPolicy: deny" — a misleading signal when visibleReplyMode: "auto" triggers the path instead.
  • The WhatsApp integration-test case drops the "Do not use the message tool" assertion without a corresponding change to buildGroupChatContext, silently weakening coverage.
  • The "For action=send, include target and message" system-prompt line is unchanged in auto mode, even though target is now optional (falls back to current channel) — agents may try to resolve a channel ID they don't need.

Confidence Score: 4/5

Safe to merge; no P0/P1 defects found — remaining issues are a misleading log, a weakened test assertion, and a system-prompt instruction that may confuse agents in auto mode.

All three findings are P2: none produce wrong data, security issues, or guaranteed runtime failures. The plugin-binding log stale message is misleading but the behavior change is likely low-impact for Discord. Score stays at 4 given P2s only.

src/auto-reply/reply/dispatch-from-config.ts (plugin-binding skip comment/log), src/auto-reply/reply.triggers.group-intro-prompts.cases.ts (WhatsApp expected value), src/agents/system-prompt.ts (target instruction in auto mode)

Comments Outside Diff (2)

  1. src/auto-reply/reply/dispatch-from-config.ts, line 571-580 (link)

    P2 Stale comment and log message under new visibleReplyMode path

    The plugin-binding skip block's comment and log message both say "Under deny" / "sendPolicy: deny", but suppressDelivery now also fires when toolOnlyVisibleReplies is true. A Discord channel in visibleReplyMode: "auto" that happens to own a plugin binding will have its plugin handler silently skipped with a misleading log line — the comment rationale ("emit outbound replies we cannot rewind") may not apply to all plugin handlers, and the stale message makes debugging harder.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/auto-reply/reply/dispatch-from-config.ts
    Line: 571-580
    
    Comment:
    **Stale comment and log message under new `visibleReplyMode` path**
    
    The plugin-binding skip block's comment and log message both say "Under deny" / "sendPolicy: deny", but `suppressDelivery` now also fires when `toolOnlyVisibleReplies` is true. A Discord channel in `visibleReplyMode: "auto"` that happens to own a plugin binding will have its plugin handler silently skipped with a misleading log line — the comment rationale ("emit outbound replies we cannot rewind") may not apply to all plugin handlers, and the stale message makes debugging harder.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/agents/system-prompt.ts, line 382 (link)

    P2 target instruction unchanged despite new optional-target fallback in auto mode

    In visibleReplyMode: "auto", handle-action.ts now falls back to readCurrentDiscordTarget when to/target are absent, so agents don't need to know the channel ID. However, this line still unconditionally tells all agents "For action=send, include target and message." An agent in auto mode trying to compose a reply may unnecessarily attempt to look up a channel ID. Wrapping this line in the same visibleReplyMode branch or explicitly noting that target defaults to the current channel would prevent the instruction from conflicting with the intended usage.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/system-prompt.ts
    Line: 382
    
    Comment:
    **`target` instruction unchanged despite new optional-target fallback in auto mode**
    
    In `visibleReplyMode: "auto"`, `handle-action.ts` now falls back to `readCurrentDiscordTarget` when `to`/`target` are absent, so agents don't need to know the channel ID. However, this line still unconditionally tells all agents "For `action=send`, include `target` and `message`." An agent in auto mode trying to compose a reply may unnecessarily attempt to look up a channel ID. Wrapping this line in the same `visibleReplyMode` branch or explicitly noting that `target` defaults to the current channel would prevent the instruction from conflicting with the intended usage.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/dispatch-from-config.ts
Line: 571-580

Comment:
**Stale comment and log message under new `visibleReplyMode` path**

The plugin-binding skip block's comment and log message both say "Under deny" / "sendPolicy: deny", but `suppressDelivery` now also fires when `toolOnlyVisibleReplies` is true. A Discord channel in `visibleReplyMode: "auto"` that happens to own a plugin binding will have its plugin handler silently skipped with a misleading log line — the comment rationale ("emit outbound replies we cannot rewind") may not apply to all plugin handlers, and the stale message makes debugging harder.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/auto-reply/reply.triggers.group-intro-prompts.cases.ts
Line: 46-49

Comment:
**WhatsApp test expectation weakened but function output unchanged**

`buildGroupChatContext` still produces `"…Do not use the message tool to send to this same group - just reply normally."` for legacy mode (confirmed by the unchanged assertion in `groups.test.ts`). Removing that phrase from the expected value here drops coverage of the instruction without explanation — if the intent is to eventually remove that guidance, the function itself should be updated first; if not, restoring the full expected string keeps the integration test properly pinned.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/system-prompt.ts
Line: 382

Comment:
**`target` instruction unchanged despite new optional-target fallback in auto mode**

In `visibleReplyMode: "auto"`, `handle-action.ts` now falls back to `readCurrentDiscordTarget` when `to`/`target` are absent, so agents don't need to know the channel ID. However, this line still unconditionally tells all agents "For `action=send`, include `target` and `message`." An agent in auto mode trying to compose a reply may unnecessarily attempt to look up a channel ID. Wrapping this line in the same `visibleReplyMode` branch or explicitly noting that `target` defaults to the current channel would prevent the instruction from conflicting with the intended usage.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(discord): require message tool for ..." | Re-trigger Greptile

Comment on lines 46 to 49
expected: [
"You are in a WhatsApp group chat. Your replies are automatically sent to this group chat. Do not use the message tool to send to this same group - just reply normally.",
"You are in a WhatsApp group chat. Your replies are automatically sent to this group chat.",
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 WhatsApp test expectation weakened but function output unchanged

buildGroupChatContext still produces "…Do not use the message tool to send to this same group - just reply normally." for legacy mode (confirmed by the unchanged assertion in groups.test.ts). Removing that phrase from the expected value here drops coverage of the instruction without explanation — if the intent is to eventually remove that guidance, the function itself should be updated first; if not, restoring the full expected string keeps the integration test properly pinned.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply.triggers.group-intro-prompts.cases.ts
Line: 46-49

Comment:
**WhatsApp test expectation weakened but function output unchanged**

`buildGroupChatContext` still produces `"…Do not use the message tool to send to this same group - just reply normally."` for legacy mode (confirmed by the unchanged assertion in `groups.test.ts`). Removing that phrase from the expected value here drops coverage of the instruction without explanation — if the intent is to eventually remove that guidance, the function itself should be updated first; if not, restoring the full expected string keeps the integration test properly pinned.

How can I resolve this? If you propose a fix, please make it concise.

@scoootscooob scoootscooob changed the title feat(discord): require message tool for auto visible replies fix(discord): require message tool for auto visible replies Apr 28, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the docs Improvements or additions to documentation label Apr 28, 2026
@scoootscooob scoootscooob changed the title fix(discord): require message tool for auto visible replies fix(discord): make always-on auto replies more robust Apr 28, 2026
@steipete
steipete force-pushed the codex/discord-tool-only-visible-replies branch from 5398057 to a3c14b0 Compare April 28, 2026 04:07
@aisle-research-bot

aisle-research-bot Bot commented Apr 28, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Cross-context message tool sends allowed within provider by default (prompt-injection exfiltration risk)
2 🟠 High sendPolicy: deny does not prevent outbound messaging via message tool (policy bypass)
3 🔵 Low Discord "message_tool_only" mode still sends typing indicators (visible side-channel)
1. 🟠 Cross-context message tool sends allowed within provider by default (prompt-injection exfiltration risk)
Property Value
Severity High
CWE CWE-201
Location src/infra/outbound/outbound-policy.ts:106-127

Description

message(action=send) can be coerced (e.g., via prompt injection in a group/channel) to send content to other conversations within the same provider (e.g., other Discord channels), because cross-context enforcement allows same-provider sends by default.

  • The new message-tool-only prompting explicitly encourages delivering visible output via message(action=send) and makes target optional by defaulting to the current channel.
  • However, if an attacker convinces the model to include an explicit target/to (e.g., channel:<other-id>), the server-side policy check returns early when allowWithinProvider is not set to false (default per schema: true).
  • This enables cross-channel data exfiltration within the same provider, potentially leaking private/model-internal content or sensitive session context to an attacker-controlled channel.

Vulnerable logic:

// default: allowWithinProvider === true
if (allowWithinProvider) {
  return;
}

This is especially risky in group contexts where untrusted users can influence the assistant and where the new message-tool-only mode makes tool sends the primary way to produce visible output.

Recommendation

Harden cross-context messaging defaults and require explicit opt-in.

Suggested changes:

  1. Default-deny same-provider cross-context sends unless explicitly enabled:
const allowWithinProvider =
  params.cfg.tools?.message?.crossContext?.allowWithinProvider === true;
if (!allowWithinProvider) {// enforce same-target unless explicitly allowed
}
  1. In message-tool-only delivery mode, consider forcing same-target regardless of config unless the run is explicitly marked as trusted/owner-initiated.

  2. Add an allowlist mechanism (e.g., permitted target IDs per session/channel) or require a higher-trust capability for cross-context sends.

  3. Log and/or alert on cross-context sends (target differs from toolContext.currentChannelId) to detect abuse.

2. 🟠 sendPolicy: deny does not prevent outbound messaging via `message` tool (policy bypass)
Property Value
Severity High
CWE CWE-285
Location src/auto-reply/reply/dispatch-from-config.ts:594-623

Description

sendPolicy: "deny" is resolved in dispatchReplyFromConfig, but the refactor changes the semantics of suppression to only cover automatic reply/notice/indicator delivery while explicitly stating that "explicit message tool sends remain available".

This creates a policy bypass if sendPolicy: deny is intended to mean no outbound side effects:

  • Input: session/config sendPolicy is resolved to deny.
  • Enforcement: dispatchReplyFromConfig sets suppressDelivery to suppress automatic replies, but does not disable outbound-capable tools.
  • Sink: the message tool (src/agents/tools/message-tool.ts) can still execute runMessageActionForTool(...) which can send messages/reactions/broadcasts, producing outbound side effects despite sendPolicy: deny.

Vulnerable logic (policy narrowed to automatic delivery only):

// blocked; explicit message tool sends remain available.
const sendPolicyDenied = sendPolicy === "deny";
...
const suppressDelivery = sendPolicyDenied || suppressAutomaticSourceDelivery;

Because message-tool.ts contains no sendPolicy checks, an agent operating in a denied session can still call message(action=send|reply|broadcast|react|...) to deliver outbound content.

Recommendation

Treat sendPolicy: "deny" as a hard guard for all outbound side effects, including tool-driven sends.

Options:

  1. Disable outbound-capable tools when sendPolicyDenied is true (preferred):
  • Pass a flag into the embedded/CLI runner to disable the message tool (and any other outbound tools like sessions_send).
  1. Enforce in the tool implementation (defense-in-depth):
  • Add a sendPolicy/deliveryAllowed boolean into tool options and reject outbound actions when denied.

Example (tool-level guard):

// inside message tool handler
if (options?.sendPolicy === "deny" && action !== "read") {
  throw new Error("Outbound messaging is blocked by sendPolicy: deny");
}

Ensure the enforcement covers all outbound actions (send, reply, broadcast, reactions, uploads, etc.), not just final/automatic reply dispatch.

3. 🔵 Discord "message_tool_only" mode still sends typing indicators (visible side-channel)
Property Value
Severity Low
CWE CWE-200
Location extensions/discord/src/monitor/message-handler.process.ts:579-597

Description

In Discord guild/group privacy mode (sourceReplyDeliveryMode: "message_tool_only"), the handler suppresses ack/status reactions and draft streaming, but still starts Discord typing indicators.

  • Input: cfg.messages.groupChat.visibleReplies controls sourceReplyDeliveryMode for guild messages
  • When it resolves to "message_tool_only", the code disables reactions and draft streaming
  • However, createChannelReplyPipeline() is always configured with typing.start: sendTyping(...), which will produce a Discord-visible typing indicator in the channel/thread even when replies are intended to be tool-only/private

This leaks that the bot is actively processing a message, which can undermine the intended “no visible output unless message tool is used” privacy guarantee.

Vulnerable code (typing is unconditional):

const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
  ...
  typing: {
    start: () => sendTyping({ rest: feedbackRest, channelId: typingChannelId }),
    ...
  },
});

Recommendation

Gate typing indicators behind sourceReplyDeliveryMode !== "message_tool_only" (similar to reactions/streaming), or add a dedicated config flag for typing in tool-only mode.

Example:

const typingEnabled = sourceReplyDeliveryMode !== "message_tool_only";

const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
  cfg,
  agentId: route.agentId,
  channel: "discord",
  accountId: route.accountId,
  typing: typingEnabled
    ? {
        start: () => sendTyping({ rest: feedbackRest, channelId: typingChannelId }),
        onStartError: (err) => { /* ... */ },
        maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS,
      }
    : undefined,
});

This prevents any Discord-visible activity in tool-only mode unless explicitly requested via a tool action.


Analyzed PR: #73046 at commit d8dc4eb

Last updated on: 2026-04-28T06:40:13Z

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation size: L and removed size: M labels Apr 28, 2026
@steipete
steipete force-pushed the codex/discord-tool-only-visible-replies branch from d30a9fc to 0a63cd8 Compare April 28, 2026 06:28
@openclaw-barnacle openclaw-barnacle Bot added the channel: slack Channel integration: slack label Apr 28, 2026
@steipete
steipete merged commit 3c63620 into openclaw:main Apr 28, 2026
67 of 70 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via squash after rewriting the delivery mode into the shared group-room config.

  • Source head: d8dc4eb
  • Merge commit: 3c63620
  • Proof: Testbox generated the post-rebase config baseline hash; local focused checks passed (git diff --check, targeted oxfmt, changelog attribution, and 283 touched-path tests); PR CI required/normal checks passed after the Slack expectation fix.
  • Note: the optional parity gate is rerunning after an unrelated mock QA thread-memory-isolation failure.

Thanks @scoootscooob!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord channel: slack Channel integration: slack docs Improvements or additions to documentation maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants