Architecture
This page is a map, not a deep dive: it sketches how mtui is put together so
the rest of the book (and the source) is easier to navigate. For the day-to-day
contributor workflow — toolchain, quality gates, adding a command, testing — see
the Developer guide. For the authoritative contributor spec and
definition of done see AGENTS.md, which lives in the repository root.
Workspace layout
mtui is a Cargo workspace of single-purpose crates. Lower crates never depend on
higher ones; mtui-core is the composition root that wires everything together.
| Crate | Job |
|---|---|
mtui-types | Domain types + the error hierarchy. Pure, sync, no I/O. Includes the typed report_document model of the JSON-native TeReGen report (schema v1.0). |
mtui-config | TOML config + XDG path resolution. |
mtui-hosts | SSH/SFTP (russh), the Target/HostsGroup model, locks, the pool arbiter. Async. |
mtui-datasources | Shared HTTP; refhosts resolve/search/verify; the openQA/QEM/Gitea/OBS/oqa-search clients. Async. |
mtui-testreport | Testreport lifecycle (TeReGen v2 document ingest/upload) and the update workflow (actions/checks/export). |
mtui-core | The Command trait + registry, Session, the dispatch engine, and the wiring that ties the crates together. |
mtui (root) | Facade package owning src/bin/{mtui,mtui-mcp}.rs behind the cli/mcp features; its integration tests are in tests/it.rs. |
mtui-tui | The report-document editor: a walker over the report JSON Schema, the form model built on it, and the ratatui screen. Pure and sync; depends only on mtui-types, so neither the MCP server nor a --no-default-features build compiles ratatui. |
mtui-cli | The reedline REPL library, including the terminal handover around the mtui-tui editor. |
mtui-mcp | The rmcp server library. |
The command registry is the single source of truth
Every command implements one Command trait and is registered into a central
registry by an explicit register_all() (no auto-registration magic). Three
consumers iterate that one registry:
- the REPL’s command dispatch and tab-completion;
- the generated command reference;
- the
mtui-mcptool synthesiser, which turns each non-denied command’sclaparg spec into a JSON tool schema.
So adding, renaming, or removing a command updates the REPL, the docs, and the MCP tool surface together — they cannot drift. See MCP server for the deny-list that keeps REPL-only commands off the wire.
Session state, not globals
Commands operate on an explicit Session (config, the HostsGroup targets, the
loaded templates/metadata, the display) passed into each call — there are no
hidden globals.
Trait injection and the no-cycles rule
The update-workflow doer/check registries live in mtui-testreport, but the
install/uninstall template that needs them lives in mtui-hosts — the lower
crate. mtui-hosts therefore declares a PlanProvider trait in terms of its own
types, and mtui-testreport’s WorkflowRegistry implements it. The rule when
two crates need to cooperate across that boundary: define a trait in the lower
crate and inject an implementation from the higher one — never introduce a crate
cycle.
Inject at the point of use. update_flow::perform_install /
perform_uninstall install the provider on the HostsGroup immediately before
driving the template. OperationGroup::plans has exactly one consumer, so there
is exactly one site that cannot forget. Injecting where the group is built
looks tidier but is not equivalent: an earlier design that wired one central
spot was never called at all, leaving install and uninstall running no
command while still reporting success.
Contracts
These data-format and workflow contracts keep mtui interoperable with the SUSE
maintenance ecosystem. Each is owed to something live: the RRID grammar to
OBS/IBS/QEM, the refhosts.yml schema to the qam-metadata fleet database, the
report document schema to TeReGen, the MCP tool surface to downstream
LLM clients, and the on-host formats to other mtui processes sharing a fleet
— including older releases, which is why they are a wire format and not an
implementation detail. The crates/*/tests/ fixtures are the authority.
- RRID grammar —
project:kind:maintenance_id:review_idand its parse errors (see the FAQ). refhosts.ymlschema — location-grouped on disk, but rows are merged/flattened/de-duplicated at load; parses identically to the golden fixtures.- Report document schema v1 — the JSON schema TeReGen serves
(
report_document::SCHEMA_JSON); drift from the live schema is checked at load. - Remote-lock wire format — one line,
timestamp:user:pid[:comment], shared by the operation lock and the pool-claim lock (see Workflow concepts). - MCP tool names/schemas — downstream LLM configs depend on them; the synthesised, slimmed schemas are snapshot-tested.
- openQA
BUILDquery string —:{type}:{number}:{package}(crates/mtui-datasources/src/openqa/base.rs::OpenQABase::new), owed to openSUSE/qem-bot, which writes it (types/submissions.py:236), not to TeReGen. Each component is qem-bot’s own:typemirrors qem-dashboard’sincidents.typecolumn (git/smelt,mtui_types::UpdateSource);numberis the dashboard’s own incident number, which for every SLFO update (git- or OBS-served) is the review id, not the maintenance id (crates/mtui-datasources/src/qem_dashboard/incident.rs::QemIncident::incident_number);packageis chosen by qem-bot’ssort_packagesordering (demote arch-suffixed/-livepatch-names, then shortest, then alphabetical), not plain shortest-by-length. A Product Increment is connected to neither qem-dashboard nor openQA, so mtui skips the dashboard fetch for PI outright rather than querying a service that was never going to answer.
Deliberate design choices
- TOML config, not INI.
- Native OBS/IBS API for the QAM review workflow, not an
oscsubprocess. - Two static binaries (
mtui,mtui-mcp), no runtime interpreter or virtualenv to install. - Async I/O (
tokio) with true parallel host fan-out. - Git-vs-OBS is decided per update, from the report document, not from the RRID.
During the SL-Micro 6.0/6.1 cutover both workflows share the
SLFO:1.1id space, so no rule over the RRID’s shape can be correct — and an update can briefly be served both ways at once.mtui_types::UpdateSourceis a selection, not an observation:Gitwhen the report document’sworkflowisgitea,Obsotherwise, resolved once at load. On a dual-served updateGitwins by design, and mtui leaves that update’s OBS review request untouched —assign/approve/reject/commentonly ever reach the Gitea side. Seeapprovefor the operator-facing consequence.