# Common Components

> **Conformance:** L0 / L1 / L2 (see `README.md`)
> **Audience:** Human engineers AND AI coding agents.

> **📦 Post-extraction note:** This document was originally written when VManager was the only consumer and the components lived under `src/vmanager/`. The components are now packaged as **`mgf-common`** and the implementation lives at `src/mgf/common/`. When you see a citation like `src/vmanager/observability/X.py` in the body, the canonical location is now `src/mgf/common/observability/X.py` — VManager re-exports the names for source-compat. The recommended action for new projects has shifted from "copy the template" to **"depend on `mgf-common`"** (lift-and-copy is the fallback when you genuinely need a divergent shape). VManager-specific extensions (`SpecValidationError`, `DomainNotFoundError`, etc.) remain consumer-side examples.

---

## What this document is

This is the **catalogue** of common components currently shipped and supported across the standards. For each component it answers:

1. **What it is** — one paragraph, no jargon.
2. **What it solves** — the concrete problem it removes.
3. **Where it lives in VManager today** — file paths so you can read the code.
4. **Status** — supported / partial / planned, and the conformance level (L0 / L1 / L2) it currently meets.
5. **Public API** — the small surface other code depends on.
6. **How to lift into a new project** — the exact steps to copy it.
7. **Versioning + compatibility notes**.

The goal is that an engineer (or AI agent) starting a new desktop / mobile project can read this one document, lift the listed templates, and have a production-grade foundation in under a day.

The deeper "why" of each component is in the topic doc:
- [`ERROR_HANDLING.md`](ERROR_HANDLING.md)
- [`LOGGING.md`](LOGGING.md)
- [`CONFIGURATION.md`](CONFIGURATION.md)
- [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md)

---

## Table of contents

| Component | Status | Conformance | Section |
|---|---|---|---|
| 1. Typed Exception Hierarchy + Translation | ✅ Supported | L2 | [§1](#1-typed-exception-hierarchy--translation) |
| 2. Central Configuration (`Settings`) | ✅ Supported | L2 (round 3 migrated VManager to pydantic-settings via mgf-common) | [§2](#2-central-configuration-settings) |
| 3. Central Logging | ✅ Supported | L2 (round 2 landed; OTel correlation via `setup_otel`) | [§3](#3-central-logging) |
| 4. Provider ABC pattern | ✅ Supported | L2 | [§4](#4-provider-abc-pattern) |
| 5. Subprocess wrapper (shell) | ✅ Supported | L1 (lift candidate for `mgf.common`) | [§5](#5-subprocess-wrapper) |
| 6. SSH client wrapper | ✅ Supported | L1 (lift candidate for `mgf.common`) | [§6](#6-ssh-client-wrapper) |
| 7. Cancellation + Progress | ✅ Supported | L1 (lift planned; FEEDBACK.md) | [§7](#7-cancellation--progress) |
| 8. GUI Worker pattern (Qt) | ✅ Supported | L2 | [§8](#8-gui-worker-pattern-qt) |
| 9. UI Designer-first loader | ✅ Supported | L1 | [§9](#9-ui-designer-first-loader) |
| 10. Vault (credentials) | ✅ Supported | L1 (consumer-side; lift candidate) | [§10](#10-vault-credentials) |
| 11. Crash Reporter | ✅ Supported | L2 (local + Sentry/OTLP/HTTP sinks) | [§11](#11-crash-reporter) |
| 12. OpenTelemetry / Tracing | ✅ Supported | L2 (full SDK + `operation_span` + auto-instrumentation) | [§12](#12-opentelemetry--tracing) |
| 13. GUI Log Viewer Dialog | ❌ Planned | — (template ready) | [§13](#13-gui-log-viewer-dialog) |
| 14. Sensitive-Field Redactor | ✅ Supported | L2 | [§14](#14-sensitive-field-redactor) |
| 15. Security infrastructure | 📘 Standard ([`SECURITY_STANDARD.md`](SECURITY_STANDARD.md)) | L1+L2 (per the SC-* matrix) | [§15](#15-security-infrastructure) |
| 16. Test infrastructure | 📘 Standard ([`TESTING.md`](TESTING.md)) | L1 (pytest + mocks); L2 (property tests + benchmarks) | [§16](#16-test-infrastructure) |

---

## 1. Typed Exception Hierarchy + Translation

**What it is.** A single shallow class hierarchy with a project-base `<Project>Error`, 7–8 second-level categories (`ConfigError`, `ProviderError`, `OperationError`, `GuestError`, `EnvironmentError`, `VaultError`, `SchedulerError`), and concrete subclasses that carry **machine-readable fields** (not just message strings). Plus a single CLI top-level handler that maps each category to a documented exit code, and a single boundary function (`_translate(...)`) per provider that converts vendor exceptions to typed app exceptions.

**What it solves.**
- Callers can match by **category** (`except OperationError:`) without knowing every concrete subclass.
- Errors carry **structured context** (`CommandError.argv`, `CommandError.stderr`) for precise reporting without re-running.
- The user sees a categorised message AND the program returns a categorised exit code.
- Third-party exceptions never leak past the boundary that owns them.

**Where it lives in VManager.**
- Hierarchy: `src/vmanager/exceptions.py` (8 categories, 25 concrete types).
- CLI handler: `src/vmanager/cli/__main__.py::_run_and_translate_exceptions`.
- Provider translator: `src/vmanager/providers/libvirt_provider.py::_translate`.
- Subprocess translator: `src/vmanager/common/shell.py::run_command`.
- SSH translator: `src/vmanager/common/ssh.py::SSHClient.connect/run`.

**Status.** ✅ Supported, L1. Close to L2 with three documented gaps: (a) some libvirt translations drop `from e`, (b) GUI worker only catches typed exceptions, (c) no top-level catch-all + crash reporter.

**Public API.**
```python
from <app>.exceptions import (
    AppError,                  # base
    ConfigError, ProviderError, OperationError, GuestError,
    EnvironmentError, VaultError, SchedulerError,           # categories
    SchemaValidationError, CommandError,
    ResourceNotFoundError, ResourceAlreadyExistsError,      # concrete (sample)
)
```

**How to lift into a new project.**
1. Read [`ERROR_HANDLING.md`](ERROR_HANDLING.md) §2 for the full mental model.
2. Copy the `exceptions.py` template from `ERROR_HANDLING.md` §2.3 verbatim.
3. Rename `AppError` to `<YourProject>Error`.
4. Add your project's domain-specific subclasses under the correct category (most domain failures fit under `OperationError` or `ProviderError`).
5. Copy the CLI exit-code translation function from `ERROR_HANDLING.md` §4.1 into your `cli/__main__.py`.
6. Wire `sys.excepthook`, `threading.excepthook`, and (for GUI) the Qt handler — see `ERROR_HANDLING.md` §4.

**Versioning + compatibility.**
- Adding a new concrete subclass under an existing category is **non-breaking**.
- Renaming or removing a category is **breaking** — bump the project's MAJOR version.
- Adding a field to a concrete exception's `__init__` is breaking if existing call sites pass positional args; prefer keyword-only args from day one.

---

## 2. Central Configuration (`Settings`)

**What it is.** A single typed object (`pydantic_settings.BaseSettings` going forward; `dataclass` in the current VManager codebase) loaded once at the entry point and passed by constructor injection to every consumer. Layered loading: CLI flags → env vars → `.env` → config file → typed defaults. Schema-versioned with a migration chain. Atomic writes. Lazy path expansion.

**What it solves.**
- One typed object instead of "every module reads its own config." Tests inject `Settings(field=value)`; production loads from disk.
- Typo protection (unknown keys raise).
- Forward / backward-compatible schema evolution.
- Crash mid-write never corrupts state.
- Tests that monkeypatch `HOME` per test get clean isolation.

**Where it lives in VManager.**
- Schema + load/save/migrate: `src/vmanager/config.py::AppConfig` (today; planned migration to `settings.py::Settings`).
- Default path resolver: `src/vmanager/config.py::default_config_path`.
- CLI wiring: `src/vmanager/cli/__main__.py::main` (the top-level click group callback).
- GUI wiring: `src/vmanager/gui/app.py::main` (the Qt entry point).

**Status.** ✅ Supported, L1. Migration to **pydantic-settings** is the priority-P0 gap-closure step for L2 — see [`CONFIGURATION.md`](CONFIGURATION.md) §14.

**Public API.**
```python
from <app>.settings import (
    Settings, load_settings, save_settings, default_config_path,
    CURRENT_SCHEMA_VERSION,
)

# At entry point:
settings = load_settings(config_path=cli_path, overrides={"logging": {"level": "DEBUG"}})

# Pass `settings` (or a sub-model `settings.logging`) to every consumer.
```

**How to lift into a new project.**
1. Read [`CONFIGURATION.md`](CONFIGURATION.md) §3 for the full schema design.
2. Copy the `settings.py` template from `CONFIGURATION.md` §3.1 verbatim.
3. Rename `<app>` → your project, `<APP>_` → your env-var prefix.
4. Define your own field set: keep the standard sub-models (`LoggingSettings`, `StorageSettings`) and add domain-specific ones (e.g., `DatabaseSettings`, `BackendSettings`).
5. Wire `load_settings(...)` from CLI / GUI / service entry points. Stuff the `Settings` into a context (`ctx.obj` for Click) for downstream commands.
6. For each satellite YAML file (rules, schedules, scripts), copy the §4.2 template and rename.

**Versioning + compatibility.**
- Adding a new field with a default is **non-breaking** — older configs load fine (missing key → default).
- Removing or renaming a field is **breaking** — bump `CURRENT_SCHEMA_VERSION`, add a migration step.
- Tightening a validator (e.g., narrowing a range) may break existing valid configs; treat as breaking.

---

## 3. Central Logging

**What it is.** A single setup function (`setup_logging`) called once per entry point. Hierarchical loggers via `logging.getLogger(__name__)`. JSON formatter for non-TTY output (machine-parseable for Loki/ELK/Splunk/Datadog). Text formatter with ANSI colour for TTY. Rotating file sink + console always; OTLP / syslog / journald / HTTP webhook opt-in via env vars. Sensitive-field redactor in front of every sink. OpenTelemetry trace context auto-attached when a span is active.

**What it solves.**
- Operators get human output on the terminal AND structured records in `<state>/logs/<app>.log`.
- A single user action can be reconstructed end-to-end via `trace_id`.
- Secrets cannot leak through logs (redactor catches the obvious cases).
- Headless services log to journald automatically; cloud-shipping is one env var away.

**Where it lives in VManager today.**
- Acquisition: 21 `logging.getLogger("vmanager.*")` calls across the codebase.
- Setup: `src/vmanager/cli/__main__.py` (`logging.basicConfig` call inside the click main).
- Format: `"%(asctime)s %(levelname)s %(name)s: %(message)s"` — plain text, no JSON.
- No file sink, no rotation, no OTel, no redaction, no GUI viewer.

**Status.** 🟡 **Partial.** Naming and per-module logger acquisition are L1-grade. Everything else is L0. The full L2 templates (JsonFormatter, TextFormatter, Redactor, OTel setup, log viewer dialog) are ready in [`LOGGING.md`](LOGGING.md) — bringing VManager to L2 is the gap-closure plan there.

**Public API (target — what `LOGGING.md` ships).**
```python
from <app>.observability.logging_setup import setup_logging
from <app>.observability.otel_setup import setup_otel, operation_span

# At entry point:
setup_logging(level=settings.logging.level, fmt=settings.logging.format)
setup_otel(enabled=settings.logging.enable_otlp)

# In any module:
import logging
LOG = logging.getLogger(__name__)
LOG.info("operation start", extra={"operation": "vm.create", "vm_name": "x"})
LOG.exception("operation failed") # auto-includes traceback

# Wrapping an operation:
with operation_span("vm.create", vm_name="x"):
    handle = build_and_run(spec, provider)
```

**How to lift into a new project.**
1. Read [`LOGGING.md`](LOGGING.md) Rules box + §5 (setup) + §7 (correlation).
2. Copy `logging_setup.py`, `json_formatter.py`, `text_formatter.py`, `redaction.py`, `otel_setup.py` from `LOGGING.md` §5–§7 verbatim into `src/<app>/observability/`.
3. Rename `<app>` and `<APP>` placeholders.
4. Call `setup_logging(...)` from your CLI `main`, GUI `app.run`, and any headless entry point. Optionally call `setup_otel(...)` after.
5. In every module, do `LOG = logging.getLogger(__name__)` — never bare `logging.info(...)`.
6. Wrap every long-running use case in `with operation_span("<subject>.<verb>"):`.
7. (GUI only) Copy the log-viewer dialog template from `LOGGING.md` §9 and wire it into a Help → View Logs menu item.

**Versioning + compatibility.**
- The standard formatter's JSON shape is the public contract. Top-level keys (`ts`, `level`, `logger`, `msg`, `trace_id`, `span_id`) MUST NOT be renamed without a MAJOR bump.
- Adding new top-level keys or new `extra={}` fields is non-breaking.
- The redactor's default key set is conservative (well-known secret-bearing names). Adding to it is non-breaking; removing from it is breaking.

---

## 4. Provider ABC pattern

**What it is.** Every third-party I/O dependency (hypervisor, DB, cloud SDK, hardware driver, ML model) sits behind an abstract base class with at least **two implementations** — production and mock. The mock supports a `FailurePlan` for deterministic failure injection.

**What it solves.**
- Every test runs without the real third-party daemon.
- Switching backends is a new class, not a refactor.
- Failure paths can be exhaustively tested via the mock's failure injection.

**Where it lives in VManager.**
- ABC: `src/vmanager/providers/base.py::VMProvider`.
- Production: `src/vmanager/providers/libvirt_provider.py::LibvirtProvider` (the **only** file that imports `libvirt`).
- Mock: `src/vmanager/providers/mock_provider.py::MockProvider` (in-memory, deterministic, ships `seed_*` and `FailurePlan` helpers).
- Lock-in test: `tests/unit/test_provider_base.py::EXPECTED_METHODS` (frozenset) — adding/removing methods to the ABC breaks the test deliberately, forcing both impls to update in the same commit.

**Status.** ✅ Supported, L2.

**Public API.**
```python
from <app>.providers.base import ResourceProvider
from <app>.providers.mock_provider import MockProvider, FailurePlan

# Inject in production:
provider: ResourceProvider = LibvirtProvider(uri=settings.backend_uri)

# Inject in tests:
provider = MockProvider()
provider.failure_plan.inject(method="get", exc=ResourceNotFoundError("x"), match_name="x")
```

**How to lift into a new project.**
1. Read [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §1.2.
2. Identify the third-party dependency you want to abstract.
3. Create `src/<app>/providers/base.py` with an `ABC` listing every method you need.
4. Implement `<vendor>_provider.py` — the **only** file that imports the vendor SDK. Add a `_translate()` function that maps vendor exceptions to your typed hierarchy.
5. Implement `mock_provider.py` — in-memory, with `seed_*` helpers and a `FailurePlan` class.
6. Add a `tests/unit/test_provider_base.py::EXPECTED_METHODS` lock-in test.
7. Add a CI lint that the vendor SDK is imported only inside `<vendor>_provider.py`.

**Versioning.** Adding a new abstract method is breaking — both implementations must update in the same commit. Removing or renaming is also breaking. Locked in by the EXPECTED_METHODS test.

---

## 5. Subprocess wrapper

**What it is.** A single function (`run_command(argv, *, timeout, check, capture)`) that wraps `subprocess.run`, takes argv as a list (never `shell=True`), and translates non-zero exit / timeout / OSError into a typed `CommandError(argv, returncode, stderr)`.

**What it solves.**
- One place to enforce no-`shell=True` (security).
- One place to translate subprocess errors uniformly.
- Cancellation hook for long-running streaming ops.
- Test seam: tests inject a fake `run_fn` instead of spawning real processes.

**Where it lives in VManager.**
- `src/vmanager/common/shell.py::run_command`.

**Status.** ✅ Supported, L1. Could grow OTel span instrumentation for L2 (template in `LOGGING.md` §7.2).

**Public API.**
```python
from <app>.common.shell import run_command
from <app>.exceptions import CommandError

try:
    result = run_command(["qemu-img", "info", str(disk)], timeout=10)
except CommandError as e:
    raise OperationError(f"could not read disk info: {e}") from e
```

**How to lift.** Copy `src/vmanager/common/shell.py` into `src/<app>/common/shell.py`. Rename the exception import. Add a CI lint that `import subprocess` appears nowhere else in core.

---

## 6. SSH client wrapper

**What it is.** A small `SSHClient` class wrapping `paramiko.SSHClient`. Validates that `host` is a literal IP (no DNS — DNS is the caller's problem). Translates `paramiko.AuthenticationException` → `GuestUnreachableError`, `SSHException`/`OSError` → `GuestUnreachableError`, command exit non-zero → `GuestCommandError`. Optional `SSHPool` for connection reuse keyed on `(host, port, username, auth_digest)`.

**What it solves.**
- One typed-exception layer over paramiko.
- Connection pooling without leaking paramiko types into core.
- Strong test seam: every test passes a fake factory that records calls.

**Where it lives in VManager.**
- `src/vmanager/common/ssh.py::SSHClient`, `SSHPool`.

**Status.** ✅ Supported, L1.

**Public API.**
```python
from <app>.common.ssh import SSHClient, SSHPool
from <app>.vault import Credential

client = SSHClient(host="10.0.0.5", credential=cred, timeout=30)
client.connect()
result = client.run("uptime", check=True)
```

**How to lift.** Copy `src/vmanager/common/ssh.py` into your project. Rename exception imports. If you don't need pooling, drop the `SSHPool` class.

---

## 7. Cancellation + Progress

**What it is.** Two small classes:
- `CancellationToken` — a thread-safe boolean flag. Callers check `is_cancelled()` in their inner loops; cancellers call `cancel()`.
- `ProgressReporter` — a protocol with `report(message: str, current: int = 0, total: int = 0)` and `is_cancelled() -> bool`. Long-running ops accept it as a parameter; production passes a stdout / log adapter, GUI passes a `_QtProgressBridge`, tests pass a `RecordingReporter` that captures every call.

**What it solves.**
- Cancellation is **explicit** — no thread interruption, no global flag.
- Progress UX is decoupled from operation logic — the same `download_image()` function powers a CLI progress bar, a Qt progress dialog, and a unit test recorder.

**Where it lives in VManager.**
- `src/vmanager/common/progress.py::ProgressReporter`, `CancellationToken`, `RecordingReporter`.
- Qt bridge: `src/vmanager/gui/workers/core_worker.py::_QtProgressBridge`.

**Status.** ✅ Supported, L1.

**Public API.**
```python
from <app>.common.progress import ProgressReporter, CancellationToken, RecordingReporter

def long_op(target, *, reporter: ProgressReporter) -> Result:
    for i, item in enumerate(target.items):
        if reporter.is_cancelled():
            raise OperationError("cancelled by user")
        do_thing(item)
        reporter.report(f"processed {i+1}/{len(target.items)}",
                        current=i+1, total=len(target.items))
    return Result(...)
```

**How to lift.** Copy `src/vmanager/common/progress.py`. Rename exception imports if any.

---

## 8. GUI Worker pattern (Qt)

**What it is.** A `CoreWorker(QThread)` base class for one-shot blocking operations from the UI. Owns a `CancellationToken`. Emits three signals: `progress(str)`, `finished_with_result(object)`, `error(str)`. Subclasses override `do_work()` and call exactly one core function. Long-lived workers that need an event loop use `QObject` + `moveToThread` instead.

**What it solves.**
- The UI never blocks during background work.
- One pattern to teach: every worker looks identical to the next.
- Cancellation is wired through the `CancellationToken` to the underlying op.
- Cross-thread results delivered via auto-queued signals — no manual `QMetaObject.invokeMethod`.

**Where it lives in VManager.**
- Base: `src/vmanager/gui/workers/core_worker.py::CoreWorker`.
- Concrete examples: `lifecycle_workers.py`, `snapshot_workers.py`, `vm_create_worker.py`, `image_download_worker.py`, `lab_workers.py`, `ova_workers.py` (one-shot); `metrics_worker.py` (long-lived QObject pattern); `monitor_worker.py` (lives on main thread, see comment in file).

**Status.** ✅ Supported, L1. **One documented gap:** `CoreWorker.run()` only catches `VManagerError`, not bare `Exception` — see [`ERROR_HANDLING.md`](ERROR_HANDLING.md) §11 P0 item 2.

**Public API.**
```python
from <app>.gui.workers.core_worker import CoreWorker

class MyWorker(CoreWorker):
    def __init__(self, arg) -> None:
        super().__init__()
        self._arg = arg

    def do_work(self) -> Any:
        return some_core_function(self._arg, reporter=self.reporter)

# In a dialog:
worker = MyWorker(arg)
worker.progress.connect(self._on_progress)
worker.finished_with_result.connect(self._on_done)
worker.error.connect(self._on_error)
worker.start()
```

**How to lift.**
1. Read [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §2.1.
2. Copy `src/vmanager/gui/workers/core_worker.py` into your project.
3. **Apply the L1 patch immediately**: change the `except VManagerError` to also catch `Exception` and route untyped exceptions through the crash reporter (template in `ERROR_HANDLING.md` §5).
4. Subclass per blocking operation. The body of `do_work()` should be a single function call into core.
5. Long-lived workers (timer-driven, event-loop-needing): use the `QObject` + `moveToThread` pattern from `metrics_worker.py`. Read VManager `docs/claude/CLAUDE.md` critical facts on QThread teardown — that is the **only** reliable shutdown sequence for a `QObject`-on-`QThread` worker.

**Versioning.** Signal names are public contract — `finished_with_result`, `error`, `progress` MUST NOT be renamed (a project-wide signal name collision is a debugging nightmare).

---

## 9. UI Designer-first loader

**What it is.** Every dialog / window / widget is designed in Qt Designer (`.ui` XML). Loaded at runtime via `QUiLoader` resolved through `importlib.resources` (works from source tree AND installed wheel). A controller class wraps the loaded widget — never subclasses `QWidget` directly. Errors during load (missing `.ui`, missing child) raise a typed `GuiLoadError(AppError)`.

**What it solves.**
- Designers (or future engineers) can edit the UI without re-reading every controller.
- Layouts are visible in a tool, diff-able in version control.
- One reliable lookup pattern (`importlib.resources`) that works in editable installs and in zip wheels.

**Where it lives in VManager.**
- Loader: `src/vmanager/gui/ui_loader.py::load_ui, require_child, GuiLoadError`.
- `.ui` files: `src/vmanager/gui/ui/`.

**Status.** ✅ Supported, L1.

**Public API.**
```python
from <app>.gui.ui_loader import load_ui, require_child
from PySide6.QtWidgets import QPushButton

class MyDialog:
    def __init__(self) -> None:
        self.dialog = load_ui("my_dialog.ui")  # resolved via importlib.resources
        self.ok_button: QPushButton = require_child(self.dialog, "okButton", QPushButton)
        self.ok_button.clicked.connect(self._on_ok)
```

**How to lift.** Copy `src/vmanager/gui/ui_loader.py`. Add an empty `__init__.py` next to `gui/ui/` so `importlib.resources.files("<app>.gui.ui")` resolves it as a package.

---

## 10. Vault (credentials)

**What it is.** A `Vault` ABC with two implementations: `SystemKeyringVault` (libsecret on Linux / Keychain on macOS / Credential Manager on Windows via `keyring` or `secretstorage`) and `EncryptedFileVault` (Fernet AES-128 fallback). Backend selected at construction time via an injectable factory; on factory failure, transparently switches to file backend. Backend is queryable (`vault.backend`).

**What it solves.**
- Secrets never live in `config.yaml`, version control, or log dumps.
- The same code works on systems with and without a keyring daemon.
- The active backend is observable (rule **DP-10** no silent fallback).

**Where it lives in VManager.**
- `src/vmanager/vault/__init__.py`, `keyring.py`, `models.py`.

**Status.** ✅ Supported, L1. Documented gap: backend choice not logged at startup (see [`CONFIGURATION.md`](CONFIGURATION.md) §14 P1 item 8).

**Public API.**
```python
from <app>.vault import CredentialVault, Credential

vault = CredentialVault() # picks keyring → file fallback
vault.set("vm:prod-1", Credential(username="admin", secret="..."))
cred = vault.get("vm:prod-1")
print(vault.backend) # "keyring" | "file"
```

**How to lift.** Copy the `vault/` package. Add `cryptography` and (Linux only) `secretstorage` to deps.

---

## 11. Crash Reporter

**What it is.** A small module that accepts an exception, builds a structured JSON crash report (timestamp, exception type + message + traceback, cause chain, platform info, app version), writes it atomically to `<state>/crashes/`, and best-effort ships it to a configured remote sink (Sentry, OTLP, HTTP webhook).

**What it solves.**
- Untyped errors don't vanish — they're persisted locally and (optionally) forwarded.
- Users can attach a single JSON file to a bug report instead of trying to copy a terminal traceback.
- The remote sink is opt-in via env var — no default network calls.

**Where it lives in VManager.** ✅ `src/vmanager/observability/crash_reporter.py` — landed round 2 (2026-04-21).

**Status.** Supported, L1.

**Public API (target).**
```python
from <app>.observability.crash_reporter import report_now

try:
    risky_thing()
except Exception as e:
    path = report_now(e, source="cli")
    click.echo(f"crashed; report at {path}", err=True)
```

**How to lift.** Copy the template from `ERROR_HANDLING.md` §6.1 into `src/<app>/observability/crash_reporter.py`. Rename `<app>` / `<APP>`. Add the `report_now` call at every top-level handler (CLI catch-all, `sys.excepthook`, `threading.excepthook`, GUI worker untyped catch, asyncio handler).

---

## 12. OpenTelemetry / Tracing

**What it is.** OpenTelemetry SDK initialization with OTLP exporter (configurable via standard `OTEL_EXPORTER_OTLP_ENDPOINT` env var) and an `operation_span(name, **attrs)` context manager that wraps L2 use cases. When OTel is active, every log record auto-carries `trace_id` + `span_id` (the JSON formatter does this). When OTel is **not** initialised, `operation_span` is a no-op — code wrapped in it always works.

**What it solves.**
- Strong correlation across logs, spans, and metrics (Tier C).
- One-line wrap to instrument an operation.
- Zero cost when disabled.
- Out-of-the-box compatibility with Jaeger, Tempo, Honeycomb, Datadog, AWS X-Ray.

**Where it lives in VManager.** 🟡 Skeleton landed round 2 at `src/vmanager/observability/otel_setup.py` — `setup_otel`, `operation_span`, `build_otlp_log_handler` all present and tested (no-op when SDK absent). Per-operation span wrapping across the codebase is round 4.

**Status.** Skeleton ready for use; full instrumentation is round 4.

**Public API (target).**
```python
from <app>.observability.otel_setup import setup_otel, operation_span

# At entry point:
setup_otel(enabled=settings.logging.enable_otlp)

# Wrapping an operation:
with operation_span("vm.create", vm_name=spec.name):
    handle = build_and_run(spec, provider)
```

**How to lift.** Copy the template from `LOGGING.md` §7.2. Add `opentelemetry-sdk` and `opentelemetry-exporter-otlp-proto-http` to an optional `[observability]` extra in `pyproject.toml`. Wrap each L2 use case in `operation_span(...)`.

---

## 13. GUI Log Viewer Dialog

**What it is.** A QDialog (designed in `log_viewer.ui`) that reads the rotating-file log sink, parses JSON records, and renders them in a `QPlainTextEdit` with level filter, search, copy, and tail-mode (5-second poll for new lines). Help → View Logs menu item.

**What it solves.**
- Users without a terminal can see what the app is doing.
- Bug reports include the log dump (Copy button → clipboard).
- Live tail during a long operation.

**Where it lives in VManager.** ❌ Not yet — template ready in [`LOGGING.md`](LOGGING.md) §9.1.

**Status.** Planned. Priority P2 in the gap-closure plan.

**Public API (target).**
```python
from <app>.gui.dialogs.log_viewer_dialog import LogViewerDialog

# In main window menu handler:
def _on_view_logs(self) -> None:
    dlg = LogViewerDialog(log_file=self._settings.logging.file_path)
    dlg.dialog.exec()
```

**How to lift.** Copy the template from `LOGGING.md` §9.1 into `src/<app>/gui/dialogs/log_viewer_dialog.py`. Create the matching `gui/ui/log_viewer.ui` in Qt Designer.

---

## 14. Sensitive-Field Redactor

**What it is.** A `Redactor` class that walks a log record's structured payload (the dict that `JsonFormatter` produces, or the extras dict for `TextFormatter`) and replaces values whose **key** matches a known secret-bearing name (`password`, `token`, `secret`, `authorization`, etc.) OR whose **value** matches a known pattern (`Bearer ...`, JWTs, PEM blocks). Configurable extra keys + regexes per project.

**What it solves.**
- Secrets caught accidentally in `extra={}` or in interpolated message strings don't leak through logs.
- One central policy — no per-call-site decisions about what to redact.

**Where it lives in VManager.** ✅ `src/vmanager/observability/redaction.py` — landed round 2.

**Status.** Supported, L1.

**Public API (target).**
```python
from <app>.observability.redaction import Redactor

# In setup_logging:
redactor = Redactor.default() # or Redactor(extra_keys=frozenset({"my_proprietary_key"}))
formatter = JsonFormatter(redactor=redactor)
```

**How to lift.** Copy the template from `LOGGING.md` §6.2.

---

## 15. Security infrastructure

**What it is.** The security standard ([`SECURITY_STANDARD.md`](SECURITY_STANDARD.md))
plus the runtime pieces it relies on: the **Vault** boundary
(component §10), the **Redactor** (component §14), the **subprocess
wrapper** (component §5), the **atomic-write** pattern (rule
**CF-04**), and the **traceback-scrubbing** convention at translation
seams (rule **EH-02** + **SC-11**).

**What it solves.** A single document tells human engineers and AI
agents what "secure code" means in this family of projects, and
points each rule at the concrete component that implements it.
Without it, security is sprinkled across five other docs and no one
sees the whole picture.

**Where it lives.** [`SECURITY_STANDARD.md`](SECURITY_STANDARD.md) — eighteen MUSTs
(SC-01..SC-18), a threat-model template, an enforcement matrix
mapping every MUST to a mechanical check (lint / mypy / test pin /
CI step).

**Status.** ✅ Standard published in v2.0 of the standards.
Implementation pieces are catalogued in their own component
sections; SECURITY_STANDARD.md is the index + the rule set + the threat
model template.

**Public API.** None — it's a standard, not a library. The runtime
pieces are imported as listed in their own component sections.

**How to lift into a new project.**
1. Read [`SECURITY_STANDARD.md`](SECURITY_STANDARD.md) Rules box top-to-bottom.
2. Copy the repository-root `SECURITY.md` template (vulnerability
   reporting policy) from §11.1 into your project root.
3. Adopt the mechanical checks from §15: `pip-audit` step in CI,
   `gitleaks` pre-commit, the SC-* pin tests under
   `tests/unit/security/`.
4. Customise the §12 threat model template to your project's
   actual trust boundaries (see VManager's threat model for a
   worked example).

**Versioning.** New MUSTs MAJOR-bump the standards. Mechanical-check
additions are MINOR (existing code stays compliant; new code uses
the new check).

---

## 16. Test infrastructure

**What it is.** The testing standard ([`TESTING.md`](TESTING.md))
consolidating patterns previously scattered across DESIGN_PRINCIPLES
(rule DP-05), LOGGING (§11 pitfalls), ERROR_HANDLING (§5.1, §9), and
CONFIGURATION (§11). Eighteen MUSTs (TS-01..TS-18) covering test
layout, fixture hierarchy, mock-provider pattern, property tests,
GUI testing, async testing, performance benchmarks, coverage, and
test isolation.

**What it solves.** A single home for testing rules so consumers
don't re-discover the load-bearing patterns (RotatingFileHandler
close discipline, session-scoped TracerProvider, HOME monkeypatching
isolation, `qtbot.addWidget` pitfall, pytest stderr-capture in
benchmarks).

**Where it lives.** [`TESTING.md`](TESTING.md). The patterns
themselves are exercised in `mgf-common`'s and VManager's existing
test suites — the doc is the codification.

**Status.** ✅ Standard published in v2.0 of the standards.

**Public API.** None — it's a standard. The runtime pieces it relies
on (`pytest`, `pytest-qt`, `pytest-asyncio`, `pytest-randomly`,
`hypothesis`) are added to a project's `[dev]` extra.

**How to lift into a new project.**
1. Read [`TESTING.md`](TESTING.md) Rules box.
2. Adopt `[tool.pytest.ini_options]` settings from §9.1 +
   `filterwarnings = ["error"]` (TS-04) and strict markers (TS-05).
3. Mirror `tests/` to `src/` 1:1 (TS-01); create the
   `tests/test_mirror.py` lint script.
4. Add a `tests/unit/test_default_path_resolution.py` consolidated
   pin file for every `_default_X()` helper (TS-03 / CF-04).
5. Adopt `pytest-randomly` so test-order dependencies surface (TS-03).
6. Wire the `pip-audit` + `gitleaks` security gates from
   [`SECURITY_STANDARD.md`](SECURITY_STANDARD.md) §15.
7. Set the coverage gate at 80% (project-wide) + 90% (changed
   lines in PR).

**Versioning.** New MUSTs MAJOR-bump. Per-test-pattern templates
(e.g., a new fixture shape) are MINOR.

---

## How to bootstrap a new project from this catalogue

The recommended order for a fresh desktop / mobile project:

| Step | What | From |
|---|---|---|
| 1 | Create the project skeleton (`src/<app>/`, `tests/unit/`, `pyproject.toml`) | — |
| 2 | Add `mgf-common` to dependencies (`pip install mgf-common[observability]`) | [`mgf-common`](https://pypi.org/project/mgf-common/) |
| 3 | Wire `mgf.common.configure(app_name=..., app_version=...)` into every entry point | `mgf-common` README |
| 4 | Subclass `AppError` for project-specific exceptions (§1) | `ERROR_HANDLING.md` §2 |
| 5 | Subclass `BaseAppSettings` via `make_settings_config(env_prefix=...)` (§2) | `CONFIGURATION.md` §3 |
| 6 | Call `setup_logging(...)` and `setup_otel(...)` from each entry point (§3 + §12 + §14) | `LOGGING.md` §5 + §7 |
| 7 | Wire `install_excepthook()`, `install_threading_excepthook()`; wrap each use case in `operation_span(...)` (§11 + §12) | `ERROR_HANDLING.md` §4 + §6 |
| 8 | Adopt the testing standard: pytest config + mirror + isolation (§16) | `TESTING.md` Rules box |
| 9 | Adopt the security standard: `gitleaks` pre-commit + `pip-audit` CI + `tests/unit/security/` pins + repo-root `SECURITY.md` (vuln-reporting policy per SC-12) | `SECURITY_STANDARD.md` Rules box + §15 |
| 10 | If you have a third-party I/O dep: build the **provider ABC** (§4) | `DESIGN_PRINCIPLES.md` §1.2 |
| 11 | If you need shell / SSH: lift §5 / §6 from VManager (or wait for the `mgf.common` lift if your timeline allows) | `src/vmanager/common/` |
| 12 | If you handle credentials: lift the **vault** (§10) | `src/vmanager/vault/` |
| 13 | If GUI: lift the **UI loader** (§9), **CoreWorker** base (§8), **log viewer dialog** (§13), Qt excepthook | `LOGGING.md` §9, `ERROR_HANDLING.md` §4.5 |

After step 9 you have an L1-conformant skeleton with all
cross-cutting standards (security + testing + logging + config +
exceptions) in place. After step 13 you have L2 for a full
desktop application.

---

## What you do NOT lift

These belong to VManager specifically and SHOULD NOT be copied verbatim:
- `src/vmanager/configurator/` — libvirt-domain-XML manipulation; domain-specific.
- `src/vmanager/creator/`, `src/vmanager/lifecycle/`, `src/vmanager/operations/` — VM use cases; domain-specific.
- `src/vmanager/scheduler/`, `src/vmanager/hooks/`, `src/vmanager/monitor/` — built on the common components but the *features* are VManager's.
- `src/vmanager/lab/`, `src/vmanager/interchange/`, `src/vmanager/network/`, `src/vmanager/storage/`, `src/vmanager/vault/keyring.py` (the keyring **integration** can be reused; the specific secret schema cannot).

The pattern: **abstractions and infrastructure ARE shared; concrete domain code is NOT.**

---

## Maintaining compliance over time (cornerstone rhythm)

Adopting the standards once is the easy part. Keeping the consumer
in-sync as it grows and as `mgf-common` evolves is the harder
long-term game. VManager (the reference consumer) landed on a
rhythm that has shipped three rounds of closures over a single day
with zero regressions; future consumers SHOULD adopt the same
rhythm.

### The compliance document

Every consumer SHOULD carry a `docs/STANDARDS_COMPLIANCE.md` (or
equivalent name). It is a **rolling**, not a **one-shot**,
document. Its job is:

1. **The compliance table** — one row per MUST and relevant SHOULD,
   status (✅ / 🟠 / ❌), one-line evidence citing file + line +
   pinning test. This is the audit-ready snapshot.
2. **The open-gaps section** — live list of known violations, each
   with severity, impact, close criteria, effort estimate. When
   empty, the project is considered compliant at the audited level.
3. **The closed-gaps history** — every closed gap row, with the
   commit SHA that closed it and the test(s) that pin it. This is
   how future sessions (human or AI agent) know what has already
   been considered and decided.
4. **The observations section** — findings deliberately NOT filed
   as gaps. "Considered and deferred" is the third state between
   "open" and "closed". Writing these down prevents the same
   observation from coming up every audit and being freshly
   re-evaluated.

### The audit → close → audit rhythm

Run in rounds:

1. **Audit.** Spawn parallel Explore agents (or do it by hand) to
   scan the code against each MUST. Replace spot-checks with
   programmatic greps — `grep -rn "LOG\.error(f" src/` is cheaper
   than reading every file, and the evidence is reproducible.
2. **Triage.** Classify every finding: open gap, observation, or
   false positive. Write a one-sentence reason for each. The
   false positives still go into the compliance doc (as closed
   rows with the rationale) so they don't re-fire next audit.
3. **Close.** One gap at a time, or bundle housekeeping items
   with a real gap closure into a single "compliance sweep"
   commit. The sweep pattern keeps trivial doc churn from
   accumulating indefinitely between real engineering commits.
4. **Repeat.** After the last gap closes, re-audit. A deep audit
   almost always surfaces something the previous one missed; the
   round-by-round pattern is what drives the compliance surface
   toward actual zero rather than pretend zero.

Each round's commit message SHOULD cite the compliance doc rows
it closes (by ID) so the git history is reconstructable from
blame alone.

### The "compliance sweep" commit pattern

Bundle in one commit:
- One real gap closure (the main story).
- Any stale-reference housekeeping that touches the same
  subject (e.g., doc refs to a retired file, renamed contract,
  stale test counts).

Don't bundle unrelated housekeeping from different subjects. The
commit's value is in its narrative: *"we closed gap X, and while
we were there, we also fixed the three doc references to the
now-retired Y"*. That narrative fits in one git blame click; a
grab-bag of unrelated items doesn't.

### Observations vs. gaps

Some findings aren't worth acting on immediately: cosmetic churn
that would touch hundreds of files for marginal compliance value,
convention calls where both shapes coexist safely, aspirational
MUSTs with no mechanical enforcement path. **Record them
explicitly** under an "Observations" subsection:

```markdown
### Observations (noted, not filed as gaps)

- **`from __future__ import annotations` missing in ~119 files.**
  Convention calls for it; codebase works without it. A dedicated
  sweep PR is not planned; future PRs SHOULD add the import when
  they touch one of those files anyway.

- **Test-mirror inconsistency (DP-05).** 10 modules use flat
  `tests/unit/test_<module>.py` files rather than
  `tests/unit/<module>/` directories. Both shapes coexist. Not
  filed — convention call.
```

Next audit reads these first and skips re-evaluating them, saving
an entire round's worth of cycles.

### Consumer-side guards

When the standards code lives upstream (in `mgf-common`), the
consumer no longer owns the implementation. Two rules follow:

- **Keep a thin consumer-side test per upstream MUST.** It catches
  pin regressions (a future `mgf-common` release that silently
  violates its own rule) *and* consumer-side extensions (a new
  redactor field, a domain-specific formatter, a project-local
  handler) that the upstream benchmarks don't see. See
  `LOGGING.md` §11.2 for the canonical shape.
- **Consolidate cross-cutting pins in one file.** Rules that
  apply to many modules (CF-04 lazy-path expansion, EH-09
  actionable hints, etc.) MUST have their pins in a single named
  file, not scattered per-subsystem. See `CONFIGURATION.md` §10.1.

Both patterns survive the consumer/library extraction boundary:
when a new component lifts from the consumer into `mgf-common`,
the consumer-side pin stays put as a regression guard.

### The single-session audit pattern (AI agents)

VManager's three rounds were driven by three Explore agent runs
spanning a single session. The pattern:

1. **One agent per audit dimension.** Parallel Explore agents
   (programmatic rule scan, upstream state check, subtle-surface
   audit) return in comparable time; the main session synthesises
   the three reports into one gap list.
2. **Verify before filing.** An agent can flag a false positive
   (our Round 3 LG-05 in Qt's message handler was one) — the
   main session reads the actual file before filing a gap to
   prevent the audit doc from getting noisy.
3. **Close in the same session.** Each round is "audit → close
   → commit → push" within one session. A gap that waits a week
   drifts out of context and takes twice as long to close.

---

## Compatibility & versioning of the standards

Each component above carries an implicit "since" version of the cornerstone. The current cornerstone version is **v1.0** (see top of this doc).

When this catalogue introduces a new component:
- It MUST appear in the table of contents above.
- It MUST have a status (✅ supported / 🟡 partial / ❌ planned).
- It MUST cite where its template lives (a topic doc section, or `src/vmanager/...`).
- It MUST appear with a brief entry in the next minor revision of this doc.

When a component changes its public API:
- A non-breaking field/method addition: PATCH to the topic doc.
- A breaking rename or signature change: MINOR/MAJOR per the standard's semver rules in `README.md`.

---

## Open invitations (when adding the next component)

If you find yourself building one of these in a new project, **add it to this catalogue** rather than letting it stay project-local:

| Possible component | Why useful |
|---|---|
| **Feature-flag service** | Toggle features per build / per user without code change. Trivial in stdlib (`tomllib` + a `Flags` dataclass). |
| **Async task queue (in-process)** | For desktop apps that need a small "do this in the background" with retries, persistence to disk, and cancellation. |
| **Plugin loader** | `importlib.metadata.entry_points`-based registry for user-installed extensions. |
| **Update checker** | Polls a release feed; surfaces "new version available" in the UI. Honours opt-out. |
| **Telemetry opt-in dialog** | First-run dialog explaining what telemetry is sent + an opt-in checkbox. Required by EU privacy law for any non-anonymous reporting. |
| **Background scheduler** | The systemd-timer + tick-state pattern from VManager's `scheduler/` is reusable as-is for any "run periodic tasks even when the app is closed" need. |

---

## Cross-references

- Conformance keywords (MUST / SHOULD / MAY) and version policy: [`README.md`](README.md).
- Layering, replaceability, testability: [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md).
- Full hierarchy + handler / reporter design: [`ERROR_HANDLING.md`](ERROR_HANDLING.md).
- Full logging stack design: [`LOGGING.md`](LOGGING.md).
- Full configuration design: [`CONFIGURATION.md`](CONFIGURATION.md).

---

*End of `COMMON_COMPONENTS.md`.*
