rust-sop
Rust SOP — joelclaw Standard Operating Procedures
Idiomatic Rust patterns and conventions for all Rust code in joelclaw. Primary consumer: clawnode (embedded PDS + mesh daemon). Written for agent workers — include this skill when delegating Rust work to codex.
Project Conventions
Stack
| Layer | Crate | Notes |
|---|---|---|
| Runtime | tokio |
Multi-thread by default, current_thread for tests |
| HTTP | axum |
XRPC endpoints, health checks |
| Database | libsql |
Local-first, native vector search (F32_BLOB, DiskANN) |
| Serialization | serde + serde_json |
All AT Proto records are JSON |
| Error handling | thiserror (libraries), anyhow (binaries) |
Never unwrap() in production |
| CLI | clap (derive) |
Single binary: daemon + CLI subcommands |
| Logging | tracing + tracing-subscriber |
Structured, not println |
| Testing | built-in + tokio::test |
Property tests with proptest where useful |
| AT Proto types | atrium crates |
Code-generated from lexicons |
Project Structure
clawnode/
├── Cargo.toml
├── src/
│ ├── main.rs # CLI entry, clap dispatch
│ ├── lib.rs # Public API surface
│ ├── daemon/ # Long-running daemon logic
│ │ ├── mod.rs
│ │ ├── server.rs # Axum HTTP server (XRPC)
│ │ ├── firehose.rs # WebSocket subscribeRepos
│ │ └── heartbeat.rs # Presence + health
│ ├── pds/ # Embedded PDS
│ │ ├── mod.rs
│ │ ├── repo.rs # MST / record storage
│ │ ├── records.rs # CRUD operations
│ │ └── auth.rs # Session management
│ ├── storage/ # libSQL abstraction
│ │ ├── mod.rs
│ │ ├── migrations.rs # Schema migrations
│ │ └── vectors.rs # Vector search helpers
│ ├── mesh/ # Service discovery + proxy
│ │ ├── mod.rs
│ │ ├── registry.rs # ServiceRegistry trait
│ │ └── proxy.rs # Redis/Typesense/Inngest proxy
│ ├── socket/ # Unix socket JSON-RPC
│ │ └── mod.rs
│ └── cli/ # CLI subcommands
│ ├── mod.rs
│ ├── status.rs
│ ├── recall.rs
│ └── send.rs
├── tests/
│ ├── integration/
│ └── common/
└── migrations/
└── 001_initial.sql
Naming Conventions
- Crate name:
clawnode(binary), internal lib crate alsoclawnode - Modules: snake_case, flat where possible (
storage.rsnotstorage/mod.rsunless submodules needed) - Types: PascalCase. Prefix domain types:
PdsRecord,MeshNode,ServiceEntry - Traits: Adjective or noun (
Discoverable,ServiceRegistry,RecordStore) - Error enums:
<Module>Error(StorageError,PdsError,MeshError) - Constants: SCREAMING_SNAKE_CASE
Core Patterns
Error Handling
// Library code: thiserror for typed errors
#[derive(thiserror::Error, Debug)]
pub enum PdsError {
#[error("record not found: {collection}/{rkey}")]
NotFound { collection: String, rkey: String },
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("invalid record: {0}")]
InvalidRecord(String),
}
// Application/binary code: anyhow for ergonomic error chains
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = load_config()
.context("failed to load clawnode config")?;
Ok(())
}
Rules:
- Never
unwrap()in production code. Useexpect("reason")only for truly invariant conditions. - Use
?operator everywhere. Add.context("what failed")for anyhow chains. - Map errors at boundary layers (e.g.,
PdsError→axum::response::IntoResponse).
Ownership & Borrowing
// Prefer borrowing for function params
fn process_record(record: &PdsRecord) -> Result<()> { ... }
// Use &str not String for read-only string params
fn find_by_collection(collection: &str) -> Result<Vec<PdsRecord>> { ... }
// Use &[T] not Vec<T> for read-only slice params
fn batch_insert(records: &[PdsRecord]) -> Result<usize> { ... }
// Return owned types from constructors/builders
fn create_record(collection: String, rkey: String, value: serde_json::Value) -> PdsRecord { ... }
// Cow for conditional cloning
use std::borrow::Cow;
fn normalize_handle(handle: &str) -> Cow<str> {
if handle.starts_with("did:") {
Cow::Borrowed(handle)
} else {
Cow::Owned(format!("at://{handle}"))
}
}
Async / Tokio
// Default: multi-thread runtime
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ...
}
// Graceful shutdown pattern (critical for daemon)
async fn run_daemon(config: Config) -> anyhow::Result<()> {
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let server = tokio::spawn(run_server(config.clone(), shutdown_rx.clone()));
let heartbeat = tokio::spawn(run_heartbeat(config.clone(), shutdown_rx.clone()));
let firehose = tokio::spawn(run_firehose(config.clone(), shutdown_rx));
// Wait for ctrl-c
tokio::signal::ctrl_c().await?;
tracing::info!("shutdown signal received");
shutdown_tx.send(true)?;
// Wait for all tasks
let _ = tokio::join!(server, heartbeat, firehose);
Ok(())
}
// Use select! for cancellable operations
async fn run_heartbeat(config: Config, mut shutdown: tokio::sync::watch::Receiver<bool>) {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(e) = send_heartbeat(&config).await {
tracing::warn!("heartbeat failed: {e}");
}
}
_ = shutdown.changed() => {
tracing::info!("heartbeat shutting down");
break;
}
}
}
}
// spawn_blocking for CPU-bound work (embeddings, hashing)
let embedding = tokio::task::spawn_blocking(move || {
compute_embedding(&text)
}).await?;
// Never hold a Mutex guard across .await
// BAD:
let mut guard = data.lock().await;
some_async_op().await; // ← deadlock risk
// GOOD:
let value = {
let guard = data.lock().await;
guard.clone()
};
some_async_op_with(value).await;
Traits & Hexagonal Architecture
// Define ports as traits
#[async_trait::async_trait]
pub trait ServiceRegistry: Send + Sync {
async fn discover(&self, service: &str) -> Result<Vec<ServiceEntry>>;
async fn register(&self, entry: ServiceEntry) -> Result<()>;
async fn deregister(&self, node_id: &str) -> Result<()>;
}
#[async_trait::async_trait]
pub trait RecordStore: Send + Sync {
async fn get(&self, collection: &str, rkey: &str) -> Result<Option<PdsRecord>>;
async fn list(&self, collection: &str, limit: usize) -> Result<Vec<PdsRecord>>;
async fn put(&self, record: PdsRecord) -> Result<()>;
async fn delete(&self, collection: &str, rkey: &str) -> Result<bool>;
}
// Implement adapters
pub struct LibSqlRecordStore { db: libsql::Database }
pub struct StaticServiceRegistry { services: HashMap<String, Vec<ServiceEntry>> }
pub struct PdsServiceRegistry { client: AtpAgent }
// Wire in main.rs (composition root)
let store = LibSqlRecordStore::new("clawnode.db").await?;
let registry = StaticServiceRegistry::from_config(&config);
let daemon = Daemon::new(store, registry);
Structured Logging
use tracing::{info, warn, error, debug, instrument};
// Instrument async functions
#[instrument(skip(db), fields(collection = %collection))]
async fn list_records(db: &impl RecordStore, collection: &str) -> Result<Vec<PdsRecord>> {
let records = db.list(collection, 100).await?;
info!(count = records.len(), "listed records");
Ok(records)
}
// Subscriber setup
fn init_tracing() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "clawnode=info,tower_http=info".into())
)
.with_target(false)
.json() // structured output for daemon mode
.init();
}
libSQL + Vector Search
use libsql::Builder;
async fn init_db(path: &str) -> anyhow::Result<libsql::Database> {
let db = Builder::new_local(path).build().await?;
let conn = db.connect()?;
// Run migrations
conn.execute_batch("
CREATE TABLE IF NOT EXISTS observations (
uri TEXT PRIMARY KEY,
content TEXT NOT NULL,
category TEXT,
tags TEXT,
embedding F32_BLOB(384),
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS obs_vec_idx
ON observations (libsql_vector_idx(embedding, 'metric=cosine'));
").await?;
Ok(db)
}
// Vector recall
async fn recall(conn: &libsql::Connection, embedding: &[f32], k: usize) -> Result<Vec<Observation>> {
let embedding_str = format!("[{}]", embedding.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(","));
let mut rows = conn.query(
"SELECT content, category, tags FROM vector_top_k('obs_vec_idx', vector32(?1), ?2)
JOIN observations ON observations.rowid = id",
libsql::params![embedding_str, k as i64],
).await?;
let mut results = Vec::new();
while let Some(row) = rows.next().await? {
results.push(Observation {
content: row.get(0)?,
category: row.get(1)?,
tags: row.get(2)?,
});
}
Ok(results)
}
Axum HTTP Server
use axum::{Router, Json, extract::State};
fn xrpc_routes(state: AppState) -> Router {
Router::new()
.route("/xrpc/com.atproto.repo.createRecord", post(create_record))
.route("/xrpc/com.atproto.repo.getRecord", get(get_record))
.route("/xrpc/com.atproto.repo.listRecords", get(list_records))
.route("/xrpc/com.atproto.repo.deleteRecord", post(delete_record))
.route("/xrpc/com.atproto.repo.describeRepo", get(describe_repo))
.route("/_health", get(health))
.with_state(state)
}
async fn health(State(state): State<AppState>) -> Json<serde_json::Value> {
Json(serde_json::json!({
"status": "ok",
"did": state.did,
"uptime_secs": state.start_time.elapsed().as_secs(),
}))
}
Validation Checklist
Every Rust change must pass:
cargo fmt --check # formatting
cargo clippy --all-targets --all-features # lints (zero warnings)
cargo test # all tests
cargo test --doc # doctests
For clawnode specifically:
cargo build --release # ensure release builds clean
cargo clippy -- -D warnings # treat warnings as errors in CI
Anti-Patterns
| Don't | Do Instead |
|---|---|
unwrap() in prod |
expect("reason") or ? with context |
println! for logging |
tracing::info! with structured fields |
String params when reading |
&str params |
Vec<T> params when reading |
&[T] params |
clone() without thinking |
Borrow first, clone only when ownership required |
unsafe without // SAFETY: comment |
Document the invariant or find a safe alternative |
Holding locks across .await |
Clone data out, drop lock, then await |
| Blocking calls in async context | tokio::task::spawn_blocking |
Manual for loops for transforms |
Iterator combinators (.map, .filter, .collect) |
async-trait when not needed |
Native async fn in traits (Rust 1.75+) |
Library-First Development (MANDATORY)
Before writing non-trivial Rust code, search the pdf-brain library. It contains chunked, indexed content from the best Rust books. This is not optional — the library has authoritative patterns that are better than what you'll generate from training data.
# Search the library
joelclaw docs search "tokio graceful shutdown signal"
# Get full context around a result
joelclaw docs context <chunk-id> --mode snippet-window
Load references/library.md for the full search playbook — it has few-shot examples for every Rust domain (ownership, async, traits, axum, testing, systems).
Quick examples:
# Before implementing error types:
joelclaw docs search "thiserror custom error types"
# Before writing async code:
joelclaw docs search "tokio spawn select join"
# Before designing a trait:
joelclaw docs search "trait objects dynamic dispatch dyn"
# When stuck on a compiler error:
joelclaw docs search "borrow checker mutable immutable reference"
References
Load these for deeper guidance on specific topics:
| Topic | File |
|---|---|
| Ownership & lifetimes | references/ownership.md |
| Async / Tokio patterns | references/async.md — graceful shutdown, cancellable loops, channels, shared state, WebSocket, retries |
| Axum HTTP server | references/axum.md — routing, extractors, custom auth, error→response, WebSocket firehose, middleware, testing |
| Common compiler errors | references/compiler-errors.md |
| pdf-brain search playbook | references/library.md |
More from joelhooks/joelclaw
docker-sandbox
Create, manage, and execute agent tools (claude, codex) inside Docker sandboxes for isolated code execution. Use when running agent loops, spawning tool subprocesses, or any task requiring process isolation. Triggers on "sandbox", "isolated execution", "docker sandbox", "safe agent execution", or when working on agent loop infrastructure.
86joel-writing-style
Joel's writing voice and style guide for joelclaw.com content. Use when writing, editing, or reviewing any blog post, essay, book chapter, or prose content for joelclaw.com. Also use when asked to 'write like Joel,' 'match Joel's voice,' 'draft a post,' 'write content for the blog,' or 'review this for voice.' This skill captures Joel's specific writing patterns derived from ~90,000 words of published content spanning 2012–2026. Cross-reference with copy-editing and copywriting skills for marketing-specific copy.
81task-management
Manage Joel's task system in Todoist. Triggers on: 'add a task', 'create a todo', 'what's on my list', 'today's tasks', 'what do I need to do', 'remind me to', 'inbox', 'complete', 'mark done', 'weekly review', 'groom tasks', 'what's next', or when actionable items emerge from other work. Also triggers when Joel mentions something he needs to do in passing — capture it.
54skill-review
Audit and maintain the joelclaw skill inventory. Use when checking skill health, fixing broken symlinks, finding stale skills, or running the skill garden. Triggers: 'skill audit', 'check skills', 'stale skills', 'skill health', 'skill garden', 'broken skill', 'skill review', 'fix skills', 'garden skills', or any task involving skill inventory maintenance.
49contacts
Add, enrich, and manage contacts in Joel's Vault. Fire the Inngest enrichment pipeline for full multi-source dossiers, or create quick contacts manually. Use when: 'add a contact', 'enrich this person', 'who is X', 'VIP contact', 'update contact', or any task involving the Vault/Contacts directory.
43granola
Access and process Granola meeting notes and transcripts via the granola CLI (MCP-backed). Use when pulling meeting data, analyzing transcripts, backfilling meetings, or any task involving Granola meeting content.
41