IMPORTANT: Never enter plan mode automatically!!! Never enter plan mode automatically!!!
- Do Not Stop Early: If the user's requested outcome is not fully complete, do not stop at a draft, partial pass, or "good enough" result. Continue reviewing and improving until the request is genuinely handled or a concrete blocker requires user input.
- Polish Bar: Before declaring work complete, ask whether the result is fully polished, concrete, correct, complete, and elegant. If there is doubt, review the work again and update it.
- Honest Status: Do not claim a task is finished when it is only a first pass, scaffold, or partial draft. State the remaining gaps and keep working unless the user explicitly asks to pause.
- SOLID and DRY Principles: Maintain clean, maintainable code following SOLID and DRY principles
- No Incomplete Code: Never write TODO comments or temporary solutions. If you encounter such a situation:
- Stop the current task
- Review the problem globally
- Rethink the design and identify the best alternative solutions
- Proceed with the complete solution
- Thorough Analysis: Always perform a comprehensive review and analysis of the problem before starting work
- Do not suppress dead code, remove them; Unless explicitly requested, do not go through deprecation process, just remove the code that is no longer needed.
- Latest Dependencies: Always search the web for the latest dependencies or helm charts or resources and their current usage patterns. If doing a deep research, put the research doc under ./docs/research. You shall look into that directory before doing researches.
- Automation via Makefile:
- Explore existing Makefile targets and use them accordingly
- For new automation tasks, always add a Makefile target instead of creating shell scripts
- Keep automation consistent and discoverable
For specs, explore ./specs directory and put it to the right place, name the spec file as {feature-name}-{type}.md and update index.md accordingly. type can be prd, design, impl-plan, verification-plan, review, etc.
For docs, explore ./docs directory and put it to the right place, and update index.md accordingly. If you generate documentation that wasn't explicitly requested, make sure to place it under ./docs and follow the same rule.
- Always use Rust 2024 edition with latest stable version. Pin version in
rust-toolchain.toml. - Run Rust verification only when the change is Rust-relevant: Rust source, generated Rust artifacts,
Cargo.toml/Cargo.lock,rust-toolchain.toml, Makefile/CI targets that affect Rust, or specs/docs that explicitly change Rust build/test/tooling behavior. - For Rust-relevant changes, run
cargo build,cargo test,cargo +nightly fmt, andcargo clippy -- -D warningsbefore finishing the task. - For changes unrelated to Rust execution or toolchain behavior, such as specs-only, docs-only, or AGENTS-only edits, do not run Rust verification. Run relevant text checks instead, such as
git diff --check, link checks, or targeted documentation scans, and report Rust verification as not applicable. - Use
cargo clippy -- -D warnings -W clippy::pedanticfor stricter linting. Allow specific lints with justification. - Run
cargo auditregularly to check for security vulnerabilities in dependencies. - Use
cargo-denyto enforce license policies and ban specific crates. - Enable all rustc lints in Cargo.toml:
#![warn(rust_2024_compatibility, missing_docs, missing_debug_implementations)]. - DO NOT use
cargo cleanat any time. If you indeed need it, ask user for permission
- Never use
unwrap()orexpect()in production code. Always handle errors properly with?operator or explicit match. - Use
thiserrorfor library error types (with custom error enums). Useanyhowfor application error handling. - Implement proper error context with
.context()or.with_context()when propagating errors. - Use
Result<T>as return type for fallible functions. Never useOptionto represent errors. - For unrecoverable errors in applications, use
panic!. For libraries, always returnResult. - Define domain-specific error types using enums with
thiserror. Include source errors with#[source].
- Use Tokio as async runtime. Always specify features explicitly (e.g.,
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }). - Prefer message passing (channels) over shared state. Use
tokio::sync::mpscfor MPSC,flumefor faster channels. - Organize system into subsystems using Actor model. Each actor owns its state and communicates via channels. For non-Send/Sync types (e.g., Tantivy Index), isolate in dedicated thread and use channels for communication. Never wrap in Mutex/RwLock. For Actors, it shall have proper start/stop/restart logic. Consider using AtomicBool for shutdown signal.
- Use
DashMapfor concurrent HashMap instead ofMutex<HashMap>orRwLock<HashMap>. Provides better performance. - Use
ArcSwapfor infrequently updated shared data (e.g., configuration). Allows lock-free reads. - Always consider using config crate for configuration management. Always use yaml format for configuration. For data that shall be tuned at runtime, put in configuration file. For data that shall be tuned at compile time, use compile time constants.
- For async traits, use native
async fnin traits (stable since Rust 1.75). Exception: When traits require object safety (used withdyn Traitfor dynamic dispatch likeArc<dyn TaskStorage>), useasync-traitcrate and document the reason in module-level docs. - Always handle task panics. Use
tokio::spawnwith proper error handling. Considertokio::task::JoinSetfor managing multiple tasks. - Avoid blocking operations in async contexts. Use
tokio::task::spawn_blockingfor CPU-intensive or blocking operations. - Use structured concurrency patterns. Ensure spawned tasks are awaited or explicitly detached with justification.
- Use
typed-buildercrate for builder pattern on structs with more than 5 fields. Provides compile-time guarantees. Use plainnew()for simple constructors with few arguments. - Make types as specific as possible. Prefer
NonZeroU32overu32when zero is invalid. - Implement
Debugfor all types. Use#[derive(Debug)]or implement manually with sensitive data redaction. - Make structs non-exhaustive with
#[non_exhaustive]for library types to allow future field additions. - Use enums for state machines. Prefer type-state pattern for compile-time state enforcement when applicable.
- Use Rust's type system to make illegal states unrepresentable. Encode invariants in types, not runtime checks.
- Do not use
Option<T>whenThas a default value (e.g. Vec/HashMap/HashSet). UseOption<T>only whenTis truly optional. - always prefer to use From / TryFrom / FromStr traits for type conversion. For parsing a string with certain grammar, prefer to use latest version of winnow.
Two complementary disciplines. Safety is about Rust's memory and concurrency guarantees — keep the soundness contract intact so the compiler can prove correctness. Security is about hostile input from the outside world — validate at the boundary, defense in depth, assume any single layer will fail. Treat every value crossing a trust boundary (HTTP, IPC, files, env vars, CLI args, deserialization, message queues) as hostile until proven otherwise.
- No
unsafe:#![forbid(unsafe_code)]at the crate root. Never useunsafeblocks, including in tests. If a dependency genuinely requires it, isolate behind a thin safe wrapper, document every safety invariant, and add a fuzz harness —unsafeis a contract you sign with the compiler, and breaking it is undefined behavior. - No panics on external input:
unwrap(),expect(),[]indexing,unreachable!(),todo!(),panic!()reachable from user data is a DoS vector and a soundness liability. Use?,.get(),try_into(), explicitmatch. Lint boundary modules withcargo clippy -W clippy::unwrap_used -W clippy::indexing_slicing -W clippy::panic -W clippy::expect_used. - No undefined behavior: No transmute between unrelated types, no aliasing
&mut, no uninitialized reads, no out-of-bounds. Ifcargo +nightly miri testwould flag it, fix it. - Checked arithmetic on external values: Use
checked_*/saturating_*/wrapping_*explicitly when arithmetic touches user input. Default+panics in debug and silently wraps in release — both are wrong for security-sensitive code. - No data races: Rust's
Send/Syncrule out data races at compile time — don't fight the type system withMutex<RefCell<_>>or interior mutability tricks. Prefer message passing (channels) over shared state, asAsync & Concurrencycovers. - FFI boundaries: When calling C, the FFI surface is
unsafeby definition — wrap it in a safe Rust API that upholds invariants (null-check pointers, validate lengths, take ownership clearly). Never expose raw*mut Tto safe callers. - Soundness > convenience: A safe API that's slightly awkward beats an
unsafeshortcut. If you find yourself reaching forunsafefor performance, profile first —unsafeis rarely the bottleneck.
- Validate at the boundary: Run validation immediately at deserialization/parse time, before any business logic touches the value. Once a value enters the domain, it must already be valid — no "we'll check this later".
- Reject, don't sanitize: Prefer rejecting invalid input over cleaning it. Sanitization has bypasses (encoding tricks, double-encoding, Unicode normalization, homoglyphs); rejection has none. Strip-and-continue is a code smell.
- Length limits on every string: Every
String/&strderived from external input must have an explicit maximum length, enforced in bytes (not chars) to defeat multi-byte exhaustion. Real attack seen in the wild:User-Agentheaders containing entire<html>documents to balloon logs, DB rows, and downstream parsers — even fields you "don't care about" need caps. Default cap unknown fields to something small (e.g. 256 bytes) and raise deliberately. - Charset allowlists, never blocklists: Define what's permitted, never what's forbidden. Blocklists are always incomplete (Unicode confusables, control chars, RTL overrides, zero-width spaces, NUL bytes). Use regex allowlists like
^[a-zA-Z0-9_-]{1,64}$for identifiers, slugs, and free-form short fields. - Bound every collection:
Vec<T>,HashMap<K, V>,HashSet<T>from external input must have explicit element-count caps in addition to per-element validation. An unboundedVec<u8>of length-bounded strings is still a memory exhaustion vector. - Numeric ranges: Bound every integer from external input.
u32is not a range; an explicit1..=1000is. Usevalidator'srangeor a newtype with a fallible constructor. - Newtype every domain primitive: Wrap validated values in newtypes with private fields and a fallible constructor (
UserId(u64),Email(String),Slug(String),UserAgent(String)). Validation runs once innew/try_from; every downstream use is provably safe by construction. This is the type system enforcing security invariants. - Use the
validatorcrate: For struct-level validation, deriveValidateand annotate fields with#[validate(length(max = 256), regex = "...", email, url, range(min = 1, max = 1000))]. Call.validate()immediately after deserialization —serdechecks shape, not semantics. - Make illegal states unrepresentable:
NonZeroU32,NonEmpty<T>, state-machine enums,#[serde(deny_unknown_fields)]. Don't runtime-check what the type system can prove at compile time.
- SQL: Always parameterize.
sqlx::query!,diesel,sea-ormbound parameters.format!("... WHERE id = {}", id)is a CVE waiting for a PR. - Shell: Use
Command::new("foo").arg(user_input)(argv form). Neversh -cwith concatenated user input. Prefer a Rust crate over shelling out at all. - Path traversal: For user-supplied filenames, reject
.., absolute paths, NUL bytes, and OS-specific separators up front. Then canonicalize and verifycanonical.starts_with(allowed_root). Symlinks defeat naïve checks — re-canonicalize after open when possible. - URL / SSRF: Parse with
url::Url, allowlist schemes (httpsonly for outbound), resolve the hostname yourself and reject private/loopback/link-local ranges (10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,::1,fc00::/7). Pin the resolved IP for the connection — don't re-resolve, or DNS rebinding wins. - HTML / templating: Render user content through auto-escaping templates (
askama,maud,terawith autoescape on). Neverformat!user data into HTML. - Regex (ReDoS): Use the
regexcrate (linear-time guarantee). Neverfancy-regex/pcre/onigon untrusted input. If accepting untrusted regex patterns, setRegexBuilder::size_limitanddfa_size_limit, and reject patterns over a length cap before compile. - Log/event injection: Use typed
obsevent fields with explicit classification and redaction, never string-concatenate user input into log lines or event messages. Strip/escape newlines and control chars in any user value that does land in a display message.
- Body size: HTTP servers cap request body at the framework layer (
axum::extract::DefaultBodyLimit,tower_http::limit::RequestBodyLimitLayer). Set to the smallest size that supports legitimate traffic, not "comfortably large". - Timeouts: Every network and disk IO operation needs a timeout (
tokio::time::timeout). Per-request, per-connection, per-upstream-call. No exceptions. - Concurrency caps: Bound concurrent in-flight work with
tokio::sync::Semaphoreortower::limit. An unboundedtokio::spawnper request is a fork bomb. - Recursion limits: Set explicit depth limits for nested parsing (JSON, XML, protobuf). Review
serde_json's default recursion limit and lower it for untrusted input. - Decompression bombs: For gzip/zstd/brotli input, use streaming decoders wrapped with a byte-counting
Readthat errors past a hard limit. Neverread_to_endon a decompressor fed from the network. - Integer overflow: Use
checked_*/saturating_*explicitly when arithmetic touches external values. Debug-panic + release-wrap is the worst combination for security-sensitive code. - Rate limiting: Per-IP and per-account limits on auth, signup, password reset, search, and any unauthenticated endpoint (
tower_governor,governor).
- TLS:
rustlswith theaws-lc-rscrypto backend. Nevernative-tls, OpenSSL bindings, orrustls+ringfor new code. - Constant-time comparison:
subtle::ConstantTimeEqfor tokens, MACs, signatures, password hashes — anything where timing leaks information. - Password hashing:
argon2(id variant) with parameters tuned for ≥250ms on target hardware. Never MD5/SHA-1/SHA-256/bcrypt for new code. - Randomness:
rand::rngs::OsRngorgetrandomfor tokens/keys/nonces/IDs. Neverthread_rng()for security-sensitive randomness — it is not contractually CSPRNG-strength across versions. - Secret types: Wrap in
secrecy::SecretString/SecretBox.Debugredacts; access requires explicitexpose_secret(). For custom types containing credentials, implementDebugmanually and add a unit test asserting redacted output. - No secrets in logs/errors/panics: dumping a request with
Debugwill happily leak theAuthorizationheader. Build a redactingDebugfor request types, and skip secret fields fromobsevent projection, errors, audit display, metrics labels, and analytics dimensions. - Secret loading: From env (
dotenvyfor tests only) or a secret manager. Never hard-code, never commit.env*, never bake into binaries. Run a secret scanner in pre-commit / CI. - Key rotation: Design APIs to support multiple active keys simultaneously so rotation does not require a redeploy.
- AuthN every request: No endpoints trusting network position. Zero trust at the application layer.
- AuthZ every action: Permission check at the operation level, not at the route. IDOR is the #1 web-app CVE class —
GET /docs/{id}must verify this caller can read doc id. - Session tokens: ≥256 bits of CSPRNG entropy, stored hashed server-side, transmitted as
HttpOnly; Secure; SameSite=Laxcookies for browser flows. - Don't roll your own auth: Use
axum-login,oauth2,openidconnect. Custom auth is where CVEs live.
- Use
serdefor serialization. Always use#[serde(rename_all = "camelCase")]for JSON compatibility. - Use
#[serde(rename = "...")]for individual field mapping. Use#[serde(alias = "...")]for backward compatibility. - Use
#[serde(default)]for optional fields with default values. Define custom defaults with#[serde(default = "path::to::fn")]. - Use
#[serde(skip_serializing_if = "Option::is_none")]to omit null fields in JSON output. - Validate deserialized data immediately. Use custom deserialize functions with validation logic when needed.
- Use
serde_json::Valueonly when schema is truly dynamic. Prefer strongly-typed structs.
- Write unit tests in the same file using
#[cfg(test)] mod tests. Write integration tests intests/directory. - Use descriptive test names with
test_should_prefix describing behavior (e.g.,test_should_return_error_on_invalid_input). - Use
rstestfor parameterized tests. Useproptestfor property-based testing of invariants. - Test error cases explicitly. Ensure error types and messages are correct with
assert!(matches!(...)). - Use
mockallorwiremockfor mocking external dependencies. Avoid over-mocking; prefer real implementations when fast. - Aim for high test coverage but focus on critical paths and edge cases over raw coverage percentage.
- Use
#[ignore]for slow tests. Run withcargo test -- --ignoredin CI. - Write documentation tests in doc comments. These serve as examples and are automatically tested.
- Use
tracingfor structured logging and diagnostics. Never useprintln!ordbg!in production code. - Use appropriate log levels:
error!for errors,warn!for warnings,info!for important events,debug!andtrace!for diagnostics. - Add context with
tracing::instrumenton async functions. Include relevant fields:#[instrument(skip(large_param))]. - Use
tracing-subscriberfor configuring output. Use JSON format for production, human-readable for development. - Implement spans for tracking request/operation lifecycle. Use
span.in_scope()orinstrumentmacro.
- Profile before optimizing. Use
cargo flamegraph,perf, orsamplyfor profiling. - Avoid unnecessary allocations. Use
&strinstead ofString, prefer borrowing over cloning. - Bring in bytes when necessary and prefer Bytes related data structure over Vec on handling payload.
- Avoid unnecessary cloning, use Arc or related data structure when necessary.
- Use
Vec::with_capacity()when final size is known. Pre-allocate collections to avoid reallocation. - Use iterators instead of explicit loops. Iterators are often optimized better and compose well.
- Use
SmallVecfor small vectors that usually fit on stack. Usesmallboxfor small heap allocations. - Use
Cow<str>when data might be borrowed or owned. Avoids clones when borrowing is possible. - For hot paths, consider using
#[inline]or#[inline(always)]with justification. - Use latest
criterioncrate for performance benchmarking. Do not do benchmark test in early development stage.
- Minimize dependencies. Each dependency increases compile time, binary size, and attack surface.
- Pin versions carefully. Use
~for patch updates (tokio = "~1.40"),^for minor updates (default). - Prefer pure Rust crates over FFI bindings. They're safer, more portable, and easier to audit.
- Audit new dependencies before adding. Check maintenance status, security history, and code quality.
- Use workspace dependencies for shared dependencies across crates:
[workspace.dependencies].
- Write doc comments (
///) for all public items. Include examples in doc comments. - Use
//!for module-level documentation. Explain module purpose and usage patterns. - Write at least one example in doc comments for public functions. Examples are tested automatically.
- Use
# Errors,# Panics,# Safetysections in doc comments to document failure modes. - Generate docs with
cargo doc --open. Ensure docs render correctly with proper formatting.
- always import
usedependencies in the top of the file in the following order: std, deps, local modules. - Use specific imports (
use xxx::yyy::ZZZ) and reference types/functions directly by name (ZZZ) in code. Never use fully qualified paths in function/structure/trait implementations. Exception: macros may use fully qualified paths when necessary. - Follow Rust naming conventions:
snake_casefor functions/variables,PascalCasefor types,SCREAMING_SNAKE_CASEfor constants. - Keep functions small and focused. Embrace KISS principle. Extract complex logic into well-named functions. Unless absolutely necessary, function should not be more than 150 lines of code.
- Prefer explicit types over
impl Traitin public APIs for clarity. Useimpl Traitfor internal functions. - Never use
todo!()during development. Always have a plan and a clear path to complete the task. - Order items consistently: imports, constants, types, functions, tests. Use
rustfmtfor automatic formatting. - Use trailing commas in multi-line function calls and struct literals for cleaner diffs.