The pushover tool priority parameter schema used integer enum values
[-2, -1, 0, 1, 2]. OpenAI-compatible APIs accept this, but the Gemini
API (and Gemini-relay proxies) strictly require all enum values to be
strings, rejecting the request with 400 Bad Request.
This causes every agent turn to fail with a non_retryable error when
using Gemini models, regardless of user message content, because tool
schemas are included in every request.
Fix: remove the enum constraint, keeping integer type and description
documenting the valid range. This is valid for both OpenAI and Gemini
providers and requires no changes to execute() which already uses
as_i64() with range validation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add URL scheme validation before HTTP requests that transmit sensitive
data (account IDs, phone numbers, user IDs). All endpoints already use
HTTPS URLs, but this explicit check satisfies CodeQL rust/cleartext-
transmission analysis and prevents future regressions if URLs are
changed.
Affected files: composio.rs, whatsapp.rs, qq.rs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- add scope-aware proxy schema and runtime wiring for providers/channels/tools
- add agent callable proxy_config tool for fast proxy setup
- standardize docs system with index, template, and playbooks
Wire the existing provider-layer streaming infrastructure through the
channel trait and agent loop so Telegram users see tokens arrive
progressively via editMessageText, instead of waiting for the full
response.
Changes:
- Add StreamMode enum (off/partial/block) and draft_update_interval_ms
to TelegramConfig (backward-compatible defaults: off, 1000ms)
- Add supports_draft_updates/send_draft/update_draft/finalize_draft to
Channel trait with no-op defaults (zero impact on existing channels)
- Implement draft methods on TelegramChannel using sendMessage +
editMessageText with rate limiting and Markdown fallback
- Add on_delta mpsc::Sender<String> parameter to run_tool_call_loop
(None preserves existing behavior)
- Wire streaming in process_channel_message: when channel supports
drafts, send initial draft, spawn updater task, finalize on completion
Edge cases handled:
- 4096-char limit: finalize draft and fall back to chunked send
- Broken Markdown: use no parse_mode during streaming, apply on finalize
- Edit failures: fall back to sending complete response as new message
- Rate limiting: configurable draft_update_interval_ms (default 1s)
Add native web search capability that works regardless of LLM tool-calling
support. This is particularly useful for GLM models via Z.AI that don't
reliably support standard tool calling formats.
Features:
- DuckDuckGo provider (free, no API key required)
- Brave Search provider (optional, requires API key)
- Configurable max results and timeout
- Enabled by default
Configuration (config.toml):
[web_search]
enabled = true
provider = "duckduckgo"
max_results = 5
The tool allows agents to search the web for current information without
requiring proper tool calling support from the LLM.
Also includes CI workflow fix for first-interaction action inputs.
## Problem
The test suite contained several categories of latent brittleness
identified in docs/testing-brittle-tests.md that would surface during
refactoring or cross-platform (Windows) CI execution:
1. Hardcoded Unix paths: \Path::new("/tmp")\ and \PathBuf::from("/tmp")\
used as workspace directories in agent tests, which fail on Windows
where /tmp does not exist.
2. Exact string match assertions: ~20 \ssert_eq!(response, "exact text")\
assertions in agent unit and e2e tests that break on any mock wording
change, even when the underlying orchestration behavior is correct.
3. Fragile error message string matching: \.contains("specific message")\
assertions coupled to internal error wording rather than testing the
error category or behavioral outcome.
## What Changed
### Hardcoded paths → platform-agnostic temp dirs (4 files, 7 locations)
- \src/agent/tests.rs\: Replaced all 4 instances of \Path::new("/tmp")\
and \PathBuf::from("/tmp")\ with \std::env::temp_dir()\ in
\make_memory()\, \uild_agent_with()\, \uild_agent_with_memory()\,
and \uild_agent_with_config()\ helpers.
- \ ests/agent_e2e.rs\: Replaced all 3 instances in \make_memory()\,
\uild_agent()\, and \uild_agent_xml()\ helpers.
### Exact string assertions → behavioral checks (2 files, ~20 locations)
- \src/agent/tests.rs\: Converted 10 \ssert_eq!(response, "...")\ to
\ssert!(!response.is_empty(), "descriptive message")\ across tests for
text pass-through, tool execution, tool failure recovery, XML dispatch,
mixed text+tool responses, multi-tool batch, and run_single delegation.
- \ ests/agent_e2e.rs\: Converted 9 exact-match assertions to behavioral
checks. Multi-turn test now uses \ssert_ne!(r1, r2)\ to verify
sequential responses are distinct without coupling to exact wording.
- Provider error propagation test simplified to \ssert!(result.is_err())\
without asserting on the error message string.
### Fragile error message assertions → structural checks (2 files)
- \src/tools/git_operations.rs\: Replaced fragile OR-branch string match
(\contains("git repository") || contains("Git command failed")\) with
structural assertions: checks \!result.success\, error is non-empty,
and error does NOT mention autonomy/read-only (verifying the failure
is git-related, not permission-related).
- \src/cron/scheduler.rs\: Replaced \contains("agent job failed:")\ with
\!success\ and \!output.is_empty()\ checks that verify failure behavior
without coupling to exact log format.
## What Was NOT Changed (and why)
- \src/agent/loop_.rs\ parser tests: Exact string assertions are the
contract for XML tool call parsing — the exact output IS the spec.
- \src/providers/reliable.rs\: Error message assertions test the error
format contract (provider/model attribution in failure messages).
- \src/service/mod.rs\: Already platform-gated with \#[cfg]\; XML escape
test is a formatting contract where exact match is appropriate.
- \src/config/schema.rs\: TOML test strings use /tmp as data values for
deserialization tests, not filesystem access; HOME tests already use
\std::env::temp_dir()\.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The MemoryCategory::Custom variant already exists in the memory backend
but the memory_store tool only accepted core/daily/conversation. Now any
string is accepted as a category, passing through to Custom(name) for
non-builtin values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove 'branch' from requires_write_access() to resolve the
contradiction where branch listing was classified as both read-only
and write-requiring. Branch listing only enumerates local refs and
has no side effects, so it should remain available under ReadOnly
autonomy mode.
Add regression tests:
- branch_is_not_write_gated: verifies classification consistency
- allows_branch_listing_in_readonly_mode: verifies end-to-end
execution under ReadOnly autonomy
- is_read_only_detection: now explicitly asserts branch is read-only
Resolveszeroclaw-labs/zeroclaw#612
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes#607
The http_request tool validated the initial URL against the domain
allowlist and private-host rules, but reqwest's default redirect policy
followed redirects automatically without revalidating each hop. This
allowed SSRF via redirect chains from allowed domains to internal hosts.
Set redirect policy to Policy::none() so 3xx responses are returned
as-is. Callers that need to follow redirects must issue a new request,
which goes through validate_url again.
Severity: High — SSRF/allowlist bypass via redirect chains.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes#601
The Linux screenshot path uses sh -c with single-quote interpolation.
A filename containing quote characters could break quoting and inject
shell tokens. Add a check that rejects filenames with any shell-breaking
characters (quotes, backticks, dollar signs, semicolons, pipes, etc.)
before passing to the shell command.
Severity: High — command injection in tool execution path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(security): add HTTP hostname canonicalization edge-case tests
Document that Rust's IpAddr::parse() rejects non-standard IP notations
(octal, hex, decimal integer, zero-padded) which provides defense-in-depth
against SSRF bypass attempts. Tests only — no production code changes.
Closes#515
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply rustfmt to providers/mod.rs
Fix pre-existing formatting issue from main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): expand git argument sanitization
Expand sanitize_git_args() blocklist to also reject --pager=, --editor=,
-c (config injection), --no-verify, and > in arguments. Apply validation
to git_add() paths and git_diff() files argument (previously only called
from git_checkout()). The -c check uses exact match to avoid
false-positives on --cached.
Closes#516
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply rustfmt to providers/mod.rs
Fix pre-existing formatting issue from main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(memory): add session_id isolation to Memory trait
Add optional session_id parameter to store(), recall(), and list()
methods across the Memory trait and all four backends (sqlite, markdown,
lucid, none). This enables per-session memory isolation so different
agent sessions cannot cross-read each other's stored memories.
Changes:
- traits.rs: Add session_id: Option<&str> to store/recall/list
- sqlite.rs: Schema migration (ALTER TABLE ADD COLUMN session_id),
index, persist/filter by session_id in all query paths
- markdown.rs, lucid.rs, none.rs: Updated signatures
- All callers pass None for backward compatibility
- 5 new tests: session-filtered recall, cross-session isolation,
session-filtered list, no-filter returns all, migration idempotency
Closes#518
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(channels): fix discord _channel_id typo and lark missing reply_to
Pre-existing compilation errors on main after reply_to was added to
ChannelMessage: discord.rs used _channel_id (underscore prefix) but
referenced channel_id, and lark.rs was missing the reply_to field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
browser.rs:
- Extract parse_browser_action() from Tool::execute, removing one
#[allow(clippy::too_many_lines)] suppression
irc.rs:
- Replace 10-parameter IrcChannel::new() with IrcChannelConfig struct,
removing #[allow(clippy::too_many_arguments)] suppression
- Update all call sites (mod.rs and tests)
Closes#366
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added `JobType`, `SessionTarget`, `Schedule`, `DeliveryConfig`, `CronJob`, `CronRun`, and `CronJobPatch` types in `src/cron/types.rs` for cron job configuration and management.
- Introduced `CronAddTool`, `CronListTool`, `CronRemoveTool`, `CronRunTool`, `CronRunsTool`, and `CronUpdateTool` in `src/tools` for adding, listing, removing, running, and updating cron jobs.
- Updated the `run` function in `src/daemon/mod.rs` to conditionally start the scheduler based on the cron configuration.
- Modified command-line argument parsing in `src/lib.rs` and `src/main.rs` to support new cron job commands.
- Enhanced the onboarding wizard in `src/onboard/wizard.rs` to include cron configuration.
- Added tests for cron job tools to ensure functionality and error handling.
Eliminate low-risk clippy warnings as part of the strict lint backlog (#409):
- Remove unused `uuid::Uuid` imports from slack and telegram channels
- Remove unnecessary `mut` and redundant rebindings in agent loop
- Prefix unused `channel_id` variable in discord channel
- Remove unused test imports (`ChatResponse`, `ToolCall`, `TempDir`, `Path`)
- Block file:// URLs which bypassed all SSRF and domain-allowlist
controls, enabling arbitrary local file exfiltration via browser
- Harden is_private_host() to match http_request.rs coverage:
multicast, broadcast, reserved (240/4), shared address space
(100.64/10), documentation IPs, benchmarking IPs
- Add .localhost subdomain and .local mDNS TLD blocking
- Extract is_non_global_v4() and is_non_global_v6() helpers
Closes#361
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>