@chatpanel/channels
v0.1.0
Published
Drive ChatPanel agents from external messaging surfaces (Telegram, WhatsApp). One normalize→invoke→stream core per platform, wired through @chatpanel/pii redaction and the @chatpanel/events audit log. The local Telegram shape is outbound-only (long-poll)
Maintainers
Readme
@chatpanel/channels
Drive your local ChatPanel agent from an external messaging surface — today Telegram, next WhatsApp. A phone message becomes an agent turn on your machine, streamed back to the chat, with PII redacted before anything leaves for the model and every run written to an audit log.
Design doc: chatpanel/docs/feature-f7-remote-channels.md.
Why this shape
The Telegram adapter uses getUpdates long-poll, which is outbound-only — the same
property as Claude Code Remote Control: your machine never opens an inbound port, works behind
NAT, needs no tunnel or public IP. The only hop that reaches the agent is the bridge on
127.0.0.1:4319, and it never leaves the box.
Telegram getUpdates ─long-poll→ @chatpanel/channels ─POST /chat (bridge token)→ chatpanel-bridge
↑ editMessageText (throttled) ←── SSE delta/done ──────────────────────────────────┘
│ normalize → pairing gate → pii.redact → capability(actor.kind:'channel')
▼ append capability.invoked / privacy.redacted / privacy.egressLayout
| file | role | pure? |
|---|---|---|
| src/normalize.js | platform message → one normalized shape | ✅ |
| src/pairing.js | who may drive it, and their reach ceiling | ✅ |
| src/invoke.js | normalized → capability invocation + pii redact + audit events | ✅ |
| src/stream.js | bridge SSE → folded reply (+ Telegram split/throttle) | ✅ |
| src/bridge.js | POST /chat (SSE) + /cancel on the local bridge | net |
| src/eventlog.js | append-only JSONL sink over @chatpanel/events appender | net |
| src/adapters/telegram.js | getUpdates long-poll transport (the LOCAL shape) | net |
| bin/chatpanel-channels.js | CLI that wires it together | — |
The pure core is unit-testable without a bot or a bridge (npm test = 26 tests, incl. a
real-socket SSE integration test).
Conversations are multi-turn: each chat keeps a bounded, redacted history so a follow-up
("…and the second one?") resolves against the prior answer. /new forgets it.
Run
npm install -g @chatpanel/channels # or npx @chatpanel/channels telegram- Start the bridge once (it writes
~/.chatpanel/bridge-token):chatpanel-bridge. - Create a bot with @BotFather, copy its token.
- Run the adapter:
TELEGRAM_BOT_TOKEN=123:abc chatpanel-channels telegram- Message your bot. It will say not paired. Pair the chat:
- Bootstrap (dev): set
CHANNELS_ALLOW=<yourChatId>before starting, or - Codes (real): mint a one-time code (
pairing.requestCode()— surfaced in the extension) and send/pair <code>from the phone.
- Bootstrap (dev): set
Chat commands: /pair <code>, /new (forget the conversation + fresh privacy vault), /stop, /help.
Env
| var | default | meaning |
|---|---|---|
| TELEGRAM_BOT_TOKEN | — | BotFather token (required) |
| CHANNELS_ALLOW | — | comma list of chat ids to pre-pair (bootstrap) |
| CHANNELS_AGENT | claude | bridge engine id (claude, codex, …) |
| CHANNELS_PRIVACY | standard | standard restores real values for you · strict keeps [[PERSON_1]] in the reply |
| CHANNELS_PII_TIER | basic | basic regex · full (needs a roster) also pseudonymizes people/orgs |
| CHATPANEL_BRIDGE_URL | http://127.0.0.1:4319 | bridge address |
| CHATPANEL_BRIDGE_TOKEN | — | overrides the token file |
Security posture (read before shipping)
This is the honest §7 of the design doc, made concrete:
- PII redaction on egress is on by default and mandatory. Telegram bot traffic is not
end-to-end encrypted, so inbound text is redacted (
@chatpanel/pii) before it reaches the agent/model.CHANNELS_PRIVACY=strictadditionally keeps placeholders in the reply so the provider never sees a real value either. - Pairing is authentication, not authorization. A chat-id allowlist + a paired code proves
who sent a message. It does not bound what that message may do. Prompt-injection →
tool execution is the real risk, because the bridge
/chatruns shell/filesystem tools. reachis carried but tool-scoping is the next layer (not yet built). Until it lands, run against a bridge whose agent is read-only / ask-mode for anything destructive. Do not point a write-capable, auto-approving agent at an untrusted chat.- Secrets: the BotFather token and the bridge token are the crown jewels. The bridge token
is read
0600from~/.chatpanel; keep the bot token out of shell history (use a file / a keychain in a shipped build).
Every run leaves capability.invoked / privacy.redacted / privacy.egress in
~/.chatpanel/channels/events.jsonl — the audit trail neither Claude Code Remote nor Hermes
has.
Status
Local (Telegram) shape: built. The WhatsApp/Cloudflare relay shape and the per-actor tool-authorization policy are designed but not built — see the feature doc, §5 build order.
