8000 docs: architecture v2, internal maintainer docs, and error-handling guide by norrietaylor · Pull Request #858 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 220 additions & 41 deletions docs/architecture.md

Large diffs are not rendered by default.

146 changes: 73 additions & 73 deletions docs/ci-strategy.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
# 0001 — Rust error-handling strategy

- Status: Accepted
- Date: 2026-06-02
- Deciders: Evan, Tom DNetto, Norrie Taylor
- Refs: [#352](https://github.com/gominimal/minimal/issues/352)

## Context
# Rust error handling

New Rust crates need a consistent error-handling convention. The prior pattern
hand-writes `Display`, `From`, and `std::error::Error` impls for every error
enum — high boilerplate, easy to drift across crates. Constraint: user-facing
errors must stay actionable, not just structurally tidy.

## Decision
enum: high boilerplate, easy to drift across crates. User-facing errors must
stay actionable, not just structurally tidy. Two layers, chosen by the role of
the code, satisfy that.

Two layers, chosen by the role of the code.

### Library crates — structured errors via `thiserror`
## Library crates - structured errors via `thiserror`

- `#[derive(Debug, thiserror::Error)]` on error enums.
- `#[error("...")]` generates `Display`; `#[from]` generates `From`. This
Expand Down Expand Up @@ -86,22 +76,22 @@ pub enum Error {
}
```

### Application / binary code — opaque propagation via `anyhow`
## Application / binary code - opaque propagation via `anyhow`

Use `anyhow` where the specific error type does not matter and errors are only
propagated upward. This is the common case in application (non-library) code.

### User-facing CLIs — `color_eyre` instead of `anyhow`
## User-facing CLIs - `color_eyre` instead of `anyhow`

For CLIs that surface errors to a human, use `color_eyre` for better-formatted
error reports. `eyre` is a fork of `anyhow` and fills the same
opaque-propagation role, so it is a drop-in substitute at the application
boundary.

### Rationale
## Rationale

`thiserror` (library, structured) and `anyhow` / `eyre` (application, opaque)
solve different problems and compose — they are complementary, not
solve different problems and compose; they are complementary, not
alternatives. `thiserror` does not compromise actionable messages: it only
removes the `Display` / `From` boilerplate; the message strings remain fully
under our control.
Expand All @@ -110,17 +100,6 @@ under our control.

- New crates adopt this convention from the start.
- No retrofit mandate for existing crates. Migration of existing hand-rolled
error enums is out of scope for this ADR unless raised separately.
error enums is out of scope here unless raised separately.
- Adds `thiserror` (library deps) and `anyhow` / `color_eyre` (binary deps) to
the dependency set.

## Sign-off & Rationale Summary

This decision was reached by team consensus (Evan, Tom DNetto, Norrie Taylor)
during an engineering discussion in June 2026. The two-layer approach —
`thiserror` for library crates, `anyhow`/`color_eyre` for application crates —
was selected because it separates structural error contracts (library boundaries)
from opaque propagation (application plumbing). All three deciders confirmed that
the approach eliminates boilerplate without sacrificing the ability to craft
actionable user-facing messages, and that it aligns with the Rust community's
widely-adopted conventions for this problem. No dissenting views were recorded.
21 changes: 21 additions & 0 deletions docs/internal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Internal docs

Maintainer-facing documents. Everything here is publicly visible in the
repository (the repo is public), but none of it is published on the docs
site or written for end users: expect maintainer context, working notes,
and process detail.

## Index

| Document | Contents |
|---|---|
| [licensing.md](licensing.md) | Licensing decisions and per-file/per-crate license layout for the workspace. |
| [session-domain-diag.md](session-domain-diag.md) | Session domain model: provider/session vernacular, deployment diagrams, socket lifecycle, and bootstrap rules. |
Comment thread
norrietaylor marked this conversation as resolved.
Resolved

## Related corpora outside this directory

- `docs/specs/`, design specifications.
- `docs/spikes/`, time-boxed investigations.

Both are, like this directory, excluded from the docs site but remain
publicly visible in the repository.
76 changes: 76 additions & 0 deletions docs/internal/licensing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: Licensing stance
description: Internal record of the project's license posture, LGPL exceptions, and redistribution obligations.
---

> This is internal documentation. It is not published to the docs site.

# Licensing stance

Minimal is licensed under Apache-2.0 (declared once in the workspace
`Cargo.toml` and inherited by every crate via
`license.workspace = true`). The license text lives at the repo root
(`LICENSE`); redistribution attributions live in `NOTICE`. Inbound
contributions are governed by the Contributor License Agreement (see
`CONTRIBUTING.md` and `legal/`). Dependency license policy is enforced
by `cargo deny` against `deny.toml`, which points back at this document
for the exceptions explained below.

## LGPL exceptions in deny.toml

### malachite family: document-and-accept

Five crates (`malachite`, `malachite-base`, `malachite-float`,
`malachite-nz`, `malachite-q`) are licensed LGPL-3.0-only and are
statically linked into our binaries. They are pulled in transitively via
the `nickel-lang-core` git dependency; we do not depend on them
directly.

Stance: document and accept. LGPL-3.0 §4(d)(0) requires conveying both
the Minimal Corresponding Source (the LGPL library source) and the
Corresponding Application Code (everything else needed to relink) in a
form suitable for relinking, via one of the GPLv3 §6 conveyance methods.

**Source-conveyance mechanism:** We rely on GPLv3 §6(d), providing a
network location from which to download the Corresponding Source. The
malachite crates are published on crates.io at pinned versions recorded
in `Cargo.lock`. Note that `Cargo.lock` itself only records registry
URLs and checksums; it does not convey the sources. The actual
conveyance depends on crates.io remaining available and retaining those
versions.

**Relinking form:** This repository (Corresponding Application Code) is
public, and anyone can rebuild the binaries with a modified malachite by
editing `Cargo.toml` to override the dependency. The statically linked
binary format does not impede relinking because users have the complete
application source.

**Risk acknowledgment:** If crates.io were to remove the pinned
malachite versions, the §6(d) conveyance would fail. For distribution
channels requiring stronger guarantees (e.g., air-gapped environments or
long-term archival), vendoring malachite sources under `vendor/` or
providing a §6(b) written offer valid for three years would be required.
Neither is currently implemented; the current stance is acceptable for
our distribution model (source-available binaries rebuilt on demand).

### hakoniwa: linking exception, Linux-only

`hakoniwa` ships under LGPL-3.0-only WITH LGPL-3.0-linking-exception,
so static linking is expressly permitted without LGPL relink
obligations. Its footprint is Linux-only: it is used by `sandbox2` and
`minimald`, and no longer reaches macOS builds since `mctx` was
decoupled from it in #721.

## Redistributed binaries in darwin release artifacts

The macOS (darwin/arm64) release artifacts redistribute two prebuilt
Apache-2.0 components, pinned in `vendor/`:

- libkrun v1.19.4 (`vendor/libkrun/libkrun.lock`), shipped as
`libkrun.1.dylib`, https://github.com/containers/libkrun
- gvproxy v0.8.9 from gvisor-tap-vsock
(`vendor/gvproxy/gvproxy.lock`),
https://github.com/containers/gvisor-tap-vsock

Apache-2.0 redistribution requires retaining the license and notices;
both are attributed in the root `NOTICE` file.
9 changes: 9 additions & 0 deletions docs/internal/minvmd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
Comment thread
norrietaylor marked this conversation as resolved.
Resolved
title: minvmd
description: The host daemon that boots the Linux microVM hosting minimald, bridging the CLI to a Linux session daemon.
---

This page embeds the maintained crate document
[`crates/minvmd/README.md`](https://github.com/gominimal/minimal/blob/main/crates/minvmd/README.md).

<!--@include: ../../crates/minvmd/README.md-->
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ flowchart TD
|------|---------|
| Session | The isolation environment. |
| Provider | Creates and hosts sessions; exposes a socket. |
| minimald | Linux provider — hosts sessions directly. Also runs inside a VM. |
| minvmd | VM provider — hosts VMs; each VM runs a minimald and minvmd proxies its socket to the host. |
| minhosted | Hosted provider — hosts sessions on an externally managed backend. |
| mincloud | Cloud provider — hosts sessions on a cloud backend. |
| minimald | Linux provider: hosts sessions directly. Also runs inside a VM. |
| minvmd | VM provider: hosts VMs; each VM runs a minimald and minvmd proxies its socket to the host. |
| minhosted | Hosted provider: hosts sessions on an externally managed backend. |
| mincloud | Cloud provider: hosts sessions on a cloud backend. |
| Minimal | Client process; discovers providers via sockets, starts a default/config provider when none exist. |

> Note: `minhosted` and `mincloud` are placeholders. They could be specialized into neocloud or hyperscaler-specific backends (e.g. a per-provider daemon for a specific neocloud, or AWS/GCP/Azure).
9 changes: 9 additions & 0 deletions docs/internal/sessions-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Session composition
description: How session contributions flow from declarative sources to the Composition consumed by the apply layer.
---

This page embeds the maintained crate document
[`crates/sessions/docs/COMPOSITION.md`](https://github.com/gominimal/minimal/blob/main/crates/sessions/docs/COMPOSITION.md).

<!--@include: ../../crates/sessions/docs/COMPOSITION.md-->
23 changes: 12 additions & 11 deletions docs/rust-coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
## Functional over Imperative

- Iterator chains over manual loops. map, filter, collect, try_fold, flat_map instead of for with mutable accumulators. Use for only when side effects dominate or control flow makes a chain awkward.
- Combinators on Option/Result. map, and_then, ok_or, unwrap_or_else, map_err — not match that reconstructs the same enum.
- Combinators on Option/Result. map, and_then, ok_or, unwrap_or_else, map_err, not match that reconstructs the same enum.
- Immutable by default. Only mut when required. Building a Vec by push? Try .collect().
- No index-based iteration. .iter(), .enumerate(), .zip(), .windows(), .chunks() over for i in 0..xs.len().

## Idiomaticity

- Cheapest reference that works. &str over &String, &[T] over &Vec<T>, &Path over &PathBuf, impl AsRef<Path> at boundaries.
- impl IntoIterator<Item = T> for consuming a sequence; impl Iterator<Item = T> for returning one (avoid allocating).
- Make illegal states unrepresentable. Encode invariants in the type system: enums for mutually exclusive states (not bool flags + Options), non-empty collections via Vec1 or (T, Vec<T>), parsed types instead of
- Cheapest reference that works. `&str` over `&String`, `&[T]` over `&Vec<T>`, `&Path` over `&PathBuf`, `impl AsRef<Path>` at boundaries.
- `impl IntoIterator<Item = T>` for consuming a sequence; `impl Iterator<Item = T>` for returning one (avoid allocating).
- Make illegal states unrepresentable. Encode invariants in the type system: enums for mutually exclusive states (not bool flags + Options), non-empty collections via Vec1 or `(T, Vec<T>)`, parsed types instead of
validated-then-passed-as-string. If a function can't be called in some state, that state shouldn't typecheck.
- Newtypes for domain values (struct UserId(u64)) over bare primitives. Use the https://crates.io/crates/nutype crate when the type needs trivial invariants enforced (non-empty, range bounds, regex, trimmed, etc.) — it generates the
- Newtypes for domain values (struct UserId(u64)) over bare primitives. Use the https://crates.io/crates/nutype crate when the type needs trivial invariants enforced (non-empty, range bounds, regex, trimmed, etc.), it generates the
validating constructor and keeps the inner value unconstructable elsewhere.
- Default only when the default is meaningful.
- Cow<'_, str> for sometimes-owned, sometimes-borrowed values.
- `Cow<'_, str>` for sometimes-owned, sometimes-borrowed values.
- Builder pattern for types with more than a couple of fields or any optional config. Owned style: pub fn with_x(mut self, x: T) -> Self, never &mut self -> &mut Self.
- Cloning discipline. .clone() is fine for Arc/Rc and small Copy-ish types; cloning a String/Vec/HashMap to dodge the borrow checker means restructure or borrow instead.
- Captured-identifier formatting. format!("{path}") over format!("{}", path).
Expand All @@ -25,7 +25,7 @@ validating constructor and keeps the inner value unconstructable elsewhere.

## Error Handling

- Library crates: typed error enum via thiserror. `#[derive(Debug, thiserror::Error)]`; `#[error("...")]` for Display, `#[from]` for From. Variants are distinct failure modes. Keep `#[non_exhaustive]` on public error enums. See [decision 0001](decisions/0001-rust-error-handling-strategy.md).
- Library crates: typed error enum via thiserror. `#[derive(Debug, thiserror::Error)]`; `#[error("...")]` for Display, `#[from]` for From. Variants are distinct failure modes. Keep `#[non_exhaustive]` on public error enums. See [Rust error handling](error-handling.md).
- Application crates: anyhow::Result for opaque propagation. Attach .context(...) / .with_context(|| ...) at layer boundaries. User-facing CLIs use color_eyre instead of anyhow for better-formatted reports.
- unwrap()/panic! only for broken invariants. unwrap() in tests only; in production use expect("why the invariant holds"). Never for recoverable conditions.
- Never swallow errors. No let _ = result; or .ok() discards without a comment justifying it.
Expand All @@ -35,20 +35,21 @@ validating constructor and keeps the inner value unconstructable elsewhere.

## Async & Concurrency

- No blocking in async context. No std::fs, std::thread::sleep, blocking network, or sync Mutex held across .await — use tokio::fs, tokio::time::sleep, tokio::sync::Mutex.
- Don't reach for Arc<Mutex<T>> reflexively. Plain Arc<T> suffices if T is immutable after construction. Spawned tasks must satisfy Send + 'static — design data flow accordingly.
- No blocking in async context. No std::fs, std::thread::sleep, blocking network, or sync Mutex held across .await, use tokio::fs, tokio::time::sleep, tokio::sync::Mutex.
- No `tokio::task::block_in_place`. It interacts poorly with `hakoniwa`; move blocking work to `tokio::task::spawn_blocking` or a dedicated thread.
- Don't reach for `Arc<Mutex<T>>` reflexively. Plain `Arc<T>` suffices if `T` is immutable after construction. Spawned tasks must satisfy `Send + 'static`, design data flow accordingly.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets also add that tokio::task::block_in_place is banned due to poor interactions with hakoniwa.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added under Async & Concurrency: "No tokio::task::block_in_place. It interacts poorly with hakoniwa; move blocking work to tokio::task::spawn_blocking or a dedicated thread." ef2117d.


## Logging

- Use tracing — no println!/eprintln! outside user-facing CLI output. Structured fields and spans, not interpolated strings: tracing::info!(pkg = %name, "building"), not info!("building {name}").
- Use tracing, no println!/eprintln! outside user-facing CLI output. Structured fields and spans, not interpolated strings: tracing::info!(pkg = %name, "building"), not info!("building {name}").

## Conventions

- Every change: cargo clippy --all-targets -- -D warnings, cargo test. Justify any #[allow(...)] with a comment.
- unsafe requires a // SAFETY: comment covering every caller invariant.
- #[must_use] on Result-shaped returns, builders, and anything where silently dropping the value is a bug.
- #[non_exhaustive] on public enums/structs that may grow.
- Private by default. Widen to pub(crate) before pub. Curate the public API via pub use at the crate root — internal module paths shouldn't be part of the public surface.
- Private by default. Widen to pub(crate) before pub. Curate the public API via pub use at the crate root, internal module paths shouldn't be part of the public surface.
- Testing: behavior, not implementation; one concept per test; insta for snapshot-shaped output; integration tests under tests/ for cross-crate flows.
- No dead code, commented-out code, or ownerless TODOs.
- Dependencies: prefer std, then crates already in the workspace, then crates listed on https://blessed.rs before reaching elsewhere. Versions are pinned in the workspace Cargo.toml; crate-level Cargo.tomls inherit via workspace = true,
Expand Down
Loading
0