msg-gateway
A standalone Rust message gateway that bridges user-facing communication protocols (Telegram, Discord, Slack, Email) to backend agent protocols (Pipelit, OpenCode). Both user-facing adapters and backends can run as external subprocesses, making it easy to add new protocols in any language.
Features
- Multi-protocol support — Telegram, Discord, Slack, Email, Generic HTTP/WebSocket
- External adapter architecture — Adapters run as separate processes, written in any language
- Pluggable backends — Built-in support for Pipelit and OpenCode; external backends run as subprocesses
- Named backend routing — Route different credentials to different backend instances
- Message filtering — CEL-based guardrails for inbound/outbound message validation
- File handling — Automatic download/upload of attachments with local caching
- Health monitoring — Emergency alerts when backend is unreachable
- Hot reload — Config and guardrail changes apply without restart
- Admin API — CRUD operations for credentials
plitCLI tool — Pipelit ecosystem CLI for chat, admin, and agent integration
Quick Start
# Install
# Bootstrap (interactive — sets up Pipelit, LLM provider, credentials)
# Launch the full stack
# Chat with your AI agent
Manual Setup (advanced)
# Build from source
# Configure
# Edit config.json with your credentials
# Run gateway only
GATEWAY_CONFIG=config.json
Configuration
Environment variables can be referenced with ${VAR_NAME} syntax.
Backends
Backends receive messages from the gateway and process them with AI/LLM services. The gateway supports two backend types:
Built-in Backends
Built-in backends are compiled into the gateway binary:
- Pipelit (
protocol: "pipelit") — Webhook-based backend with callback support - OpenCode (
protocol: "opencode") — REST + SSE backend with session management (built-in Rust implementation)
External Backends
External backends run as separate subprocesses in backends_dir. Each backend directory contains:
backends/opencode/
├── adapter.json # {"name": "opencode", "command": "node", "args": ["dist/main.js"]}
├── dist/main.js # Backend implementation
└── package.json
External backends receive environment variables:
BACKEND_PORT— Port to listen onGATEWAY_URL— Gateway callback URLBACKEND_TOKEN— Auth token for gateway requestsBACKEND_CONFIG— JSON config blob (fromconfig.backends[name].config)
External backends must implement:
POST /send— Receive messages from gatewayGET /health— Health check endpoint
Each credential specifies which backend to route to via the backend field. The gateway spawns one backend instance per named backend entry in config.backends, shared across all credentials referencing that backend.
Adapters
Adapters are external processes in adapters_dir. Each adapter directory contains:
adapters/telegram/
├── adapter.json # {"name": "telegram", "command": "python3", "args": ["main.py"]}
├── main.py # Adapter implementation
└── requirements.txt
Adapters receive environment variables:
INSTANCE_ID— Unique instance identifierADAPTER_PORT— Port to listen onGATEWAY_URL— Gateway callback URLCREDENTIAL_ID— Credential identifierCREDENTIAL_TOKEN— Protocol auth tokenCREDENTIAL_CONFIG— JSON config blob
Built-in Generic Adapter
The generic adapter is built-in and provides REST + WebSocket interface:
# Send message via REST
# Connect via WebSocket
Guardrails
Guardrails let you filter inbound messages using CEL (Common Expression Language) expressions. Each rule is a JSON file in guardrails_dir. Rules are evaluated in lexicographic filename order, so zero-padded prefixes (01-, 02-, ...) give you predictable ordering.
Rule format
Each file contains a single JSON object:
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Human-readable rule name |
type |
"cel" |
"cel" |
Rule type (only CEL supported) |
expression |
string | required | CEL expression that must evaluate to bool |
action |
"block" or "log" |
"block" |
What to do when the expression is true |
direction |
"inbound", "outbound", or "both" |
"inbound" |
Which messages to apply the rule to |
on_error |
"allow" or "block" |
"allow" |
Behavior when CEL evaluation fails |
reject_message |
string | none | Body returned in the HTTP 403 response when blocked |
enabled |
bool | true |
Set to false to disable without deleting the file |
CEL expression examples
The message variable is available in every expression:
# Block messages containing sensitive keywords (Rust regex syntax)
message.text.matches("(?i)(password|secret|api_key)")
# Block messages over 10000 characters
size(message.text) > 10000
# Log messages that include file attachments (never blocks)
size(message.attachments) > 0
# Block messages from a specific source protocol
message.source.protocol == "telegram"
Example rule files
Limitations
matches()uses Rust regex syntax, not RE2 or the Google CEL spec. Lookaheads and backreferences are not supported. Case-insensitive matching uses the(?i)flag.has()is not available.Option<T>fields serialize asnullwhenNone, so CEL sees them asnullrather than absent. However, fields withskip_serializing_if(likeattachmentswhen empty) are omitted from the CEL context entirely. Useon_error: "allow"(the default) so rules referencing omitted fields fail open instead of blocking.- Outbound guardrails are not evaluated in v1. Only
"direction": "inbound"rules take effect.
Hot reload
Guardrail rules reload automatically when rule files in guardrails_dir change. No restart needed.
Configuration
Point guardrails_dir at a directory of rule files:
If guardrails_dir is omitted and a guardrails/ directory exists next to config.json, it's picked up automatically.
CLI Tool (plit)
A standalone command-line client for interacting with the gateway. Supports interactive chat, one-shot messaging, WebSocket streaming, credential management, and health checks. Backend-agnostic — works with Pipelit, OpenCode, or any external backend.
Install
# Binary at target/release/plit
Usage
# Set connection defaults
# Interactive chat REPL
# One-shot send (pipe-friendly)
|
# Stream responses as JSONL (for agents, scripts, jq)
# Health check
# Credential management (requires GATEWAY_ADMIN_TOKEN)
Output Modes
- TTY (interactive terminal) — human-readable formatted output
- Piped (stdout redirected) — auto-switches to JSON/JSONL
--jsonflag — force JSON output in any context
Environment Variables
| Variable | Used by | Description |
|---|---|---|
GATEWAY_URL |
all commands | Gateway URL (default: http://localhost:8080) |
GATEWAY_TOKEN |
chat, send, listen | Credential token for authentication |
GATEWAY_ADMIN_TOKEN |
credentials, health | Admin token for management commands |
API Endpoints
| Endpoint | Description |
|---|---|
GET /health |
Health check |
POST /api/v1/send |
Send message to user (backend → gateway → adapter) |
POST /api/v1/adapter/inbound |
Receive message from adapter |
GET /files/{id} |
Download cached file |
GET /admin/credentials |
List credentials |
POST /admin/credentials |
Create credential |
PATCH /admin/credentials/{id}/activate |
Activate credential |
Development
Local Setup
# Run tests
# Run with coverage
# Lint
# Format
Pre-Push Validation (Recommended)
Install a pre-push hook to catch issues before CI:
This prevents formatting, linting, and test failures from reaching CI.
Contributing
Contributions are welcome! Please read CLAUDE.md for detailed development guidelines.
Quick Start
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Install the pre-push hook (see Development section above)
- Make your changes
- Run the full check suite:
cargo fmt --all && cargo clippy -- -D warnings && cargo test - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
PR Quality Checklist
Before opening a PR, verify:
-
cargo fmt --all— Code is formatted -
cargo clippy --all-targets --all-features -- -D warnings— No warnings -
cargo test --all-features— All tests pass -
cargo build --release— Release build succeeds - Error handling uses
Result<?>/map_err(nounwrap()/expect()in production code) - Structured logging includes relevant context fields (e.g.,
credential_id,message_id) - Config secrets use
${ENV_VAR}syntax (no hardcoded tokens) - New functionality includes tests
All PRs must pass CI checks (lint, test, build) and AI code review.
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.