Orientation for coding agents and contributors working in this repository.
Tool-neutral and canonical: tool-specific instruction files (e.g.
CLAUDE.md) include or link this document rather than restating it.
Minimal is a declarative, content-addressed package/build system plus a session plane for sandboxed development environments. The system is two cooperating planes: the build plane (declarative packages → dependency graph → sandboxed builds → content-addressed cache) and the session plane (long-lived isolated dev environments, natively on Linux or inside a libkrun microVM). Start with docs/architecture.md.
The workspace builds min, mip, minimald, and minvmd:
| Binary | Role | Reference |
|---|---|---|
min |
Session CLI (built from the minimal crate; its [[bin]] target is min) |
docs/reference/cli-min.md |
mip |
Package/build CLI | docs/reference/cli-mip.md |
minimald |
Session daemon: SSH server hosting sessions and task/sandbox executions | docs/reference/cli-minimald.md |
minvmd |
MicroVM host daemon: boots Linux guests via libkrun, bridges host UDS to in-VM vsock | docs/reference/cli-minvmd.md |
The CLI reference overview is docs/reference/cli.md.
Run just crates to print the workspace's crates with their descriptions. Use
it when you need to know which crate is responsible for something, or where a
change belongs.
docs/architecture.md §0 splits the crates into core crates and the product surface, and §3 adds which plane each crate belongs to (build, session, shared).
When you change a crate's scope, update its Cargo.toml description too, so
this information stays current.
Use these terms in code, comments, docs and commit messages. The BOX spec
(docs/specs/29-spec-box-local-first)
plans the box model they describe. That spec defines the Box Types and the
spec settings. Today the tree has BoxId and BoxRecord, and minimald
still branches on session or task. The rest of this section is the target
for new code.
- A box is the thing itself: its record, id, state, lifecycle, events,
spec and processes. Code about the box says box, as
BoxIdandBoxRecorddo. - A Box Type names a kind of box, never the box itself. session and task are Box Types. The BOX spec lists the rest.
- A sandbox is the operating-system isolation (namespaces, mounts,
cgroups) that every box runs in: the
sandboxcrate. Package builds are boxes of thebuildBox Type, though todaymipruns them in the sandbox directly, outside a box. - A Box Host is a running
minimaldthat creates and hosts boxes, as the architecture glossary defines it, not the machine it runs on. Gatehouse calls an enrolled Box Host a node.
Rules that follow from these:
boxis a reserved word in Rust. A module about boxes isboxesorbox_<noun>, and a type isBox<Noun>. A bareBoxshadowsstd::boxed::Box.- Code never uses session or task to mean the box. Some wire, on-disk,
printed and environment names say session because a spec keeps them for
compatibility. Those keep their serialised name, and the Rust name says box
with a serde
renameoralias. - Under the BOX spec, a box's behaviour follows the settings in its spec: its lifetime, PTY, timeout and network mode. It never follows the Box Type name. Besides type resolution, the Box Spec projection reads the type's name, source and root, and admission checks a spec against the root's constraints. Until then, do not add new branching on session versus task where a setting can carry the difference.
- New files, modules and types about the box take box names from the start. Rename code that still says session for the box in a pull request of its own. Never mix a rename into a move or a feature change.
| Platform | What you can build and test |
|---|---|
| Linux amd64 / arm64 | Full workspace natively: just ci builds, lints, and tests everything, including every binary. |
| macOS arm64 | Session stack natively (just ci); every Linux-only crate via just test-cross. |
Build and test only through just. The recipes carry the per-OS scoping,
the pinned flags, and the ordering constraints (notably the macOS codesign);
a hand-rolled cargo invocation silently drops them. If something has no
recipe, add one — the justfile is where local build knowledge lives.
minimald does not build natively on macOS: its sandbox stack
(hakoniwa → libcgroups → procfs) is Linux-only. That is a native-host
limit, not a coverage gap. Consequences:
just test,just clippy, andjust fixscope themselves: on macOS they resolve to-p minvmd -p boxesautomatically (the justfile'sscopevariable), on Linux to--workspace.just test-crosscovers the Linux-only crates —minimaldand theminCLI's test targets included — by cross-compiling to musl in Docker (--workspace --exclude minvmd). It is the macOS answer for anything the darwin scope skips. The first run compiles under emulation and can take an hour+.- The
minCLI's macOS coverage is also proved end-to-end (scripts/session-e2e.sh, driven byjust e2e), run in CI on the self-hosted Apple Silicon runner against a real microVM.
The native scoping is temporary: CI widens to --workspace once the tree is
mac-buildable (see the comments in .github/workflows/ci-macos.yml).
Run just preflight to check this host for everything below. Each failed
check prints the command that fixes it.
- Rust: pinned by rust-toolchain.toml (rustup picks
it up automatically; components
rustfmtandclippy). - protoc: required by
crates/remote-proto/build.rs(prost + tonic). Needs proto3optionalsupport; current Debian/Ubuntuprotobuf-compilerqualifies (installlibprotobuf-devtoo; it carries the well-known protos and is only a "recommends" ofprotobuf-compiler). The musl cross image pins protoc 25.1 in Cross.toml because its base image's apt protoc is too old. - Linux build deps (per README.md):
build-essential openssl pkg-config libssl-dev git protobuf-compiler. - VM bring-up on Linux (
just up-kvm,just test-vm,just e2e): a KVM host with durablekvmgroup membership, plusjq,cpio, andzstd;cross(Docker) when no native musl toolchain is present (scripts/build-initramfs.shauto-detects). - macOS: Xcode CLT (
codesign,otool,install_name_tool); libkrun via Homebrew (brew install slp/krun/libkrun) for the VM recipes (just up,just test-vm,just e2e);jq,zstd, andcpio(brew install jq zstd cpio) for theartifacts/initramfsscripts those recipes run.just initramfsandjust test-crossbuild throughcross, which needs a running Docker daemon and the x86_64 Linux toolchain at the pinned version. The cross-rs images are x86_64, and rustup installs that toolchain on an arm64 Mac only with--force-non-host:rustup toolchain add <channel>-x86_64-unknown-linux-gnu --profile minimal --force-non-host. - python3, on any host:
just cratesruns it (stdlib only). The session e2e needs it too, because its interactive attaches run through a real pty (scripts/e2e-attach-pty.py). Only a tty can answer the session-exit prompt. No packages to install, but the interpreter must be onPATH.
Run just --list to see every recipe available here, each with its own
description. Bare just prints the same list. That output is generated from
the justfile, so it cannot drift; this section deliberately does not restate it.
[linux]/[macos] attributes scope recipes per OS, so the list on your host
already shows only what you can run on it.
The justfile is the local twin of the frozen CI workflows:
the CI YAML schedules, the logic lives here and in scripts/
(docs/ci-strategy.md §8). .scratch/ is a shared scratchpad; recipes manage
only their own artifacts there (which is why clean is scoped, never
rm -rf .scratch wholesale).
Verified against the current tree; sources in parentheses.
- Codesign last on macOS. The hypervisor-entitlement
codesignmust be the last thing to touch theminvmdbinary; any later cargo call that relinks it drops the signature (EINVAL fromkrun_start_enter), so re-sign after every build (justfileminvmd-build/test-vm). mip updaterewriteslocked_commit. It re-resolves the upstream pins and editsminimal.tomlin place (crates/op/src/project/update.rs); an easy accidental diff, and it turns pinned cache fetches into new resolutions. The fetch scripts warn explicitly: do not run it as part of artifact fetching (scripts/fetch-libkrun.sh).- Initramfs features come from the
FEATURESenv.scripts/build-initramfs.shstill honoursFEATURESwhen it compiles the guestminimald, but the justfile passes none, so every guest daemon the recipes ship carries the default feature set; with a prebuiltMINIMALD_BIN,TARGET/FEATURESare ignored. - macOS
--outputmust stay under the repo. On macOS theminimalshim runs the CLI inside a VM and only syncs the project dir back to the host, somaterialize --outputpaths outside the repo tree are not visible to it (crates/minvmd/README.md). - SIGTTOU wedges foreground boots. With a TTY on stdin and
MINVMD_BOOT_LOGunset, libkrun's console setuptcsetattr()s the terminal from a background process group, so SIGTTOU stops the whole group and the boot dies silently at the timeout. Redirect stdin from/dev/nullor setMINVMD_BOOT_LOG(scripts/bench-minvmd-boot.sh). - Cold VM boots overrun the default timeouts. The generic guest kernel
can spend 40–70 s probing hardware before pid-1 starts, overrunning the
60 s READY / 75 s autospawn / 30 s lifecycle defaults; the justfile
exports 150 s
MINVMD_READY_TIMEOUT_SECS/MINIMAL_SPAWN_TIMEOUT_SECS/MINVMD_LIFECYCLE_BOOT_TIMEOUT_SECSfor every recipe; do the same for manual cold boots outsidejust(justfile header exports). - Leaked VMMs and proxies hold their sockets. A VM in
krun_start_enterignores SIGTERM. A Ctrl-C'd or killed boot can leave a detached__krun-vmmand the gvproxy switch alive, and then every later boot fails. Abep serveproxy that outlives its boot holds its listener socket and that boot's token. Reap withjust reap(scripts/reap-vms.sh), which SIGKILLs this checkout's strandedminvmd,__krun-vmm,gvproxyandbep serveprocesses. It matches only this checkout's binaries, so parallel checkouts are safe. minvsminimalnaming. The CLI crate isminimalbut its binary ismin; build with-p minimal, runtarget/debug/min. Cargo never removes old binary names, so a stale pre-renametarget/debug/minimalcan linger; don't invoke or PATH-resolve it. On macOS,minimalonPATHis typically the distribution shim (~/.minimal/shim/bin/minimal), distinct from anything this repo builds (justfile comments,crates/minvmd/README.md).
docs/ci-strategy.md covers the CI lanes and what runs
where.
docs/internal/release-pipeline.md covers
the release and promotion mechanics. ls .github/workflows/ lists the
workflows on main.
The required-check vocabulary is the lane aggregators: ci-success,
ci-linux-native-success, ci-linux-kvm-success, ci-macos-success,
ci-shell-installer-success. Each is green when its lane's jobs succeeded
or were path-skipped. ci-kani-success is not among them: the Kani lane is
advisory until its harnesses have soaked, promoted by adding
ci-kani-success to the branch-protection ruleset (no workflow change
needed). The nightlies are distinct on purpose: nightly-tests
proves, nightly ships.
- Never commit to
main. Branch first, with afeat/,fix/,docs/,chore/, orrefactor/prefix. - Conventional Commits, enforced by commitlint: docs/commit-conventions.md.
- Rust standards: docs/rust-coding-standards.md.
- Specs: work carrying
kind:epicgets one, atdocs/specs/NN-spec-<slug>/NN-spec-<slug>.md. Start from docs/specs/TEMPLATE.md; requirements are written as EARS and the format is decided in gominimal/foundry. - Prose: markdown is Vale-linted:
just lint-proselints the markdown your branch touches (committed against main, staged, unstaged, untracked),just lint-prose <file.md>lints specific files, andjust lint-prose --alllints the tree. Drive files you edit to zero, and leave untouched files' alerts alone rather than mass-rewriting them..vale.inipins the third-party styles. The vocabulary and the first-party rules live understyles/. .github/workflows/is frozen and CODEOWNER-gated. Do not edit it. Extend CI coverage through convention-discovered tests,scripts/, and thejustfile, per the test-extension contract in CONTRIBUTING.md and the design in docs/ci-strategy.md.- Do not restate an inventory. Counts and lists of crates, workflows,
binaries, and recipes are wrong as soon as one changes. Link to the document
that owns the list, or give the command that prints it (
just crates,ls .github/workflows/,just --list). The prose rules reject a stated count (inventory.StatedCount).
Everything min, mip, the daemons and the installer scripts print for a
person is part of the product. Treat a message like an interface: specify it,
test it, and review it.
Every message passes one test. It states the current situation in plain words, or it tells the user one action to take and what that action does. Remove or rewrite a message that does neither.
- Use the words the user types and sees: command names, flag names, file paths. Do not name internal components, design terms, RPC names, or requirement identifiers. A product term that the user configures or sees, such as the Box Egress Proxy, is not an internal component. A process name or a crate name is.
- An instruction names a command the user can run as written. If no such command exists, state the situation and stop.
- A command that takes an argument shows it. Fill in the value when the message knows it. Use a placeholder in angle brackets when it does not, or when the value is a secret. A secret prints only where the command itself just issued it for the user to keep.
- Say only what the code can prove. Leave out a remedy that you did not check against the code path that prints the message.
- When one condition covers several causes with different outcomes, say what can happen, not one outcome.
- Give each fact its own sentence. Do not merge sentences that read alike but state different facts.
- A label that prints after both a pass mark and a fail mark is a noun phrase, never a statement.
- A refusal says what the tool refused, why, and the next step. Where a flag
and a
minimal.tomlsetting both give the remedy, name both. - Use one name for one thing across the CLI, the dashboard and the docs. When a sentence appears in two places, change both or neither.
- A logged warning is not a message for the user. Log records go to the log. Write a message for the user by hand.
Do not add a message before the box shell on a healthy start. On an interactive attach, the shell's first paint replaces whatever the command printed before it, so nobody can read a line there. A command that does not attach, or that runs without a terminal, prints lines that stay. A healthy start is one that reaches the shell with nothing for the user to do. A value that a command prints on stdout for a script to read is output, not a message.
- This rule governs new messages. A line that a spec requires at session start stays until that spec changes. Do not remove it to satisfy this rule.
- On the healthy path, do not add a warning or a note to an interactive start or attach.
- A new line can appear before the shell only when the command cannot continue without a decision from the user. That line is a prompt that waits for an answer. A prompt is never for information, and an advisory never becomes a prompt.
- Without a terminal, or with
--no-input, a new prompt is an immediate error that names the flag that answers it, and the command does not wait. - Anything else worth telling the user goes to a surface that stays, such as
min ls,min session policy,min dashor a notice inside the box. A detail for whoever debugs the start goes to the log. - An error that ends the command is not covered by this rule: the command exits non-zero and the text stays on screen.
When you specify or plan work that adds or changes a message:
- In the spec, state what the message must tell the user, the condition that prints it, and the surface it appears on. Do not write the exact text in a spec. A spec binds the behaviour, and the wording can change without it.
- Write the exact text in the issue or the plan task, and pin it in the test that checks it. A wording change updates the code and the test. It changes a spec only to remove a quote of the old text, as the last item says.
- Say why it is true: name the code path or state that makes it so.
- List the tests and scripts that pin the text, so the change updates them.
- If a spec still quotes the old text, say so. Replace the quote with what the message must convey in the same piece of work, or in an issue linked from the PR.
When you review such work, read each new string as a user who has never seen the design. Apply the test above before you approve.
The canonical pre-PR command is just ci, per
CONTRIBUTING.md ("Before opening a
PR"): it runs the same gates the PR lanes run (fmt, clippy, cargo-deny,
the test suite, doctests; plus just test-ignored on Linux), dispatched
for your OS, and then clippy-strict, which applies a stricter lint set to
the lines your branch changed. Run just clippy-strict alone to see just
those. That gate is local only: no CI lane runs it, so its hits never turn
a PR red, and in a revise or conflict round the directive bounds the
change, so report its hits in notes rather than fix them. just fix
does not run it, so nothing in the edit loop asks you to. Platform notes
for agents:
- macOS: the workspace does not build natively (see
Platform matrix);
just ciruns the darwin-capable scope, andjust test-crosscovers the Linux-only crates viacross(Docker required). A change touchingminimaldor theminCLI's tests is unverified untiljust test-crosshas run. - VM/daemon-path changes: also run
just e2e(the session proof) and/orjust test-vm(the VM integration harnesses). - Tight iteration loops:
just fix(fmt →clippy --fix→ fmt, with--allow-dirtyso it runs mid-edit) thenjust test. Both carry this host's scope; neither needs a hand-writtencargoline. Everything that loop prints is something to fix; the strict gate is not in it.