Daemon that bridges chat platforms (Rocket.Chat and more) to AI agent backends (Claude, OpenCode)
Project description
agent-chat-gateway
Inspired by OpenClaw's vision of making AI agents accessible from any messaging app, agent-chat-gateway takes that idea to the team and developer layer: rather than running a new AI agent for you, it bridges your existing local agent sessions โ Claude CLI, OpenCode, or any custom backend โ to your team's chat platform. No code changes to your agent required; configure once, and your whole team can collaborate through it.
How it compares to Claude Code Channels: Claude Code's native Channels feature (v2.1.80+) connects a single Claude Code session to Telegram, Discord, or iMessage โ a great fit for personal use.
agent-chat-gatewaywas developed independently before Channels shipped and is designed for a different layer: multi-user team deployments with multiple agent backends, role-based access control, human-in-the-loop permission approval, and sticky sessions shared across an entire workspace.
What's Supported
| Supported today | Extensible via | |
|---|---|---|
| Chat platforms | Rocket.Chat | Connector ABC |
| Agent backends | Claude CLI, OpenCode | AgentBackend ABC |
Features
- ๐ Pluggable connectors โ Rocket.Chat today; add Slack, Discord, and others via the
ConnectorABC - ๐ค Multiple agent backends โ Claude CLI and OpenCode out of the box; add your own via
AgentBackendABC - ๐ Role-based access control โ Owner and Guest roles with per-tool allow-lists (regex-based, tree-sitter Bash parsing)
- ๐ก๏ธ Human-in-the-loop approval โ Sensitive tool calls pause for owner
approve/denybefore proceeding (similar to Claude Code's permission relay) - ๐ Sticky sessions โ Each chat room keeps its own persistent agent session across daemon restarts
- ๐ Attachment support โ Files uploaded in chat are automatically downloaded and injected into the agent prompt
- ๐ง Context injection โ Load domain knowledge, system prompts, and room profiles into agent sessions at startup
- โก Multi-connector โ Run multiple chat platform connections simultaneously
Key Concepts
| Term | Meaning |
|---|---|
| Connector | Adapter for a chat platform (e.g., Rocket.Chat). Handles incoming messages and posts replies. |
| Agent backend | The AI tool the gateway dispatches to (e.g., Claude CLI, OpenCode). |
| Watcher | A binding between one chat room and one agent backend. One watcher per room. |
Use Cases
1. Build a Super-Powered Chatbot for Your Team
Turn your existing agentic tool (Claude, OpenCode, or any custom backend) into a shared team assistant in your chat workspace. Configure multiple rooms with different agents, define per-role tool access, and keep humans in the loop for sensitive operations.
2. Access Your Agent from Any Messaging App
Already have Claude CLI or OpenCode running on your machine? Bridge it to your team's Rocket.Chat workspace so everyone can interact with it directly from chat โ no terminal required. Similar in concept to Claude Code's Channels feature, but works with any agent backend and is built for multi-user team access.
3. Continue an Existing Agent Session Remotely
Pin a chat room to a specific agent session ID and pick up exactly where you left off from your messaging app โ similar in spirit to Claude Code's Remote Control feature, but accessible from your team's shared chat room rather than a personal device.
See docs/user-guide.md for configuration examples for each use case.
Quick Start
Recommended: AI-guided install
The easiest way to install is to ask your AI agent to do it for you โ it handles missing dependencies, config setup, and troubleshooting automatically:
Claude Code:
claude "Please install agent-chat-gateway by following the instructions at https://raw.githubusercontent.com/HammerMei/agent-chat-gateway/main/docs/install-agent.md"
OpenCode:
opencode "Please install agent-chat-gateway by following the instructions at https://raw.githubusercontent.com/HammerMei/agent-chat-gateway/main/docs/install-agent.md"
Manual install
# Requires: Python 3.12+, git
curl -fsSL https://raw.githubusercontent.com/HammerMei/agent-chat-gateway/main/install.sh | bash
The installer checks Python 3.12+, installs uv if needed, clones the repo, and launches the interactive setup wizard.
# Start the daemon
agent-chat-gateway start
# Check it's running
agent-chat-gateway status
agent-chat-gateway list
See docs/install-agent.md for a full step-by-step installation guide.
Architecture
RC User @mention
โ RC Server (DDP WebSocket event)
โ connectors/rocketchat/ (DDP listener, normalization, attachment download)
โ core/message_processor.py (per-room queue consumer)
โ enqueue() (intercepts approve/deny permission commands)
โ agent.send() (calls AgentBackend, blocks on response)
โ AgentBackend (session lifecycle + message dispatch)
โ PreToolUse HTTP hook (pauses for owner approval if permissions.enabled)
โ connector.send_text() (posts response back to room via REST)
The daemon exposes a Unix domain socket (~/.agent-chat-gateway/control.sock) so the
agent-chat-gateway CLI can send commands (list/pause/resume/reset watchers) to the running process.
Module Layout
agent-chat-gateway/
โโโ config.yaml # User-editable: connectors, agents, permissions
โโโ pyproject.toml
โโโ gateway/
โโโ __init__.py
โโโ agents/
โ โโโ __init__.py # AgentBackend ABC (create_session, send โ AgentResponse)
โ โโโ response.py # AgentResponse + TokenUsage normalized dataclasses
โ โโโ session.py # AgentSession: thin scripting wrapper around AgentBackend
โ โโโ claude/ # ClaudeBackend: drives the Claude CLI subprocess
โ โโโ opencode/ # OpenCodeBackend: drives the opencode CLI subprocess
โ โโโ hooks/
โ โโโ role-enforcement.ts # opencode plugin: guest RBAC + owner approval trigger
โโโ cli.py # argparse entry point (start/stop/restart/status/list/pause/resume/reset/send)
โโโ config.py # YAML loader โ GatewayConfig / AgentConfig / PermissionConfig
โโโ daemon.py # Double-fork daemonize, PID file, signal handling
โโโ service.py # Top-level orchestrator: connectors + brokers + SessionManagers
โโโ control.py # Unix socket ControlServer for CLIโdaemon command routing
โโโ core/ # Platform-agnostic library
โ โโโ connector.py # Connector ABC + normalized dataclasses (Room, IncomingMessage, โฆ)
โ โโโ session_manager.py # Per-connector orchestrator; delegates to collaborators below
โ โโโ dispatch.py # MessageDispatcher: routes messages to per-room processors
โ โโโ message_processor.py # Per-room async queue consumer
โ โโโ agent_turn_runner.py # Execute one agent turn (typing โ send โ deliver)
โ โโโ watcher_lifecycle.py # Watcher state machines (start/pause/resume/reset/stop)
โ โโโ permission.py # PermissionBroker ABC
โ โโโ permission_state.py # PermissionRegistry (in-memory) + PermissionRequest
โ โโโ tool_match.py # Tool allow-list matching (regex + tree-sitter bash parsing)
โ โโโ context_injector.py # Inject context files into agent sessions
โ โโโ state_store.py # Persist WatcherState to JSON
โ โโโ config.py # Shared config types (ConnectorConfig, AgentConfig, โฆ)
โโโ connectors/
โโโ rocketchat/ # Rocket.Chat connector (DDP WebSocket + REST)
โ โโโ connector.py # RocketChatConnector(Connector)
โ โโโ config.py # RocketChatConfig dataclass
โ โโโ normalize.py # RC raw DDP doc โ IncomingMessage
โ โโโ outbound.py # send_text / typing indicator / online notification
โ โโโ rest.py # RC REST client (login, post_message, upload_file, โฆ)
โ โโโ websocket.py # DDP-over-WebSocket client with reconnect + keepalive
โโโ script/
โโโ connector.py # ScriptConnector โ in-memory, for scripting & tests
Key components
| Component | Responsibility |
|---|---|
core/connector.py |
Platform-agnostic Connector ABC and normalized dataclasses (Room, IncomingMessage, UserRole). |
core/session_manager.py |
Orchestrates the connector, spawns MessageProcessor per room, persists state, handles CLI control socket. |
core/message_processor.py |
Dequeues inbound messages, intercepts approve/deny permission commands, builds agent prompt, calls AgentBackend.send(), posts replies. |
connectors/rocketchat/ |
All Rocket.Chat knowledge: DDP subscription, REST API, message normalization, RBAC role resolution, attachment download. |
connectors/script/ |
In-memory ScriptConnector for scripting, testing, and agent-to-agent piping. Zero network calls. |
agents/response.py |
AgentResponse and TokenUsage normalized dataclasses returned by all backends. |
agents/session.py |
AgentSession โ thin scripting wrapper around AgentBackend. No connector, no room, no state file. |
core/permission.py + permission_state.py |
Human-in-the-loop approval system. Brokers intercept tool calls, post RC notifications, and await owner decisions. |
core/tool_match.py |
Tool allow-list matching; bash commands split via tree-sitter AST to prevent compound-command bypasses. |
service.py |
Wires config โ connectors โ permission brokers โ SessionManager instances. One SessionManager per connector. |
config.py |
Loads config.yaml into typed dataclasses. Supports env-var expansion ($VAR). |
Configuration (config.yaml)
connectors:
- name: rc-home
type: rocketchat
server:
url: https://your-rocketchat.example.com
username: bot-username
password: "$RC_PASSWORD" # env-var expansion supported
allowed_users:
owners:
- alice # full access: tools, tasks, approve/deny permissions
guests: [] # conversation-only; restricted tool whitelist
attachments:
max_file_size_mb: 10
download_timeout: 30
cache_dir: agent-chat.cache
default_agent: assistance
agents:
assistance:
type: claude
command: claude
working_directory: /tmp/acg # required: agent working directory
timeout: 360 # seconds; must be > permissions.timeout
new_session_args: []
session_prefix: agent-chat # prefix for Claude session names
permissions:
enabled: true # enable human-in-the-loop tool approval
timeout: 300 # seconds before auto-deny (must be < timeout above)
skip_owner_approval: false # set true to auto-approve all owner tool calls
owner_allowed_tools: # auto-approved for owners (no prompt shown)
- tool: "bash"
params: "ls.*|pwd|echo.*"
guest_allowed_tools: # auto-approved for guests; others silently denied
- tool: "read"
Role-Based Access Control
Every message sent to the agent subprocess is prefixed with a trusted header injected by the connector:
[Rocket.Chat #<room> | from: <username> | role: owner|guest] <message text>
The agent uses this prefix to determine the sender's role. Two enforcement layers run in parallel in the agent subprocess environment:
Layer 1 โ Shell hook (Claude Code PreToolUse)
~/.claude/hooks/pretooluse-role-enforcement.sh runs on every tool call:
ACG_ROLE=ownerโ exits 0 (allow all tools)ACG_ROLE=guest+ tool inACG_ALLOWED_TOOLSโ exits 0ACG_ROLE=guest+ tool NOT in whitelist โ exits 2 (block, no further hooks run)
Layer 2 โ opencode plugin (tool.execute.before)
gateway/agents/opencode/hooks/role-enforcement.ts is symlinked into the working
directory's .opencode/plugins/ folder. Same logic as the shell hook for guests.
For owners, triggers output.status = "ask" on sensitive tools to fire opencode's
permission.asked SSE event (used by the permission approval system).
Human-in-the-Loop Permission Approval
When permissions.enabled: true, sensitive tool calls (Bash, Write, Edit,
MultiEdit, NotebookEdit) are paused and require explicit owner approval via RC chat
before the agent proceeds.
How it works
Agent attempts sensitive tool call
โ PreToolUse HTTP hook fires (Claude) / permission.asked SSE event (opencode)
โ PermissionBroker posts approval request to RC chat room:
๐ **Permission required** `[a3k9]`
**Tool:** `Bash`
**Params:** `command='rm ./build'`
Reply `approve a3k9` or `deny a3k9`
โ Owner replies in RC chat
โ MessageDispatcher.dispatch() intercepts the command (BEFORE the queue)
โ asyncio.Future resolved โ agent subprocess unblocked
โ Tool executes (approve) or is skipped (deny)
Approval commands
Type these directly in the RC chat room (no slash prefix โ RC client would intercept / commands):
approve a3k9 โ allow the tool call
deny a3k9 โ block the tool call
Important: These commands are intercepted by the gateway before they reach the agent. The agent never sees them. If you type a wrong ID:
- Wrong length โ
โ ๏ธ Invalid ID โ expected 4 characters - ID not found โ
โ ๏ธ No pending permission request with ID
Timeout
Requests not resolved within permissions.timeout seconds are auto-denied:
โฑ๏ธ Permission `a3k9` timed out โ auto-denied.
Concurrency
While a tool call is pending approval, new messages are queued and will not be processed
until the pending request is resolved. Only approve / deny commands unblock the queue.
Known limitation โ Bash sandbox
Claude Code's Bash tool sandbox restricts execution to the working directory.
approve resolves the HTTP hook, but Claude Code's sandbox enforcement is a separate layer
that cannot be overridden via hooks. Operations outside the working directory (e.g.
rm ~/foo) will still be blocked by Claude Code's Bash sandbox after approval.
Architecture
GatewayService
โโโ PermissionRegistry (shared in-process store: request_id โ asyncio.Future)
โโโ session_room_map (session_id โ room_id, for broker โ RC room routing)
โโโ ClaudePermissionBroker (HTTP server on random localhost port)
โ โโโ generates temp settings.json passed as --settings to every claude -p call
โ {"hooks": {"PreToolUse": [{"matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit",
โ "type": "http", "url": "http://127.0.0.1:<port>/hook"}]}}
โโโ OpenCodePermissionBroker (SSE listener on opencode's /events endpoint)
โ โโโ calls POST /permission/{id}/reply on owner decision
โโโ expiry_task (background: auto-deny requests older than timeout)
Agent Backends
ClaudeBackend (agents/claude/)
Drives the Claude CLI via subprocesses:
-
Session creation โ
claude -p --output-format json [new_session_args] [--settings <path>]Parsessession_idfrom the JSON response and stores it in state. -
Message sending โ
claude -p --resume <session-id> --output-format stream-json --verbose [--settings <path>]Streams one JSON event per line; extracts text fromtype="assistant"content blocks, metadata fromtype="result"(session_id, cost, duration, turns, token usage). -
Permission hook โ when
permissions.enabled,--settings <path>is injected into every call pointing to a gateway-generated settings file with the HTTPPreToolUsehook. -
Environment isolation โ strips
CLAUDECODEfrom the subprocess environment; mergesACG_ROLE/ACG_ALLOWED_TOOLSper-message for role enforcement. -
Attachments โ injects file paths into the prompt text (Claude CLI does not support native
-fattachments in-pmode).
OpenCodeBackend (agents/opencode/)
Drives the opencode CLI:
- Session creation โ
opencode run --format json [new_session_args] - Message sending โ
opencode run -s <session-id> --format json [-f <file>...] - Permission โ
permission.askedSSE events from opencode's HTTP server, triggered by therole-enforcement.tsplugin settingoutput.status = "ask". - Attachments โ native
-fflag support.
Agent Response (AgentResponse)
Every AgentBackend.send() call returns an AgentResponse:
@dataclass
class TokenUsage:
input_tokens: int = 0
output_tokens: int = 0
cache_read_tokens: int = 0
cache_write_tokens: int = 0
reasoning_tokens: int = 0 # opencode only
@dataclass
class AgentResponse:
text: str # agent's reply (always present)
session_id: str | None = None
usage: TokenUsage | None = None
cost_usd: float | None = None
duration_ms: int | None = None
num_turns: int | None = None
is_error: bool = False
def __str__(self) -> str:
return self.text # transparent use in f-strings
MessageProcessor logs token usage automatically when response.usage is present:
Agent usage [@alice] in=1234 out=256 cache_read=512 cost=$0.0042
Runtime files
All runtime state lives in ~/.agent-chat-gateway/:
| File | Contents |
|---|---|
gateway.pid |
PID of the running daemon |
gateway.log |
Daemon log output |
control.sock |
Unix domain socket for CLI commands |
state.<connector>.json |
Persisted watcher definitions per connector |
CLI usage
# Daemon lifecycle
agent-chat-gateway start [--config path/to/config.yaml]
agent-chat-gateway restart [--config path/to/config.yaml]
agent-chat-gateway status
agent-chat-gateway stop
# Watcher control (requires running daemon)
agent-chat-gateway list [--connector NAME]
agent-chat-gateway pause <watcher-name> [--connector NAME]
agent-chat-gateway resume <watcher-name> [--connector NAME]
agent-chat-gateway reset <watcher-name> [--connector NAME] # clear state, start fresh session
# Send messages directly to a room (bypasses agent)
agent-chat-gateway send <room> "message text"
agent-chat-gateway send <room> - # read from stdin
agent-chat-gateway send <room> --file message.txt
agent-chat-gateway send <room> --attach file.pdf --file caption.txt
# Setup
agent-chat-gateway onboard # interactive setup wizard
agent-chat-gateway upgrade # check and install updates
Adding an Agent Integration
1. Implement the AgentBackend ABC
from gateway.agents import AgentBackend
from gateway.agents.response import AgentResponse
class MyBackend(AgentBackend):
async def create_session(
self,
working_directory: str,
extra_args: list[str] | None = None,
session_title: str | None = None,
) -> str:
"""Start a new session. Return an opaque session_id string."""
...
async def send(
self,
session_id: str,
prompt: str,
working_directory: str,
timeout: int,
attachments: list[str] | None = None,
env: dict[str, str] | None = None,
) -> AgentResponse:
"""Send a message to an existing session. Raise asyncio.TimeoutError on timeout."""
...
2. Register in service.py
from .agents.mybackend.adapter import MyBackend
# inside _build_agent_backend():
if agent_cfg.type == "mybackend":
return MyBackend(command=agent_cfg.command, timeout=timeout)
3. Add to config.yaml
agents:
myagent:
type: mybackend
command: mybinary
new_session_args: []
4. Smoke-test with ScriptConnector
import asyncio
from gateway.connectors.script.connector import ScriptConnector
from gateway.core.session_manager import SessionManager
from gateway.core.config import CoreConfig
from gateway.agents.mybackend.adapter import MyBackend
connector = ScriptConnector()
backend = MyBackend(command="mybinary", timeout=60)
config = CoreConfig(timeout=60, agents={"default": backend}, default_agent="default")
manager = SessionManager(connector, backend, "default", config)
async def test():
await manager.run_once()
await manager.add_session("test-room", None, "/tmp")
await connector.inject("Hello")
reply = await connector.receive_reply()
print(reply)
asyncio.run(test())
Adding a new Connector
Implement the Connector ABC from gateway/core/connector.py:
from gateway.core.connector import Connector, IncomingMessage, Room
from gateway.agents.response import AgentResponse
class SlackConnector(Connector):
async def connect(self) -> None: ...
async def disconnect(self) -> None: ...
def register_handler(self, handler) -> None: ...
async def send_text(self, room_id: str, response: AgentResponse) -> None: ...
async def resolve_room(self, room_name: str) -> Room: ...
def format_prompt_prefix(self, msg: IncomingMessage) -> str:
return f"[Slack #{msg.room.name} | from: {msg.sender.username} | role: {msg.role.value}]"
Then instantiate it in service.py via connector_factory().
Direct Scripting with AgentSession
For one-off tasks and agent-to-agent pipelines, skip the full gateway stack:
from gateway.agents.session import AgentSession
from gateway.agents.claude.adapter import ClaudeBackend
from gateway.agents.opencode.adapter import OpenCodeBackend
# Single agent
async with AgentSession(ClaudeBackend("claude", [], 120), "/my/project") as session:
response = await session.send("What files are here?")
print(response) # __str__ โ response.text
# Agent-to-agent pipeline
async with (
AgentSession(OpenCodeBackend("opencode", [], 120), cwd) as oc,
AgentSession(ClaudeBackend("claude", ["--agent", "assistance"], 120), cwd) as cc,
):
summary = await oc.send("Summarize the codebase")
review = await cc.send(f"Review this summary:\n{summary}")
print(review)
Documentation
| Document | Description |
|---|---|
| docs/install-agent.md | Installation & getting started guide |
| docs/user-guide.md | Full configuration reference and operational guide |
| docs/architecture.md | System architecture, module breakdown, data flow diagrams |
| docs/permission-reference.md | RBAC and human-in-the-loop approval deep dive |
| docs/supported-features.md | Supported features, known limitations, and roadmap |
| docs/requirements.md | Functional specification (behavioral requirements) |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_chat_gateway-0.1.6.tar.gz.
File metadata
- Download URL: agent_chat_gateway-0.1.6.tar.gz
- Upload date:
- Size: 382.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ff46db34fe7da384fe85e4ceac8e3e730053e543a2549d86885f76a3cd41c27
|
|
| MD5 |
74b6e98507cfd6571902fe909433daad
|
|
| BLAKE2b-256 |
43bfb7b970627f8f1f7ba49a3ebce04f4f80ea901e09830d8d93c95e9fc4ab96
|
Provenance
The following attestation bundles were made for agent_chat_gateway-0.1.6.tar.gz:
Publisher:
publish.yml on HammerMei/agent-chat-gateway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_chat_gateway-0.1.6.tar.gz -
Subject digest:
6ff46db34fe7da384fe85e4ceac8e3e730053e543a2549d86885f76a3cd41c27 - Sigstore transparency entry: 1238622737
- Sigstore integration time:
-
Permalink:
HammerMei/agent-chat-gateway@3786105e992c0323b6da62ef569855442abd0f18 -
Branch / Tag:
refs/tags/v0.1.6 - Owner: https://github.com/HammerMei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3786105e992c0323b6da62ef569855442abd0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file agent_chat_gateway-0.1.6-py3-none-any.whl.
File metadata
- Download URL: agent_chat_gateway-0.1.6-py3-none-any.whl
- Upload date:
- Size: 193.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1374622966cc9d0aa37bf51136f8e42fc2d281d43392c31979b3960759792e06
|
|
| MD5 |
d8d1807a96ecc5d663659399191d0091
|
|
| BLAKE2b-256 |
318d225027b66121d48dec98500f82886e1a8cadc77bfc4223ef30883b48f424
|
Provenance
The following attestation bundles were made for agent_chat_gateway-0.1.6-py3-none-any.whl:
Publisher:
publish.yml on HammerMei/agent-chat-gateway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_chat_gateway-0.1.6-py3-none-any.whl -
Subject digest:
1374622966cc9d0aa37bf51136f8e42fc2d281d43392c31979b3960759792e06 - Sigstore transparency entry: 1238622738
- Sigstore integration time:
-
Permalink:
HammerMei/agent-chat-gateway@3786105e992c0323b6da62ef569855442abd0f18 -
Branch / Tag:
refs/tags/v0.1.6 - Owner: https://github.com/HammerMei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3786105e992c0323b6da62ef569855442abd0f18 -
Trigger Event:
push
-
Statement type: