Introduce the WASM plugin system foundation:
- Add extism 1.9 as an optional dependency behind `plugins-wasm` feature
- Create `src/plugins/` module with manifest types, error types, and stub host
- Add `Plugin` CLI subcommands (list, install, remove, info) behind cfg gate
- Add `PluginsConfig` to the config schema with sensible defaults
All plugin code is behind `#[cfg(feature = "plugins-wasm")]` so the default
build is unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add device_registry, pending_pairings to test AppState instances and
pairing_dashboard to test GatewayConfig to fix compilation of tests
after the new pairing dashboard fields were introduced.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ConnectParams struct for an optional first-frame connect handshake.
If the first WebSocket message is {"type":"connect",...}, connection
parameters (session_id, device_name, capabilities) are extracted and
a "connected" ack is sent back. Old clients sending "message" first
still work unchanged (backward-compatible).
Extract process_chat_message() helper to avoid duplication between
fallback first-message handling and the main message loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce DeviceRegistry, PairingStore, and five new API endpoints:
- POST /api/pairing/initiate — generate a new pairing code
- POST /api/pairing/submit — submit code with device metadata
- GET /api/devices — list paired devices
- DELETE /api/devices/{id} — revoke a paired device
- POST /api/devices/{id}/rotate — rotate a device token
Wire into AppState and gateway router. Registry is only created
when require_pairing is enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add PairingDashboardConfig struct with configurable code_length,
ttl_secs, max_pending, max_attempts, and lockout_secs fields.
Nested under GatewayConfig as `pairing_dashboard` with serde defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Export commands module from lib.rs (pub mod commands) for external consumers
- Add --force and --version flags to the Update CLI command
- Wire version parameter through to check() and run() in update.rs,
supporting targeted version fetches via GitHub releases/tags API
- Add WebSocket handshake check (check_websocket_handshake) to the full
self-test suite in self_test.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `zeroclaw update` command with a 6-phase self-update pipeline:
1. Preflight — check GitHub releases API for newer version
2. Download — fetch platform-specific binary to temp dir
3. Backup — copy current binary to .bak for rollback
4. Validate — size check + --version smoke test on download
5. Swap — overwrite current binary with new version
6. Smoke test — verify updated binary runs, rollback on failure
Supports --check flag for update-check-only mode without installing.
Includes version comparison logic with unit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `zeroclaw self-test` command with two modes:
- Quick mode (--quick): 8 offline checks including config, workspace,
SQLite, provider/tool/channel registries, security policy, and version
- Full mode (default): adds gateway health and memory round-trip checks
Creates src/commands/ module structure with self_test and update stubs.
Adds indicatif and tempfile runtime dependencies for the update pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Revert "always generate pairing code" to tighter security posture:
codes are only generated on first startup when no tokens exist. Add
a CLI hint to the gateway banner so operators know how to re-pair
on demand. Fix install.sh to not use --new on fresh install (avoids
invalidating the auto-generated code). Fix onboard to show an
informational message instead of a throwaway PairingGuard.
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(tools): add Google Workspace CLI (gws) integration
Adds GoogleWorkspaceTool for interacting with Google Drive, Sheets,
Gmail, Calendar, Docs, and other Workspace services via CLI.
- Config-gated (google_workspace.enabled)
- Service allowlist for restricted access
- Requires shell access for CLI delegation
- Input validation against shell injection
- Wrong-type rejection for all optional parameters
- Config validation for allowed_services (empty, duplicate, malformed)
- Registered in integrations registry and CLI discovery
Closes#2986
* style: fix cargo fmt + clippy violations
* feat(google-workspace): expand config with auth, rate limits, and audit settings
* fix(tools): define missing GWS_TIMEOUT_SECS constant
* fix: Box::pin large futures and resolve duplicate Default impl
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* 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>
* feat(providers): add Claude Code, Gemini CLI, and KiloCLI subprocess providers
Adds three new local subprocess-based providers for AI CLI tools.
Each provider spawns the CLI as a child process, communicates via
stdin/stdout pipes, and parses responses into ChatResponse format.
* fix: resolve clippy unnecessary_debug_formatting and rustfmt violations
* fix: resolve remaining clippy unnecessary_debug_formatting in CLI providers
* fix(providers): add AiAgent CLI category for subprocess providers
The schedule field in cron_add used a bare {"type":"object"} with a
description string encoding a tagged union in pseudo-notation. The patch
field in cron_update was an opaque {"type":"object"} despite CronJobPatch
having nine fully-typed fields. Both gaps cause weaker instruction-following
models to produce malformed or missing nested JSON when invoking these tools.
Changes:
- cron_add: expand schedule into a oneOf discriminated union with explicit
properties and required fields for each variant (cron/at/every), matching
the Schedule enum in src/cron/types.rs exactly
- cron_add: add descriptions to all previously undocumented top-level fields
- cron_add: expand delivery from a bare inline comment to fully-specified
properties with per-field descriptions
- cron_update: expand patch from opaque object to full properties matching
CronJobPatch (name, enabled, command, prompt, model, session_target,
delete_after_run, schedule, delivery)
- cron_update: schedule inside patch mirrors the same oneOf expansion
- Both: add inline NOTE comments flagging that oneOf is correct for
OpenAI-compatible APIs but SchemaCleanr::clean_for_gemini must be
applied if Gemini native tool calling is ever wired up
- Both: add schema-shape tests using the existing test_config/test_security
helper pattern, covering oneOf variant structure, required fields, and
delivery channel enum completeness
No behavior changes. No new dependencies. Backward compatible: the runtime
deserialization path (serde on Schedule/CronJobPatch) is unchanged.
Co-authored-by: Argenis <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
Both entries had hardcoded |_| IntegrationStatus::Available, ignoring
the live config entirely. Users with cron.enabled = true or
browser.enabled = true saw 'Available' on the /integrations dashboard
card instead of 'Active'.
Root cause: status_fn closures did not capture the Config argument.
Fix: replace the |_| stubs with |c| closures that check c.cron.enabled
and c.browser.enabled respectively, matching the pattern used by every
other wired entry in the registry (Telegram, Discord, Shell, etc.).
What did NOT change: ComingSoon entries, always-Active entries (Shell,
File System), platform entries, or any other registry logic.
* feat(security): add Merkle hash-chain audit trail
Each audit entry now includes a SHA-256 hash linking it to the previous
entry (entry_hash, prev_hash, sequence), forming a tamper-evident chain.
Modifying any entry invalidates all subsequent hashes.
- Chain fields added to AuditEvent with #[serde(default)] for backward compat
- AuditLogger tracks chain state and recovers from existing logs on restart
- verify_chain() validates hash linkage, sequence continuity, and integrity
- Five new tests: genesis seed, multi-entry verify, tamper detection,
sequence gap detection, and cross-restart chain recovery
* fix(security): replace personal name with neutral label in audit tests
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>
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>
* feat(tools): add browser delegation tool for corporate web app interaction
Adds BrowserDelegateTool that delegates browser-based tasks to Claude Code
(or other browser-capable CLIs) for interacting with corporate tools
(Teams, Outlook, Jira, Confluence) via browser automation. Includes domain
validation (allow/blocklist), task templates, Chrome profile persistence
for SSO sessions, and timeout management.
* fix: resolve clippy violation in browser delegation tool
* fix(browser-delegate): validate URLs embedded in task text against domain policy
Scan the task text for http(s):// URLs using regex and validate each
against the allow/block domain lists before forwarding to the browser
CLI subprocess. This prevents bypassing domain restrictions by
embedding blocked URLs in the task parameter.
* fix(browser-delegate): constrain URL schemes, gate on runtime, document config
- Add has_shell_access gate so BrowserDelegateTool is only registered on
shell-capable runtimes (skipped with warning on WASM/edge runtimes)
- Add boundary tests for javascript: and data: URL scheme rejection
- URL scheme validation (http/https only) and config docs were already
addressed by a prior commit on this branch
* fix(tools): address CodeRabbit review findings for browser delegation
Remove dead `max_concurrent_tasks` config field and expand doc comments
on the `[browser_delegate]` config section in schema.rs.
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>
Some OpenAI models (o1, o3, o4, gpt-5 variants) only accept temperature=1.0 and return errors with other values like 0.7. This change automatically adjusts the temperature parameter based on the model being used.
Changes:
- Add adjust_temperature_for_model() function to detect reasoning models
- Apply temperature adjustment in chat_with_system(), chat(), and chat_with_tools()
- Preserve user-specified temperature for standard models (gpt-4o, gpt-4-turbo, etc.)
- Force temperature=1.0 for reasoning models (o1, o3, o4, gpt-5, gpt-5-mini, gpt-5-nano, gpt-5.x-chat-latest)
Testing:
- Add 7 unit tests covering reasoning models, standard models, and edge cases
- All tests pass successfully
- Empirical testing documented in docs/openai-temperature-compatibility.md
Impact:
- Fixes temperature errors when using o1, o3, o4, and gpt-5 model families
- No breaking changes - transparent adjustment for end users
- Standard models continue to work with flexible temperature values
Risk: Low - isolated change within OpenAI provider, well-tested
Rollback: Revert this commit to restore previous behavior
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add HandStarted, HandCompleted, and HandFailed event variants to
ObserverEvent, and HandRunDuration, HandFindingsCount, HandSuccessRate
metric variants to ObserverMetric. Update all observer backends (log,
noop, verbose, prometheus, otel) to handle the new variants with
appropriate instrumentation. Prometheus backend registers hand_runs
counter, hand_duration histogram, and hand_findings counter. OTel
backend creates spans and records metrics for hand runs.
The /memory dashboard page rendered a black screen when MemoryCategory::Custom
was serialized by serde's derived impl as a tagged object {"custom":"..."} but
the frontend expected a plain string. No navigation was possible without using
the browser Back button.
Changes:
- src/memory/traits.rs: replace derived serde impls with custom serialize
(delegates to Display, emits plain snake_case string) and deserialize
(parses known variants by name, falls through to Custom(s) for unknown).
Adds memory_category_serde_uses_snake_case and memory_category_custom_roundtrip
tests. No persistent storage migration needed — all backends (SQLite, Markdown,
Postgres) use their own category_to_str/str_to_category helpers and never
read serde-serialized category values back from disk.
- web/src/App.tsx: export ErrorBoundary class so render crashes surface a
recoverable UI instead of a black screen. Adds aria-live="polite" to the
pairing error paragraph for screen reader accessibility.
- web/src/components/layout/Layout.tsx: wrap Outlet in ErrorBoundary keyed
by pathname so the navigation shell stays mounted during a page crash and
the boundary resets on route change.
Co-authored-by: Argenis <theonlyhennygod@gmail.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>
* 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>
* 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 env var resolution for AiHubMix (AIHUBMIX_API_KEY) and SiliconFlow
(SILICONFLOW_API_KEY) so users can authenticate via environment variables.
Add factory and credential resolution tests for AiHubMix, SiliconFlow,
and Codex OAuth to ensure all provider aliases work correctly.
- Add HeartbeatMetrics struct with uptime, consecutive success/failure
counts, EMA tick duration, and total ticks
- Add compute_adaptive_interval() for exponential backoff on failures
and faster polling when high-priority tasks are present
- Add SQLite-backed task run history (src/heartbeat/store.rs) mirroring
the cron/store.rs pattern with output truncation and pruning
- Add dead-man's switch that alerts if heartbeat stops ticking
- Wire metrics, history recording, and adaptive sleep into daemon worker
- Add config fields: adaptive, min/max_interval_minutes,
deadman_timeout_minutes, deadman_channel, deadman_to, max_run_history
- All new fields are backward-compatible with serde defaults
Several recently-added Config fields (data_retention, cloud_ops,
conversational_ai, security_ops) were missing #[serde(default)],
causing deserialization failures when those sections are absent
from config files. Also fixes security field whose #[serde(default)]
was accidentally consumed by the backup doc comment.
Fixes test failures: agent_config_deserializes and
browser_config_backward_compat_missing_section.
* feat(tools): add cloud transformation accelerator tools
Add cloud_ops and cloud_patterns tools providing read-only cloud
transformation analysis: IaC review, migration assessment, cost
analysis, and Well-Architected Framework architecture review.
Includes CloudOpsConfig, SecurityOpsConfig, and ConversationalAiConfig
schema additions, Box::pin fixes for recursive async in cron scheduler,
and approval_manager field in ChannelRuntimeContext test constructors.
Original work by @rareba. Rebased on latest master with conflict
resolution (kept SwarmConfig/SwarmStrategy exports, swarm tool
registration, and approval_manager in test constructors).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt Box::pin calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add BackupTool for creating, listing, verifying, and restoring
timestamped workspace backups with SHA-256 manifest integrity
checking. Add DataManagementTool for retention status, time-based
purge, and storage statistics. Both tools are config-driven via
new BackupConfig and DataRetentionConfig sections.
Original work by @rareba. Rebased on latest master with conflict
resolution for SwarmConfig/SwarmStrategy exports and swarm tool
registration, and added missing approval_manager fields in
ChannelRuntimeContext test constructors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(security): add MCSS security operations tool
Add managed cybersecurity service (MCSS) tool with alert triage,
incident response playbook execution, vulnerability scan parsing,
and security report generation. Includes SecurityOpsConfig, playbook
engine with approval gating, vulnerability scoring, and full test
coverage. Also fixes pre-existing missing approval_manager field in
ChannelRuntimeContext test constructors.
Original work by @rareba. Supersedes #3599.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add SecurityOpsConfig to re-exports, fix test constructors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a new read-only project_intel tool that provides:
- Status report generation (weekly/sprint/month)
- Risk scanning with configurable sensitivity
- Client update drafting (formal/casual, client/internal)
- Sprint summary generation
- Heuristic effort estimation
Includes multi-language report templates (EN, DE, FR, IT),
ProjectIntelConfig schema with validation, and comprehensive tests.
Also fixes missing approval_manager field in 4 ChannelRuntimeContext
test constructors.
Supersedes #3591 — rebased on latest master. Original work by @rareba.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a new `nodes` module with HMAC-SHA256 authenticated transport for
secure inter-node communication over standard HTTPS. Includes replay
protection via timestamped nonces and constant-time signature
comparison.
Also adds `NodeTransportConfig` to the config schema and fixes missing
`approval_manager` field in four `ChannelRuntimeContext` test
constructors that failed compilation on latest master.
Original work by @rareba. Rebased on latest master to resolve merge
conflicts (SwarmConfig/SwarmStrategy exports, duplicate MCP validation,
test constructor fields).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add Microsoft 365 tool providing access to Outlook mail, Teams messages,
Calendar events, OneDrive files, and SharePoint search via Microsoft
Graph API. Includes OAuth2 token caching (client credentials and device
code flows), security policy enforcement, and config validation.
Rebased on latest master, resolving conflicts with SwarmConfig exports
and adding approval_manager to ChannelRuntimeContext test constructors.
Original work by @rareba.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
Wrap all crate::agent::run() calls with Box::pin() across scheduler,
daemon, gateway tests, and main.rs to satisfy clippy::large_futures.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Commit 811fab3b added is_service_environment() as a top-level function and
called it from two sites. The call at line 445 is at module scope and resolves
fine. The call at line 1473 is inside mod native_backend, which is a child
module — Rust does not implicitly import parent-scope items, so the unqualified
name fails with E0425 (cannot find function in this scope).
Fix: prefix the call with super:: so it resolves to the parent module's
function, matching how mod native_backend already imports other parent items
(e.g. use super::BrowserAction).
The browser-native feature flag is required to reproduce:
cargo check --features browser-native # fails without this fix
cargo check --features browser-native # clean with this fix
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* feat(security): add Nevis IAM integration for SSO/MFA authentication
Add NevisAuthProvider supporting OAuth2/OIDC token validation (local JWKS +
remote introspection), FIDO2/passkey/OTP MFA verification, session management,
and health checks. Add IamPolicy engine mapping Nevis roles to ZeroClaw tool
and workspace permissions with deny-by-default enforcement and audit logging.
Add NevisConfig and NevisRoleMappingConfig to config schema with client_secret
wired through SecretStore encrypt/decrypt. All features disabled by default.
Rebased on latest master to resolve merge conflicts in security/mod.rs (redact
function) and config/schema.rs (test section).
Original work by @rareba. Supersedes #3593.
Co-Authored-By: rareba <5985289+rareba@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt Box::pin calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: rareba <5985289+rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tunnel): add OpenVPN tunnel provider
Add OpenVPN as a new tunnel provider alongside cloudflare, tailscale,
ngrok, and custom. Includes config schema, validation, factory wiring,
and comprehensive unit tests.
Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing approval_manager field to ChannelRuntimeContext constructors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add workspace profile management, security boundary enforcement, and
a workspace management tool for isolated client engagements.
Original work by @rareba. Supersedes #3597 — rebased on latest master.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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>
- 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>
Remove the --interactive flag from `zeroclaw onboard`. The command now
auto-detects whether stdin/stdout are a TTY: if yes and no provider
flags are given, it launches the full interactive wizard; otherwise it
runs the quick (scriptable) setup path.
This means all three install methods work with a single flow:
curl -fsSL https://zeroclawlabs.ai/install.sh | bash
cargo install zeroclawlabs && zeroclaw onboard
docker run … zeroclaw onboard --api-key …
When zeroclaw runs as a service, the process inherits a minimal
environment without HOME, DISPLAY, or user namespaces. Headless
browsers (Chromium/Firefox) need HOME for profile/cache dirs and
fail with sandbox errors without user namespaces.
- Detect service environment via INVOCATION_ID, JOURNAL_STREAM,
or missing HOME on Linux
- Auto-apply --no-sandbox and --disable-dev-shm-usage for Chrome
in service mode
- Set HOME fallback and CHROMIUM_FLAGS on agent-browser commands
- systemd unit: add Environment=HOME=%h and PassEnvironment
- OpenRC script: export HOME=/var/lib/zeroclaw with start_pre()
to create the directory
Closes#3584
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>
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>
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
Introduce the Hands system — autonomous agent packages that run on
schedules and accumulate knowledge over time. Each Hand maintains a
rolling context of findings across runs so the agent grows smarter
with every execution.
This PR adds:
- Hand definition type (TOML-deserializable, reuses cron Schedule)
- HandRun / HandRunStatus for execution records
- HandContext for rolling cross-run knowledge accumulation
- File-based persistence (load/save context as JSON)
- Directory-based Hand loading from ~/.zeroclaw/hands/*.toml
- 20 unit tests covering deserialization, persistence roundtrip,
history capping, fact deduplication, and error handling
Execution integration with the agent loop is deferred to a follow-up.
Replace full-body buffering (`response.text().await`) in
`decode_responses_body()` with incremental `bytes_stream()` chunk
processing. The previous approach held the HTTP connection open until
every byte had arrived; on high-latency links the long-lived connection
would frequently drop mid-read, producing the "error decoding response
body" failure on the first attempt (succeeding only after retry).
Reading chunks incrementally lets each network segment complete within
its own timeout window, eliminating the systematic first-attempt failure.
Closes#3544
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When `block_high_risk_commands = true`, commands like `curl` and `wget`
were unconditionally blocked even if explicitly listed in
`allowed_commands`. This made it impossible to use legitimate API calls
in full autonomy mode.
Now, if a command is explicitly named in `allowed_commands` (not via
the wildcard `*`), it is exempt from the `block_high_risk_commands`
gate. The wildcard entry intentionally does NOT grant this exemption,
preserving the safety net for broad allowlists.
Other security gates (supervised-mode approval, rate limiting, path
policy, argument validation) remain fully enforced.
Closes#3567
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Replace unsafe byte-index string slicing (`&text[..N]`) with
char-boundary-safe alternatives in memory consolidation and security
redaction to prevent panics when multi-byte UTF-8 characters (e.g.
Chinese/Japanese/Korean) span the slice boundary.
Fixes the same class of bug as the prior fix in `execute_one_tool`
(commit 8fcbb6eb), applied to two remaining instances:
- `src/memory/consolidation.rs`: truncation at byte 4000 and 200
- `src/security/mod.rs`: `redact()` prefix at byte 4
Adds regression tests with CJK input for both locations.
Closes#3533
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI `cron add` command always routed the second positional argument
through shell security policy validation, which blocked natural language
prompts like "Check server health: disk space, memory, CPU load". This
adds an `--agent` flag to `cron add`, `cron add-at`, `cron add-every`,
and `cron once` so that natural language prompts are correctly stored as
agent jobs without shell command validation.
Closes#3563
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The http_request tool unconditionally blocked all private/LAN hosts with
no opt-out, preventing legitimate use cases like calling a local Home
Assistant instance or internal APIs. This adds an `allow_private_hosts`
config flag (default: false) under `[http_request]` that, when set to
true, skips the private-host SSRF check while still enforcing the domain
allowlist.
Closes#3568
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>
Upgrade heartbeat system with 4 key improvements:
- Two-phase heartbeat: Phase 1 asks LLM "skip or run?" to save API cost
on quiet periods. Phase 2 executes only selected tasks.
- Structured task format: `- [priority|status] task text` with
high/medium/low priority and active/paused/completed status.
- Decision intelligence: LLM-driven smart filtering via structured prompt
at temperature 0.0 for deterministic decisions.
- Delivery routing: auto-detect best configured channel when no explicit
target is set (telegram > discord > slack > mattermost).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add SignalChannel import and match arm in deliver_announcement() so
cron jobs with delivery.channel = "signal" are handled instead of
rejected as unsupported.
Closes#3476
* fix: resolve web dashboard 404 on static assets and SPA fallback
Strip leading slash from asset paths after prefix removal so rust-embed
can find the files. Return 503 with a helpful build hint when index.html
is missing instead of a generic 404.
Closes#3508
* fix: return concrete Response type to fix match arm type mismatch
When multiple tool calls execute in a single turn, each tool result was
emitted as a separate role="user" message. Anthropic's API rejects
adjacent messages with the same role, and newer models like
claude-sonnet-4-6 respond with 500 Internal Server Error instead of a
descriptive 400.
Merge consecutive same-role messages in convert_messages() so that
multiple tool_result blocks are combined into one user message, and
consecutive user/assistant messages are also properly coalesced.
Fixes#3493
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
New fast inference providers:
- Cerebras, SambaNova, Hyperbolic
New model hosting platforms:
- DeepInfra, Hugging Face, AI21 Labs, Reka, Baseten, Nscale,
Anyscale, Nebius AI Studio, Friendli AI, Lepton AI
New Chinese AI providers:
- Stepfun, Baichuan, 01.AI (Yi), Tencent Hunyuan
Also fixed missing list_providers() entries for Telnyx and Azure OpenAI.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(release+providers): fix release race condition, add 3 providers
Release fix (two parts):
1. Replace softprops/action-gh-release with `gh release create` — the
CLI uploads assets atomically with the release in a single call,
avoiding the immutable release race condition
2. Move website redeploy to a separate job with `if: always()` — so the
website updates regardless of publish outcome
Both release-beta-on-push.yml and release-stable-manual.yml are fixed.
Provider additions:
- SiliconFlow (siliconflow, silicon-flow)
- AiHubMix (aihubmix)
- LiteLLM router (litellm, lite-llm)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: trigger CI
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
* feat(install): consolidate one-click installer with branded output and inline onboarding
- Add blue color scheme with 🦀 crab emoji branding throughout installer
- Add structured [1/3] [2/3] [3/3] step output with ✓/·/✗ indicators
- Consolidate onboarding into install.sh: inline provider selection menu,
API key prompt, and model override — no separate wizard step needed
- Replace --onboard/--interactive-onboard with --skip-onboard (opt-out)
- Add OS detection display, install method, version detection, upgrade vs
fresh install logic
- Add post-install gateway service install/restart, doctor health check
- Add dashboard URL (port 42617) with clipboard copy and browser auto-open
- Add docs link (https://www.zeroclawlabs.ai/docs) to success output
- Display pairing code after onboarding in Rust CLI (src/main.rs)
- Remove --interactive flag from `zeroclaw onboard` CLI command
- Remove redundant scripts/install-release.sh legacy redirect
- Update all --interactive references across codebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(onboard): auto-pair and include bearer token in dashboard URL
After onboarding, the CLI now auto-pairs using the generated pairing
code to produce a bearer token, then displays the dashboard URL with
the token embedded (e.g. http://127.0.0.1:42617?token=zc_...) so
users can access the dashboard immediately without a separate pairing
step. The token is also persisted to config for gateway restarts.
The install script captures this token-bearing URL from the onboard
output and uses it for clipboard copy and browser auto-open.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* security(onboard): revert token-in-URL, keep pairing code terminal-only
Removes the auto-pair + token-in-URL approach in favor of the original
secure pairing flow. Bearer tokens should never appear in URLs where
they can leak via browser history, Referer headers, clipboard, or
proxy logs. The pairing code stays in the terminal and the user enters
it in the dashboard to complete the handshake securely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.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
Replace single-turn chat with persistent Agent to maintain conversation
history across WebSocket turns within the same connection.
Co-authored-by: staz <starzwan2333@gmail.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.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>
* ci: add x86_64-pc-windows-msvc to build matrix
* fix: prevent test deadlock in ensure_onboard_overwrite_allowed
Gate non-interactive terminal check behind cfg!(not(test)) so tests with
force=false do not hang waiting on stdin. cfg!(test) path bails immediately
with a clear message. No changes to extra_headers, mcp, nodes, or shellexpand.
---------
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* fix(linq): accept current webhook payload shape
* style(linq): satisfy clippy lifetime lint
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* Ignore JetBrains .idea folder
* fix(ollama): support stringified JSON tool call arguments
* providers: allow ZEROCLAW_PROVIDER_URL env var to override Ollama base URL
Supports container deployments where Ollama runs on a Docker network host
(e.g. http://ollama:11434) without requiring config.toml changes.
Includes regression test ensuring the environment override works.
* fix(clippy): replace Default::default() with ProviderRuntimeOptions::default()
---------
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add a WebSocket endpoint at /ws/nodes where external processes and
devices can connect and advertise their capabilities at runtime.
The gateway tracks connected nodes in a NodeRegistry and exposes
their capabilities as dynamically available tools via NodeTool.
- Add src/gateway/nodes.rs: WebSocket endpoint, NodeRegistry, protocol
- Add src/tools/node_tool.rs: Tool trait wrapper for node capabilities
- Add NodesConfig to config schema (disabled by default)
- Wire /ws/nodes route into gateway router
- Add NodeRegistry to AppState and all test constructions
- Re-export NodesConfig and NodeTool from module roots
Closes#3093
A single cron job with a malformed `next_run` timestamp in the database
was silently stopping all scheduled jobs. The `due_jobs` query matched
rows whose `next_run` was lexicographically past-due (including
non-RFC3339 values like "2026-03-12 03:11:13" which sort before valid
RFC3339 strings), then `map_cron_job_row` failed to parse the timestamp,
the `row?` propagation caused `due_jobs` to return `Err`, and the
scheduler marked itself as `error` and skipped every subsequent tick —
taking down all other healthy jobs with it.
The fix changes the row iteration in `due_jobs` to log a warning and
skip unparseable rows rather than aborting the entire result set. Valid
jobs continue to fire; the broken row is surfaced in the logs without
collateral damage to the scheduler.
Co-authored-by: ZeroClaw <zeroclaw@users.noreply.github.com>
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
Use `cmd.exe /C` instead of `sh -c` on Windows via cfg(target_os).
Make the shell allowlist, forbidden paths, env vars, risk classification,
and path detection platform-aware so the shell tool works correctly on
Windows without changing Unix behavior.
Closes#3327
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
Add GET /api/cron/{id}/runs?limit=N endpoint that returns recent stored
runs for a cron job, with server-side limit clamping to 1-100 (default 20).
Frontend adds a CronRun type, API client function, and an expandable
run history panel on the Cron page showing status, timestamps, duration,
and output for each run, with loading, empty, error, and refresh states.
Closes#3299
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
When workspace_only=true and allowed_roots is configured, several tools
(file_read, content_search, glob_search) rejected absolute paths before
the allowed_roots allowlist was consulted. Additionally, tilde paths
(~/...) passed is_path_allowed but were then incorrectly joined with
workspace_dir as literal relative paths.
Changes:
- Add SecurityPolicy::resolve_tool_path() to properly expand tilde
paths and handle absolute vs relative path resolution for tools
- Add SecurityPolicy::is_under_allowed_root() for tool pre-checks to
consult the allowed_roots allowlist before rejecting absolute paths
- Update file_read to use resolve_tool_path instead of workspace_dir.join
- Update content_search and glob_search absolute-path pre-checks to
allow paths under allowed_roots
- Add tests covering workspace_only + allowed_roots scenarios
Closes#3082
PR #3409 fixed AtomicU64 usage on 32-bit targets in other files but
missed src/tools/mcp_client.rs. Apply the same cfg(target_has_atomic)
pattern used in channels/irc.rs to conditionally select AtomicU64 vs
AtomicU32.
Closes#3430
* 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>
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
The URL parser captured the first https:// URL found in cloudflared
stderr output. When cloudflared emits a quic-go UDP buffer warning
containing a github.com link, that documentation URL was incorrectly
captured as the tunnel's public URL.
Extract URL parsing into a testable helper function that skips known
documentation domains (github.com, cloudflare.com/docs,
developers.cloudflare.com) and recognises tunnel-specific log prefixes
("Visit it at", "Route at", "Registered tunnel connection") and the
.trycloudflare.com domain.
Closes#3413
* fix: gate prometheus and fix AtomicU64 for 32-bit targets (#3335)
Closes#3335
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix import ordering for cfg-gated atomics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
MCP (Model Context Protocol) config and tool modules were added on the
old `main` branch but never made it to `master`. This restores the full
MCP subsystem: config schema, transport layer (stdio/HTTP/SSE), client
registry, tool wrapper, config validation, and channel wiring.
Closes#3379
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When reasoning_enabled is configured, the Ollama provider sends
think=true to all models. Models that don't support the think parameter
(e.g. qwen3.5:0.8b) cause request failures that the reliable provider
classifies as retryable, leading to an infinite retry loop.
Fix: when a request with think=true fails, automatically retry once
with think omitted. This lets the call succeed on models that lack
reasoning support while preserving thinking for capable models.
Closes#3183
Related #850
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>