# Common Components

> **Conformance:** L1 / L2 / L3 (see `README.md`)
> **Audience:** Human engineers AND AI coding agents.
> **Propagation (DOC-18):** changing a rule here ripples — walk DOCUMENTATION.md §10.4 and update its dependents in the same commit (README *Index of MUSTs*, the `mgf-fed standards` / `agents` CLI, and any parallel copies / pins the matrix names).

> **📦 Note.** The canonical implementation for each component below lives in **`mgf-common`** at `src/mgf/common/`. The recommended action for new projects is **"depend on `mgf-common`"**; lift-and-copy is the fallback when you genuinely need a divergent shape.

---

## 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 `mgf-common`** — the canonical implementation.
4. **Status** — supported / partial / planned, and the conformance level (L1 / L2 / L3) 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 (pydantic-settings shape ships in `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) |
| 17. Atomic-write helper (`atomic_write`) | ✅ Supported | L2 (`mgf.common._io.atomic_write`) | [§17](#17-atomic-write-helper) |

---

## 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`, `HostEnvironmentError`, `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 `mgf-common`.** Base `AppError` and category
classes at `src/mgf/common/exceptions/_base.py` +
`_well_known.py`. CLI exit-code map at
`src/mgf/common/cli/_exit_codes.py`. Top-level handler at
`src/mgf/common/cli/_handler.py`.

**Status.** ✅ Supported, L2. The full template (subclassing
`AppError`, the `_translate` boundary helper, the CLI
exit-code map) is documented in [`ERROR_HANDLING.md`](ERROR_HANDLING.md) §2.

**Public API.**
```python
from <app>.exceptions import (
    AppError,                  # base
    ConfigError, ProviderError, OperationError, GuestError,
    HostEnvironmentError, 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`) 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 `mgf-common`.** Base settings shape at
`src/mgf/common/config/_settings.py`. Loader (env / file /
defaults precedence) at `src/mgf/common/config/_loader.py`.
Platform path resolver at `src/mgf/common/config/_paths.py`.

**Status.** ✅ Supported, L2. Canonical pydantic-settings shape
is documented at [`CONFIGURATION.md`](CONFIGURATION.md) §3.

**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.

**Rules this primitive discharges.** Subclassing
`mgf.common.config.BaseAppSettings` (the canonical CF-01 shape)
satisfies **CF-02 / CF-03 / CF-06 / CF-08 conformant-for-free** —
you inherit layered precedence, the `version` field, unknown-key
rejection, and the load-once wiring without writing any of them.
The per-rule breakdown (what the base provides, where to look) is
canonical at [`CONFIGURATION.md`](CONFIGURATION.md) §0b; this entry
just flags that "depend on `mgf-common`" is the action that
discharges them, so an audit row can land `✅ conformant` directly
instead of `⏳ no-surface-yet`.

**Implicit footprint (for partial adopters).** Taking the base
also takes its defaults: `extra="forbid"` (a typo in the YAML is a
hard `ValidationError`, not a silent ignore) and `version: int = 1`
(a config written by a newer build fails loud on an older one). If
you lift the shape by hand instead of depending on `mgf-common`,
you own those four rules yourself — the audit row flips from
"inherited" to "explicit decision," per §0b's override discipline.

---

## 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 `mgf-common`.** `setup_logging` at
`src/mgf/common/observability/logging_setup.py`; JsonFormatter
at `json_formatter.py`; Redactor at `redaction.py`; OTel setup
at `otel_setup.py`; excepthook installers at `excepthook.py`.

**Status.** ✅ Supported, L2. The standards doc at
[`LOGGING.md`](LOGGING.md) is the *contract*; the code at
`src/mgf/common/observability/` is the *implementation*.
Consumers depend on `mgf-common` rather than copying templates.

**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": "user.create", "user_id": "x"})
LOG.exception("operation failed") # auto-includes traceback

# Wrapping an operation:
with operation_span("user.create", user_id="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 `mgf-common`.** This pattern is documented
at [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §1.2; the ABC + mock split is a
project-side template rather than a shipped class — each
consumer owns its own provider hierarchy. The pattern is
exercised in `mgf-common`'s own internal seams (e.g. the
plugin loader, the doctor-mode dispatcher).

**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.

**Status.** ✅ Supported, L1 (per-project template — each
consumer owns its own subprocess seam). 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(["ffprobe", str(path)], timeout=10)
except CommandError as e:
    raise OperationError(f"could not read file info: {e}") from e
```

**Where it lives.** This is a per-project template (each
consumer owns its own subprocess seam). Reference shape lives
inline in [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §1.2. 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.

**Status.** ✅ Supported, L1 (per-project template). The
typed-translation-seam discipline mirrors §1 (Typed Exception
Hierarchy).

**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.** This is a per-project template (each consumer
owns its own SSH seam). The shape is documented in
[`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §1.2. 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.

**Status.** ✅ Supported, L1 (per-project template). The
`RecordingReporter` test helper is the canonical recording
shape; GUIs wire a `_QtProgressBridge` from the worker
infrastructure (§8).

**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.** Per-project template. Implementation is small
(~80 lines for both classes + the recording test helper);
copy from the shape in this section's Public API + the §8 Qt
bridge example.

---

## 8. GUI Worker pattern (Qt)

**What it is.** A per-project Qt worker base for running blocking core operations off the UI thread. The **signal contract and worker invariant are owned by [`GUI_CONSUMERS.md`](GUI_CONSUMERS.md) GUI-02** (the single canonical worker contract: off-thread, exactly-one-terminal-signal, cancellation-is-not-failure) — this section is only the `mgf.common`-side note that the base class is a per-project lift, not a shipped class. The canonical reference mechanism is a `QObject` worker moved to a `QThread` emitting `completed(result)` / `failed(exc)` / `cancelled()`; an equivalent that meets the GUI-02 invariant (e.g. qasync / `asyncio.to_thread`) is conformant. The base owns a `CancellationToken`; subclasses override `do_work()` and call exactly one core function. Long-lived workers that need an event loop use `QObject` + `moveToThread`.

**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`.

**Status.** ✅ Supported, L1 (per-project GUI template). The
hardened `CoreWorker.run()` MUST catch bare `Exception` in
addition to typed app errors and route untyped exceptions
through the crash reporter — see [`ERROR_HANDLING.md`](ERROR_HANDLING.md) §5 for the
shape.

**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.completed.connect(self._on_done)      # GUI-02 terminal signal
worker.failed.connect(self._on_error)        # GUI-02 terminal signal
worker.cancelled.connect(self._on_cancelled) # GUI-02: cancellation != failure
worker.start()
```

**How to lift.**
1. Read [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §2.1.
2. Implement `CoreWorker(QThread)` following the Public API
   above. Catch bare `Exception` in `run()` and route untyped
   failures through the crash reporter (template in
   `ERROR_HANDLING.md` §5).
3. Subclass per blocking operation. The body of `do_work()`
   should be a single function call into core.
4. Long-lived workers (timer-driven, event-loop-needing): use
   the `QObject` + `moveToThread` pattern. The teardown
   sequence is load-bearing: stop the worker → quit the
   thread's event loop → wait for the thread to finish →
   only then delete the worker. Skipping any step leaks the
   thread or crashes Qt at shutdown.

**Versioning.** The signal contract is owned by [`GUI_CONSUMERS.md`](GUI_CONSUMERS.md) GUI-02 — there is no second, divergent vocabulary to pin here. What `mgf.common` guarantees is the GUI-02 *invariant* (off-thread, exactly-one-terminal-signal, cancellation-is-not-failure); a project that lifts this base meets the invariant via the canonical `completed`/`failed`/`cancelled` names or a recorded equivalent.

---

## 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.

**Status.** ✅ Supported, L1 (per-project GUI template). The
load helper + `GuiLoadError` typed exception are small enough
to lift from the Public API shape below.

**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.** Implement `load_ui` + `require_child` +
`GuiLoadError` (a few dozen lines) following the Public API
shape. 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 `mgf-common`.** Vault interface + the
`EncryptedFileVault` fallback at `src/mgf/common/vault/`.

**Status.** ✅ Supported, L2. The vault always logs the active
backend choice at construction time per **CF-05** / **SC-17**;
operators see "is this configured?" not which secret backend
is in use until they're already authorised to know.

**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 `mgf-common`.** ✅
`src/mgf/common/observability/crash_reporter.py` with the local
+ remote-sink fan-out, the redactor pass before egress, and
the fail-safe minimal payload on redactor failure.

**Status.** Supported, L2.

**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 `mgf-common`.** ✅
`src/mgf/common/observability/otel_setup.py` — `setup_otel`,
`operation_span`, `build_otlp_log_handler` all present and
tested; no-op when the OTel SDK is absent.

**Status.** ✅ Supported, L2. Per-operation span wrapping is
consumer-side discipline: each L2 use case wraps its body in
`operation_span(...)`.

**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("user.create", user_id=spec.id):
    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.** Template ready in [`LOGGING.md`](LOGGING.md) §9.1.

**Status.** Planned. The canonical `LogViewerDialog` template
at [`LOGGING.md`](LOGGING.md) §9 is the reference shape consumers wire when
they want a GUI log viewer.

**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 `mgf-common`.** ✅ `src/mgf/common/observability/redaction.py`
— the canonical implementation. Recommended import:
`from mgf.common.observability.redaction import Redactor`.

**Status.** Supported, L2.

**Hardening invariants.** The redactor's `_walk` is:
- **Depth-bounded** (`_MAX_REDACT_DEPTH = 64`). A pathologically-
  nested payload cannot raise `RecursionError` *inside* the
  redactor mid-scrub — the sentinel
  `[REDACTION-DEPTH-EXCEEDED]` is inserted and walk returns.
- **Cycle-guarded** (`id()`-keyed `seen` set with `try / finally`
  discard). A self-referential payload doesn't infinite-loop;
  legitimately-shared SIBLING substructure is walked both
  times (the `seen` entry is popped on exit). Only true cycles
  are caught.

These bounds matter because a redactor that crashes propagates
the crash to the caller's `LOG.info(...)`, breaking the calling
process. Failing-safe (sentinel + return) beats failing-loud
(propagating exception) for a defense-in-depth layer.

**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 depth-capped, cycle-guarded template
from `LOGGING.md` §6.2 — or, recommended, depend on `mgf-common`
and import directly.

---

## 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.

**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 own test suite — 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 — resolve from the self-hosted devpi index, not pypi.org (a single `--index-url https://195.15.203.250.sslip.io/magogi/prod/+simple/` serves `mgf-*` + third-party; set it as the default index via `[[tool.uv.index]]` or `uv sync --index-url …`, anonymous — no named index, token, or `[tool.uv.sources]` pin needed per [`PACKAGE_DISTRIBUTION.md`](PACKAGE_DISTRIBUTION.md) §6) | [`mgf-common`](https://195.15.203.250.sslip.io/magogi/prod/mgf-common) (devpi package page — PKG-01/PKG-10) |
| 3 | Wire `mgf.common.bootstrap(app_name=..., app_version=...)` into every entry point (wires identity + logging + OTel + excepthooks transactionally) | `mgf-common` README |
| 4 | Subclass `AppError` for project-specific exceptions (§1) | `ERROR_HANDLING.md` §2 |
| 5 | Subclass `BaseAppSettings` via `settings_config(env_prefix=...)` (§2) | `CONFIGURATION.md` §3 |
| 6 | (Done by `bootstrap()` above — `setup_logging` / `setup_otel` shims remain for framework-owned cases) | `LOGGING.md` §5 + §7 |
| 7 | (Done by `bootstrap()` above — `install_excepthook` / `install_threading_excepthook` shims remain); 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: implement the §5 / §6 per-project templates | §5 + §6 above |
| 12 | If you handle credentials: depend on `mgf-common`'s vault (§10) | `mgf.common.vault` |
| 13 | If GUI: implement 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

Concrete domain code — your project's actual use cases, the
files that import vendor SDKs by name, the schemas that
encode your business invariants — SHOULD NOT be copied across
projects. The components above are *abstractions and
infrastructure*; the rule that separates the two is mechanical:

- A module that imports a vendor SDK (`libvirt`, `boto3`,
  `stripe`, `paramiko`, etc.) directly is domain code.
- A module that operates on dicts / dataclasses / `Path`s /
  `subprocess.run` calls but has no third-party knowledge is
  infrastructure.

**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. The rhythm below has shipped multiple rounds of
closures with zero regressions on real consumer projects;
adopting it from day one of a new consumer keeps the compliance
surface tractable as the code grows.

### 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)

Each audit round is "audit → close → commit → push" within one
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 —
   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.** 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 canonical implementation or template lives.
- 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`.

---

## 17. Atomic-write helper

**What it is.** A small, focused helper that takes a path + a
body (`str | bytes`) + a file mode, and writes the file
atomically: write to a `<file>.tmp` sibling, `fsync` the body,
`chmod` to the requested mode, `os.replace` to the target path,
optionally `fsync` the parent directory. The helper handles
every error path (temp-file cleanup on raise, partial writes,
permission errors) so callers don't re-implement the recipe.

**What it solves.** Three historically-divergent in-tree
implementations of the temp+rename pattern lived across
`mgf-common`'s call sites (config save, vault save, crash
report write). They disagreed on which subsidiary step they
handled — some skipped `fsync` (data could be lost on a power
event), some used a global-temp path that broke
`os.replace`'s atomicity guarantee (the rename across
filesystems is no longer atomic), some left half-written
`.tmp` files on exception. The shared helper is the one
verified contract — closed by **RA-01**.

**Where it lives in `mgf-common`.** ✅ `src/mgf/common/_io.py::atomic_write`.

**Status.** Supported, L2.

**Public API.**

```python
from mgf.common._io import atomic_write

# Bytes or string content; mode defaults to 0o600 (private);
# fsync_dir defaults to True (the rename itself is durable).
atomic_write(path, content, mode=0o600, fsync_dir=True)

# For high-throughput non-durable writes (crash report fan-out,
# where the cost of fsync(dir) on every report exceeds the
# value):
atomic_write(path, content, mode=0o600, fsync_dir=False)
```

**Call sites in `mgf-common` core** — the helper is the *only*
write recipe in core (pinned by **RA-01**):

| Call site | Mode | `fsync_dir` |
|---|---|---|
| `mgf.common.config._loader::save_settings` | `0o600` | `True` (durable rename for config) |
| `mgf.common.vault.EncryptedFileVault::save` | `0o600` | `True` (durable rename for vault) |
| `mgf.common.observability.crash_reporter._write_local` | `0o600` | `False` (a missed report is acceptable; a stalled exit isn't) |
| `mgf.common.cli._doctor_live::emit_runtime_snapshot` | `0o600` (per **SC-04**) | `True` |
| `mgf.common.cli._migrate`, `_catch_up` consumer-source rewrites | preserve target's existing mode | `True` |

**How to lift into a new project.** Two paths:

1. **Recommended:** depend on `mgf-common` and import directly.
   The helper is part of the `_io` module — semi-public; the
   contract is stable.
2. **Copy:** lift the implementation into your project's
   `<app>/_io.py`. Maintain the same signature; pin in CI that
   no other call site in your project re-implements the
   recipe. (A grep for `tmp.replace\|os.replace\|.write_text.*tmp`
   in src/ MUST return zero non-helper hits.)

**Anti-patterns:**
- **Inline `tmp = path.with_suffix(".tmp"); tmp.write_text(...); tmp.replace(path)`** — works for the happy path but skips the body `fsync`. A crash between write and rename leaves the old file (good); a power loss between rename and metadata flush can lose the new file (bad). Use the helper.
- **Per-call `mode` decisions scattered everywhere.** Bundle the
  mode discipline at the call site (one constant, one mode);
  don't recompute "should this be 0o600 or 0o644?" per write.
- **Skipping `fsync_dir=True` for state files that MUST survive
  a power event.** The default is `True` for a reason; flip
  it only when the latency cost dominates the durability
  value (crash reports — see the table above).

---

## 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** | A systemd-timer + tick-state pattern 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`.*
