mc tool — install and run a program package
A tool is a package of kind = "exe": a program mc tool install builds on your own machine
with the compiler that is running, and mc tool run runs — boxed under the permissions the package
declared and you confirmed, on a host that has a sandbox, and directly on one that does not. It is
the package half of the registry aimed at programs rather than libraries (a library is
kind = "obj" or has no [project], is compiled into your own code, and is never installed).
This page is the reference; the everyday view is ../guide/25-packages.md
and the manifest keys are in packages.md § 3 and toml.md § [[permission]].
The commands #
mc tool install NAME[@VERSION] resolve, show the plan and the permissions, fetch, build, launch
mc tool install [DIR] every tool a project's lock names
mc tool list what is installed
mc tool remove NAME unlink the launcher and the manifests
mc tool upgrade [NAME] the newest of the same major, then install
mc tool run NAME [-- ARGS] what the launcher calls
mc tool box-args <mc.toml> (hidden) the derived sandbox flags, one per line
The flags — --yes, --registry, --libs-dir, --bin-dir, --workspace — are in
cli.md § 3g.
mc tool install [DIR] reads the project's lock, not its [tools] table: the version
installed is the one mc pkg sync pinned. A project whose only package table is [tools] is a
project WITH dependencies — the lock is read for it exactly as it is for a [deps] one, and
without one the answer is mc.lock is stale and its run: line, not silence. The constraint in
[tools] is still checked at its own file:line:col before anything is read or fetched, with
the same message [deps] gets (`a version constraint must be X.Y.Z, =X.Y.Z, ~X.Y.Z, ^X.Y.Z,
=X.Y.Z, or *`, exit 2).
install, step by step #
- Resolve. The tool is the root of the same MVS graph
mc pkguses; its own[deps]are libraries. A name that is a library, not a tool, is refused. - The plan is the prompt.
mchas no prompt; it prints the archives it would fetch and a permission table — one fixed sentence per permission, from the index row, on screen before a byte is fetched — and stops.--yesaccepts both. - Fetch into
<libs>(~/.mc/libs, or--libs-dir), exactly asmc pkg syncdoes. - Stage a buildable copy under
~/.mc/tools/<name>/v<ver>/:mc.tomland the declared files,deps/<lib>/for every library, and anmc.lock— the vendored shapemc buildrehashes. - Build it by spawning this compiler (
mc build), the same trust amc buildof any project has; the tool it produces is what runs boxed later. - Record and launch. The install manifest
~/.mc/tools/<name>/v<ver>.tomlis written last (toml.md §[tool]), and a one-line launcher goes in~/.mc/bin/<bin>(--bin-dir).
[replace] is not consulted #
[replace] name = "../tree" (packages.md § 7) points a name at a local tree for
the build to compile. mc tool ignores it, for the tool and for the libraries staged beside
it alike: a tool is not compiled into your program, it is a program installed from the registry at
the version the lock pins, and mc build already skips every tool row before [replace] is
consulted. A replaced name in the build list is announced once, above the plan:
note: [replace] hello_tool = "../toy" is ignored by mc tool: a tool is installed from the registry
To develop a tool against a local tree, build it where it lives (mc build <tree>) and run the
binary directly; the installed copy is always the registry's.
There is no --unconfined (the M48 amendment): the plan, the table and the acceptance are the same
on every host. On a host without a sandbox the plan adds one line —
note: this host has no sandbox; the permissions above are recorded, not enforced — and the
manifest records sandbox = false. Nothing else differs until run.
The ~/.mc layout #
~/.mc/libs/<pack>/v<ver>/ the fetched trees (shared with mc pkg)
~/.mc/tools/<name>/v<ver>/ the staged, built copy: mc.toml, files, deps/, mc.lock, build/<bin>
~/.mc/tools/<name>/v<ver>.toml the install manifest, written last
~/.mc/tools/installed one line `<name> <version> <bin>` per install (mc has no readdir)
~/.mc/bin/<bin> the launcher (macOS/Linux: a short sh script; Windows: <bin>.cmd)
--bin-dir and --libs-dir override the two roots so CI can run with no HOME; the tools root is
the parent of <libs> with tools/ on it, and the default bin directory the same parent with
bin/. remove unlinks the launcher and the manifests; mc has no rmdir, so the directories
stay, and mc tool run afterwards is is not installed.
The launcher #
#!/bin/sh
exec "/path/to/mc" tool run hello_tool --libs-dir "/path/to/libs" -- "$@"
One line. It names the compiler that installed it by absolute path — so a tool keeps working when
mc is not on PATH — and it embeds --libs-dir only to locate the install (the tools root is the
parent of <libs>); it carries no sandbox flags, so the way permissions map to a box can change
with no launcher regenerated. All the logic is in mc tool run.
run — boxed here, direct there #
mc tool run NAME reads the newest installed version's manifest and:
- on a host with a sandbox (Linux), derives the
mc sandbox execflags from the manifest's permissions and spawns the box, stdio inherited, exiting with the child's status; - on a host without one (macOS, Windows), runs the binary directly.
The decision is made at run time, not from the manifest's sandbox field — that field is only a
fact mc tool list shows.
The workspace #
Several permissions are relative to a workspace: the directory the tool is invoked from
(--workspace DIR, default the current directory). An editor sets it to the project folder; a shell
to wherever you are. $HOME and / are refused without an explicit --workspace, so a tool run
from your home directory is not silently handed every file you own.
The one mapping: permissions to sandbox primitives #
mc tool box-args <mc.toml> prints the flags a permission set derives to, one line per permission —
the single definition, shared by mc tool run and by a registry validator (which spawns exactly
this verb). The mapping (sandbox.md § The primitives):
| permission | flags |
|---|---|
fs.read <path> | --ro <resolved> --at-path |
fs.write <path> | --rw <resolved> --at-path (a home/<rel> directory is created first) |
fs.read tmp / fs.write tmp | --tmp |
net | --allow=net |
exec NAME | --bin NAME |
env NAME | --env NAME |
| (none) | the install tree read-only, an empty netns, the program profile |
workspace resolves to the workspace directory, workspace/<rel> and home/<rel> to a directory
beneath it or beneath $HOME; --at-path binds each at its own absolute path inside the box, so a
tool handed an absolute path (as an editor hands a language server file:///…) can reach it, and a
path outside its granted roots is sandbox: refused: open <path>, exit 125.
The (none) row — a tool that declares no permission — runs with its own install tree bound
read-only (--ro <tree> --at-path) and with no granted writable root and nothing of yours bound
at all: it can read nothing of your files and create nothing that survives. The one residual is that
mc sandbox exec always overlays the binary's own directory as /src, a copy-on-write overlay,
so the tool's current directory is technically writable — but every write lands in an ephemeral upper
layer on the box tmpfs and the host tree is never touched, and the writes are discarded when the box
exits. A literally read-only /src for exec is a future sandbox mode; the security property a
zero-permission tool must have — it cannot reach your files and cannot persist anything — holds today,
and scripts/check-tool.sh proves boxed that it cannot create a host-visible file.
Boxed vs direct, and what is verified #
On Linux a tool that reads a file outside its fs.read root is stopped by the box, and one that
opens a socket without net or forks without exec is refused — the C2 sandbox around it. On macOS
and Windows there is no box (see sandbox.md § Hosts), so the permissions are recorded
and not enforced; the same install, the same manifest, and a tool you chose to install runs. What
the registry verifies about a tool, and what it cannot verify about a library, is in
packages.md § Permissions.
Manifest integrity against a hostile package #
A tool package's mc.toml is attacker-controlled — it comes from a registry — so the compiler treats
every field of it as untrusted:
- Values written into a generated manifest are escaped.
mc tool installrecords the tool's[project].out, name, binary and permissions into~/.mc/tools/<name>/v<ver>.toml, and the lock and cache-manifest writers record a fetched tree's own strings. Each free-text value is passed through a TOML basic-string escaper first, so a value carrying a"and a newline — e.g.out = "app\"\n[tool]\npermissions = [...]"— becomes one escaped string and cannot splice a second table or key into the file. A control byte in such a value is refused outright. mc tool runre-validates the stored permissions. Before any permission is mapped to a sandbox flag, every line in the install manifest is checked against the same rules a[[permission]]row passes at install — a syntactically valid,..-free, containment-checked line — and a manifest carrying anything else (a hand edit, a corrupted file) is refused withthe install manifest carries an invalid permission: <line>. No unrecognized string ever falls through to "the whole workspace, writable".- A version is a version, not a path. A
[[versions]].version(or a lock or installed-index version) that is not SemVer's charset —[0-9A-Za-z.+_-], no leading dot, no..— is refused before it becomes~/.mc/tools/<name>/v<version>/or<libs>/<name>/v<version>/, so a craftedversion = "../../../../tmp/evil"cannot stage or build a tree outside the package root. - A permission set larger than the box is refused before you accept it. A set that maps to more
read-only roots, writable roots,
--binprograms or--envvariables than the box accepts is refused at install (too many fs.read permissions for the box: N (at most 16)), rather than being accepted and locked and then refused at every latermc tool run.