Skip to content

fix(discord): read allowed_guilds as IDs and deny every guild on an unreadable allowlist - #6338

Merged
WillemJiang merged 3 commits into
bytedance:mainfrom
Lesereingrape:fix/discord-allowed-guilds-ids
Oct 5, 2026
Merged

WillemJiang merged 3 commits into
bytedance:mainfrom
Lesereingrape:fix/discord-allowed-guilds-ids

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

Fixes #6337

Why

config.example.yaml:3042 tells operators that channels.discord.allowed_guilds "can also be a single guild ID", but DiscordChannel.__init__ builds the allowlist by iterating the configured value. A scalar therefore becomes one entry per character, and a value the parser cannot read at all becomes an empty set — which the inbound gate then reads as "no allowlist".

So an operator who restricts the bot to their own server either blocks that server and admits three unrelated ones, or, with a value like "@acme" or a {id: …} mapping from the runtime channel config, opens the bot to every guild it has joined. Nothing is logged either way. Fixes #6337.

What changed

  • allowed_guilds accepts the documented shorthand: a single ID — quoted or not — is one entry, not its digits. allowed_guilds: 321 no longer raises TypeError: 'int' object is not iterable while the channel is being constructed.
  • An entry that is not a positive numeric guild ID is now reported (WARNING, naming the entry) instead of dropped by a bare continue.
  • Behavior change: an allowlist that yields no usable ID now denies every guild and logs at ERROR, instead of silently allowing all. The operator asked for a restriction; an unreadable one no longer opens the bot. Leaving the key unset, or [], still means allow all.
  • allowed_channels (the mention_only exemption list), parsed by the same lines, gets the same scalar handling. Its IDs are kept verbatim — an unreadable entry there only leaves mention_only in force, which is already fail-closed.

The shape follows the merged Telegram counterpart, _parse_allowed_users (#6230): None means "no allowlist", an empty frozenset means "deny all", and the gate is a small _check_guild() predicate rather than a truthiness test on the set.

Surface area

  • Frontend UI
  • Backend API — inbound gate in backend/app/channels/discord.py
  • Agents / LangGraph
  • Sandbox
  • Skills
  • MCP
  • Config / setup
  • Docs
  • CI infra
  • Default behavior change — a configured-but-unreadable allowed_guilds now denies every guild rather than allowing all; the log line tells the operator to fix the value. Unset/[] is unchanged.
  • Docs / tests / CI only

Screenshots / Recording

N/A — no frontend change.

Bug fix verification

  • Test path: backend/tests/test_discord_channel.py::TestDiscordAllowedGuilds and ::TestDiscordAllowedChannels (26 cases, added in the first commit).
  • Red on main, green on this branch: yes.
# main @ 5872d07e, tests only:  24 failed, 2 passed
# this branch:                  26 passed

The two passes on main are the negative controls ("322", "457" — values that must deny). Red covers both shapes measured on main:

{'allowed_guilds': '321'}  -> {3, 2, 1}   # guild 321 blocked, 1-3 admitted
{'allowed_guilds': 321}    -> TypeError: 'int' object is not iterable (discord.py:55)
{'allowed_guilds': '@acme'}-> set()       # gate at :501 reads empty as allow-all
{'allowed_guilds': {'id': 321}} -> set()  # ditto

TestDiscordAllowedGuilds::test_on_message_applies_the_allowlist and TestDiscordAllowedChannels::test_exempt_channel_needs_no_mention drive the real _on_message path with the documented scalar, so the assertion is about admission, not about the parsed set.

Validation

  • pytest tests/test_discord_channel.py — 53 passed.
  • pytest tests/test_channels.py — 413 passed, 2 errors. Both errors are pre-existing and Windows-only: a CJK-parametrized test_split_text_preserves_content_within_discord_limit id makes pytest's own PYTEST_CURRENT_TEST exceed the 32767-character os.environ limit during teardown. Reproduced identically (9 passed, 2 errors) at 5872d07e with this branch's changes reverted, so it is not caused by this diff, and CI runs Ubuntu.
  • ruff check + ruff format --diff over both files — clean.
  • Not run: make test (full backend suite) and the blocking-I/O suite, which need the repo's uv-managed dev group on Linux; this change touches only config parsing in one channel constructor and its inbound gate.

AI assistance

Tool(s) used: Qoder (agent session on this account).

How was it used: locating the parsing site and the config.example.yaml promise, writing the failing tests first, writing the patch, and running the tests/lint above. The measured red/green numbers and the pre-existing Windows errors are reproduced verbatim from the runs.

Has a human read every line of this diff? Not yet — left unchecked deliberately; the account owner has not reviewed this diff line by line at the time of filing. Tick this box once they have.

…he bot

config.example.yaml documents channels.discord.allowed_guilds as a value that
"can also be a single guild ID", but the constructor iterates it: a scalar
string becomes one entry per character, and an unquoted YAML int raises
TypeError. Whatever remains is gated by truthiness, so an allowlist with no
parseable entry leaves the set empty and admits every guild.
…able

The allowlist was built by iterating the configured value, so the documented
single-guild shorthand ("can also be a single guild ID" in
config.example.yaml) became one entry per character: allowed_guilds: "321"
admitted guilds 1, 2 and 3 while blocking 321, and an unquoted YAML int raised
TypeError in the constructor. The empty result then read as "no allowlist", so
a restriction the operator configured could open the bot to every guild the bot
joined.

Parse both ID lists through helpers that treat a scalar as a one-entry list and
drop unreadable entries with a warning, following the telegram channel: an
allowlist with no parseable entry now denies every guild and logs at ERROR.
Mention exemptions keep their IDs verbatim, where an unreadable entry only
leaves mention_only in force.
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines labels Oct 5, 2026

@willem-bd willem-bd left a comment

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.

Reviewed against 8f6b662. The parser is correct and mirrors the merged Telegram precedent (_parse_allowed_users, #6230): None means no allowlist, an empty frozenset means deny-all, and _check_guild replaces the truthiness test. I ran backend/tests/test_discord_channel.py in a clean worktree at this head (import path verified to resolve to the worktree): 53 passed; ruff check and ruff format --check are clean on both files. The fail-closed change for an unreadable allowlist is deliberate, tested, and the right call. One suggestion below: the operator-facing comments for these keys in config.example.yaml were not updated alongside the behavior change.

- ``bot_token``: Discord Bot token.
- ``allowed_guilds``: (optional) List of allowed Discord guild IDs. Empty = allow all.
- ``allowed_guilds``: (optional) List of allowed Discord guild IDs, or a
single ID. Empty = allow all; a non-empty list with no valid ID denies

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.

Docs sync: the operator-facing template comments for these keys still describe the old behavior. In config.example.yaml (line 3042 at this head) the allowed_guilds comment reads "empty = allow all guilds; can also be a single guild ID" — it does not mention that a non-empty value yielding no usable ID now denies every guild, which is the default behavior change this PR declares (its own "Config / setup" and "Docs" surface boxes are unchecked). The allowed_channels comment two lines below likewise does not mention the single-ID shorthand this PR adds. Since config.example.yaml is the primary operator doc for channels.discord, please update both comments in this change set per the repo's docs-update policy.

@WillemJiang WillemJiang added this to the 2.2.0 milestone Oct 5, 2026
@github-actions github-actions Bot added the area:docs Documentation and Markdown only label Oct 5, 2026
@WillemJiang
WillemJiang merged commit 2523be6 into bytedance:main Oct 5, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ area:docs Documentation and Markdown only risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] discord: a single allowed_guilds ID is split into digits, and an unreadable allowlist opens the bot to every guild

3 participants