Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 each execute() method
  • Path security: is_path_allowed() and forbidden_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:

PRBatchToolsStatus
#49051 – FoundationShellTool (establishes wrappers.rs)Merged
#49442 – File toolsFileReadTool, FileWriteTool, FileEditToolOpen
#49473 – Search toolsGlobSearchTool, ContentSearchToolOpen
#49484 – Doc/ImagePdfReadTool, ImageInfoToolOpen
#49495 – Cron toolsCronAddTool, CronRemoveTool, CronUpdateToolOpen
#49526a – AI CLIGeminiCliTool, ClaudeCodeTool, CodexCliTool, OpenCodeCliToolOpen
#49536b – RunnerClaudeCodeRunnerToolOpen
#49547 – NetworkBrowserTool, HttpRequestTool, WebFetchTool, SkillToolOpen

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:

  1. CronRunTool: record_action() is deliberately placed after command validation to prevent path-probing attacks from consuming rate-limit budget. Preserved as-is.
  2. GoogleWorkspaceTool: Comment reads “Charge action budget only after all validation passes.” This is a security-conscious design – wrapping would break the invariant.
  3. BrowserTool / HttpRequestTool / WebFetchTool: Use can_act() (read-only gating) rather than enforce_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

  1. CI compatibility matters: ZeroClaw’s CI runs cargo clippy -- -D warnings (warnings as errors) and strict cargo fmt checks. Wrapper functions that replace direct type usage create unused import warnings. Solution: #[allow(unused_imports)] on combined pub use statements to satisfy both clippy and rustfmt simultaneously.

  2. 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.

  3. Security audit is upstream’s problem: Pre-existing cargo audit failures (e.g., RUSTSEC-2026-0049 in rustls-webpki) block all PRs equally. This is not something contributors can fix – it requires upstream dependency updates.