Skip to content

Add Feishu as an external Bot channel - #413

Draft
luw2007 wants to merge 13 commits into
CopilotKit:mainfrom
luw2007:feat/feishu-channel-public
Draft

Add Feishu as an external Bot channel#413
luw2007 wants to merge 13 commits into
CopilotKit:mainfrom
luw2007:feat/feishu-channel-public

Conversation

@luw2007

@luw2007 luw2007 commented Sep 7, 2026

Copy link
Copy Markdown

Problem

OpenBot has no Feishu/Lark transport. Deployments using Feishu cannot message a coworker, link their Feishu identity, or inspect those external conversations in OpenBot.

Change

  • generalize the external conversation model shared with Slack so provider identity is preserved end to end
  • add Feishu long-connection ingress and replies with the official Node SDK
  • route direct messages and group mentions into the existing OpenBot turn engine
  • link Feishu identities to OpenBot accounts with the existing short-lived link-token boundary
  • load one or more Feishu app credentials from a strict server-local JSON file
  • add operator documentation and focused coverage for routing, linking, provider isolation, lifecycle, and configuration

Configuration

Set OPENBOT_FEISHU_APPS_FILE to a server-local JSON file containing appId, appSecret, and tenantKey. Unset means disabled. See docs/feishu.md.

Verification

  • bun run typecheck
  • bun test server/tests/feishu-channel.test.ts server/tests/config.test.ts server/tests/external-link-routes.test.ts server/tests/external-link-token.test.ts app/tests/external-thread-route.test.ts app/tests/sidebar-roster.test.ts (159 pass)

Review note

This draft is based on the still-open Slack architecture in #297. Current main has moved 53 commits and does not contain that prerequisite, so the branch intentionally remains a draft until #297 lands or this stack is restacked onto its successor.

luwei.will and others added 13 commits September 4, 2026 18:48
Four callers now build a Bot for a person — a chat request, a routine's
headless turn, a hop delivered to another Bot, and the boundary's own
lookup — and each passed the same eleven collaborators positionally. One
of them getting an argument wrong is a Bot that runs and quietly holds
different tools or a different role from the one the person is talking
to. ActorAgentResolver binds them once.

Choosing a coworker moves out of the HTTP route for the same reason: it
was the routing model call, the visibility rule, and the channel.routed
row all written inside a Hono handler, so nothing that is not an HTTP
request could route. CoworkerRoutingService owns the decision, and the
route turns its outcome into status codes.

That move makes an explicit name cheap enough to honour: a message that
names exactly one coworker on the asking person's roster no longer pays
a model call to be told what the person already said. Two matches are
refused with both names rather than guessed at.
A person mentions @openBot in a Slack thread and names or describes the
coworker they want. The thread is pinned to that coworker and replies
continue with it, without another mention.

Channels SDK owns Slack ingress, delivery, streaming and files. This
deployment stays the authority for everything that decides what may
happen: every turn re-resolves the Slack speaker to an OpenBot user and
reloads THAT person's roster, grants, policy and audit identity. A
second person in the same thread who cannot see the pinned coworker is
refused rather than run as the person who started it.

The coworker is built by the same resolver a browser turn uses, so a
Slack turn holds the same tools, the same standing role, the same
signed run assertion and the same stall guard. Its computer runs
through the same gateway, which means the same boundary decides, and
the same audit row is written.

Secrets, sign-in control and 2FA are never asked for in Slack. The
thread gets an expiring link to this deployment's own screen, and the
bounded assistance wait resumes when control is released there.

An unlinked Slack user is told so and handed a signed, expiring link;
the agent does not run. An exact match between a verified Slack email
and one active OpenBot account may create the first link. Nothing
already linked is ever silently reassigned.

State lives in Postgres, not in the process: the thread binding, the
transcript, the identity link and the approval decisions are all
tables, so a reply delivered to a second replica finds the same
conversation. The bindings table is append-only by trigger.
The Slack side of a conversation was only in Slack: a person could not
read what their coworker had done, and the account link and the secure
prompt a Slack turn sends somebody to had nowhere to land.

Three surfaces, all behind the existing session guard. Confirming a
Slack account is theirs happens on a page that reads the signed link
token and binds only to the OpenBot user completing the flow, with a
sign-in return that comes back to the same confirmation rather than the
roster. Taking the wheel or answering a secure prompt happens on the
coworker's own screen, reached from the expiring link in the thread.
And a Slack thread appears in the conversation sidebar, labelled, next
to the channels it already lists, opening a read-only transcript of the
turns as they were stored.

The computer tools a Slack turn calls are declared once, in shared, so
the browser and the channel offer the same contract rather than two
drifting copies of it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants