Connect agents
Human bridges
nats-hub is a communication layer, not a chat app. Humans join the bus through bridge workers — thin adapters that translate between NATS envelopes and a human-facing transport (Telegram, SMS, Email, Slack, Discord, Postiz, …).
Every bridge is the same shape. Only the transport import differs.
The two directions
Human → NATS: human sends message → envelope published to inbox.<agent>
NATS → Human: envelope on inbox.<bridge> → forwarded to the human's chatThe bus stays agnostic: agents never know they're talking to a human. They just
see envelopes on inbox.<identity>.
Email is built as a plugin with a gate in the middle (agents get their own
addresses, and spam never wakes them): see EMAIL_PLUGIN.md.
Template: telegram_bridge.py
telegram_bridge.py is the reference implementation. It:
- Connects to NATS and subscribes to
channel.inbox.<identity>. - On each envelope → forwards
extract_text(payload)to the Telegram chat (bot.send_message). This is the NATS → Human direction. - Polls Telegram for inbound messages and publishes each as an envelope to
inbox.<recipient-agent>. This is the Human → NATS direction.
Dry-run mode (no credentials needed)
Without the --telegram-token, the bridge runs in --dry-run mode: it logs every
message it would send instead of calling the API. This lets you verify the
NATS wiring end-to-end without a Telegram bot token:
# terminal 1 — router
./target/debug/hub-server --db-path nats_hub.db
# terminal 2 — bridge (dry-run)
python3 telegram_bridge.py --identity human-bridge-telegram --dry-run
# terminal 3 — send a message that should reach the human
./target/debug/hub-publish --channel inbox.human-bridge-telegram \
--to human-bridge-telegram --from agentA --message "Hello human"
# → bridge logs: [dry-run] → Telegram chat None: Hello humanLive mode
export TG_TOKEN="<bot token>"
python3 telegram_bridge.py \
--identity human-bridge-telegram \
--chat-id 123456789 \
--recipient orchestratorDiscord bridge: discord_bridge.py
discord_bridge.py is the same shape applied to Discord. It subscribes to
channel.inbox.<identity>, forwards envelopes to a Discord channel
(channel.send), and publishes inbound channel messages to
inbox.<recipient-agent> via hub.send.<channel>.
It is an optional dependency bridge: it needs discord.py
(pip install discord.py) only in live mode. Dry-run works without the SDK —
the import is wrapped in try/except, so the script still loads and
exercises the NATS wiring with no Discord credentials and no discord.py
installed.
Note: the bot must have the Message Content Intent enabled on the Discord Developer Portal, and the intent is requested at startup (
intents.message_content = True).
Dry-run mode (no credentials needed)
# terminal 1 — router
./target/debug/hub-server --db-path nats_hub.db
# terminal 2 — bridge (dry-run; no token, no discord.py required)
python3 discord_bridge.py --identity human-bridge-discord --dry-run
# terminal 3 — send a message that should reach the human
./target/debug/hub-publish --channel inbox.human-bridge-discord \
--to human-bridge-discord --from agentA --message "Hello human"
# → bridge logs: [dry-run] → Discord channel None: Hello humanLive mode
export DISCORD_BOT_TOKEN="<bot token>"
export DISCORD_CHANNEL_ID="123456789012345678"
python3 discord_bridge.py \
--identity human-bridge-discord \
--recipient orchestratorLive mode requires discord.py:
pip install discord.pyIf discord.py is missing and --dry-run is not set, the bridge exits with an
install hint rather than crashing inside the import.
Adding another bridge (SMS, Email, Slack, …)
Copy telegram_bridge.py and swap the transport:
| Transport | Inbound (Human→NATS) | Outbound (NATS→Human) |
|---|---|---|
| Telegram | python-telegram-bot polling | bot.send_message |
| SMS (Twilio) | Twilio webhook → publish | Twilio REST messages.create |
| IMAP idle → publish | SMTP sendmail | |
| Slack | slack-bolt socket mode | client.chat_postMessage |
| Discord | discord.py on_message | channel.send |
| Postiz | Postiz webhook → publish | postiz post |
Each bridge:
- subscribes to
channel.inbox.<identity>, - extracts a string from the payload (
text/message/content/prompt), - forwards to the human,
- and publishes inbound human messages to
inbox.<recipient>.
No changes to the Rust bus are required — bridges are pure adapters.