# Architecture > **Audience**: contributors, security reviewers, AI agents driving > a `cargo build` and wondering what they're staring at. > > **Length budget**: one page. For depth on any single subsystem, > follow the file links — every module has a top-of-file `//!` > block that explains itself. > > **Every structural claim here is checked** by > `tests/architecture_claims.rs` (names, paths, gate order, I/O rules) > and by the `main.rs` tests (the commands in the process table parse). > A claim that cannot be checked does not belong on this page. ## What proxxx is A single Rust binary with **three callers**: a CLI (scriptable, JSON-friendly), a TUI (interactive, ratatui), and an MCP server (LLM agents). It talks to Proxmox VE and Proxmox Backup Server over REST, SSH and WebSocket. No agent runs on the cluster. ## The gates, and which caller runs which Two layers. The **gateway layer** is shared by construction; the **caller layer** is not, and differs per caller. **Gateway layer: every mutation, every caller.** Every mutating HTTP request `PxClient` sends is built in one of four places in `src/api/client.rs`: the private helpers `post`, `put` and `delete`, and the streaming storage upload. Each runs, in this order and before the request leaves the process: 1. `PxClient::guard_read_only`: refuses when the profile is `read_only`. 2. `incident::check_not_frozen_for`: refuses during an incident freeze (global, or of this profile). 3. `audit::MutationAuditCtx::write_ahead_at`: the write-ahead entry in the HMAC-chained audit log; refuses (exit 8) when it cannot be written. The outcome is recorded after the response. A new gateway method that mutates through the helpers cannot skip them, and the test fails on a mutating request built anywhere else in `src/api` without them. **Caller layer: risk preflight and human approval.** | | CLI | TUI | MCP | | :--- | :--- | :--- | :--- | | Risk preflight (`app::preflight`) | `assess_deep` on stop, restart, delete, migrate, disk move and disk resize; a Severe risk refuses (exit 6) unless `--allow-risk` | `assess` (no deep checks) on operations run from the operation queue; a Severe risk fails the operation. Actions from the command palette run no preflight, but stop, restart, delete and snapshot refuse a locked or HA-managed guest (`guest_block_reason`) | `assess_deep` on stop, restart, delete and migrate when the approval is requested: a Severe risk refuses and nobody is asked; the other risks are added to the approval message | | Human approval (`hitl`) | start, stop, restart, suspend and resume with a matching policy are queued for Telegram approval and run by `proxxx hitl serve`; with `--secure`, destructive commands are refused (exit 8) | `hitl::policy::check_policies` plus `--secure`: Telegram approval before the side effect runs | every destructive tool needs a matching policy and is queued for Telegram approval; `proxxx hitl serve` executes it after approval | An approved operation is checked again when `proxxx hitl serve` runs it: for stop, restart, delete and migrate, `app::preflight::assess` on the guest as it is then refuses a Severe risk (an approver cannot pass `--allow-risk`). ## Module map (`src/`) | Module | Responsibility | Sync / Async | | :--- | :--- | :--- | | [`api/`](src/api/) | PVE REST client: `ProxmoxGateway` trait, `PxClient`, typed `ApiError`, retry, per-profile TLS pin | async | | [`pbs/`](src/pbs/) | PBS REST client + restore handoff to `proxmox-backup-client` | async | | [`wsterm/`](src/wsterm/) | Termproxy WebSocket (serial console), with frame and message size caps | async | | [`ssh/`](src/ssh/) | russh-based sessions and PTY | async | | [`access/`](src/access/) | parser for effective permissions (`pveum` output) | sync | | [`config/`](src/config/) | TOML profiles, secret resolution, live reload (`config::watcher`) | sync (load), async (watch) | | [`audit/`](src/audit/) | append-only SQLite audit log with an HMAC-SHA256 chain | sync | | [`incident/`](src/incident/) | freeze lock (`incident freeze` / `thaw`) | sync | | [`app.rs`](src/app.rs) | the TUI reducer: `Action × AppState → AppState + SideEffect`; no `.await` and no file, network or process I/O | sync | | [`app/`](src/app/) | helpers: preflight, snaptree, cache, HA preview, queue, search. `preflight`, `patch` and `cache` do I/O | mixed | | [`alerts/`](src/alerts/) | alert engine, dedup and notifiers | async | | [`hitl/`](src/hitl/) | Telegram approvals: signed callbacks, pending store, execution after approval | async | | [`mcp/`](src/mcp/) | MCP server: stdio JSON-RPC and Streamable HTTP, tool registry in `mcp::tools` | async | | [`metrics/`](src/metrics/) | Prometheus exporter | async | | [`tui/`](src/tui/) | ratatui render loop, views, widgets, event loop | sync (render), async (events) | | [`cli/`](src/cli/) | clap dispatch and per-domain handlers | async | | [`handoff/`](src/handoff/) | SPICE `.vv` file, noVNC URL, `remote-viewer` launch | sync | | [`util/`](src/util/) | sanitize, output format, shutdown, panic hook, durable writes | sync | `AppState` is used only by the TUI: `src/tui/`, `src/app.rs` and the reducer helpers `src/app/queue.rs` and `src/app/search.rs`. The CLI and MCP paths are imperative and reach the gates directly. ## Data flow: deleting guest 100 ### CLI: `proxxx delete 100 --yes` ``` main ─► cli::execute ├─ enforce_cli_secure_mode (--secure: refuse, exit 8) ├─ cli::common::find_guest_full (GET /cluster/resources) ├─ cli::common::enforce_preflight_with_notes │ └─ app::preflight::assess_deep (Severe: exit 6) └─ execute_delete ─► PxClient::delete_guest ├─ GET status/current (must be stopped) └─ PxClient::delete (gateway layer) ``` ### TUI: `d` on a guest row ``` tui::event::map_key ─► Action::EnqueueBatchOperation([DeleteGuest]) app::update ─► app::queue::QueuedOp in AppState.op_queue 'C' in the queue ─► Action::ExecuteQueue ─► SideEffect::ExecuteQueue tui dispatch_side_effect ├─ queue snapshot saved first (refuses to run if it cannot be) ├─ HITL check per operation (policy / --secure) ├─ app::preflight::assess (Severe: operation fails) └─ PxClient::delete_guest ─► PxClient::delete (gateway layer) ``` ### MCP: `tools/call delete_guest {"guest_id": 100}` ``` stdin line (16 MiB cap, mcp::server::MAX_RPC_LINE_BYTES) ─► mcp::server::RpcRequest ─► mcp::dispatch::dispatch_rpc └─ tokio::time::timeout(tool budget) ─► mcp::dispatch::handle_tool_call ├─ registry lookup + parameter type checks └─ destructive: hitl::policy::check_policies ├─ no policy: refused └─ policy: app::preflight::assess_deep_with ├─ Severe: refused, nobody asked └─ otherwise: pending request stored, Telegram approval sent with the risks, result "intercepted" proxxx hitl serve, on approval: hitl::daemon::handle_callback_update ─► guest re-checked against the request ─► app::preflight::assess on the live guest (Severe: refused) ─► mcp::dispatch::exec_delete_guest ─► PxClient::delete_guest (gateway layer) ``` ## Process model | Process | Entry point | Lifecycle | | :--- | :--- | :--- | | One-shot CLI | `proxxx ` | runs one command and exits | | TUI | `proxxx` (no command) | interactive until quit | | Background daemon | `proxxx daemon serve` | long-running: alerts, HITL listener and schedule tick in one process | | Alerts watcher | `proxxx alerts watch` | long-running | | HITL listener | `proxxx hitl serve` | long-running Telegram long-poll | | MCP stdio | `proxxx mcp serve` | one process per client session | | MCP HTTP | `proxxx mcp serve-http --port 8080` | long-running HTTP server | | Prometheus exporter | `proxxx metrics serve` | long-running HTTP server | All of them are the same binary; `src/main.rs` routes by command, and no command means the TUI. ## Build profiles `[profile.release]` in `Cargo.toml`: `lto = true`, `codegen-units = 1`, `strip = true`, `opt-level = "z"`. `scripts/gate.sh` runs clippy and the test suite with `--release`, so the gate tests the optimisation level that ships. ## Cross-references * [README.md](README.md) — user-facing surface * [THREAT_MODEL.md](THREAT_MODEL.md) — attack surfaces + mitigations * [SECURITY.md](SECURITY.md) — vulnerability reporting * [CONTRIBUTING.md](CONTRIBUTING.md) — how to land a PR * [`pre-commit/01-feature-coverage.md`](pre-commit/01-feature-coverage.md) — per-feature live-verification matrix * [`pre-commit/03-security-invariants.md`](pre-commit/03-security-invariants.md) — security invariant ledger ## Non-goals These are stated intent, not checked behaviour. proxxx does **not**: * render graphical SPICE / VNC frames (hands off to `remote-viewer` / system browser) * re-implement `pveum` in Rust (when ground truth lives in pveum, we shell out, parse, and stay out of the way) * manage cluster bring-up (corosync / qdevice mutations are read-only or operator-in-the-loop) * replace the Proxmox web UI (built for the workflows where the web UI is slow, repetitive, or unreachable from a terminal context)