Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

CowAgent Admin & Developer Guide

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.

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
  2. System Architecture
  3. Requirements & Installation
  4. Configuration Mastery
  5. Model Provider Matrix
  6. Voice Provider Matrix
  7. Channel Deployment
  8. Agent Subsystem Internals
  9. Plugin Development
  10. Security Analysis
  11. Operations
  12. Repository Structure & Code Patterns
  13. Potential Improvements
  14. Operational Troubleshooting
  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.


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_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

# 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_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.

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_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:

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

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

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