From 772ed4cac3cdec7317c5c3c745110ce48cd41562 Mon Sep 17 00:00:00 2001 From: duomi Date: Sat, 14 Mar 2026 01:47:29 +0800 Subject: [PATCH] docs(config): cover advanced agent settings --- README.md | 63 ++++++- config/config.example.json | 160 +++++++++++++++++- docs/configuration.md | 246 ++++++++++++++++++++++++++++ docs/design/provider-refactoring.md | 20 ++- docs/tools_configuration.md | 92 ++++++++++- pkg/config/config_example_test.go | 82 ++++++++++ 6 files changed, 648 insertions(+), 15 deletions(-) create mode 100644 docs/configuration.md create mode 100644 pkg/config/config_example_test.go diff --git a/README.md b/README.md index 58cdfe323..d19e00705 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,7 @@ picoclaw onboard * [Tavily](https://tavily.com) - Optimized for AI Agents (1000 requests/month) * DuckDuckGo - Built-in fallback (no API key required) -> **Note**: See `config.example.json` for a complete configuration template. +> **Note**: See [config/config.example.json](config/config.example.json) for a complete template, and [docs/configuration.md](docs/configuration.md) for multi-agent, routing, session, channel, and tool-limit reference notes. **4. Chat** @@ -750,6 +750,8 @@ Connect Picoclaw to the Agent Social Network simply by sending a single message Config file: `~/.picoclaw/config.json` +Start from [config/config.example.json](config/config.example.json). For advanced fields that are easy to miss, see [docs/configuration.md](docs/configuration.md) and [docs/tools_configuration.md](docs/tools_configuration.md). + ### Environment Variables You can override default paths using environment variables. This is useful for portable installations, containerized deployments, or running picoclaw as a system service. These variables are independent and control different paths. @@ -1016,6 +1018,59 @@ This design also enables **multi-agent support** with flexible provider selectio - **Load balancing**: Distribute requests across multiple endpoints - **Centralized configuration**: Manage all providers in one place +#### Multi-Agent and Bindings + +Use `agents.list` as an array of agent definitions, then route specific traffic with top-level `bindings`: + +```json +{ + "agents": { + "defaults": { + "model_name": "gpt-5.4", + "routing": { + "enabled": true, + "light_model": "deepseek", + "threshold": 0.35 + } + }, + "list": [ + { + "id": "main", + "default": true, + "model": "gpt-5.4", + "subagents": { + "allow_agents": ["coder"] + } + }, + { + "id": "coder", + "workspace": "~/.picoclaw/workspace/code", + "model": { + "primary": "claude-sonnet-4.6", + "fallbacks": ["gpt-5.4"] + }, + "skills": ["Code", "git-essentials"] + } + ] + }, + "bindings": [ + { + "agent_id": "coder", + "match": { + "channel": "telegram", + "account_id": "*", + "peer": { + "kind": "direct", + "id": "123456789" + } + } + } + ] +} +``` + +The `skills` field limits which skills are visible to that agent. Tool permissions still come from the global `tools` section. For `dm_scope`, `identity_links`, binding priority, Pico/IRC channel blocks, and tool hard limits such as `web.fetch_limit_bytes`, see [docs/configuration.md](docs/configuration.md). + #### 📋 All Supported Vendors | Vendor | `model` Prefix | Default API Base | Protocol | API Key | @@ -1306,12 +1361,14 @@ picoclaw agent -m "Hello" { "agents": { "defaults": { - "model": "anthropic/claude-opus-4-5" + "model": "openrouter/anthropic/claude-sonnet-4.6" } }, "session": { "dm_scope": "per-channel-peer", - "backlog_limit": 20 + "identity_links": { + "me": ["telegram:123456789", "discord:me#1234"] + } }, "providers": { "openrouter": { diff --git a/config/config.example.json b/config/config.example.json index 094aa46df..51f6b46c3 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -3,12 +3,85 @@ "defaults": { "workspace": "~/.picoclaw/workspace", "restrict_to_workspace": true, + "allow_read_outside_workspace": false, "model_name": "gpt-5.4", + "model_fallbacks": [ + "claude-sonnet-4.6" + ], + "image_model": "gpt-5.4", + "image_model_fallbacks": [], "max_tokens": 8192, "temperature": 0.7, "max_tool_iterations": 20, "summarize_message_threshold": 20, - "summarize_token_percent": 75 + "summarize_token_percent": 75, + "max_media_size": 20971520, + "routing": { + "enabled": false, + "light_model": "deepseek", + "threshold": 0.35 + } + }, + "list": [ + { + "id": "main", + "default": true, + "name": "Main Assistant", + "model": "gpt-5.4", + "subagents": { + "allow_agents": [ + "coder" + ] + } + }, + { + "id": "coder", + "name": "Code Agent", + "workspace": "~/.picoclaw/workspace/code", + "model": { + "primary": "claude-sonnet-4.6", + "fallbacks": [ + "gpt-5.4" + ] + }, + "skills": [ + "Code", + "git-essentials" + ], + "subagents": { + "allow_agents": [ + "main" + ], + "model": { + "primary": "claude-sonnet-4.6", + "fallbacks": [ + "gpt-5.4" + ] + } + } + } + ] + }, + "bindings": [ + { + "agent_id": "coder", + "match": { + "channel": "telegram", + "account_id": "*", + "peer": { + "kind": "direct", + "id": "YOUR_TELEGRAM_USER_ID" + } + } + } + ], + "session": { + "dm_scope": "per-channel-peer", + "identity_links": { + "duomi": [ + "telegram:YOUR_TELEGRAM_USER_ID", + "discord:YOUR_DISCORD_USER_ID" + ] } }, "model_list": [ @@ -75,6 +148,13 @@ "allow_from": [ "YOUR_USER_ID" ], + "typing": { + "enabled": true + }, + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + }, "reasoning_channel_id": "" }, "discord": { @@ -85,6 +165,13 @@ "group_trigger": { "mention_only": false }, + "typing": { + "enabled": true + }, + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + }, "reasoning_channel_id": "" }, "qq": { @@ -92,6 +179,11 @@ "app_id": "YOUR_QQ_APP_ID", "app_secret": "YOUR_QQ_APP_SECRET", "allow_from": [], + "group_trigger": { + "mention_only": true + }, + "max_message_length": 2000, + "send_markdown": false, "reasoning_channel_id": "" }, "maixcam": { @@ -124,6 +216,9 @@ "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "allow_from": [], + "group_trigger": { + "mention_only": true + }, "reasoning_channel_id": "" }, "slack": { @@ -131,6 +226,16 @@ "bot_token": "xoxb-YOUR-BOT-TOKEN", "app_token": "xapp-YOUR-APP-TOKEN", "allow_from": [], + "group_trigger": { + "mention_only": true + }, + "typing": { + "enabled": true + }, + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + }, "reasoning_channel_id": "" }, "matrix": { @@ -154,8 +259,20 @@ "enabled": false, "channel_secret": "YOUR_LINE_CHANNEL_SECRET", "channel_access_token": "YOUR_LINE_CHANNEL_ACCESS_TOKEN", + "webhook_host": "0.0.0.0", + "webhook_port": 18791, "webhook_path": "/webhook/line", "allow_from": [], + "group_trigger": { + "mention_only": true + }, + "typing": { + "enabled": true + }, + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + }, "reasoning_channel_id": "" }, "onebot": { @@ -165,6 +282,18 @@ "reconnect_interval": 5, "group_trigger_prefix": [], "allow_from": [], + "group_trigger": { + "prefixes": [ + "!pico" + ] + }, + "typing": { + "enabled": false + }, + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + }, "reasoning_channel_id": "" }, "wecom": { @@ -176,6 +305,9 @@ "webhook_path": "/webhook/wecom", "allow_from": [], "reply_timeout": 5, + "group_trigger": { + "mention_only": true + }, "reasoning_channel_id": "" }, "wecom_app": { @@ -189,6 +321,9 @@ "webhook_path": "/webhook/wecom-app", "allow_from": [], "reply_timeout": 5, + "group_trigger": { + "mention_only": true + }, "reasoning_channel_id": "" }, "wecom_aibot": { @@ -201,6 +336,21 @@ "welcome_message": "Hello! I'm your AI assistant. How can I help you today?", "reasoning_channel_id": "" }, + "pico": { + "enabled": false, + "token": "YOUR_PICO_CHANNEL_TOKEN", + "allow_token_query": false, + "allow_origins": [], + "ping_interval": 30, + "read_timeout": 60, + "write_timeout": 10, + "max_connections": 100, + "allow_from": [], + "placeholder": { + "enabled": true, + "text": "Thinking... 💭" + } + }, "irc": { "enabled": false, "server": "irc.libera.chat:6697", @@ -307,6 +457,7 @@ "allow_write_paths": null, "web": { "enabled": true, + "proxy": "", "brave": { "enabled": false, "api_key": "YOUR_BRAVE_API_KEY", @@ -426,8 +577,10 @@ "exec": { "enabled": true, "enable_deny_patterns": true, + "allow_remote": true, "custom_deny_patterns": null, - "custom_allow_patterns": null + "custom_allow_patterns": null, + "timeout_seconds": 60 }, "skills": { "enabled": true, @@ -481,7 +634,8 @@ "enabled": true }, "read_file": { - "enabled": true + "enabled": true, + "max_read_file_size": 65536 }, "spawn": { "enabled": true diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 000000000..360feb302 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,246 @@ +# Configuration Reference + +`config/config.example.json` is the copy-paste template. This document is the field guide for the parts that are easy to miss when you only skim `README.md`. + +## Top-Level Layout + +| Key | Type | Default | Notes | +| --- | --- | --- | --- | +| `agents` | object | required | Agent defaults plus optional `agents.list` overrides | +| `bindings` | array | `[]` | Route specific channels/peers/accounts to specific agent IDs | +| `session` | object | `{"dm_scope":"per-channel-peer"}` | Controls DM session isolation and cross-platform identity linking | +| `model_list` | array | built-in starter list | Preferred provider/model configuration | +| `channels` | object | channel-specific defaults | Each channel stays disabled until `enabled: true` | +| `tools` | object | see [Tools Configuration](tools_configuration.md) | Global tool availability and limits | +| `heartbeat` | object | `{"enabled":true,"interval":30}` | Periodic task loop | +| `devices` | object | `{"enabled":false,"monitor_usb":true}` | USB/device integration | +| `voice` | object | `{"echo_transcription":false}` | Voice UX tweaks | +| `gateway` | object | `{"host":"127.0.0.1","port":18790}` | Shared HTTP server for webhook channels | + +## Agent Defaults + +`agents.defaults` sets the baseline for every agent instance. Entries in `agents.list` override these fields per agent. + +| Field | Type | Default | Notes | +| --- | --- | --- | --- | +| `workspace` | string | `~/.picoclaw/workspace` | Base workspace for the implicit main agent | +| `restrict_to_workspace` | bool | `true` | Restricts file and exec tools to the workspace boundary | +| `allow_read_outside_workspace` | bool | `false` | Lets read-only tools escape the workspace while writes stay restricted | +| `model_name` | string | empty | Preferred alias from `model_list` | +| `model` | string | empty | Deprecated alias for `model_name`; still accepted | +| `model_fallbacks` | array | `[]` | Fallback aliases from `model_list` | +| `image_model` | string | empty | Separate model alias for image tasks | +| `image_model_fallbacks` | array | `[]` | Image-model fallback aliases | +| `max_tokens` | int | `32768` | Agent response budget | +| `temperature` | number or null | provider default | `null` means do not override provider defaults | +| `max_tool_iterations` | int | `50` | Upper bound on tool loop turns | +| `summarize_message_threshold` | int | `20` | Start compressing history after this many messages | +| `summarize_token_percent` | int | `75` | Compression target relative to current context | +| `max_media_size` | int | `20971520` | Per-attachment cap in bytes (20 MB) | +| `routing` | object | disabled | Lightweight model-routing classifier for cheap/simple turns | + +### Routing + +When `agents.defaults.routing.enabled` is `true`, PicoClaw scores each incoming message. Messages below `threshold` are sent to `light_model`; everything else stays on the agent's primary model. + +| Field | Type | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | bool | `false` | Turns routing on/off | +| `light_model` | string | empty | Alias from `model_list` used for simpler turns | +| `threshold` | number | `0` | Score in `[0,1]`; lower scores are considered simpler | + +## Multi-Agent Setup + +`agents.list` is an array, not a map. Each entry creates a concrete agent instance. + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `id` | string | yes | Stable agent ID used by bindings and subagent permissions | +| `default` | bool | no | Marks the default agent; otherwise the first list entry wins | +| `name` | string | no | Human-readable label | +| `workspace` | string | no | Per-agent workspace override | +| `model` | string or object | no | Either `"gpt-5.4"` or `{"primary":"gpt-5.4","fallbacks":["claude-sonnet-4.6"]}` | +| `skills` | array | no | Limits which `SKILL.md` entries are exposed to this agent | +| `subagents` | object | no | Controls which agents this agent may spawn | + +### Subagent Rules + +| Field | Type | Notes | +| --- | --- | --- | +| `allow_agents` | array | Agent IDs this agent may spawn, or `"*"` to allow every configured agent | +| `model` | string or object | Override the model/fallback set used when this agent spawns a subagent | + +### Example + +```json +{ + "agents": { + "defaults": { + "model_name": "gpt-5.4", + "routing": { + "enabled": true, + "light_model": "deepseek", + "threshold": 0.35 + } + }, + "list": [ + { + "id": "main", + "default": true, + "model": "gpt-5.4", + "subagents": { + "allow_agents": ["coder"] + } + }, + { + "id": "coder", + "workspace": "~/.picoclaw/workspace/code", + "model": { + "primary": "claude-sonnet-4.6", + "fallbacks": ["gpt-5.4"] + }, + "skills": ["Code", "git-essentials"] + } + ] + }, + "bindings": [ + { + "agent_id": "coder", + "match": { + "channel": "telegram", + "account_id": "*", + "peer": { + "kind": "direct", + "id": "123456789" + } + } + } + ] +} +``` + +`skills` only filters visible skills. Tool permissions still come from the global `tools.*` config and the workspace sandbox. + +## Bindings + +Bindings route inbound traffic to an agent before any tool execution starts. + +| Field | Type | Notes | +| --- | --- | --- | +| `agent_id` | string | Target agent ID | +| `match.channel` | string | Required channel name such as `telegram`, `discord`, `feishu` | +| `match.account_id` | string | Optional multi-account filter; use `*` for any account | +| `match.peer.kind` | string | Optional peer type: `direct`, `group`, or `channel` | +| `match.peer.id` | string | Optional peer ID | +| `match.guild_id` | string | Optional Discord guild match | +| `match.team_id` | string | Optional Slack team match | + +Binding priority is fixed in code: + +1. `peer` +2. `parent_peer` +3. `guild_id` +4. `team_id` +5. `account_id` +6. channel-wide wildcard (`account_id: "*"`) +7. default agent + +## Session Management + +### DM Scope + +`session.dm_scope` only affects direct messages. Group and channel peers always keep their own per-peer session keys. + +| Value | Behavior | +| --- | --- | +| `main` | Reuse a single shared main session | +| `per-peer` | One DM session per person across all channels | +| `per-channel-peer` | One DM session per channel/person pair | +| `per-account-channel-peer` | One DM session per account + channel + person pair | + +### Identity Links + +`session.identity_links` collapses multiple platform IDs into one canonical identity when DM scope is not `main`. + +```json +{ + "session": { + "dm_scope": "per-peer", + "identity_links": { + "duomi": [ + "telegram:123456789", + "discord:duomi#1234", + "feishu:ou_xxx" + ] + } + } +} +``` + +If a DM arrives from any listed identity, PicoClaw reuses the canonical session key (`duomi` in this example). + +## Channels + +Channel docs under `docs/channels/` remain the setup guide for tokens and platform-specific steps. The table below focuses on shared runtime fields that were missing from the central docs. + +### Shared Channel Fields + +| Field | Applies To | Notes | +| --- | --- | --- | +| `allow_from` | most channels | Whitelist of users, groups, or room IDs allowed to talk to the bot | +| `group_trigger.mention_only` | group-capable channels | Reply only when mentioned | +| `group_trigger.prefixes` | prefix-based channels | Reply when a message starts with one of the configured prefixes | +| `typing.enabled` | Telegram, Discord, Slack, LINE, OneBot, IRC | Enables typing indicators where supported | +| `placeholder.enabled` | Telegram, Feishu, Discord, Matrix, LINE, OneBot, Pico, Slack | Send a quick placeholder response before the final answer | +| `placeholder.text` | same as above | Placeholder message text | +| `reasoning_channel_id` | supported channels | Redirect long reasoning traces to a separate channel/room | + +### Less-Documented Channel Blocks + +| Channel | Field | Default | Notes | +| --- | --- | --- | --- | +| `qq` | `max_message_length` | `2000` | Hard cap before QQ responses are split | +| `qq` | `send_markdown` | `false` | Prefer QQ markdown cards when supported | +| `matrix` | `device_id` | empty | Optional device identifier for login/session reuse | +| `matrix` | `join_on_invite` | `true` | Auto-join rooms when invited | +| `matrix` | `message_format` | empty | Rendering mode override | +| `maixcam` | `host` / `port` | `0.0.0.0` / `18790` | TCP endpoint PicoClaw listens on for MaixCam clients | +| `wecom` / `wecom_app` / `wecom_aibot` | `reply_timeout` | `5` | Timeout in seconds before falling back to async behavior | +| `wecom_aibot` | `max_steps` | `10` | Maximum streaming steps per response | +| `wecom_aibot` | `welcome_message` | built-in greeting | Sent on `enter_chat`; empty disables it | +| `pico` | `allow_token_query` | `false` | Accept auth token in query string instead of headers | +| `pico` | `allow_origins` | `[]` | CORS allow-list for browser clients | +| `pico` | `ping_interval` | `30` | WebSocket ping interval in seconds | +| `pico` | `read_timeout` | `60` | Max read wait in seconds | +| `pico` | `write_timeout` | `10` | Max write wait in seconds | +| `pico` | `max_connections` | `100` | Concurrent connection ceiling | +| `irc` | `request_caps` | `["server-time","message-tags"]` in example | Requested IRC capabilities | +| `irc` | `typing.enabled` | `false` | Whether to emit typing indicators into IRC | + +## Tool Limits and Escape Hatches + +See [Tools Configuration](tools_configuration.md) for full per-tool examples. These are the fields most likely to affect safety or operational limits: + +| Field | Default | Notes | +| --- | --- | --- | +| `tools.allow_read_paths` | `null` | Regex allow-list applied on top of the workspace sandbox for read-only tools | +| `tools.allow_write_paths` | `null` | Regex allow-list for write operations | +| `tools.web.proxy` | empty | Optional proxy for web search/fetch | +| `tools.web.fetch_limit_bytes` | `10485760` | Max bytes fetched by web tools before truncation/refusal | +| `tools.exec.allow_remote` | `true` | Allows exec from remote channels; set `false` to require internal channels only | +| `tools.exec.custom_allow_patterns` | `null` | Regex allow-list evaluated before deny rules | +| `tools.exec.custom_deny_patterns` | `null` | Extra regex deny rules | +| `tools.exec.timeout_seconds` | `60` | Per-command timeout | +| `tools.read_file.max_read_file_size` | `65536` | Max bytes returned by the read-file tool | +| `tools.media_cleanup.max_age_minutes` | `30` | Delete old media files after this many minutes | +| `tools.media_cleanup.interval_minutes` | `5` | Cleanup sweep interval | +| `tools.skills.max_concurrent_searches` | `2` | Limits parallel skill searches/download lookups | +| `tools.skills.search_cache.max_size` | `50` | Skill search cache size | +| `tools.skills.search_cache.ttl_seconds` | `300` | Skill search cache TTL | +| `tools.mcp.discovery.ttl` | `5` | How many turns a discovered MCP tool remains unlocked | + +## See Also + +- [config/config.example.json](../config/config.example.json) +- [Tools Configuration](tools_configuration.md) +- [Model-list Migration Guide](migration/model-list-migration.md) diff --git a/docs/design/provider-refactoring.md b/docs/design/provider-refactoring.md index 38f379c50..3bdaefce2 100644 --- a/docs/design/provider-refactoring.md +++ b/docs/design/provider-refactoring.md @@ -179,18 +179,22 @@ Identify protocol via prefix in `model` field: "defaults": { "model": "deepseek-chat" }, - "coder": { - "model": "gpt-5.4", - "system_prompt": "You are a coding assistant..." - }, - "translator": { - "model": "claude-sonnet-4.6" - } + "list": [ + { + "id": "coder", + "default": true, + "model": "gpt-5.4" + }, + { + "id": "translator", + "model": "claude-sonnet-4.6" + } + ] } } ``` -Each Agent only needs to specify `model` (corresponds to `model_name` in `model_list`). +Each agent only needs to specify `model` (corresponds to `model_name` in `model_list`). ### 3.3 Industry Comparison diff --git a/docs/tools_configuration.md b/docs/tools_configuration.md index 8c8eb31f0..b353d2ac9 100644 --- a/docs/tools_configuration.md +++ b/docs/tools_configuration.md @@ -1,6 +1,14 @@ # Tools Configuration -PicoClaw's tools configuration is located in the `tools` field of `config.json`. +PicoClaw's tools configuration lives under the `tools` field in `config.json`. +For agent/session/channel fields outside the tool system, see [configuration.md](configuration.md). + +## Top-Level Tool Gates + +| Config | Type | Default | Description | +| --- | --- | --- | --- | +| `allow_read_paths` | array or null | `null` | Regex allow-list layered on top of the workspace sandbox for read-only tools | +| `allow_write_paths` | array or null | `null` | Regex allow-list for write-capable tools | ## Directory Structure @@ -30,6 +38,14 @@ PicoClaw's tools configuration is located in the `tools` field of `config.json`. Web tools are used for web search and fetching. +### Global Web Config + +| Config | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | bool | true | Enable/disable the entire web tool family | +| `proxy` | string | `""` | Optional proxy URL for web search/fetch | +| `fetch_limit_bytes` | int | `10485760` | Maximum response size fetched by web tools before truncation/refusal | + ### Brave | Config | Type | Default | Description | @@ -53,19 +69,53 @@ Web tools are used for web search and fetching. | `api_key` | string | - | Perplexity API key | | `max_results` | int | 5 | Maximum number of results | +### Tavily + +| Config | Type | Default | Description | +|---------------|--------|---------|---------------------------| +| `enabled` | bool | false | Enable Tavily search | +| `api_key` | string | - | Tavily API key | +| `base_url` | string | `""` | Optional Tavily-compatible endpoint | +| `max_results` | int | 5 | Maximum number of results | + +### SearXNG + +| Config | Type | Default | Description | +|---------------|--------|---------|------------------------------| +| `enabled` | bool | false | Enable SearXNG search | +| `base_url` | string | `""` | Base URL of your SearXNG instance | +| `max_results` | int | 5 | Maximum number of results | + +### GLM Search + +| Config | Type | Default | Description | +|-----------------|--------|---------|-------------| +| `enabled` | bool | false | Enable Zhipu GLM web search | +| `api_key` | string | - | GLM API key | +| `base_url` | string | `https://open.bigmodel.cn/api/paas/v4/web_search` | Search endpoint | +| `search_engine` | string | `search_std` | Backend name such as `search_std`, `search_pro`, `search_pro_sogou`, `search_pro_quark` | +| `max_results` | int | 5 | Maximum number of results | + ## Exec Tool The exec tool is used to execute shell commands. | Config | Type | Default | Description | |------------------------|-------|---------|--------------------------------------------| +| `enabled` | bool | true | Enable/disable the exec tool | | `enable_deny_patterns` | bool | true | Enable default dangerous command blocking | +| `allow_remote` | bool | true | Allow exec calls from remote channels | +| `timeout_seconds` | int | 60 | Per-command timeout (`0` uses the built-in default) | | `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) | +| `custom_allow_patterns` | array | [] | Regex allow-list checked before deny rules | ### Functionality - **`enable_deny_patterns`**: Set to `false` to completely disable the default dangerous command blocking patterns +- **`allow_remote`**: Set to `false` to require an internal/local channel context before exec may run +- **`timeout_seconds`**: Set to `0` to fall back to the compiled default timeout - **`custom_deny_patterns`**: Add custom deny regex patterns; commands matching these will be blocked +- **`custom_allow_patterns`**: Explicit allow regexes that are evaluated before deny rules ### Default Blocked Command Patterns @@ -106,8 +156,48 @@ The cron tool is used for scheduling periodic tasks. | Config | Type | Default | Description | |------------------------|------|---------|------------------------------------------------| +| `enabled` | bool | true | Enable/disable the cron tool | | `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit | +## Skills Tool + +The skills tool controls skill search, installation, and registry lookup. + +| Config | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | bool | true | Enable skill-related tools | +| `max_concurrent_searches` | int | 2 | Max concurrent skill search/download lookups | +| `github.proxy` | string | `""` | Optional proxy for GitHub-backed skill downloads | +| `github.token` | string | `""` | GitHub token for higher rate limits/private repos | +| `search_cache.max_size` | int | 50 | Number of cached skill-search results | +| `search_cache.ttl_seconds` | int | 300 | Skill-search cache TTL | +| `registries.clawhub.enabled` | bool | true | Enable the default ClawHub registry | +| `registries.clawhub.base_url` | string | `https://clawhub.ai` | Registry base URL | +| `registries.clawhub.auth_token` | string | `""` | Optional registry auth token | +| `registries.clawhub.search_path` | string | `""` | Override search endpoint path | +| `registries.clawhub.skills_path` | string | `""` | Override skill metadata endpoint path | +| `registries.clawhub.download_path` | string | `""` | Override download endpoint path | +| `registries.clawhub.timeout` | int | `0` | Custom timeout override (`0` means use default) | +| `registries.clawhub.max_zip_size` | int | `0` | Custom zip size cap (`0` means use default) | +| `registries.clawhub.max_response_size` | int | `0` | Custom response size cap (`0` means use default) | + +## Media Cleanup Tool + +Media cleanup deletes temporary media artifacts created during channel/tool use. + +| Config | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | bool | true | Enable background cleanup | +| `max_age_minutes` | int | 30 | Delete media older than this | +| `interval_minutes` | int | 5 | Cleanup sweep cadence | + +## Read File Tool + +| Config | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | bool | true | Enable the read-file tool | +| `max_read_file_size` | int | 65536 | Maximum bytes returned by a single read | + ## MCP Tool The MCP tool enables integration with external Model Context Protocol servers. diff --git a/pkg/config/config_example_test.go b/pkg/config/config_example_test.go new file mode 100644 index 000000000..886914cdf --- /dev/null +++ b/pkg/config/config_example_test.go @@ -0,0 +1,82 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestConfigExample_DocumentsAdvancedFields(t *testing.T) { + examplePath := filepath.Join("..", "..", "config", "config.example.json") + data, err := os.ReadFile(examplePath) + if err != nil { + t.Fatalf("read config example: %v", err) + } + + var root map[string]any + if err := json.Unmarshal(data, &root); err != nil { + t.Fatalf("unmarshal config example: %v", err) + } + + requiredPaths := [][]any{ + {"agents", "defaults", "allow_read_outside_workspace"}, + {"agents", "defaults", "image_model"}, + {"agents", "defaults", "image_model_fallbacks"}, + {"agents", "defaults", "max_media_size"}, + {"agents", "defaults", "routing", "light_model"}, + {"agents", "list", 0, "subagents", "allow_agents"}, + {"bindings", 0, "match", "peer", "kind"}, + {"session", "identity_links"}, + {"channels", "telegram", "typing", "enabled"}, + {"channels", "telegram", "placeholder", "text"}, + {"channels", "qq", "max_message_length"}, + {"channels", "qq", "send_markdown"}, + {"channels", "dingtalk", "group_trigger", "mention_only"}, + {"channels", "slack", "typing", "enabled"}, + {"channels", "line", "typing", "enabled"}, + {"channels", "onebot", "group_trigger", "prefixes"}, + {"channels", "pico", "allow_token_query"}, + {"channels", "pico", "max_connections"}, + {"tools", "web", "proxy"}, + {"tools", "web", "fetch_limit_bytes"}, + {"tools", "exec", "allow_remote"}, + {"tools", "exec", "custom_allow_patterns"}, + {"tools", "exec", "timeout_seconds"}, + {"tools", "media_cleanup", "max_age_minutes"}, + {"tools", "read_file", "max_read_file_size"}, + } + + for _, path := range requiredPaths { + if _, ok := lookupPath(root, path...); !ok { + t.Errorf("config example missing documented path %v", path) + } + } +} + +func lookupPath(root any, parts ...any) (any, bool) { + current := root + for _, part := range parts { + switch key := part.(type) { + case string: + obj, ok := current.(map[string]any) + if !ok { + return nil, false + } + next, ok := obj[key] + if !ok { + return nil, false + } + current = next + case int: + list, ok := current.([]any) + if !ok || key < 0 || key >= len(list) { + return nil, false + } + current = list[key] + default: + return nil, false + } + } + return current, true +}