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>
When auto_save is enabled and a photo is sent on the first turn of a
Telegram session, the [IMAGE:] marker was duplicated because:
1. auto_save stores the photo message (including the marker) to memory
2. build_memory_context recalls the just-saved entry as relevant context
3. The recalled marker is prepended to the original message content
Fix: skip memory context entries containing [IMAGE:] markers in
should_skip_memory_context_entry so auto-saved photo messages are not
re-injected through memory context enrichment.
Closes#2403
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When workspace_only=true, is_path_allowed() blanket-rejected all
absolute paths. This blocked legitimate tool calls that referenced
files inside the workspace using an absolute path (e.g. saving a
screenshot to /home/user/.zeroclaw/workspace/images/example.png).
The fix checks whether an absolute path falls within workspace_dir or
any configured allowed_root before rejecting it, mirroring the priority
order already used by is_resolved_path_allowed(). Paths outside the
workspace and allowed roots are still blocked, and the forbidden-paths
list continues to apply to all other absolute paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Discord gateway event loop silently dropped websocket Ping frames
(via a catch-all `_ => continue`) without responding with Pong. After
splitting the websocket stream into read/write halves, automatic
Ping/Pong handling is disabled, so the server-side (Cloudflare/Discord)
eventually considers the client unresponsive and stops sending events.
Additionally, websocket read errors (`Some(Err(_))`) were silently
swallowed by the same catch-all, preventing reconnection on transient
failures.
This patch:
- Responds to `Message::Ping` with `Message::Pong` to maintain the
websocket keepalive contract
- Breaks the event loop on `Some(Err(_))` with a warning log, allowing
the supervisor to reconnect
Closes#2896
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Electric Blue dashboard (PR #2804) sends the pairing token as a
?token= query parameter, but the WS handler only checked that single
source. Earlier PR #2193 had established a three-source precedence
chain (header > subprotocol > query param) which was lost.
Add extract_ws_token() with the documented precedence:
1. Authorization: Bearer <token> header
2. Sec-WebSocket-Protocol: bearer.<token> subprotocol
3. ?token=<token> query parameter
This ensures browser-based clients (which cannot set custom headers)
can authenticate via query param or subprotocol, while non-browser
clients can use the standard Authorization header.
Includes 9 unit tests covering each source, precedence ordering,
and empty-value fallthrough.
Closes#2884
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename shadowed `histories` and `store` bindings in three test functions
to eliminate variable shadows that are flagged under stricter lint
configurations (clippy::shadow_unrelated). The initial bindings are
consumed by struct initialization; the second bindings that lock the
mutex guard are now named distinctly (`locked_histories`, `cleanup_store`).
Closes#2443
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The provider HTTP request timeout was hardcoded at 120 seconds in
`OpenAiCompatibleProvider::http_client()`. This makes it configurable
via the `provider_timeout_secs` config key and the
`ZEROCLAW_PROVIDER_TIMEOUT_SECS` environment variable, defaulting
to 120s for backward compatibility.
Changes:
- Add `provider_timeout_secs` field to Config with serde default
- Add `ZEROCLAW_PROVIDER_TIMEOUT_SECS` env var override
- Add `timeout_secs` field and `with_timeout_secs()` builder on
`OpenAiCompatibleProvider`
- Add `provider_timeout_secs` to `ProviderRuntimeOptions`
- Thread config value through agent loop, channels, gateway, and tools
- Use `compat()` closure in provider factory to apply timeout to all
compatible providers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `agent.tool_call_dedup_exempt` config key (list of tool names) to
allow specific tools to bypass the within-turn identical-signature
deduplication check in run_tool_call_loop. This fixes the browser
snapshot polling use case where repeated calls with identical arguments
are legitimate and should not be suppressed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a `zeroclaw channel send` subcommand that sends a one-off message
through a configured channel without starting the full agent loop.
This enables hardware sensor triggers (e.g., range sensors on
Raspberry Pi) to push notifications to Telegram and other platforms.
Usage:
zeroclaw channel send 'Alert!' --channel-id telegram --recipient <chat_id>
Supported channels: telegram, discord, slack.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When running through a PTY chain (kubectl exec, SSH, remote terminals),
the transport layer may split data frames at space (0x20) boundaries,
interrupting multi-byte UTF-8 characters mid-sequence. Rust's
BufRead::read_line requires valid UTF-8 and returns InvalidData
immediately, crashing the interactive agent loop.
Replace stdin().read_line() with byte-level read_until(b'\n') followed
by String::from_utf8_lossy() in both the main input loop and the
/clear confirmation prompt. This reads raw bytes without UTF-8
validation during transport, then does lossy conversion (replacing any
truly invalid bytes with U+FFFD instead of crashing).
Also set ENV LANG=C.UTF-8 in both Dockerfile stages as defense-in-depth
to ensure the container locale defaults to UTF-8.
Closes#2984
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The credential leak detector's check_high_entropy_tokens would
false-positive on URL path segments (e.g. long alphanumeric filenames)
because extract_candidate_tokens included '/' in the token character
set, creating long mixed-alpha-digit tokens that exceeded the Shannon
entropy threshold.
Fix: strip URLs from content before extracting candidate tokens for
entropy analysis. Structural pattern checks (API keys, JWTs, AWS
credentials) use dedicated regexes and are unaffected.
Closes#3064
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
WebSearchTool previously stored the Brave API key once at boot and never
re-read it. This caused three failures: (1) keys set after boot via
web_search_config were ignored, (2) encrypted keys passed as raw enc2:
blobs to the Brave API, and (3) keys absent at startup left the tool
permanently broken.
The fix adds lazy key resolution at execution time. A fast path returns
the boot-time key when it is plaintext and non-empty. When the boot key
is missing or still encrypted, the tool re-reads config.toml, decrypts
the value through SecretStore, and uses the result. This also means
runtime config updates (e.g. `web_search_config set brave_api_key=...`)
are picked up on the next search invocation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace byte-level `&val[..4]` slice with `char_indices().nth(4)` to
prevent a panic when the captured credential value contains multi-byte
UTF-8 characters (e.g. Chinese text). Adds a regression test with
CJK input.
Closes#3024
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Extract `self.bot_handle.lock().take()` into a separate `let` binding
so the parking_lot::MutexGuard is dropped before the `.await`, making
the listen future Send again.
Closes#3312
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the `use_feishu: bool` field on `LarkChannel` with a
`platform: LarkPlatform` enum field, add `mention_only` to the
`new_with_platform` constructor, and introduce `from_lark_config` /
`from_feishu_config` factory methods so the channel factory in
`mod.rs` and the existing tests compile.
Resolves#3302
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Centralize cron shell command validation so all entrypoints enforce the
same security policy (allowlist + risk gate + approval) before
persistence and execution.
Changes:
- Add validate_shell_command() and validate_shell_command_with_security()
as the single validation gate for all cron shell paths
- Add add_shell_job_with_approval() and update_shell_job_with_approval()
that validate before persisting
- Add add_once_validated() and add_once_at_validated() for one-shot jobs
- Make raw add_shell_job/add_job/add_once/add_once_at pub(crate) to
prevent unvalidated writes from outside the cron module
- Route gateway API through validated creation path
- Route schedule tool through validated helpers (single validation)
- Route cron_add/cron_update tools through validated helpers
- Unify scheduler execution validation via validate_shell_command_with_security
- CLI update handler uses full validate_command_execution instead of
just is_command_allowed
- Add focused tests for validation parity across entrypoints
- Standardize error format to "blocked by security policy: {reason}"
Closes#2741Closes#2742
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cli): honor config default_temperature in agent command
Fixes#3033
The agent command was using a hardcoded default_value of 0.7 for the
--temperature parameter, which ignored the default_temperature setting
in the config file.
Changes:
- Changed temperature from f64 to Option<f64>
- Removed hardcoded default_value
- Use config.default_temperature when --temperature is not provided
Users can now set default_temperature in config.toml and have it
honored when running 'zeroclaw agent' without --temperature.
Risk: low (behavior change: now honors config instead of hardcoded value)
* fix: resolve moved value error for config in agent command
Extract final_temperature before passing config to agent::run() to
avoid use-of-moved-value error (config is moved as the first argument
while config.default_temperature was being accessed in the same call).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add vision support to Anthropic provider to enable image understanding:
- Add ImageSource struct for Anthropic's image content block format
- Add Image variant to NativeContentOut enum
- Implement capabilities() returning vision: true
- Update convert_messages() to parse [IMAGE:...] markers and convert
them to Anthropic's native image content blocks
- Support both data URIs and local file paths
- Add comprehensive tests for vision functionality
Fixes#3163
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add default_subject field to EmailConfig to allow users to customize
the default subject line for outgoing emails. Previously hardcoded as
"ZeroClaw Message".
- Add default_subject field with serde default
- Update send() method to use configured default
- Add tests for new functionality
Fixes#2878
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
When [web_fetch] section was specified in config without explicit
allowed_domains, serde used Vec::default() (empty vector) instead of
the wildcard ["*"] default. This caused all web fetch requests to be
rejected unexpectedly.
Fix by adding explicit serde default function that returns vec!["*"].
Fixes#2941
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add support for openai-responses, open-ai-responses, openai-chat-completions,
and open-ai-chat-completions as aliases for wire_api configuration values.
This aligns the parser with documented values that users expect to work.
Fixes#2735
* feat(onboard): add --reinit flag to prevent accidental config overwrite
Add --reinit flag to onboard command that:
- Backs up existing ~/.zeroclaw directory with timestamp
- Starts fresh initialization after backup
- Requires --interactive mode to work
- Prevents accidental configuration loss
This addresses issue #3013 where onboard could accidentally
overwrite all configuration without warning.
Closes#3013🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): SHA-pin all third-party GitHub Actions
Replace mutable version tags with immutable commit SHAs to prevent
tag-hijacking supply chain attacks (P1 finding).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: retrigger CI after startup_failure
* fix(onboard): address PR #3102 review issues for --reinit flag
- Use resolve_runtime_dirs_for_onboarding() instead of hardcoded ~/.zeroclaw
- Remove unsafe relative path fallback, bail instead
- Add user confirmation prompt before reinitializing config
- Update docs/reference/cli/commands-reference.md with --reinit docs
* style: fix cargo fmt and clippy violations
- Fix import ordering in src/config/mod.rs (rustfmt)
- Collapse single-arg encrypt/decrypt calls in src/config/schema.rs (rustfmt)
- Box::pin large onboard futures to fix clippy::large_futures in src/main.rs
These violations were blocking CI lint checks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Simian Astronaut 7 <simianastronaut7@gmail.com>
The daemon previously only handled SIGINT (Ctrl+C), ignoring SIGTERM
which is the standard termination signal used by Docker, Kubernetes,
and systemd. This caused containers to wait for the grace period
then be force-killed with SIGKILL.
Now the daemon handles both SIGINT and SIGTERM for graceful shutdown.
Fixes#2529
- Strip `<think>...</think>` blocks in parse_tool_calls(), XmlToolDispatcher,
and OllamaProvider before processing tool-call XML
- Add effective_content() fallback: when content is empty after stripping
think tags, check the thinking field for tool-call XML
- Add strip_think_tags() to ollama.rs, loop_.rs, and dispatcher.rs
- Add comprehensive tests for think-tag stripping and tool-call parsing
Fixes#3079
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- #3009: Add handle_api_integrations_settings endpoint returning JSON with
per-integration enabled/category/status so /api/integrations/settings no
longer falls through to the SPA static handler.
- #3010: Extract Sec-WebSocket-Protocol header in handle_ws_chat and echo
back "zeroclaw.v1" via ws.protocols() when the client requests it.
- #3038: Generate a persistent session_id (crypto.randomUUID stored in
sessionStorage) in the web WS client and pass it as a query parameter.
Add session_id: Option<String> to WsQuery on the server side so the
backend can key conversations by session across reconnects.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a test verifying Telegram bot_token is encrypted on save and
decrypted on load, and add .gitignore entries for local state backups.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wraps the WhatsApp Web listen() in a reconnect loop. When Event::LoggedOut
fires, the bot is torn down, stale session DB files are deleted, and after
exponential backoff (3s base, 300s cap, max 10 retries) the loop restarts
triggering fresh QR pairing.
- Broadcast channel for logout signaling from event handler to listen loop
- Session file cleanup (primary + WAL + SHM) only on explicit LoggedOut
- Proper resource ordering: client lock, abort handle, drop bot/device
- Tests for retry delay, counter, session purge, and file paths helpers
Adds symmetric encrypt/decrypt calls for all channel secret fields in
Config::save() and Config::load_or_init(). Previously only nostr.private_key
was handled, leaving all other channel secrets (bot_token, app_token,
access_token, api_token, password, etc.) and gateway.paired_tokens stored
as plaintext when secrets.encrypt = true.
Closes#3175, closes#3173.
Co-authored-by: jameslcowan
Adds opencode-go as a first-class provider with dedicated API endpoint,
env var, onboarding wizard wiring, and test coverage.
CI failures are pre-existing on master (Rust 1.94 formatting/lint changes per #3207).
The live tool call notifications feature sends extra messages to the
channel when tools are invoked. Update 5 tests that assumed exactly
1 sent message to instead check the last message in the list.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Enable a single Matrix bot instance to respond in multiple rooms:
- Disable the room_id filter so messages from all joined rooms are processed
- Embed room_id in reply_target as "user||room_id" for routing replies
- Include room_id in channel field for per-room conversation isolation
- Extract room_id from recipient in send() for correct message routing
The configured room_id still serves as a fallback for direct sends
without a "||" separator in the recipient.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Implement pin_message and unpin_message for the Matrix channel using
the m.room.pinned_events state event. Adds default no-op trait methods
to the Channel trait so other channel implementations are unaffected.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Implement add_reaction/remove_reaction for Matrix channel using
ReactionEventContent and redaction. Add threading support via
Relation::Thread in send() and thread_ts extraction from incoming
messages, enabling threaded conversations.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add ChannelNotifyObserver that wraps the observer to forward tool-call
events as real-time threaded messages on messaging channels. Include
tool arguments (truncated) in ToolCallStart events for better
visibility into what tools are doing. Auto-thread final replies when
tools were used.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
The system prompt is built once at daemon startup and cached. The
"Current Date & Time" section becomes stale immediately. This patch
replaces it with a fresh timestamp every time build_channel_system_prompt
is called (i.e. per incoming message).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When embedding_provider differs from default_provider (e.g. default=gemini,
embedding=openai), the caller-supplied api_key belongs to the chat provider.
Passing it to the embedding endpoint causes 401 Unauthorized (gemini key
sent to api.openai.com/v1/embeddings).
Add embedding_provider_env_key() which looks up OPENAI_API_KEY,
OPENROUTER_API_KEY, or COHERE_API_KEY before falling back to the
caller-supplied key. This matches the provider-specific env var resolution
in providers/mod.rs without introducing cross-module coupling.
Also add config_secrets_survive_save_load_roundtrip test: full save→load
cycle with channel credentials (telegram, discord, slack bot_token,
slack app_token) and gateway paired_tokens, verifying that enc2: values
are correctly decrypted by Config::load_or_init(). Regression guard for
issues #3173 and #3175.
Closes#3083
Co-authored-by: ZeroClaw Bot <zeroclaw_bot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
- `/model <name>` now auto-resolves provider from configured model_routes
by matching model name or hint, fixing 404 when switching to models on
different providers (e.g. `/model kimi-k2.5` with anthropic default)
- Conversation history is no longer cleared on `/model` or `/models` —
users can explicitly reset via `/new`
- Matrix channel now supports `/model`, `/models`, and `/new` commands
- `/model` (no args) lists configured model routes with hints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The onboard wizard futures exceed clippy's large_futures threshold
(16KB+). Wrap in Box::pin to heap-allocate and fix the lint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Import ordering in config/mod.rs and line-wrapping in config/schema.rs
were left unformatted by PR #2994. Run cargo fmt to fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HTTP clients had no timeouts and could hang forever; add 30s total and
10s connect timeouts. Replace clock-nanos-based jitter with rand::random
for proper randomness. Add a 1000-entry cap to the user display name
cache with expired-entry pruning. Fix truncate_text to avoid scanning
the full string twice when checking for truncation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Google TTS was passing the API key as a URL query parameter, which can
appear in logs and proxy access records. Move it to the x-goog-api-key
header instead. Add input validation for ElevenLabs voice IDs (reject
non-alphanumeric/dash/underscore characters) and restrict Edge TTS
binary_path to allowed basenames (edge-tts, edge-playback).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Slack private file fetching was sending the bot token on every redirect
hop. Since Slack CDN redirects use pre-signed URLs, sending the bearer
token to CDN hosts is unnecessary credential exposure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin endpoints (/admin/shutdown, /admin/paircode, /admin/paircode/new) were
completely unauthenticated, allowing any network client to shut down the gateway
or read/generate pairing codes. Add require_localhost() guard that returns 403
for non-loopback IPs.
Replace std::process::exit(0) in shutdown handler with a tokio watch channel
for graceful shutdown, allowing proper destructor cleanup and connection
draining. Replace the 500ms sleep race in the restart command with a poll loop
that waits for the port to actually become free.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add --new flag to GetPaircode command in src/lib.rs
- Update main.rs to handle GetPaircode { new } parameter
- Add /admin/paircode/new POST endpoint in gateway/mod.rs
- Enhance documentation for constant_time_eq security function
Refs: #3015
The bitwise & operator is intentional in constant_time_eq() to prevent
timing side-channel attacks. Both comparisons must always execute to
ensure constant-time behavior regardless of the first comparison result.
- Revert logical && back to bitwise &
- Add #[allow(clippy::needless_bitwise_bool)] annotation
- Add explanatory comment documenting the intentional use
- Add security warning for 0.0.0.0 binding in help text
- Implement proper gateway shutdown before restart via /admin/shutdown endpoint
- Fetch live pairing code from running gateway via /admin/paircode endpoint
- Extract duplicate code into helper functions
- Fix clippy warnings
- Fix Critical: Split illegal or-pattern (Some(...) | None) into separate match arms
- Fix Major: Implement restart command with graceful shutdown check
- Fix Major: Improve get-paircode to check gateway status and provide clear instructions
- Fix Minor: Update help text to document public-bind precondition
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add GatewayCommands enum with three subcommands:
- start: Start the gateway server (default behavior preserved)
- restart: Restart the gateway server
- get-paircode: Show current pairing status without restarting
This improves gateway management by allowing users to:
1. Restart gateway without manual stop/start
2. Check pairing status without disrupting running gateway
Closes#3014Closes#3015🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Modern macOS (Ventura+) stores iMessage content in the attributedBody
column as a binary typedstream blob rather than the text column. The
existing SQL filter `AND m.text IS NOT NULL` silently dropped all
incoming messages on affected systems.
Add a length-prefix extractor for the typedstream format and fall back
to attributedBody when text is NULL or empty. Includes real captured
blob fixtures and 14 new parser/integration tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduced a new function `check_api_key_prefix` to validate API key prefixes against their associated providers. This helps catch mismatches early in the process. Added unit tests to ensure correct functionality for various scenarios, including known and unknown key formats. This enhancement improves error handling and user guidance when incorrect provider keys are used.
Apply cargo fmt to fix formatting diffs in openrouter.rs and serial.rs.
Add web/dist placeholder step to lint, test, and build jobs so
RustEmbed compiles without the gitignored frontend assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Align src/peripherals/ and docs/hardware/ with the firmware directory renames
- Covers both compiled references (include_str!, constants) and documentation
Adds pluggable Text-to-Speech subsystem with TtsProvider trait,
TtsManager for provider selection, and per-provider config structs.
Includes secret encryption for TTS API keys.
- CI now builds across all 5 targets (linux x86/arm64, macOS x86/arm64,
Windows) matching the release matrix
- Fix chat_fails_without_credentials test to accept "builder error"
which occurs in CI environments without native TLS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Telegram message originates from a forum topic, the thread_id was
extracted and used for reply routing but never stored in ChannelMessage.thread_ts.
This caused all messages from the same sender to share conversation history
regardless of which topic they were posted in.
Changes:
- Set thread_ts to the extracted thread_id in parse_update_message,
try_parse_voice_message, and try_parse_attachment_message
- Use 'ref' in if-let patterns to avoid moving thread_id before it's assigned
- Update conversation_history_key() to include thread_ts when present,
producing keys like 'telegram_<thread_id>_<sender>' for forum topics
- Update conversation_memory_key() to also include thread_ts for memory isolation
This enables proper per-topic session isolation in Telegram forum groups while
preserving existing behavior for regular groups and DMs (where thread_ts is None).
Closes#1532