8000 minimal/AGENTS.md at main · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
361 lines (314 loc) · 19.4 KB

AGENTS.md

File metadata and controls

361 lines (314 loc) · 19.4 KB

AGENTS.md

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.

Orientation

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.

Workspace crates

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.

Naming: box, Box Type, sandbox

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 BoxId and BoxRecord do.
  • 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 sandbox crate. Package builds are boxes of the build Box Type, though today mip runs them in the sandbox directly, outside a box.
  • A Box Host is a running minimald that 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:

  • box is a reserved word in Rust. A module about boxes is boxes or box_<noun>, and a type is Box<Noun>. A bare Box shadows std::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 rename or alias.
  • 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 matrix

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, and just fix scope themselves: on macOS they resolve to -p minvmd -p boxes automatically (the justfile's scope variable), on Linux to --workspace.
  • just test-cross covers the Linux-only crates — minimald and the min CLI'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 min CLI's macOS coverage is also proved end-to-end (scripts/session-e2e.sh, driven by just 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).

System dependencies

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 rustfmt and clippy).
  • protoc: required by crates/remote-proto/build.rs (prost + tonic). Needs proto3 optional support; current Debian/Ubuntu protobuf-compiler qualifies (install libprotobuf-dev too; it carries the well-known protos and is only a "recommends" of protobuf-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 durable kvm group membership, plus jq, cpio, and zstd; cross (Docker) when no native musl toolchain is present (scripts/build-initramfs.sh auto-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, and cpio (brew install jq zstd cpio) for the artifacts/initramfs scripts those recipes run. just initramfs and just test-cross build through cross, 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 crates runs 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 on PATH.

justfile recipes

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

Footguns

Verified against the current tree; sources in parentheses.

  • Codesign last on macOS. The hypervisor-entitlement codesign must be the last thing to touch the minvmd binary; any later cargo call that relinks it drops the signature (EINVAL from krun_start_enter), so re-sign after every build (justfile minvmd-build / test-vm).
  • mip update rewrites locked_commit. It re-resolves the upstream pins and edits minimal.toml in 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 FEATURES env. scripts/build-initramfs.sh still honours FEATURES when it compiles the guest minimald, but the justfile passes none, so every guest daemon the recipes ship carries the default feature set; with a prebuilt MINIMALD_BIN, TARGET/FEATURES are ignored.
  • macOS --output must stay under the repo. On macOS the minimal shim runs the CLI inside a VM and only syncs the project dir back to the host, so materialize --output paths 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_LOG unset, libkrun's console setup tcsetattr()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/null or set MINVMD_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_SECS for every recipe; do the same for manual cold boots outside just (justfile header exports).
  • Leaked VMMs and proxies hold their sockets. A VM in krun_start_enter ignores SIGTERM. A Ctrl-C'd or killed boot can leave a detached __krun-vmm and the gvproxy switch alive, and then every later boot fails. A bep serve proxy that outlives its boot holds its listener socket and that boot's token. Reap with just reap (scripts/reap-vms.sh), which SIGKILLs this checkout's stranded minvmd, __krun-vmm, gvproxy and bep serve processes. It matches only this checkout's binaries, so parallel checkouts are safe.
  • min vs minimal naming. The CLI crate is minimal but its binary is min; build with -p minimal, run target/debug/min. Cargo never removes old binary names, so a stale pre-rename target/debug/minimal can linger; don't invoke or PATH-resolve it. On macOS, minimal on PATH is typically the distribution shim (~/.minimal/shim/bin/minimal), distinct from anything this repo builds (justfile comments, crates/minvmd/README.md).

CI lane map

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.

Conventions and hard rules

  • Never commit to main. Branch first, with a feat/, fix/, docs/, chore/, or refactor/ prefix.
  • Conventional Commits, enforced by commitlint: docs/commit-conventions.md.
  • Rust standards: docs/rust-coding-standards.md.
  • Specs: work carrying kind:epic gets one, at docs/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-prose lints the markdown your branch touches (committed against main, staged, unstaged, untracked), just lint-prose <file.md> lints specific files, and just lint-prose --all lints the tree. Drive files you edit to zero, and leave untouched files' alerts alone rather than mass-rewriting them. .vale.ini pins the third-party styles. The vocabulary and the first-party rules live under styles/.
  • .github/workflows/ is frozen and CODEOWNER-gated. Do not edit it. Extend CI coverage through convention-discovered tests, scripts/, and the justfile, 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).

User-facing text

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.toml setting 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 dash or 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.

Pre-PR verification

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 ci runs the darwin-capable scope, and just test-cross covers the Linux-only crates via cross (Docker required). A change touching minimald or the min CLI's tests is unverified until just test-cross has run.
  • VM/daemon-path changes: also run just e2e (the session proof) and/or just test-vm (the VM integration harnesses).
  • Tight iteration loops: just fix (fmt → clippy --fix → fmt, with --allow-dirty so it runs mid-edit) then just test. Both carry this host's scope; neither needs a hand-written cargo line. Everything that loop prints is something to fix; the strict gate is not in it.
0