fix(config): coerce numeric Discord IDs to strings instead of rejecting (#45125)

Merged via squash.

Prepared head SHA: 099ba514a1
Co-authored-by: moliendocode <29582793+moliendocode@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
Moliendo 2026-04-01 15:07:28 -03:00 committed by GitHub
parent 4f407d2658
commit d076153fc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 402 additions and 108 deletions

View File

@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode.
## 2026.4.1

View File

@ -10199,10 +10199,7 @@
{
"path": "channels.discord.accounts.*.allowFrom.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -10452,10 +10449,7 @@
{
"path": "channels.discord.accounts.*.dm.allowFrom.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -10485,10 +10479,7 @@
{
"path": "channels.discord.accounts.*.dm.groupChannels.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -10710,10 +10701,7 @@
{
"path": "channels.discord.accounts.*.execApprovals.approvers.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -10937,10 +10925,7 @@
{
"path": "channels.discord.accounts.*.guilds.*.channels.*.roles.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -11140,10 +11125,7 @@
{
"path": "channels.discord.accounts.*.guilds.*.channels.*.users.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -11199,10 +11181,7 @@
{
"path": "channels.discord.accounts.*.guilds.*.roles.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -11382,10 +11361,7 @@
{
"path": "channels.discord.accounts.*.guilds.*.users.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -12625,10 +12601,7 @@
{
"path": "channels.discord.allowFrom.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -12936,10 +12909,7 @@
{
"path": "channels.discord.dm.allowFrom.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -12969,10 +12939,7 @@
{
"path": "channels.discord.dm.groupChannels.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -13240,10 +13207,7 @@
{
"path": "channels.discord.execApprovals.approvers.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -13467,10 +13431,7 @@
{
"path": "channels.discord.guilds.*.channels.*.roles.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -13670,10 +13631,7 @@
{
"path": "channels.discord.guilds.*.channels.*.users.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -13729,10 +13687,7 @@
{
"path": "channels.discord.guilds.*.roles.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
@ -13912,10 +13867,7 @@
{
"path": "channels.discord.guilds.*.users.*",
"kind": "channel",
"type": [
"number",
"string"
],
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,

View File

@ -895,7 +895,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -919,10 +919,10 @@
{"recordType":"path","path":"channels.discord.accounts.*.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -943,7 +943,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@ -963,7 +963,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -983,12 +983,12 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@ -1006,7 +1006,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@ -1120,7 +1120,7 @@
{"recordType":"path","path":"channels.discord.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \"mentions\" to only accept bot messages that mention the bot.","hasChildren":false}
{"recordType":"path","path":"channels.discord.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle).","hasChildren":false}
{"recordType":"path","path":"channels.discord.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.","hasChildren":false}
@ -1145,10 +1145,10 @@
{"recordType":"path","path":"channels.discord.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord DM Policy","help":"Direct message access control (\"pairing\" recommended). \"open\" requires channels.discord.allowFrom=[\"*\"] (legacy: channels.discord.dm.allowFrom).","hasChildren":false}
{"recordType":"path","path":"channels.discord.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -1169,7 +1169,7 @@
{"recordType":"path","path":"channels.discord.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@ -1189,7 +1189,7 @@
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -1209,12 +1209,12 @@
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@ -1232,7 +1232,7 @@
{"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}

View File

@ -41,6 +41,26 @@ describe("doctor discord provider repairs", () => {
"channels.discord.guilds.main.channels.general.users[0]",
"channels.discord.guilds.main.channels.general.roles[0]",
]);
expect(hits.every((hit) => hit.safe)).toBe(true);
});
it("marks unsafe numeric ids as not safe", () => {
const cfg = {
channels: {
discord: {
allowFrom: [106232522769186816, -1, 123.45, 42],
},
},
} as unknown as OpenClawConfig;
const hits = scanDiscordNumericIdEntries(cfg);
expect(hits).toEqual([
{ path: "channels.discord.allowFrom[0]", entry: 106232522769186816, safe: false },
{ path: "channels.discord.allowFrom[1]", entry: -1, safe: false },
{ path: "channels.discord.allowFrom[2]", entry: 123.45, safe: false },
{ path: "channels.discord.allowFrom[3]", entry: 42, safe: true },
]);
});
it("repairs numeric discord ids into strings", () => {
@ -50,7 +70,33 @@ describe("doctor discord provider repairs", () => {
allowFrom: [123],
accounts: {
work: {
allowFrom: [234],
dm: { allowFrom: [345], groupChannels: [456] },
execApprovals: { approvers: [456] },
guilds: {
ops: {
users: [567],
roles: [678],
channels: {
alerts: {
users: [789],
roles: [890],
},
},
},
},
},
},
guilds: {
main: {
users: [111],
roles: [222],
channels: {
general: {
users: [333],
roles: [444],
},
},
},
},
},
@ -59,27 +105,154 @@ describe("doctor discord provider repairs", () => {
const result = maybeRepairDiscordNumericIds(cfg);
expect(result.changes).toEqual([
expect.stringContaining("channels.discord.allowFrom: converted 1 numeric entry to strings"),
expect.stringContaining(
"channels.discord.accounts.work.execApprovals.approvers: converted 1 numeric entry to strings",
),
]);
expect(result.changes).toEqual(
expect.arrayContaining([
expect.stringContaining("channels.discord.allowFrom: converted 1 numeric entry to strings"),
expect.stringContaining(
"channels.discord.accounts.work.allowFrom: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.dm.allowFrom: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.dm.groupChannels: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.execApprovals.approvers: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.guilds.ops.users: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.guilds.ops.roles: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.guilds.ops.channels.alerts.users: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.accounts.work.guilds.ops.channels.alerts.roles: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.guilds.main.users: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.guilds.main.roles: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.guilds.main.channels.general.users: converted 1 numeric entry to strings",
),
expect.stringContaining(
"channels.discord.guilds.main.channels.general.roles: converted 1 numeric entry to strings",
),
]),
);
expect(result.config.channels?.discord?.allowFrom).toEqual(["123"]);
expect(result.config.channels?.discord?.guilds?.main?.users).toEqual(["111"]);
expect(result.config.channels?.discord?.guilds?.main?.roles).toEqual(["222"]);
expect(result.config.channels?.discord?.guilds?.main?.channels?.general?.users).toEqual([
"333",
]);
expect(result.config.channels?.discord?.guilds?.main?.channels?.general?.roles).toEqual([
"444",
]);
expect(result.config.channels?.discord?.accounts?.work?.allowFrom).toEqual(["234"]);
expect(result.config.channels?.discord?.accounts?.work?.dm?.allowFrom).toEqual(["345"]);
expect(result.config.channels?.discord?.accounts?.work?.dm?.groupChannels).toEqual(["456"]);
expect(result.config.channels?.discord?.accounts?.work?.execApprovals?.approvers).toEqual([
"456",
]);
expect(result.config.channels?.discord?.accounts?.work?.guilds?.ops?.users).toEqual(["567"]);
expect(result.config.channels?.discord?.accounts?.work?.guilds?.ops?.roles).toEqual(["678"]);
expect(
result.config.channels?.discord?.accounts?.work?.guilds?.ops?.channels?.alerts?.users,
).toEqual(["789"]);
expect(
result.config.channels?.discord?.accounts?.work?.guilds?.ops?.channels?.alerts?.roles,
).toEqual(["890"]);
});
it("formats numeric id warnings", () => {
it("skips entire list when it contains unsafe numeric ids", () => {
const cfg = {
channels: {
discord: {
allowFrom: [42, 106232522769186816, -1, 123.45],
dm: { allowFrom: [99] },
},
},
} as unknown as OpenClawConfig;
const result = maybeRepairDiscordNumericIds(cfg);
expect(result.changes).toEqual([
expect.stringContaining(
"channels.discord.dm.allowFrom: converted 1 numeric entry to strings",
),
]);
expect(result.config.channels?.discord?.allowFrom).toEqual([
42, 106232522769186816, -1, 123.45,
]);
expect(result.config.channels?.discord?.dm?.allowFrom).toEqual(["99"]);
});
it("returns repair warnings when unsafe numeric ids block doctor fix", () => {
const cfg = {
channels: {
discord: {
allowFrom: [106232522769186816],
},
},
} as unknown as OpenClawConfig;
const result = maybeRepairDiscordNumericIds(cfg, {
doctorFixCommand: "openclaw doctor --fix",
});
expect(result.changes).toEqual([]);
expect(result.warnings).toEqual([
expect.stringContaining("could not be auto-repaired"),
expect.stringContaining('rerun "openclaw doctor --fix"'),
]);
});
it("formats numeric id warnings for safe entries", () => {
const warnings = collectDiscordNumericIdWarnings({
hits: [{ path: "channels.discord.allowFrom[0]", entry: 123 }],
hits: [{ path: "channels.discord.allowFrom[0]", entry: 123, safe: true }],
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings).toEqual([
expect.stringContaining("Discord allowlists contain 1 numeric entries"),
expect.stringContaining("Discord allowlists contain 1 numeric entry"),
expect.stringContaining('run "openclaw doctor --fix"'),
]);
});
it("formats numeric id warnings for unsafe entries", () => {
const warnings = collectDiscordNumericIdWarnings({
hits: [{ path: "channels.discord.allowFrom[0]", entry: 106232522769186816, safe: false }],
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings).toEqual([
expect.stringContaining("cannot be auto-repaired"),
expect.stringContaining("manually quote the original values"),
]);
});
it("formats warnings for mixed safe and unsafe entries", () => {
const warnings = collectDiscordNumericIdWarnings({
hits: [
{ path: "channels.discord.allowFrom[0]", entry: 123, safe: true },
{ path: "channels.discord.dm.allowFrom[0]", entry: 456, safe: true },
{ path: "channels.discord.dm.allowFrom[1]", entry: 106232522769186816, safe: false },
],
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings).toHaveLength(4);
expect(warnings[0]).toContain("1 numeric entry");
expect(warnings[1]).toContain('run "openclaw doctor --fix"');
expect(warnings[2]).toContain("2 numeric entries in lists that cannot be auto-repaired");
expect(warnings[2]).toContain("channels.discord.dm.allowFrom[0]");
expect(warnings[3]).toContain('rerun "openclaw doctor --fix"');
});
});

View File

@ -3,7 +3,7 @@ import { sanitizeForLog } from "../../../terminal/ansi.js";
import { asObjectRecord } from "../shared/object.js";
import type { DoctorAccountRecord } from "../types.js";
type DiscordNumericIdHit = { path: string; entry: number };
type DiscordNumericIdHit = { path: string; entry: number; safe: boolean };
type DiscordIdListRef = {
pathLabel: string;
@ -102,7 +102,11 @@ export function scanDiscordNumericIdEntries(cfg: OpenClawConfig): DiscordNumeric
if (typeof entry !== "number") {
continue;
}
hits.push({ path: `${pathLabel}[${index}]`, entry });
hits.push({
path: `${pathLabel}[${index}]`,
entry,
safe: Number.isSafeInteger(entry) && entry >= 0,
});
}
};
@ -122,17 +126,88 @@ export function collectDiscordNumericIdWarnings(params: {
if (params.hits.length === 0) {
return [];
}
const samplePath = sanitizeForLog(params.hits[0]?.path ?? "channels.discord.allowFrom");
const sampleEntry = sanitizeForLog(String(params.hits[0]?.entry ?? ""));
const lines: string[] = [];
const hitsByListPath = new Map<string, DiscordNumericIdHit[]>();
for (const hit of params.hits) {
const listPath = hit.path.replace(/\[\d+\]$/, "");
const existing = hitsByListPath.get(listPath);
if (existing) {
existing.push(hit);
continue;
}
hitsByListPath.set(listPath, [hit]);
}
const repairableHits: DiscordNumericIdHit[] = [];
const blockedHits: DiscordNumericIdHit[] = [];
for (const hits of hitsByListPath.values()) {
if (hits.some((hit) => !hit.safe)) {
blockedHits.push(...hits);
continue;
}
repairableHits.push(...hits);
}
if (repairableHits.length > 0) {
const sample = repairableHits[0];
const samplePath = sanitizeForLog(sample.path);
const sampleEntry = sanitizeForLog(String(sample.entry));
lines.push(
`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${samplePath}=${sampleEntry}).`,
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
);
}
if (blockedHits.length > 0) {
const sample = blockedHits[0];
const samplePath = sanitizeForLog(sample.path);
lines.push(
`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${samplePath}).`,
`- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`,
);
}
return lines;
}
function collectBlockedDiscordNumericIdRepairWarnings(params: {
hits: DiscordNumericIdHit[];
doctorFixCommand: string;
}): string[] {
const hitsByListPath = new Map<string, DiscordNumericIdHit[]>();
for (const hit of params.hits) {
const listPath = hit.path.replace(/\[\d+\]$/, "");
const existing = hitsByListPath.get(listPath);
if (existing) {
existing.push(hit);
continue;
}
hitsByListPath.set(listPath, [hit]);
}
const blockedHits: DiscordNumericIdHit[] = [];
for (const hits of hitsByListPath.values()) {
if (hits.some((hit) => !hit.safe)) {
blockedHits.push(...hits);
}
}
if (blockedHits.length === 0) {
return [];
}
const sample = blockedHits[0];
const samplePath = sanitizeForLog(sample.path);
return [
`- Discord allowlists contain ${params.hits.length} numeric entries (e.g. ${samplePath}=${sampleEntry}).`,
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that could not be auto-repaired (e.g. ${samplePath}).`,
`- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`,
];
}
export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): {
export function maybeRepairDiscordNumericIds(
cfg: OpenClawConfig,
params?: { doctorFixCommand?: string },
): {
config: OpenClawConfig;
changes: string[];
warnings?: string[];
} {
const hits = scanDiscordNumericIdEntries(cfg);
if (hits.length === 0) {
@ -147,6 +222,12 @@ export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): {
if (!Array.isArray(raw)) {
return;
}
const hasUnsafe = raw.some(
(entry) => typeof entry === "number" && (!Number.isSafeInteger(entry) || entry < 0),
);
if (hasUnsafe) {
return;
}
let converted = 0;
const updated = raw.map((entry) => {
if (typeof entry === "number") {
@ -170,8 +251,16 @@ export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): {
}
}
if (changes.length === 0) {
const warnings =
params?.doctorFixCommand === undefined
? []
: collectBlockedDiscordNumericIdRepairWarnings({
hits,
doctorFixCommand: params.doctorFixCommand,
});
if (changes.length === 0 && warnings.length === 0) {
return { config: cfg, changes: [] };
}
return { config: next, changes };
return { config: next, changes, warnings };
}

View File

@ -71,4 +71,35 @@ describe("doctor repair sequencing", () => {
expect(result.warningNotes[0]).not.toContain("\u001B");
expect(result.warningNotes[0]).not.toContain("\r");
});
it("emits Discord warnings when unsafe numeric ids block repair", async () => {
const result = await runDoctorRepairSequence({
state: {
cfg: {
channels: {
discord: {
allowFrom: [106232522769186816],
},
},
} as unknown as OpenClawConfig,
candidate: {
channels: {
discord: {
allowFrom: [106232522769186816],
},
},
} as unknown as OpenClawConfig,
pendingChanges: false,
fixHints: [],
},
doctorFixCommand: "openclaw doctor --fix",
});
expect(result.changeNotes).toEqual([]);
expect(result.warningNotes).toHaveLength(1);
expect(result.warningNotes[0]).toContain("could not be auto-repaired");
expect(result.warningNotes[0]).toContain('rerun "openclaw doctor --fix"');
expect(result.state.pendingChanges).toBe(false);
expect(result.state.candidate.channels?.discord?.allowFrom).toEqual([106232522769186816]);
});
});

View File

@ -48,7 +48,11 @@ export async function runDoctorRepairSequence(params: {
};
applyMutation(await maybeRepairTelegramAllowFromUsernames(state.candidate));
applyMutation(maybeRepairDiscordNumericIds(state.candidate));
applyMutation(
maybeRepairDiscordNumericIds(state.candidate, {
doctorFixCommand: params.doctorFixCommand,
}),
);
applyMutation(maybeRepairOpenPolicyAllowFrom(state.candidate));
applyMutation(maybeRepairBundledPluginLoadPaths(state.candidate, process.env));
applyMutation(maybeRepairStalePluginConfig(state.candidate, process.env));

View File

@ -59,7 +59,7 @@ describe("config discord", () => {
);
});
it("rejects numeric discord allowlist entries", () => {
it("coerces safe-integer numeric discord allowlist entries to strings", () => {
const res = validateConfigObject({
channels: {
discord: {
@ -79,11 +79,42 @@ describe("config discord", () => {
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(
res.issues.some((issue) => issue.message.includes("Discord IDs must be strings")),
).toBe(true);
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.discord?.allowFrom).toEqual(["123"]);
expect(res.config.channels?.discord?.dm?.allowFrom).toEqual(["456"]);
expect(res.config.channels?.discord?.dm?.groupChannels).toEqual(["789"]);
expect(res.config.channels?.discord?.guilds?.["123"]?.users).toEqual(["111"]);
expect(res.config.channels?.discord?.guilds?.["123"]?.roles).toEqual(["222"]);
expect(res.config.channels?.discord?.guilds?.["123"]?.channels?.general?.users).toEqual([
"333",
]);
expect(res.config.channels?.discord?.guilds?.["123"]?.channels?.general?.roles).toEqual([
"444",
]);
expect(res.config.channels?.discord?.execApprovals?.approvers).toEqual(["555"]);
}
});
it("rejects numeric discord IDs that are not valid non-negative safe integers", () => {
const cases = [106232522769186816, -1, 123.45];
for (const id of cases) {
const res = validateConfigObject({
channels: {
discord: {
allowFrom: [id],
},
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(
res.issues.some((issue) =>
issue.message.includes("not a valid non-negative safe integer"),
),
).toBe(true);
}
}
});
});

View File

@ -46,9 +46,22 @@ const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional
const DiscordIdSchema = z
.union([z.string(), z.number()])
.refine((value) => typeof value === "string", {
message: "Discord IDs must be strings (wrap numeric IDs in quotes).",
});
.transform((value, ctx) => {
if (typeof value === "number") {
if (!Number.isSafeInteger(value) || value < 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
`Discord ID "${String(value)}" is not a valid non-negative safe integer. ` +
`Wrap it in quotes in your config file.`,
});
return z.NEVER;
}
return String(value);
}
return value;
})
.pipe(z.string());
const DiscordIdListSchema = z.array(DiscordIdSchema);
const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]);