ZeroClaw: Fine-Grained Tool Wrappers
ZeroClaw is a production-grade Rust AI agent framework with approximately 130,000 lines of code across 192 source files. It provides a rich tool ecosystem where each tool implements a Tool trait individually.
The Problem: Crosscutting Concern Scattering
ZeroClaw exhibits textbook crosscutting concern scattering:
- Rate limiting: 17 tool files contain nearly identical
is_rate_limited()+record_action()snippets at the top of eachexecute()method - Path security:
is_path_allowed()andforbidden_path_argument()guards are scattered across file read/write, search, PDF, and image tools - Read-only gating: Various forms of
can_act()/enforce_tool_operation()appear throughout
The Solution: Composable Wrappers
We introduced two reusable decorator types in src/tools/wrappers.rs:
#![allow(unused)]
fn main() {
pub struct RateLimitedTool<T: Tool> {
inner: T,
security: Arc<SecurityPolicy>,
}
pub struct PathGuardedTool<T: Tool> {
inner: T,
security: Arc<SecurityPolicy>,
}
}
These compose as nested wrappers:
#![allow(unused)]
fn main() {
// Before: inline guards in each tool's execute()
impl Tool for FileReadTool {
async fn execute(&self, args: Value) -> Result<ToolResult> {
if self.security.is_rate_limited("file_read") { ... }
self.security.record_action("file_read");
if !self.security.is_path_allowed(&path) { ... }
// ... actual business logic
}
}
// After: concerns extracted to wrappers
let tool = RateLimitedTool::new(
PathGuardedTool::new(FileReadTool::new(security.clone()), security.clone()),
security,
);
}
Contribution Strategy: Divide and Conquer
A single PR modifying 20+ tool files would be unreviewable for maintainers. We split the work into seven batches, each targeting a coherent group of tools:
| PR | Batch | Tools | Status |
|---|---|---|---|
| #4905 | 1 – Foundation | ShellTool (establishes wrappers.rs) | Merged |
| #4944 | 2 – File tools | FileReadTool, FileWriteTool, FileEditTool | Open |
| #4947 | 3 – Search tools | GlobSearchTool, ContentSearchTool | Open |
| #4948 | 4 – Doc/Image | PdfReadTool, ImageInfoTool | Open |
| #4949 | 5 – Cron tools | CronAddTool, CronRemoveTool, CronUpdateTool | Open |
| #4952 | 6a – AI CLI | GeminiCliTool, ClaudeCodeTool, CodexCliTool, OpenCodeCliTool | Open |
| #4953 | 6b – Runner | ClaudeCodeRunnerTool | Open |
| #4954 | 7 – Network | BrowserTool, HttpRequestTool, WebFetchTool, SkillTool | Open |
Each PR is independently reviewable: focused changes, limited scope, clear rationale.
Design Intent Discoveries
The refactoring process revealed intentional design decisions that should not be abstracted away:
CronRunTool:record_action()is deliberately placed after command validation to prevent path-probing attacks from consuming rate-limit budget. Preserved as-is.GoogleWorkspaceTool: Comment reads “Charge action budget only after all validation passes.” This is a security-conscious design – wrapping would break the invariant.BrowserTool/HttpRequestTool/WebFetchTool: Usecan_act()(read-only gating) rather thanenforce_tool_operation(). Only rate limiting was extracted; the read-only gate was preserved in-place.
These exceptions are not failures – they are findings. They document where the framework’s internal security layering is deliberately non-uniform.
Results
- Each tool’s
execute()method reduced by 12–18 lines of crosscutting code - Rate limiting logic converged from 17 scattered sites to 1 implementation (
RateLimitedTool) - Path security checks converged from 10+ sites to 1 implementation (
PathGuardedTool) - Zero new compilation errors introduced (the 27 pre-existing clippy warnings on master are from unrelated code)
Lessons Learned
-
CI compatibility matters: ZeroClaw’s CI runs
cargo clippy -- -D warnings(warnings as errors) and strictcargo fmtchecks. Wrapper functions that replace direct type usage create unused import warnings. Solution:#[allow(unused_imports)]on combinedpub usestatements to satisfy both clippy and rustfmt simultaneously. -
Rebase discipline: With 59 upstream commits in a week, feature branches diverge rapidly. Small, single-commit branches are easier to rebase than multi-commit ones.
-
Security audit is upstream’s problem: Pre-existing
cargo auditfailures (e.g.,RUSTSEC-2026-0049inrustls-webpki) block all PRs equally. This is not something contributors can fix – it requires upstream dependency updates.