* feat: add desktop companion app integration and CI/CD pipeline
- Add `zeroclaw desktop` CLI command to launch/install companion app
- Add device-aware installer (desktop/server/mobile/embedded/container)
- Replace from-source Tauri build with pre-built .dmg download flow
- Add `build-desktop` job to beta and stable release workflows
- Build universal macOS .dmg via Tauri on macos-14 runners
- Include .dmg in GitHub Release assets alongside CLI binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: bump version to 0.6.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: update Cargo.lock for 0.6.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(security): wire LeakDetector into outbound message path
Wire LeakDetector.scan() into channel response sanitization (via
sanitize_channel_response) and cron job delivery (via
deliver_announcement) to prevent credential leakage to external
channels.
Changes:
- src/channels/mod.rs: Add leak detection to sanitize_channel_response()
with tracing::warn! on detection. Follows prior art from commit fb3b7b8e.
- src/cron/scheduler.rs: Add leak detection to deliver_announcement()
before sending output to any channel (Telegram, Discord, Slack,
Mattermost, Signal, Matrix, WhatsApp, QQ).
- Added unit tests in both modules to verify redaction behavior.
All outbound channel messages and cron job delivery are now scanned for
API keys, AWS credentials, JWTs, database URLs, private keys, and
high-entropy tokens. Detected leaks are redacted before delivery and
logged via tracing::warn! with pattern details.
Ref: SPEC-1.3-leak-detector-outbound.md (gap closure §1.3)
Prior art: commit fb3b7b8e (zeroclaw_homecomming branch)
* fix(security): enforce leak redaction with RedactedOutput newtype
Replace raw String return from scan_and_redact_output with a
RedactedOutput newtype. All channel dispatch in deliver_announcement
must go through this type, so the compiler rejects any path that
skips leak detection. Replaces tests that called LeakDetector in
isolation with tests that exercise scan_and_redact_output through
the RedactedOutput boundary.
* feat(channels): add message redaction API to Channel trait
Add redact_message() method to Channel trait with default no-op implementation.
Implement for MatrixChannel using existing room.redact() SDK method.
Add comprehensive test coverage for trait default and Matrix implementation.
Changes:
- src/channels/traits.rs: Add redact_message() default method and test
- src/channels/matrix.rs: Implement redact_message() for MatrixChannel
- tests/integration/channel_matrix.rs: Add RedactMessage event, test channel impl, and lifecycle test
Tests added:
- default_redact_message_returns_success (trait default)
- redact_message_lifecycle (MatrixTestChannel)
- Updated minimal_channel_all_defaults_succeed
All tests pass. No breaking changes.
* test(channels): clarify redact_message test coverage
* fix(channels): use OwnedRoomId for matrix redact_message room lookup
RoomId is unsized (wraps str), so inlining .parse() in
get_room(&target_room.parse()?) fails to compile with the
channel-matrix feature. Use an intermediate OwnedRoomId binding,
matching the pattern used in send() and listen().
* feat(ci): add per-component labels for channels, providers, and tools
labeler.yml only had base scope labels (channel, provider, tool) matching
entire directories. The PR workflow and ci-map specs already describe
fine-grained module labels (channel:telegram, provider:kimi, tool:shell)
but the labeler config never implemented them.
Adds 27 channel, 13 provider, and 12 tool group entries. Base labels are
preserved for the compaction rule in pr-labeler.yml.
* feat(ci): add pr-path-labeler workflow to invoke actions/labeler
labeler.yml has path-label config but no workflow was invoking it.
Adds pr-path-labeler.yml on pull_request_target to apply path and
per-component labels automatically. Updates actions source policy
doc to reflect the new workflow and action.
* docs(contributing): add label registry as single reference for all PR labels
Label definitions were scattered across labeler.yml, label-policy.json,
pr-workflow.md, and ci-map.md with no consolidated view. Adds a single
registry documenting all 99 labels by category, their definitions, how
each is applied, and which automation exists vs. is specced but missing.
* docs(contributing): correct label-registry automation status
The CI was simplified from 20+ workflows to 4. Size, risk,
contributor tier, and triage label automation (pr-labeler.yml,
pr-auto-response.yml, pr-check-stale.yml) were removed during
that simplification. Update the registry to reflect that these
labels are now applied manually, not "not yet implemented."
See PR #2931 for the upstream docs cleanup.
* feat(memory): add pgvector support and Postgres knowledge graph (#4028)
Enhance PostgresMemory with optional pgvector extension for hybrid
vector+keyword recall. Add namespace and importance columns. Create
PgKnowledgeGraph with recursive CTEs for graph traversal (no AGE
dependency). Extend ConsolidationResult with facts/trend fields for
richer extraction. All behind memory-postgres feature flag.
New file: knowledge_graph_pg.rs (5 unit tests)
Modified: postgres.rs (pgvector init, namespace/importance columns),
consolidation.rs (facts/trend fields), StorageProviderConfig
* fix: update PostgresMemory::new call in CLI with pgvector params
* fix: route WebSocket connections through configured proxy (#4408)
tokio_tungstenite::connect_async does not honour proxy settings. Add
proxy-aware WebSocket connect helpers (HTTP CONNECT and SOCKS5) in
config::schema and update all six channel WebSocket connections
(discord, discord_history, slack, dingtalk, lark, qq) to use
ws_connect_with_proxy instead of connect_async.
* fix: update Cargo.lock with tokio-socks dependency
Add heuristic complexity estimation (Simple/Standard/Complex) and
post-response quality evaluation to enable cheapest-capable model
routing. When rule-based classification misses, auto_classify falls
back to complexity-tier hints for model selection. Eval gate checks
response quality and suggests retry with higher-tier model if below
threshold.
New file: eval.rs (14 unit tests)
Modified: AgentConfig (eval, auto_classify fields), classify_model()
Fixes#4409. The git_operations tool now accepts an optional 'path'
parameter to specify a subdirectory within the workspace. Path
traversal outside the workspace is rejected for security.
Fixes#4442. Empty allowed_tools arrays caused all tools to be filtered
out, making cron jobs fail. Also adds tool name validation in the
OpenRouter provider to skip tools with names that violate the OpenAI
naming regex.
Fixes#4445. Docker/podman containers running in daemon mode had no
[autonomy] section in the baked config.toml, causing file_write and
file_edit tools to be auto-denied in non-interactive mode.
Add history pruning and context-aware tool filtering to reduce token
usage per API call. History pruner collapses old tool call/result pairs
and drops oldest messages when over budget. Context analyzer suggests
relevant tools per iteration based on keyword matching.
New files: history_pruner.rs, context_analyzer.rs
Modified: AgentConfig (new fields), ToolFilterGroup (filter_builtins),
agent loop (pruning integration)
Resolves#3540. Lark/Feishu channel was not included in default features, causing builds via cargo install, brew, and other standard install methods to produce binaries without Lark support. Also fixes pre-existing lark audio test failures that were previously hidden.
The concurrency group used github.sha for push events, giving each
merge its own group. Back-to-back merges queued multiple full CI runs
that never cancelled each other, exhausting runner capacity.
Use a fixed 'push-master' group so only the latest push to master
runs CI — older runs are cancelled automatically.
Add BedrockAuth enum supporting both SigV4 and BearerToken authentication.
BEDROCK_API_KEY env var takes precedence over AWS AKSK credentials.
When a Bearer token is provided, requests use Authorization: Bearer header
instead of SigV4 signing. Existing SigV4 auth remains fully backward
compatible.
Closes#3742
Add `[shell_tool]` section to config schema with `timeout_secs` field
(default 60s). The ShellTool struct now carries the timeout as an
instance field instead of relying on the hardcoded constant, and the
full tool registry reads the value from Config at construction time.
Closes#4331
Add a `zeroclaw skills test [name] [--verbose]` command that discovers
and executes TEST.sh files inside skill directories. Each line in a
TEST.sh follows the format `command | expected_exit_code | pattern` and
is validated against actual exit code and output (substring or regex).
- Add `Test` variant to `SkillCommands` enum in lib.rs
- Create `src/skills/testing.rs` with test runner, parser, and
pretty-printed results using the console crate
- Wire the new subcommand into `handle_command()` in skills/mod.rs
- Add example TEST.sh for the browser skill
- Include 14 unit tests covering parsing, pattern matching, execution,
aggregation, and edge cases
Closes#3697
Implement a new ask_user tool that sends a question to a messaging
channel and waits for the user's response with configurable timeout.
Supports optional structured choices and works across all channels
(Telegram, Discord, Slack, CLI, Web) via the late-bound channel map
pattern used by PollTool and ReactionTool.
Closes#3698
Migrate Telegram, Discord, Slack, WhatsApp Web, and Matrix channels
from inline TranscriptionConfig/transcribe_audio() calls to the
centralized TranscriptionManager pattern for unified provider selection,
fallback, and global cap enforcement.
Each channel now:
- Stores a transcription_manager: Option<Arc<TranscriptionManager>> field
- Builds the manager in with_transcription() from config
- Calls manager.transcribe() instead of the legacy transcribe_audio() shim
- Matrix replaces the whisper-cpp shell-out with the manager API
Closes#4311
Allow transcription of forwarded/regular audio messages on WhatsApp,
not just push-to-talk voice notes. Controlled by the new
`transcribe_non_ptt_audio` option in `[transcription]` (default: false).
Closes#4343
The tool-context summary (e.g. `[Used tools: browser_open]`) was
prepended to assistant history entries for all non-Telegram channels so
the LLM could retain awareness of prior tool usage. This caused the
model to learn and reproduce the bracket format in its own output,
delivering raw log lines to end-users instead of meaningful tool results.
Three changes:
1. Stop prepending `[Used tools: …]` to stored history for all channels.
The LLM already receives tool context through the tool-call/result
messages built by `run_tool_call_loop`, making the summary redundant.
2. Strip any `[Used tools: …]` prefix from cached history on reload, so
existing sessions are cleaned up retroactively.
3. Strip the prefix in `sanitize_channel_response` as a safety net, in
case the model still echoes it from older context.
Implement ClaudeCodeRunner that spawns Claude Code in a tmux session with
HTTP hooks to POST tool execution events back to ZeroClaw's gateway,
updating a Slack message in-place with progress, plus an SSH handoff link.
Components:
- src/tools/claude_code_runner.rs: session lifecycle (tmux spawn, hook
config generation, event handling, TTL cleanup)
- src/gateway/api.rs: POST /hooks/claude-code endpoint for hook events
- src/channels/slack.rs: chat.update + chat.postMessage with ts return
- src/config/schema.rs: ClaudeCodeRunnerConfig (enabled, ssh_host,
tmux_prefix, session_ttl)
- src/tools/mod.rs: register runner tool
Closes#4287
Add interactive /config command for Slack that renders Block Kit
static_select dropdowns for provider and model selection. Interactive
block_actions payloads from Socket Mode are translated into synthetic
/models and /model commands so existing runtime command handling
applies the selection to the per-sender RouteSelectionMap.
- Enable runtime model switching for the slack channel
- Add ShowConfig variant to ChannelRuntimeCommand
- Parse /config in parse_runtime_command (gated by supports_runtime_model_switch)
- Build Block Kit JSON with provider/model dropdowns and current selection
- Detect __ZEROCLAW_BLOCK_KIT__ prefix in SlackChannel::send() to post raw blocks
- Handle interactive envelope type in Socket Mode for block_actions events
- Non-Slack channels get a plain-text /config fallback
Closes#4286
Replace the AGENTS.md symlink with a real file containing all cross-tool
agent instructions. Reduce CLAUDE.md to Claude Code-specific directives
only, with a reference back to AGENTS.md for shared conventions.
Closes#4289
Resolves#3538. Docker builds only included memory-postgres in ZEROCLAW_CARGO_FEATURES, which meant Lark/Feishu long-connection (WebSocket) support was not compiled into container images.
Includes macOS desktop menu bar app, media pipeline channels,
SOP engine improvements, and CLI tool integrations.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tauri): add desktop app scaffolding with Tauri commands
Add Tauri v2 workspace member under apps/tauri with gateway client,
shared state, and Tauri command handlers for status, health, channels,
pairing, and agent messaging. The desktop app communicates with the
ZeroClaw gateway over HTTP and is buildable independently via
`cargo check -p zeroclaw-desktop`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tauri): add system tray icon, menu, and events
Add tray module with menu construction (Show Dashboard, Status, Open
Gateway, Quit) and event handlers. Wire tray setup into the Tauri
builder's setup hook. Add icons/ placeholder directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tauri): add mobile entry, platform capabilities, icons, and tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tauri): add auto-pairing, gradient icons, and WebView integration
Desktop app now auto-pairs with the gateway on startup using localhost
admin endpoints, injects the token into the WebView localStorage, and
loads the dashboard from the gateway URL. Adds blue gradient Z tray
icons (idle/working/error/disconnected states), proper .icns app icon,
health polling with tray updates, and Tauri-aware API/WS/SSE paths in
the web frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tauri): add dock icon, keep-alive for tray mode, and install preflight
- Set macOS dock icon programmatically via NSApplication API so it shows
the blue gradient Z even in dev builds without a .app bundle
- Add RunEvent::ExitRequested handler to keep app alive as a menu bar app
when all windows are closed
- Add desktop app preflight checks to install.sh (Rust, Xcode CLT,
cargo-tauri, Node.js) with automatic build on macOS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tools): add Codex CLI and Gemini CLI harness tools
Add two new ACP harness tools following the same pattern as ClaudeCodeTool:
- CodexCliTool: delegates to `codex -q` for OpenAI Codex CLI
- GeminiCliTool: delegates to `gemini -p` for Google Gemini CLI
Both tools share the same security model (rate limiting, act-policy
enforcement, workspace containment, env sanitisation, kill-on-drop
timeout) and are config-gated via `[codex_cli]` / `[gemini_cli]`
sections with `enabled = true`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tools): use 'codex exec' instead of invalid '-q' flag
* feat(tools): add OpenCode CLI harness tool
Add opencode_cli tool following the same pattern as codex_cli and
gemini_cli. Uses `opencode run "<prompt>"` for non-interactive
delegation. Includes config struct, factory registration, env
sanitization, timeout, and tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tools): correct CLI invocation args for codex and gemini tools
- Fix codex_cli to use `codex -q` (quiet mode) instead of incorrect
`codex exec` subcommand, matching the documented behavior and the
actual @openai/codex CLI interface.
- Fix gemini_cli install instruction to reference `@google/gemini-cli`
instead of the incorrect `@anthropic-ai/gemini-cli` package name.
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
* feat(tools): add background and parallel execution to delegate tool
Add three new execution modes to the delegate tool:
- background: when true, spawns the sub-agent in a background tokio task
and returns a task_id immediately. Results are persisted to
workspace/delegate_results/{task_id}.json.
- parallel: accepts an array of agent names, runs them all concurrently
with the same prompt, and returns all results when complete.
- action parameter with check_result/list_results/cancel_task support
for managing background task lifecycle.
Cascade control: background sub-agents use child CancellationToken
derived from the parent, enabling cancel_all_background_tasks() to
abort all running background agents when the parent session ends.
Existing synchronous delegation flow is fully preserved (opt-in only).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tools): validate task_id as UUID to prevent path traversal in delegate tool
The check_result and cancel_task actions accept a user-provided task_id
that is used directly in filesystem path construction. A malicious
task_id like "../../etc/passwd" could read or overwrite arbitrary files.
Since task_ids are always generated as UUIDs internally, this adds UUID
format validation before any filesystem operations, rejecting invalid
task_id values with a clear error message.
Also updates existing tests to use valid UUID-format task_ids and adds
dedicated path traversal rejection tests.
* fix: add missing attachments field to wati ChannelMessage after media pipeline merge
* fix(channels): add missing attachments field to voice_wake and lark
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(sop): add deterministic execution mode with typed steps and approval checkpoints
Add opt-in deterministic execution to the SOP workflow engine, inspired
by OpenClaw's Lobster engine. Deterministic mode bypasses LLM round-trips
for step transitions, executing steps sequentially with piped outputs.
Key additions:
- SopExecutionMode::Deterministic variant and `deterministic: true` SOP.toml flag
- SopStepKind enum (Execute/Checkpoint) for marking approval pause points
- StepSchema for typed input/output validation (JSON Schema fragments)
- DeterministicRunState for persisting/resuming interrupted workflows
- DeterministicSavings for tracking LLM calls saved
- SopRunAction::DeterministicStep and CheckpointWait action variants
- SopRunStatus::PausedCheckpoint status
- Engine methods: start_deterministic_run, advance_deterministic_step,
resume_deterministic_run, persist/load_deterministic_state
- SopConfig in config/schema.rs with sops_dir, default_execution_mode,
max_concurrent_total, approval_timeout_secs, max_finished_runs
- Wire `pub mod sop` in lib.rs (previously dead/uncompiled module)
- Fix pre-existing test issues: TempDir import, async test annotations
All 86 SOP core tests pass (engine: 42, mod: 17, dispatch: 13, types: 14).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(sop): resolve clippy warnings and fix metrics test failures
- Add `ampersona-gates = []` feature declaration to Cargo.toml to fix
clippy `unexpected cfg condition value` errors in sop/mod.rs,
sop/audit.rs, and sop/metrics.rs.
- Use dynamic Utc::now() timestamps in metrics test helper `make_run()`
instead of hardcoded 2026-02-19 dates, which had drifted outside the
7-day/30-day windowed metric windows causing 7 test failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(sop): remove non-functional ampersona-gates feature flag
The ampersona-gates feature flag referenced ampersona_core and
ampersona_engine crates that do not exist, causing cargo check
--all-features to fail. Remove the feature flag and all gated code:
- Remove ampersona-gates from Cargo.toml [features]
- Delete src/sop/gates.rs (entire module behind cfg gate)
- Remove gated methods from audit.rs (log_gate_decision, log_phase_state)
- Remove gated MetricsProvider impl and tests from metrics.rs
- Simplify sop_status.rs gate_eval field and append_gate_status
- Update observability docs (EN + zh-CN)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style(sop): fix formatting in metrics.rs
* fix(sop): wire SOP tools into tool registry
The five SOP tools (sop_list, sop_advance, sop_execute, sop_approve,
sop_status) existed as source files but were never registered in
all_tools_with_runtime. They are now conditionally registered when
sop.sops_dir is configured.
Also fixes:
- Add mod sop + SopCommands re-export to main.rs (binary crate)
- Handle new DeterministicStep/CheckpointWait variants in match arms
- Add missing struct fields (deterministic, kind, schema, llm_calls_saved)
to test constructors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(sop): fix state file leak and add deterministic execution tests
- Fix persist_deterministic_state fallback: use system temp dir instead
of current working directory when SOP location is unset, preventing
state files from polluting the working directory.
- Add comprehensive test coverage for deterministic execution path:
start, advance, checkpoint pause, completion with savings tracking,
and rejection of non-deterministic SOPs.
- Add tests for deterministic flag in TOML manifest loading and
checkpoint kind parsing from SOP.md.
- Add serde roundtrip tests for DeterministicRunState, SopStepKind,
SopExecutionMode::Deterministic, and SopRunStatus::PausedCheckpoint.
* ci: retrigger CI
* fix: add missing attachments field to wati ChannelMessage after media pipeline merge
* fix(channels): add missing attachments field to voice_wake and lark
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
- Add error codes to WS error messages (AGENT_INIT_FAILED, AUTH_ERROR, PROVIDER_ERROR, INVALID_JSON, UNKNOWN_MESSAGE_TYPE, EMPTY_CONTENT)
- Send Close frame with code 1011 on agent init failure
- Add tracing logs for agent init and turn errors
- Increase MAX_API_ERROR_CHARS from 200 to 500
- Frontend: handle abnormal close codes and show config error hints
Fixes#3681
Supersedes #4366
Original work by mark-linyb
Narration text from native tool-call providers that doesn't end with a newline now gets one appended before dispatch to the draft updater. Prevents garbled output in Telegram drafts.
Closes#4348
Drop delta_tx before awaiting draft_updater so the mpsc channel closes and the updater task can terminate. Without this, draft-capable channels (e.g. Telegram) hang indefinitely after the tool loop completes.
Closes#4300
* feat(channels): add automatic media understanding pipeline for inbound messages
Add MediaPipeline that pre-processes inbound channel message attachments
before the agent sees them:
- Audio: transcribed via existing transcription infrastructure, annotated
as [Audio transcription: ...]
- Images: annotated with [Image: <file> attached] (vision-aware)
- Video: annotated with [Video: <file> attached] (placeholder for future API)
The pipeline is opt-in via [media_pipeline] config section (default: disabled).
Individual media types can be toggled independently.
Changes:
- New src/channels/media_pipeline.rs with MediaPipeline struct and tests
- New MediaPipelineConfig in config/schema.rs
- Added attachments field to ChannelMessage for media pass-through
- Wired pipeline into process_channel_message after hooks, before agent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): add attachments field to integration test fixtures
Add missing `attachments: vec![]` to all ChannelMessage struct literals
in channel_matrix.rs and channel_routing.rs after the new attachments
field was added to the struct in traits.rs.
Also fix schema.rs test compilation: make TempDir import unconditional
and add explicit type annotations on tokio::fs calls to resolve type
inference errors in the bootstrap file tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): add missing attachments field to gmail_push and discord_history constructors
These channels were added to master after the media pipeline PR was
originally branched. The ChannelMessage struct now requires an
attachments field, so initialise it to an empty Vec for channels
that do not yet extract attachments.
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Agent::turn_streamed() that forwards TurnEvent (Chunk, ToolCall,
ToolResult) through an mpsc channel during execution. The WebSocket
gateway uses tokio::join! to relay these events to the client in real
time instead of waiting for the full turn to complete.
Introduce chunk_reset message type so the frontend clears its draft
buffer before the authoritative done message arrives. Update the React
AgentChat page to render streamed text live in the typing indicator
area, replacing the bounce-dot animation when content is available.
Backward-compatible: the done message still carries the full_response
field unchanged, and providers that do not support streaming fall back
to the non-streaming chat path transparently.
Closes#4372
Add the [cost] section to dev/config.template.toml and
examples/config.example.toml documenting all CostConfig options
(enabled, daily_limit_usd, monthly_limit_usd, warn_at_percent,
allow_override) with their defaults and commented-out model pricing
examples.
Fixes#4373
Add `let _ =` prefix to explicitly discard the Result from
`client.encryption().backups().disable().await`, suppressing the
`unused_must_use` compiler warning.
Closes#4374
* fix(transcription): remove deployment-specific WireGuard references from doc comments
LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.
* feat(wati): add audio and voice message transcription
* fix(wati): SSRF host validation, early fromMe check, download size cap
Validate that media_url host matches the configured api_url host before
fetching, preventing SSRF with credential leakage. Move the fromMe
check before the HTTP download to avoid wasting bandwidth on outgoing
messages. Add MAX_WATI_AUDIO_BYTES (25 MiB) Content-Length pre-check.
Skip empty transcripts in parse_audio_as_message. Short-circuit
with_transcription() when config.enabled is false.
* fix(wati): remove dead field, enforce streaming size cap, log errors
- Remove unused transcription config field (clippy dead code)
- Use streaming download to enforce size cap without Content-Length
- Log network errors instead of silently swallowing with .ok()?
---------
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
* fix(transcription): remove deployment-specific WireGuard references from doc comments
LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.
* feat(mattermost): add audio transcription via TranscriptionManager
Add transcription support for Mattermost audio attachments. Routes
audio through TranscriptionManager when configured, with duration
limit enforcement and wiremock-based integration tests.
* fix(mattermost): add download size cap, HTTP status check, warn logging
Replace chained .ok()? calls in try_transcribe_audio_attachment with
explicit error handling that logs warnings on HTTP failure, non-success
status codes, and oversized files. Add MAX_MATTERMOST_AUDIO_BYTES
(25 MiB) Content-Length pre-check. Remove mp4 and webm from the
extension-only fallback in is_audio_file(). Short-circuit
with_transcription() when config.enabled is false.
* fix(mattermost): add [Voice] prefix, filter empty, fix config init
- Add [Voice] prefix to transcribed audio matching Slack/Discord
- Filter empty transcription results
- Only store transcription config on successful manager init
- Update test expectation for [Voice] prefix
---------
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
* fix(transcription): remove deployment-specific WireGuard references from doc comments
LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.
* feat(lark): add audio message transcription
* test(channels): add three missing lark audio transcription tests
- lark_audio_skips_when_manager_none: parse_event_payload_async returns
empty when transcription_manager is None
- lark_audio_routes_through_transcription_manager: wiremock end-to-end
test proving file_key → download → whisper → ChannelMessage chain
- lark_audio_token_refresh_on_invalid_token_response: wiremock test
verifying 401 triggers token invalidation and retry
Adds a #[cfg(test)] api_base_override field to LarkChannel so wiremock
can intercept requests that normally go to hardcoded Lark/Feishu API
base URLs.
* fix(lark): add audio download size cap, event_type guard, skip disabled transcription
Add MAX_LARK_AUDIO_BYTES (25 MiB) Content-Length pre-check before
reading audio response bodies on both the normal and token-refresh
paths. Add the missing im.message.receive_v1 event_type guard in
parse_event_payload_async so non-message callbacks are rejected before
the audio branch. Short-circuit with_transcription() when
config.enabled is false.
* fix(lark): address review feedback for audio transcription
- Wire parse_event_payload_async in webhook handler (was dead code)
- Use streaming download with enforced size cap (no Content-Length bypass)
- Fix test: lark_manager_some_when_valid_config asserted is_some with enabled=false
- Fix test: add missing header to lark_audio_skips_when_manager_none payload
- Remove redundant group-mention check in audio arm (shared check covers it)
---------
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
* fix(security): update blocked_commands_basic test after allowing python/node
After adding python3 and node to default_allowed_commands in #4338,
the test asserting they are blocked is now wrong. Replace with ruby
and perl which remain correctly blocked.
* fix(security): also fix python3 reference in pipe validation test
The command_with_pipes_validates_all_segments test also referenced
python3 in a blocked assertion. Replace with ruby.
Force explicit Gregorian year/month/day via Datelike/Timelike traits instead of chrono format() which inherits system locale (e.g. Buddhist calendar on Thai systems). Prepend datetime before memory context in user messages. Rename DateTimeSection header to CRITICAL CONTEXT.
* feat(tools): add cross-channel poll creation tool
Adds a poll tool that enables cross-channel poll creation with voting
support. Changes all_tools_with_runtime return type from 3-tuple to
4-tuple to accommodate the new reaction handle.
Original PR #4243 by rareba.
* ci: retrigger CI
* feat(tools): add Firecrawl fallback for JS-heavy and bot-blocked sites
Adds optional Firecrawl API integration as a fallback when standard web
fetching fails due to JavaScript-heavy pages or bot protection. Includes
configurable API key, timeout, and domain allowlist/blocklist.
Original PR #4244 by rareba.
* ci: retrigger CI
When a downloaded binary has the wrong architecture, the previous code
attempted to execute it and surfaced a raw "Exec format error (os error 8)".
Now validate_binary reads the ELF/Mach-O header first, compares the binary
architecture against the host, and reports a clear diagnostic like:
"architecture mismatch: downloaded binary is aarch64 but this host is x86_64".
Closes#4291
- Prefer HOME env var in default_config_dir() before falling back to UserDirs::new()
- Pre-create temp_default_dir in test so is_temp_directory() can canonicalize /var -> /private/var symlink on macOS
Move supports_runtime_model_switch() guard from parse_runtime_command() entry to /model and /models match arms only, so /new is available on every channel.
Closes#4236
* feat(tools): add LLM task tool for structured JSON-only sub-calls
Add LlmTaskTool — a lightweight tool that runs a single prompt through
an LLM provider with no tool access and optionally validates the
response against a caller-supplied JSON Schema. Ideal for structured
data extraction, classification, and transformation in workflows.
- Parameters: prompt (required), schema (optional JSON Schema),
model (optional override), temperature (optional override)
- Uses configured default provider/model from root config
- Validates response JSON against schema (required fields, type checks)
- Strips markdown code fences from LLM responses before validation
- Gated by ToolOperation::Act security policy
- Registered in all_tools_with_runtime (always available)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use non-constant value instead of approximate PI in tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(agent): add loop detection guardrail for repetitive tool calls
Introduces a LoopDetector that monitors a sliding window of recent tool
calls and detects three repetitive patterns:
1. Exact repeat — same tool+args called consecutively (default 3+)
2. Ping-pong — two tools alternating for 4+ cycles
3. No progress — same tool with different args but identical results (5+)
Each pattern escalates through Warning -> Block -> CircuitBreaker.
Configurable via [pacing] section: loop_detection_enabled (default true),
loop_detection_window_size (default 20), loop_detection_max_repeats
(default 3).
Wired into run_tool_call_loop alongside the existing time-gated
identical-output detector. Respects loop_ignore_tools exclusion list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): fix channel test interaction with loop detector
The max_tool_iterations channel tests use an IterativeToolProvider that
intentionally repeats identical tool calls. The loop detector (enabled by
default) fires its circuit breaker before max_tool_iterations is reached,
causing the test to fail. Disable loop detection in these two tests so
they exercise only the max_tool_iterations boundary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): address PR #4240 review — loop detector correctness and test precision
Critical fixes:
- Fix result_index/tool_calls misalignment: use enumerate() before
filter_map() so the index stays aligned with tool_calls even when
ordered_results contains None entries from skipped tool calls.
- Fix hash_value JSON key-order sensitivity: canonicalise() recursively
sorts object keys before serialisation so {"a":1,"b":2} and
{"b":2,"a":1} hash identically.
Tightened test assertions:
- ping_pong_escalates_with_more_cycles: assert Block with 5 cycles
(was loose Warning|Block|Break match).
- no_progress_escalates_to_block_and_break: assert Break at 7 calls
(was loose Block|Break match).
- no_progress_not_triggered_when_all_args_identical: assert Warning
specifically (was accepting Ok as alternative).
New tests:
- ping_pong_detects_alternation_with_varying_args (item 3)
- window_eviction_prevents_stale_pattern_detection (item 4)
- hash_value_is_key_order_independent + nested variant (item 2)
- pacing_config_serde_defaults_match_manual_default (item 5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a skill directory contains script files and `skills.allow_scripts`
is false (the default), the skill was silently skipped with only a
tracing::warn that most users never see. The LLM then reports "tool is
not available in this environment" with no guidance on how to fix it.
Now the loader emits a user-visible stderr warning that names the
skipped skill and suggests setting `skills.allow_scripts = true`.
Closes#4292
The previous substring match (e.g. "aarch64-unknown-linux") could match
both gnu and android release assets. Switch to full triples like
"aarch64-unknown-linux-gnu" so find_asset_url only selects the correct
platform binary.
Closes#4293
Cost tracking was disabled by default (enabled: false), causing the
/api/cost endpoint to always return zeros unless users explicitly
opted in. Change the default to enabled: true so cost tracking works
out of the box for all models and channels.
The prometheus crate requires AtomicU64 which is unavailable on 32-bit
ARM (armv7l/armv6l). Detect 32-bit ARM in install.sh and build with
--no-default-features, re-enabling only channel-nostr and
skill-creation (excluding observability-prometheus).
Previously, the /ws/chat handler silently ignored messages with
unrecognized types, leaving clients waiting for a response that never
comes. Now sends explicit error messages describing the expected format.
When users configure the Ollama api_url with the full endpoint path
(e.g. http://host:11434/api/chat), normalize_base_url only stripped
/api but not /api/chat, causing the final request URL to become
/api/chat/api/chat which fails on remote servers.
Closes#4342
The default_allowed_commands() list was missing common scripting
runtimes. After #3691 added serde defaults to AutonomyConfig, users
who omitted allowed_commands from their config silently fell back to
the restrictive default list, breaking shell tool access to Python.
Closes#4338
- Add tag-push trigger to Release Stable workflow so `git push origin v0.5.9`
auto-triggers the full pipeline (builds, Docker, crates.io, website,
Scoop, AUR, Homebrew, tweet) in one shot
- Add Homebrew Core as downstream job (was manual-only, never auto-triggered)
- Add `workflow_call` to pub-homebrew-core.yml so it can be called from
the stable release workflow
- Skip beta releases on version bump commits (prevents beta/stable race)
- Skip auto crates.io publish when stable tag exists (prevents double-publish)
- Auto-create git tag on manual dispatch so tag always exists for downstream
- Fix cut_release_tag.sh to reference correct workflow name
Closes#4231. Adds voice memo detection and transcription for Slack and Discord channels. Audio files are downloaded, transcribed via the existing transcription module, and passed as text to the LLM.
Closes#4235. Adds image and file message type handling in the Lark channel - downloads images/files via Lark API, detects MIME types, and passes content to the model for analysis.
* feat(agent): add thinking/reasoning level control per message
Users can set reasoning depth via /think:high etc. with resolution
hierarchy (inline > session > config > default). 6 levels from Off
to Max. Adjusts temperature and system prompt.
* fix(agent): prevent thinking level prefix from leaking across interactive turns
system_prompt was mutated in place for the first message's thinking
directive, then used as the "baseline" for restoration after each
interactive turn. This caused the first turn's thinking prefix to
persist across all subsequent turns.
Fix: save the original system_prompt before any thinking modifications
and restore from that saved copy between turns.
* feat(channels): add automatic link understanding for inbound messages
Auto-detects URLs in inbound messages, fetches content, extracts
title+summary, and enriches the message before the agent sees it.
Includes SSRF protection (rejects private IPs).
* fix(channels): use lowercased string for title extraction to prevent byte offset mismatch
extract_title used byte offsets from the lowercased HTML to index into
the original HTML. Multi-byte characters whose lowercase form has a
different byte length (e.g. İ → i̇) would produce wrong slices or panics.
Fix: extract from the lowercased string directly. Add multibyte test.
* feat(tools): enable internet access by default
Enable web_fetch, web_search, http_request, and browser tools by
default so ZeroClaw has internet access out of the box. Security
remains fully toggleable via config (set enabled = false to disable).
- web_fetch: enabled with allowed_domains = ["*"]
- web_search: enabled with DuckDuckGo (free, no API key)
- http_request: enabled with allowed_domains = ["*"]
- browser: enabled with allowed_domains = ["*"], agent_browser backend
- text_browser: remains opt-in (requires external binary)
* fix(tests): update component test for browser enabled by default
Update config_nested_optional_sections_default_when_absent to expect
browser.enabled = true, matching the new default.
Add support for defining cron jobs directly in the TOML config file via
`[[cron.jobs]]` array entries. Declarative jobs are synced to the SQLite
database at scheduler startup with upsert semantics:
- New declarative jobs are inserted
- Existing declarative jobs are updated to match config
- Stale declarative jobs (removed from config) are deleted
- Imperative jobs (created via CLI/API) are never modified
Each declarative job requires a stable `id` for merge tracking. A new
`source` column (`"imperative"` or `"declarative"`) distinguishes the
two creation paths. Shell jobs require `command`, agent jobs require
`prompt`, validated before any DB writes.
Apply exponential time decay (2^(-age/half_life), 7-day half-life) to
memory entry scores post-recall. Core memories are exempt (evergreen).
Consolidate duplicate half-life constants into a single public constant
in the decay module.
Based on PR #4266 by 5queezer with constant consolidation fix.
Apply PR #4258 changes to add whatsapp/whatsapp-web/whatsapp_web match
arm in deliver_announcement, feature-gated behind whatsapp-web.
Added is_web_config() guard to bail early when the WhatsApp config is
for Cloud API mode (no session_path), preventing a confusing runtime
failure with an empty session path.
* fix(cron): add WhatsApp Web delivery channel with backend validation
Apply PR #4258 changes to add whatsapp/whatsapp-web/whatsapp_web match
arm in deliver_announcement, feature-gated behind whatsapp-web.
Added is_web_config() guard to bail early when the WhatsApp config is
for Cloud API mode (no session_path), preventing a confusing runtime
failure with an empty session path.
* feat(gateway): add named sessions with human-readable labels
Apply PR #4267 changes with bug fixes:
- Add get_session_name trait method so WS session_start includes the
stored name on reconnect (not just when ?name= query param is present)
- Rename API now returns 404 for non-existent sessions instead of
silently succeeding
- Empty ?name= query param on WS connect no longer clears existing name
Skill tools defined in [[tools]] sections are now registered as first-class
callable tool specs via the Tool trait, rather than only appearing as XML
in the system prompt. This enables the LLM to invoke skill tools through
native function calling.
- Add SkillShellTool for shell/script kind skill tools
- Add SkillHttpTool for http kind skill tools
- Add skills_to_tools() conversion and register_skill_tools() wiring
- Wire registration into both CLI and process_message agent paths
- Update prompt rendering to mark registered tools as callable
- Update affected tests across skills, agent/prompt, and channels
Define the contract for long-lived shared state in multi-client tool
execution, covering ownership (handle pattern), identity assignment
(daemon-provided ClientId), lifecycle (validation at registration),
isolation (per-client for security state), and reload semantics
(config hash invalidation).
Add an `allowed_rooms` field to MatrixConfig that controls which rooms
the bot will accept messages from and join invites for. When the list
is non-empty, messages from unlisted rooms are silently dropped and
room invites are auto-rejected. When empty (default), all rooms are
allowed, preserving backward compatibility.
- Config: add `allowed_rooms: Vec<String>` with `#[serde(default)]`
- Message handler: replace disabled room_id filter with allowlist check
- Invite handler: auto-accept allowed rooms, auto-reject others
- Support both canonical room IDs and aliases, case-insensitive
Parse forward_from, forward_from_chat, and forward_sender_name fields
from Telegram message updates. Prepend forwarding attribution to message
content so the LLM has context about the original sender.
Closes#4118
When vision_provider is configured in [multimodal] config, messages
containing [IMAGE:] markers are automatically routed to the specified
vision-capable provider instead of failing on the default text provider.
Closes#4119
Add a piper TTS provider that communicates with a local Piper/Coqui TTS
server via an OpenAI-compatible HTTP endpoint. This enables fully offline
voice pipelines: Whisper (STT) → LLM → Piper (TTS).
Closes#4116
When a user provides a custom `auto_approve` list in their TOML
config (e.g. to add an MCP tool), serde replaces the built-in
defaults instead of merging. This causes default safe tools like
`weather`, `calculator`, and `file_read` to lose auto-approve
status and get silently denied in non-interactive channel runs.
Add `ensure_default_auto_approve()` which merges built-in entries
into the user's list after deserialization, preserving user
additions while guaranteeing defaults are always present. Users
who want to require approval for a default tool can use
`always_ask`, which takes precedence.
Closes#4247
Wrap `fetch_live_models_for_provider` calls in
`tokio::task::spawn_blocking` so the `reqwest::blocking::Client`
is created and dropped on a dedicated thread pool instead of
inside the async Tokio context. This prevents the
"Cannot drop a runtime in a context where blocking is not allowed"
panic when running `models refresh --provider openai`.
Closes#4253
Fixed issue #4139.
Previously, slicing a string at exactly 64 bytes could land in the middle of a multi-byte UTF-8 character (e.g., Chinese characters), causing a runtime panic.
Changes:
- Replaced direct byte slicing with a safe boundary lookup using .char_indices().
- Ensures truncation always occurs at a valid character boundary at or before the 64-byte limit.
- Maintained existing hyphen-trimming logic.
Co-authored-by: loriscience <loriscience@gmail.com>
Explicitly call `client.encryption().backups().disable()` when backups
are not enabled, preventing the matrix_sdk_crypto crate from attempting
room key backups on every sync cycle and spamming the logs with
"Trying to backup room keys but no backup key was found" warnings.
Closes#4227
* feat(channels): add voice wake word detection channel
Add VoiceWakeChannel behind the `voice-wake` feature flag that:
- Captures audio from the default microphone via cpal
- Uses energy-based VAD to detect speech activity
- Transcribes speech via the existing transcription API (Whisper)
- Checks for a configurable wake word in the transcription
- On detection, captures the following utterance and dispatches it
as a ChannelMessage
State machine: Listening -> Triggered -> Capturing -> Processing -> Listening
Config keys (under [channels_config.voice_wake]):
- wake_word (default: "hey zeroclaw")
- silence_timeout_ms (default: 2000)
- energy_threshold (default: 0.01)
- max_capture_secs (default: 30)
Includes tests for config parsing, state machine, RMS energy
computation, and WAV encoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): exclude voice-wake from all-features CI check
Add a `ci-all` meta-feature in Cargo.toml that includes every feature
except `voice-wake`, which requires `libasound2-dev` (ALSA) not present
on CI runners. Update the check-all-features CI job to use
`--features ci-all` instead of `--all-features`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(channels): add Gmail Pub/Sub push notifications for real-time email
Add GmailPushChannel that replaces IMAP polling with Google's Pub/Sub
push notification system for real-time email-driven automation.
- New channel at src/channels/gmail_push.rs implementing the Channel trait
- Registers Gmail watch subscription (POST /gmail/v1/users/me/watch)
with automatic renewal before the 7-day expiry
- Handles incoming Pub/Sub notifications at POST /webhook/gmail
- Fetches new messages via Gmail History API (startHistoryId-based)
- Dispatches email messages to the agent with full metadata
- Sends replies via Gmail messages.send API
- Config: gmail_push.enabled, topic, label_filter, oauth_token,
allowed_senders, webhook_url
- OAuth token encrypted at rest via existing secret store
- Webhook endpoint added to gateway router
- 30+ unit tests covering notification parsing, header extraction,
body decoding, sender allowlist, and config serialization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): fix extract_body_text_plain test
Gmail API sends base64url without padding. The decode_body function
converted URL-safe chars back to standard base64 but did not restore
the padding, causing STANDARD decoder to fail and falling back to
snippet. Add padding restoration before decoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a process-scoped Mutex that all env-var-mutating tests in
openai_codex::tests must hold. This prevents std::env::set_var /
remove_var calls from racing when Rust's test harness runs them on
parallel threads.
Affected tests:
- resolve_responses_url_prefers_explicit_endpoint_env
- resolve_responses_url_uses_provider_api_url_override
- resolve_reasoning_effort_prefers_configured_override
- resolve_reasoning_effort_uses_legacy_env_when_unconfigured
- Use char_indices for safe UTF-8 truncation instead of byte slicing
- Replace unbounded Vec with VecDeque rolling window in load_jsonl_messages
- Add path separator validation for channel/to to prevent directory traversal
When `load_session_context = true` in `[heartbeat]`, the daemon loads the
last 20 messages from the target user's JSONL session file and prepends them
to the heartbeat task prompt before calling the LLM.
This gives the companion context — who the user is, what was last discussed —
so outreach messages feel like a natural continuation rather than a blank-slate
ping. Defaults to `false` (opt-in, no change to existing behaviour).
Key behaviours:
- Session context is re-read on every heartbeat tick (not cached at startup)
- Skips context injection if only assistant messages are present (prevents
heartbeat outputs feeding back in a loop)
- Scans sessions directory for matching JSONL files using flexible filename
matching: {channel}_{to}.jsonl, {channel}_*_{to}.jsonl, or
{channel}_{to}_*.jsonl — handles varying session key formats
- Injects file mtime as "last message ~Xh ago" so the LLM knows how long
the user has been silent
Config example:
[heartbeat]
enabled = true
interval_minutes = 120
load_session_context = true
target = "telegram"
to = "your_username"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent Tools and CLI Tools section headings were static divs with no
way to collapse sections the user is not interested in, making the
page unwieldy with a large tool set.
- Convert both section heading divs to button elements toggling
agentSectionOpen / cliSectionOpen state (both default open)
- Section content renders conditionally on those booleans
- ChevronsUpDown icon added (already in lucide-react bundle) that
fades in on hover and indicates collapsed/expanded state
- No change to individual tool card parameter schema expand/collapse
Risk: Low — UI state only, no API or logic change.
Does not change: search/filter behaviour, tool card expand/collapse,
CLI tools table structure.
Three issues addressed:
Empty page: the logs page shows nothing at idle because the SSE stream
only carries ObserverEvent variants (llm_request, tool_call, error,
agent_start, agent_end). Daemon stdout and RUST_LOG tracing output go
to the terminal/log file and are never forwarded to the broadcast
channel — this is correct behaviour, not a misconfiguration. Added a
dismissible informational banner explaining what appears on the stream
and how to access tracing output (RUST_LOG + terminal).
Layout: flex-1 log entries div was missing min-h-0, which can cause
flex children to refuse to shrink below content size in some browsers.
Connection indicator: moved from the toolbar (where it cluttered the
title and controls) to a compact footer strip below the scroll area,
matching the /agent page pattern exactly.
Also added colour rules for llm_request, agent_start, agent_end,
tool_call_start event types which previously fell through to default
grey.
Risk: Low — UI layout and informational copy only, no backend change.
Does not change: SSE connection logic, event parsing, pause/resume,
type filters, or the underlying broadcast observer.
Two issues with the config editor:
Layout: the page root had no height constraint and the textarea used
min-h-[500px] resize-y, causing independent scrollbars on both the
page and the editor. Fixed by adopting the Memory/Cron flex column
pattern so the editor fills the remaining viewport height with a single
scroll surface.
Highlighting: plain textarea with no visual structure for TOML.
Added a zero-dependency layered pre-overlay technique — no new npm
packages (per CLAUDE.md anti-pattern rules). A pre element sits
absolute behind a transparent textarea; highlightToml() produces HTML
colour-coding sections, keys, strings, booleans, numbers, datetimes,
and comments via per-line regex. onScroll syncs the overlay. Tab key
inserts two spaces instead of leaving focus.
dangerouslySetInnerHTML used on the pre — content is the user's own
local config, not from the network, risk equivalent to any local editor.
Risk: Low-Medium — no API or backend change. New rendering logic
in editor only.
Does not change: save/load API calls, validation, sensitive field
masking behaviour.
The cron page used a block-flow root with no height constraint, causing
the jobs table to grow taller than the viewport and the page itself to
scroll. This was inconsistent with the Memory page pattern.
- Change page root to flex flex-col h-full matching Memory's layout
- Table wrapper gains flex-1 min-h-0 overflow-auto so it fills
remaining height and scrolls both axes internally
- Table header already has position:sticky so it pins correctly
inside the scrolling container with no CSS change needed
Risk: Low — layout only, no logic or API change.
Does not change: job CRUD, modal, catch-up toggle, run history panel.
The card heading used the key dashboard.active_channels ("Active Channels")
even though the card has a toggle between Active and All views, making the
static heading misleading. The channel list div had no height cap, causing
tall channel lists to stretch the card and break 3-column grid alignment.
- Change heading to t("dashboard.channels") — key already present in all
three locales (zh/en/tr), no i18n changes needed
- Add overflow-y-auto max-h-48 pr-1 to the channel list wrapper so it
scrolls internally instead of stretching the card
* feat: add discord history logging and search tool with persistent channel cache
* fix: remove unused channel_names field from DiscordHistoryChannel
The channel_names HashMap was declared and initialized but never used.
Channel name caching is handled via discord_memory.get()/store() with
the cache:channel_name: prefix. Remove the dead field.
* style: run cargo fmt on discord_history.rs
---------
Co-authored-by: ninenox <nisit15@hotmail.com>
* fix(web/tools): make section headings collapsible
Agent Tools and CLI Tools section headings were static divs with no
way to collapse sections the user is not interested in, making the
page unwieldy with a large tool set.
- Convert both section heading divs to button elements toggling
agentSectionOpen / cliSectionOpen state (both default open)
- Section content renders conditionally on those booleans
- ChevronsUpDown icon added (already in lucide-react bundle) that
fades in on hover and indicates collapsed/expanded state
- No change to individual tool card parameter schema expand/collapse
Risk: Low — UI state only, no API or logic change.
Does not change: search/filter behaviour, tool card expand/collapse,
CLI tools table structure.
* fix(web/tools): improve a11y and fix invalid HTML in collapsible sections
- Replace <h2> inside <button> with <span role="heading" aria-level={2}>
to fix invalid HTML (heading elements not permitted in interactive content)
- Add aria-expanded attribute to section toggle buttons for screen readers
- Add aria-controls + id linking buttons to their controlled sections
- Replace ChevronsUpDown with ChevronDown icon — ChevronsUpDown is
visually symmetric so rotating 180deg has no visible effect; ChevronDown
rotating to -90deg gives a clear directional cue
- Remove unused ChevronsUpDown import
---------
Co-authored-by: WareWolf-MoonWall <chris.hengge@gmail.com>
* fix: resolve claude-code test flakiness and update security policy
* fix: restrict `free` command to Linux-only in security policy
`free` is not available on macOS or other BSDs. Move it behind
a #[cfg(target_os = "linux")] gate so it is only included in the
default allowed commands on Linux systems.
---------
Co-authored-by: ninenox <nisit15@hotmail.com>
* feat(channels): add Gmail Pub/Sub push notifications for real-time email
Add GmailPushChannel that replaces IMAP polling with Google's Pub/Sub
push notification system for real-time email-driven automation.
- New channel at src/channels/gmail_push.rs implementing the Channel trait
- Registers Gmail watch subscription (POST /gmail/v1/users/me/watch)
with automatic renewal before the 7-day expiry
- Handles incoming Pub/Sub notifications at POST /webhook/gmail
- Fetches new messages via Gmail History API (startHistoryId-based)
- Dispatches email messages to the agent with full metadata
- Sends replies via Gmail messages.send API
- Config: gmail_push.enabled, topic, label_filter, oauth_token,
allowed_senders, webhook_url
- OAuth token encrypted at rest via existing secret store
- Webhook endpoint added to gateway router
- 30+ unit tests covering notification parsing, header extraction,
body decoding, sender allowlist, and config serialization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): fix extract_body_text_plain test
Gmail API sends base64url without padding. The decode_body function
converted URL-safe chars back to standard base64 but did not restore
the padding, causing STANDARD decoder to fail and falling back to
snippet. Add padding restoration before decoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): address critical security bugs in Gmail Pub/Sub push
- Add webhook authentication via shared secret (webhook_secret config
field or GMAIL_PUSH_WEBHOOK_SECRET env var), preventing unauthorized
message injection through the unauthenticated webhook endpoint
- Add 1MB body size limit on webhook endpoint to prevent memory exhaustion
- Fix race condition in handle_notification: hold history_id lock across
the read-fetch-update cycle to prevent duplicate message processing
when concurrent webhook notifications arrive
- Sanitize RFC 2822 headers (To/Subject) to prevent CRLF injection
attacks that could add arbitrary headers to outgoing emails
- Fix extract_email_from_header panic on malformed angle brackets by
using rfind('>') and validating bracket ordering
- Add 30s default HTTP client timeout for all Gmail API calls,
preventing indefinite hangs
- Clone tx sender before message processing loop to avoid holding
the mutex lock across network calls
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(gateway): add Live Canvas (A2UI) tool and real-time web viewer
Add a Live Canvas system that enables the agent to push rendered content
(HTML, SVG, Markdown, text) to a web-visible canvas in real time.
Backend:
- src/tools/canvas.rs: CanvasTool with render/snapshot/clear/eval actions,
backed by a shared CanvasStore (Arc<RwLock<HashMap>>) with per-canvas
broadcast channels for real-time updates
- src/gateway/canvas.rs: REST endpoints (GET/POST/DELETE /api/canvas/:id,
GET /api/canvas/:id/history, GET /api/canvas) and WebSocket endpoint
(WS /ws/canvas/:id) for real-time frame delivery
Frontend:
- web/src/pages/Canvas.tsx: Canvas viewer page with WebSocket connection,
iframe sandbox rendering, canvas switcher, frame history panel
Registration:
- CanvasTool registered in all_tools_with_runtime (always available)
- Canvas routes wired into gateway router
- CanvasStore added to AppState
- Canvas page added to App.tsx router and Sidebar navigation
- i18n keys added for en/zh/tr locales
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): share CanvasStore between tool and REST API
The CanvasTool and gateway AppState each created their own CanvasStore,
so content rendered via the tool never appeared in the REST API.
Create the CanvasStore once in the gateway, pass it to
all_tools_with_runtime via a new optional parameter, and reuse the
same instance in AppState.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): address critical security and reliability bugs in Live Canvas
- Validate content_type in REST POST endpoint against allowed set,
preventing injection of "eval" frames via the REST API
- Enforce MAX_CONTENT_SIZE (256KB) limit on REST POST endpoint,
matching tool-side validation to prevent memory exhaustion
- Add MAX_CANVAS_COUNT (100) limit to prevent unbounded canvas creation
and memory exhaustion from CanvasStore
- Handle broadcast RecvError::Lagged in WebSocket handler gracefully
instead of disconnecting the client
- Make MAX_CONTENT_SIZE and ALLOWED_CONTENT_TYPES pub for gateway reuse
- Update CanvasStore::render and subscribe to return Option for
canvas count enforcement
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
* feat(channels): add voice wake word detection channel
Add VoiceWakeChannel behind the `voice-wake` feature flag that:
- Captures audio from the default microphone via cpal
- Uses energy-based VAD to detect speech activity
- Transcribes speech via the existing transcription API (Whisper)
- Checks for a configurable wake word in the transcription
- On detection, captures the following utterance and dispatches it
as a ChannelMessage
State machine: Listening -> Triggered -> Capturing -> Processing -> Listening
Config keys (under [channels_config.voice_wake]):
- wake_word (default: "hey zeroclaw")
- silence_timeout_ms (default: 2000)
- energy_threshold (default: 0.01)
- max_capture_secs (default: 30)
Includes tests for config parsing, state machine, RMS energy
computation, and WAV encoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): exclude voice-wake from all-features CI check
Add a `ci-all` meta-feature in Cargo.toml that includes every feature
except `voice-wake`, which requires `libasound2-dev` (ALSA) not present
on CI runners. Update the check-all-features CI job to use
`--features ci-all` instead of `--all-features`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): address critical bugs in voice wake word detection
- Replace std::mem::forget(stream) with dedicated thread that holds the
cpal stream and shuts down cleanly via oneshot channel, preventing
microphone resource leaks on task cancellation
- Add config validation: energy_threshold must be positive+finite,
silence_timeout_ms >= 100ms, max_capture_secs clamped to 300
- Guard WAV encoding against u32 overflow for large audio buffers
- Add hard cap on capture_buf size to prevent unbounded memory growth
- Increase audio channel buffer from 4 to 64 slots to reduce chunk
drops during transcription API calls
- Remove dead WakeState::Processing variant that was never entered
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a fork syncs with upstream, GitHub attributes the push to the fork
owner, causing release-beta-on-push and publish-crates-auto to run
under the wrong identity — leading to confusing notifications and
guaranteed failures (missing secrets).
Add repository guards to root jobs so the entire pipeline is skipped
on forks.
- Replace manual can_act()/record_action() with enforce_tool_operation()
to match the codebase convention used by all other tools (notion,
memory_forget, claude_code, delegate, etc.), producing consistent
error messages and avoiding logic duplication.
- Add model parameter validation to prevent URL path traversal attacks
via crafted model identifiers (e.g. "../../evil-endpoint").
- Add tests for model traversal rejection and filename sanitization.
SessionsSendTool was missing security gate enforcement entirely - any agent
could send messages to any session without security policy checks. Similarly,
SessionsHistoryTool had no security enforcement for reading session data.
Changes:
- Add SecurityPolicy field to SessionsHistoryTool (enforces ToolOperation::Read)
- Add SecurityPolicy field to SessionsSendTool (enforces ToolOperation::Act)
- Add session_id validation to reject empty or non-alphanumeric-only IDs
- Pass security policy from all_tools_with_runtime registration
- Add tests for empty session_id, non-alphanumeric session_id validation
The reaction tool was passing the channel adapter name (e.g. "discord",
"slack") as the first argument to Channel::add_reaction() and
Channel::remove_reaction(), but the trait signature expects a
platform-specific channel_id (e.g. Discord channel snowflake, Slack
channel ID like "C0123ABCD"). This would cause all reaction API calls
to fail at the platform level.
Fixes:
- Add required "channel_id" parameter to the tool schema
- Extract and pass channel_id (not channel_name) to trait methods
- Update tool description to mention the new parameter
- Add MockChannel channel_id capture for test verification
- Add test asserting channel_id (not name) reaches the trait
- Update all existing tests to supply channel_id
For models with small context windows (e.g. glm-4.5-air ~8K tokens),
the system prompt alone can exceed the limit. This adds:
- max_system_prompt_chars config option (default 0 = unlimited)
- compact_context now also compacts the system prompt: skips the
Channel Capabilities section and shows only tool names
- Truncation with marker when prompt exceeds the budget
Users can set `max_system_prompt_chars = 8000` in [agent] config
to cap the system prompt for small-context models.
Closes#4124
auto_approve = ["*"] was doing exact string matching, so only the
literal tool name "*" was matched. Users expecting wildcard semantics
had every tool blocked in supervised mode.
Also adds "prompt exceeds max length" to the context-window error
detection hints (fixes GLM/ZAI error 1261 detection).
Closes#4127
* fix(publish): add aardvark-sys version and publish it before main crate
- Add version = "0.1.0" to aardvark-sys path dependency in Cargo.toml
- Update all three publish workflows to publish aardvark-sys first
- Add aardvark-sys COPY to Dockerfile for workspace builds
- Fixes cargo publish failure: "dependency aardvark-sys does not
specify a version"
* ci: publish aardvark-sys before main crate in all publish workflows
All three crates.io publish workflows now publish aardvark-sys first,
wait for indexing, then publish the main zeroclawlabs crate.
When run via `curl | bash`, stdin is the curl pipe, so sudo cannot
prompt for a password. Redirect sudo's stdin from /dev/tty to reach
the real terminal, allowing the password prompt to work in piped
invocations.
Instead of exiting with a manual remediation step, the installer now
attempts to accept the Xcode/CLT license automatically via
`sudo xcodebuild -license accept`. Falls back to a clear error message
only if sudo fails (e.g. no terminal or password).
The output format used "{action}ed" which produced "removeed" for the
remove action. Use explicit past-tense mapping instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hardware): drain stdin in subprocess test to prevent broken pipe flake
The test script did not consume stdin, so SubprocessTool's stdin write
raced against the process exit, causing intermittent EPIPE failures.
Add `cat > /dev/null` to drain stdin before producing output.
* style: format subprocess test
The Xcode license test-compile was inside install_system_deps(), which
only runs when --install-system-deps is passed. On macOS the default
path skipped this entirely, so users hit `cc` exit code 69 deep in
cargo build. Move the check into the unconditional main flow so it
always fires on Darwin.
xcrun --show-sdk-path can succeed even when the Xcode/CLT license has
not been accepted, so the previous check was ineffective. Replace it
with an actual test-compilation of a trivial C file, which reliably
triggers the exit-code-69 failure when the license is pending.
Add ImageGenTool that exposes fal.ai Flux model image generation as a
standalone tool, decoupled from the LinkedIn client. The tool accepts a
text prompt, optional filename/size/model parameters, calls the fal.ai
synchronous API, downloads the result, and saves to workspace/images/.
- New src/tools/image_gen.rs with full Tool trait implementation
- New ImageGenConfig in schema.rs (enabled, default_model, api_key_env)
- Config-gated registration in all_tools_with_runtime
- Security: checks can_act() and record_action() before execution
- Comprehensive unit tests (prompt validation, API key, size enum,
autonomy blocking, tool spec)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ReactionTool that exposes Channel::add_reaction and
Channel::remove_reaction as an agent-callable tool. Uses a
late-binding ChannelMapHandle (Arc<RwLock<HashMap>>) pattern
so the tool can be constructed during tool registry init and
populated once channels are available in start_channels.
Parameters: channel, message_id, emoji, action (add/remove).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add three new tools in src/tools/sessions.rs:
- sessions_list: lists active sessions with channel, message count, last activity
- sessions_history: reads last N messages from a session by ID
- sessions_send: appends a message to a session for inter-agent communication
All tools operate on the SessionBackend trait, using the JSONL SessionStore
by default. Registered unconditionally in all_tools_with_runtime when the
sessions directory is accessible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(config): add configurable pacing controls for slow/local LLM workloads (#2963)
Add a new `[pacing]` config section with four opt-in parameters that
let users tune timeout and loop-detection behavior for local LLMs
(Ollama, llama.cpp, vLLM) without disabling safety features entirely:
- `step_timeout_secs`: per-step LLM inference timeout independent of
the overall message budget, catching hung model responses early.
- `loop_detection_min_elapsed_secs`: time-gated loop detection that
only activates after a configurable grace period, avoiding false
positives on long-running browser/research workflows.
- `loop_ignore_tools`: per-tool loop-detection exclusions so tools
like `browser_screenshot` that structurally resemble loops are not
counted toward identical-output detection.
- `message_timeout_scale_max`: overrides the hardcoded 4x ceiling in
the channel message timeout scaling formula.
All parameters are strictly optional with no effect when absent,
preserving full backwards compatibility.
Closes#2963
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(config): add missing pacing fields in tests and call sites
* fix(config): add pacing arg to remaining cost-tracking test call sites
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* fix(install): detect un-accepted Xcode license before build
Add an xcrun check after verifying Xcode CLT is installed. When the
Xcode/CLT license has not been accepted, cc exits with code 69 and
the build fails with a cryptic linker error. This surfaces a clear
message telling the user to run `sudo xcodebuild -license accept`.
* chore(release): bump version to v0.5.5
Update version across all distribution manifests:
- Cargo.toml and Cargo.lock
- dist/aur/PKGBUILD and .SRCINFO
- dist/scoop/zeroclaw.json
* feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies
Allow each channel to optionally specify a `proxy_url` in its config,
enabling users behind restrictive networks to route channel traffic
through HTTP or SOCKS5 proxies. When set, the per-channel proxy takes
precedence over the global `[proxy]` config; when absent, the channel
falls back to the existing runtime proxy behavior.
Adds `proxy_url: Option<String>` to all 12 channel config structs
(Telegram, Discord, Slack, Mattermost, Signal, WhatsApp, Wati,
NextcloudTalk, DingTalk, QQ, Lark, Feishu) and introduces
`build_channel_proxy_client`, `build_channel_proxy_client_with_timeouts`,
and `apply_channel_proxy_to_builder` helpers that normalize proxy URLs
and integrate with the existing client cache.
Closes#3262
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(channel): add missing proxy_url fields in test initializers
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(tool): enrich delegate sub-agent system prompt and add skills_directory config key (#3046)
Sub-agents configured under [agents.<name>] previously received only the
bare system_prompt string. They now receive a structured system prompt
containing: tools section (allowed tools with parameters and invocation
protocol), skills section (from scoped or default directory), workspace
path, current date/time, safety constraints, and shell policy when shell
is in the effective tool list.
Add optional skills_directory field to DelegateAgentConfig for per-agent
scoped skill loading. When unset, falls back to default workspace
skills/ directory.
Closes#3046
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tools): add missing fields after rebase
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Adopted from #3705 by @fangxueshun with fixes:
- Added input validation for date strings (RFC 3339)
- Used chrono DateTime comparison instead of string comparison
- Added since < until validation
- Updated mem0 backend
Supersedes #3705
Fixes E0382 borrow-after-move error: wait_with_output() consumed the
child handle, making child.kill() in the timeout branch invalid.
Use kill_on_drop(true) with cmd.output() instead.
Adds per-channel cost tracking via task-local context in the tool call
loop. Budget enforcement blocks further API calls when limits are
exceeded. Resolves merge conflicts with model-switch retry loop,
reply_target parameter, and autonomy level additions on master.
Supersedes #3758
Self-hosted Whisper-compatible STT provider that POSTs audio to a
configurable HTTP endpoint (e.g. faster-whisper over WireGuard). Audio
never leaves the platform perimeter.
Implemented via red/green TDD cycles:
Wave 1 — config schema: LocalWhisperConfig struct, local_whisper field
on TranscriptionConfig + Default impl, re-export in config/mod.rs
Wave 2 — from_config validation: url non-empty, url parseable, bearer_token
non-empty, max_audio_bytes > 0, timeout_secs > 0; returns Result<Self>
Wave 3 — manager integration: registration with ? propagation (not if let Ok
— credentials come directly from config, no env-var fallback; present
section with bad values is a hard error, not a silent skip)
Wave 4 — transcribe(): resolve_audio_format() extracted from validate_audio()
so LocalWhisperProvider can resolve MIME without the 25 MB cloud cap;
size check + format resolution before HTTP send
Wave 5 — HTTP mock tests: success response, bearer auth header, 503 error
33 tests (20 baseline + 13 new), all passing. Clippy clean.
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
Slack's Block Kit supports a native `markdown` block type that accepts
standard Markdown and handles rendering. This removes the need for a
custom Markdown-to-mrkdwn converter. Messages over 12,000 chars fall
back to plain text.
Co-authored-by: Joe Hoyle <joehoyle@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add import functionality for existing OpenAI Codex auth profiles
Introduces a new command-line option to import an existing `auth.json` file for OpenAI Codex, allowing users to bypass the login flow. The import feature reads and parses the specified JSON file, extracting authentication tokens and storing them in the user's profile. This change enhances user experience by simplifying the authentication process for existing users.
- Added `import` option to `AuthCommands` enum
- Implemented `import_openai_codex_auth_profile` function to handle the import logic
- Updated `handle_auth_command` to process the import option and validate provider compatibility
- Ensured that the import feature is exclusive to the `openai-codex` provider
* feat(auth): extract expiry from JWT in OpenAI Codex import
Enhances the `import_openai_codex_auth_profile` function by extracting the expiration date from the JWT access token. This change allows for more accurate management of token lifetimes by replacing the hardcoded expiration date with a dynamic value derived from the token itself.
- Added `extract_expiry_from_jwt` function to handle JWT expiration extraction
- Updated `TokenSet` to use the extracted expiration date instead of a static value
- Add ThemeContext with light/dark/system theme support
- Migrate all hardcoded colors to CSS variables
- Add SettingsModal for theme customization
- Add font loader for dynamic font selection
- Add i18n support for Chinese and Turkish locales
- Fix accessibility: add aria-live to pairing error message
Co-authored-by: nanyuantingfeng <nanyuantingfeng@163.com>
* fix(config): add missing WhatsApp Web policy config keys (mode, dm_policy, group_policy, self_chat_mode)
* fix(onboard): add missing WhatsApp policy fields to wizard struct literals
The new mode, dm_policy, group_policy, and self_chat_mode fields added
to WhatsAppConfig need default values in the onboard wizard's struct
initializers to avoid E0063 compilation errors.
The channel path in `src/channels/mod.rs` was passing `None` as the
`model_switch_callback` to `run_tool_call_loop()`, which meant model
switching via the `model_switch` tool was silently ignored in channel
mode.
Wire the callback in following the same pattern as the CLI path:
- Pass `Some(model_switch_callback.clone())` instead of `None`
- Wrap the tool call loop in a retry loop
- Handle `ModelSwitchRequested` errors by re-creating the provider
with the new model and retrying
Fixes#4107
Replace `tokio::task::spawn_blocking()` with plain `std:🧵:Builder`
OS threads in all PostgresMemory trait methods. The sync `postgres` crate
(v0.19.x) internally calls `Runtime::block_on()`, which panics when called
from Tokio's blocking pool threads in recent Tokio versions. Plain OS threads
have no runtime context, so the nested `block_on` succeeds.
This matches the pattern already used in `PostgresMemory::initialize_client()`,
which correctly used `std:🧵:Builder` and never exhibited this bug.
A new `run_on_os_thread` helper centralizes the pattern: spawn an OS thread,
run the closure, and bridge the result back via a `tokio::sync::oneshot` channel.
Fixes#4101
Implement start_typing/stop_typing for Slack using the Assistants API
assistant.threads.setStatus method. Tracks thread context from
assistant_thread_started events and inbound messages, then sets
"is thinking..." status during processing. Status auto-clears when
the bot sends a reply via chat.postMessage.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the Weather integration as a native Rust Tool trait
implementation, consistent with the existing first-party tool
architecture (no WASM/plugin layer required).
- Add src/tools/weather_tool.rs with full WeatherTool impl
- Fetches from wttr.in ?format=j1 (no API key, global coverage)
- Supports city names (any language/script), IATA airport codes,
GPS coordinates, postal/zip codes, domain-based geolocation
- Metric (°C, km/h, mm) and imperial (°F, mph, in) units
- Current conditions + 0-3 day forecast with hourly breakdown
- Graceful error messages for unknown/invalid locations
- Respects runtime proxy config via apply_runtime_proxy_to_builder
- 36 unit tests: schema, URL building, param validation, formatting
- Register WeatherTool unconditionally in all_tools_with_runtime
(no API key needed, no config gate — same pattern as CalculatorTool)
- Flip integrations registry Weather entry from ComingSoon to Available
Closes #<issue>
Register DeepMyst (https://deepmyst.com) as an OpenAI-compatible
provider with Bearer auth and DEEPMYST_API_KEY env var support.
Aliases: "deepmyst", "deep-myst".
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The builder used sed to remove crates/robot-kit from [workspace].members because that path was not copied into the image. Cargo.lock is still generated for the full workspace (including zeroclaw-robot-kit), so the manifest and lockfile disagreed. cargo build --release --locked then tried to rewrite Cargo.lock and failed with "cannot update the lock file ... because --locked was passed" (commonly hit when ZEROCLAW_CARGO_FEATURES includes memory-postgres).
Copy crates/robot-kit/ into the image and drop the sed step so the workspace matches the committed lockfile.
Made-with: Cursor
Co-authored-by: lokinh <locnh@uniultra.xyz>
* fix(config): prevent test suite from clobbering active_workspace.toml
Refactor persist_active_workspace_config_dir() to accept the default
config directory as an explicit parameter instead of reading HOME
internally. This eliminates a hidden dependency on process-wide
environment state that caused test-suite runs to overwrite the real
user's active_workspace.toml with a stale temp-directory path.
The temp-directory guard is now unconditional (previously gated behind
cfg(not(test))). It rejects writes only when a temp config_dir targets
a non-temp default location, so test-to-test writes within temp dirs
still succeed.
Closes#4117
* fix: remove needless borrow on default_config_dir parameter
---------
Co-authored-by: lamco-office <office@lamco.io>
The channel validation in `validate_announce_delivery` was missing `qq`,
causing API-created cron jobs with QQ delivery to be rejected.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): wire MCP tools into WebSocket chat path and gateway /api/tools
Agent::from_config() did not initialize MCP tools because it was
synchronous and MCP connection requires async. The gateway tool
registry built for /api/tools also missed MCP tools for the same
reason.
Changes:
- Make Agent::from_config() async so it can call McpRegistry::connect_all()
- Add MCP tool initialization (both eager and deferred modes) to
from_config(), following the same pattern used in loop_.rs CLI/webhook paths
- Add MCP tool initialization to the gateway's tool registry so
/api/tools reflects MCP tools
- Update all three call sites (run(), handle_socket, test) to await
Closes#4042
* fix: merge master and fix formatting
* fix: remove underscore prefix from used bindings (clippy)
* feat(hardware): add RPi GPIO, Aardvark I2C/SPI/GPIO, and hardware plugin system
Extends the hardware subsystem with three clusters of functionality,
all feature-gated (hardware / peripheral-rpi) with no impact on default builds.
Raspberry Pi native support:
- src/hardware/rpi.rs: board self-discovery (model, serial, revision),
sysfs GPIO pin read/write, and ACT LED control
- scripts/99-act-led.rules: udev rule for non-root ACT LED access
- scripts/deploy-rpi.sh, scripts/rpi-config.toml, scripts/zeroclaw.service:
one-shot deployment helper and systemd service template
Total Phase Aardvark USB adapter (I2C / SPI / GPIO):
- crates/aardvark-sys/: new workspace crate with FFI bindings loaded at
runtime via libloading; graceful stub fallback when .so is absent or
arch mismatches (Rosetta 2 detection)
- src/hardware/aardvark.rs: AardvarkTransport implementing Transport trait
- src/hardware/aardvark_tools.rs: agent tools i2c_scan, i2c_read,
i2c_write, spi_transfer, gpio_aardvark
- src/hardware/datasheet.rs: datasheet search/download for detected devices
- docs/aardvark-integration.md, examples/hardware/aardvark/: guide + examples
Hardware plugin / ToolRegistry system:
- src/hardware/tool_registry.rs: ToolRegistry for hardware module tool sets
- src/hardware/loader.rs, src/hardware/manifest.rs: manifest-driven loader
- src/hardware/subprocess.rs: subprocess execution helper for board I/O
- src/gateway/hardware_context.rs: POST /api/hardware/reload endpoint
- src/hardware/mod.rs: exports all new modules; merge_hardware_tools and
load_hardware_context_prompt helpers
Integration hooks (minimal surface):
- src/hardware/device.rs: DeviceKind::Aardvark, DeviceRuntime::Aardvark,
has_aardvark / resolve_aardvark_device on DeviceRegistry
- src/hardware/transport.rs: TransportKind::Aardvark
- src/peripherals/mod.rs: gate create_board_info_tools behind hardware feature
- src/agent/loop_.rs: TOOL_CHOICE_OVERRIDE task-local for Anthropic provider
- src/providers/anthropic.rs: read TOOL_CHOICE_OVERRIDE; add tool_choice field
- Cargo.toml: add aardvark-sys to workspace and as dependency
- firmware/zeroclaw-nucleo/: update Cargo.toml and Cargo.lock
Non-goals:
- No changes to agent orchestration, channels, providers, or security policy
- No new config keys outside existing [hardware] / [peripherals] sections
- No CI workflow changes
Risk: Low. All new paths are feature-gated; aardvark.so loads at runtime
only when present. No schema migrations or persistent state introduced.
Rollback: revert this single commit.
* fix(hardware): resolve clippy and rustfmt CI failures
- struct_excessive_bools: allow on DeviceCapabilities (7 bool fields needed)
- unnecessary_debug_formatting: use .display() instead of {:?} for paths
- stable_sort_primitive: replace .sort() with .sort_unstable() on &str slices
* fix(hardware): add missing serial/uf2/pico modules declared in mod.rs
cargo fmt was exiting with code 1 because mod.rs declared pub mod serial,
uf2, pico_flash, pico_code but those files were missing from the branch.
Also apply auto-formatting to loader.rs.
* fix(hardware): apply rustfmt 1.92.0 formatting (matches CI toolchain)
* docs(scripts): add RPi deployment and interaction guide
* push
* feat(firmware): add initial Pico firmware and serial device handling
- Introduced main.py for ZeroClaw Pico firmware with a placeholder for MicroPython implementation.
- Added binary UF2 file for Pico deployment.
- Implemented serial device enumeration and validation in the hardware module, enhancing security by restricting allowed serial paths.
- Updated related modules to integrate new serial device functionality.
---------
Co-authored-by: ehushubhamshaw <eshaw1@wpi.edu>
When the gateway security guard blocks a public bind address, the error
message now mentions the Docker use case and provides clear instructions
for connecting from Docker containers.
Closes#4086
* fix(approval): auto-approve read-only tools in non-interactive mode
Add web_search_tool, web_fetch, calculator, glob_search, content_search,
and image_info to the default auto_approve list. These are read-only tools
with no side effects that were being silently denied in channel mode
(Telegram, Slack, etc.) because the non-interactive ApprovalManager
auto-denies any tool not in auto_approve when autonomy != full.
Closes#4083
* fix: remove duplicate default_otp_challenge_max_attempts function
PR #3921 accidentally introduced a duplicate definition of
default_otp_challenge_max_attempts() in config/schema.rs, causing
compilation to fail on master (E0428: name defined multiple times).
* feat(memory): add mem0 (OpenMemory) backend integration
- Implement Mem0Memory struct with full Memory trait
- Add history() audit trail, recall_filtered() with time/metadata filters
- Add store_procedural() for conversation trace extraction
- Add ProceduralMessage type to Memory trait with default no-op
- Feature-gated behind `memory-mem0` flag
- 9 unit tests covering edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(memory): add extraction_prompt config, deploy scripts, and timing instrumentation
- Add `extraction_prompt` field to `Mem0Config` for custom LLM fact
extraction prompts (e.g. Cantonese/Chinese content), with
`MEM0_EXTRACTION_PROMPT` env var fallback
- Pass `custom_instructions` in mem0 store requests so the server
uses the client-supplied prompt over its default
- Add timing instrumentation to channel message pipeline
(mem_recall_ms, elapsed_before_llm_ms, llm_call_ms, total_ms)
- Add `deploy/mem0/` with self-hosted mem0 + reranker GPU server
scripts, fully configurable via environment variables
- Update config reference docs (EN, zh-CN, VI) with `[memory.mem0]`
subsection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
# src/channels/mod.rs
* chore: remove accidentally staged worktree from index
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gemini): use default chat() for prompt-guided tool calling and add multimodal vision support
The Gemini provider's chat() override bypassed the default trait
implementation that injects tool definitions into the system prompt for
providers without native tool calling. This caused the agent loop to see
tool definitions in context but never actually invoke them, resulting in
hallucinated tool calls (e.g. claiming "Stored" without calling
memory_store).
Remove the broken chat() override so the default prompt-guided fallback
in the Provider trait handles tool injection correctly. Add an explicit
capabilities() declaration (native_tool_calling: false, vision: true).
Also add multimodal support: convert Part from a plain struct to an
untagged enum with Text and Inline variants, and add build_parts() to
extract [IMAGE:data:...] markers as Gemini inline_data parts.
Includes 14 new tests covering capabilities, Part serialization,
build_parts edge cases, and role-mapping behavior. Removes unused
ChatResponse import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(gemini): move capabilities tests to component level and add tool conversion test
Move the 3 capabilities tests (native_tool_calling, vision,
supports_native_tools) from the inline module to
tests/component/gemini_capabilities.rs since they exercise the public
Provider trait contract through the factory. Add a new
convert_tools_returns_prompt_guided test verifying the agent loop will
receive PromptGuided payload for Gemini.
Private internals tests (Part serialization, build_parts, role mapping)
remain inline since those types are not publicly exported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(gemini): fix cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(gemini): add prompt_caching field to capabilities declaration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: myclaw <myclaw@myclaws-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The `SafetySection` in `SystemPromptBuilder` always hardcoded
"Do not run destructive commands without asking" and "Do not bypass
oversight or approval mechanisms" regardless of the configured
autonomy level. This caused the gateway WebSocket path (web interface)
to instruct the LLM to simulate approval dialogs even when
`autonomy.level = "full"`.
PRs #3955/#3970/#3975 fixed the channel dispatch path
(`build_system_prompt_with_mode_and_autonomy`) but missed the
`Agent::from_config` → `SystemPromptBuilder` path used by
`gateway/ws.rs`.
Changes:
- Add `autonomy_level` field to `PromptContext`
- Rewrite `SafetySection::build()` to conditionally include/exclude
approval instructions based on autonomy level, matching the logic
already present in `build_system_prompt_with_mode_and_autonomy`
- Add `autonomy_level` field to `Agent` struct and `AgentBuilder`
- Pass `config.autonomy.level` through `Agent::from_config`
- Add tests for full/supervised autonomy safety section behavior
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The QQ channel WebSocket loop did not handle incoming Ping frames,
causing the server to consider the connection dead and drop it. Add a
Ping handler that replies with Pong, keeping the connection alive.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Claude Code CLI only supports temperatures 0.7 and 1.0, but
internal subsystems (memory consolidation, context summarizer) use
lower values like 0.1 and 0.2. Previously the provider rejected these
with a hard error, triggering retries and WARN-level log noise.
Clamp to the nearest supported value instead, since the CLI ignores
temperature anyway.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expand the minimal .editorconfig to include comprehensive settings for
all file types used in the project:
- Add root = true declaration
- Add standard settings: end_of_line, charset, trim_trailing_whitespace,
insert_final_newline
- Add Rust-specific settings (indent_size = 4, max_line_length = 100)
to match rustfmt.toml
- Add Markdown settings (preserve trailing whitespace for hard breaks)
- Add TOML, YAML, Python, Shell script, and JSON settings
This ensures consistent editor behavior across all contributors and
matches the project's formatting standards.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix(docker): default CMD to daemon instead of gateway
- Change default Docker CMD from gateway to daemon in both dev and release stages
- gateway only starts the HTTP/WebSocket server — channel listeners (Matrix, Telegram, Discord, etc.) are never spawned
- daemon starts the full runtime: gateway + channels + heartbeat + scheduler
- Users who configure channels in config.toml and run the default image get no response because the channel sync loops never start
* fix(config): add challenge_max_attempts field to OtpConfig
Add missing challenge_max_attempts field to support OTP challenge
attempt limiting. This field allows users to configure the maximum
number of OTP challenge attempts before lockout.
Fixes#3919
---------
Co-authored-by: Jah-yee <jah.yee@outlook.com>
Co-authored-by: Jah-yee <jahyee@sparklab.ai>
The /memory data grid grew unboundedly with table rows, pushing the
horizontal scrollbar to the very bottom of a tall page and making it
inaccessible without scrolling all the way down first.
- Layout: change outer shell from min-h-screen to h-screen +
overflow-hidden, and add min-h-0 to <main> so flex-1 overflow-y-auto
actually clamps at the viewport boundary instead of growing infinitely.
- Memory page: switch root div to flex-col h-full so it fills the
bounded main area; give the glass-card table wrapper flex-1 min-h-0
overflow-auto so it consumes remaining space and exposes both
scrollbars without any page-level scrolling required.
- index.css: pin .table-electric thead th with position:sticky / top:0
and a matching opaque background so column headers stay visible
during vertical scroll inside the bounded card.
The result behaves like a bounded iframe: the table fills the available
screen, rows scroll vertically, wide columns scroll horizontally, and
both scrollbars are always reachable.
* feat(slack): implement reaction support with sanitized error responses
Add add_reaction() and remove_reaction() for Slack channel, with
unicode-to-Slack emoji mapping, idempotent error handling, and
sanitized API error responses matching the pattern used by
chat.postMessage.
Based on #4089 by @joehoyle, with sanitize_api_error() applied to
reaction error paths for consistency with existing Slack methods.
Supersedes #4089
* chore(deps): bump rustls-webpki to 0.103.10 (RUSTSEC-2026-0049)
Remove the "How it works (short)" ASCII diagram section from
all 31 README files (English + 30 translations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expand README/docs navigation to include Korean, Tagalog, German, Arabic, Hindi, and Bengali locale entries. Add canonical locale hub and summary files for each new language under docs/i18n/.
Update i18n index/coverage metadata to reflect hub-level support and keep language discovery consistent across root docs entry points.
> **Shared instructions live in [`AGENTS.md`](./AGENTS.md).**
> This file contains only Claude Code-specific directives.
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
```
## Claude Code Settings
Full pre-PR validation (recommended):
Claude Code should read and follow all instructions in `AGENTS.md` at the repository root for project conventions, commands, risk tiers, workflow rules, and anti-patterns.
```bash
./dev/ci.sh all
```
## Hooks
Docs-only changes: run markdown lint and link-integrity checks. If touching bootstrap scripts: `bash -n install.sh`.
_No custom hooks defined yet._
## Project Snapshot
## Slash Commands
ZeroClaw is a Rust-first autonomous agent runtime optimized for performance, efficiency, stability, extensibility, sustainability, and security.
Core architecture is trait-driven and modular. Extend by implementing traits and registering in factory modules.
Single reference for every label used on PRs and issues. Labels are grouped by category. Each entry lists the label name, definition, and how it is applied.
Sources consolidated here:
- `.github/labeler.yml` (path-label config for `actions/labeler`)
- `docs/contributing/pr-workflow.md` (size, risk, and triage label definitions)
- `docs/contributing/ci-map.md` (automation behavior and high-risk path heuristics)
Note: The CI was simplified to 4 workflows (`ci.yml`, `release.yml`, `ci-full.yml`, `promote-release.yml`). Workflows that previously automated size, risk, contributor tier, and triage labels (`pr-labeler.yml`, `pr-auto-response.yml`, `pr-check-stale.yml`, and supporting scripts) were removed. Only path labels via `pr-path-labeler.yml` are currently automated.
---
## Path labels
Applied automatically by `pr-path-labeler.yml` using `actions/labeler`. Matches changed files against glob patterns in `.github/labeler.yml`.
Defined in `pr-workflow.md` §6.1. Based on effective changed line count, normalized for docs-only and lockfile-heavy PRs.
| Label | Threshold |
|---|---|
| `size: XS` | <= 80 lines |
| `size: S` | <= 250 lines |
| `size: M` | <= 500 lines |
| `size: L` | <= 1000 lines |
| `size: XL` | > 1000 lines |
**Applied by:** manual. The workflows that previously computed size labels (`pr-labeler.yml` and supporting scripts) were removed during CI simplification.
---
## Risk labels
Defined in `pr-workflow.md` §13.2 and `ci-map.md`. Based on a heuristic combining touched paths and change size.
| Label | Meaning |
|---|---|
| `risk: low` | No high-risk paths touched, small change |
| `superseded` | Replaced by a newer PR | Manual |
| `no-stale` | Exempt from stale automation; accepted but blocked work | Manual |
**Automation:** none currently. The workflows that handled label-driven issue closing (`pr-auto-response.yml`) and stale detection (`pr-check-stale.yml`) were removed during CI simplification.
- **Owner:** maintainers responsible for label policy and PR triage automation.
- **Update trigger:** new channels, providers, or tools added to the source tree; label policy changes; triage workflow changes.
- **Source of truth:** this document consolidates definitions from the four source files listed at the top. When definitions conflict, update the source file first, then sync this registry.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.