tool_search activates deferred MCP tools into ActivatedToolSet at runtime.
When tool_search runs in parallel with the tools it activates, a race
condition occurs where tool lookups happen before activation completes,
resulting in "Unknown tool" errors.
Force sequential execution in should_execute_tools_in_parallel() whenever
tool_search is present in the tool call batch.
Co-authored-by: Claude Code (claude-opus-4-6) <noreply@anthropic.com>
In Compact (MetadataOnly) mode, skill tools were omitted from the system
prompt alongside instructions. This meant the LLM had no visibility into
TOML-defined tools when running in Compact mode, defeating the primary
advantage of TOML skills over MD skills (structured tool metadata).
Now Compact mode skips only instructions (loaded on demand via
read_skill) while still inlining tool definitions so the LLM knows
which skill tools are available.
Closes#3702
Swap auto_save and load_context order to avoid retrieving the just-stored
user message in memory recall.
Before: load_context → auto_save (could retrieve own message)
After: auto_save → load_context (query first, store after)
This fixes the issue where "[Memory context]\n- user_msg: <current>\n<current>"
would appear, causing the user's input to be duplicated.
* feat: add Jira tool with get_ticket, search_tickets, and comment_ticket
Implements a new `jira` tool following the existing zeroclaw tool
conventions (Tool trait, SecurityPolicy, config-gated registration).
- get_ticket: configurable detail level (basic/basic_search/full/changelog)
with response shaping; always in the default allowed_actions list
- search_tickets: JQL-based search with cursor pagination (nextPageToken);
always returns basic_search shape; gated by allowed_actions
- comment_ticket: posts ADF comments with inline markdown-like syntax —
@email mentions resolved to Jira accountId, **bold**, bullet lists,
newlines; gated by allowed_actions and SecurityPolicy Act operation
Config: [jira] section with base_url, email, api_token (encrypted at
rest, falls back to JIRA_API_TOKEN env var), allowed_actions (default:
["get_ticket"]), and timeout_secs. Validated on load.
Tool description in tool_descriptions/en.toml documents all three
actions and the full comment syntax for the AI system prompt.
* fix: address jira tool code review findings
High priority:
- Validate issue_key against ^[A-Z][A-Z0-9]+-\d+$ before URL interpolation
to prevent path traversal in get_ticket and comment_ticket
Medium priority:
- Add email guard in tool registration (mod.rs) to skip with a warning
instead of registering a broken tool when jira.email is empty
- Shape comment_ticket response to return only id, author, created —
avoids exposing internal Jira metadata to the AI
- Replace O(n²) comment matching in shape_basic with a HashMap lookup
keyed by comment ID for O(1) access
- Add api_token validation in Config::validate() checking both the
config field and JIRA_API_TOKEN env var when jira.enabled = true
Low priority:
- Make shape_basic_search private (was accidentally pub)
- Extend clean_email to strip leading punctuation (( and [) so that
@(john@co.com) resolves correctly; fix suffix computation via pointer
arithmetic to handle the shifted offset
- Clarify tool_descriptions/en.toml: @prefix is required for mentions,
bare emails without @ are treated as plain text
- Handle unmatched ** in parse_inline: emit as literal text instead of
silently producing a bold node with no closing marker
* fix(jira): allow lowercase project keys in issue_key validation
Relax validate_issue_key to accept both PROJ-123 and proj-123, since
some Jira instances use lowercase custom project keys. Path traversal
protection is preserved via alphanumeric + digit-number requirement.
* feat(tools): add tool honesty instructions to system prompt
Prevent AI from fabricating tool results by injecting a CRITICAL:
Tool Honesty section into both channel and CLI/agent system prompts.
Rules: never fabricate or guess tool results, report errors as-is,
and ask the user when unsure if a tool call succeeded.
* style: sort JiraConfig import alphabetically in config/mod.rs
* style(jira): fix strict clippy lints in jira_tool
- Derive Default for LevelOfDetails instead of manual impl
- Use char arrays in trim_start_matches/trim_end_matches
- Allow cast_possible_truncation on search_tickets (usize->u32 bounded by max_results)
- Remove needless borrow on &email
* fix(ci): adapt to upstream autonomy_level additions in channels/mod.rs
- Add missing autonomy_level argument to build_system_prompt_with_mode call in test
- Add missing autonomy_level field in ChannelRuntimeContext test initializer
- Allow large_futures in load_or_init test (Config struct growth from JiraConfig)
* fix(ci): resolve duplicate and missing autonomy_level in test initializers
* fix(ci): use TelegramRecordingChannel in telegram-specific test
The test process_channel_message_executes_tool_calls_instead_of_sending_raw_json
sent messages on channel "telegram" but registered RecordingChannel (name:
"test-channel"), causing the channel lookup to return None and no messages to
be sent.
* fix(jira): prevent panics on short dates, fix dedup bug, normalize base_url
- Add date_prefix() helper to safely slice date strings instead of
panicking on empty or short strings from the Jira API.
- Replace Vec::dedup() with HashSet-based retain in extract_emails()
to correctly deduplicate non-adjacent duplicates.
- Strip trailing slashes from base_url during construction to prevent
double-slash URLs.
- Add tests for date_prefix and non-adjacent email dedup.
---------
Co-authored-by: Anatolii <anatolii@Anatoliis-MacBook.local>
Co-authored-by: Anatolii <anatolii.fesiuk@gmail.com>
Preserve assistant text from native tool-call turns in draft updates. Falls back to response_text when parsed_text is empty and native tool calls are present. Relays text through on_delta for draft-capable channels like Telegram.
Supersedes #3976. Closes#3974
Inject a human-readable summary of the active SecurityPolicy into the system prompt Safety section. LLM sees allowed commands, forbidden paths, autonomy level, and rate limits.
Supersedes #3968. Closes#2404
Resolve conflict in src/channels/mod.rs Safety section. Keeps the
PR's AutonomyConfig-based prompt construction (build_system_prompt_with_mode_and_autonomy)
while incorporating master's granular safety rules (conditional
destructive-command and ask-before-acting lines based on autonomy level).
Also fixes missing autonomy_level arg in refresh-skills test and removes
duplicate autonomy.level args from auto-merged call sites.
- Channel tool filtering (`non_cli_excluded_tools`) now respects
`autonomy.level = "full"` — full-autonomy agents keep all tools
available regardless of channel.
- Gateway `process_message` now creates and passes an `ApprovalManager`
to `agent_turn`, so `ReadOnly`/`Supervised` policies are enforced
instead of silently skipped.
- Gateway also applies `non_cli_excluded_tools` filtering with the same
full-autonomy bypass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When autonomy.level is set to "full", the channel/web system prompt no
longer includes instructions telling the model to ask for permission
before executing tools. Previously these safety lines were hardcoded
regardless of autonomy config, causing the LLM to simulate approval
dialogs in channel and web-interface modes even though the
ApprovalManager correctly allowed execution.
The fix adds an autonomy_level parameter to build_system_prompt_with_mode
and conditionally omits the "ask before acting" instructions when the
level is Full. Core safety rules (no data exfiltration, prefer trash)
are always included.
Add a locale-aware tool description system that loads translations from
TOML files in tool_descriptions/. This enables non-English users to see
tool descriptions in their language.
- Add src/i18n.rs module with ToolDescriptions loader, locale detection
(ZEROCLAW_LOCALE, LANG, LC_ALL env vars), and English fallback chain
- Add locale config field to Config struct for explicit locale override
- Create tool_descriptions/en.toml with all 47 tool descriptions
- Create tool_descriptions/zh-CN.toml with Chinese translations
- Integrate with ToolsSection::build() and build_tool_instructions()
to resolve descriptions from locale files before hardcoded fallback
- Add PromptContext.tool_descriptions field for prompt-time resolution
- Add AgentBuilder.tool_descriptions() setter for Agent construction
- Include tool_descriptions/ in Cargo.toml package include list
- Add 8 unit tests covering locale loading, fallback chains, env
detection, and config override
Closes#3901
The seen_tool_signatures HashSet was initialized outside the iteration loop, causing cross-iteration deduplication of legitimate tool calls. This triggered a self-correction spiral where the agent repeatedly attempted skipped calls until hitting max_iterations.
Moving the HashSet inside the loop ensures deduplication only applies within a single iteration, as originally intended.
Fixes#3798
Add support for switching AI models at runtime during a conversation.
The model_switch tool allows users to:
- Get current model state
- List available providers
- List models for a provider
- Switch to a different model
The switch takes effect immediately for the current conversation by
recreating the provider with the new model after tool execution.
Risk: Medium - internal state changes and provider recreation
Gateway WebSocket chat sessions were in-memory only — conversation
history was lost on gateway restart, macOS sleep/wake, or client
reconnect. This wires up the existing SessionBackend (SQLite) to
the gateway WS handler so sessions survive restarts and reconnections.
Changes:
- Add delete_session() to SessionBackend trait + SQLite implementation
- Add session_persistence and session_ttl_hours to GatewayConfig
- Add Agent::seed_history() to hydrate agent from persisted messages
- Initialize SqliteSessionBackend in run_gateway() when enabled
- Send session_start message on WS connect with session_id + resumed
- Persist user/assistant messages after each turn
- Add GET /api/sessions and DELETE /api/sessions/{id} REST endpoints
- Bump version to 0.5.0
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(runtime): add configurable reasoning effort
* fix(test): add missing reasoning_effort field in live test
Add reasoning_effort: None to ProviderRuntimeOptions construction in
openai_codex_vision_e2e.rs to fix E0063 compile error.
---------
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
* fix(tools): wire ActivatedToolSet into tool dispatch and spec advertisement
When deferred MCP tools are activated via tool_search, they are stored
in ActivatedToolSet but never consulted by the tool call loop.
tool_specs is built once before the iteration loop and never refreshed,
so the provider API tools[] parameter never includes activated tools.
find_tool only searches the static registry, so execution dispatch also
fails silently.
Thread Arc<Mutex<ActivatedToolSet>> from creation sites through to
run_tool_call_loop. Rebuild tool_specs each iteration to merge base
registry specs with activated specs. Add fallback in execute_one_tool
to check the activated set when the static registry lookup misses.
Change ActivatedToolSet internal storage from Box<dyn Tool> to
Arc<dyn Tool> so we can clone the Arc out of the mutex guard before
awaiting tool.execute() (std::sync::MutexGuard is not Send).
* fix(tools): add activated_tools field to new ChannelRuntimeContext test site
Remove duplicate tool listing from XmlToolDispatcher::prompt_instructions()
since tool listing is already handled by ToolsSection in prompt.rs. The
method now only emits the XML protocol envelope.
Also fix UTF-8 char boundary panics in memory consolidation truncation by
using char_indices() instead of manual byte-boundary scanning.
Fixes#3643
Supersedes #3678
Co-authored-by: TJUEZ <TJUEZ@users.noreply.github.com>
* fix(agent): remove bare URL → curl fallback in GLM-style tool call parser
The `parse_glm_style_tool_calls` function had a "Plain URL" fallback
that converted any bare URL line (e.g. `https://example.com`) into a
`shell` tool call running `curl -s '<url>'`. This caused:
- False positives: normal URLs in LLM replies misinterpreted as tool calls
- Swallowed replies: text with URLs not forwarded to the channel
- Unintended shell commands: `curl` executed without user intent
Explicit GLM-format tool calls like `browser_open/url>https://...` and
`shell/command>...` are unaffected — only the bare URL catch-all is
removed.
* style: cargo fmt
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Add an optional `allowed_tools` parameter that restricts which tools are
available to the agent. When `Some(list)`, only tools whose name appears
in the list are retained; when `None`, all tools remain available
(backward compatible). This enables fine-grained capability control for
cron jobs, heartbeat tasks, and CLI invocations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Channel-driven runs (Telegram, Matrix, Discord, etc.) previously bypassed
the ApprovalManager entirely — `None` was passed into the tool-call loop,
so `auto_approve`, `always_ask`, and supervised approval checks were
silently skipped for all non-CLI execution paths.
Add a non-interactive mode to ApprovalManager that enforces the same
autonomy config policies but auto-denies tools requiring interactive
approval (since no operator is present on channel runs). Specifically:
- Add `ApprovalManager::for_non_interactive()` constructor that creates
a manager which auto-denies tools needing approval instead of prompting
- Add `is_non_interactive()` method so the tool-call loop can distinguish
interactive (CLI prompt) from non-interactive (auto-deny) managers
- Update tool-call loop: non-interactive managers auto-deny instead of
the previous auto-approve behavior for non-CLI channels
- Wire the non-interactive approval manager into ChannelRuntimeContext
so channel runs enforce the full approval policy
- Add 8 tests covering non-interactive approval behavior
Security implications:
- `always_ask` tools are now denied on channels (previously bypassed)
- Supervised-mode unknown tools are now denied on channels (previously
bypassed)
- `auto_approve` tools continue to work on channels unchanged
- `full` autonomy mode is unaffected (no approval needed regardless)
- `read_only` mode is unaffected (blocks execution elsewhere)
Closes#3487
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a tool call fails (security policy block, hook cancellation, user
denial, or execution error), the failure reason is now included in the
progress message sent to the chat channel via on_delta. Previously only
a ❌ icon was shown; now users see the actual reason (e.g. "Command not
allowed by security policy") without needing to check `zeroclaw doctor
traces`.
Closes#3628
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive long-running context upgrades:
- Token-based compaction: replace message-count trigger with token
estimation (~4 chars/token). Compaction fires when estimated tokens
exceed max_context_tokens (default 32K) OR message count exceeds
max_history_messages. Cuts at user-turn boundaries only.
- Persistent sessions: JSONL append-only session files per channel
sender in {workspace}/sessions/. Sessions survive daemon restarts.
Hydrates in-memory history from disk on startup.
- LLM-driven memory consolidation: two-phase extraction after each
conversation turn. Phase 1 writes a timestamped history entry (Daily).
Phase 2 extracts new facts/preferences to Core memory (if any).
Replaces raw message auto-save with semantic extraction.
- New config fields: agent.max_context_tokens (32000),
channels_config.session_persistence (true).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The system prompt has no documentation of channel media markers
([Voice], [IMAGE:], [Document:]), causing the LLM to misinterpret
transcribed voice messages as unprocessable audio attachments instead
of responding to the transcribed text content.
Re-applies fix from merged dev PR #1697 which was lost during the
master branch migration.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* feat(provider): support custom API path suffix for custom: endpoints
Allow users to configure a custom API path for custom/compatible
providers instead of hardcoding /v1/chat/completions. Some self-hosted
LLM servers use different API paths.
Adds an optional `api_path` field to:
- Config (top-level and model_providers profile)
- ProviderRuntimeOptions
- OpenAiCompatibleProvider
When set, the custom path is appended to base_url instead of the
default /chat/completions suffix.
Closes#3125
* fix: add missing api_path field to test ModelProviderConfig initializers
Add deferred MCP tool activation to reduce context window waste.
When mcp.deferred_loading is true (the default), MCP tool schemas
are not eagerly included in the LLM context. Instead, only tool
names appear in an <available-deferred-tools> system prompt section,
and the LLM calls the built-in tool_search tool to fetch full schemas
on demand. Setting deferred_loading to false preserves the existing
eager behavior.
Closes#3095
MCP tools were not visible to delegate subagents because parent_tools
was a static snapshot taken before MCP tool wiring. Switch to interior
mutability (parking_lot::RwLock) so MCP wrappers pushed after
DelegateTool construction are visible at sub-agent execution time.
Closes#3069
* feat(agent): add tool_filter_groups for per-turn MCP tool schema filtering
Introduces per-turn MCP tool schema filtering to reduce token overhead when
many MCP tools are registered. Filtering is driven by a new config field
`agent.tool_filter_groups`, which is a list of named groups that each
specify tool glob patterns and an activation mode (`always` or `dynamic`).
Built-in (non-MCP) tools always pass through unchanged; the feature is fully
backward-compatible — an empty `tool_filter_groups` list (the default) leaves
all existing behaviour untouched.
Changes:
- src/config/schema.rs: add `ToolFilterGroupMode`, `ToolFilterGroup` types
and `tool_filter_groups` field on `AgentConfig`
- src/config/mod.rs: re-export `ToolFilterGroup`, `ToolFilterGroupMode`
- src/agent/loop_.rs: add `glob_match()`, `filter_tool_specs_for_turn()`,
`compute_excluded_mcp_tools()` helpers; wire call sites in both single-shot
and interactive REPL modes; add unit tests for all three functions
- docs/reference/api/config-reference.md: document `tool_filter_groups`
field and sub-table schema with example
- docs/i18n/el/config-reference.md: add Greek locale config-reference with
`tool_filter_groups` section (2026-03-12 update)
* Remove accidentally committed worktree directories
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(tools/mcp): add MCP subsystem tools layer with multi-transport client
Introduces a new MCP (Model Context Protocol) subsystem to the tools layer,
providing a multi-transport client implementation (stdio, HTTP, SSE) that
allows ZeroClaw agents to connect to external MCP servers and register their
exposed tools into the runtime tool registry.
New files:
- src/tools/mcp_client.rs: McpRegistry — lifecycle manager for MCP server connections
- src/tools/mcp_protocol.rs: protocol types (request/response/notifications)
- src/tools/mcp_tool.rs: McpToolWrapper — bridges MCP tools to ZeroClaw Tool trait
- src/tools/mcp_transport.rs: transport abstraction (Stdio, Http, Sse)
Wiring changes:
- src/tools/mod.rs: pub mod + pub use for new MCP modules
- src/config/schema.rs: McpTransport, McpServerConfig, McpConfig types; mcp field
on Config; validate_mcp_config; mcp unit tests
- src/config/mod.rs: re-exports McpConfig, McpServerConfig, McpTransport
- src/channels/mod.rs: MCP server init block in start_channels()
- src/agent/loop_.rs: MCP registry init in run() and process_message()
- src/onboard/wizard.rs: mcp: McpConfig::default() in both wizard constructors
* fix(tools/mcp): inject MCP tools after built-in tool filter, not before
MCP servers are user-declared external integrations. The built-in
agent.allowed_tools / agent.denied_tools filter (filter_primary_agent_tools_or_fail)
governs built-in tool governance only. Injecting MCP tools before that
filter would silently drop all MCP tools when a restrictive allowlist is
configured.
Add ordering comments at both call sites (run() CLI path and
process_message() path) to make this contract explicit for reviewers
and future merges.
Identified via: shady831213/zeroclaw-agent-mcp@3f90b78
* fix(tools/mcp): strip approved field from MCP tool args before forwarding
ZeroClaw's security model injects `approved: bool` into built-in tool
args for supervised-mode confirmation. MCP servers have no knowledge of
this field and reject calls that include it as an unexpected parameter.
Strip `approved` from object-typed args in McpToolWrapper::execute()
before forwarding to the MCP server. Non-object args pass through
unchanged (no silent conversion or rejection).
Add two unit tests:
- execute_strips_approved_field_from_object_args: verifies removal
- execute_handles_non_object_args_without_panic: verifies non-object
shapes are not broken by the stripping logic
Identified via: shady831213/zeroclaw-agent-mcp@c68be01
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Add `extra_headers` config field and `ZEROCLAW_EXTRA_HEADERS` env var
support so users can specify custom HTTP headers for provider API
requests. This enables connecting to providers that require specific
headers (e.g., User-Agent, HTTP-Referer, X-Title) without a reverse
proxy.
Config file headers serve as the base; env var headers override them.
Format: `Key:Value,Key2:Value2`
Closes#3189
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Prevent orphan `<tool_result>` blocks from leaking into LLM sessions:
- Strip `<tool_result>` blocks from cached prior turns in
`process_channel_message` so the LLM never sees a tool result
without a preceding tool call (Case A — in-memory accumulation).
- Skip memory entries containing `<tool_result` in both
`should_skip_memory_context_entry` (channel path) and
`build_context` (agent path) so SQLite-recalled tool output
is never injected as memory context (Case B — post-restart).
Closes#3402
The provider HTTP request timeout was hardcoded at 120 seconds in
`OpenAiCompatibleProvider::http_client()`. This makes it configurable
via the `provider_timeout_secs` config key and the
`ZEROCLAW_PROVIDER_TIMEOUT_SECS` environment variable, defaulting
to 120s for backward compatibility.
Changes:
- Add `provider_timeout_secs` field to Config with serde default
- Add `ZEROCLAW_PROVIDER_TIMEOUT_SECS` env var override
- Add `timeout_secs` field and `with_timeout_secs()` builder on
`OpenAiCompatibleProvider`
- Add `provider_timeout_secs` to `ProviderRuntimeOptions`
- Thread config value through agent loop, channels, gateway, and tools
- Use `compat()` closure in provider factory to apply timeout to all
compatible providers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `agent.tool_call_dedup_exempt` config key (list of tool names) to
allow specific tools to bypass the within-turn identical-signature
deduplication check in run_tool_call_loop. This fixes the browser
snapshot polling use case where repeated calls with identical arguments
are legitimate and should not be suppressed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>