Thank you for your interest in contributing to LibreFang! "Libre" means freedom, and we mean it — this project is built by its community.
Our promise: if your contribution positively helps the project, we merge it as-is. If it needs improvement, we provide active, constructive review to help you get it merged. Every contributor matters.
Active contributors are invited to join the LibreFang GitHub org — core participants who consistently contribute get commit access and a voice in project direction.
This guide covers everything you need to get started, from setting up your development environment to submitting pull requests.
- Ways to Contribute
- Contributing to the Registry
- Development Environment
- Building and Testing
- Code Style
- Dependency Policy
- Architecture Overview
- How to Add a New Agent Template
- How to Add a New Skill
- How to Add a New Channel Adapter
- How to Add a New LLM Provider
- How to Add a New Tool
- How to Write Integration Tests
- Release Articles
- Pull Request Process
- Code of Conduct
You don't need to know Rust to contribute to LibreFang. We have contribution paths for every skill level:
| What | Skills Needed | Time | Where |
|---|---|---|---|
| Write an agent template | TOML + prompt engineering | 1-2 hours | agents/ |
| Write a skill (Python) | Python | 2-4 hours | ~/.librefang/skills/ |
| Write a skill (JavaScript) | Node.js | 2-4 hours | ~/.librefang/skills/ |
| Fix typos / improve docs | Markdown | 30 min | docs/ |
| Translate docs | Markdown + language | 1-2 hours | docs/i18n/ |
| Report bugs with reproduction steps | Testing | 30 min | GitHub Issues |
| Test on uncommon platforms | Testing | 1 hour | GitHub Issues |
| What | Skills Needed | Time | Where |
|---|---|---|---|
| Add a channel adapter | Rust + platform API | half day | crates/librefang-channels/ |
| Add an LLM provider driver | Rust + provider API | half day | crates/librefang-runtime/ |
| Add a built-in tool | Rust | 2-4 hours | crates/librefang-runtime/ |
| Write/improve tests | Rust | 1-2 hours | any crate |
| What | Skills Needed | Time | Where |
|---|---|---|---|
| Kernel features | Deep Rust + architecture | 1+ days | crates/librefang-kernel/ |
| Security hardening | Rust + security | 1+ days | multiple crates |
| Performance optimization | Rust + profiling | varies | any crate |
| WASM sandbox improvements | Rust + Wasmtime | 1+ days | crates/librefang-runtime/ |
| What | Skills Needed | Time | Where |
|---|---|---|---|
| Desktop app features | Rust + Tauri + TypeScript | varies | crates/librefang-desktop/ |
| JavaScript SDK | TypeScript | varies | sdk/javascript/ |
| Python SDK | Python | varies | sdk/python/ |
| WhatsApp gateway | Node.js | varies | packages/whatsapp-gateway/ |
Tip: Look for issues labeled
good first issue— they include the files to modify, how to test, and estimated difficulty.
I want to add an agent template (no Rust):
cp -r agents/hello-world agents/my-agent
# Edit agents/my-agent/agent.toml
# Submit a PRI want to write a Python skill (no Rust):
mkdir -p ~/.librefang/skills/my-skill
# See https://docs.librefang.ai/agent/skills for the skill formatI want to fix a bug or add a Rust feature:
git clone https://github.com/librefang/librefang.git && cd librefang
cargo build --workspace # Build
cargo test --workspace # Test
cargo clippy --workspace --all-targets -- -D warnings # LintThe librefang-registry repo is the shared catalog the website browses (at librefang.ai/skills, /hands, etc.) and the CLI pulls from. Contributions are welcome without touching the main Rust codebase.
| Path | Format | What it is |
|---|---|---|
skills/<id>/SKILL.md |
directory | A prompt-only or WASM skill bundle (markdown + YAML frontmatter) |
hands/<id>/HAND.toml |
directory | An autonomous capability unit |
agents/<id>/agent.toml |
directory | A pre-built agent template |
channels/<id>.toml |
file | A messaging adapter manifest |
providers/<id>.toml |
file | An LLM provider adapter manifest |
workflows/<id>.toml |
file | A multi-step agent workflow |
plugins/<id>/plugin.toml |
directory | A runtime plugin manifest |
mcp/<id>.toml |
file | An MCP server manifest |
- Fork
librefang-registry. - Add your manifest to the right category directory. Follow the schema of an existing neighbour.
- Required TOML fields for every entry:
id,name,description,category,icon(one emoji). - Add i18n descriptions in
[i18n.zh],[i18n.ja],[i18n.ko]if you can — the website renders localized descriptions when available. - Tag with
tags = ["popular"]only if you've validated real usage; the site visually promotes popular entries. - Open a PR against the registry repo. On merge, the entry is live on librefang.ai within an hour (the Cloudflare Worker at
stats.librefang.airuns stale-while-revalidate with a 1-hour fresh window).
# Run the official site against your local registry checkout
cd librefang/web
pnpm dev
# Or install a single skill directly into a running daemon
librefang skill install /path/to/librefang-registry/skills/your-skillThe website's detail pages expect the TOML to be parseable and render the raw contents. No HTML or Markdown is interpreted — readers see the TOML as-is with syntax highlighting. Keep descriptions concise (≤ 280 chars) so they fit in meta tags and social-share cards.
Click the green "Code" button on GitHub → "Codespaces" → "Create codespace on main". The DevContainer will automatically install Rust, Python, Node.js, and build the project. You'll have a fully working environment in your browser within a few minutes.
- Rust 1.94.1+ (install via rustup)
- Git
- Python 3.8+ (optional, for Python runtime and skills)
- A supported LLM API key (Anthropic, OpenAI, Groq, etc.) for end-to-end testing
git clone https://github.com/librefang/librefang.git
cd librefang
just setup # one-time per clone — activates git hooks + fetches deps
cargo buildjust setup (which calls cargo xtask setup) does three things on a fresh clone:
- Sets
git config core.hooksPath scripts/hooksso the in-repopre-commit,pre-push, andcommit-msghooks become active. - Runs
cargo fetchto warm up the dependency cache. - Runs
pnpm installin the dashboard / web / docs sub-projects.
Hooks are scoped to fast, staged-only checks. CI is the authoritative gate (clippy, openapi/SDK drift, security audit, full test matrix).
| Hook | Runs | Target time |
|---|---|---|
pre-commit |
cargo fmt --check on staged *.rs only, CHANGELOG guard + (@user) attribution check, detect-secrets (if installed) |
< 2s |
pre-push |
Refuses direct push to main / master. Nothing else. |
< 100ms |
commit-msg |
Reject Claude / Anthropic attribution | < 50ms |
Want to lint locally before pushing? Run just lint (or
cargo clippy --workspace --all-targets -- -D warnings) on demand.
That belongs in your loop when you want it, not gating every push.
Skip the pre-push branch guard with
LIBREFANG_PREPUSH_SKIP=1 git push (or --no-verify) when the
maintainers have agreed to a release / hotfix push to main.
For secret scanning, install detect-secrets once (pipx install detect-secrets). False positives are managed via .secrets.baseline:
detect-secrets scan --baseline .secrets.baseline # update findings
detect-secrets audit .secrets.baseline # mark each as real / false-positiveIf you also use the pre-commit framework (pipx install pre-commit), the equivalent staged-only fmt + secret scan is wired in .pre-commit-config.yaml. The framework's pre-push stage is intentionally not wired — CI is the gate, no need to duplicate locally:
pre-commit install --install-hooks # commit stageYou only need to run it once per clone — git pull keeps the hooks current automatically because they live in scripts/hooks/ rather than being copied into .git/hooks/.
The first build takes a few minutes because it compiles SQLite (bundled) and Wasmtime. Subsequent builds are incremental.
For any non-trivial feature, work in a git worktree rather than the main clone. This avoids contending with other sessions on the shared target/ directory and prevents accidental edits on the wrong branch:
git worktree add /tmp/librefang-<feature> -b <branch-name> origin/main
cd /tmp/librefang-<feature>New worktrees inherit the main clone's core.hooksPath setting, so the git hooks are active there too without re-running just setup.
For running integration tests that hit a real LLM, set at least one provider key:
export GROQ_API_KEY=gsk_... # Recommended for fast, free-tier testing
export ANTHROPIC_API_KEY=sk-ant-... # For Anthropic-specific testsTests that require a real LLM key will skip gracefully if the env var is absent.
cargo build --workspacecargo test --workspaceThe test suite is currently 2,100+ tests. All must pass before merging.
cargo test -p librefang-kernel
cargo test -p librefang-runtime
cargo test -p librefang-memorycargo clippy --workspace --all-targets -- -D warningsThe CI pipeline enforces zero clippy warnings.
cargo fmt --allAlways run cargo fmt before committing. CI will reject unformatted code.
After building, verify your local setup:
cargo run -- doctorCI splits the full-workspace test run into 4 nextest shards that run in
parallel. Each shard runs cargo nextest run --workspace --partition hash:N/4, which deterministically buckets every test into exactly one shard
by the hash of its name. Adding or removing tests does not reshuffle existing
buckets, so cache hit rates stay stable across runs. The matrix is
fail-fast: false so a failure in one shard does not mask failures in the
others.
Sharding only kicks in for the full-run lane (push to main, or a PR
that touches the workspace Cargo.toml / Cargo.lock). The selective lane
(typical PR touching a few crates) keeps running on a single runner — shard
fan-out has no benefit when the affected crate set is small, and adds startup
overhead.
A weekly job (.github/workflows/build-timings.yml, Mondays 07:00 UTC) tracks
compile hotspots:
# Local: collect a per-crate compile-time snapshot for the current HEAD.
cargo xtask build-timings
# Writes bench-results/build-timings/<git-sha>.json by parsing
# target/cargo-timings/cargo-timing*.html (the embedded UNIT_DATA array).
# Local: compare the latest snapshot against bench-results/build-timings/baseline.json.
cargo xtask compare-build-timings
# Exits non-zero (annotated, not blocking) when any crate regressed by
# more than 10%.The baseline file is seeded by committing the first weekly run's snapshot. After that, weekly snapshots are uploaded as workflow artifacts (90-day retention) for trend tracking — they are not auto-committed back into the repo.
cargo xtask ci, cargo xtask pre-commit, and cargo xtask coverage
probe the host on startup and may auto-throttle cargo concurrency to avoid
OOM-ing low-spec laptops (refs #3301).
cargo xtask bench detects the mode and prints a loud warning when throttled
(benchmark numbers are unreliable at CARGO_BUILD_JOBS=1) but does not
apply the throttle — set LIBREFANG_LOCAL_CHECK_MODE=full before running
benchmarks to ensure comparable results.
cargo xtask dev, cargo xtask api-docs, and cargo xtask codegen do
not apply throttling: dev runs interactive hot-reload (not a
compile-heavy batch job), and api-docs/codegen generate artifacts from
already-compiled outputs rather than triggering expensive full builds.
Three modes, controlled by the LIBREFANG_LOCAL_CHECK_MODE environment
variable:
| Mode | When | Effect |
|---|---|---|
full |
CI=true env, or auto-detect on capable hosts |
No env tweaks (matches historical behaviour) |
throttled |
Auto-detect on mem < 16 GB or cpus < 4 |
CARGO_BUILD_JOBS=1, appends -C codegen-units=1 to RUSTFLAGS, sets RUST_MIN_STACK=8388608 |
off |
User explicit opt-out | No env tweaks (you know your machine; we won't touch anything) |
The mode is printed at the top of each affected subcommand:
xtask ci: local-check-mode = throttled (cpus=4, mem=8 GB)
Auto-detect heuristic:
- If
CIenv is set (GitHub Actions, GitLab CI, CircleCI, etc.), mode is forced tofull. - Else if total RAM < 16 GB or available CPU count < 4, mode is
throttled. - Otherwise, mode is
full.
Manual override:
LIBREFANG_LOCAL_CHECK_MODE=full cargo xtask ci # force full concurrency
LIBREFANG_LOCAL_CHECK_MODE=throttled cargo xtask ci # force throttled
LIBREFANG_LOCAL_CHECK_MODE=off cargo xtask ci # disable env tweaks entirelyExisting values for CARGO_BUILD_JOBS / RUST_MIN_STACK are preserved
(only set when unset); RUSTFLAGS is appended to, never replaced.
- Formatting: Use
rustfmtwith default settings. Runcargo fmt --allbefore every commit. - Linting:
cargo clippy --workspace -- -D warningsmust pass with zero warnings. - Documentation: All public types and functions must have doc comments (
///). - Error Handling: Use
thiserrorfor error types. Avoidunwrap()in library code; prefer?propagation. - Naming:
- Types:
PascalCase(e.g.,LibreFangKernel,AgentManifest) - Functions/methods:
snake_case - Constants:
SCREAMING_SNAKE_CASE - Crate names:
librefang-{name}(kebab-case)
- Types:
- Dependencies: Workspace dependencies are declared in the root
Cargo.toml. Prefer reusing workspace deps over adding new ones. If you need a new dependency, justify it in the PR. - Testing: Every new feature must include tests. Use
tempfile::TempDirfor filesystem isolation and random port binding for network tests. - Serde: All config structs use
#[serde(default)]for forward compatibility with partial TOML.
LibreFang ships as a single binary that runs other people's agents on your hardware, so every crate we pull in is part of the trust boundary. The rules below codify how we add, audit, and remove third-party code.
- Justify it in the PR description. Explain what it does, why a workspace crate or the standard library is not sufficient, and link to the upstream repo and license.
- License must be on the allow-list in
deny.toml. Anything outsideApache-2.0,Apache-2.0 WITH LLVM-exception,MIT,BSD-2-Clause,BSD-3-Clause,0BSD,ISC,MPL-2.0,Unicode-DFS-2016,Unicode-3.0,Zlib,CC0-1.0,CDLA-Permissive-2.0requires a maintainer-level decision and an explicit[[licenses.clarify]]orexceptionsentry with rationale. - No git dependencies by default.
unknown-git = "deny"is set indeny.toml. If you absolutely need an unreleased upstream change, add the repo URL toallow-gitwith an inline comment linking to the upstream PR / issue and a target version where it can be removed. - Reuse the workspace dependency table in the root
Cargo.toml([workspace.dependencies]) so version bumps stay coordinated across crates.
Every entry under [patch.crates-io] (in the root Cargo.toml) MUST carry a comment block recording:
- Why the patch exists — CVE ID, bug report, or unmerged feature.
- Upstream link — the issue or PR we are waiting on.
- Pinned version — the exact upstream tag / SHA we are tracking, so reviewers can diff against it.
- Removal trigger — the upstream version that will let us drop the patch (e.g. "remove once foo ≥ 1.4 is published").
A patch without an audit comment is rejected at review.
The cargo-deny workflow runs on every PR and main push that touches Cargo.toml, Cargo.lock, crates/**/Cargo.toml, xtask/Cargo.toml, or deny.toml. It executes two independent checks:
cargo deny check advisories— RustSec database, yanked crates, vulnerability hits.cargo deny check bans licenses sources— duplicate / wildcard / banned crates, license allow-list, registry / git source allow-list.
A RUSTSEC-* advisory that we cannot immediately fix may be temporarily ignored by adding it to [advisories].ignore in deny.toml, but only with:
- A comment naming the upstream issue / PR being tracked.
- An explicit reviewer sign-off in the PR that adds the ignore.
- A scheduled re-evaluation (typically removed within one release cycle).
cargo-deny.yml also runs on a schedule: cron so freshly published RustSec advisories against pinned dependencies are surfaced even when no Cargo.toml has changed. Treat a red scheduled run as a security ticket — open an issue and either bump the dep or document the ignore as above.
LibreFang is organized as a Cargo workspace with 14 crates:
| Crate | Role |
|---|---|
librefang-types |
Shared type definitions, taint tracking, manifest signing (Ed25519), model catalog, MCP/A2A config types |
librefang-memory |
SQLite-backed memory substrate with vector embeddings, usage tracking, canonical sessions, JSONL mirroring |
librefang-runtime |
Agent loop, 3 LLM drivers (Anthropic/Gemini/OpenAI-compat), 53 built-in tools, WASM sandbox, MCP client/server, A2A protocol |
librefang-hands |
Hands system (curated autonomous capability packages), 7 bundled hands |
librefang-extensions |
Integration registry (25 bundled MCP templates), AES-256-GCM credential vault, OAuth2 PKCE |
librefang-kernel |
Assembles all subsystems: workflow engine, RBAC auth, heartbeat monitor, cron scheduler, config hot-reload |
librefang-api |
REST/WS/SSE API (Axum 0.8), 76 endpoints, 14-page SPA dashboard, OpenAI-compatible /v1/chat/completions |
librefang-channels |
40 channel adapters (Telegram, Discord, Slack, WhatsApp, and 36 more), formatter, rate limiter |
librefang-wire |
OFP (LibreFang Protocol): TCP P2P networking with HMAC-SHA256 mutual authentication |
librefang-cli |
Clap CLI with daemon auto-detect (HTTP mode vs. in-process fallback), MCP server |
librefang-migrate |
Migration engine for importing from OpenClaw (and future frameworks) |
librefang-skills |
Skill system: 60 bundled skills, FangHub marketplace, OpenClaw compatibility, prompt injection scanning |
librefang-desktop |
Tauri 2.0 native desktop app (WebView + system tray + single-instance + notifications) |
xtask |
Build automation tasks |
KernelHandletrait: Defined inlibrefang-runtime, implemented onLibreFangKernelinlibrefang-kernel. This avoids circular crate dependencies while enabling inter-agent tools.- Shared memory: A fixed UUID (
AgentId(Uuid::from_bytes([0..0, 0x01]))) provides a cross-agent KV namespace. - Daemon detection: The CLI checks
~/.librefang/daemon.jsonand pings the health endpoint. If a daemon is running, commands use HTTP; otherwise, they boot an in-process kernel. - Capability-based security: Every agent operation is checked against the agent's granted capabilities before execution.
Agent templates live in the agents/ directory. Each template is a folder containing an agent.toml manifest.
- Create a new directory under
agents/:
agents/my-agent/agent.toml
- Write the manifest:
name = "my-agent"
version = "0.1.0"
description = "A brief description of what this agent does."
author = "librefang"
module = "builtin:chat"
tags = ["category"]
[model]
provider = "groq"
model = "llama-3.3-70b-versatile"
[resources]
max_llm_tokens_per_hour = 100000
[capabilities]
tools = ["file_read", "file_list", "web_fetch"]
memory_read = ["*"]
memory_write = ["self.*"]
agent_spawn = false- Include a system prompt if needed by adding it to the
[model]section:
[model]
provider = "anthropic"
model = "claude-sonnet-4-20250514"
system_prompt = """
You are a specialized agent that...
"""- Test by spawning:
librefang agent spawn agents/my-agent/agent.toml- Submit a PR with the new template.
Skills are reusable capabilities that agents can invoke. They can be written in Python, JavaScript, or as pure prompt templates — no Rust required.
| Type | Language | Description |
|---|---|---|
prompt |
None (TOML only) | A prompt template with variables |
python |
Python 3.8+ | A Python script with run() entry point |
javascript |
Node.js 18+ | A JS module with run() export |
- Create a skill directory:
my-skill/
skill.toml
main.py
- Write the manifest (
skill.toml):
name = "my-skill"
version = "0.1.0"
description = "What this skill does."
author = "your-name"
runtime = "python"
entry = "main.py"
tags = ["utility"]
[input]
url = { type = "string", description = "URL to process", required = true }- Write the implementation (
main.py):
def run(input: dict) -> str:
url = input["url"]
# Your logic here
return f"Processed: {url}"- Test locally:
librefang skill test ./my-skill --input '{"url": "https://example.com"}'- Submit as a PR to
skills/community/or publish to FangHub.
For skills that are just prompt engineering, no code is needed:
name = "summarize-email"
version = "0.1.0"
description = "Summarize an email thread."
runtime = "promptonly"
tags = ["email", "productivity"]
[input]
thread = { type = "string", description = "The email thread text", required = true }
[prompt]
template = """
Summarize the following email thread in 3 bullet points:
{{thread}}
"""Channels are sidecar-first. A new channel adapter is an
out-of-process subprocess (Python or any language) that speaks
newline-delimited JSON-RPC over stdin/stdout. New in-process Rust
adapters are rejected by a policy gate (see the maintainers-only note
below). See docs/architecture/sidecar-channels.md for the full
model.
-
Install the SDK:
pip install librefang-sdk(source:sdk/python/). -
Subclass
SidecarAdapter. Implementon_send(deliver to the platform) and, for platforms you poll,produce(push inbound messages viaemit). Declare the rich features you support incapabilities— anything you don't declare degrades to plain text automatically.from librefang.sidecar import Content, SidecarAdapter, protocol, run_stdio class MyAdapter(SidecarAdapter): capabilities = ["typing"] async def on_send(self, cmd): ... # deliver cmd.text / cmd.content to the platform async def produce(self, emit): async for m in my_platform_stream(): emit(protocol.message(m.user_id, m.user_name, content=Content.text(m.text))) if __name__ == "__main__": run_stdio(MyAdapter())
Start from
sdk/python/librefang/sidecar/template/and readsdk/python/librefang/sidecar/adapters/ntfy.py— the canonical migration (a real SSE-in / HTTP-out adapter, stdlib-only). -
Register it in
~/.librefang/config.toml:[[sidecar_channels]] name = "myplatform" command = "python3" args = ["adapters/my_adapter.py"] # restart / backoff / ready_timeout / message_buffer / overflow … # are all optional — see librefang.toml.example for defaults.
-
stdout is the protocol channel — never
print()to it. Log viafrom librefang.sidecar import logging. The daemon supervises the process (crash → backoff restart → circuit-break); your job is platform reconnection (with_backoff) and being crash-safe. -
Add tests (the
librefang.sidecarSDK is unit-test-friendly with injectable I/O — seesdk/python/tests/) and submit a PR.
The ~46 pre-existing in-process adapters under
crates/librefang-channels/src/ are grandfathered in
channels-allowlist.txt. scripts/hooks/pre-commit and
cargo xtask channel-policy (run in CI) reject any new file that
impls ChannelAdapter and is not on that allowlist. Adding a new
in-process adapter requires an explicit maintainer decision and an
allowlist entry in a separate reviewed commit — it is not the normal
path. Such adapters still owe a tests/<channel>_wiremock.rs
send-path test (see crates/librefang-channels/CLAUDE.md).
LLM provider drivers live in crates/librefang-runtime/src/. LibreFang uses three driver families that cover most providers:
| Driver | Covers |
|---|---|
openai_compat |
Any OpenAI-compatible API (Groq, Together, Mistral, local Ollama, etc.) |
anthropic |
Anthropic Claude models |
gemini |
Google Gemini models |
Most new providers don't need a new driver — just add an entry to the model catalog in crates/librefang-types/src/models.rs:
- Add the provider constant and its base URL.
- Add model entries with context window sizes and pricing.
- Add aliases if desired (e.g.,
"fast" -> "groq/llama-3.3-70b"). - Write a test verifying the model resolves correctly.
- Create
crates/librefang-runtime/src/my_provider.rs. - Implement the
LlmDrivertrait (seeanthropic.rsfor reference). - Register it in the driver factory in
crates/librefang-runtime/src/llm_driver.rs. - Add config types in
crates/librefang-types/src/config.rs. - Write integration tests (they should skip gracefully if the API key env var is absent).
Built-in tools are defined in crates/librefang-runtime/src/tool_runner.rs.
- Add the tool implementation function:
async fn tool_my_tool(input: &serde_json::Value) -> Result<String, String> {
let param = input["param"]
.as_str()
.ok_or("Missing 'param' field")?;
// Tool logic here
Ok(format!("Result: {param}"))
}- Register it in the
execute_toolmatch block:
"my_tool" => tool_my_tool(input).await,- Add the tool definition to
builtin_tool_definitions():
ToolDefinition {
name: "my_tool".to_string(),
description: "Description shown to the LLM.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"param": {
"type": "string",
"description": "The parameter description"
}
},
"required": ["param"]
}),
},- Agents that need the tool must list it in their manifest:
[capabilities]
tools = ["my_tool"]-
Write tests for the tool function.
-
If the tool requires kernel access (e.g., inter-agent communication), accept
Option<&Arc<dyn KernelHandle>>and handle theNonecase gracefully.
LibreFang has 2,100+ tests covering all crates. Every new feature must include tests. This section explains where tests live, how to structure them, and how to run them.
Tests in LibreFang are inline — they live alongside the source code in #[cfg(test)] modules at the bottom of each .rs file:
crates/librefang-kernel/src/metering.rs # contains #[cfg(test)] mod tests { ... }
crates/librefang-memory/src/substrate.rs # contains #[cfg(test)] mod tests { ... }
crates/librefang-runtime/src/routing.rs # contains #[cfg(test)] mod tests { ... }
This is the standard Rust convention and keeps tests close to the code they verify.
- Test module:
#[cfg(test)] mod tests { ... }at the bottom of the file. - Test functions:
test_<what_is_being_tested>insnake_case.- Good:
test_record_and_check_quota_under,test_substrate_kv,test_routing_table_lookup - Avoid:
test1,it_works,my_test
- Good:
Follow the setup / action / assertion pattern:
- Setup — create the dependencies your code needs (in-memory databases, config structs, etc.).
- Action — call the function or method under test.
- Assertion — verify the result with
assert!,assert_eq!, or pattern matching.
Many crates provide helpers for setup. For example, MemorySubstrate::open_in_memory(0.1) creates an in-memory SQLite database, and MeteringEngine tests use a shared setup() function.
All tests in the workspace:
cargo test --workspaceTests for a specific crate:
cargo test -p librefang-kernel
cargo test -p librefang-memory
cargo test -p librefang-runtimeA single test by name:
cargo test -p librefang-kernel test_record_and_check_quota_underShow output from passing tests (useful for debugging):
cargo test -p librefang-memory -- --nocapture#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_my_feature() {
// Setup
let config = MyConfig::default();
// Action
let result = config.validate();
// Assertion
assert!(result.is_ok());
}
}#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_my_async_feature() {
// Setup
let substrate = MemorySubstrate::open_in_memory(0.1).unwrap();
let agent_id = AgentId::new();
// Action
substrate
.set(agent_id, "key", serde_json::json!("value"))
.await
.unwrap();
let val = substrate.get(agent_id, "key").await.unwrap();
// Assertion
assert_eq!(val, Some(serde_json::json!("value")));
}
}- Use
#[tokio::test]for any test that calls.await. Most crates in LibreFang already depend ontokiowith thetest-utilfeature. - Use in-memory databases for isolation.
MemorySubstrate::open_in_memory(0.1)avoids touching the real filesystem. - Use
tempfile::TempDirwhen you need a real directory (e.g., skill loading, file I/O tests). The directory is automatically cleaned up when theTempDirvalue is dropped. - Use
Default::default()to construct config structs with sensible defaults, then override only the fields relevant to your test. - Skip tests that need external services by checking for environment variables:
#[tokio::test] async fn test_llm_integration() { let api_key = match std::env::var("GROQ_API_KEY") { Ok(k) => k, Err(_) => { eprintln!("Skipping: GROQ_API_KEY not set"); return; } }; // ... test with real API }
- Extract a
setup()helper when multiple tests in the same module need the same boilerplate (seecrates/librefang-kernel/src/metering.rsfor an example). - Test error cases too — verify that invalid input returns the expected error, not just that the happy path works.
Each dated release in CHANGELOG.md (e.g. ## [2026.4.27]) should land with a
companion file in articles/release-<YYYY.M.D>.md. Two GitHub workflows
consume these files on push to main:
.github/workflows/devto-publish.yml— creates / updates the matching dev.to post (title-keyed, idempotent)..github/workflows/release-notify.yml— posts a GitHub Discussion under the release tag using the article body.
If the article is missing on a release tag, the dev.to post and the GitHub
Discussion are silently skipped — public release comms quietly stop. The
articles/ directory drifted out of sync with CHANGELOG.md after
2026-03-22 (#3397) for exactly this reason.
To scaffold an article from a CHANGELOG entry:
bash scripts/changelog-to-article.sh <YYYY.M.D> [<git-tag>]The script slices the matching ## [YYYY.M.D] section out of CHANGELOG.md
and writes articles/release-<YYYY.M.D>.md with the front matter shape
expected by devto-publish.yml. The optional second argument overrides the
default v<YYYY.M.D> placeholder for canonical_url — pass the real CalVer
tag (e.g. v2026.4.27-beta6) when you have it. Review the file, hand-edit
the lead paragraph if the release deserves a narrative beyond the bullet
list, then commit alongside the CHANGELOG bump.
-
Fork and branch: Create a feature branch from
main. Use descriptive names likefeat/add-matrix-adapterorfix/session-restore-crash. -
Make your changes: Follow the code style guidelines above.
-
Test thoroughly:
cargo test --workspacemust pass (all 2,100+ tests).cargo clippy --workspace --all-targets -- -D warningsmust produce zero warnings.cargo fmt --all --checkmust produce no diff.
-
Write a clear PR description: Explain what changed and why. Include before/after examples if applicable.
-
One concern per PR: Keep PRs focused. A single PR should address one feature, one bug fix, or one refactor -- not all three.
-
Review process: At least one maintainer must approve before merge. Maintainers give an initial response within 7 days. If your PR needs changes, we provide specific, actionable suggestions — we don't leave you guessing. Contributor attribution is always preserved. See
GOVERNANCE.mdfor full project policy. -
CI must pass: All automated checks must be green before merge.
Use clear, imperative-mood messages:
Add Matrix channel adapter with E2EE support
Fix session restore crash on kernel reboot
Refactor capability manager to use DashMap
When you add a bullet to the ## [Unreleased] section of CHANGELOG.md,
end the line with your GitHub login in parentheses, e.g.
- Add Matrix channel adapter with E2EE support (#1234) (@your-login)
This is enforced by scripts/check-changelog-attribution.py (wired into the
pre-commit hook and the CHANGELOG Attribution CI job). The check runs
only against the lines your PR adds — historical entries that predate
this convention are not retroactively flagged, and you should not backfill
them. To audit the current [Unreleased] block before cutting a release:
python3 scripts/check-changelog-attribution.py --all-unreleased
The accepted format is (@username) matching \(@[A-Za-z0-9_][A-Za-z0-9_-]*\).
See issue #3400 for the rationale.
This project follows the local CODE_OF_CONDUCT.md. By participating, you agree to uphold a welcoming, inclusive, and harassment-free environment for everyone.
Please report unacceptable behavior to the maintainers.
- Ask in GitHub Discussions for questions or ideas.
- Open a GitHub Issue for bugs or feature requests.
- Check the docs/ directory for detailed guides on specific topics.
- Read GOVERNANCE.md for decision-making, maintainer expectations, and attribution rules.