Skip to content

Channels (Slack/Telegram/Discord/WhatsApp) + app bridge - #120

Merged
its-janghoon merged 29 commits into
developfrom
feat/channels-app-bridge
Oct 17, 2026
Merged

its-janghoon merged 29 commits into
developfrom
feat/channels-app-bridge

Conversation

@its-janghoon

Copy link
Copy Markdown
Member

What's done

  • @redrob-code/channels-core: Transport, Allowlist, Dispatcher (one conversation = one session, /new), Renderer, config at ~/.redrob/channels.json, 0600 file secret store.
  • Four transports: Slack (Bolt Socket Mode, manifest yaml, moved from packages/slack), Telegram (grammY long-poll), Discord (discord.js gateway), WhatsApp (Baileys QR pairing).
  • Daemon: starts enabled channels on redrob serve, disables idle exit while a channel is on, POST /channels/reload, GET /channels/status.
  • App bridge: POST /v1/apps/register, heartbeat, DELETE /v1/apps/:id, GET /v1/apps; loopback only; registered apps appear as runtime MCP servers app:<name> and keep the daemon alive.
  • SDKs: packages/app-bridge (TS) and crates/redrob-app-bridge (Rust, loopback MCP server + register/heartbeat), engine-client::bridge_app().
  • Autostart: redrob service install|uninstall (systemd user / launchd / schtasks ONLOGON); first channels enable registers, last disable unregisters.
  • CLI: redrob channels list|status|enable|disable|allow|set-secret|slack-manifest --url, redrob apps list|unregister.
  • Docs: docs/CHANNELS.md, docs/APP-BRIDGE.md.

What's not done

  • Live round-trip with a real bot token (pending a Telegram token).
  • App repos (browser/office/cowork/design/canvas/film/shape/query/recall) have their bridge PRs open; their Cargo/npm pins move to the release tag after this ships.

What to review

  • Allowlist default (deny all until an ID is allowed) in channels-core.
  • /v1/apps loopback guard and the idle-exit interaction in daemon.
  • autostart.ts platform branches.

Intentions

Phone (Slack/Telegram/Discord/WhatsApp) -> Code daemon -> any registered Redrob app, with no relay server: each user runs their own bot app, tokens stay on their machine.

its-janghoon and others added 29 commits October 16, 2026 13:28
New workspace package @redrob-code/channels-core: the transport contract,
channels.json load/save, a 0600 file secret store behind SecretStore, a
deny-by-default allowlist, a conversation->session dispatcher persisted in
channels/sessions.json, a markdown-to-text renderer with chunking, and the
ChannelManager that starts enabled transports and attaches each to the daemon
as channel:<name>.
redrob serve --register starts every enabled channel from channels.json
through the (still empty) channelFactories registry, turns idle exit off
while any transport runs, and stops them on the way out. The server gains
GET /channels/status and POST /channels/reload behind a controller seam the
serve command fills in, following the clients group pattern.
list | status | enable | disable | allow | set-secret | slack-manifest.
Edits channels.json and the 0600 secrets file, then asks a running
registered daemon to POST /channels/reload; without one the change waits
for the next start. slack-manifest prints the Socket Mode app manifest, or
with --url the api.slack.com link that creates the app from it.
Files, in-chat commands, daemon behaviour, the two endpoints, the CLI and
the Transport contract Stage 2 transports implement.
bun.lock learns the new workspace package. The manager's daemon attachment
passes the SDK the {url, headers} it actually reads.
A desktop app registers its loopback MCP server with POST /v1/apps/register,
heartbeats it, and the daemon drops it when the heartbeats stop (same 90s
TTL as /api/clients). Only loopback mcp_url is accepted. Each registration
holds a client attachment so the idle-exit clock does not run while an app
is bridged; the serve tick sweeps apps before clients.
The MCP service seeds every new instance with the apps already registered
and subscribes to the registry so a registration connects app:<name> on
every live instance and an unregistration (or TTL expiry) disconnects it
and drops its tools. Adds remove(name) to the service. Runtime only: the
user's config file is never written.
JS SDK (@redrob-labs/sdk/daemon): apps.register/heartbeat/unregister/list
and bridgeApp() that registers and heartbeats at ttl/3 until stop().
Rust (redrob-engine-client): register_app/heartbeat_app/unregister_app/apps
and bridge_app() with a heartbeat thread that re-registers on 404.
CLI: redrob apps list [--json] and redrob apps unregister <id>.
docs/APP-BRIDGE.md: endpoints and payloads, TTL and heartbeat, the loopback
rule, the app:<name> MCP naming, JS and Rust examples, the CLI. A pointer
section in LOCAL-ENGINE-API.md.
@redrob-code/channels-telegram: TelegramTransport + telegramTransport factory. Chat id is the conversation, user id the sender; groups answer only mentions and replies; typing indicator; 4096 max chunk. Secret telegram.botToken documented in the package README and the CHANNELS.md secrets table.
New workspace package @redrob-code/channels-discord. One Discord channel
(DM, guild channel or thread) is one conversation; the sender is the
author's user id. DMs are always delivered, guild messages only when the
bot is mentioned, with the mention stripped. maxChunk 2000, typing via
sendTyping(), gateway reconnects left to discord.js, start() rejects on
a bad token. Secret: discord.botToken.

Documents the Message Content privileged gateway intent in the package
README and docs/CHANNELS.md, and adds the secrets table there.
…e daemon

A Rust Redrob app calls AppBridge::start with its tools: a minimal MCP
streamable-HTTP server (initialize, ping, tools/list, tools/call;
notifications 202) comes up on 127.0.0.1 at a free port on a std thread,
registers through redrob-engine-client's bridge_app (heartbeat and
re-register included) and unregisters on stop or drop. No async runtime
so egui apps can call it directly. README with an example; a short
"Using redrob-app-bridge (Rust)" section in docs/APP-BRIDGE.md.
startAppBridge serves ToolDefs over streamable-HTTP MCP on 127.0.0.1 (port 0,
one McpServer per session so tools/list_changed can be pushed), bridges the
URL into the daemon via @redrob-labs/sdk/daemon connectOrStart + bridgeApp,
and unregisters on stop(). zod or JSON Schema inputs. README with example
and a Using section in docs/APP-BRIDGE.md.
New workspace package @redrob-code/channels-whatsapp. Pairs as a linked
device via QR (no secret); start() resolves while unpaired and the QR is
exposed through qr()/qrDataUrl() and written to channels/whatsapp/qr.png.
conversationId = remoteJid, senderId = phone number; fromMe and status
broadcasts dropped; maxChunk 4000; composing presence as typing().
Move packages/slack (Bolt 3, Socket Mode) into packages/channels/slack as a
Transport for channels-core. One Slack thread is one conversation
(`<channel>:<thread_ts ?? ts>`), replies go in-thread, app_mention and
message.im/channels are handled with bot_id/subtype dropped and mentions
de-duplicated. Tokens come from the channel secret store (slack.botToken,
slack.appToken), never the environment. The Slack app manifest YAML now lives
in this package (manifestYaml, manifestUrl).

The old standalone bot and its workspace entry are removed. The CLI's
slack-manifest command keeps its own copy of the manifest for now: pointing it
at this package needs a dependency in packages/redrob/package.json, which the
parent wires after merge.
…nels-app-bridge

# Conflicts:
#	bun.lock
#	docs/CHANNELS.md
…els-app-bridge

# Conflicts:
#	bun.lock
#	docs/CHANNELS.md
…nels-app-bridge

# Conflicts:
#	bun.lock
#	docs/CHANNELS.md
The daemon now wires all four stage-2 transports into channelFactories and
depends on their workspace packages; bun.lock regenerated after the merges.
daemon/autostart.ts registers a systemd user unit, a launchd agent or a
Task Scheduler ONLOGON task running `redrob serve --register --idle-exit 0`
with the installing binary. `redrob channels enable` installs it on the first
enabled channel and `disable` removes it when none is left; `redrob service
install|uninstall|status` are the by-hand versions.
Apps outside this repo install the bridge from npm next to @redrob-labs/sdk,
so the package takes the published scope and drops private.
Drop push triggers from test, typecheck, engine-client and gitflow and the
weekly schedule from upstream-sync. A merge is a pull request that already
passed. Document that packages/app-bridge is vendored by apps, not published.
…new routes

The generated client lagged the protocol (channels.status, channels.reload,
apps.list/register/heartbeat/unregister), which failed check:generated, and
the HttpApi exerciser flagged the same six routes as missing scenarios.
@its-janghoon
its-janghoon merged commit 31a7dd3 into develop Oct 17, 2026
19 checks passed
@its-janghoon
its-janghoon deleted the feat/channels-app-bridge branch October 17, 2026 06:41
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.

1 participant