[AI-assisted] feat: add discord agent tool for full Discord API access#43819
[AI-assisted] feat: add discord agent tool for full Discord API access#43819ragesaq wants to merge 3 commits into
Conversation
…agement - Add createDiscordTool() wrapper around handleDiscordAction() - Register discord tool in openclaw-tools.ts - Add discord tool definition to tool-catalog.ts (messaging section) - Supports all existing Discord actions: sendMessage, deleteMessage, react, readMessages, thread management, moderation, etc. - Tool respects existing action gates and permission policies - Available in 'messaging' and 'full' profiles Fixes: Missing Discord tool exposure to agents despite existing action handlers in discord-actions.ts
There was a problem hiding this comment.
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1fa60fad7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| execute: async (_toolCallId, args) => { | ||
| const params = args as Record<string, unknown>; | ||
| const action = params.action; | ||
| const { action: _action, ...actionParams } = params; |
There was a problem hiding this comment.
Forward action field to Discord action handler
The tool removes action from params (const { action: _action, ...actionParams } = params) before calling handleDiscordAction, but handleDiscordAction immediately requires params.action via readStringParam(..., { required: true }). In practice, any discord tool invocation now fails with a missing-action error and returns ok: false, so none of the newly wired Discord operations can run. Pass the original params object (or reattach action) when invoking the handler.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR wires up the existing Discord action handlers (
Confidence Score: 1/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/tools/discord-tool.ts
Line: 66-80
Comment:
**`action` stripped before reaching `handleDiscordAction` — every call will fail**
`actionParams` is `params` with the `action` key removed. But `handleDiscordAction` internally calls `readStringParam(params, "action", { required: true })` (see `discord-actions.ts` line 65), which will throw `ToolInputError: action required` on every invocation. The outer `try/catch` will catch this and return `{ ok: false, error: "Discord action failed: action required" }`, so the tool is silently broken for all actions.
The fix is straightforward — pass the full `params` object (which already includes `action`) instead of `actionParams`:
```suggestion
const result = await handleDiscordAction(params as Record<string, unknown>, cfg, {
mediaLocalRoots: undefined,
});
```
The `_action` destructuring on line 65 is then unused and can also be removed.
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/tools/discord-tool.ts
Line: 70-76
Comment:
**Channel ID resolution operates on stripped `actionParams`**
The `channelId` resolution block mutates `actionParams.channelId`, but because `actionParams` is discarded in favour of `params` (once the stripping bug is fixed), this mutation would have no effect. The resolved channel ID needs to be set on the object that is actually passed to `handleDiscordAction`.
If the intent is to normalise the channel ID before routing, you should mutate `params.channelId` (or `params` itself) rather than the stripped copy:
```suggestion
if (params.channelId && typeof params.channelId === "string") {
try {
params.channelId = resolveDiscordChannelId(params.channelId);
} catch {
// If resolution fails, pass through as-is and let the action handler validate
}
}
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: b1fa60f |
| "The 'action' parameter is required. Examples: sendMessage, deleteMessage, react, readMessages", | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| // Resolve channel ID if provided as a raw ID | ||
| if (actionParams.channelId && typeof actionParams.channelId === "string") { | ||
| try { | ||
| actionParams.channelId = resolveDiscordChannelId(actionParams.channelId); | ||
| } catch { | ||
| // If resolution fails, pass through as-is and let the action handler validate | ||
| } | ||
| } | ||
|
|
||
| const result = await handleDiscordAction(actionParams as Record<string, unknown>, cfg, { |
There was a problem hiding this comment.
action stripped before reaching handleDiscordAction — every call will fail
actionParams is params with the action key removed. But handleDiscordAction internally calls readStringParam(params, "action", { required: true }) (see discord-actions.ts line 65), which will throw ToolInputError: action required on every invocation. The outer try/catch will catch this and return { ok: false, error: "Discord action failed: action required" }, so the tool is silently broken for all actions.
The fix is straightforward — pass the full params object (which already includes action) instead of actionParams:
| "The 'action' parameter is required. Examples: sendMessage, deleteMessage, react, readMessages", | |
| }); | |
| } | |
| try { | |
| // Resolve channel ID if provided as a raw ID | |
| if (actionParams.channelId && typeof actionParams.channelId === "string") { | |
| try { | |
| actionParams.channelId = resolveDiscordChannelId(actionParams.channelId); | |
| } catch { | |
| // If resolution fails, pass through as-is and let the action handler validate | |
| } | |
| } | |
| const result = await handleDiscordAction(actionParams as Record<string, unknown>, cfg, { | |
| const result = await handleDiscordAction(params as Record<string, unknown>, cfg, { | |
| mediaLocalRoots: undefined, | |
| }); |
The _action destructuring on line 65 is then unused and can also be removed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/discord-tool.ts
Line: 66-80
Comment:
**`action` stripped before reaching `handleDiscordAction` — every call will fail**
`actionParams` is `params` with the `action` key removed. But `handleDiscordAction` internally calls `readStringParam(params, "action", { required: true })` (see `discord-actions.ts` line 65), which will throw `ToolInputError: action required` on every invocation. The outer `try/catch` will catch this and return `{ ok: false, error: "Discord action failed: action required" }`, so the tool is silently broken for all actions.
The fix is straightforward — pass the full `params` object (which already includes `action`) instead of `actionParams`:
```suggestion
const result = await handleDiscordAction(params as Record<string, unknown>, cfg, {
mediaLocalRoots: undefined,
});
```
The `_action` destructuring on line 65 is then unused and can also be removed.
How can I resolve this? If you propose a fix, please make it concise.| try { | ||
| // Resolve channel ID if provided as a raw ID | ||
| if (actionParams.channelId && typeof actionParams.channelId === "string") { | ||
| try { | ||
| actionParams.channelId = resolveDiscordChannelId(actionParams.channelId); | ||
| } catch { | ||
| // If resolution fails, pass through as-is and let the action handler validate |
There was a problem hiding this comment.
Channel ID resolution operates on stripped actionParams
The channelId resolution block mutates actionParams.channelId, but because actionParams is discarded in favour of params (once the stripping bug is fixed), this mutation would have no effect. The resolved channel ID needs to be set on the object that is actually passed to handleDiscordAction.
If the intent is to normalise the channel ID before routing, you should mutate params.channelId (or params itself) rather than the stripped copy:
| try { | |
| // Resolve channel ID if provided as a raw ID | |
| if (actionParams.channelId && typeof actionParams.channelId === "string") { | |
| try { | |
| actionParams.channelId = resolveDiscordChannelId(actionParams.channelId); | |
| } catch { | |
| // If resolution fails, pass through as-is and let the action handler validate | |
| if (params.channelId && typeof params.channelId === "string") { | |
| try { | |
| params.channelId = resolveDiscordChannelId(params.channelId); | |
| } catch { | |
| // If resolution fails, pass through as-is and let the action handler validate | |
| } | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/discord-tool.ts
Line: 70-76
Comment:
**Channel ID resolution operates on stripped `actionParams`**
The `channelId` resolution block mutates `actionParams.channelId`, but because `actionParams` is discarded in favour of `params` (once the stripping bug is fixed), this mutation would have no effect. The resolved channel ID needs to be set on the object that is actually passed to `handleDiscordAction`.
If the intent is to normalise the channel ID before routing, you should mutate `params.channelId` (or `params` itself) rather than the stripped copy:
```suggestion
if (params.channelId && typeof params.channelId === "string") {
try {
params.channelId = resolveDiscordChannelId(params.channelId);
} catch {
// If resolution fails, pass through as-is and let the action handler validate
}
}
```
How can I resolve this? If you propose a fix, please make it concise.- Don't strip action before passing to handler (it needs it) - Mutate params.channelId not discarded actionParams - Both issues caught by Greptile bot review on PR openclaw#43819
Bot Review Response[Greptile #2922868122] [Greptile #2922868184] Channel ID resolution operates on stripped [Copilot #2922866237] Forward action field to Discord action handler — ✅ Fixed in 39072c9 All three bots caught the same bug from different angles — |
Tests cover: - Tool metadata (name, label, description) - Missing action validation - Non-string action validation - Full round-trip (action + params -> result) - Channel ID resolution - Unknown channel ID passthrough - Error handling (structured result, no crashes)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d5feb1052
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const result = await handleDiscordAction(params, cfg, { | ||
| mediaLocalRoots: undefined, | ||
| }); |
There was a problem hiding this comment.
Pass trusted sender identity into moderation actions
This wrapper forwards tool arguments directly to handleDiscordAction without attaching a trusted requester identity, which opens a moderation authz gap: handleDiscordModerationAction only enforces permission checks when senderUserId is present, and otherwise returns early (verifySenderModerationPermission). In tool-driven contexts this means a ban/kick/timeout call can omit or spoof senderUserId and still run with bot permissions, unlike the existing message-action path that requires requesterSenderId for Discord moderation (src/channels/plugins/message-actions.ts).
Useful? React with 👍 / 👎.
| const result = await handleDiscordAction(params, cfg, { | ||
| mediaLocalRoots: undefined, | ||
| }); |
There was a problem hiding this comment.
Apply agent account context before dispatching Discord actions
createOpenClawTools passes agentAccountId into this tool, but the value is never applied to outgoing params before calling handleDiscordAction. When models omit accountId (likely, since it is not in this schema), Discord routing falls back to the default account, so sessions bound to a non-default Discord account can act on the wrong bot token/guild permissions or fail unexpectedly; the message tool avoids this by injecting a default account context.
Useful? React with 👍 / 👎.
|
Closing this as implemented after Codex review. Current main already gives agents broad Discord action access through the existing What I checked:
So I’m closing this as already implemented rather than keeping a duplicate issue open. Review notes: reviewed against 50e36983bb2d; fix evidence: release v2026.4.22, commit 00bd2cf7a376. |
Summary
messagetool, which only supports sending via Discord.discordtool wrapper (discord-tool.ts), registered it in openclaw-tools.ts, added tool definition to tool-catalog.ts (messaging section, "messaging" + "full" profiles).messagetool, no config/schema changes.Change Type
Scope
Linked Issue/PR
User-visible / Behavior Changes
Agents can now call
discord({ action: "deleteMessage", channelId: "...", messageId: "..." })and all other Discord actions directly. Previously these actions were only callable via internal bot hooks triggered by user messages, not by agent code.Security Impact
discord.actionsconfig (existing). Owner-only actions (ban, timeout) enforced by existing authz layer in discord-actions-moderation.ts.Repro + Verification
Environment
tools.allowincludes "discord",channels.discord.actions.*gates in effectSteps
pnpm build— compiles cleanpnpm check— format, tsgo, lint, lint:tmp all passpnpm test— 573/573 tests passdiscordappears in messaging section with "messaging" + "full" profilesExpected
Tool is registered and agents can call discord actions with proper permission gates enforced.
Actual
Matches expected. Build clean, all checks pass, tool appears in catalog.
Evidence
Human Verification
messagetool.Compatibility / Migration
Failure Recovery
tools.allowin config, or remove the tool definition from tool-catalog.ts + the registration line from openclaw-tools.ts.Risks and Mitigations
discord.actionsconfig (existing). Owner-only actions (ban, timeout) enforced by existing authz layer. Bot must have the Discord permission for the action to succeed.messagetool adds Discord delete in the future.messagetool outbound adapter is send-only by design.discordtool is the full-featured Discord interface. Both can coexist.AI/Vibe-Coded PR
pnpm build,pnpm check(format + tsgo + lint),pnpm test(573/573 pass), including 37 discord-action-specific testsdiscordtool was never registered despite being intools.allowand the handlers being fully implemented. Found issues Feature: Add Discord Server/Guild Management Actions (Channels, Categories, Topics) #458 (Jan 2026) and [Discord] Expose channel-permission-set and channel-permission-remove in message tool #6538 (Feb 2026) as the originating feature requests, both still open with no existing PRs.createDiscordTool()in discord-tool.ts) that calls the existinghandleDiscordAction()function. The action handlers, permission gates, Discord API calls, and test suite are entirely unchanged. The only additions are the tool wrapper, its import/registration in openclaw-tools.ts, and its definition in tool-catalog.ts.codex review --base origin/main: Codex CLI not available on this machine. Will address any bot review comments when they appear.