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>
* feat(tools): add native LinkedIn integration tool
Add a config-gated LinkedIn tool that enables ZeroClaw to interact with
LinkedIn's REST API via OAuth2. Supports creating posts, listing own
posts, commenting, reacting, deleting posts, viewing engagement stats,
and retrieving profile info.
Architecture:
- linkedin.rs: Tool trait impl with action-dispatched design
- linkedin_client.rs: OAuth2 token management and API wrappers
- Config-gated via [linkedin] enabled = false (default off)
- Credentials loaded from workspace .env file
- Automatic token refresh with line-targeted .env update
39 unit tests covering security enforcement, parameter validation,
credential parsing, and token management.
* feat(linkedin): configurable content strategy and API version
- Expand LinkedInConfig with api_version and nested LinkedInContentConfig
(rss_feeds, github_users, github_repos, topics, persona, instructions)
- Add get_content_strategy tool action so agents can read config at runtime
- Fix hardcoded LinkedIn API version 202402 (expired) → configurable,
defaulting to 202602
- LinkedInClient accepts api_version as parameter instead of static header
- 4 new tests (43 total), all passing
* feat(linkedin): add multi-provider image generation for posts
Add ImageGenerator with provider chain (DALL-E, Stability AI, Imagen, Flux)
and SVG fallback card. LinkedIn tool create_post now supports generate_image
parameter. Includes LinkedIn image upload (register → upload → reference),
configurable provider priority, and 14 new tests.
* feat(whatsapp): add voice note transcription and TTS voice replies
- Add STT support: download incoming voice notes via wa-rs, transcribe
with OpenAI Whisper (or Groq), send transcribed text to agent
- Add TTS support: synthesize agent replies to Opus audio via OpenAI
TTS, upload encrypted media, send as WhatsApp voice note (ptt=true)
- Voice replies only trigger when user sends a voice note; text
messages get text replies only. Flag is consumed after one use to
prevent multiple voice notes per agent turn
- Fix transcription module to support OpenAI API key (not just Groq):
auto-detect provider from API URL, check ANTHROPIC_OAUTH_TOKEN /
OPENAI_API_KEY / GROQ_API_KEY env vars in priority order
- Add optional api_key field to TranscriptionConfig for explicit key
- Add response_format: opus to OpenAI TTS for WhatsApp compatibility
- Add channel capability note so agent knows TTS is automatic
- Wire transcription + TTS config into WhatsApp Web channel builder
* fix(providers): prefer ANTHROPIC_OAUTH_TOKEN over global api_key
When the Anthropic provider is used alongside a non-Anthropic primary
provider (e.g. custom: gateway), the global api_key would be passed
as credential override, bypassing provider-specific env vars. This
caused Claude Code subscription tokens (sk-ant-oat01-*) to be ignored
in favor of the unrelated gateway JWT.
Fix: for the anthropic provider, check ANTHROPIC_OAUTH_TOKEN and
ANTHROPIC_API_KEY env vars before falling back to the credential
override. This mirrors the existing MiniMax OAuth pattern and enables
subscription-based auth to work as a fallback provider.
* feat(linkedin): add scheduled post support via LinkedIn API
Add scheduled_at parameter to create_post and create_post_with_image.
When provided (RFC 3339 timestamp), the post is created as a DRAFT
with scheduledPublishOptions so LinkedIn publishes it automatically
at the specified time. This enables the cron job to schedule a week
of posts in advance directly on LinkedIn.
* fix(providers): prefer env vars for openai and groq credential resolution
Generalize the Anthropic OAuth fix to also cover openai and groq
providers. When used alongside a non-matching primary provider (e.g.
a custom: gateway), the global api_key would be passed as credential
override, causing auth failures. Now checks provider-specific env
vars (OPENAI_API_KEY, GROQ_API_KEY) before falling back to the
credential override.
* fix(whatsapp): debounce voice replies to voice final answer only
The voice note TTS was triggering on the first send() call, which was
often intermediate tool output (URLs, JSON, web fetch results) rather
than the actual answer. This produced incomprehensible voice notes.
Fix: accumulate substantive replies (>30 chars, not URLs/JSON/code)
in a pending_voice map. A spawned debounce task waits 4 seconds after
the last substantive message, then synthesizes and sends ONE voice
note with the final answer. Intermediate tool outputs are skipped.
This ensures the user hears the actual answer in the correct language,
not raw tool output in English.
* fix(whatsapp): voice in = voice out, text in = text out
Rewrite voice reply logic with clean separation:
- Voice note received: ALL text output suppressed. Latest message
accumulated silently. After 5s of no new messages, ONE voice note
sent with the final answer. No tool outputs, no text, just voice.
- Text received: normal text reply, no voice.
Atomic debounce: multiple spawned tasks race but only one can extract
the pending message (remove-inside-lock pattern). Prevents duplicate
voice notes.
* fix(whatsapp): voice replies send both text and voice note
Voice note in → text replies sent normally in real-time PLUS one
voice note with the final answer after 10s debounce. Only substantive
natural-language messages are voiced (tool outputs, URLs, JSON, code
blocks filtered out). Longer debounce (10s) ensures the agent
completes its full tool chain before the voice note fires.
Text in → text out only, no voice.
* fix(channels): suppress tool narration and ack reactions
- Add system prompt instruction telling the agent to NEVER narrate
tool usage (no "Let me fetch..." or "I will use http_request...")
- Disable ack_reactions (emoji reactions on incoming messages)
- Users see only the final answer, no intermediate steps
* docs(claude): add full CONTRIBUTING.md guidelines to CLAUDE.md
Add PR template requirements, code naming conventions, architecture
boundary rules, validation commands, and branch naming guidance
directly to CLAUDE.md for AI assistant reference.
* fix(docs): add blank lines around headings in CLAUDE.md for markdown lint
* fix(channels): strengthen tool narration suppression and fix large_futures
- Move anti-narration instruction to top of channel system prompt
- Add emphatic instruction for WhatsApp/voice channels specifically
- Add outbound message filter to strip tool-call-like patterns (⏳, 🔧)
- Box::pin the two-phase heartbeat agent::run call (16664 bytes on Linux)
* feat(channels): add Reddit, Bluesky, and generic Webhook adapters
- Reddit: OAuth2 polling for mentions/DMs/replies, comment and DM sending
- Bluesky: AT Protocol session auth, notification polling, post replies
- Webhook: Axum HTTP server for inbound, configurable outbound POST/PUT
- All three follow existing channel patterns with tests
* fix(channels): use neutral test fixtures and improve test naming in webhook
* feat(stt): add multi-provider STT with TranscriptionProvider trait
Refactors single-endpoint transcription to support multiple providers:
Groq (existing), OpenAI Whisper, Deepgram, AssemblyAI, and Google Cloud
Speech-to-Text. Adds TranscriptionManager for provider routing with
backward-compatible config fields.
* style: fix cargo fmt + clippy violations
* fix: Box::pin large futures and resolve merge conflicts with master
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.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
Adds audio message detection and transcription to WhatsApp Web channel.
Voice messages (PTT) are downloaded, transcribed via the existing
transcription subsystem (Groq Whisper), and delivered as text content.
- TranscriptionConfig field with builder pattern
- Duration limit enforcement before download
- MIME type mapping for audio formats
- Graceful error handling (skip on failure)
- Preserves full retry/reconnect state machine from master
* fix(channel): resolve multi-room reply routing regression (#3224)
PR #3224 (f0f0f808, "feat(matrix): add multi-room support") changed the
channel name format in matrix.rs from "matrix" to "matrix:!roomId", but
the channel lookup in mod.rs still does an exact match against
channels_by_name, which is keyed by Channel::name() (returns "matrix").
This mismatch causes target_channel to always resolve to None for Matrix
messages, silently dropping all replies.
Fix: fall back to a prefix match on the base channel name (before ':')
when the exact lookup fails. This preserves multi-room conversation
isolation while correctly routing replies to the originating channel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply cargo fmt to channel routing fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Sandeep (Claude) <sghael+claude@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the LLM hallucinates an invalid model ID through the
model_routing_config tool's set_default action, the invalid model gets
persisted to config.toml. The channel hot-reload then picks it up and
every subsequent message fails with a non-retryable 404, permanently
killing the connection with no user recovery path.
Fix with two layers of defense:
1. Tool probe-and-rollback: after saving the new model, send a minimal
chat request to verify the model is accessible. If the API returns a
non-retryable error (404, auth failure, etc.), automatically restore
the previous config and return a failure notice to the LLM.
2. Channel safety net: in maybe_apply_runtime_config_update, reject
config reloads when warmup fails with a non-retryable error instead
of applying the broken config anyway.
Co-authored-by: Christian Pojoni <christian.pojoni@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When mention_only is enabled, the bot correctly requires an @mention in
guild (server) channels. However, Direct Messages have no guild_id and
are inherently private and addressed to the bot — requiring a @mention
in a DM is never correct and silently drops all DM messages.
Changes:
- src/channels/discord.rs: detect DMs via absence of guild_id in the
gateway payload, compute effective_mention_only = self.mention_only && !is_dm,
and pass that to normalize_incoming_content instead of self.mention_only.
DMs bypass the mention gate; guild messages retain existing behaviour.
- Adds three tests: DM bypasses mention gate, guild message without mention
is rejected, guild message with mention passes and strips the mention tag.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* feat(channels): add X/Twitter and Mochat channel integrations
Add two new channel implementations to close competitive gaps:
- X/Twitter: Twitter API v2 with mentions polling, tweet threading
(auto-splits at 280 chars), DM support, and rate limit handling
- Mochat: HTTP polling-based integration with Mochat customer service
platform, configurable poll interval, message dedup
Both channels follow the existing Channel trait pattern with full
config schema integration, health checks, and dedup.
Closes competitive gap: NanoClaw had X/Twitter, Nanobot had Mochat.
* fix(channels): use write! instead of format_push_string for clippy
Replace url.push_str(&format!(...)) with write!(url, ...) to satisfy
clippy::format_push_string lint on CI.
* fix(channels): rename reply_to parameter to avoid legacy field grep
The component test source_does_not_use_legacy_reply_to_field greps
for "reply_to:" in source files. Rename the parameter to
reply_tweet_id to pass this check.
Add `initial_prompt: Option<String>` to `TranscriptionConfig` and pass
it as the `prompt` field in the Whisper API multipart POST when present.
This lets users bias transcription toward expected vocabulary (proper
nouns, technical terms) via the config file.
Closes#2881
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): strip vision markers from history for non-vision providers
When a user sends an image via Telegram to a non-vision provider, the
`[IMAGE:/path]` marker gets stored in the JSONL session file. Previously,
the rollback only removed it from in-memory history, not from the JSONL
file. On restart, the marker was reloaded and permanently broke the
conversation.
Two fixes:
1. `rollback_orphan_user_turn` now also calls `remove_last` on the
session store so the poisoned entry is removed from disk.
2. When building history for a non-vision provider, `[IMAGE:]` markers
are stripped from older history messages (and empty turns are dropped).
Fixes#3674
* fix(agent): only strip vision markers from older history, not current message
The initial fix stripped [IMAGE:] markers from all prior_turns including
the current message, which caused the vision check to never fire. Now
only strip from turns before the last one (the current request), so
fresh image sends still get a proper vision capability error.
* fix(ci): decouple tweet from Docker push in release workflows
Remove Docker from the tweet job's dependency chain in both beta and
stable release workflows. Docker multi-platform builds are slow and
can be cancelled by concurrency groups, which was blocking the tweet
from ever firing. The tweet announces the GitHub Release, not the
Docker image.
* fix(qq): send markdown messages instead of plain text
Change msg_type from 0 (plain text) to 2 (markdown) and wrap content
in a markdown object per QQ's API documentation. This ensures markdown
formatting (bold, italic, code blocks, etc.) renders properly in QQ
clients instead of displaying raw syntax.
Fixes#3647
Add Notion integration with two components:
- NotionChannel: polls a Notion database for tasks with configurable
status properties, concurrency limits, and stale task recovery
- NotionTool: provides CRUD operations (query_database, read_page,
create_page, update_page) for agent-driven Notion interactions
Includes config schema (NotionConfig), onboarding wizard support,
and full unit test coverage for both channel and tool.
Supersedes #3609 — rebased on latest master to resolve merge conflicts
with swarm feature additions in config/mod.rs.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Box::pin the cron_run execute_job_now call to satisfy clippy::large_futures
- Add missing approval_manager field to 4 query_classification test constructors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The QueryClassificationConfig was parsed from config but never applied
during channel message processing. This adds the query_classification
field to ChannelRuntimeContext and invokes the classifier in
process_channel_message to override the route when a classification
rule matches a model_routes hint.
Closes#3579
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>
The Matrix channel listener was building channel keys as `matrix:<room_id>`,
but the runtime channel mapping expects the plain key `matrix`. This mismatch
caused replies to silently drop in deployments using the Matrix channel.
Closes#3477
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The WhatsApp Web QR code was not shown during onboarding channel launch
because the wizard allowed configuring WhatsApp Web mode even when the
binary was built without the `whatsapp-web` feature flag. At runtime,
the channel was silently skipped with only a tracing::warn that most
users never see.
- Add compile-time warning in the onboarding wizard when WhatsApp Web
mode is selected but the feature is not compiled in
- Add eprintln! in collect_configured_channels so users see a visible
terminal warning when the feature is missing at startup
Closes#3577
Matrix image messages used lowercase `[image: ...]` format instead of
the canonical `[IMAGE:...]` marker used by all other channels (Telegram,
Slack, Discord, QQ, LinQ). This caused Matrix image attachments to
bypass the multimodal vision pipeline which looks for `[IMAGE:...]`.
Closes#3486
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>
Nextcloud Talk bot webhooks send event type "Create" for new chat
messages, but the parser only accepted "message". This caused all
valid messages to be skipped with "skipping non-message event: Create".
Accept both "message" and "Create" as valid event types.
Closes#3491
Documents with image extensions (jpg, png, etc.) are routed to
[Document: name] /path instead of [IMAGE:/path], bypassing the
multimodal pipeline entirely. This causes the model to have no vision
input for images sent as Telegram Documents.
Re-applies fix from merged dev PR #1631 which was lost during the
master branch migration.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
parse_attachment_markers uses .find(']') which matches the first ]
in the content. Filenames containing brackets (e.g. yt-dlp output
'Video [G4PvTrTp7Tc].mp4') get truncated at the inner bracket,
causing the send to fail with 'path not found'.
Uses depth-tracking bracket matching instead.
Re-applies fix from merged dev PR #1632 which was lost during the
master branch migration.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* feat(channels): add show_tool_calls config to suppress tool notifications
When show_tool_calls is false, the ChannelNotifyObserver drains tool
events silently instead of forwarding them as individual messages to
the channel. Server-side logs remain unaffected.
Defaults to true for backwards compatibility.
* docs: add before/after screenshots for show_tool_calls PR
* docs(config): add doc comment on show_tool_calls field
---------
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Conversation history on long-running channel sessions (e.g. Feishu) grew
unbounded until the provider returned a context-window-exceeded error.
The existing reactive compaction only kicked in *after* the error,
causing the user's message to be lost and requiring a resend.
Add proactive_trim_turns() which estimates total character count and
drops the oldest turns before the request reaches the provider. The
budget (400 k chars ≈ 100 k tokens) leaves headroom for system prompt,
memory context, and model output.
Closes#3460
* fix(linq): accept current webhook payload shape
* style(linq): satisfy clippy lifetime lint
---------
Co-authored-by: argenis de la rosa <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
Send a read receipt after receiving each message, start a typing
notification while processing, and stop it before sending the response.
This gives Matrix users visual feedback that the bot has seen their
message and is working on a reply.
Closes#3357
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Implement the Channel trait for WeCom Bot Webhook, supporting
outbound text messages via the WeCom webhook API. The channel
is send-only; inbound messages can be routed through the gateway
webhook subsystem.
Closes#3396
Users can now set `ack_reactions = false` in `[channels_config]` to
suppress the 👀/✅/⚠️ acknowledgement reactions on incoming messages.
The option defaults to `true`, preserving existing behavior.
Closes#3403
The polling-based Slack listener only called conversations.history, which
returns top-level channel messages but not thread replies. Users replying
inside a thread were invisible to the bot after its initial response.
Add conversations.replies polling for active threads discovered in
channel history. Track thread parents with reply_count > 0, periodically
fetch new replies, and emit them as ChannelMessage with the correct
thread_ts so the bot can continue multi-turn conversations in-thread.
Stale threads are evicted after 24 hours or when the tracker exceeds
50 entries.
Closes#3084
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(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>
Rust treats `~` as a literal path character, not a home directory
shorthand. Several config resolution paths used `PathBuf::from()` on
user-provided strings without expanding `~` first, causing a literal
`~` folder to be created in the working directory.
Apply `shellexpand::tilde()` to all user-facing path inputs:
- ZEROCLAW_CONFIG_DIR env var (config/schema.rs, onboard/wizard.rs)
- ZEROCLAW_WORKSPACE env var (config/schema.rs, onboard/wizard.rs,
channels/matrix.rs)
- active_workspace.toml marker file config_dir (config/schema.rs)
The WhatsApp Web session_path was already correctly expanded via
shellexpand::tilde() in whatsapp_web.rs.
Closes#3417
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <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