agentleFS
chinkanchinkan/RustFox/CLAUDE.md

RustFox

CLAUDE.md11 starsChanged 41 days agoMITView on GitHub

RustFox is a Telegram AI assistant written in Rust. It connects to Telegram as a bot, uses OpenRouter LLM for inference (default model: qwen/qwen3-235b-a22b), provides built-in sandboxed tools (file I/O, command execution), and supports MCP (Model Context Protocol) servers for extensible tool integration. It implements an agentic loop that iterates tool calls until a final text response is produced (max iterations configurable, default 25). Copy config.example.toml to config.toml and fill in credentials. The config.toml file is gitignored and must never…

  • Reads credentials

We split this CLAUDE.md into the rules it gives an agent

17 rules. About 0 are common, 1 is distinctive, and 16 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 16

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If LLM returns tool calls, execute_tool() dispatches to built-in tools or MCP tools

    line 72
  2. Very specific:

    Use anyhow::bail!() for early returns with error messages

    line 103
  3. Very specific:

    Use .context("message") on Result chains for context propagation

    line 104
  4. Very specific:

    All file and command operations go through validate_sandbox_path() which canonicalizes both the sandbox root and the requested path, then …

    line 110
  5. Very specific:

    All jobs use dtolnay/rust-toolchain@stable and Swatinem/rust-cache@v2 for caching.

    line 140
  6. Distinctive:

    No automated tests exist yet.

    line 144
  7. Very specific:

    Place unit tests in #[cfg(test)] mod tests blocks within each source file

    line 146
  8. Very specific:

    Add a ToolDefinition entry in builtin_tool_definitions() in src/tools.rs

    line 154
  9. Very specific:

    Add a match arm in execute_builtin_tool() in src/tools.rs

    line 155
  10. Very specific:

    Use validate_sandbox_path() if the tool accesses the filesystem

    line 156
  11. Very specific:

    Add a new if text == "/command" block in handle_message() in src/bot.rs (before the LLM processing section)

    line 160
  12. Very specific:

    Update default_model() in src/config.rs.

    line 164
  13. Very specific:

    Add a [[mcp_servers]] block to config.toml with name, command, args, and optional env fields.

    line 168
  14. Very specific:

    Create skills/<skill-name>/SKILL.md with frontmatter and instruction body

    line 190
  15. Very specific:

    Every backend implements Backend from src/supervisor/backend/mod.rs.

    line 265
  16. Very specific:

    All four tables are created idempotently in MemoryStore at startup.

    line 358
  17. Very specific:

    Default five-role vocabulary.

    line 368

≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md - RustFox Development Guide

Project Overview

RustFox is a Telegram AI assistant written in Rust. It connects to Telegram as a bot, uses OpenRouter LLM for inference (default model: qwen/qwen3-235b-a22b), provides built-in sandboxed tools (file I/O, command execution), and supports MCP (Model Context Protocol) servers for extensible tool integration. It implements an agentic loop that iterates tool calls until a final text response is produced (max iterations configurable, default 25).

Build & Run

# Build (debug)
cargo build

# Build (release)
cargo build --release

# Run (uses ./config.toml by default)
cargo run

# Run with custom config path
cargo run -- /path/to/config.toml

# Check without building
cargo check

# Format code
cargo fmt

# Lint
cargo clippy

Configuration

Copy config.example.toml to config.toml and fill in credentials. The config.toml file is gitignored and must never be committed. Required fields:

  • telegram.bot_token - Telegram Bot API token
  • telegram.allowed_user_ids - Whitelist of Telegram user IDs
  • openrouter.api_key - OpenRouter API key
  • sandbox.allowed_directory - Directory for sandboxed file/command operations

Home directory

RustFox stores all state under a single home directory (default ~/.rustfox), resolved as: RUSTFOX_HOME env (absolute) → [general].home config → ~/.rustfox. Layout: config.toml, rustfox.db, skills/, agents/, workspace/ (the sandbox), artifacts/, user_model.md. Each path can be pinned to an absolute location in config.toml; unset paths fall back to the home default. Run isolated instances with RUSTFOX_HOME=.... See docs/persistent-home-directory.md. Path resolution lives in src/home.rs (Config::resolve writes the resolved absolute paths back into the config). Bundled skills/agents are seed-copied on first run; /update-skills re-syncs them using <home>/skills-lock.json.

Architecture

src/
├── main.rs      # Entry point: logging init, config loading, MCP setup, bot launch
├── config.rs    # TOML config parsing (Config, TelegramConfig, OpenRouterConfig, SandboxConfig, McpServerConfig)
├── llm.rs       # OpenRouter API client (ChatMessage, ToolCall, ToolDefinition, LlmClient)
├── tools.rs     # Built-in tool definitions and execution with sandbox path validation
├── mcp.rs       # MCP client manager (McpManager, McpConnection) for external tool servers
└── bot.rs       # Telegram bot handler: message routing, agentic loop, conversation state

Data Flow

  1. User sends a Telegram message
  2. bot.rs filters by allowed_user_ids, routes commands (/start, /clear, /tools)
  3. Non-command messages enter process_with_llm() which runs the agentic loop
  4. llm.rs sends conversation history + tool definitions to OpenRouter
  5. If LLM returns tool calls, execute_tool() dispatches to built-in tools or MCP tools
  6. Tool results are appended to conversation and the loop repeats (up to 10 iterations)
  7. Final text response is split into <=4000 char chunks and sent back via Telegram

Key Components

  • AppState (bot.rs): Shared state holding LlmClient, Config, McpManager, and per-user Conversation map behind a Mutex
  • LlmClient (llm.rs): Stateless HTTP client for OpenRouter's /chat/completions endpoint with tool-calling support
  • McpManager (mcp.rs): Manages stdio-based MCP server child processes. Tools are namespaced as mcp_{server_name}_{tool_name}
  • Sandbox validation (tools.rs): All file/command operations are restricted to the configured sandbox directory via path canonicalization

Code Conventions

Rust Patterns

  • Edition: 2021
  • Async runtime: Tokio with full features
  • Error handling: anyhow::Result throughout, with .context() / .with_context() for error messages
  • Logging: tracing crate with tracing-subscriber (env filter: RUST_LOG, default info,rustfox=debug)
  • Serialization: serde derive macros with #[serde(skip_serializing_if = "Option::is_none")] for optional fields
  • Shared state: Arc<AppState> passed via teloxide's dependency injection (dptree::deps!)
  • Concurrency: tokio::sync::Mutex for per-user conversation map (not std::sync::Mutex)

Naming

  • Module names are single words (bot, config, llm, mcp, tools)
  • Struct fields use snake_case
  • JSON field renames use #[serde(rename = "type")] where the Rust field name differs from the API field

Error Handling Style

  • Use anyhow::bail!() for early returns with error messages
  • Use .context("message") on Result chains for context propagation
  • MCP connection failures are logged but do not abort startup (connect_all catches errors)
  • Tool execution errors return error strings to the LLM rather than crashing

Security

  • All file and command operations go through validate_sandbox_path() which canonicalizes both the sandbox root and the requested path, then verifies the requested path starts with the sandbox root
  • The bot only responds to user IDs in allowed_user_ids
  • config.toml (containing secrets) is gitignored

Dependencies

CratePurpose
tokioAsync runtime
teloxideTelegram bot framework
reqwestHTTP client for OpenRouter API
serde / serde_jsonSerialization
tomlConfig file parsing
rmcpOfficial MCP Rust SDK (stdio transport)
tracing / tracing-subscriberStructured logging
anyhowError handling
futuresAsync utilities

CI (GitHub Actions)

CI runs on every push to main and on pull requests targeting main. The pipeline is defined in .github/workflows/ci.yml and runs five parallel jobs:

JobCommandPurpose
Checkcargo checkFast compilation check
Formatcargo fmt --all -- --checkEnforces consistent formatting
Clippycargo clippy -- -D warningsLint — all warnings are errors
Testcargo testRuns all unit and integration tests
Buildcargo build --releaseRelease build (runs after all other jobs pass)

All jobs use dtolnay/rust-toolchain@stable and Swatinem/rust-cache@v2 for caching. Before opening a PR, ensure cargo fmt, cargo clippy -- -D warnings, and cargo test pass locally.

Testing

No automated tests exist yet. When adding tests:

  • Place unit tests in #[cfg(test)] mod tests blocks within each source file
  • Integration tests go in a top-level tests/ directory
  • The sandbox path validation logic in tools.rs and message splitting in bot.rs are good candidates for unit tests

Common Tasks

Adding a new built-in tool

  1. Add a ToolDefinition entry in builtin_tool_definitions() in src/tools.rs
  2. Add a match arm in execute_builtin_tool() in src/tools.rs
  3. Use validate_sandbox_path() if the tool accesses the filesystem

Adding a new bot command

  1. Add a new if text == "/command" block in handle_message() in src/bot.rs (before the LLM processing section)

Changing the default LLM model

Update default_model() in src/config.rs. Users can also override this in their config.toml.

Adding a new MCP server

Add a [[mcp_servers]] block to config.toml with name, command, args, and optional env fields. See config.example.toml for examples.

Adding a new bot skill

Bot skills are natural-language instructions loaded at startup and injected into the LLM's system prompt. Each skill must be in its own folder following the Claude agent skills format:

skills/
  skill-name/
    SKILL.md           # Required: YAML frontmatter + instruction body
    supporting-file.*  # Optional: templates, examples, reference docs

SKILL.md frontmatter:

---
name: skill-name       # lowercase letters, numbers, hyphens only
description: Brief description of what this skill does
tags: [tag1, tag2]     # optional: for organization
---
  1. Create skills/<skill-name>/SKILL.md with frontmatter and instruction body
  2. The skill is auto-loaded at startup — no code changes needed
  3. Configure the skills directory in config.toml: [skills] directory = "skills"

All skills are represented in the system prompt by metadata only (name + description). Instruction skills (no model in frontmatter) have their full content loaded by the agent via read_skill_file(skill_name="...", relative_path="SKILL.md") when relevant. Subagent skills (model set) are invoked via invoke_agent(agent="name", prompt="..."). The orchestration skill teaches the agent when to call which subagent and when to override the model (e.g. model="anthropic/claude-sonnet-4-6" for thread-writer-hk).

Subagent tool whitelist: For subagent skills, the frontmatter tools: list must use the exact tool names as seen by the agent. MCP tools are named mcp_{server_name}_{tool_name} (e.g. mcp_google-workspace_query_gmail_emails). These names are logged at startup when MCP servers connect (MCP server 'X' provides N tools). A mismatch (e.g. declaring search_gmail_messages when the server exposes query_gmail_emails) causes the subagent to have no access to that tool.

Daily News to Threads flow: The daily-news-to-threads orchestration skill (instruction) directs the main agent to: (1) call the news-fetcher subagent (default model) to get AI news from Gmail Google 快訊, (2) call the thread-writer-hk subagent with model override to write a HK-style Threads thread with verified links, (3) post the thread via Threads MCP and report success. Requires Gmail (google-workspace), fetch, and threads MCP servers in config.

Files Not to Commit

  • config.toml - Contains API keys and tokens
  • .env - Environment variables
  • /target/ - Build artifacts

Supervisor (Autopilot v2)

The supervisor is a generic autonomous task runner that lives alongside the existing chat agent. It accepts a free-form request, classifies it, picks a plan, dispatches work to one or more backends (reasoning, shell, MCP, Claude Code CLI, Codex CLI, scripts), verifies the result, and persists artifacts + audit transitions to SQLite.

Module tree (src/supervisor/)

src/supervisor/
 mod.rs              — Supervisor facade: submit / execute_now / pause / resume / state / artifacts
 task.rs             — Task, TaskType, RiskLevel, ExecutionMode, TaskStatus enums
 job.rs              — Job, JobType, JobStatus, JobOutput, Evidence
 state.rs            — transition_allowed() — single source of truth for the state machine
 store.rs            — TaskStore: CRUD over sup_tasks / sup_jobs / sup_transitions
 intake.rs           — IntakeRouter::normalize() → Task from raw text
 classifier.rs       — Classifier trait + HeuristicClassifier / LlmBackedClassifier / SkillAwareClassifier
 policy.rs           — PolicyEngine: AutoExecute | Clarify | RequireApproval | UseFallbackBackend | StopAndReport
 planner.rs          — Planner: Task → Plan { jobs, parallel_groups }
 workflow.rs         — Fast / Standard / Rigorous workflow stage templates
 orchestrator.rs     — Orchestrator: executes Plan with fallback + parallel groups + subjob spawning
 verification.rs     — VerificationEngine: ≥1 evidence per job gate
 artifact.rs         — ArtifactManager: write_text() (redacts) + list()
 workspace.rs        — WorkspaceManager: per-task git branch / optional worktree
 reporter.rs         — Human-readable per-job summary
 redact.rs           — Secret scrubber for api_key / password / secret / token / bearer values
 backend/
  mod.rs            — Backend trait + BackendCapabilities + Registry + RunContext
  reasoning.rs      — Wraps the chat Agent
  shell.rs          — Sandboxed shell commands
  mcp.rs            — Calls tools on a connected MCP server
  claude_code.rs    — Spawns the `claude` CLI as a backend
  codex.rs          — Spawns the `codex` CLI as a backend
  script.rs         — Runs a script file from the sandbox

Lifecycle

INTAKE → CLASSIFY → ROUTE
              ↓
       (CLARIFY) | (PREPARE_WORKSPACE)? → PLAN → EXECUTE
              ↓                                    ↓
              (Paused ⇄ Execute)         REVIEW (rigorous mode)
                                                   ↓
                                              VERIFY
                                                   ↓
                              REPORT → ARCHIVE → DONE
                                  ↘ Failed   ↘ Cancelled

state.rs::transition_allowed(from, to) enumerates every legal edge. Add a new arm there before introducing a new state — the rest of the supervisor treats unknown transitions as bugs.

Backend trait + adding a new backend

Every backend implements Backend from src/supervisor/backend/mod.rs. The defaults from spec §10 (prepare, collect_result, verify_result, cancel, resume) are already provided; most backends only override name, capabilities, can_handle, and run. Register an Arc<MyBackend> into the Registry at startup.

struct EchoBackend;
#[async_trait::async_trait]
impl rustfox::supervisor::backend::Backend for EchoBackend {
    fn name(&self) -> &str { "echo" }
    fn capabilities(&self) -> rustfox::supervisor::backend::BackendCapabilities {
        rustfox::supervisor::backend::BackendCapabilities { reasoning: true, ..Default::default() }
    }
    fn can_handle(&self, _: &rustfox::supervisor::job::JobType) -> bool { true }
    async fn run(&self, job: &mut rustfox::supervisor::job::Job, _: &rustfox::supervisor::backend::RunContext)
        -> anyhow::Result<rustfox::supervisor::job::JobOutput> { /* ... */ todo!() }
}
let mut reg = rustfox::supervisor::backend::Registry::new();
reg.register(std::sync::Arc::new(EchoBackend));

Adding a workflow skill pack

Drop a skills/sup-<name>/SKILL.md with frontmatter:

---
name: sup-<name>
description: One-line summary
supervisor:
  workflow: research          # or: writing | refactor | research | ops | review
  required_capabilities: [research, reasoning]
---

Skill packs are auto-loaded by the existing SkillRegistry at startup; the SkillAwareClassifier consults them and overrides the default required_capabilities when the request keyword matches the skill name (prefix sup- is stripped before matching).

TOML config keys

[supervisor]
default_autonomy_mode = "standard"   # "fast" | "standard" | "rigorous"
artifacts_dir         = "supervisor/artifacts"

[supervisor.risk]
require_approval_for_low    = false
require_approval_for_medium = false
auto_execute_only_low       = false   # when true, Medium escalates to RequireApproval

Defaults preserve M1–M6 behavior (Medium-risk auto-executes). Flip individual fields to tighten the gate.

Bot commands

CommandBehaviour
/supervise <text>Submit a new supervisor task
/tasksList active / recent tasks
/resume <id>Resume a paused task
/cancel <id>Cancel a task
/approve <id>Approve a task that hit RequireApproval
/clarify <id> <text>Reply to a Clarify prompt

The command parser is wired and emits a startup log line in main.rs; routing user commands into supervisor handlers in the live Telegram dispatcher is a minimum-viable integration (M3.8 / M7.3) and the full handler surface is a follow-up task.

Artifacts

Per-task artifacts are written to <supervisor.artifacts_dir>/<task_id>/<filename> and indexed in sup_artifacts (kind, path, sha256, bytes). Every artifact write goes through redact::redact(), which scrubs values that follow api_key, password, secret, token, or bearer (case-insensitive) and replaces them with *** while preserving the key + separator so the file stays human-readable. Standard kinds emitted by the pipeline: intake, classification, policy, plan, workspace (when workspace prepared), and result (Reporter Markdown summary).

Database tables added

TablePurpose
sup_tasksOne row per submitted task — title, user_request, classification (task_type / risk_level / execution_mode), current state, platform / user / chat origin
sup_jobsOne row per job dispatched within a task — backend, goal, prompt, status, result_summary, error, optional parent_job_id for spawned subjobs
sup_transitionsAppend-only audit log of every state change (from_state, to_state, actor, reason, occurred_at)
sup_artifactsIndex of files written under artifacts_dir (task_id, job_id, kind, path, sha256, bytes)

All four tables are created idempotently in MemoryStore at startup.

Agent skills

Issue tracker

GitHub Issues. See docs/agents/issue-tracker.md.

Triage labels

Default five-role vocabulary. See docs/agents/triage-labels.md.

Domain docs

Single-context. See docs/agents/domain.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.