Keybase Channel Plugin — E2E encrypted team chat for cobot #87

Open
opened 2026-02-23 17:34:06 +00:00 by nazim · 0 comments
Contributor

Summary

Add a Keybase channel plugin so cobot can send/receive messages in Keybase team channels, using the same extension point architecture as the Telegram plugin.

Architecture Analysis

Cobot's communication stack has three layers of extension points:

communication plugin (layer 1)
  defines: communication.receive, communication.send, communication.typing
      ↕
session plugin (layer 2) 
  implements: communication.*
  defines: session.receive, session.send, session.typing, session.presence
      ↕
channel plugins (layer 3: telegram, nostr, keybase...)
  implements: session.*

The session plugin polls all channels via session.receive, aggregates messages, and routes outgoing messages by channel_type. The loop plugin polls communication, feeds messages to the LLM, and sends responses back.

A new channel plugin just needs to:

  1. Implement session.receive → return list[IncomingMessage]
  2. Implement session.send → accept OutgoingMessage, return bool
  3. Implement session.typing → send typing indicator
  4. Optionally implement session.presence → set online/offline status

The session plugin auto-discovers all implementations and routes by channel_type. Zero changes needed to session, communication, or loop plugins.

How Keybase Chat API Works

Keybase exposes a JSON API via the keybase CLI binary:

Send a message:

keybase chat api -m '{
  "method": "send",
  "params": {"options": {
    "channel": {"name": "teamname", "members_type": "team", "topic_name": "general"},
    "message": {"body": "Hello from cobot!"}
  }}
}'

Listen for messages (streaming):

keybase chat api-listen --filter-channel '{"name": "teamname", "members_type": "team"}'
# Outputs one JSON object per line for each incoming message

Read recent messages:

keybase chat api -m '{
  "method": "read",
  "params": {"options": {
    "channel": {"name": "teamname", "members_type": "team", "topic_name": "general"}
  }}
}'

Auth (headless):

keybase oneshot --username botname --paperkey "word1 word2 ... wordN"

No Python dependencies needed — just subprocess calls to keybase chat api.

Implementation Plan

File structure

cobot/plugins/keybase/
├── __init__.py
├── plugin.py          # KeybasePlugin
└── tests/
    └── test_keybase.py

Plugin skeleton

class KeybasePlugin(Plugin):
    meta = PluginMeta(
        id="keybase",
        version="0.1.0",
        capabilities=["communication"],
        dependencies=["session"],
        priority=30,  # Same level as telegram
        extension_points=[
            "keybase.on_message",  # Keybase-specific hooks
        ],
        implements={
            "session.receive": "poll_updates",
            "session.send": "send_message",
            "session.typing": "send_typing",
        },
    )

Config

keybase:
  username: cobotbot
  paperkey_env: KEYBASE_PAPERKEY   # env var name (never plaintext in config)
  team: cobotdev
  channels:                         # which channels to listen to
    - general
    - dev
  default_channel: general          # for broadcasts
  poll_mode: listen                  # "listen" (streaming) or "read" (polling)

Phase 1: Send messages (~0.5 day)

  • start() → run keybase oneshot to authenticate
  • send_message(OutgoingMessage)keybase chat api -m '{"method": "send", ...}'
  • get_default_channel_id() → returns configured default channel
  • Test: cobot posts to #general on startup

Phase 2: Receive messages (~1 day)

Two strategies (config: poll_mode):

A) listen mode (recommended):

  • start() spawns keybase chat api-listen as async subprocess
  • Background task reads stdout line by line, parses JSON, queues IncomingMessage
  • poll_updates() drains the queue and returns messages
  • Pro: instant delivery, no polling delay
  • Con: long-running subprocess to manage

B) read mode (simpler fallback):

  • poll_updates() calls keybase chat api -m '{"method": "read", ...}'
  • Tracks last seen message ID, returns only new ones
  • Pro: stateless, no subprocess
  • Con: polling delay (depends on loop interval)

Both return list[IncomingMessage] with:

  • channel_type = "keybase"
  • channel_id = "teamname#channelname"
  • sender_id = keybase_username
  • sender_name = keybase_username

Phase 3: Session spawning (~1 day)

This is the interesting part. When a message arrives from Keybase, it flows through the same pipeline as Telegram:

keybase.poll_updates() → IncomingMessage(channel_type="keybase")
  → session.poll_all_channels() aggregates
    → loop.run() picks up message
      → loop._handle_message()
        → LLM processes, generates response
          → OutgoingMessage(channel_type="keybase")
            → session.send() routes to keybase.send_message()

For multi-channel awareness, the loop needs to know which channel a message came from. This already works — IncomingMessage.channel_type and channel_id flow through the entire pipeline. The LLM sees the channel context and responds back to the same channel.

For spawning sub-sessions (e.g. a Keybase thread becomes its own conversation context):

  • The subagent plugin already handles isolated sessions
  • A Keybase DM or thread could trigger subagent.spawn() with channel_type="keybase" + channel_id="user:alice" as the reply channel
  • This follows the same pattern as OpenClaw's session spawning — the sub-session gets a dedicated conversation history and replies back to the originating channel

Key decision: Should each Keybase channel get its own conversation history (separate sessions), or share one? Recommendation: separate session per channel_id#dev discussions shouldn't bleed into #general context.

This might need a small enhancement to the loop/session layer: a session registry that maps (channel_type, channel_id) → conversation history. Currently the loop has one shared history. This is the same problem that would arise with any multi-channel setup (Telegram groups + Keybase + DMs).

Phase 4: Encrypted KV store + KBFS (~1 day)

Bonus capabilities unique to Keybase:

  • keybase kvstore api → encrypted team-scoped key-value store (agent state, shared secrets)
  • /keybase/team/cobotdev/ → encrypted filesystem (shared files, private plugins)
  • Expose as tools: kv_get, kv_put, kbfs_read, kbfs_write

Dependencies

  • keybase CLI binary installed on the host
  • A Keybase account + paperkey for the bot
  • A Keybase team to join
  • No Python library dependencies (subprocess only)

Effort

Phase Effort Delivers
Phase 1: Send 0.5 day Post to Keybase channels
Phase 2: Receive 1 day Bidirectional messaging
Phase 3: Sessions 1 day Per-channel conversation context
Phase 4: KV + KBFS 1 day Encrypted storage + files
Total 3.5 days Full Keybase integration

Open Questions

  1. Session-per-channel vs shared session — the current loop has one conversation history. Multi-channel support (Keybase, Telegram, DMs) needs a session registry. Should this be part of this ticket or a separate "multi-session support" epic?

  2. Bot account — create a new Keybase user (e.g. cobotai) or run as an existing user?

  3. Team namecobotdev? cobot? ultanio?

References

  • Keybase bot docs
  • pykeybasebot — official Python lib (reference only, we use subprocess)
  • Telegram plugin: cobot/plugins/telegram/plugin.py — direct template to follow
  • Session plugin: cobot/plugins/session/plugin.py — routing layer
  • #86 — MCP client support (another tool integration pattern)
  • #85 — code-index-mcp
## Summary Add a Keybase channel plugin so cobot can send/receive messages in Keybase team channels, using the same extension point architecture as the Telegram plugin. ## Architecture Analysis Cobot's communication stack has **three layers** of extension points: ``` communication plugin (layer 1) defines: communication.receive, communication.send, communication.typing ↕ session plugin (layer 2) implements: communication.* defines: session.receive, session.send, session.typing, session.presence ↕ channel plugins (layer 3: telegram, nostr, keybase...) implements: session.* ``` The **session plugin** polls all channels via `session.receive`, aggregates messages, and routes outgoing messages by `channel_type`. The **loop plugin** polls communication, feeds messages to the LLM, and sends responses back. A new channel plugin just needs to: 1. Implement `session.receive` → return `list[IncomingMessage]` 2. Implement `session.send` → accept `OutgoingMessage`, return `bool` 3. Implement `session.typing` → send typing indicator 4. Optionally implement `session.presence` → set online/offline status The session plugin auto-discovers all implementations and routes by `channel_type`. **Zero changes needed to session, communication, or loop plugins.** ## How Keybase Chat API Works Keybase exposes a JSON API via the `keybase` CLI binary: **Send a message:** ```bash keybase chat api -m '{ "method": "send", "params": {"options": { "channel": {"name": "teamname", "members_type": "team", "topic_name": "general"}, "message": {"body": "Hello from cobot!"} }} }' ``` **Listen for messages (streaming):** ```bash keybase chat api-listen --filter-channel '{"name": "teamname", "members_type": "team"}' # Outputs one JSON object per line for each incoming message ``` **Read recent messages:** ```bash keybase chat api -m '{ "method": "read", "params": {"options": { "channel": {"name": "teamname", "members_type": "team", "topic_name": "general"} }} }' ``` **Auth (headless):** ```bash keybase oneshot --username botname --paperkey "word1 word2 ... wordN" ``` No Python dependencies needed — just subprocess calls to `keybase chat api`. ## Implementation Plan ### File structure ``` cobot/plugins/keybase/ ├── __init__.py ├── plugin.py # KeybasePlugin └── tests/ └── test_keybase.py ``` ### Plugin skeleton ```python class KeybasePlugin(Plugin): meta = PluginMeta( id="keybase", version="0.1.0", capabilities=["communication"], dependencies=["session"], priority=30, # Same level as telegram extension_points=[ "keybase.on_message", # Keybase-specific hooks ], implements={ "session.receive": "poll_updates", "session.send": "send_message", "session.typing": "send_typing", }, ) ``` ### Config ```yaml keybase: username: cobotbot paperkey_env: KEYBASE_PAPERKEY # env var name (never plaintext in config) team: cobotdev channels: # which channels to listen to - general - dev default_channel: general # for broadcasts poll_mode: listen # "listen" (streaming) or "read" (polling) ``` ### Phase 1: Send messages (~0.5 day) - `start()` → run `keybase oneshot` to authenticate - `send_message(OutgoingMessage)` → `keybase chat api -m '{"method": "send", ...}'` - `get_default_channel_id()` → returns configured default channel - Test: cobot posts to `#general` on startup ### Phase 2: Receive messages (~1 day) **Two strategies (config: `poll_mode`):** **A) `listen` mode (recommended):** - `start()` spawns `keybase chat api-listen` as async subprocess - Background task reads stdout line by line, parses JSON, queues `IncomingMessage` - `poll_updates()` drains the queue and returns messages - Pro: instant delivery, no polling delay - Con: long-running subprocess to manage **B) `read` mode (simpler fallback):** - `poll_updates()` calls `keybase chat api -m '{"method": "read", ...}'` - Tracks last seen message ID, returns only new ones - Pro: stateless, no subprocess - Con: polling delay (depends on loop interval) Both return `list[IncomingMessage]` with: - `channel_type = "keybase"` - `channel_id = "teamname#channelname"` - `sender_id = keybase_username` - `sender_name = keybase_username` ### Phase 3: Session spawning (~1 day) This is the interesting part. When a message arrives from Keybase, it flows through the same pipeline as Telegram: ``` keybase.poll_updates() → IncomingMessage(channel_type="keybase") → session.poll_all_channels() aggregates → loop.run() picks up message → loop._handle_message() → LLM processes, generates response → OutgoingMessage(channel_type="keybase") → session.send() routes to keybase.send_message() ``` For **multi-channel awareness**, the loop needs to know which channel a message came from. This already works — `IncomingMessage.channel_type` and `channel_id` flow through the entire pipeline. The LLM sees the channel context and responds back to the same channel. For **spawning sub-sessions** (e.g. a Keybase thread becomes its own conversation context): - The `subagent` plugin already handles isolated sessions - A Keybase DM or thread could trigger `subagent.spawn()` with `channel_type="keybase"` + `channel_id="user:alice"` as the reply channel - This follows the same pattern as OpenClaw's session spawning — the sub-session gets a dedicated conversation history and replies back to the originating channel **Key decision:** Should each Keybase channel get its own conversation history (separate sessions), or share one? Recommendation: **separate session per channel_id** — `#dev` discussions shouldn't bleed into `#general` context. This might need a small enhancement to the loop/session layer: a session registry that maps `(channel_type, channel_id)` → conversation history. Currently the loop has one shared history. This is the same problem that would arise with any multi-channel setup (Telegram groups + Keybase + DMs). ### Phase 4: Encrypted KV store + KBFS (~1 day) Bonus capabilities unique to Keybase: - `keybase kvstore api` → encrypted team-scoped key-value store (agent state, shared secrets) - `/keybase/team/cobotdev/` → encrypted filesystem (shared files, private plugins) - Expose as tools: `kv_get`, `kv_put`, `kbfs_read`, `kbfs_write` ## Dependencies - `keybase` CLI binary installed on the host - A Keybase account + paperkey for the bot - A Keybase team to join - No Python library dependencies (subprocess only) ## Effort | Phase | Effort | Delivers | |-------|--------|----------| | Phase 1: Send | 0.5 day | Post to Keybase channels | | Phase 2: Receive | 1 day | Bidirectional messaging | | Phase 3: Sessions | 1 day | Per-channel conversation context | | Phase 4: KV + KBFS | 1 day | Encrypted storage + files | | **Total** | **3.5 days** | Full Keybase integration | ## Open Questions 1. **Session-per-channel vs shared session** — the current loop has one conversation history. Multi-channel support (Keybase, Telegram, DMs) needs a session registry. Should this be part of this ticket or a separate "multi-session support" epic? 2. **Bot account** — create a new Keybase user (e.g. `cobotai`) or run as an existing user? 3. **Team name** — `cobotdev`? `cobot`? `ultanio`? ## References - [Keybase bot docs](https://book.keybase.io/docs/bots) - [pykeybasebot](https://github.com/keybase/pykeybasebot) — official Python lib (reference only, we use subprocess) - Telegram plugin: `cobot/plugins/telegram/plugin.py` — direct template to follow - Session plugin: `cobot/plugins/session/plugin.py` — routing layer - #86 — MCP client support (another tool integration pattern) - #85 — code-index-mcp
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ultanio/cobot#87
No description provided.