fix: admit trusted peer bots in SlackTransport.receive (#7463) - #7610
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The admission is mirrored but its provenance is dropped: Watch
Suggestions
[DESIGN-REVIEWED] ae1fce6 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence is in. The production Slack inbound gate lives at First-Principles-Verdict: CONCERNS The trust decision is now spelled inline at two drop sites — this PR fixes today's divergence by installing the machine that produces tomorrow's. What this change shipsIntent: make the unwired transport inbound path admit allow-listed peer bots exactly as the live Socket Mode path does — a FIX (latent; closes reported issue #7463, honestly declared as having no observable effect today).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] ae1fce6 |
|
Disposition: rebutted (direction re-litigation, not an implementation defect)
The subtraction proposed here is exactly option 2 of the two shapes issue #7463 itself offers ("Either consult trusted_bot_ids here the same way events.py does, or ... say so in the comment"). This PR was dispatched on option 1 by the pipeline's triage decision, which the issue's own text supports: the ordering lesson, the audited denials, and the not-eaten trusted bot_message are all named there as applying "to the first option". So the review is re-opening a decided A-vs-B, not finding a defect in the chosen A. On the substance of the block:
Happy to defer to a maintainer ruling if one lands the other way; absent that, the PR keeps the issue's option 1 as dispatched. |
SlackTransport.receive dropped every bot-authored event unconditionally while the Socket Mode drop site (slack/events.py) admits peer bots on a positive slack.trusted_bot_ids match, so the two Slack inbound paths disagreed about whether an allow-listed peer bot is admissible -- and the transport's comment asserted the stale pre-trusted-bots rationale as intentional. Mirror the events.py trust decision in receive(): a new keyword-only trusted_bot_ids constructor param (frozen snapshot, empty default keeps the blanket drop), positive-match admission with the gateway's own bot id never trusted, fail-closed on an unverified self identity, SEL-audited denials and admissions, and the trust decision ordered BEFORE the subtype filter so a trusted bot's bot_message is not eaten. The admitted bot's bot_id stands in as user_id; loop bounding stays the dispatch layer's job. Specs updated in the same commit. Closes #7463
3f00564 to
ae1fce6
Compare
|
Disposition: fixed -- span=d27520644eb8
Legitimate: the spec paragraph this PR already rewrites carried pre-existing stale capability values contradicting |
|
Answering the Design Review CONCERNS verdict, one disposition per concern: 1.
The mechanism concern is real and now tracked, but the prescribed change widens the channel-neutral 2. The admission predicate is duplicated verbatim across events.py and transport.py -- accepted-and-deferred to #7622.
Legitimate -- both this lane and First Principles independently converged on it, and it is the cause-level fix for the #7463 class. Deferred rather than folded in because the extraction rewrites the LIVE Socket Mode hot path ( |
|
Answering the First Principles CONCERNS verdict, one disposition per concern: 1. Second spelling of a security decision (extraction into one shared owner) -- accepted-and-deferred to #7622.
Agreed on the cause-level diagnosis -- the decision needs a single owner, and your framing ("fixes today's divergence by installing the machine that produces tomorrow's") is fair. Deferred rather than folded in because the extraction rewrites the LIVE Socket Mode gate inside a PR whose risk posture is touching only the unwired file, and 2. The "agreement" is already inexact (live config read vs constructor snapshot) -- accepted-and-deferred, same issue. Real residual difference, disclosed in #7622's body as a decision the extraction must settle (the shared predicate takes the set as an argument; the wiring layer picks the read timing). Constructor snapshot was kept here deliberately: it matches the transport's existing 3. Doc capability-figure correction (40000->3900, 5->10) is an undeclared rider -- fixed (declared). Accurate: it landed via the GPT lane's advisory finding on the same spec paragraph this PR rewrites (see the span=d27520644eb8 disposition above). The PR body's Tests/What-changed sections now note it; declared in the body's "What changed" list as of this round. |
bolichen97
left a comment
There was a problem hiding this comment.
Reviewed via parallel subagent audit: diff matches description, CI fully green, no blocking findings, no unresolved threads.
Problem / Motivation
slack.trusted_bot_idsis effective at the Socket Mode drop site (slack/events.py), butSlackTransport.receiveinsrc/kiro_crew/slack/transport.pystill drops every bot-authored event unconditionally (bot_idorsubtype == "bot_message"), and its comment asserts the opposite of what the platform does ("a bot id is never in the owner-only allow-list anyway" --trusted_bot_idsis a second allow-list that exists precisely to admit bot ids). The two Slack inbound paths disagree about whether an allow-listed peer bot is admissible.Why it matters
The defect is latent --
SlackTransporthas no production construction site, so no message is dropped today. The cost is deferred: whoever wires Slack inbound through the transport abstraction silently reintroduces the exact defect the trusted-bots feature fixed, in a file that looks settled, and the stale comment actively tells that reader the drop is intentional.What changed (motivation -> approach -> change)
Symptom:
trusted_bot_idsappears zero times intransport.pywhileevents.pygates on exactly that set. Root cause: when the trusted-bots admission landed on the Socket Mode path, the parallel (unwired) transport inbound path kept the old blanket bot drop and its now-false rationale. Change: mirror the events.py trust decision inreceive(), keeping its load-bearing properties:trusted_bot_idsis a new keyword-only constructor param (frozen snapshot, same immutability rationale asallowed_users; empty default keeps dropping every bot event, deny-by-default).bot_idmatch against the allow-list; the gateway's own bot id is never trusted even when listed (error=own_bot_id_never_trusted), and an unverified self identity (validated_self_bot_id()empty) fails closed (error=trusted_bot_requires_verified_self_id).subtype == "bot_message"filter, so untrusted denials are SEL-audited (operation="slack_transport.receive",error=untrusted_bot) rather than silently subtype-dropped, and a trusted bot'sbot_messageis not eaten.bot_idstands in asuser_id, its admission is audited (outcome="allowed",resources="trusted_bot"), and it is dispatched without consulting the owner-onlyallowed_userslist -- the positive allow-list match is its authorization, exactly as on the events.py path. Loop bounding (the per-thread trusted-bot turn cap) remains the dispatch layer's job.docs/system-specs/modules/messaging.md(theSlackTransportparagraph) anddocs/system-specs/modules/slack-gateway.md(the trusted-bots bullet) are updated in the same commit.messaging.mdparagraph's stale capability figures are corrected to matchSLACK_CAPABILITIESin code (max_message_chars40000 -> 3900 =SLACK_MSG_LIMIT;max_buttons5 -> 10), per the GPT review lane's advisory finding on this PR.Tests
New
TestReceiveTrustedBotsclass intest/test_slack_transport.py:test_trusted_bot_admitted-- atrusted_bot_idsmember carryingsubtype == "bot_message"reaches dispatch withbot_idasuser_id(locks trust-before-subtype ordering).test_untrusted_bot_still_dropped-- a bot outside the allow-list never reaches dispatch.test_untrusted_bot_denial_is_audited-- the denial emits SEL (error=untrusted_bot).test_trusted_admission_is_audited-- the admission emits SEL (resources="trusted_bot").test_own_bot_never_trusted_even_when_listed-- self id in the allow-list still drops.test_unverified_self_id_fails_closed-- emptyvalidated_self_bot_id()admits no bots.test_trusted_set_is_frozen_snapshot-- mutating the source set post-construction cannot widen admission.All pre-existing transport tests pass unchanged (empty default preserves the blanket drop).
Manual verification
N/A -- unit coverage sufficient: the transport has no production construction site (nothing routes through
receive()today), so the gate is pure in-process logic fully exercised by the unit tests.Related Issues
Closes #7463
Pattern harvest
Rule candidate: review-prompt
Pattern: a security gate fixed on one inbound path was not mirrored to the parallel path guarding the same boundary, and the parallel path's comment still documented the pre-fix rationale as intentional.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)