- 22 AI providers (OpenRouter, Anthropic, OpenAI, Mistral, etc.) - 7 channels (CLI, Telegram, Discord, Slack, iMessage, Matrix, Webhook) - 5-step onboarding wizard with Project Context personalization - OpenClaw-aligned system prompt (SOUL.md, IDENTITY.md, USER.md, AGENTS.md, etc.) - SQLite memory backend with auto-save - Skills system with on-demand loading - Security: autonomy levels, command allowlists, cost limits - 532 tests passing, 0 clippy warnings
5.4 KiB
5.4 KiB
Contributing to ZeroClaw
Thanks for your interest in contributing to ZeroClaw! This guide will help you get started.
Development Setup
# Clone the repo
git clone https://github.com/theonlyhennygod/zeroclaw.git
cd zeroclaw
# Build
cargo build
# Run tests (180 tests, all must pass)
cargo test
# Format & lint (must pass before PR)
cargo fmt && cargo clippy -- -D warnings
# Release build (~3.1MB)
cargo build --release
Architecture: Trait-Based Pluggability
ZeroClaw's architecture is built on traits — every subsystem is swappable. This means contributing a new integration is as simple as implementing a trait and registering it in the factory function.
src/
├── providers/ # LLM backends → Provider trait
├── channels/ # Messaging → Channel trait
├── observability/ # Metrics/logging → Observer trait
├── runtime/ # Platform adapters → RuntimeAdapter trait
├── tools/ # Agent tools → Tool trait
├── memory/ # Persistence/brain → Memory trait
└── security/ # Sandboxing → SecurityPolicy
How to Add a New Provider
Create src/providers/your_provider.rs:
use async_trait::async_trait;
use anyhow::Result;
use crate::providers::traits::Provider;
pub struct YourProvider {
api_key: String,
client: reqwest::Client,
}
impl YourProvider {
pub fn new(api_key: Option<&str>) -> Self {
Self {
api_key: api_key.unwrap_or_default().to_string(),
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl Provider for YourProvider {
async fn chat(&self, message: &str, model: &str, temperature: f64) -> Result<String> {
// Your API call here
todo!()
}
}
Then register it in src/providers/mod.rs:
"your_provider" => Ok(Box::new(your_provider::YourProvider::new(api_key))),
How to Add a New Channel
Create src/channels/your_channel.rs:
use async_trait::async_trait;
use anyhow::Result;
use tokio::sync::mpsc;
use crate::channels::traits::{Channel, ChannelMessage};
pub struct YourChannel { /* config fields */ }
#[async_trait]
impl Channel for YourChannel {
fn name(&self) -> &str { "your_channel" }
async fn send(&self, message: &str, recipient: &str) -> Result<()> {
// Send message via your platform
todo!()
}
async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> Result<()> {
// Listen for incoming messages, forward to tx
todo!()
}
async fn health_check(&self) -> bool { true }
}
How to Add a New Observer
Create src/observability/your_observer.rs:
use crate::observability::traits::{Observer, ObserverEvent, ObserverMetric};
pub struct YourObserver { /* client, config, etc. */ }
impl Observer for YourObserver {
fn record_event(&self, event: &ObserverEvent) {
// Push event to your backend
}
fn record_metric(&self, metric: &ObserverMetric) {
// Push metric to your backend
}
fn name(&self) -> &str { "your_observer" }
}
How to Add a New Tool
Create src/tools/your_tool.rs:
use async_trait::async_trait;
use anyhow::Result;
use serde_json::{json, Value};
use crate::tools::traits::{Tool, ToolResult};
pub struct YourTool { /* security policy, config, etc. */ }
#[async_trait]
impl Tool for YourTool {
fn name(&self) -> &str { "your_tool" }
fn description(&self) -> &str { "Does something useful" }
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"input": { "type": "string", "description": "The input" }
},
"required": ["input"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let input = args["input"].as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'input'"))?;
Ok(ToolResult {
success: true,
output: format!("Processed: {input}"),
error: None,
})
}
}
Pull Request Checklist
cargo fmt— code is formattedcargo clippy -- -D warnings— no warningscargo test— all 129+ tests pass- New code has inline
#[cfg(test)]tests - No new dependencies unless absolutely necessary (we optimize for binary size)
- README updated if adding user-facing features
- Follows existing code patterns and conventions
Commit Convention
We use Conventional Commits:
feat: add Anthropic provider
fix: path traversal edge case with symlinks
docs: update contributing guide
test: add heartbeat unicode parsing tests
refactor: extract common security checks
chore: bump tokio to 1.43
Code Style
- Minimal dependencies — every crate adds to binary size
- Inline tests —
#[cfg(test)] mod tests {}at the bottom of each file - Trait-first — define the trait, then implement
- Security by default — sandbox everything, allowlist, never blocklist
- No unwrap in production code — use
?,anyhow, orthiserror
Reporting Issues
- Bugs: Include OS, Rust version, steps to reproduce, expected vs actual
- Features: Describe the use case, propose which trait to extend
- Security: See SECURITY.md for responsible disclosure
License
By contributing, you agree that your contributions will be licensed under the MIT License.