Install
$ agentstack add skill-anantbhandarkar-make-it-right-mir-backend-rust ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/mir-backend-rust · Make It Right (Rust runtime)
The middle tier. mir-backend decides what is correct (any language). The framework module (e.g. mir-backend-rust-axum) knows the library's mechanics. This tier owns what is true for all Rust async backends because they run on Tokio — the concurrency model and ownership rules that Axum, Actix-web, Warp, and Poem all inherit.
Runtime assumed: async Rust on Tokio (current-thread or multi-thread scheduler). The notes hold for any Tokio-based framework. Load order: mir-backend → mir-backend-rust → ``.
The Tokio async Rust footguns AI walks into (framework-agnostic)
1. DON'T BLOCK THE ASYNC RUNTIME
Tokio runs async tasks on a fixed pool of worker threads (by default, one per CPU core). A blocking call on any of those threads stalls every task scheduled to that thread — the async "event loop" analog.
Blocking operations that must never appear inside an async fn on a Tokio thread:
std::thread::sleep→ usetokio::time::sleep- Synchronous file I/O (
std::fs::read,std::fs::write) → usetokio::fsorspawn_blocking - Blocking DB or socket calls (any sync driver, e.g.
postgrescrate's sync API) → use async driver (sqlx,tokio-postgres) orspawn_blocking - Heavy CPU computation (hashing, encoding, ML inference, large sort) — blocks a Tokio thread even though it's "not I/O" → use
tokio::task::spawn_blocking(runs on a separate blocking thread pool) or a dedicatedrayonthreadpool
// WRONG — blocks a Tokio worker thread
async fn handler() -> &'static str {
std::thread::sleep(std::time::Duration::from_secs(1)); // starves executor
"done"
}
// RIGHT — yields to the runtime
async fn handler() -> &'static str {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
"done"
}
// RIGHT — offload blocking I/O / CPU
async fn hash_password(pw: String) -> String {
tokio::task::spawn_blocking(move || bcrypt::hash(pw, 12).unwrap())
.await
.unwrap()
}
AI routinely copies sync stdlib code into async handlers without noticing the runtime impact. Flag every std::thread::sleep, std::fs, and sync driver call found inside async fn.
2. Holding a guard across .await — compile error or deadlock
std::sync::MutexGuard is !Send. Holding it across an .await makes the enclosing future !Send, which means it cannot be spawned on a multi-threaded Tokio runtime (tokio::spawn requires Send). The compiler catches this — but AI often "fixes" it by switching to Arc> everywhere, which is overkill.
The right rule:
- Drop the guard before awaiting. Scope the lock into a block; do the await after the block closes.
- Use
tokio::sync::Mutexonly when the guard genuinely needs to be held across an.await(e.g. you're performing an async operation atomically while holding the lock). It is slower thanstd::sync::Mutex— default tostdand scope tightly. std::sync::RwLockhas the same!Sendguard constraint.
// WRONG — guard held across .await, won't compile on multi-thread runtime
async fn increment(state: Arc>) {
let mut guard = state.lock().unwrap();
some_async_work().await; // compiler error: future is !Send
*guard += 1;
}
// RIGHT — release before awaiting
async fn increment(state: Arc>) {
some_async_work().await; // await first, or...
let mut guard = state.lock().unwrap();
*guard += 1;
// guard drops here, before any subsequent await
}
// ALSO RIGHT — scope the lock to prevent guard crossing .await
async fn read_then_work(state: Arc>) {
let snapshot = {
let guard = state.lock().unwrap();
guard.clone() // copy out what you need
}; // guard dropped here
do_async_work(snapshot).await;
}
3. Cancellation safety — futures are dropped at any .await
A Tokio future can be dropped (cancelled) at any .await point by:
tokio::time::timeoutexpiring- A
tokio::select!branch losing the race - A client disconnecting mid-request
- Task being aborted via
JoinHandle::abort
This means "mutate A; .await; mutate B" is NOT atomic — if the future is dropped after mutating A but before mutating B, you get partial state.
// DANGEROUS — partial mutation if dropped at the .await
async fn transfer(db: &Db, from: u64, to: u64, amount: i64) {
db.debit(from, amount).await; // dropped here? 'from' debited, 'to' never credited
db.credit(to, amount).await;
}
// RIGHT — wrap in a DB transaction so partial mutations roll back
async fn transfer(db: &Db, from: u64, to: u64, amount: i64) {
db.transaction(|tx| async move {
tx.debit(from, amount).await?;
tx.credit(to, amount).await?;
Ok(())
}).await.unwrap();
}
For select!: the branches that lose are dropped immediately — any state they were accumulating is lost. If a branch does I/O you care about, use cancellation-safe primitives (recv on a channel is cancellation-safe; read on an AsyncRead may not be). Consult the Tokio docs' cancellation-safety table for each primitive.
tokio::select! also drops the other branch futures on every iteration — this is not a bug, but AI often writes stateful code inside select branches assuming they survive across iterations.
4. Panics, poisoned Mutexes, and task boundaries
- A panic inside a
tokio::spawn'd task unwinds only that task. TheJoinHandlereturnsErr(JoinError)— you must check it to know the task panicked. IgnoringJoinHandles means silent failures. - A panic while holding a
std::sync::Mutexpoisons the mutex. Every subsequentlock()returnsErr(PoisonError). Either call.unwrap_or_else(|e| e.into_inner())(accepting the possibly-corrupt state) or treat poison as a fatal error. - Decide on a panic strategy at task boundaries: either catch panics via
JoinHandle, usecatch_unwind, or treat a task panic as a process-fatal error and let a supervisor restart. Don't silently swallowJoinHandleresults.
let handle = tokio::spawn(async move { risky_work().await });
match handle.await {
Ok(result) => process(result),
Err(e) if e.is_panic() => tracing::error!("task panicked: {:?}", e),
Err(e) => tracing::error!("task cancelled: {:?}", e),
}
5. Shared state — Arc and 'static bounds on spawned tasks
tokio::spawn requires the future to be 'static — it cannot borrow from the caller's stack. AI frequently tries to pass &self or local references into a spawn'd closure and hits a lifetime error, then "fixes" it with unsafe or unnecessary cloning.
The correct pattern:
- Share data via
Arc(cheap clone, reference-counted heap allocation). - Mutable shared data via
Arc>orArc>. - Move the
Arcclone into the task — not a reference to it.
let state = Arc::new(Mutex::new(vec![]));
let state_clone = Arc::clone(&state);
tokio::spawn(async move {
state_clone.lock().unwrap().push(42); // owns the clone, 'static ok
});
For read-heavy workloads, prefer Arc> or Arc> (drop before awaiting) over a Mutex to allow concurrent readers.
6. Bounded channels for backpressure — unbounded channels can OOM
tokio::sync::mpsc::unbounded_channel never exerts backpressure — a slow consumer and fast producer will buffer messages in memory until the process OOM-kills. Default to bounded channels (tokio::sync::mpsc::channel(capacity)) so that the sender blocks (awaits) when the buffer is full, propagating backpressure upstream.
// RISKY — unbounded, can grow without limit
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::();
// RIGHT — bounded; sender .await's when full, applies backpressure
let (tx, rx) = tokio::sync::mpsc::channel::(256);
Choose capacity based on acceptable latency buffering and memory budget. A capacity of 1 maximizes backpressure responsiveness; very large capacities approach unbounded behavior.
7. Timeouts on every outbound call — connection pool sizing
Every call that crosses a process boundary (HTTP, DB, Redis, gRPC, DNS) must have a timeout. Without one, a slow dependency holds a Tokio task (and possibly a connection from the pool) open indefinitely, leading to exhausted pools and cascading failures.
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(5),
http_client.get(url).send()
).await
.map_err(|_| AppError::Timeout)? // timeout elapsed
??; // reqwest error
Connection pool sizing (deadpool, sqlx::Pool, bb8): max_connections should reflect the actual DB/service concurrency limit, not be set to a huge number "just to be safe." Too many pool connections starve the DB. Too few starve the app under load. Set connection_timeout on pool acquisition — a task that can't get a connection should fail fast, not wait forever.
How this slots into the pipeline
- Gate 0/5 (model choice): Confirm the async runtime is Tokio. State the concurrency model (async I/O,
spawn_blockingfor CPU/blocking, rayon for parallel CPU). A blocking call on a Tokio thread is a runtime-level defect — flag it before any framework-specific review. - Gate 6 (implementation): no blocking calls inside
async fn; guards dropped before awaits;Arcfor shared state; bounded channels; timeout on every outbound call. - Gate 7 (review): the reliability-reviewer checks items 1–7 here for any Tokio-based Rust service; framework mechanics are in the framework module.
Edit boundary (what belongs here vs. above/below)
- Generic, all-language rules (idempotency, invariants, gates, observability) → up to
mir-backend. - A specific library's mechanics (Axum extractors/state, Actix workers/
web::Data, tower middleware) → down to the framework module (mir-backend-rust-). - Here: only what every async Rust backend shares because of Tokio — blocking the runtime, guard-across-await, cancellation safety, panic/poison,
Arc+'static, bounded channels, timeout discipline. - A different runtime (Node, Go, Python…) → its own
mir-backend-tier. Never widen this one.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: anantbhandarkar
- Source: anantbhandarkar/make-it-right
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.