feat(channel): add bot-at-bot support#7
Open
mazhe-nerd wants to merge 2 commits into
Open
Conversation
Support multiple bots collaborating in one chat (@-ing each other to hand off work). All additive and opt-in; default receive/send/normalize behavior is unchanged. - InboundMessage.sender_type / sender_is_bot: pass through the raw sender.sender_type (previously dropped); Identity.is_bot semantics unchanged. - get_bot_identity(): the bot's own identity; raises not_connected before connect. - get_chat_members() / get_chat_bots(): chat roster (users / bots), paginated and cached; resolve_chat_members hook to source the roster externally. - reply(msg, ...): replies to the trigger and follows its thread shape. - @name -> open_id: name-only structured mentions and, with resolve_mentions_in_text, @name tokens in text/markdown are resolved against the chat roster; unknown/ambiguous names are left as plain text. - <at> hardening: escape attacker-controlled display names and validate open_ids at every <at> sink (the @ALL sentinel is preserved). - policy.bot_loop_guard: opt-in sliding-window guard against bot ping-pong, drop or reject (reason bot_loop). - allow_from / group_allowlist: warn on a misplaced cli_ app id. Version 1.1.0 -> 1.2.0.
…y P2) Resolves the blocking deep-review of PR #7: - content_text default restored (keeps the bot's own rendered @-mention); new InboundMessage.body_text carries the bot-mention-stripped view. - reply(): long thread replies keep every chunk in-thread (not just the first). - chunker never splits an <at>...</at> tag across message boundaries. - chat member cache: keyed by (chat_id, id_type); name->open_id index only from open_id members; a full API refresh rebuilds the index (drops departed members, re-evaluates name collisions); pagination tracks completeness + warns on truncation; repeated page_token guard. - roster collection / sender-name resolution moved after safety admission, so a rejected message can't write the roster or trigger the members API. - members request: validates HTTP status + JSON shape (no empty-success on errors, no leaked AttributeError); tenant-token verify runs off the event loop with a total timeout and unified error wrapping. - SendOpts.resolve_mentions_in_text accepted from dict opts. - runtime update_policy() atomically reconfigures the bot loop guard; loop-guard config validation + monotonic-clock out-of-order handling. - docs: correct the include_bot permission name; document body_text and the resolve_chat_members empty-list semantics.
Collaborator
Author
|
Addressed a round of review feedback ( Behavior / correctness
Roster / cache
Requests / safety
Docs: corrected the A few lower-risk items (extra roster hardening, singleflight, thread-safety, name-scan boundary refinements) are tracked as follow-ups. Full test suite green across Python 3.8–3.12, plus an end-to-end double-bot run against a real chat. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Support multiple bots collaborating in one chat — agents
@-ing each other tohand off work. The channel layer sits between the low-level OpenAPI SDK and the
app, so it's where the extra signals and guards a bot-to-bot handoff needs
belong. Everything here is opt-in and additive; default behavior is
unchanged.
Ports the bot-at-bot support from the Node channel SDK
(larksuite/channel-sdk-node#10) to the Python SDK.
Changes (SDK-facing)
Context — know who sent a message
InboundMessage.sender_type('user' | 'bot' | 'system' | 'anonymous' | 'app')and the convenience
sender_is_bot, passed through from the raw event(previously dropped).
Identity.is_botsemantics are unchanged.channel.get_bot_identity()→ the bot's ownBotIdentity(raisesFeishuChannelError(code="not_connected")beforeconnect()), e.g. to inlineinto a system prompt.
resolve_sender_namesfillsInboundMessage.sender_namefrom the chatroster.
Roster & outbound
channel.get_chat_members(chat_id)— a chat's users (paginated, cached).channel.get_chat_bots(chat_id)— a chat's bots (users and bots are servedby separate endpoints; this covers the bots).
channel.reply(msg, ...)— replies to the triggering message and keeps thereply in-thread when that message was in a topic thread.
@name → open_idresolution: name-only entries inOutboundText/OutboundPostmentions, and — with opt-inSendOpts.resolve_mentions_in_text—
@nametokens in a text/markdown body, resolved against the chat roster. Aname that is unknown or shared by more than one member is left as plain text
and never mis-mentioned.
resolve_chat_membershook to source the roster from your own directoryinstead of the API.
<at>output is hardened: attacker-influenced display names are escaped andopen_ids validated at every sink (the
@allsentinel is preserved).Policy & safety
PolicyConfigdocs clarifyallow_from(sender idsou_…) vsgroup_allowlist(chat idsoc_…); a misplaced app id (cli_…) now logs awarning instead of silently matching nothing.
policy.bot_loop_guardbreaks bot ↔ bot@-mention ping-pong loops(sliding window over "another bot @-ed me" messages; on trip either drops or
emits a
rejectwithreason="bot_loop"; a human message resets it). NewChatMember/BotLoopGuardConfigare exported.@-only mention (no body) is delivered as a valid wake —mentioned_botis
Truewith emptycontent_text, and the bot's own mention is stripped fromthe rendered content so
mentioned_bot and not content_text.strip()detects abare poke.
Compatibility
New fields are optional, new methods/options are additive, and every new
behavior is opt-in (off by default). Without opting in, receive/send/connect/
normalization behavior is identical to the previous release, except that a
message which
@-mentions the bot no longer renders the bot's own@nameincontent_text(it is stripped). NewChannelConfigfields are appended at theend, preserving positional construction. Version
1.1.0→1.2.0.Testing
(name-collision → ambiguous, TTL, capacity),
@nameresolution (includinglinear-time / ReDoS-safe scanning), the
<at>escaping/validation, the loopguard,
reply,get_chat_members/get_chat_bots(pagination, caching,chat_idURL-encoding), the allowlist warning, and the empty-@wake.sender_is_bot,
get_bot_identity,get_chat_members/get_chat_bots,reply,the
@-name handoff, the empty-@wake, and the loop-guard trip.