multi-channel agent gateway
TypeScript (ESM, NodeNext). Gateway polls channels for messages, stores them in SQLite, queues per group, spawns a Docker container per agent invocation. Agent reads messages from stdin as XML, uses Claude Code SDK, writes results to stdout as JSON. Gateway streams responses back to the originating channel.
Channel (tg/discord/wa/email/web)
| message
v
SQLite DB ---- store message + attachments
|
v
GroupQueue ---- per-group FIFO, one agent at a time
|
v
Router -------- JID -> group lookup, prompt assembly
| system messages, diary injection
v
Container ----- docker run, stdin pipe, mount workspace
| /workspace/{group,share,web,ipc}
v
Claude Code --- SDK: tools, subagents, skills, MCP
|
v
IPC (SIGUSR1) - file-based request/response
| agent -> gateway actions
v
Action Registry Zod-validated, tier-authorized
|
v
Channel ------- send text, files, typing indicators
Key modules:
| Module | Purpose |
|---|---|
index.ts | main loop, channel init, message routing |
config.ts | typed constants from .env + env vars |
db.ts | SQLite (better-sqlite3): messages, groups, sessions, tasks |
container-runner.ts | docker lifecycle, stdin pipe, mount assembly |
group-queue.ts | per-group message queueing, concurrency control |
router.ts | JID resolution, prompt formatting, XML history |
action-registry.ts | unified action system (Zod schemas, authorization) |
ipc.ts | container-gateway IPC (request-response + legacy) |
task-scheduler.ts | cron-based scheduled tasks |
channels/ | telegram, whatsapp, discord, email, web adapters |
actions/ | action handlers: messaging, tasks, groups, session, inject |
Five channel adapters implementing a shared interface (Channel in types.ts):
connect(), sendMessage(), sendDocument?(), ownsJid().
Each channel is enabled by config presence — no token, no channel. JIDs use URI-like prefixes
for cross-channel routing.
| Channel | Library | Enabled by | JID prefix | Notes |
|---|---|---|---|---|
| Telegram | grammy | TELEGRAM_BOT_TOKEN | tg: |
long-poll, markdown-to-HTML, 4096-char chunking, typing indicator |
| Discord | discord.js | DISCORD_BOT_TOKEN | discord: |
gateway mode, @mention trigger, 2000-char split |
| baileys | store/auth/creds.json | (native) | QR pairing, read receipts, markdown conversion | |
| imapflow + nodemailer | EMAIL_IMAP_HOST | email: |
IMAP IDLE real-time, SMTP reply threading via email_threads table |
|
| Web (Slink) | built-in | always (HTTP POST) | web: |
HMAC JWT auth, rate limiting, SSE streaming, sloth.js widget |
All channels support /chatid and /ping commands.
Forward metadata (origin, reply-to) extracted per channel and rendered as nested XML in agent prompt.
Per-channel output styles (formatting conventions) activated via SDK settings.
Each agent runs Claude Code inside a Docker container. The gateway mounts a structured workspace,
pipes messages via stdin, and reads JSON results from stdout. IPC is signal-driven: gateway writes
a file then sends SIGUSR1; agent wakes immediately (500ms fallback poll).
MAX_CONCURRENT_CONTAINERS limits total parallel agents (default 5).
IDLE_TIMEOUT controls container keepalive between messages (default 30min).
| Mount | Path | Access |
|---|---|---|
self | /workspace/self | kanipi source (ro, root only) |
group | /workspace/group | current group workdir (rw, ro for workers) |
share | /workspace/share | world-level shared state (rw root/world, ro rest) |
web | /workspace/web | vite-served output (rw, root/world only) |
ipc | /workspace/ipc | IPC + session state (rw) |
.claude | /home/node/.claude | CLAUDE.md, skills, memory (rw, ro for tier 2+) |
extra | /workspace/extra/... | allowlisted additional mounts (ro) |
Agents request actions via IPC files. Gateway validates with Zod schemas and dispatches.
Domain handlers: messaging (send_message, send_document, inject_message),
tasks (schedule_task, cancel_task),
groups (delegate_group, set_routing_rules),
session (reset_session).
Authorization enforced per action — agents can only act on their own group.
inject_message inserts into DB without channel delivery (enables retry after OOM kills).
| Tier | Folder | Access |
|---|---|---|
| 0 (root) | main | full rw, /workspace/self visible |
| 1 (world) | main/code | rw group + share, rw web |
| 2 (agent) | main/code/py | rw workdir, ro setup files |
| 3 (worker) | (deeper) | all ro |
Tier derived from folder depth. Mount isolation varies: root/world get full rw; agents get rw workdir with ro CLAUDE.md/skills; workers get everything ro.
Skills seeded from container/skills/ to ~/.claude/skills/ on first spawn per group.
SOUL.md defines agent personality per group. CLAUDE.md defines behavior and instructions.
Migration system: MIGRATION_VERSION + numbered files in migrations/;
/migrate skill syncs groups when version advances.
Agent-runner TypeScript source is mounted into containers and recompiled with bunx tsc at runtime.
Changes to agent behavior deploy without rebuilding the container image.
Circuit breaker: 3 consecutive failures per group opens the breaker. New user message resets. Agent errors advance the cursor, notify the user, and evict the session. No exponential backoff loops on permanent failures.
Six memory layers with different persistence, scope, and injection mechanisms. Push layers are injected by the gateway. Pull layers are searched by the agent. The underlying pattern: markdown files in a directory, summaries selected and injected as XML.
| Layer | Storage | Scope | Injection | Status |
|---|---|---|---|---|
| Messages | SQLite | per-group | stdin XML history (recent N messages) | shipped |
| Session | SDK JSONL | per-container | Claude Code native --resume |
shipped |
| Managed | CLAUDE.md + MEMORY.md | per-group | Claude Code native read | shipped |
| Diary | diary/*.md | per-group | gateway reads 2 most recent, injects as <knowledge layer="diary"> XML on session start |
shipped |
| Facts | facts/*.md | per-world | pull: agent searches via MCP tool | planned |
| Episodes | episodes/*.md | per-group | scheduled aggregation of diary into weekly/monthly | planned |
Diary uses YAML frontmatter summaries. Age labels: "today", "yesterday", "3 days ago".
PreCompact hook nudges agent to write diary entries before context compression.
System messages (new-session, new-day) injected as XML; last 2 previous sessions included as
<previous_session> elements.
Groups are the organizational unit. Each group maps to a folder, a channel JID, and an agent configuration. Groups are registered via CLI. Unregistered chats are silently dropped. Multiple JIDs can share one folder (multi-channel groups).
World = first folder segment. worldOf('atlas/support') === 'atlas'.
Authorization is world-scoped: cross-world actions are denied.
/workspace/share is mounted per-world (rw for root/world, ro for deeper groups).
Parent groups delegate to children via routing rules. Five rule types: command (prefix match), pattern (regex, max 200 chars), keyword (case-insensitive), sender (regex), default (fallback). Rules evaluated in tier order; first match wins. Delegation is parent-to-child only, same world, max depth 3.
First group registered defaults to folder=root with direct mode (all messages processed).
Subsequent groups use trigger mode: agent responds only when mentioned (@name).
kanipi config <instance> group list # registered + discovered kanipi config <instance> group add <jid> [folder] # register group kanipi config <instance> group rm <jid> # unregister kanipi config <instance> user add|rm|list|passwd # manage web auth kanipi config <instance> mount add|rm|list # manage container mounts
Attachment pipeline: download, detect MIME (magic bytes via file-type), enrich, deliver.
Handlers run in parallel per attachment type. Configurable per instance:
MEDIA_ENABLED, VOICE_TRANSCRIPTION_ENABLED, VIDEO_TRANSCRIPTION_ENABLED.
Whisper service (kanipi-whisper Docker image) transcribes voice messages before agent delivery.
Per-group language hints via .whisper-language file (one BCP-47 code per line).
Parallel passes: auto-detect + each configured language. Output labeled
[voice/auto→en] or [voice/cs].
Agent-writable .whisper-language configures transcription.
Agents send files via send_document IPC action. Path-safe: must be under group dir.
Telegram routes photos/videos/audio to native methods (inline display). WhatsApp routes by MIME.
Discord uses AttachmentBuilder.
/put, /get, /ls for bidirectional file transfer between chat users and group workspace.
Deny globs, symlink escape protection, atomic writes. Disabled by default (FILE_TRANSFER_ENABLED).
Gateway writes .gateway-caps TOML manifest to group dir before each spawn.
Agent reads it to answer capability questions accurately (voice, video, media limits, web host).
Vite dev server managed by the entrypoint (not the TS gateway). Serves from web/ directory.
/pub/ prefix = public (no auth). /priv/ = requires auth.
Auth: argon2-hashed local accounts + JWT sessions. SLOTH_USERS for quick setup.
POST /pub/s/<token> accepts messages via HTTP.
Optional Authorization: Bearer <jwt> for higher rate limits
(SLINK_AUTH_RPM default 60/min vs SLINK_ANON_RPM 10/min).
SSE streaming at /_sloth/stream for agent-to-browser push.
sloth.js widget for embedding in web pages.
SQLite-backed scheduler with three modes: cron, interval, once.
Agents request scheduled tasks via schedule_task IPC action.
Two context modes: group (shares conversation history) or isolated (fresh context per run).
Run log with per-task status and last result. TIMEZONE validated via Intl.DateTimeFormat.
A product is a group configured for a specific role. Same gateway, different CLAUDE.md (behavior) + SOUL.md (persona) + skills (capabilities) + mounts (data) + tasks. The gateway runs groups, not products.
Code support agent. Mounted repos, workspace knowledge, support persona. Searches code, researches via subagents, answers architecture questions.
Channels: Telegram, Discord · shipped
Research associate and knowledge mapper. Message from phone, agent researches topics, builds knowledge pages, maps connections. Vite serves results live.
Channels: Telegram, Web · shipped
Drafts public posts and engages in relevant conversations. V2 middleware intercepts post tool calls into a sign-off queue.
Channels: X/Twitter, Discord · planned
Node.js 22+, Docker, bun. Anthropic credentials:
CLAUDE_CODE_OAUTH_TOKEN (from claude login) or ANTHROPIC_API_KEY.
make image # gateway image make -C container image # agent image ./kanipi create foo # seed /srv/data/kanipi_foo/ edit /srv/data/kanipi_foo/.env # set tokens ./kanipi config foo group add tg:-123456789 # register main group ./kanipi foo # start
npm install && make build npx tsx src/cli.ts create foo edit /srv/data/kanipi_foo/.env make -C container image # agent container still needs docker npx tsx src/cli.ts config foo group add tg:-123456789 npm run dev
/srv/data/kanipi_foo/
.env # config (tokens, ports, flags)
store/ # SQLite DB, whatsapp auth
groups/main/ # root group workdir
logs/ # conversation logs
diary/ # agent daily notes
CLAUDE.md # agent behavior
SOUL.md # agent personality
data/sessions/ # per-session state
data/ipc/ # agent IPC files
web/pub/ # public web (no auth)
web/priv/ # private web (auth required)
| Key | Purpose |
|---|---|
ASSISTANT_NAME | instance name |
TELEGRAM_BOT_TOKEN | enables telegram |
DISCORD_BOT_TOKEN | enables discord |
EMAIL_IMAP_HOST | enables email (IMAP IDLE) |
CONTAINER_IMAGE | agent docker image name |
CLAUDE_CODE_OAUTH_TOKEN | passed to agent containers |
IDLE_TIMEOUT | container keepalive (ms, default 1800000) |
MAX_CONCURRENT_CONTAINERS | parallel agent limit (default 5) |
MEDIA_ENABLED | attachment pipeline (default false) |
VITE_PORT | enables web serving |
WHISPER_BASE_URL | whisper service URL |
TIMEZONE | cron timezone (validated, fallback UTC) |
sudo cp /srv/data/kanipi_foo/kanipi_foo.service /etc/systemd/system/ sudo systemctl enable --now kanipi_foo
| Milestone | Status | Key items |
|---|---|---|
| v1 | shipped | 5 channels, routing, actions, diary, auth, 560 tests |
| v1m1 | next | facts injection, SSE per-sender, session recovery, /work skill |
| v1m2 | planned | message MCP tools, agent messaging, identity linking, message WAL |
| v2m1 | planned | episodic memory, agent teams, IPC-to-MCP proxy, feed adapters (twitter, reddit, facebook) |
| v2m2 | planned | Go gateway rewrite (single binary, native concurrency, same interfaces) |
| Metric | Value |
|---|---|
| Source files | 45 TypeScript |
| Source LOC | 10,788 |
| Test files | 39 files · 11,680 LOC |
| Test:code ratio | 1.08:1 |
| Spec files | 81 specification documents |
| Tests | 560 |
| Channels | Telegram, WhatsApp, Discord, Email, Web (Slink) |
| Config | .env + env vars, SQLite-backed group state |
| Deploy | systemd per instance, kanipi create CLI |
| Base | NanoClaw fork (upstream v1.1.3) |
| Runtime tested | yes (v1.2.0) |