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
- Executive Summary
- System Architecture
- Requirements & Installation
- Configuration Mastery
- Model Provider Matrix
- Voice Provider Matrix
- Channel Deployment
- Agent Subsystem Internals
- Plugin Development
- Security Analysis
- Operations
- Repository Structure & Code Patterns
- Potential Improvements
- Operational Troubleshooting
- 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.
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
┌──────────────────────────────────────────┐
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_typeis 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-workerThreadPoolExecutor. Admin commands (#…) jump the queue;/cancelbypasses 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
# 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.
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_port8080,wechatcomapp_port9898, 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
config.jsonis parsed. Duplicate top-level keys are deep-merged (lists concatenate, dicts merge) rather than silently dropped, and a warning is logged.- Legacy singular sections
tool/skillare folded into the canonical pluraltools/skills. - Environment variables override config: any env var whose lowercased name matches a key in
available_settingreplaces it (values areeval'd, withtrue/falsecoerced to booleans). This is how secret-free deployments (e.g. Railway) inject API keys. - 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. - Per-skill config (
skills.<name>.<key>) is flattened toSKILL_<NAME>_<KEY>env vars.
Note
Secrets are masked in logs by
drag_sensitive()— any key containingkeyorsecretis shown asabc*****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.
4.6 Model / provider credential keys
Each provider has an API-key (and usually api-base) field. The most common:
{
"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_linkaiis true andcloud_deployment_id(or envCLOUD_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 incommon/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 |
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
Agentinstance persession_id(self.agentsmap); a default agent for session-less calls. - Agent construction —
create_agent()wires anAgentLLMModel(wrappingbot_factory), the full tool set (ToolManager), theSkillManager, and thememory_manager, withmax_steps,max_context_tokens, and workspace. - Cancellation — a cancel registry keyed by per-turn
request_id(web) orsession_id(IM); polled at safe checkpoints; driven by/cancel. - Thinking mode — passes
thinking/reasoning_effortkwargs whenenable_thinkingis set. - MCP hot-reload — after each reply, a background thread diffs
~/cow/mcp.jsonand 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 anAgentEventHandler, 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:
~/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
# 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/.envfor the agent runtime.- Skill secrets are intentionally kept in
~/.cow/.env, separate from the~/cowworkspace 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
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:
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
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-globalconfig. - 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/i18nresolvescow_lang; user-facing strings use at(zh, en)helper (also inrun.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.pyis ~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_passwordempty andweb_file_serve_root=~are convenient but unsafe if exposed. Consider warning loudly (or refusing to bind a non-loopbackweb_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/cgidependency caps supported Python at 3.12; migrating the web layer offweb.pywould unblock newer runtimes. - WeChat MP token bypass (#2860). Enforce a non-empty
wechatmp_tokenat 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.
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
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_passwordset;web_host=127.0.0.1if not proxied - [ ]
web_file_serve_rootnarrowed (never/) - [ ] Running as a dedicated non-root user / container
- [ ]
wechatmp_tokennon-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.