# Contributing to proxxx Thanks for considering a contribution. proxxx is a security-sensitive infrastructure tool — a small bug can lock an admin out of their cluster — so the bar is "demonstrate it works against a real PVE node, not just in unit tests". This document explains how. ## TL;DR 1. Open an issue first for non-trivial work to align on scope. 2. Run the full local gate before opening a PR (`scripts/gate.sh`). 3. Live-cluster verification is required for changes that touch the API client, the mutation lifecycle, or any TUI that issues writes. 4. Security-impacting changes go through `SECURITY.md`, not a PR. 5. Be kind (see `CODE_OF_CONDUCT.md`). ## Before you start - **Issue first**: open a GitHub issue describing what you want to change. For feature requests, mention which existing surface it would touch and whether it overlaps with any documented design boundary (`README.md` → "Honest non-goals"). For bug reports, include `proxxx version --json`, OS / arch, and a minimal repro. - **No surprise mega-PRs**: a 2000-line PR with no preceding issue is unlikely to land. Break it down or open the design conversation first. - **Existing roadmap**: the `pre-commit/01-feature-coverage.md` matrix tracks every feature with its verification status (✅ live / 🟡 partial / ⚠️ wiremock-only / ❌ not verified). Pick something that's already on the matrix or propose a new row. ## Development environment ```bash # Toolchain pinned in rust-toolchain.toml (1.95.0 + rustfmt + clippy # + x86_64-unknown-linux-musl). rustup picks it up automatically # on first cargo invocation. git clone https://github.com/fabriziosalmi/proxxx.git cd proxxx cargo build --release # ~1 minute cold; cached after that ./target/release/proxxx init # Edit ~/.config/proxxx/config.toml (Linux) or # ~/Library/Application Support/dev.proxxx.proxxx/config.toml (macOS) ./target/release/proxxx ls nodes ``` For VitePress docs: ```bash cd docs && npm install && npx vitepress dev # http://localhost:5173 — edit Markdown under docs/, hot reloads ``` ## The gate (commit acceptance criteria) Every commit on `main` passes an 8-stage local gate AND CI. Bypassing either with `--no-verify` is owned by the bypasser. Stages: | # | Stage | What | Roughly | | - | --- | --- | --- | | 0 | secret regression scan | Looks for tokens / passwords accidentally committed | <1 s | | 1 | `cargo fmt --check` | Code style | ~3 s | | 2 | `cargo clippy --release --all-targets -- -D warnings` | Lints, deny tier (`unwrap_used`, `expect_used`, `panic`, `todo`, `await_holding_lock`) | 10–60 s | | 3 | `cargo audit --deny warnings` | Supply-chain CVEs from `Cargo.lock` | 3–5 s | | 4 | `cargo deny check` | License whitelist + banned crates + crates.io-only sources + wildcard ban | 2–4 s | | 5 | `cargo test --release --all-targets` | All unit + integration + wiremock + proptest cases (~25 properties × 256 random cases) | 10–90 s | | 6 | `tests/live/test_run.sh` | 87 read-only probes against a real PVE cluster | ~30 s | | 7 | `tests/live/test_mutation.sh` | LXC 9999 lifecycle + cluster-level CRUD + QEMU 9998; opt-in QGA via `PROXXX_E2E_QGA_VMID=` | ~60 s | End-to-end wall time against a reachable cluster: **~340–480 s** (a release build dominates; stages 6+7 themselves are ~100 s combined). Hook setup: ```bash git config core.hooksPath .githooks chmod +x scripts/gate.sh .githooks/pre-commit .githooks/pre-push mkdir -p ~/.config/proxxx && chmod 700 ~/.config/proxxx cp tests/live/env.local.example ~/.config/proxxx/live-env chmod 600 ~/.config/proxxx/live-env # fill in PROXXX_E2E_PVE_URL / TOKEN / NODE ``` Credentials live **outside the working tree**: a live PVE token inside a checkout is one `git add -f` or one world-readable clone away from leaking. The harnesses read `~/.config/proxxx/live-env` first and fall back to the gitignored `tests/live/env.local` for existing setups; `PROXXX_E2E_ENV_FILE` overrides both. Stages 6 + 7 require a reachable PVE cluster. If you don't have one, **explicitly skip stages 6 + 7** in your PR description — a maintainer will run them. Don't silently bypass. ### Script gates (CI also runs these) | # | Command | What it enforces | Time | | :-- | :--- | :--- | :--- | | 6 | `ruff check .` | Python lint (`scripts/`, `assets/`) | ~1 s | | 7 | `shellcheck -S error tests/live/*.sh scripts/*.sh` | Shell errors in the live harnesses | ~1 s | | 8 | `gitleaks detect` | Committed secrets (CI scans history too) | ~2 s | Added by the 2026-09-09 audit (#274): these three surfaces shipped unchecked. `shellcheck` starts at `-S error` deliberately — the tree carries style and warning findings that are real but not worth blocking on today, and a gate that is red by default is a gate people learn to ignore. ### Coverage ratchet (CI) The CI coverage job measures line coverage with `cargo llvm-cov` and checks it against `.coverage-floor` with `scripts/coverage-ratchet.py` (#298). The floor is the last measured value minus 0.5pp. - **Coverage fell below the floor:** the job fails and lists the files whose uncovered lines grew. Add tests for them, or remove the untested code. - **Coverage rose by more than 0.5pp:** the job fails and asks you to raise the floor in the same pull request. The run's `coverage-lcov` artifact holds the new file as `lcov.info.coverage-floor`; commit it as `.coverage-floor`. Do not write the floor from a local run: coverage measured on macOS differs from the Linux CI value. ## Live-cluster verification The single most useful thing a contributor can do is run a change against a real cluster and report results. The harness expects: - A 1-3 node PVE cluster (lab is fine — `pve1`, `pve2`, `pve3`). - VMID 9999 free for the LXC mutation lifecycle. - VMID 9998 free for the QEMU mutation lifecycle (boots from an alpine ISO; auto-cleaned via `trap EXIT`). - Optionally a QGA-enabled QEMU VM for the agent-required round-trip probes — set `PROXXX_E2E_QGA_VMID=`. The env-file template carries the alpine apk one-liner to install qemu-guest-agent. Findings format (paste in PR description): ``` Cluster: PVE 9.x.y, 3 nodes, ~16 GB RAM Local gate: PASS=88 / FAIL=0 (read), PASS=53 / FAIL=0 (mutation+QGA) Notes: ``` ## Writing tests - **Wiremock first**: every API client method gets at least one wiremock test covering the URL path + form encoding + at least one PVE-quirky shape (numerics-as-strings, hyphen-renamed fields, `null` where the schema implies a value, bool-from-int). - **Live regression after**: when a live test surfaces a wire-shape bug, add a wiremock that pins the OBSERVED shape — not just the PVE-docs shape. Real PVE drifts from its docs. - **Pre-flight risk variants**: any new destructive op MUST add at least one variant to `src/app/preflight.rs` if it raises a new class of risk; otherwise document why an existing variant covers it. - **No `unwrap` in production paths**. `cargo clippy` enforces. Tests are relaxed via `cfg_attr(test, allow(...))`. - **Concurrent code gets a concurrent test.** A bare `#[tokio::test]` runs on one thread and cannot observe a race. Code that several callers reach at once (the HITL approval store, the audit chain, the TUI queue writer, any `JoinSet`/`Semaphore` fan-out, anything shared between the CLI, TUI, MCP and daemon processes) needs a `#[tokio::test(flavor = "multi_thread")]` (or `std::thread`) test whose callers overlap for real, behind a `Barrier`, and whose assertion fails when the protection is removed (break it once to check). Run it a few dozen times in Linux too: fsync and scheduling differ from macOS, and the audit key race (#482) reproduced only there. Examples: `hitl::pending::concurrency_tests`, the fan-out test in `tui::refresh_cost_tests` (#294). ### Fuzzing `fuzz/` holds cargo-fuzz targets for the code that reads untrusted input. Each asserts an invariant, not only the absence of a panic: | Target | Invariant | | :--- | :--- | | `mcp_shape` | A value an MCP `Name` or `Upid` parameter accepts is one path segment: no separator, no relative segment, no control character. | | `sanitize` | `sanitize_display` and `safe_text` leave no C0 (except TAB), DEL or C1 control, drop nothing else, and are idempotent; `truncate_ellipsis` stays within its bound. | | `guest_specs` | `--ipconfig`, `--net` and `--disk` values parse without panicking, and print/parse is stable. | | `api_json` | Deserializing PVE responses into the API types never panics. | They need a nightly toolchain and cargo-fuzz (`.github/workflows/fuzz.yml` pins both and runs them weekly and on PRs that touch `fuzz/`): ```sh cargo +stable install --locked --version 0.12.0 cargo-fuzz cd fuzz cargo +nightly-2026-10-15 fuzz run --dev -s none sanitize -- -max_total_time=60 ``` Use the dev profile: a release build of the proxxx lib with coverage instrumentation needs more than 6 GB for one `rustc`. A crashing input lands in `fuzz/artifacts//`; turn it into a regular test before fixing the bug. ## Pull requests - Branch from `main`, name like `feat/` / `fix/` / `docs/`. - Commits in **conventional style** (`feat:`, `fix:`, `docs:`, `test:`, `chore:`, `refactor:`); the gate enforces nothing about the message format but reviewers will ask you to rewrite if it reads as a noise. - One concept per PR. Mixed concerns get split. - New functionality and bug fixes come with tests in the automated suite: a unit, integration, wiremock or property test that fails without the change (see [Writing tests](#writing-tests)). A PR without a new test says why, for example a dependency bump or a docs-only change. - Update `CHANGELOG.md` under `[Unreleased]` describing what changes for users. - If the change touches a row in `pre-commit/01-feature-coverage.md`, update the row's status emoji + cite the test or live evidence. ## What gets refused (saving you time) - PRs that bypass the gate (`--no-verify`, deleted hooks, soften clippy lints, skip stages without a stated reason). - PRs that introduce a new dep larger than ~200 KB without a clear load-bearing justification — proxxx is "single static binary" by design. - PRs that re-implement something PVE already does (e.g. a Perl-side algorithm); we shell out instead, per the architectural review. - "Improvements" to error messages that swallow detail. Errors should remain typed and forensically useful. ## Scope reminder Out-of-scope features (already declared design boundaries — won't land): - A web UI for proxxx itself (proxxx is terminal-only by design). - Re-implementation of `pve-access-control` (we shell out via `pveum`). - WebAuthn enrolment from the TUI (browser cert ceremony required). - Single-file extraction from PBS archives (full archive only). The full list lives in `README.md` → "Honest non-goals". ### Surface freeze No new resource family lands: no new group of `ProxmoxGateway` methods and no new declarative state family. The freeze lifts for one family at a time, when all three of these hold: 1. Line coverage is at or above the floor checked in as `.coverage-floor` (#298). 2. The gateway family it would join is under its size ceiling (#293). 3. A named person or project outside this repository has asked for it in an issue. The floor exists since #298. Until the ceilings (#293) exist, condition 2 cannot be met, so the freeze holds. A request that does not meet them is labelled `frozen: awaiting-demand` and stays open: that label is the default answer, not the exception. Fixes, tests, documentation, and changes inside an existing family are not affected. ### Documentation claims need a test A claim about behaviour or structure, in any Markdown page or doc comment, either has a test that fails when the claim stops being true, or it is not written. A claim that describes a goal rather than the code says so ("intended", "planned"). Removing a claim that has no test is always acceptable. What already holds this: - The `--help` of every command is snapshotted (`src/snapshots/proxxx__tests__cli_help.snap`). A renamed flag or a removed subcommand fails the test; review and accept an intended change with `cargo insta review` (or `INSTA_UPDATE=always cargo test --bin proxxx`, then read the diff before committing it). - The supported library surface (`api`, `config`, `app::preflight`, `state::model`) carries doctests that `cargo test` runs. - The structural claims in `ARCHITECTURE.md` are checked by `tests/architecture_claims.rs`. A pull request that changes documented behaviour updates the documentation and the test that holds it in the same change. ## Questions? Open a discussion on GitHub or an issue. For security findings, do not open a public issue — see `SECURITY.md`.