Contributing to rlox¶
Thank you for your interest in contributing to rlox!
Development Setup¶
# Clone the repository
git clone https://github.com/wojciechkpl/rlox.git
cd rlox
# Create Python virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install maturin numpy gymnasium torch pytest ruff
# Build the Rust extension (always use --release)
maturin develop --release
# Verify
python -c "import rlox; print('rlox ready')"
Running Tests¶
# Rust tests. --no-fail-fast matters: without it cargo stops at the first failing
# test *binary* and later binaries' failures stay hidden.
cargo test --workspace --no-fail-fast
# Python tests — run `tests/`, not `tests/python/`, which is what CI runs.
# `tests/python/` alone collects 1583 of 2233 tests, skipping tests/agentic/ and
# the repo-hygiene guards. That narrower path is why a conftest collision between
# tests/agentic/ and tests/python/ went unnoticed locally while it was aborting
# collection for the entire suite in CI.
pip install -e ".[all]" # after maturin develop
pytest tests/ -q
# Quick smoke test (skip slow integration tests)
pytest tests/ -m "not slow" -q
# Specific test file
pytest tests/python/test_offline_rl.py -v
# Quick algorithm smoke test
pytest tests/python/test_algorithm_smoke.py -v
The Linux-only sandbox tests¶
rlox-sandbox builds and runs on Linux only, and many of its tests need host
capabilities a CI shared runner cannot provide. They are gated by two env vars
(see crates/rlox-sandbox/tests/common/mod.rs):
| Variable | Unlocks | Requires |
|---|---|---|
RLOX_SANDBOX_CGROUP_TESTS |
every test that calls run_sandboxed and asserts on the exit status |
cgroup v2 self-migration — the process must already sit inside a user-delegated cgroup scope |
RLOX_SANDBOX_ADVERSARIAL_TESTS |
tests that detonate real fork/memory/pids bombs | the above, plus a scope-level TasksMax/MemoryMax backstop |
Without them the sandbox child cannot enter its cgroup leaf, run_sandboxed
returns SetupError("child could not write to cgroup.procs …"), and every
containment assertion becomes vacuous — so those tests skip with an explicit
SKIP <name>: … line (visible with -- --nocapture) instead of failing.
scripts/wk-sync-test.sh exports both and wraps the run in
systemd-run --user --scope --slice=rlox.slice, so the full suite runs there:
The gates are explicit opt-ins rather than runtime probes on purpose: on the host that is supposed to have the capability, a regression must fail loudly instead of silently self-skipping.
Before you push¶
Run every CI gate locally in one command:
bash scripts/check-ci-local.sh # all gates
bash scripts/check-ci-local.sh rust # or just one half
SLOW=1 bash scripts/check-ci-local.sh # + the convergence tests (~20-40 min)
WK=1 bash scripts/check-ci-local.sh # + the Linux sandbox suite on wk-system
Use SLOW=1 before merging anything that touches an algorithm's training path
or a convergence threshold. CI runs the convergence tests on pushes to main
only, so a regression there cannot be caught by a PR — it turns main red after
merge. That is exactly how TQC's convergence test broke main: the job had never
run on the PR that introduced it.
Install the pre-push hook once and the fast gates run automatically:
Code Style¶
# Rust
cargo fmt --all
# NOTE: plain `cargo clippy --workspace` FAILS on macOS — rlox-sandbox is
# Linux-only (namespaces, seccomp, cgroup v2) and its `seccompiler` dependency
# does not compile against a macOS libc. Lint it against a Linux target instead,
# otherwise every lint inside `#[cfg(target_os = "linux")]` stays invisible until
# CI runs. Both commands are what check-ci-local.sh does for you:
cargo clippy --workspace --exclude rlox-sandbox --all-targets
rustup target add x86_64-unknown-linux-gnu # once
cargo clippy -p rlox-sandbox --all-targets --target x86_64-unknown-linux-gnu
# Python
ruff check python/
ruff format python/
The toolchain is pinned in rust-toolchain.toml so local clippy enforces exactly
the lint set CI does. Clippy adds lints every release; an unpinned stable meant
CI could fail on lints an older local toolchain never reported.
target-cpu=native and portability¶
.cargo/config.toml sets build.rustflags = ["-C", "target-cpu=native"], so
local builds are tuned for your CPU. That must never leak into CI or a release:
CI shares a build cache across runners with different CPU features (a proc-macro
dylib from another runner kills rustc with SIGILL), and a published wheel has
to run on any CPU of its architecture.
Every Rust-building workflow therefore sets CARGO_ENCODED_RUSTFLAGS: "". Use
that spelling — CARGO_BUILD_RUSTFLAGS: "" does not work: cargo treats an
empty value for that key as unset and falls back to the config file, so it is a
silent no-op. tests/test_repo_hygiene.py enforces both halves of this.
Python version matrix¶
requires-python is >=3.10 and CI runs a 3.10–3.13 matrix, but your venv is a
single version — so version-gated code fails only on CI. The classic case is a
3.11+ stdlib module imported without its 3.10 backport:
try:
import tomllib # stdlib from 3.11
except ModuleNotFoundError: # Python 3.10
import tomli as tomllib
rlox.config._load_toml implements this; reuse it where you can import from
rlox. Two guards cover the gap: tests/test_repo_hygiene.py catches unguarded
version-gated imports statically (any interpreter), and
scripts/check-ci-local.sh runs tests/agentic/ on 3.10 in a throwaway uv
venv. Both exist because this bug shipped in the rlox train --config x.toml CLI
path and in the prime-rl launcher, where a broad except Exception turned the
ModuleNotFoundError into a silent "reward 0.0".
Project Structure¶
crates/
rlox-core/ # Rust data plane: buffers, envs, GAE, KL, pipeline
rlox-nn/ # Backend-agnostic NN traits
rlox-candle/ # Candle backend (inference + hybrid collection)
rlox-burn/ # Burn backend (alternative)
rlox-python/ # PyO3 bindings
python/rlox/
algorithms/ # PPO, SAC, DQN, TD3, A2C, MAPPO, DreamerV3, IMPALA, offline RL, LLM
offline/ # Offline RL base class + protocols
exploration/ # Noise strategies + intrinsic rewards
wrappers/ # VecNormalize and other env wrappers
callbacks.py # Training callbacks
policies.py # Neural network policies
trainers.py # High-level trainers for all algorithms
runner.py # Config-driven training (train_from_config)
dashboard.py # MetricsCollector, TerminalDashboard, HTMLReport
tests/python/ # Python test suite
docs/ # MkDocs documentation
Pull Request Process¶
- Create a feature branch from
main - Write tests first (TDD when possible)
- Ensure all Rust and Python tests pass
- Run
cargo fmtandruff format - Update documentation if adding new features
- Keep PRs focused: one feature or fix per PR
Adding a New Algorithm¶
- Create
python/rlox/algorithms/your_algo.py - Implement using existing primitives (buffers, GAE, etc.)
- Add tests in
tests/python/test_your_algo.py - Add to
docs/examples.mdanddocs/python-guide.md - Update
docs/index.mdalgorithm list
Adding a New Rust Primitive¶
- Implement in the appropriate
crates/rlox-core/src/module - Add unit tests in the same file
- Create PyO3 bindings in
crates/rlox-python/src/ - Register in
crates/rlox-python/src/lib.rs - Export in
python/rlox/__init__.py - Add Python integration tests