Cargo.toml
Rama main · réplica de lectura del repositorio original.
[package]
name = "proxxx"
version = "0.17.0"
edition = "2021"
authors = ["Fabrizio Salmi <fabrizio.salmi@gmail.com>"]
description = "The ultimate Proxmox TUI — fast, secure, uncompromising"
license = "MIT"
repository = "https://github.com/fabriziosalmi/proxxx"
homepage = "https://fabriziosalmi.github.io/proxxx/"
documentation = "https://fabriziosalmi.github.io/proxxx/"
readme = "README.md"
keywords = ["proxmox", "tui", "cli", "virtualization", "infrastructure"]
categories = ["command-line-utilities", "visualization"]
# Match rust-toolchain.toml so consumers (and `cargo publish` if it
# ever happens) get a clear "you need at least this" floor instead of
# a cryptic syntax error from a feature we silently rely on.
rust-version = "1.95"
[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
# HTTP client
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "cookies", "stream", "multipart"] }
# Decode gzip bodies PVE sends unasked (#351). Already in the tree through
# another dependency; this uses the same locked version.
flate2 = "1.1"
# Real-uid + passwd lookup for the audit trail's operator field: `$USER`
# is caller-controlled, so the identity written into the HMAC-chained log
# is anchored on `getuid()` instead (audit #256). Also used by
# tests/resilience_chaos_e2e.rs to raise signals at the current pid.
# Already in the build graph transitively via tokio + rustls, so this
# declaration costs nothing.
libc = "0.2"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# TUI
ratatui = "0.30"
crossterm = { version = "0.29", features = ["event-stream"] }
# CLI
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
# Config
toml = "1.1"
toml_edit = "0.25"
directories = "6"
# Error handling.
# - `thiserror` for typed domain errors (audit) — `proxxx::api::ApiError`
# exposes actionable variants (Unauthorized / NotFound / RateLimited /
# PayloadTooLarge / Transport / Parse) the UI can `match` on.
# - `anyhow` for the application boundary (main, CLI dispatch) where we
# collapse everything into one report-friendly error chain.
anyhow = "1"
thiserror = "2"
# Logging (async, file-based — TUI owns stdout)
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
# Rate limiting
governor = "0.8"
# Fuzzy search
nucleo = "0.5"
# Async trait
async-trait = "0.1"
nucleo-matcher = "0.3.1"
dirs = "7.0.0"
rusqlite = { version = "0.40.0", features = ["bundled"] }
# SSH layer
russh = "0.65"
# `russh-keys` was folded back into `russh` proper in 0.60 — the
# functionality lives at `russh::keys::*` now. That removed rsa 0.9.10,
# but russh still pulls rsa 0.10.0-rc.18 (directly and through ssh-key)
# for RSA user keys, and RUSTSEC-2023-0071 still applies: see
# `.cargo/audit.toml` and `deny.toml` for the rationale.
hex = "0.4"
sha2 = "0.11"
# Phase 17 audit fix: HMAC-SHA256 signing for HITL Telegram
# callback_data. Defends against bot-token leak — an attacker who
# steals the token can send messages as the bot but cannot forge
# valid callback_data because the HMAC key is a separate secret
# auto-bootstrapped at `<config_dir>/telegram_hmac.key` (0600).
# Bumped to 0.13 in lockstep with sha2 0.11 — hmac 0.13 requires
# the new `Digest` trait shape that sha2 0.11 implements.
hmac = "0.13"
# OS-native randomness for the HMAC-key bootstrap. Already in the
# tree (transitive via rustls / tokio-rustls); promoting to a direct
# dep so the call site is grep-able. Bumped to 0.4 (was 0.2) — the
# `getrandom::getrandom(buf)` function was renamed to `getrandom::
# fill(buf)` in 0.3 and the 0.2 alias removed in 0.4. See call sites
# in `src/audit/mod.rs` and `src/hitl/hmac_key.rs`.
getrandom = "0.4"
# PTY screen state for SSH guest sessions (feature 1a).
# Using vt100-ctt (up-to-date fork of vt100) because vt100 0.16 pins
# unicode-width >= 0.2.1, conflicting with ratatui 0.29's exact pin to 0.2.0.
# `default-features = false` drops the `tui-term` feature (which pulls
# ratatui 0.29 transitively + its `paste`/`lru` advisories). We only
# use `vt100::Parser` directly; the tui-term widget is not in our
# render path. Snapshots from the parser feed our own ratatui-0.30
# rendering layer in `tui/widgets/pty.rs`.
vt100 = { package = "vt100-ctt", version = "0.17", default-features = false }
# WebSocket client for Proxmox termproxy serial console (feature 1b).
# Uses rustls (matching reqwest's existing rustls-tls feature) so we don't
# pull in native-tls / openssl-sys as a new system dep.
tokio-tungstenite = { version = "0.29", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] }
futures-util = { version = "0.3", default-features = false, features = ["sink"] }
# HTTP server for MCP Streamable HTTP transport (2025-03-26 spec).
# axum is already tokio-native; tower-http adds SSE + timeout middleware.
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio"] }
tower-http = { version = "0.7", default-features = false, features = ["timeout"] }
tokio-stream = { version = "0.1", default-features = false, features = ["time", "sync"] }
# `tokio-util io` adapts an async `File` into a Stream so multipart
# uploads can be POSTed without buffering multi-GB files in RAM.
tokio-util = { version = "0.7", default-features = false, features = ["io"] }
# Custom rustls verifier for `verify_tls = false` profiles (homelab self-
# signed). Pulled in directly so we can build a ClientConfig that mirrors
# the reqwest `danger_accept_invalid_certs` flag.
rustls = { version = "0.23", default-features = false, features = ["ring"] }
# Phase 13 / #320: TLS pinning (TOFU) for self-signed PVE clusters and
# PBS fingerprint pinning (#414). `tls_pin_mode = "tofu"` fetches the leaf
# cert on first connect via a tokio-rustls TLS handshake, pins it to disk,
# and configures rustls with `PinnedLeafVerifier` via `use_preconfigured_tls`.
# Must match rustls 0.23 above.
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
# Atomic, TOCTOU-safe temp-file creation for the SPICE .vv handoff path
# (audit). Provides O_EXCL + crypto-random suffix + permissions
# applied at creation time, so a malicious local process can't pre-place
# a symlink at the expected path and steal the SPICE password.
tempfile = "3"
# Macro audit: secret zeroing.
# `Zeroizing<String>` overwrites the heap bytes with zeros in `Drop`,
# so a core dump or swap-out after the secret has been released cannot
# leak the Proxmox / PBS / Telegram credentials. The wrapper has no
# performance cost on the hot path; only the Drop call does the memset.
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
[features]
default = ["keychain"]
# Enables the OS keychain credential store on macOS (`keyring` with
# `apple-native` / Security.framework). On Linux and other targets `keyring`
# is not a dependency, so this feature is a no-op there (`--no-default-features`
# remains accepted everywhere).
keychain = ["dep:keyring"]
[target.'cfg(target_os = "macos")'.dependencies]
# OS keychain (macOS only — #319). Declared under target-specific dependencies
# with `apple-native` so macOS links Security.framework (`UntilDelete`
# persistence) while Linux musl/gnu builds do not pull `keyring` or D-Bus.
keyring = { version = "3", features = ["apple-native"], optional = true }
[dev-dependencies]
wiremock = "0.6"
insta = { version = "1", features = ["json"] }
tokio-test = "0.4"
rcgen = { version = "0.14", default-features = false, features = ["ring"] }
# Benchmark suite (Mission 1, Bench protocol).
# `criterion` is the standard Rust benchmarking harness — it supplies
# `black_box` to defeat LLVM dead-code elimination, statistical
# sample collection (default 100 samples, t-test outlier filtering),
# and `cargo bench` integration. We use it through the `harness =
# false` per-bench flag so each bench file is its own binary.
criterion = { version = "0.5", features = ["html_reports"] }
# E2E test suite (Mission 2 will use this — declared here so adding
# `#[serial]` tests later doesn't require a Cargo.toml churn).
serial_test = "4"
# Property-based testing for invariants that example tests can't pin.
# `proptest` generates randomized inputs against `prop_assert!` predicates,
# shrinks failures to a minimal counterexample, and runs deterministically
# in CI via a fixed seed. We use it where invariants are stronger than
# "this specific input → this specific output" — sanitize must strip
# every C0 byte regardless of input shape; the snaptree builder must
# terminate on any graph (incl. cycles); the audit HMAC chain must
# detect a single-byte mutation anywhere in any sequence.
proptest = { version = "1", default-features = false, features = ["std"] }
# ── Benchmark binaries ──────────────────────────────────
# Each `[[bench]]` produces an isolated binary so failures or
# allocation pollution in one don't bleed into others.
[[bench]]
name = "serde_parse"
harness = false
[[bench]]
name = "snaptree_assemble"
harness = false
[[bench]]
name = "fuzzy_search"
harness = false
# Audit: release profile choices, with intent.
#
# - `lto = true` is the same as `lto = "fat"` per Cargo docs (cross-crate
# inlining across the whole dep graph). Fat LTO is ON.
# - `codegen-units = 1` forces single-translation-unit codegen, the
# slowest-but-most-optimal mode.
# - `strip = true` removes symbol info from the final binary
# (~15→6 MiB on this codebase).
# - `opt-level = "z"` is INTENTIONALLY size-optimised, not speed-
# optimised. proxxx is shipped as a single binary distributed via
# homebrew / direct download; size matters more than the marginal
# ~5% runtime gain of `opt-level = 3`. The CPU-bound paths
# (`benches/serde_parse.rs` etc.) measure within budget at "z".
# - `panic = "unwind"` (the default) is INTENTIONALLY kept. The
# flight-recorder panic hook (`util::panic_hook`) and the
# `TerminalGuard::Drop` BOTH depend on Rust's unwind machinery to
# restore the terminal on panic. Switching to `panic = "abort"`
# would shave another ~200 KiB off the binary at the cost of
# leaving the user's terminal in a broken raw-mode state on any
# unwrap / assert / index OOB. Not worth it.
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z" # size > speed (see comment above; benches stay in budget)
# panic = "unwind" — DO NOT change to "abort". TerminalGuard + panic_hook
# need unwind for terminal restoration. Reviewed
# ── PARANOID CLIPPY ──────────────────────────────────────
[lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
clone_on_ref_ptr = "warn"
todo = "deny"
panic = "deny"
# Phase polish baseline — `indexing_slicing` was at "warn", flagging
# 140 already-bounds-checked sites in TUI renderers + snapshot
# walkers. None are real OOB risks — every site is already gated by
# `if i < v.len()` or the iterator was just collected from a sized
# source. The `panic = "deny"` lint above catches the real failure
# mode (an actual index-out-of-bounds at runtime is a panic), so
# `indexing_slicing` adds noise without signal. Demoted to "allow"
# with this reasoning so future drift on `panic` is what matters.
indexing_slicing = "allow"
# Audit #3 — codebase is clean today, but without an automated guard a
# regression that holds a sync lock across an .await would deadlock the
# Tokio scheduler under load and not get caught until production.
await_holding_lock = "deny"
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
# ── Justified pedantic/nursery suppressions ──────────────────────────
#
# These categories produce dozens of false positives for proxxx's
# domain. Each entry below is a deliberate scope decision, not laziness.
# Removing one means scanning every site individually for the rare real
# bug — which we'll do when we have a reason. Documented here so a
# future maintainer can re-evaluate.
#
# Numeric casts: proxxx renders cluster metrics — bytes, CPU times,
# memory — as percentages and human-friendly sizes. The natural shape
# is `(used as f64 / total as f64) * 100.0`, which trips
# `cast_precision_loss`/`cast_possible_truncation`/`cast_sign_loss` on
# every divisor. Precision loss in a 0..100 % display is irrelevant;
# the alternative of `TryFrom + unwrap` would just add noise.
cast_precision_loss = "allow"
cast_possible_truncation = "allow"
cast_sign_loss = "allow"
cast_possible_wrap = "allow"
# Function shape: TUI render functions and CLI handlers handle many
# branches by domain (one match arm per command, one case per cluster
# event). Splitting them reduces locality without improving clarity.
# `too_many_arguments` flags clap dispatchers and the `ProxmoxGateway`
# trait — both have wide signatures because the underlying PVE API
# does. Re-evaluate per-fn if a real legibility issue surfaces.
# Audit #272: back to `warn` — the blanket allow also covered
# `tui::dispatch_side_effect` and `tui::run`, which are deep rather than
# flat, so nothing reported when a function doubled in size. The genuine
# exemptions (wide, flat, exhaustive dispatch over a closed enum) now
# carry a per-function `#[allow]` naming themselves, which is a visible
# decision rather than a silence.
too_many_lines = "warn"
too_many_arguments = "allow"
# Layout: a const or struct decl colocated with its first use is
# clearer than hoisting to the top of the function. Hoisted constants
# get their own clippy lint anyway (`unnecessary_wraps` etc.) — pick
# your poison.
items_after_statements = "allow"
# Style: we're not picking sides on these. Both forms compile to
# equivalent code; clippy preferring one is taste, not safety.
option_if_let_else = "allow"
match_wildcard_for_single_variants = "allow"
format_push_string = "allow"
significant_drop_tightening = "allow"
# Visibility convention: proxxx marks crate-internal items `pub(crate)`
# deliberately — as an intent signal ("this is a crate-internal API
# surface") — even inside private modules where module privacy already
# bounds them. `redundant_pub_crate` fights that convention, firing on
# every such item (the shared `state::test_support` mock, the reconcile
# drift helpers reused by `state::converge`, the state-apply renderers
# reused by `reconcile converge`, …). We keep the explicit marker.
redundant_pub_crate = "allow"
# More taste-level lints. Each one I evaluated and decided the lint
# isn't load-bearing for proxxx's domain.
#
# `match_same_arms`: collapsing `Err(NotFound) => …, Err(Forbidden) =>
# …` into one arm hides the categorical distinction we WANT to be
# explicit about; same for PVE error families.
#
# `many_single_char_names`: PBS uses single-letter API field names
# (`{T, ct, sz}`) that we deserialize 1:1; renaming defeats the
# round-trip.
#
# `too_long_first_doc_paragraph`: many of our doc comments open with
# a multi-clause sentence describing the WHY of the function. Splitting
# at the first `.` hides intent.
#
# `literal_string_with_formatting_args`: false-fires on shell command
# strings (`"qm config {VMID}"`-style templates) where the `{}` is
# meant for the SHELL or for downstream substitution, not Rust format!.
#
# `unreadable_literal`: most hits are timestamps/UPIDs in test
# fixtures; readability isn't improved by adding underscores to a
# random-looking string.
#
# `manual_let_else`, `map_unwrap_or`, `or_fun_call`,
# `field_reassign_with_default`, `assigning_clones`,
# `useless_let_if_seq`, `if_not_else`: all stylistic transforms that
# are equivalent in performance and clarity. Choosing for the project
# is more legibility-cost than the lint saves.
#
# `implicit_hasher`: forces generic over `S: BuildHasher` everywhere
# we accept a `HashMap`. Adds one trait bound for every signature with
# zero practical benefit on the proxxx call surface.
#
# `clone_on_ref_ptr`: the deny config above already enforces
# `Arc::clone(&x)` for our own code; warn-level here picks up
# transitive false positives from macros.
#
# `doc_lazy_continuation`: rustdoc renders both forms identically.
match_same_arms = "allow"
many_single_char_names = "allow"
too_long_first_doc_paragraph = "allow"
literal_string_with_formatting_args = "allow"
unreadable_literal = "allow"
manual_let_else = "allow"
map_unwrap_or = "allow"
or_fun_call = "allow"
field_reassign_with_default = "allow"
assigning_clones = "allow"
useless_let_if_seq = "allow"
if_not_else = "allow"
implicit_hasher = "allow"
doc_lazy_continuation = "allow"
# `# Errors` doc sections on every fn returning `Result` is documentation
# theatre for an internal-API codebase. Errors flow through `anyhow` /
# `ApiError` and are categorised at the boundary (cli, mcp, tui). The
# domain types tell the operator far more than "may fail" prose.
missing_errors_doc = "allow"
# Same for `# Panics`: panics are explicitly forbidden via the `panic`
# = "deny" rule above; if a function CAN panic it's a deny-level issue,
# not a doc-paragraph issue.
missing_panics_doc = "allow"
# `format!("Hello, {x}")` vs `format!("Hello, {}", x)` is a stylistic
# preference that flips between Rust toolchain versions. We've already
# auto-applied where possible; the remainder appear inside macros
# clippy can't reach. Allowing avoids periodic noise.
uninlined_format_args = "allow"
# ── Debian package (.deb) — `cargo deb` ──────────────────────────────
# Built in CI for the two linux-musl targets (release.yml) and attached to
# each GitHub release. The release binary is statically linked against musl,
# so the package declares NO runtime dependencies — it installs cleanly on
# any Debian/Ubuntu, including a Proxmox VE node, without pulling shared libs.
[package.metadata.deb]
maintainer = "Fabrizio Salmi <fabrizio.salmi@gmail.com>"
copyright = "2026, Fabrizio Salmi <fabrizio.salmi@gmail.com>"
license-file = ["LICENSE", "0"]
section = "admin"
priority = "optional"
# Static musl binary — no shared-library dependencies to resolve.
depends = ""
extended-description = """\
proxxx is a fast, secure, single-binary Proxmox VE manager: CLI, TUI, MCP \
server, and a unified daemon (alerts, HITL, schedule, and GitOps reconcile) \
in one executable. GitOps state across eight families with pre-flight risk \
gates, an HMAC-chained audit log, and signed, SBOM-shipped releases. No \
telemetry — it talks only to the PVE/PBS endpoints you configure."""
assets = [
["target/release/proxxx", "usr/bin/", "755"],
["README.md", "usr/share/doc/proxxx/README.md", "644"],
["CHANGELOG.md", "usr/share/doc/proxxx/CHANGELOG.md", "644"],
["SECURITY.md", "usr/share/doc/proxxx/SECURITY.md", "644"],
]