* fix(agent): prevent self-repair notification spam for stuck jobs When a stuck job exceeds max repair attempts, self-repair returns ManualRequired but never transitions the job to a terminal state. detect_stuck_jobs() re-finds it every cycle (~60s), sending a Telegram notification each time — infinite spam. Two-layer fix: 1. repair_stuck_job: transition to Failed before returning ManualRequired, so detect_stuck_jobs stops finding the job 2. Agent loop: HashSet dedup prevents duplicate ManualRequired notifications per job (defense-in-depth if transition fails) Also adds Pending → Failed to the state machine — stuck Pending jobs (dispatched but never started) could not be terminated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): handle transition failure in ManualRequired and adjust message Log error if Failed transition fails, and adjust the ManualRequired message to accurately reflect whether the job was marked failed or not. Addresses gemini-code-assist feedback on PR #1867. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): flatten nested Result, dedup Failed notifications, fix test state path Adversarial review findings: 1. CRITICAL: update_context returns Result<Result<()>>. Using .is_ok() on the outer only checks job existence, not transition success. Fixed: matches!(result, Ok(Ok(()))). 2. IMPORTANT: RepairResult::Failed arm had same spam potential as ManualRequired. Applied same dedup pattern. 3. IMPORTANT: Test exercised Pending→Failed (bypassing production path). Now transitions through InProgress→Stuck→Failed. 4. Dropped unrelated Cargo.lock version bump. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update state machine diagram to include Pending -> Failed Adversarial review finding: CLAUDE.md state diagram was the canonical reference but didn't show the new Pending -> Failed transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: j-bloggs <j-bloggs@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
IronClaw Development Guide
IronClaw is a secure personal AI assistant — user-first security, self-expanding tools, defense in depth, multi-channel access with proactive background execution.
Build & Test
cargo fmt # format
cargo clippy --all --benches --tests --examples --all-features # lint (zero warnings)
cargo test # unit tests
cargo test --features integration # + PostgreSQL tests
RUST_LOG=ironclaw=debug cargo run # run with logging
E2E tests: see tests/e2e/CLAUDE.md.
Code Style
- Prefer
crate::for cross-module imports;super::is fine in tests and intra-module refs - No
pub usere-exports unless exposing to downstream consumers - No
.unwrap()or.expect()in production code (tests are fine) - Use
thiserrorfor error types inerror.rs - Map errors with context:
.map_err(|e| SomeError::Variant { reason: e.to_string() })? - Prefer strong types over strings (enums, newtypes)
- Keep functions focused, extract helpers when logic is reused
- Comments for non-obvious logic only
- Prompt templates live in files, not Rust code: Multi-line prompt strings (mission goals, system prompts, CodeAct preambles) go in
crates/ironclaw_engine/prompts/*.mdand are loaded viainclude_str!(). Never inline large prompt templates as Rust string constants — they're hard to read, review, and iterate on. Single-line format strings are fine inline. - Logging levels matter for REPL/TUI:
info!andwarn!output appears in the REPL and corrupts the terminal UI. Usedebug!for internal diagnostics (trace analysis, reflection results, engine internals). Reserveinfo!for user-facing status that the REPL intentionally renders. Background tasks (reflection, trace analysis) must NEVER useinfo!— it breaks the interactive display.
Architecture
Prefer generic/extensible architectures over hardcoding specific integrations. Ask clarifying questions about the desired abstraction level before implementing.
Key traits for extensibility: Database, Channel, Tool, LlmProvider, SuccessEvaluator, EmbeddingProvider, NetworkPolicyDecider, Hook, Observer, Tunnel.
All I/O is async with tokio. Use Arc<T> for shared state, RwLock for concurrent access.
LLM data is never deleted. All LLM output — context fed to the model, reasoning, tool calls, messages, events, steps — is the most valuable data in the system. Never strip, truncate, or delete it from the database. Mark with timestamps, make filterable, but always retain. In-memory HashMaps are caches; the database (via Workspace) is the source of truth. "Cleanup" means evicting from in-memory caches, never deleting database rows.
Extracted Crates
Safety logic lives in crates/ironclaw_safety/, skills in crates/ironclaw_skills/. Import directly from the extracted crate (e.g. use ironclaw_safety::SafetyLayer, use ironclaw_skills::SkillRegistry). Do not use crate::safety:: or crate::skills:: for types that originate in extracted crates — src/safety/mod.rs and src/skills/mod.rs no longer glob-re-export. Local items defined in those modules (e.g. crate::skills::attenuate_tools) are fine.
Project Structure
crates/
└── ironclaw_safety/ # Extracted: prompt injection, validation, leak detection, policy
src/
├── lib.rs # Library root, module declarations
├── main.rs # Entry point, CLI args, startup
├── app.rs # App startup orchestration (channel wiring, DB init)
├── bootstrap.rs # Base directory resolution (~/.ironclaw), early .env loading
├── settings.rs # User settings persistence (~/.ironclaw/settings.json)
├── service.rs # OS service management (launchd/systemd daemon install)
├── tracing_fmt.rs # Custom tracing formatter
├── util.rs # Shared utilities
├── config/ # Configuration from env vars (split by subsystem)
│ ├── mod.rs # Re-exports all config types; top-level Config struct
│ ├── agent.rs, llm.rs, channels.rs, database.rs, sandbox.rs, skills.rs
│ ├── heartbeat.rs, routines.rs, safety.rs, embeddings.rs, wasm.rs
│ ├── tunnel.rs # Tunnel provider config (TUNNEL_PROVIDER, TUNNEL_URL, etc.)
│ └── secrets.rs, hygiene.rs, builder.rs, helpers.rs
├── error.rs # Error types (thiserror)
│
├── agent/ # Core agent loop, dispatcher, scheduler, sessions — see src/agent/CLAUDE.md
│
├── channels/ # Multi-channel input
│ ├── channel.rs # Channel trait, IncomingMessage, OutgoingResponse
│ ├── manager.rs # ChannelManager merges streams
│ ├── cli/ # Full TUI with Ratatui
│ ├── http.rs # HTTP webhook (axum) with secret validation
│ ├── webhook_server.rs # Unified HTTP server composing all webhook routes
│ ├── repl.rs # Simple REPL (for testing)
│ ├── web/ # Web gateway (browser UI) — see src/channels/web/CLAUDE.md
│ └── wasm/ # WASM channel runtime
│ ├── mod.rs
│ ├── bundled.rs # Bundled channel discovery
│ ├── capabilities.rs # Channel-specific capabilities (HTTP endpoint, emit rate)
│ ├── error.rs # WASM channel error types
│ ├── runtime.rs # WASM channel execution runtime
│ ├── setup.rs # WasmChannelSetup, setup_wasm_channels(), inject_channel_credentials()
│ └── wrapper.rs # Channel trait wrapper for WASM modules
│
├── cli/ # CLI subcommands (clap)
│ ├── mod.rs # Cli struct, Command enum (run/onboard/config/tool/registry/mcp/memory/pairing/service/doctor/status/completion)
│ └── config.rs, tool.rs, registry.rs, mcp.rs, memory.rs, pairing.rs, service.rs, doctor.rs, status.rs, completion.rs
│
├── registry/ # Extension registry catalog
│ ├── manifest.rs # ExtensionManifest, ArtifactSpec, BundleDefinition types
│ ├── catalog.rs # RegistryCatalog: load from filesystem and embedded JSON
│ └── installer.rs # RegistryInstaller: download, verify, install WASM artifacts
│
├── hooks/ # Lifecycle hooks (6 points: BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse)
│
├── tunnel/ # Tunnel abstraction for public internet exposure
│ ├── mod.rs # Tunnel trait, TunnelProviderConfig, create_tunnel(), start_managed_tunnel()
│ ├── cloudflare.rs # CloudflareTunnel (cloudflared binary)
│ ├── ngrok.rs # NgrokTunnel
│ ├── tailscale.rs # TailscaleTunnel (serve/funnel modes)
│ ├── custom.rs # CustomTunnel (arbitrary command with {host}/{port})
│ └── none.rs # NoneTunnel (local-only, no exposure)
│
├── observability/ # Pluggable event/metric recording (noop, log, multi)
│
├── orchestrator/ # Internal HTTP API for sandbox containers
│ ├── api.rs # Axum endpoints (LLM proxy, events, prompts)
│ ├── auth.rs # Per-job bearer token store
│ └── job_manager.rs # Container lifecycle (create, stop, cleanup)
│
├── worker/ # Runs inside Docker containers
│ ├── container.rs # Container worker runtime (ContainerDelegate + shared agentic loop)
│ ├── job.rs # Background job worker (JobDelegate + shared agentic loop)
│ ├── claude_bridge.rs # Claude Code bridge (spawns claude CLI)
│ └── proxy_llm.rs # LlmProvider that proxies through orchestrator
│
├── safety/ # Re-export shim for crates/ironclaw_safety (see Extracted Crates)
│
├── llm/ # Multi-provider LLM integration — see src/llm/CLAUDE.md
│
├── tools/ # Extensible tool system
│ ├── tool.rs # Tool trait, ToolOutput, ToolError
│ ├── registry.rs # ToolRegistry for discovery
│ ├── rate_limiter.rs # Shared sliding-window rate limiter
│ ├── builtin/ # Built-in tools (echo, time, json, http, web_fetch, file, shell, memory, message, job, routine, extension_tools, skill_tools, secrets_tools)
│ ├── builder/ # Dynamic tool building
│ │ ├── core.rs # BuildRequirement, SoftwareType, Language
│ │ ├── templates.rs # Project scaffolding
│ │ ├── testing.rs # Test harness integration
│ │ └── validation.rs # WASM validation
│ ├── mcp/ # Model Context Protocol
│ │ ├── client.rs # MCP client over HTTP
│ │ ├── factory.rs # create_client_from_config() — transport dispatch factory
│ │ ├── protocol.rs # JSON-RPC types
│ │ └── session.rs # MCP session management (Mcp-Session-Id header, per-server state)
│ └── wasm/ # Full WASM sandbox (wasmtime)
│ ├── runtime.rs # Module compilation and caching
│ ├── wrapper.rs # Tool trait wrapper for WASM modules
│ ├── host.rs # Host functions (logging, time, workspace)
│ ├── limits.rs # Fuel metering and memory limiting
│ ├── allowlist.rs # Network endpoint allowlisting
│ ├── credential_injector.rs # Safe credential injection
│ ├── loader.rs # WASM tool discovery from filesystem
│ ├── rate_limiter.rs # Per-tool rate limiting
│ ├── error.rs # WASM-specific error types
│ └── storage.rs # Linear memory persistence
│
├── db/ # Dual-backend persistence (PostgreSQL + libSQL) — see src/db/CLAUDE.md
│
├── workspace/ # Persistent memory system — see src/workspace/README.md
│
├── context/ # Job context isolation (JobState, JobContext, ContextManager)
├── estimation/ # Cost/time/value estimation with EMA learning
├── evaluation/ # Success evaluation (rule-based, LLM-based)
│
├── sandbox/ # Docker execution sandbox
│ ├── config.rs # SandboxConfig, SandboxPolicy enum (ReadOnly/WorkspaceWrite/FullAccess)
│ ├── manager.rs # SandboxManager orchestration
│ ├── container.rs # ContainerRunner, Docker lifecycle
│ └── proxy/ # Network proxy: domain allowlist, credential injection, CONNECT tunnel
│
├── secrets/ # Secrets management (AES-256-GCM, OS keychain for master key)
│
├── profile.rs # Psychographic profile types, 9-dimension analysis framework
│
├── setup/ # 7-step onboarding wizard — see src/setup/README.md
│
├── skills/ # SKILL.md prompt extension system — see .claude/rules/skills.md
│
└── history/ # Persistence (PostgreSQL repositories, analytics)
tests/
├── *.rs # Integration tests (workspace, heartbeat, WS gateway, pairing, etc.)
├── test-pages/ # HTML→Markdown conversion fixtures
└── e2e/ # Python/Playwright E2E scenarios (see tests/e2e/CLAUDE.md)
Database
Dual-backend: PostgreSQL + libSQL/Turso. All new persistence features must support both backends. See src/db/CLAUDE.md and .claude/rules/database.md.
Module Specs
When modifying a module with a spec, read the spec first. Code follows spec; spec is the tiebreaker.
Module-owned initialization: Module-specific initialization logic (database connection, transport creation, channel setup) must live in the owning module as a public factory function — not in main.rs or app.rs. These entry-point files orchestrate calls to module factories. Feature-flag branching (#[cfg(feature = ...)]) must be confined to the module that owns the abstraction.
| Module | Spec |
|---|---|
src/agent/ |
src/agent/CLAUDE.md |
src/channels/web/ |
src/channels/web/CLAUDE.md |
src/db/ |
src/db/CLAUDE.md |
src/llm/ |
src/llm/CLAUDE.md |
src/setup/ |
src/setup/README.md |
src/tools/ |
src/tools/README.md |
src/workspace/ |
src/workspace/README.md |
crates/ironclaw_engine/ |
crates/ironclaw_engine/CLAUDE.md |
tests/e2e/ |
tests/e2e/CLAUDE.md |
Job State Machine
Pending -> InProgress -> Completed -> Submitted -> Accepted
\ \-> Failed
\-> Failed \-> Stuck -> InProgress (recovery)
\-> Failed
Skills System
SKILL.md files extend the agent's prompt with domain-specific instructions. See .claude/rules/skills.md for full details.
- Trust model: Trusted (user-placed in
~/.ironclaw/skills/or workspaceskills/, full tool access) vs Installed (registry, read-only tools) - Selection pipeline: gating (check bin/env/config requirements) -> scoring (keywords/patterns/tags) -> budget (fit within
SKILLS_MAX_TOKENS) -> attenuation (trust-based tool ceiling) - Skill tools:
skill_list,skill_search,skill_install,skill_remove
Configuration
See .env.example for all environment variables. LLM backends (nearai, openai, anthropic, ollama, openai_compatible, tinfoil, bedrock) documented in src/llm/CLAUDE.md.
Adding a New Channel
- Create
src/channels/my_channel.rs - Implement the
Channeltrait - Add config in
src/config/channels.rs - Wire up in
src/app.rschannel setup section
Workspace & Memory
Persistent memory with hybrid search (FTS + vector via RRF). Four tools: memory_search, memory_write, memory_read, memory_tree. Identity files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md) injected into system prompt. Heartbeat system runs proactive periodic execution (default: 30 minutes), reading HEARTBEAT.md and notifying via channel if findings. See src/workspace/README.md.
Debugging
RUST_LOG=ironclaw=trace cargo run # verbose
RUST_LOG=ironclaw::agent=debug cargo run # agent module only
RUST_LOG=ironclaw=debug,tower_http=debug cargo run # + HTTP request logging
Current Limitations
- Domain-specific tools (
marketplace.rs,restaurant.rs, etc.) are stubs - Integration tests need testcontainers for PostgreSQL
- MCP: no streaming support; stdio/HTTP/Unix transports all use request-response
- WIT bindgen: auto-extract tool schema from WASM is stubbed
- Built tools get empty capabilities; need UX for granting access
- No tool versioning or rollback
- Observability: only
logandnoopbackends (no OpenTelemetry)