# CowAgent Admin & Developer Guide

This guide is the operational and architectural reference for **CowAgent** — for the administrators, operators, and developers who deploy it, configure it, secure it, extend it, and keep it running. For end-user instructions (chatting, skills, channels from a user's seat), see the companion **User Guide**.

CowAgent is an open-source, self-hosted "super AI assistant and Agent harness," written in Python. It is the Agent-architecture successor to the widely deployed `chatgpt-on-wechat` project (same author, `zhayujie`). A single process runs an autonomous LLM agent — planning tasks, invoking tools and skills, building long-term memory and a knowledge base — and exposes it simultaneously across a built-in Web console and a dozen messaging channels.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [System Architecture](#2-system-architecture)
3. [Requirements & Installation](#3-requirements--installation)
4. [Configuration Mastery](#4-configuration-mastery)
5. [Model Provider Matrix](#5-model-provider-matrix)
6. [Voice Provider Matrix](#6-voice-provider-matrix)
7. [Channel Deployment](#7-channel-deployment)
8. [Agent Subsystem Internals](#8-agent-subsystem-internals)
9. [Plugin Development](#9-plugin-development)
10. [Security Analysis](#10-security-analysis)
11. [Operations](#11-operations)
12. [Repository Structure & Code Patterns](#12-repository-structure--code-patterns)
13. [Potential Improvements](#13-potential-improvements)
14. [Operational Troubleshooting](#14-operational-troubleshooting)
15. [Quick Reference](#15-quick-reference)

---

## 1. Executive Summary

| Attribute | Detail |
|---|---|
| **Project** | `zhayujie/CowAgent` |
| **What it is** | Self-hosted autonomous AI agent + multi-channel chatbot harness |
| **Language** | Python (3.7–3.12; **not** 3.13+) |
| **License** | MIT |
| **Lineage** | Agent-era successor to `chatgpt-on-wechat` |
| **Process model** | Single Python process; channels run in daemon threads |
| **Persistence** | Filesystem workspace (`~/cow`) + SQLite conversation store |
| **External deps** | One LLM provider API; everything else optional |
| **Default model** | `deepseek-v4-flash` |
| **Default surface** | Web console on `http://localhost:9899` |
| **Channels** | Web, WeChat (personal/Official Account/WeCom/Kefu), Feishu, DingTalk, QQ, Telegram, Slack, Discord — 13 types |
| **LLM providers** | 18 provider adapters (OpenAI, Claude, Gemini, DeepSeek, Qwen, GLM, Kimi, MiniMax, Doubao, MiMo, ERNIE, ModelScope, Xunfei, LinkAI, …) |
| **Voice engines** | 15 (OpenAI, Azure, Baidu, iFlytek, Edge, ElevenLabs, offline pytts, …) |

### Architecture at a glance

CowAgent is organized as eight cooperating subsystems — **Plan, Memory, Knowledge, Tools, Skills, Models, Channels, CLI** — wired together by a central **Bridge**. The dominant design pattern throughout is **abstract base class + directory of one-file adapters**: channels, models, and voice engines are each a factory that instantiates a concrete implementation by name. This is what lets the project support dozens of providers without entangling them.

### Two operating modes

| Mode | `agent` | Behavior |
|---|---|---|
| **Agent** (default) | `true` | Autonomous loop: plan → call tools/skills → update memory/knowledge → reply. Has OS access. |
| **Chat** (classic) | `false` | Stateless-ish conversational reply via `SessionManager`. Lighter, cheaper; target of the classic plugin system. |

> [!IMPORTANT]
> The single most consequential operational fact about CowAgent: **in Agent mode the process can execute arbitrary shell commands and read/write files on its host.** This is a feature (it's what makes the agent useful), but it makes the trust boundary of the deployment host the trust boundary of the whole system. Plan deployments accordingly — see [Security Analysis](#10-security-analysis).

---

## 2. System Architecture

### 2.1 Module map

| Subsystem | Source | Responsibility |
|---|---|---|
| **Plan** | `agent/protocol/` | The agent loop — intent → multi-step plan → iterative tool calls |
| **Memory** | `agent/memory/` | Three-tier memory (context → daily → core), Deep Dream distillation, hybrid keyword+vector retrieval, SQLite conversation store |
| **Knowledge** | `agent/knowledge/` | Topic-organized Markdown wiki + knowledge graph |
| **Tools** | `agent/tools/` | 10+ tools: file I/O, bash, browser, scheduler, web search/fetch, memory, vision, env_config, send, MCP |
| **Skills** | `agent/skills/` | Load/manage/execute SKILL.md skills (Agent Skills standard) |
| **Models** | `models/` | Unified LLM access; `bot_factory` + per-provider bots + OpenAI-compatible adapter |
| **Channels** | `channel/` | Message transport; `channel_factory` + per-platform channels + shared `ChatChannel` |
| **Voice** | `voice/` | STT/TTS; `voice/factory` + per-engine adapters + audio conversion |
| **Bridge** | `bridge/` | Routes a message to Agent mode or Chat mode; wires the agent to the model/tool infrastructure |
| **Plugins** | `plugins/` | Classic event-hook plugin system (chatgpt-on-wechat lineage) |
| **CLI** | `cli/` | The `cow` command (click-based) for service/skill management |
| **Common** | `common/` | Config constants, i18n, logging, utilities, optional LinkAI cloud client |

### 2.2 Runtime data flow

```text
                         ┌──────────────────────────────────────────┐
   user message          │                CowAgent process          │
  (Web / WeChat /        │                                          │
   Feishu / Telegram…)   │   channel_factory.create_channel(name)   │
        │                │              │                            │
        ▼                │              ▼                            │
   ┌─────────┐  produce  │   ┌──────────────────┐  per-session       │
   │ Channel │──────────▶│   │   ChatChannel    │  serial queue      │
   │ (adapter)│          │   │  (shared logic)  │  + thread pool     │
   └─────────┘           │   └────────┬─────────┘                    │
        ▲                │            │ build_reply_content          │
        │ send(reply)    │            ▼                              │
        │                │      ┌───────────┐   agent? ──► AgentBridge│
        │                │      │  Bridge   │──────────────┐         │
        │                │      └───────────┘   chat?       │        │
        │                │            │      fetch_reply_content│     │
        │                │            ▼                     ▼        │
        │                │   ┌──────────────┐    ┌────────────────┐  │
        │                │   │ SessionMgr + │    │  Agent loop:   │  │
        │                │   │ bot_factory  │    │  model + tools │  │
        │                │   │  (chat mode) │    │  + skills +    │  │
        │                │   └──────┬───────┘    │  memory +      │  │
        │                │          │            │  knowledge +   │  │
        │                │          ▼            │  scheduler+MCP │  │
        │                │   ┌──────────────┐    └───────┬────────┘  │
        └────────────────┼───│  LLM provider (HTTP, OpenAI-compatible)│
                         │   └──────────────┘            │           │
                         │     models/bot_factory.create_bot(type)   │
                         └──────────────────────────────────────────┘
```

Each incoming message becomes a `Context`. `ChatChannel` (the abstract base shared by every channel) enforces triggers and whitelists, emits plugin events at four lifecycle points, then calls `build_reply_content`, which the `Bridge` routes to either the **Agent loop** (`AgentBridge.agent_reply`) or **Chat mode** (`fetch_reply_content`). The reply travels back out through the same channel's `send()`.

### 2.3 The factory + adapter pattern

Three subsystems share an identical shape. Each has an abstract base, a factory that maps a string to a concrete class, and a directory of one-file implementations:

| Subsystem | Base class | Factory | Adapter directory |
|---|---|---|---|
| Channels | `channel/channel.py` → `Channel` (and `ChatChannel`) | `channel/channel_factory.py::create_channel` | `channel/<platform>/` |
| Models | `models/bot.py` → `Bot` | `models/bot_factory.py::create_bot` | `models/<provider>/` |
| Voice | `voice/voice.py` → `Voice` | `voice/factory.py::create_voice` | `voice/<engine>/` |

To add a provider/channel/engine you write one adapter directory and add one branch to its factory. Documenting CowAgent therefore means documenting the **abstraction and the matrix of adapters**, not each adapter's internals.

### 2.4 The `ChannelManager` and concurrency

`app.py` owns a `ChannelManager` that runs each configured channel in its own daemon thread (`startup()` per channel). Notable behaviors:

- `channel_type` is parsed into a **list** — multiple channels run concurrently in one process.
- The Web console is started first (clean logs) and is auto-added unless `web_console: false`.
- Channels can be added, removed, or restarted at runtime (used by the optional LinkAI cloud console for hot reconfiguration).
- Per-session message processing is serialized through a `Dequeue` + `BoundedSemaphore` (`concurrency_in_session`, default 1) and dispatched on an 8-worker `ThreadPoolExecutor`. Admin commands (`#…`) jump the queue; `/cancel` bypasses it entirely.

### 2.5 The OpenAI-compatible adapter (key abstraction)

`models/openai_compatible_bot.py::OpenAICompatibleBot` is the linchpin of multi-provider tool calling. The agent speaks an Anthropic-style message/tool format internally; this mixin converts to/from OpenAI's `tool_calls`/`tool`/`reasoning_content` format, injects the system prompt, calls the provider over HTTP (sync or SSE streaming), and exposes `call_with_tools()` and `call_vision()`. `AgentBridge` dynamically grafts this mixin onto any bot that lacks native tool calling, so a provider only needs an OpenAI-compatible `/chat/completions` endpoint to gain full agent capability.

---

## 3. Requirements & Installation

### 3.1 Host requirements

| Requirement | Detail |
|---|---|
| OS | Linux, macOS, Windows |
| Python | **3.7–3.12** (3.12 recommended). 3.13+ is unsupported: `web.py` needs the removed stdlib `cgi` module |
| Core Python deps | `aiohttp` (<3.10), `requests`, `web.py`, `PyYAML`, `click`, `croniter`, `Pillow`, `numpy`, `python-dotenv`, `json-repair` |
| Optional deps | `wechatpy` (WeChat), `lark-oapi` (Feishu), `dingtalk_stream`, `python-telegram-bot`, `slack_bolt`, `discord.py`, `dashscope`, `zai-sdk`, `pycryptodome` |
| Outbound network | HTTPS to the chosen LLM provider |

### 3.2 Installation paths

```bash
# Guided one-liner (clone + deps + interactive config + start)
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)

# Manual
git clone https://github.com/zhayujie/CowAgent.git
cd CowAgent
pip install -r requirements.txt
pip install -e .                  # registers the `cow` console entry point
cp config-template.json config.json
python app.py
```

The installer/management script `run.sh` provides `start|stop|restart|status|logs|config|update`, detects a compatible Python, falls back from GitHub to the **Gitee mirror**, and uses the **Tsinghua PyPI mirror** for China networks.

### 3.3 Docker

A Dockerfile ships under `docker/`. The image (historically tagged `zhayujie/chatgpt-on-wechat`) runs the app as a **non-root `agent` user** and expects the workspace mounted at `/home/agent/cow` and config at `/app/config.json`.

```bash
docker run -d --name cowagent \
  -p 9899:9899 \
  -v $PWD/config.json:/app/config.json \
  -v $PWD/cow-data:/home/agent/cow \
  zhayujie/chatgpt-on-wechat
```

The entrypoint honors `CHATGPT_ON_WECHAT_PREFIX`, `CHATGPT_ON_WECHAT_CONFIG_PATH`, and `CHATGPT_ON_WECHAT_EXEC` environment variables and fixes ownership of mounted volumes before dropping privileges.

> [!TIP]
> For channels that need an inbound webhook (WeChat Official Account, WeCom self-built app), publish the matching port (`wechatmp_port` 8080, `wechatcomapp_port` 9898, etc.) and terminate TLS at a reverse proxy.

---

## 4. Configuration Mastery

CowAgent reads `./config.json` (falling back to `./config-template.json` if absent). The authoritative schema is the `available_setting` dictionary in `config.py` — every key the program reads is listed there with a documented default. **Any key not in that dictionary is ignored** for typed access. Files are read as UTF-8-with-BOM tolerant.

### 4.1 Loading and override precedence

1. `config.json` is parsed. Duplicate top-level keys are **deep-merged** (lists concatenate, dicts merge) rather than silently dropped, and a warning is logged.
2. Legacy singular sections `tool`/`skill` are folded into the canonical plural `tools`/`skills`.
3. **Environment variables override config**: any env var whose lowercased name matches a key in `available_setting` replaces it (values are `eval`'d, with `true`/`false` coerced to booleans). This is how secret-free deployments (e.g. Railway) inject API keys.
4. Selected keys are also **exported back to the environment** (e.g. `open_ai_api_key` → `OPENAI_API_KEY`) so subprocess-based skill scripts can read them. Existing env vars always win.
5. Per-skill config (`skills.<name>.<key>`) is flattened to `SKILL_<NAME>_<KEY>` env vars.

> [!NOTE]
> Secrets are masked in logs by `drag_sensitive()` — any key containing `key` or `secret` is shown as `abc*****xyz`. The config file itself stores them in plaintext.

### 4.2 Core / Agent settings

| Key | Default | Purpose |
|---|---|---|
| `cow_lang` | `auto` | UI/CLI/reply/prompt language: `auto`, `zh`, `en` |
| `model` | `gpt-3.5-turbo` | Active model name (template ships `deepseek-v4-flash`) |
| `bot_type` | `""` | Force a provider adapter; inferred from model name if empty. Use `openai`/`custom` for OpenAI-compatible APIs |
| `agent` | `true` | Enable Agent mode |
| `agent_workspace` | `~/cow` | Workspace for skills, memory, knowledge |
| `agent_max_context_tokens` | `50000` | Max context tokens per run |
| `agent_max_context_turns` | `20` | Max conversation turns retained |
| `agent_max_steps` | `20` | Max tool/decision steps per task |
| `enable_thinking` | `false` | Extended-reasoning mode |
| `reasoning_effort` | `high` | `high` or `max` (only when thinking on) |
| `knowledge` | `true` | Enable the knowledge base |
| `character_desc` | (assistant persona) | System persona (chat mode) |
| `mcp_servers` | `[]` | MCP server list; each entry is `stdio` (local process) or `sse` (remote URL) |
| `skill` | `{}` | Per-skill runtime config (flattened to env vars) |

### 4.3 Trigger / behavior settings

| Key | Default | Purpose |
|---|---|---|
| `single_chat_prefix` | `["bot", "@bot"]` | 1-on-1 trigger words; `[""]` replies to all |
| `single_chat_reply_prefix` | `"[bot] "` | Prefix on 1-on-1 replies |
| `group_chat_prefix` | `["@bot"]` | Group trigger words |
| `group_chat_keyword` | `[]` | Group trigger keywords |
| `no_need_at` | `false` | Reply in group without @ |
| `group_at_off` | `false` | Disable @-triggering in groups |
| `group_name_white_list` | `["group1","group2"]` | Groups where auto-reply is on (`ALL_GROUP` = all) |
| `group_shared_session` | `false` | All members share one session vs. per-user |
| `nick_name_black_list` | `[]` | Ignored nicknames |
| `trigger_by_self` | `false` | Allow the bot to trigger on its own messages |
| `image_create_prefix` | `["画","看","找"]` | Prefixes that request image generation |
| `clear_memory_commands` | `["#清除记忆"]` | Session-reset command(s) |
| `plugin_trigger_prefix` | `$` | Plugin command prefix (admin commands use `#`) |
| `concurrency_in_session` | `1` | In-flight messages per session (>1 risks out-of-order) |
| `expires_in_seconds` | `3600` | Idle session expiry |
| `conversation_max_tokens` | `1000` | Context memory size (chat mode) |

### 4.4 Web console settings

| Key | Default | Purpose |
|---|---|---|
| `web_console` | `true` | Auto-start the Web console |
| `web_host` | `""` (auto) | Bind address |
| `web_port` | `9899` | Console port |
| `web_password` | `""` (no auth) | Console password — **set this if exposed** |
| `web_session_expire_days` | `30` | Auth session lifetime |
| `web_file_serve_root` | `~` | Root the `/api/file` endpoint may serve — **never set to `/`** |

### 4.5 Voice settings

| Key | Default | Purpose |
|---|---|---|
| `voice_to_text` | `openai` | STT engine |
| `text_to_voice` | `openai` | TTS engine |
| `speech_recognition` | `true` | Transcribe incoming voice |
| `group_speech_recognition` | `false` | Transcribe group voice |
| `voice_reply_voice` | `false` | Reply with voice to voice input |
| `always_reply_voice` | `false` | Always reply with voice |
| `text_to_voice_model` / `tts_voice_id` | `tts-1` / `alloy` | TTS model and voice |

Engine-specific keys exist for each provider (e.g. `baidu_app_id`/`baidu_api_key`/`baidu_secret_key`, `azure_voice_api_key`/`azure_voice_region`, `xi_api_key`/`xi_voice_id` for ElevenLabs). See the [Voice Provider Matrix](#6-voice-provider-matrix).

### 4.6 Model / provider credential keys

Each provider has an API-key (and usually api-base) field. The most common:

```json
{
  "open_ai_api_key": "", "open_ai_api_base": "https://api.openai.com/v1",
  "claude_api_key": "",  "claude_api_base": "https://api.anthropic.com/v1",
  "gemini_api_key": "",  "gemini_api_base": "https://generativelanguage.googleapis.com",
  "deepseek_api_key": "","deepseek_api_base": "https://api.deepseek.com/v1",
  "dashscope_api_key": "",                 /* Qwen */
  "zhipu_ai_api_key": "", "zhipu_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
  "moonshot_api_key": "", "moonshot_base_url": "https://api.moonshot.cn/v1",
  "ark_api_key": "",      "ark_base_url": "https://ark.cn-beijing.volces.com/api/v3",  /* Doubao */
  "minimax_api_key": "", "Minimax_group_id": "",
  "mimo_api_key": "",     "mimo_api_base": "https://api.xiaomimimo.com/v1",
  "qianfan_api_key": "",  "qianfan_api_base": "https://qianfan.baidubce.com/v2",  /* ERNIE */
  "modelscope_api_key": "",
  "custom_api_key": "", "custom_api_base": "",   /* any OpenAI-compatible endpoint, bot_type="custom" */
  "use_linkai": false, "linkai_api_key": "", "linkai_app_code": ""
}
```

Generation parameters (`temperature` 0.9, `top_p` 1, `frequency_penalty`, `presence_penalty`, `request_timeout` 180, `timeout` 120, `rate_limit_chatgpt` 20) apply to chat-mode/OpenAI-compatible calls. GPT-5/o1-class models automatically have penalty/temperature params stripped.

### 4.7 LinkAI cloud (optional, off by default)

| Key | Default | Purpose |
|---|---|---|
| `use_linkai` | `false` | Master switch for LinkAI integration |
| `linkai_api_key` | `""` | LinkAI key (one key → all models) |
| `cloud_deployment_id` | `""` | Enables the remote management console |
| `cloud_host` | `client.link-ai.tech` | Cloud control-plane host |

> [!IMPORTANT]
> The cloud client connects to LinkAI's console **only when both `use_linkai` is true and `cloud_deployment_id` (or env `CLOUD_DEPLOYMENT_ID`) is set.** Otherwise the module is never even imported and CowAgent runs as a purely local application. When enabled, the console can remotely change config, add/remove channels, push messages, and sync skills/memory/knowledge — treat that key as a privileged credential.

---

## 5. Model Provider Matrix

The provider adapter is chosen by `bot_type`, which is normally **inferred from the model name** (prefix match) unless you set it explicitly or enable LinkAI.

| Provider | `bot_type` const | Adapter class | Example models | Credential |
|---|---|---|---|---|
| OpenAI (official SDK) | `openAI` | `OpenAIBot` | `gpt-5.4`, `gpt-4o` | `open_ai_api_key` |
| OpenAI-compatible | `openai`/`chatGPT`/`custom` | `ChatGPTBot` | any OpenAI-format model | `open_ai_api_key` / `custom_api_key` |
| Azure OpenAI | `chatGPTOnAzure` | `AzureChatGPTBot` | deployment name | `open_ai_api_key` + azure_* |
| Claude (Anthropic) | `claudeAPI` | `ClaudeAPIBot` | `claude-opus-4-8`, `claude-sonnet-4-6` | `claude_api_key` |
| Gemini (Google) | `gemini` | `GoogleGeminiBot` | `gemini-3.5-flash`, `gemini-3.1-pro-preview` | `gemini_api_key` |
| DeepSeek | `deepseek` | `DeepSeekBot` | `deepseek-v4-flash`, `deepseek-v4-pro` | `deepseek_api_key` |
| Qwen (DashScope) | `dashscope`/`qwen` | `DashscopeBot` | `qwen3.7-max`, `qwen3.7-plus` | `dashscope_api_key` |
| GLM (Zhipu) | `zhipu` | `ZHIPUAIBot` | `glm-5.1`, `glm-4.7` | `zhipu_ai_api_key` |
| Kimi (Moonshot) | `moonshot` | `MoonshotBot` | `kimi-k2.6` | `moonshot_api_key` |
| MiniMax | `minimax` | `MinimaxBot` | `MiniMax-M3` | `minimax_api_key` |
| Doubao (Volc Ark) | `doubao` | `DoubaoBot` | `doubao-seed-2-0-pro-260215` | `ark_api_key` |
| MiMo (Xiaomi) | `mimo` | `MimoBot` | `mimo-v2.5-pro` | `mimo_api_key` |
| ERNIE (Baidu Qianfan) | `qianfan` | `QianfanBot` | `ernie-5.1`, `ernie-x1.1` | `qianfan_api_key` |
| Baidu Wenxin (legacy) | `baidu` | `BaiduWenxinBot` | `wenxin`, `wenxin-4` | `baidu_wenxin_api_key` |
| Xunfei Spark | `xunfei` | `XunFeiBot` | `xunfei` | `xunfei_app_id`/`_api_key`/`_api_secret` |
| ModelScope | `modelscope` | `ModelScopeBot` | `Qwen/Qwen3.5-27B`, … | `modelscope_api_key` |
| LinkAI gateway | `linkai` | `LinkAIBot` | any | `linkai_api_key` |

**Name-prefix routing** (`AgentLLMModel._MODEL_PREFIX_MAP`): `qwen`/`qwq`/`qvq`→DashScope, `gemini`→Gemini, `glm`→Zhipu, `claude`→Claude, `moonshot`/`kimi`→Moonshot, `doubao`→Doubao, `deepseek`→DeepSeek, `ernie`→Qianfan; anything unmatched → OpenAI-compatible.

> [!NOTE]
> Agent-recommended (tool-calling-tuned) defaults per provider: `deepseek-v4-flash` (global default), `claude-opus-4-8`, `gemini-3.1-pro-preview`/`gemini-3.5-flash`, `gpt-5.4`, `glm-5.1`, `kimi-k2.6`, `MiniMax-M3`, `qwen3.7-max`, `mimo-v2.5-pro`, `doubao-seed-2-0`. The full enumerated model list lives in `common/const.py::MODEL_LIST`.

---

## 6. Voice Provider Matrix

STT and TTS engines are selected independently by `voice_to_text` and `text_to_voice` and instantiated by `voice/factory.py::create_voice`. The `Voice` base class defines `voiceToText()` and `textToVoice()`.

| Engine value | Adapter | Required config | Notes |
|---|---|---|---|
| `openai` | `OpenaiVoice` | `open_ai_api_key` | Whisper STT + TTS |
| `baidu` | `BaiduVoice` | `baidu_app_id`/`baidu_api_key`/`baidu_secret_key`, `baidu_dev_pid` | |
| `google` | `GoogleVoice` | — | |
| `azure` | `AzureVoice` | `azure_voice_api_key`, `azure_voice_region` | |
| `xunfei` | `XunfeiVoice` | `xunfei_app_id`/`_api_key`/`_api_secret` | |
| `ali` / `dashscope` | `AliVoice` / `DashScopeVoice` | `dashscope_api_key` | |
| `tencent` | `TencentVoice` | Tencent cloud creds | |
| `minimax` | `MinimaxVoice` | `minimax_api_key` | |
| `zhipu` | `ZhipuAIVoice` | `zhipu_ai_api_key` | |
| `mimo` | `MimoVoice` | `mimo_api_key` | |
| `elevenlabs` | `ElevenLabsVoice` | `xi_api_key`, `xi_voice_id` | |
| `edge` | `EdgeVoice` | — | **Free**, online, no key |
| `pytts` | `PyttsVoice` | — | **Offline** TTS |
| `linkai` | `LinkAIVoice` | `linkai_api_key` | |

Audio is normalized through `voice/audio_convert.py` (`any_to_wav`/`mp3`/`sil`/`amr`), including SILK encode/decode for WeChat voice (requires `pysilk`; conversion requires `pydub` + `ffmpeg`).

---

## 7. Channel Deployment

Active channels are set by `channel_type` (string, comma-separated, or list). All channels share `ChatChannel`'s trigger/whitelist/plugin pipeline.

| Channel | `channel_type` | Connectivity | Key config |
|---|---|---|---|
| Web console | `web` | local HTTP | `web_port` 9899, `web_password` |
| Terminal | `terminal` | local TTY (`app.py --cmd`) | — |
| WeChat (personal) | `weixin` | outbound (QR login) | `weixin_token` (auto QR if empty) |
| Official Account (subscription) | `wechatmp` | **inbound webhook** | `wechatmp_token`, `wechatmp_app_id/secret`, `wechatmp_port` 8080 |
| Official Account (service) | `wechatmp_service` | inbound webhook | as above |
| WeCom self-built app | `wechatcom_app` | inbound webhook | `wechatcom_corp_id`, `wechatcomapp_*`, `wechatcomapp_port` 9898 |
| WeCom smart bot | `wecom_bot` | long-connection | `wecom_bot_id`, `wecom_bot_secret` |
| WeChat Customer Service | `wechat_kf` | inbound webhook | `wechat_kf_corp_id/token/secret/aes_key`, port 9888 |
| Feishu / Lark | `feishu` | WebSocket or webhook | `feishu_app_id/secret`, `feishu_event_mode` |
| DingTalk | `dingtalk` | long-connection (Stream) | `dingtalk_client_id/secret` |
| QQ | `qq` | official bot | `qq_app_id`, `qq_app_secret` |
| Telegram | `telegram` | Bot API (outbound) | `telegram_token`, `telegram_proxy` |
| Slack | `slack` | Socket Mode | `slack_bot_token`, `slack_app_token` |
| Discord | `discord` | Gateway | `discord_token` |

**Connectivity classes for ops planning:**

- **No inbound port needed** (outbound or long-connection): `web` (local), `weixin`, `wecom_bot`, `feishu` (websocket), `dingtalk`, `telegram`, `slack`, `discord`, `qq`. These run behind NAT with no public IP.
- **Inbound webhook needed** (publish a port + TLS): `wechatmp`, `wechatmp_service`, `wechatcom_app`, `wechat_kf`, `feishu` (webhook mode). Put these behind a reverse proxy with HTTPS.

Group trigger modes for IM platforms (`telegram_group_trigger`, `slack_group_trigger`, `discord_group_trigger`): `mention_or_reply` (recommended), `mention_only`, `all`.

---

## 8. Agent Subsystem Internals

### 8.1 AgentBridge and per-session agents

`bridge/agent_bridge.py::AgentBridge` is the integration layer between the channel pipeline and the agent core (`agent/protocol/`). Key responsibilities:

- **Per-session isolation** — one `Agent` instance per `session_id` (`self.agents` map); a default agent for session-less calls.
- **Agent construction** — `create_agent()` wires an `AgentLLMModel` (wrapping `bot_factory`), the full tool set (`ToolManager`), the `SkillManager`, and the `memory_manager`, with `max_steps`, `max_context_tokens`, and workspace.
- **Cancellation** — a cancel registry keyed by per-turn `request_id` (web) or `session_id` (IM); polled at safe checkpoints; driven by `/cancel`.
- **Thinking mode** — passes `thinking`/`reasoning_effort` kwargs when `enable_thinking` is set.
- **MCP hot-reload** — after each reply, a background thread diffs `~/cow/mcp.json` and syncs added/removed MCP tools into the live agent.
- **Scheduler integration** — eagerly starts the scheduler at boot; attaches context to the scheduler tool; excludes the scheduler tool during scheduled-task execution to prevent recursion.
- **Streaming** — `run_stream()` emits events to an `AgentEventHandler`, which the web channel renders as SSE.

### 8.2 Memory and the conversation store

Memory is persisted to a **SQLite conversation store** (`agent/memory/`, `get_conversation_store()`), governed by `conversation_persistence` (default on). New messages are appended after each run; "thinking" blocks are stripped before persistence unless `enable_thinking` is on. Scheduler-injected turns are marked (`[SCHEDULED]`) and pruned with a sliding window (`scheduler_inject_max_per_session`, default 3) so recurring tasks don't grow unbounded. The three-tier memory model (context → daily → `MEMORY.md`) plus daily Deep Dream distillation and hybrid keyword+vector retrieval lives alongside it in the workspace.

Workspace layout:

```text
~/cow/
├── SYSTEM.md        USER.md        MEMORY.md      mcp.json
├── memory/YYYY-MM-DD.md
├── knowledge/index.md  +  <topic>/*.md
└── skills/<name>/SKILL.md
~/.cow/.env          # skill secrets (separate from workspace)
```

### 8.3 Tools and MCP

`agent/tools/` holds the built-in tools, each in its own subpackage: `bash`, `browser`, `edit`, `read`, `write`, `ls`, `send`, `web_fetch`, `web_search`, `vision`, `memory`, `env_config`, `scheduler`, plus the `mcp` integration. `ToolManager` discovers and instantiates them; conditional tools (e.g. `web_search`) enable only when their credentials (`BOCHA_API_KEY`/`LINKAI_API_KEY`) are present and refresh on env changes. **MCP servers** declared in `config.mcp_servers` or `~/cow/mcp.json` run as `stdio` (local `npx`/`uvx` subprocess) or `sse` (remote URL), warmed at startup and hot-reloaded per message.

### 8.4 Skills (Agent Skills standard)

`agent/skills/` implements `SkillLoader` / `SkillManager` / `SkillService`. A skill is a `SKILL.md` with frontmatter declaring `description`, `requires` (`bins`, `anyBins`, `env`, `anyEnv`), `os`, `always`, and `disable_model_invocation`. `should_include_skill()` **auto-enables** a skill when its requirements are met — present binaries and env vars gate availability. Visible skills are formatted into the system prompt as `<available_skills>` XML. Built-in skills ship in the project `skills/` directory (`skill-creator`, `image-generation`/`openai-image-vision`, `knowledge-wiki`) and are synced to `~/cow/skills/` at startup; custom skills are authored via the `skill-creator` skill. Remote install pulls from the Skill Hub (`skills.cowagent.ai`), GitHub, ClawHub, LinkAI, or a URL.

---

## 9. Plugin Development

The classic plugin system (inherited from `chatgpt-on-wechat`) is distinct from skills: plugins hook into the **message pipeline** via events, and are primarily relevant in chat mode.

### 9.1 Event model

`PluginManager` (a singleton) emits an `EventContext` at four points in `ChatChannel`:

| Event | Fired | Payload |
|---|---|---|
| `ON_RECEIVE_MESSAGE` | message received | `channel`, `context` |
| `ON_HANDLE_CONTEXT` | before generating a reply | `channel`, `context`, `reply` |
| `ON_DECORATE_REPLY` | after reply, before formatting | `channel`, `context`, `reply` |
| `ON_SEND_REPLY` | before sending | `channel`, `context`, `reply` |

A handler sets `EventAction`: `CONTINUE` (pass to next plugin), `BREAK` (stop plugins, run default logic), or `BREAK_PASS` (stop everything). Plugins are ordered by `priority`.

### 9.2 Authoring a plugin

```python
# plugins/myplugin/__init__.py
from plugins import Plugin, Event, EventContext, EventAction, register

@register(name="MyPlugin", desire_priority=50, desc="…", version="1.0", author="you")
class MyPlugin(Plugin):
    def __init__(self):
        super().__init__()
        self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context

    def on_handle_context(self, e_context: EventContext):
        ctx = e_context["context"]
        if ctx.content == "ping":
            from bridge.reply import Reply, ReplyType
            e_context["reply"] = Reply(ReplyType.TEXT, "pong")
            e_context.action = EventAction.BREAK_PASS
```

Drop the package in `plugins/`, then `#scanp` (admin chat command) to load it. State lives in `plugins/plugins.json` (enable/priority) and `plugins/config.json` (global plugin config); per-plugin config can also sit in the plugin's own `config.json`.

### 9.3 Built-in and marketplace plugins

Preset plugins (can't be updated independently — they ship with the project): `godcmd` (admin auth/commands), `role`, `keyword`, `banwords`, `finish`, `hello`, `dungeon`, `linkai`, `tool`. Third-party plugins install from a Git URL via `#installp` (uses `dulwich` to clone + installs `requirements.txt`); a curated catalog lives in `plugins/source.json`.

> [!CAUTION]
> Installing a plugin clones and runs arbitrary third-party code and pip-installs its requirements. Restrict who holds admin (`godcmd`) rights, and vet plugin sources.

---

## 10. Security Analysis

### 10.1 Threat model summary

CowAgent's defining risk is that **Agent mode grants the LLM direct, tool-mediated control of the host OS** (bash, file read/write/edit, browser, package install). Anyone who can send the agent messages can, in principle, drive those tools. The security posture therefore depends almost entirely on controlling **who can reach the agent** and **what the host can lose**.

### 10.2 Findings and hardening

| Area | Risk | Mitigation |
|---|---|---|
| **Web console auth** | `web_password` empty by default → unauthenticated access to an OS-capable agent | Set `web_password`; bind `web_host` to `127.0.0.1`; reverse-proxy with auth if remote |
| **File-serving endpoint** | `web_file_serve_root` defaults to `~`; `/` would serve the whole filesystem | Set to a narrow dir (e.g. `~/cow`); never `/` |
| **Host access** | Agent runs shell/file/browser tools as the process user | Run as a dedicated non-root user (the Docker image already does); containerize; avoid sensitive mounts |
| **Plaintext secrets** | API keys stored in `config.json` | Prefer env-var override; restrict file perms; logs auto-mask keys via `drag_sensitive()` |
| **Plugin/skill install** | Arbitrary git clone + pip install + script execution | Limit admin rights; install only from trusted sources |
| **MCP servers** | Spawn local subprocesses (`npx`/`uvx`) | Only configure trusted MCP servers; review `mcp.json` |
| **WeChat MP webhook bypass** | Issue #2860: signature verification bypassed when `wechatmp_token` is empty | **Always set a non-empty `wechatmp_token`** on public Official Account channels |
| **LinkAI cloud key** | Enables remote config/channel/message control | Treat `linkai_api_key` + `cloud_deployment_id` as privileged; leave disabled if unused |

### 10.3 Secret handling

- `config.py::load_config()` syncs select keys to environment variables (e.g. `open_ai_api_key`→`OPENAI_API_KEY`) for subprocess skills; existing env vars are never overwritten.
- `AgentBridge._migrate_config_to_env()` also writes select keys to `~/.cow/.env` for the agent runtime.
- Skill secrets are intentionally kept in `~/.cow/.env`, separate from the `~/cow` workspace that holds memory/knowledge content.

> [!IMPORTANT]
> When malicious or untrusted use is a concern (a public-facing channel, a shared host), assume the agent's tools can be weaponized and isolate accordingly: dedicated low-privilege user, container with no sensitive mounts, network egress controls, and a narrow `web_file_serve_root`.

---

## 11. Operations

### 11.1 Service lifecycle

```bash
cow start [-f|--foreground] [--no-logs]
cow stop
cow restart [--no-logs]
cow status            # status + model + channel
cow logs [-f] [-n N]  # default 50 lines
cow update            # git pull + reinstall deps + restart
```

Without the `cow` entry point, the `./run.sh` script provides the same verbs and runs `app.py` under `nohup setsid` (Linux) / `nohup` (macOS), writing to `nohup.out`. PID discovery is by `ps` match on `app.py` + the project dir.

### 11.2 Updates and rollback

`cow update` / `./run.sh update` pulls latest (GitHub → Gitee fallback), reinstalls dependencies, and restarts. `./run.sh config` re-runs interactive configuration and **backs up** the existing `config.json` to a timestamped file first. There is no built-in migration tooling — review `config-template.json` after major updates for new keys.

### 11.3 Backup

Back up two trees:

- **`config.json`** — credentials and behavior.
- **`~/cow/`** (and `~/.cow/.env`) — memory, knowledge, custom skills, MCP config, and secrets.

The SQLite conversation store also lives under the workspace/appdata dir. `appdata_dir` (default empty → project root) holds `user_datas.pkl` (per-user data, saved on SIGTERM/SIGINT).

### 11.4 Logging and observability

Logging is via `common/log.py`; set `"debug": true` for DEBUG level (more verbose, prints config with masked secrets at startup). Logs print a banner with resolved language, channel, model, and mode at init. There is no built-in metrics/OTel surface — observe via log tailing and `cow status`.

### 11.5 Environment-variable deployment

For platforms that deploy from Git without a config file (e.g. Railway), set environment variables matching `available_setting` keys (case-insensitive). Example:

```bash
export model=deepseek-v4-flash
export deepseek_api_key=sk-...
export channel_type=web
export web_password=...
python app.py
```

Values are `eval`'d (so `["a","b"]`, `true`/`false`, and numbers work); they override `config.json`.

---

## 12. Repository Structure & Code Patterns

```text
CowAgent/
├── app.py                 # entry point: ChannelManager + run loop
├── config.py              # config schema (available_setting), load/override
├── config-template.json   # minimal starter config
├── run.sh                 # installer + lifecycle management script
├── requirements.txt
├── cli/                   # `cow` click CLI (commands/: context, install, knowledge, process, skill)
├── bridge/                # Bridge + AgentBridge (Agent↔Chat routing, agent integration)
├── channel/               # Channel base + ChatChannel + per-platform adapters (13)
├── models/                # Bot base + bot_factory + OpenAICompatibleBot + per-provider bots (18)
├── voice/                 # Voice base + factory + per-engine adapters (15) + audio_convert
├── translate/             # translation adapters (baidu, youdao)
├── agent/                 # protocol (core loop), tools/, skills/, memory/, knowledge/, prompt/, chat/
├── plugins/               # classic plugin framework + preset plugins
├── skills/                # built-in skills (skill-creator, image-generation, knowledge-wiki)
├── common/                # const.py, i18n, log, utils, cloud_client (LinkAI)
├── docker/                # Dockerfile + entrypoint
└── docs/                  # Mintlify docs site (en/zh/ja)
```

**Patterns worth knowing:**

- **Factory + adapter** everywhere (channels/models/voice/tools/skills) — extension is additive.
- **Singletons** via `@singleton` (`PluginManager`, `Bridge`) and module-global `config`.
- **Threaded concurrency** — daemon thread per channel; per-session serial queues over a shared thread pool; `ctypes`-based thread interruption for forced channel stop.
- **Lazy imports** — adapters import their heavy SDKs only when instantiated, so missing optional deps don't break startup.
- **Dynamic mixin injection** — `add_openai_compatible_support()` rewrites a bot's `__class__` at runtime to add tool calling.
- **i18n** — `common/i18n` resolves `cow_lang`; user-facing strings use a `t(zh, en)` helper (also in `run.sh`).

**Extraction note for downstream AI/maintainers:** AST extraction yielded 249 classes / 1,007 functions / 36 click CLI entries / 0 HTTP endpoints. The zero endpoints is expected — the Web console uses `web.py` internal routing (in the 185 KB `channel/web/web_channel.py`), not decorator-based routes, so the HTTP surface isn't captured by the extractor and must be read from that file directly.

---

## 13. Potential Improvements

Observations from the source, offered as a maintenance/roadmap backlog:

- **`channel/web/web_channel.py` is ~185 KB in one file.** It bundles the entire Web console (HTTP routes, SSE, file serving, model/knowledge UIs). Splitting routing, the API surface, and the UI into modules would improve maintainability and make the HTTP surface auto-discoverable.
- **Web console security defaults are permissive.** `web_password` empty and `web_file_serve_root` = `~` are convenient but unsafe if exposed. Consider warning loudly (or refusing to bind a non-loopback `web_host`) when no password is set.
- **No built-in sandbox for agent tools.** Given OS-level tool access, an optional restricted/sandboxed execution mode (allowlist of commands, containerized bash) would materially reduce blast radius for shared deployments.
- **Plaintext secrets in `config.json`.** A first-class secrets backend (or mandatory env-var mode for keys) would reduce accidental key leakage.
- **Python 3.13 blocker.** The `web.py`/`cgi` dependency caps supported Python at 3.12; migrating the web layer off `web.py` would unblock newer runtimes.
- **WeChat MP token bypass (#2860).** Enforce a non-empty `wechatmp_token` at config-validation time rather than relying on operators to set it.
- **No structured metrics.** Adding optional Prometheus/OTel counters (requests, tool calls, token usage per session) would help operators size and bill multi-user deployments.

---

## 14. Operational Troubleshooting

### Service won't start: `config.json not found`
Run `./run.sh` (guided) or `cp config-template.json config.json`, set `model` + key, then `cow start`.

### `Found Python 3.13, but requires 3.7-3.12`
Install Python 3.12 and re-run. `web.py` needs the removed `cgi` module. Don't force-run on 3.13+.

### Channel thread dies on startup
Check `cow logs` for the per-channel error (`[ChannelManager] Channel '<name>' startup error`). Usually a missing optional dependency (install the channel's package) or bad credentials. Other channels keep running.

### Webhook channel never receives messages
For `wechatmp`/`wechatcom_app`/`wechat_kf`/`feishu`(webhook): confirm the inbound port is published and reachable, TLS terminates at your proxy, and the platform's callback URL/token match config. For `feishu`, switching `feishu_event_mode` to `websocket` removes the inbound requirement.

### Agent runs are expensive / hit step limits
Lower `agent_max_steps`, disable `enable_thinking`, switch to `deepseek-v4-flash`, or set `agent: false` for pure chat. Each step is a model call.

### Scheduled tasks not firing after restart
The scheduler eager-starts at boot. Verify the service is running (`cow status`) and have the agent re-list tasks. Persisted scheduler sessions are trimmed by `scheduler_inject_max_per_session`.

### Web console reachable without a password
Expected default. Set `web_password` and restrict `web_host`/`web_file_serve_root` before exposing it. See [Security Analysis](#10-security-analysis).

### MCP tools not appearing
Confirm `mcp_servers` in config or `~/cow/mcp.json` is valid; the runtime that runs the server (`npx`/`uvx`) is on `PATH`; check logs for `[App] MCP warmup` / `MCP hot-reload`. Changes apply on the next message.

### High memory growth over long uptime
Per-session agents accumulate; clearing sessions (`/context clear`, or `AgentBridge.clear_all_sessions`) and the scheduler sliding-window pruning bound it. For many concurrent users, monitor process RSS and recycle periodically.

---

## 15. Quick Reference

**Entry point:** `python app.py` (or `cow start`) → `ChannelManager.start(channels, first_start=True)`
**Config file:** `./config.json` (template fallback `./config-template.json`); schema = `config.py::available_setting`
**Workspace:** `~/cow` (skills/memory/knowledge/mcp.json); secrets in `~/.cow/.env`
**Web console:** `web_port` 9899 — set `web_password`, narrow `web_file_serve_root`
**Mode switch:** `agent` true (autonomous, OS access) / false (chat)
**Model select:** `model` + provider key; `bot_type` to force; LinkAI for one-key-all
**Multi-channel:** `channel_type: ["web","feishu","telegram"]`

**Lifecycle**

```bash
cow start | stop | restart | status | logs | update | install-browser
cow skill list|search|install|uninstall|enable|disable|info
./run.sh start|stop|restart|status|logs|config|update     # PATH-free fallback
```

**Security checklist**

- [ ] `web_password` set; `web_host` = `127.0.0.1` if not proxied
- [ ] `web_file_serve_root` narrowed (never `/`)
- [ ] Running as a dedicated non-root user / container
- [ ] `wechatmp_token` non-empty on Official Account channels
- [ ] Secrets via env vars where possible; config file perms restricted
- [ ] Only trusted skills / plugins / MCP servers installed
- [ ] LinkAI cloud left disabled unless required

**Architecture in one line:** factory-instantiated channels feed a shared `ChatChannel` pipeline → `Bridge` routes to the per-session `AgentBridge` agent loop (model + tools + skills + memory + knowledge + scheduler + MCP) or to chat-mode `SessionManager`, all over an OpenAI-compatible HTTP model layer.

---

_Generated June 2026 from zhayujie/CowAgent at 2b670e7. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
