https://github.com/aiwhiteteam/harnessgate
Connect Claude Managed Agents or any harness runtime to any messaging platform.
https://github.com/aiwhiteteam/harnessgate
ai-agent claude discord-bot gateway managed-agents multi-channel slack-bot telegram-bot typescript
Last synced: 21 days ago
JSON representation
Connect Claude Managed Agents or any harness runtime to any messaging platform.
- Host: GitHub
- URL: https://github.com/aiwhiteteam/harnessgate
- Owner: aiwhiteteam
- License: mit
- Created: 2026-04-12T03:18:38.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-24T17:25:52.000Z (about 1 month ago)
- Last Synced: 2026-04-24T18:28:51.917Z (about 1 month ago)
- Topics: ai-agent, claude, discord-bot, gateway, managed-agents, multi-channel, slack-bot, telegram-bot, typescript
- Language: TypeScript
- Homepage:
- Size: 196 KB
- Stars: 5
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
HarnessGate
Connect any messaging platform to Claude Managed Agents.
---
## Why HarnessGate and Claude Managed Agents?
Standard agent runtimes like the Anthropic ecosystem, E2B, and the OpenAI Agents SDK give you a secure, production-grade harness out of the box. Existing chatbot frameworks (OpenClaw, Botpress) ship their **own agent loop** instead — so to plug into one of these runtimes, you end up **bypassing the framework entirely** just to pipe messages through.
HarnessGate takes a different approach: **no local agent loop.** It's a pure bridge that connects social media platforms to a provider runtime such as Claude Managed Agents or E2B. The gateway just routes messages between platforms and the agent.
This means:
- Claude Managed Agents features work out of the box (tool confirmation, custom tools, multi-agent threads, extended thinking)
- Any future agent runtime plugs in with 4 methods
- No competing agent loops, no bypassed infrastructure, no wasted abstractions
```
[Telegram] [Discord] [Slack] [WhatsApp] [Teams] [Web UI]
| | | | | |
+----+----+----+---+-------+----------+------+
| |
PlatformAdapter interface (per platform)
| |
+----+----+
|
Bridge (orchestrator)
SessionMap + StreamManager
|
Provider interface
|
+----------+----------+----------+
| Claude | HTTP | Custom |
| Managed | (any | (npm pkg |
| Agents | server) | or file)|
+----------+----------+----------+
```
## Features
- **Provider-agnostic** — Claude Managed Agents, any HTTP server, or bring your own
- **Platform adapters** — Telegram, Discord, Slack, WhatsApp, Teams, Web UI
- **Multi-app** — run multiple app instances per platform, each mapped to a different agent
- **Session management** — automatic session creation, SQLite persistence, multi-turn conversations
- **Buffer-then-send** — accumulates agent responses, sends as one message per turn
- **Auto-split** — respects per-platform message length limits
- **Event passthrough** — provider-specific events forwarded via `bridge.onEvent()` listeners
## Quick Start
```bash
npm install harnessgate
```
```typescript
import { Bridge, ClaudeProvider, TelegramAdapter } from "harnessgate";
const provider = new ClaudeProvider(process.env.ANTHROPIC_API_KEY!);
const bridge = new Bridge(provider, {
provider: { type: "claude" },
platforms: { telegram: { botToken: process.env.TELEGRAM_BOT_TOKEN! } },
});
// Route users to agents (agentId + environmentId from your DB)
bridge.setUserResolver(async (sender) => ({
userId: sender.id,
agentId: "agent_01XXXX",
environmentId: "env_01XXXX",
}));
bridge.addPlatform(new TelegramAdapter());
await bridge.start();
```
See [`examples/demo-web/`](examples/demo-web/) for a minimal starter or [`examples/demo-telegram/`](examples/demo-telegram/) for a Telegram bot.
### Run an example from the repo
```bash
git clone https://github.com/aiwhiteteam/harnessgate.git
cd harnessgate
pnpm install
cd examples/demo-telegram
cp .env.example .env
# Add ANTHROPIC_API_KEY in .env
# Fill in botToken in src/main.ts
# Implement your agentId/environmentId lookup in the user resolver
pnpm build
node --env-file=.env dist/main.js
```
## Multi-Bot / appId
Every platform adapter supports running multiple app instances simultaneously. Each app connects to the platform and receives a platform-assigned `appId` — an opaque identifier that flows through every `InboundMessage` and `ChannelTarget`.
```typescript
// Add multiple Telegram bots at runtime
const supportBotId = await bridge.connect("telegram", { botToken: process.env.SUPPORT_BOT_TOKEN });
const salesBotId = await bridge.connect("telegram", { botToken: process.env.SALES_BOT_TOKEN });
// Route based on which bot received the message
bridge.setUserResolver(async (sender, platform, message) => {
const agentId = await db.getAgentForBot(message.appId);
const environmentId = await db.getEnvironmentForBot(message.appId);
return { userId: sender.id, agentId, environmentId };
});
```
### appId per platform
Each platform exposes a different identifier as `appId`. The adapter reads it from the platform SDK after connecting:
| Platform | Source | Example value |
|----------|--------|---------------|
| Telegram | `bot.botInfo.id` | `"123456789"` |
| Discord | `client.application.id` | `"1098765432101234567"` |
| Slack | `event.api_app_id` | `"A0123456789"` |
| WhatsApp | WABA phone number ID | `"106540352267890"` |
| Teams | `activity.recipient.id` | `"28:abc123..."` |
| Web | N/A (single instance) | — |
The `appId` is included in session keys as `app:`, so each bot maintains separate conversation sessions even in the same channel.
## Session Management
Each conversation context gets its own Claude session:
| Context | Session scope | Example key |
|---------|--------------|-------------|
| DM | Per user | `telegram:direct:123:u:user99` |
| Group/Channel | Shared (all users) | `slack:group:ch1` |
| Thread | Per thread | `discord:thread:ch1:t:thread99` |
Sessions persist to SQLite when you wire one in (survives restarts), otherwise the bridge falls back to in-memory storage:
```typescript
import { SqliteSessionStore } from "harnessgate";
bridge.setSessionStore(new SqliteSessionStore("./harnessgate.db"));
```
For custom stores (Supabase, Postgres, Redis), implement the `SessionStore` interface directly:
```typescript
bridge.setSessionStore({
async get(key) { /* query your DB */ },
async set(key, entry) { /* upsert */ },
async delete(key) { /* delete */ },
async touch(key) { /* update lastActiveAt */ },
});
```
See [`examples/with-supabase/main.ts`](examples/with-supabase/main.ts) for a complete example with Supabase for both auth and session persistence.
## Provider Setup
HarnessGate supports three ways to connect an agent runtime:
### 1. Claude Managed Agents
```bash
# .env
ANTHROPIC_API_KEY=sk-ant-...
```
```typescript
import { ClaudeProvider } from "harnessgate";
const provider = new ClaudeProvider(process.env.ANTHROPIC_API_KEY!);
```
Connects to [Claude Managed Agents](https://docs.anthropic.com/en/docs/managed-agents/overview). Full support for streaming, tool confirmation, custom tools, extended thinking, and multi-agent threads.
For Claude, `agentId` and `environmentId` come from your `UserResolver`, not static provider config:
```typescript
bridge.setUserResolver(async (sender) => ({
userId: sender.id,
agentId: "agent_01XXXX",
environmentId: "env_01XXXX",
}));
```
### 2. HTTP — any server
```bash
# .env
MY_TOKEN=...
```
```typescript
import { HttpProvider } from "harnessgate";
const provider = new HttpProvider({
baseUrl: "http://localhost:8080",
headers: { Authorization: `Bearer ${process.env.MY_TOKEN}` },
});
```
Connects to **any** HTTP server that implements these endpoints:
| Endpoint | Request | Response |
|----------|---------|----------|
| `POST /sessions` | `{ systemPrompt? }` | `{ id: "session-123" }` |
| `POST /sessions/{id}/message` | `{ message: "hello", sessionId: "..." }` | `200 OK` |
| `GET /sessions/{id}/stream` | SSE stream | `data: {"type": "message", "text": "..."}` |
| `DELETE /sessions/{id}` | — | `200 OK` |
Your server can be written in any language. SSE events can use either format:
```
# HarnessGate-native format (recommended)
data: {"type": "message", "text": "Hello!"}
data: {"type": "status", "status": "idle"}
# Simple format (auto-detected)
data: {"response": "Hello!"}
data: {"text": "Hello!"}
```
Custom endpoint paths:
```typescript
const provider = new HttpProvider({
baseUrl: "http://localhost:8080",
endpoints: {
createSession: "POST /api/conversations",
sendMessage: "POST /api/conversations/{sessionId}/chat",
stream: "GET /api/conversations/{sessionId}/events",
destroySession: "DELETE /api/conversations/{sessionId}",
},
});
```
### 3. Custom provider — npm package or local file
```bash
# .env
MY_PROVIDER_API_KEY=...
```
```typescript
// npm package
import MyProvider from "@my-org/my-langgraph-provider";
// or local file
import MyProvider from "./my-provider.js";
const provider = new MyProvider({ apiKey: process.env.MY_PROVIDER_API_KEY! });
```
The package/file must default-export a class implementing the `Provider` interface:
```typescript
import type { Provider } from "harnessgate";
export default class MyProvider implements Provider {
readonly id = "my-provider";
readonly capabilities = {
interrupt: false,
toolConfirmation: false,
customTools: false,
thinking: false,
};
constructor(config: Record) {
// config contains whatever you pass to `new MyProvider({ ... })`
}
async createSession(opts) { /* ... */ }
async sendMessage(sessionId, message) { /* ... */ }
async *stream(sessionId, signal) { /* ... */ }
async destroySession(sessionId) { /* ... */ }
}
```
## User Auth
HarnessGate supports per-user access control and agent routing via a `UserResolver`:
```typescript
bridge.setUserResolver(async (sender, platform, message) => {
const user = await db.findUser(platform, sender.id);
if (!user?.isActive) return null; // reject
return {
userId: user.id,
agentId: user.agentId, // which agent template
environmentId: user.envId, // which environment
metadata: { plan: user.plan }, // passed to provider session
};
});
```
Return `null` to reject. When no resolver is set, all users are allowed with their platform ID as the user ID.
Claude requires the resolver to return both `agentId` and `environmentId` for session creation.
## Platform Configuration
Pass per-platform config to the `Bridge` constructor under `platforms`, keyed by platform id. Each entry is forwarded to that adapter's `start()`. Add an adapter via `bridge.addPlatform(...)` for each platform you want to enable.
```bash
# .env
TELEGRAM_BOT_TOKEN=...
DISCORD_BOT_TOKEN=...
SLACK_BOT_TOKEN=...
SLACK_APP_TOKEN=...
WHATSAPP_PHONE_NUMBER_ID=...
WHATSAPP_ACCESS_TOKEN=...
WHATSAPP_VERIFY_TOKEN=...
TEAMS_APP_ID=...
TEAMS_APP_PASSWORD=...
```
```typescript
import {
Bridge,
TelegramAdapter,
DiscordAdapter,
SlackAdapter,
WhatsAppAdapter,
TeamsAdapter,
WebAdapter,
} from "harnessgate";
const bridge = new Bridge(provider, {
provider: { type: "claude" },
platforms: {
web: { port: 3000 },
telegram: { botToken: process.env.TELEGRAM_BOT_TOKEN! },
discord: { token: process.env.DISCORD_BOT_TOKEN! },
slack: {
botToken: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
},
whatsapp: {
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
port: 8080,
},
teams: {
appId: process.env.TEAMS_APP_ID!,
appPassword: process.env.TEAMS_APP_PASSWORD!,
port: 3978,
},
},
});
// Only adapters you addPlatform() are started — omit any you don't need.
bridge.addPlatform(new WebAdapter());
bridge.addPlatform(new TelegramAdapter());
// bridge.addPlatform(new DiscordAdapter());
// bridge.addPlatform(new SlackAdapter());
// bridge.addPlatform(new WhatsAppAdapter());
// bridge.addPlatform(new TeamsAdapter());
await bridge.start();
```
Load `.env` via `node --env-file=.env dist/main.js` or any `dotenv`-style loader.
## Platform Setup Guides
Detailed setup instructions for each platform, including SaaS multi-tenant distribution:
| Platform | Guide | Library |
|----------|-------|---------|
| Telegram | [`docs/setup-telegram.md`](docs/setup-telegram.md) | grammY |
| Discord | [`docs/setup-discord.md`](docs/setup-discord.md) | discord.js |
| Slack | [`docs/setup-slack.md`](docs/setup-slack.md) | @slack/bolt |
| WhatsApp | [`docs/setup-whatsapp.md`](docs/setup-whatsapp.md) | Cloud API (fetch) |
| Teams | [`docs/setup-teams.md`](docs/setup-teams.md) | botbuilder |
| Web | [`docs/setup-web.md`](docs/setup-web.md) | Built-in HTTP |
## Project Structure
```
harnessgate/
├── src/
│ ├── index.ts # Barrel exports
│ ├── bridge.ts # Orchestrator
│ ├── session-map.ts # Session persistence
│ ├── stream-manager.ts # SSE stream lifecycle
│ ├── platforms/
│ │ ├── telegram-adapter.ts # grammY
│ │ ├── discord-adapter.ts # discord.js
│ │ ├── slack-adapter.ts # @slack/bolt
│ │ ├── whatsapp-adapter.ts # Cloud API (fetch)
│ │ ├── teams-adapter.ts # Bot Framework SDK
│ │ └── web-adapter.ts # Built-in HTTP + SSE
│ └── providers/
│ ├── claude-provider.ts # Claude Managed Agents
│ └── http-provider.ts # Generic HTTP
├── docs/ # Platform setup guides
└── examples/ # Starter projects
```
## Extending HarnessGate
See [CONTRIBUTING.md](CONTRIBUTING.md) for step-by-step guides on adding platforms and providers.
### Provider interface
```typescript
interface Provider {
readonly id: string;
readonly capabilities: ProviderCapabilities;
// Required — every provider
createSession(opts: CreateSessionOpts): Promise;
sendMessage(sessionId: string, message: MessagePayload): Promise;
stream(sessionId: string, signal: AbortSignal): AsyncIterable;
destroySession(sessionId: string): Promise;
// Optional — capability-gated
interrupt?(sessionId: string): Promise;
confirmTool?(sessionId: string, toolUseId: string, approved: boolean): Promise;
submitToolResult?(sessionId: string, toolUseId: string, result: unknown): Promise;
}
```
### Platform interface
```typescript
interface PlatformAdapter {
readonly id: string;
readonly capabilities: PlatformCapabilities;
start(ctx: PlatformContext): Promise;
stop(): Promise;
send(target: ChannelTarget, message: OutboundMessage): Promise;
sendTyping?(target: ChannelTarget): Promise;
connect?(credentials: Record, ctx: PlatformContext): Promise;
disconnect?(appId: string): Promise;
activeConnections?(): string[];
}
```
## Requirements
- Node.js >= 22
- pnpm
## License
MIT