burrow-engine
September 6, 2026 · View on GitHub
The Burrow core — the engine that powers everything Burrow.
burrow-engine this repo: the Rust library where Burrow's real logic lives
├── burrow-cli the CLI that wraps the engine (agents, scripts, CI)
└── Burrow the GUIs (macOS, Windows) that wrap the engine
This Rust library and binary implement Burrow's command logic and shared JSON contracts.
The historical bash/Go implementation lives in
burrow-digger.
Burrow-owned code uses FSL-1.1-ALv2. Adapted upstream portions and build dependencies retain their own notices in THIRD-PARTY-NOTICES.md. This public snapshot uses anonymized contract fixtures; their origin and preserved invariants are documented in FIXTURE_PROVENANCE.md.
Design rules
-
Near-zero dependencies. Everything that links the engine inherits its dependency tree, so the tree is
stdplus two sanctioned, documented exceptions, each justified inCargo.tomlbeside its declaration:image(PNG/JPEG decoding, default features off) —photoshas to decode pixels for its perceptual hash, and no subprocess does that cleanly.windows-sys, target-scoped under[target.'cfg(windows)'.dependencies]—neton Windows attributes connections to processes through IP Helper. A macOS or Linux build does not compile or link it.
Everything else is a subprocess plus a hand-written parser, or a direct
extern "C"into the system library (geteuid,host_processor_info). Adding a third crate is a design decision, not a convenience, and needs the same paragraph inCargo.toml. -
Pure first. Logic lives in pure, unit-tested modules. Anything that spawns a process does so through an injected runner (
platform::Runner/TimedRunner,net::Runner,dupes::FclonesRunner,uninstall::bundle::Runner), so every decision — argv, budgets, fallback order, what a failure means — is driven by a fake in tests and the real spawn is one thin function.src/cli.rsis argv parsing plus the envelope and nothing else; a command's orchestration lives in its own module (uninstall::apply,clean::execute,purge, …). Process plumbing itself (run_command*,CommandFailure) isplatform's. -
Three wire shapes, all defined in this crate.
- The envelope for every buffered result:
{ok, burrow_cli, engine, command, data | error{kind, message}}(src/envelope.rs). One contract, defined here and nowhere else. - NDJSON event lines for
clean --stream(with or without--plan),purge --stream,optimize --stream,status --watchandanalyze --progress: one JSON object per line, flushed as it happens, ending in a terminal line.clean/purgeshare one vocabulary (would_remove…done{dry_run:true,…};removed/failed/protected…done{…}),optimizemirrors it (would_run/task…done),status --watchemits the buffereddataobject per tick, andanalyze --progressemits{type:"progress",…}ticks then{type:"result", data}. The serializers are pure (clean::stream,optimize,analyze::json);cli.rsonly writes and flushes. - The bare JSON array of
uninstall --list— oracle-defined (bin/uninstall.shprints a top-level array and the app'sMoleClient.parseAppsdecodes exactly that), so it is the one command that is not enveloped.
Every hand-written
to_jsonescapes strings through the crate's single escaper (json::escape); field names are checked character by character against the goldens undersrc/**/*.golden.json. Their public copies preserve captured structures and safety verdicts while replacing private identities, as documented inFIXTURE_PROVENANCE.md. Never regenerate expected results from the engine under test to make a failing test pass.Run
python3 scripts/check_fixtures.pyandcargo testto check the published contracts. The fixture provenance records the historical reference implementations; tests must preserve the established field names, omission behavior and NDJSON event vocabulary. - The envelope for every buffered result:
clean --plan <file>
clean scans; clean --plan <file> does not. The GUI runs the dry run, shows every candidate,
lets the user untick some, writes what is left to a file, and hands the file to the engine —
so what gets removed is what was reviewed, not what a second scan happens to find.
<file>is UTF-8 text, one absolute path per line; blank lines and#comments are ignored.- The engine removes only the listed paths, in file order, each through the same guarded
remover as
clean --apply(protection tables, whitelist,validate_path_for_deletion, Trash or--permanent, verified absence). Neither planner runs. - A listed path is refused unless the clean target table could have enumerated it on this
machine (component-wise against the same table
cleanscans, resolved against the same home): reported asprotectedwith reasonnot_a_clean_target. A covered path the planner's own rails would have skipped is refused with reasonprotected. - Without
--apply(or with--dry-run) it is a dry run over the same list with the same refusals;--apply --dry-runis the usual contradiction.--streamemits the same NDJSON lines as the scan's stream, with refusals as{"event":"protected","path":…,"reason":…}. - Output is the scan's shape plus
"plan": {"file", "listed", "refused", "missing", "refusals": [{"path", "reason"}]}. History and byte accounting are the onesclean --applywrites.
Environment
| Variable | Effect |
|---|---|
BURROW_HOME | The user's home, highest precedence over HOME/USERPROFILE. The app's privileged helper sets it when it launches the engine elevated, because under sudo HOME is root's and every ~-relative scan would answer for /var/root; a resolved root home without it is refused rather than cleaned. |
BURROW_PRIVILEGED | 1 when the privileged helper launched the engine. With it (or an effective uid of 0) helper binaries — fclones, brctl, trash, and the developer tools clean may spawn (uv, go, pnpm, pip3, …) — are resolved only from trusted locations, never from PATH. |
BURROW_TOOLS_DIR | An extra directory helpers may be taken from in privileged mode — honoured only when it lies inside the engine binary's own directory (the app bundle's Resources/). |
BURROW_WATCH_FRAMES | Bounds status --watch to N frames (tests and scripts); unset means until stdout closes. |