← back

Kanipi v1.2.0

multi-channel agent gateway

TL;DR
Kanipi is a multi-tenant gateway that routes messages from Telegram, Discord, WhatsApp, Email, and Web to containerized Claude Code agents. Each agent runs in Docker with full SDK access (subagents, tools, skills, MCP). The gateway handles routing, queueing, IPC, media processing, and memory injection. Agents are configurations, not code. 45 source files, 10,788 LOC. 39 test files, 11,680 LOC. 81 specs.

Architecture

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:

ModulePurpose
index.tsmain loop, channel init, message routing
config.tstyped constants from .env + env vars
db.tsSQLite (better-sqlite3): messages, groups, sessions, tasks
container-runner.tsdocker lifecycle, stdin pipe, mount assembly
group-queue.tsper-group message queueing, concurrency control
router.tsJID resolution, prompt formatting, XML history
action-registry.tsunified action system (Zod schemas, authorization)
ipc.tscontainer-gateway IPC (request-response + legacy)
task-scheduler.tscron-based scheduled tasks
channels/telegram, whatsapp, discord, email, web adapters
actions/action handlers: messaging, tasks, groups, session, inject

Channels

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.

ChannelLibraryEnabled byJID prefixNotes
Telegramgrammy TELEGRAM_BOT_TOKENtg: long-poll, markdown-to-HTML, 4096-char chunking, typing indicator
Discorddiscord.js DISCORD_BOT_TOKENdiscord: gateway mode, @mention trigger, 2000-char split
WhatsAppbaileys store/auth/creds.json(native) QR pairing, read receipts, markdown conversion
Emailimapflow + nodemailer EMAIL_IMAP_HOSTemail: 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.

Agent Runtime

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).

Workspace mounts

MountPathAccess
self/workspace/selfkanipi source (ro, root only)
group/workspace/groupcurrent group workdir (rw, ro for workers)
share/workspace/shareworld-level shared state (rw root/world, ro rest)
web/workspace/webvite-served output (rw, root/world only)
ipc/workspace/ipcIPC + session state (rw)
.claude/home/node/.claudeCLAUDE.md, skills, memory (rw, ro for tier 2+)
extra/workspace/extra/...allowlisted additional mounts (ro)

Action registry

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).

4-tier permissions

TierFolderAccess
0 (root)mainfull rw, /workspace/self visible
1 (world)main/coderw group + share, rw web
2 (agent)main/code/pyrw 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 and identity

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.

Hot-patching

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.

Error handling

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.

Memory Layers

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.

LayerStorageScopeInjectionStatus
MessagesSQLiteper-group stdin XML history (recent N messages) shipped
SessionSDK JSONLper-container Claude Code native --resume shipped
ManagedCLAUDE.md + MEMORY.mdper-group Claude Code native read shipped
Diarydiary/*.mdper-group gateway reads 2 most recent, injects as <knowledge layer="diary"> XML on session start shipped
Factsfacts/*.mdper-world pull: agent searches via MCP tool planned
Episodesepisodes/*.mdper-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 and Routing

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).

Worlds

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).

Hierarchical delegation

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.

Trigger mode

First group registered defaults to folder=root with direct mode (all messages processed). Subsequent groups use trigger mode: agent responds only when mentioned (@name).

CLI

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

Media Processing

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.

Voice transcription

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.

File sending

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.

File commands

/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).

Capability introspection

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).

Web Interface

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.

Slink (web channel)

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.

Task Scheduling

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.

Products as Configurations

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.

Atlas

Code support agent. Mounted repos, workspace knowledge, support persona. Searches code, researches via subagents, answers architecture questions.

Channels: Telegram, Discord · shipped

Yonder

Research associate and knowledge mapper. Message from phone, agent researches topics, builds knowledge pages, maps connections. Vite serves results live.

Channels: Telegram, Web · shipped

Evangelist

Drafts public posts and engages in relevant conversations. V2 middleware intercepts post tool calls into a sign-off queue.

Channels: X/Twitter, Discord · planned

Quick Start

Prerequisites

Node.js 22+, Docker, bun. Anthropic credentials: CLAUDE_CODE_OAUTH_TOKEN (from claude login) or ANTHROPIC_API_KEY.

Docker deployment

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

Bare metal / development

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

Instance layout

/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 config

KeyPurpose
ASSISTANT_NAMEinstance name
TELEGRAM_BOT_TOKENenables telegram
DISCORD_BOT_TOKENenables discord
EMAIL_IMAP_HOSTenables email (IMAP IDLE)
CONTAINER_IMAGEagent docker image name
CLAUDE_CODE_OAUTH_TOKENpassed to agent containers
IDLE_TIMEOUTcontainer keepalive (ms, default 1800000)
MAX_CONCURRENT_CONTAINERSparallel agent limit (default 5)
MEDIA_ENABLEDattachment pipeline (default false)
VITE_PORTenables web serving
WHISPER_BASE_URLwhisper service URL
TIMEZONEcron timezone (validated, fallback UTC)

systemd deployment

sudo cp /srv/data/kanipi_foo/kanipi_foo.service /etc/systemd/system/
sudo systemctl enable --now kanipi_foo

Roadmap

MilestoneStatusKey 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)

Stats

MetricValue
Source files45 TypeScript
Source LOC10,788
Test files39 files · 11,680 LOC
Test:code ratio1.08:1
Spec files81 specification documents
Tests560
ChannelsTelegram, WhatsApp, Discord, Email, Web (Slink)
Config.env + env vars, SQLite-backed group state
Deploysystemd per instance, kanipi create CLI
BaseNanoClaw fork (upstream v1.1.3)
Runtime testedyes (v1.2.0)

Links

Explore