Repository navigation
fix(discord): thread-parent validation, starter routing, safe mentions/links, snapshots, guarded downloads (#229) - #275
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 28 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (9)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… Discord-sent thread parents, survive nested error bodies (#229)
…rries parent_id (#229)
# Conflicts: # docs/UPSTREAM_SYNC.md
|
Merge gate: CI green on
Local full validation on Bots: CodeRabbit was rate-limited (no review). No other bot comments. Merging with --admin (Protect Main requires a code-owner approval). |
Summary
Brings the Discord webhook adapter's correctness and security fixes up to chat@4.41.1.
post_message(including the slash-command branch),edit_message,delete_message,add_reaction,remove_reaction,start_typingandfetch_messagesnow confirm that a thread id's thread segment belongs to its channel segment, using a fresh 5-minute cache entry orGET /channels/{thread}. On a mismatch they raiseValidationError("discord", "Discord thread {t} does not belong to channel {p}"). A forgeddiscord:g:A:<thread in B>can no longer slip past channel-scoped guards such as the [4.41/C3] Conversation context + AI tool scoping (read & write guards, strict_scope) #195 agent-tool scope._encode_interaction_thread_id, a port of upstreamencodeInteractionThreadId), forwarded messages and forwarded reactions. Forwarded reactions now usedata["thread"]before the cache or a lookup._with_message_channel): when the message id equals the thread segment, the call goes to the thread first and falls back to the parent channel only on Discord error10008.DiscordApiError(status, body)with.codeis attached to every_discord_fetchNetworkError. Recovery from160004checks.codeinstead of searching the error text for "160004".replace_bare_mentionsscanner ([4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193), so emails, URLs, code and existing<@…>/<#…>tokens are left alone. A link whose label equals its URL renders bare.<https://…>and[t](<https://…>)keep their brackets._flatten): handled in forwardedMESSAGE_CREATEevents,parse_message/fetch_messages, and the type-21 starter'sreferenced_message.rehydrate_attachment, withfetch_metadata={"url": url}. Itsfetch_datadownloads throughchat_sdk.shared.download.download_attachment(url, adapter="discord"), and errors that are not alreadyNetworkErrorare wrapped in one.width/heightare now carried as well (upstream has had them since 4.31).Upstream commits mapped
490fa00e0d4e3ee4d4c52cadreplace_bare_mentions6de45723rehydrate_attachmentb605cf634bdf7213_flattensnapshotsa94995e5rehydrate_attachmentb6fa24c6c4f709fe_with_message_channel,DiscordApiErrorb7c9316b_resolve_thread_channel_id/_remember_thread_parent61b98fcadata.thread(Gateway half N/A, #57)Tests ported
From
index.test.ts, ported intotests/test_discord_adapter.py:thread starter message routing: all 3 testsrehydrateAttachment: all 3 testsFrom
markdown.test.ts, ported intotests/test_discord_format.py: all 8toAst/fromAstlink and mention tests and all 6renderPostable (mentions)tests. The existing raw-mention test was renamed to its upstream title so it isn't duplicated.Python-specific tests:
DiscordApiErrorcode parsing: non-JSON, string code and bool code all giveNone160004recovery through the real_discord_fetchMessage.to_json/from_jsonand still downloads after rehydrationadapter="discord"defaults, and unknown errors are wrapped inNetworkErrorThe starter-routing and 160004 tests go through the real
_discord_fetchwith a fake aiohttp session, soDiscordApiErroris built the way production builds it.Existing fixtures updated: 7 thread-op tests in
test_discord_extended.pynow return the parent lookup first, and assert it, as upstream does. The two 160004 fixtures now attachDiscordApiError.test_discord_final.py::test_fetches_from_thread_channelduplicated an extended test, so it now covers the cached-parent path (no lookup).Fidelity
Delta vs committed report (HEAD): missing 198 -> 198 (+0)(after merging main). The Discord test files are not fidelity-mapped yet (#78), soscripts/fidelity_target.jsonis unchanged. Strict at the pin: 733/733 (730 real tests + 3 absorbers).Divergences
Recorded in
docs/UPSTREAM_SYNC.md(new "Discord correctness and security" section, plus one non-parity row):[t](<https://…>)is recognized from the parsed destination: the<…>are stripped fromurland the style goes indata.<https://…>stays a text node. The rendered output is the same as upstream.fetch_metadata={"url": url}on inbound attachments, as the issue specifies. Upstream sets none and falls back tourl. Both rehydrate the same way.Other Python-only differences change only how many requests are made, not the results:
Consumer impact
Discord only.
GET /channels/{thread}per thread whose parent isn't cached. Replies to inbound events usually hit the cache.ValidationError.fetch_data.parent_idnow encodes asdiscord:g:T(wasdiscord:g:T:T), matching upstream._discord_fetchfor thread ids must return the parent lookup first, or seed_remember_thread_parent.post_messageto a different conversation now goes to that channel instead of PATCHing the interaction's@originalresponse (upstreamtryPostSlashResponseparity, a pre-existing translation bug).threadlacksparent_id(upstream's forwarder always sends it) is no longer encoded with a guessed parent: the parent is looked up for a threadchannel_type, otherwise the channel alone is used.Validation
ruff check and format: clean.
audit_test_quality: 0 hard failures.--check-docs: OK.--strictat the pin: 733/733. pytest: 6688 passed, 24 skipped (after merging main). pyrefly: 0 errors.Closes #229
Part of #184
Review
gpt-6-astra round 1 raised one P2:
from_ast(to_ast("<@123><@456>"))returns<@123>@456. Upstream does the same (markdown.ts:106plus:164; I ran upstreamreplaceBareMentionsunder Node at chat@4.41.1 and got"<@123>@456"). Per the parity-triage rule I added a code comment instead of new code. Round 2 passed with no actionable findings.Merge gate
Final HEAD:
6ba22c8. Main (2aaef13) merged in with no conflicts.fidelity_target.jsonregenerated with no change (+0).Independent review findings
post_messagecaptured posts to other conversations. It now requiresslash_ctx.channel_id == thread_id, as upstreamtryPostSlashResponsedoes (index.ts:1466 at 4.41.1; also present at 4.31.0). Two reviewers reported this one. Test:test_slash_context_for_another_conversation_does_not_capture_the_post. The "every path" comment was softened._handle_forwarded_messagecached a guessed parent (thread.parent_iddefaulting tochannel_id). In astra round 1 this became: usethreadonly when it carriesparent_id. Test:test_forwarded_thread_without_parent_id_still_gets_replies[thread-channel-type|no-channel-type]._parse_discord_error_codenow also catchesRecursionErrorfrom deeply nested bodies. Test:test_parses_only_a_numeric_json_code[deeply-nested].test_forwarded_thread_parent_looked_up_from_discord_is_remembered) and the URL-quoted lookup path (test_thread_segment_is_url_quoted_in_the_parent_lookup). Each new test was checked against the unfixed or mutated code, where it fails.gpt-6-astra (this convergence pass; 2 rounds)
b35ebbf): one P2. A forwardedthreadwithoutparent_idstill encodeddiscord:g:T:T, and the new validation rejected replies to it. Fixed in6ba22c8with a test.6ba22c8): no actionable findings.Bots: CodeRabbit was rate-limited (no review). There were no inline or gemini comments.
CI on
6ba22c8: Lint & Type Check, test (3.12), test (3.13), CodeQL / Analyze all pass.Local full validation on
6ba22c8: ruff check/format clean; audit 0 hard failures;--check-docsOK;--strict733/733; pytest 6688 passed, 24 skipped; pyrefly 0 errors.