CLAUDE.md · git:20260614.fa02424 · 2026-06-14 · sha256 0e0860f7f0a4a7ab
CLAUDE.md git:20260614.fa02424A
Immutable. This exact content never changes and is served at /api/v1/blob/0e0860f7f0a4a7ab.
# 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
```bash
# 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
| Crate | Purpose |
|-------|---------|
| `tokio` | Async runtime |
| `teloxide` | Telegram bot framework |
| `reqwest` | HTTP client for OpenRouter API |
| `serde` / `serde_json` | Serialization |
| `toml` | Config file parsing |
| `rmcp` | Official MCP Rust SDK (stdio transport) |
| `tracing` / `tracing-subscriber` | Structured logging |
| `anyhow` | Error handling |
| `futures` | Async 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:
| Job | Command | Purpose |
|-----|---------|---------|
| **Check** | `cargo check` | Fast compilation check |
| **Format** | `cargo fmt --all -- --check` | Enforces consistent formatting |
| **Clippy** | `cargo clippy -- -D warnings` | Lint — all warnings are errors |
| **Test** | `cargo test` | Runs all unit and integration tests |
| **Build** | `cargo build --release` | Release 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:**
```yaml
---
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.
```rust
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:
```yaml
---
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
```toml
[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
| Command | Behaviour |
|---------|-----------|
| `/supervise <text>` | Submit a new supervisor task |
| `/tasks` | List 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
| Table | Purpose |
|-------|---------|
| `sup_tasks` | One row per submitted task — title, user_request, classification (`task_type` / `risk_level` / `execution_mode`), current `state`, platform / user / chat origin |
| `sup_jobs` | One row per job dispatched within a task — backend, goal, prompt, status, result_summary, error, optional `parent_job_id` for spawned subjobs |
| `sup_transitions` | Append-only audit log of every state change (`from_state`, `to_state`, `actor`, `reason`, `occurred_at`) |
| `sup_artifacts` | Index of files written under `artifacts_dir` (`task_id`, `job_id`, `kind`, `path`, `sha256`, `bytes`) |
All four tables are created idempotently in `MemoryStore` at startup.