Imported from qhkm/zeptoclaw (
AGENTS.md). Install upstream withnpx skills add qhkm/zeptoclaw. Copyright stays with the author.
AGENTS.md
Project-level guidance for coding agents working in this repository.
Project Snapshot
- Language: Rust (edition 2021)
- Core binary:
zeptoclaw(src/main.rsthin entrypoint; CLI handlers insrc/cli/); stripped release size budgets are 11MB on linux-x86_64 and 7MB on aarch64, checked locally when relevant; escape valve for genuine new heavy deps is feature-gating, not bumping the budget - Extra binary:
benchmark(src/bin/benchmark.rs) - Benchmarks:
benches/message_bus.rs - Integration tests:
tests/integration.rs - Agent coding benchmark fixture:
test-coding/with intentionally buggy Python code and stdlib verification tests - Pristine agent comparison fixture:
test-coding-pristine/preserves the original failing state for repeatable head-to-head runs - Codebase: ~154,000 lines of Rust under
src/ - Channels: 10 active built-ins (Telegram, Slack, Discord, WhatsApp Web, WhatsApp Cloud, Lark, Email, Webhook, Serial, ACP); MQTT code is present but the Cargo feature is parked until
rumqttcno longer pins the vulnerablerustls-webpki - Runtimes: 6 (Native, Docker, Apple Container, Landlock, Firejail, Bubblewrap)
- Peripherals: 4 boards (ESP32, RPi, Arduino, Nucleo) with GPIO, I2C, NVS, Serial
- Skills: OpenClaw-compatible (reads
metadata.zeptoclaw>metadata.openclaw> raw) - Plugins: Command-mode (shell template) + Binary-mode (JSON-RPC 2.0 stdin/stdout)
- Library facade:
ZeptoAgent::builder()for embedding as a crate (Tauri, GUI apps) - Runtime provider resolution: builds chain in registry order only when
providers.fallback.enabled; honorsproviders.fallback.provider; can wrap chain withRetryProviderviaproviders.retry.* - Provider introspection CLI:
zeptoclaw provider statusprints resolved providers, wrapper config (retry/fallback), and quota usage snapshot - Provider onboarding validation: Anthropic uses
GET /v1/models; OpenAI-compatible presets validate keys with read-only endpoint checks, including Zhipu/GLM viaGET /models - Model discoverability hardening: gateway-style slash IDs (for example
anthropic/...) only infer OpenRouter when that provider is actually available, and live/model fetchnow honorsapi_versionwhile normalizing Azure deployment bases to/openai/models - OpenAI-compatible serve tool calling:
/v1/chat/completionsforwards request tools to providers, returns assistant/tool messages plus tool-call payloads in OpenAI format, streams tool-call deltas even for providers using the defaultchat_stream()adapter, and rejects unsupportedtool_choicevalues instead of silently ignoring them - Channel dispatch: avoids holding the channels map
RwLockacross asyncsend()awaits - Channel supervisor: polling (15s) detects dead channels, restarts with 60s cooldown, max 5 restarts
- Channel panic isolation: Slack/Discord/Webhook/WhatsApp/WhatsApp Web/WhatsApp Cloud/Lark/Email/Serial spawned tasks are wrapped with
catch_unwindand panic logging; MQTT code remains present while its Cargo feature is parked - Webhook auth hardening: generic webhook supports optional HMAC-SHA256 body signatures plus fixed server-side sender/chat identity by default (
trust_payload_identityis an explicit legacy escape hatch); WhatsApp Cloud verifiesX-Hub-Signature-256whenapp_secretis configured - Telegram allowlist hardening: numeric user IDs are the safe default for new setups; legacy username matching remains available only through
channels.telegram.allow_usernamesfor compatibility and emits warnings when non-numeric allowlist entries are present - Telegram config compatibility:
channels.telegramaccepts legacybot_token,allowed_senders, andallowed_chatskeys, and auto-enables whenenabledis omitted but a Telegram token is present - Email allowlist limitation surfaced:
channels.email.allowed_sendersmatches the parsedFromheader only and now emits config/runtime warnings so authenticated-mail enforcement is pushed upstream - Telegram outbound formatting: sends HTML parse mode with
||spoiler||→<tg-spoiler>conversion - Telegram response streaming: opt-in
channels.telegram.streamingroutes provider deltas through cumulative outbound stream phases, edits UTF-16-safe previews at a bounded cadence, preserves reply/topic routing, and falls back to a fresh final HTML message after preview failures - Discord outbound delivery: supports reply references and thread-create metadata (
discord_thread_*) inOutboundMessage - Cron scheduling hardening: dispatch timeout + exponential error backoff + one-shot delete-after-run only on success
- Model switching: Telegram
/modelsupports per-chat overrides (in-memory + long-term) - Persona switching:
/personacommand with presets and custom text, LTM persistence per chat - CLI interactive mode: TTY-gated local slash commands with rustyline tab completion when available, persisted REPL history, inline tool approval prompts, session-scoped
/trustoverride for local use,/modeland/personaoverrides,/tools,/template, and/clear - Memory injection: per-message query-matched injection via shared LTM on
AgentLoop(startup static injection removed) - Long-term memory tool guidance:
longterm_memorynow advertises explicit use/counter-use trigger phrases so agents persist durable corrections and preferences without duplicating repo docs or task-scoped context - Tool execution convergence: agent loop, MCP server, and embedded
ZeptoAgentfacade all route throughkernel::execute_tool()(shared safety scan + taint checks + single metrics recording); the facade also enforces per-tool timeout, panic capture, and optional approval handling for embedded coding backends - Coding tool hardening:
grepnow surfaces subprocess failures instead of silently returning "No matches";shelltruncates output at 2,000 lines / 50KB;edit_filerejects emptyold_textand supports optionalexpected_replacementsfor safer surgical edits - Tool composition: natural language tool creation with
{{param}}template interpolation - Filesystem hardening: filesystem write/edit tools now create parent directories one component at a time inside the workspace and use secure no-follow writes; mount validation rejects Unix regular-file mounts with multiple hard links in both blocked-path and allowlist flows; safety pre-scan keeps full path scanning while scanning file bodies with a narrow
shell_injectioncarve-out instead of skipping content wholesale - Secret storage hardening: config and panel token writes use user-only permissions on Unix (0600 files and 0700 ZeptoClaw directories) and repair permissions left by older versions
- Panel auth hardening: static bearer tokens use constant-time comparison, WebSocket connections exchange them for bounded 30-second single-use tickets, and CLI output points to the token file instead of printing the secret
- Safer default execution posture: fresh configs now start in
agent_mode = "assistant"with approvals enabled under therequire_for_dangerouspolicy - Gateway startup guard: degrade after N crashes to prevent crash loops
- Loop guard: SHA256 tool-call repetition detection with warn + circuit-breaker stop
- In-memory audit hash-chain:
src/audit.rsappends SHA-256-linked entries (record_audit_chain_event,verify_audit_chain_integrity,recent_audit_entries,audit_tip_hash), andkernel::execute_tool()now emits tool execution chain events with shell/network/spawn classification - Tool execution hardening: per-tool-call timeout + panic capture in both
process_messageandprocess_message_streamingtooljoin_allpaths - Runtime subprocess hardening: Native, Docker, Apple Container, Landlock, Firejail, and Bubblewrap scrub secret-like inherited environment variables by default;
runtime.env_passthroughexplicitly opts names back in, Unix timeouts terminate/reap the process group, and timed-out Docker containers are force-removed - Streaming tool parity:
process_message_streaming()now mirrors non-streaming hook callbacks, usage-metric accounting, success/failure logging, thinking/response feedback, and malformed tool-argument parse preservation - Context trimming: normal/emergency/critical compaction tiers (70%/90%/95%)
- Session repair: auto-fixes orphan tool results, empty/duplicate messages, alternation issues
- r8r bridge: optional WebSocket client for workflow approvals, health updates, and replay-safe duplicate-event acknowledgments
- Config hot-reload: gateway polls config mtime every 30s and applies provider/channel/safety updates
- Config validation:
zeptoclaw config checkrecognizes top-leveltunnelandr8r_bridge, plus agent defaults such astimezone,tool_timeout_secs, andsystem_prompt - Validation runs locally; GitHub Actions CI, E2E, and PR hygiene workflows are removed. Optional feature paths require local checks when changed; tag-triggered release and Docker publishing remain enabled
- Dependency audit baseline:
cargo deny checkpasses with patchedanyhow1.0.103,bcrypt0.19.2,chacha200.10.2,crossbeam-epoch0.9.20,h20.4.19,quinn-proto0.11.15,quick-xml0.41,lopdf0.42, andrustls0.23.45 (RUSTSEC-2026-0285) - Tool-schema repair:
utils::tool_schemasanitizes outbound schemas inToolRegistry::definitions*()(illegal property keys,typearrays, bare-string schemas,$refsiblings, top-level combinators,propertieson bare object schemas for llama.cpp GBNF) and repairs inbound args at every nesting depth inkernel::execute_tool()viaunrename_tool_args()+coerce_tool_args(); never rewrites a string the schema already permits, so"07030"survives a["string","integer"]union - Reasoning models:
reasoning_content(non-stream + stream delta) andfinish_reasonare parsed on the OpenAI-compatible path;resolve_assistant_text()falls back to reasoning only when there is no content and no tool calls, warning withfinish_reason. Reasoning deltas never stream as reply text.ChatOptions::with_reasoning_effort()setsreasoning_effort, omitted when unset - MCP transport: supports both HTTP and stdio MCP servers (
urlorcommand+ args/env) with tool registration duringcreate_agent() - Hands-lite:
HAND.toml+ bundled hands (researcher,coder,monitor) +handCLI - Panel CLI fallback: feature-disabled builds still parse
zeptoclaw panel ...and return explicit--features panelguidance instead of a raw unknown-subcommand error - Uninstall CLI:
zeptoclaw uninstallremoves~/.zeptoclaw;--remove-binarydeletes direct installs in~/.local/binor/usr/local/binand defers Homebrew/Cargo binaries to their package managers - Process exit codes: explicit
mainmapping for success (0) and error (1); uncaught panic/crash remains Rust default (101) - Tests: current local validation passes
cargo fmt -- --check,cargo clippy -- -D warnings,cargo nextest run --lib(3531 passed, 6 skipped), andcargo test --doc(128 passed, 27 ignored)
Task Tracking Protocol
Every session MUST track work via GitHub Issues.
- Start of session — Run
gh issue list --state open --limit 20and present open issues - New work — If no issue exists for the requested work, create one with
gh issue createbefore writing code. Use labels: type (bug/feat/rfc/chore/docs), area (area:tools/area:channels/etc.), priority (P1/P2/P3) - End of work — Create PR with
Closes #Nin body, orgh issue close Nfor direct commits - NEVER merge PRs — Only the user merges PRs. After creating a PR, present the URL and local validation results to the user, and only merge after explicit user approval
Skip issue creation only for trivial changes (typo fixes, one-line tweaks).
Post-Implementation Checklist
After completing ANY feature, you MUST:
- Close the GitHub issue —
Closes #Nin PR orgh issue close N CLAUDE.md— update architecture tree, test counts, module descriptions, new CLI flagsAGENTS.md(this file) — update project snapshot above
If you skip this, the next agent starts with stale context and wastes time.
File Ownership Rules (for parallel agents)
To avoid merge conflicts when multiple agents work simultaneously:
| Zone | Files | Rule |
|---|---|---|
| Shared (wire last) | src/main.rs, src/config/types.rs, */mod.rs |
Only ONE agent touches these. Wiring done after all parallel work finishes. |
| Provider | src/providers/<name>.rs |
One agent per provider file. |
| Tool | src/tools/<name>.rs |
One agent per tool file. |
| Utils | src/utils/<name>.rs |
One agent per util file. |
| New files | Any new *.rs |
Safe — no conflicts if it's a new file. |
Required Quality Gates
Before finishing any non-trivial change, run:
cargo fmt -- --check
cargo clippy -- -D warnings
cargo nextest run --lib
cargo test --doc
If benchmark-related code is changed, also run:
cargo bench --bench message_bus --no-run
Coding Rules
- Keep changes minimal and focused.
- Prefer small, composable functions over large blocks.
- Do not add
unwrap()/expect()in production paths unless failure is truly unrecoverable. - Preserve existing module boundaries and public APIs unless explicitly requested.
- Keep comments short and only where intent is non-obvious.
Runtime and Provider Notes
- Runtime isolation features must remain opt-in and degrade safely to native runtime.
- Provider wiring should remain consistent across config, onboarding, status output, and runtime behavior.
- Do not hardcode a single provider path when multiple providers are supported.
Documentation Rules
- Keep README/docs claims aligned with executable behavior.
- Do not add performance numbers unless they are reproducible with repository commands.
- If adding new commands or workflows, include a runnable example.
Release Versioning
- Use
patchfor backward-compatible bug fixes, reliability hardening, docs corrections, and internal refactors that do not add user-visible capability. - Use
minorfor backward-compatible new functionality such as new commands, flags, config fields, tools, providers, runtimes, channels, or other opt-in capabilities. - If upgrading should only give existing users fixes, choose
patch. - If upgrading gives existing users new capabilities without requiring migration, choose
minor.
Change Hygiene
- Do not revert unrelated local changes.
- If you detect unexpected file modifications during work, pause and ask before proceeding.
- Include file/line references when reporting review findings.
Common Patterns
Adding a config field
- Add field + doc comment to struct in
src/config/types.rs - Set default in
Defaultimpl - Add env override in
src/config/mod.rsif needed - Add field name to
KNOWN_TOP_LEVELinsrc/config/validate.rs
Adding a new tool
- Create
src/tools/<name>.rs - Implement
Tooltrait (name(),description(),parameters(),execute()) - Add
pub mod <name>;insrc/tools/mod.rs - Register in
src/kernel/registrar.rsinsideregister_all_tools()behindfilter.is_enabled("<name>") - If the tool assumes laptop/server environment (bash, filesystem, shell): make it opt-in by gating on
coding_tools_on(see the grep/find block in registrar.rs), add it to theTOOLSarray insrc/cli/tools.rswithopt_in: true, and add it toopt_in_tool_hint()insrc/tools/registry.rs
Adding a provider wrapper
- Create
src/providers/<name>.rs - Implement
LLMProvidertrait (must impl bothchat()andchat_stream()) - Add
pub mod <name>;+ re-export insrc/providers/mod.rs - Wire in
src/cli/common.rsprovider resolution
Key code patterns
ProviderRefwrapper indelegate.rs— convertsArc<dyn LLMProvider>toBox<dyn LLMProvider>- Builder pattern for provider wrappers —
RetryProvider::new(inner).with_max_retries(5) - Interior mutability via
Mutex<HashMap>— used in MetricsCollector, OpenAIProvider - Atomic counters via
AtomicU64— used in TokenBudget for lock-free token tracking - Recursion blocking — check
ctx.channelto prevent delegate/spawn infinite loops