This release is a pre-release and may not be stable for production use.
bosn — let your agents use Docker all day without filling the disk
An AI agent working a long-horizon task runs the same containerized build hundreds of times. Docker keeps every image, volume, and container it is told to make, and has no notion of "this one is finished." So the agent's disk usage only goes up — until the day the engine dies with the machine full.
bosn is the thing that knows when a container resource is still in use and when it may
be deleted, so a build cache stays warm across hundreds of runs while total storage stays
bounded — with nobody remembering to clean up.
It is a CLI plus a background daemon that owns the containers, volumes, and images your workflow creates. Every resource it makes is labeled with who owns it, what content it was built from, and which workspace it belongs to; whether anything is using it right now is a lease in bosn's own database. Storage is bounded by three ceilings — 1,000 resources, 100 GiB managed, 10 GiB free — and eviction re-checks after every delete, so crossing the byte ceiling trims back under it instead of clearing the cache.
Also useful if you are not an agent: CI runners with persistent workers hit the same
rebuild-forever shape, monorepo teams get the machine-scoped cache sharing described below,
and anyone who has ever run docker system prune and destroyed a cache they wanted gets a
policy instead of a blunt instrument.
Status
Working: the manifest, generation digests, converge-then-run, the sqlite registry, the daemon singleton, leases, adoption (including
--legacyimport), tiered GC, and daemon-owned build jobs withbosn attach. All covered end to end against a real Docker engine on Linux CI.Partial: the Docker front door —
bosn-docker initandbosn-docker compose {up,down,logs,ps}only, over a small compose subset. See Coming from Docker.Not built: podman;
docker/docker-composeshims; and BuildKit's own layer cache, which is outside the label contract — bosn manages containers, volumes, and images, and never prunes the builder.⚠ WSL is refused outright.
bosnexits non-zero inside WSL — its Windows loopback daemon is unreachable from there. Use a native Windows shell, macOS, or Linux. (The check lives inbosn;bosn-dockerdoes not yet refuse it. The incident below happened to a WSL VHDX; bosn runs on the Windows side of that boundary, not inside it.)Full v3 design: issue #1; bringup plan: issue #3. Issue #1 predates the rename and calls the project dockhand; the design is unchanged.
The failure this prevents
This is not hypothetical. It happened, on one developer machine, in the summer of 2026.
Every agent-created git worktree spawned its own five-volume Rust cache group (target,
CARGO_HOME, RUSTUP_HOME, cargo-chef, .soldr). Retained history showed 268 Docker
invocations and zero cleanup calls. Nothing had a TTL, so nothing was ever deleted:
| Date | State |
|---|---|
| Jul 25 | 60 volumes, 141 GB |
| Aug 7 | 101 volumes, 273 GB (+ 89 GB images, 38 GB writable layers) |
| Aug 9 | ~312 GB of volumes; host volume under 1 GB free |
| Aug 13 | 452.6 GiB VHDX, 804 KiB free — ext4 journal aborted, read-only remount, engine dead |
Recovery took elevated privileges, a full WSL shutdown, and deleting the Docker data VHDX: 452.6 GiB restored by destroying every image, container, and volume on the machine.
The lesson is narrow and specific: a dry-run cleanup command that nobody invokes is not a
lifecycle. docker system prune is not a policy, because something has to decide when
to run it and what is still needed — and neither an agent nor a busy human ever does.
First run
uv tool install git+https://github.com/zackees/bosn # puts `bosn` and `bosn-docker` on PATH
Write a bosn.toml at your repo root:
[stack.test]
dockerfile = "docker/test.Dockerfile" # this file, and everything it COPYs, form the digest
family = "rust" # the machine-cache sharing key — see below
default = true
[stack.test.volumes]
target = { scope = "stack" } # per (workspace, stack) — survives digest changes
cargo-reg = { scope = "machine" } # per (family, volume) — shared across every repo
rustup = { scope = "machine" }
[task.unit]
stack = "test"
cmd = "sh test" # run as `sh -c`; the image needs whatever you name
Then:
bosn ensure # converge and register without running anything (pre-warm)
bosn unit # run the task
bosn status # what exists, what holds it, bytes vs ceiling
Nothing needs starting — the first command lazily spawns the daemon.
For unattended cleanup, install the login launcher once:
bosn __daemon --autostart # systemd user timer / launchd plist / Windows Startup entry
This matters more than it looks. GC runs inside the daemon every 5 minutes, but the daemon
idle-retires after 15 minutes and is only respawned by the next command. Without the login
launcher, cleanup happens only while you are actively using bosn. (On macOS the plist takes
effect at next login; only Linux enables the timer immediately. On Windows the launcher is a
Startup .cmd loop, so it leaves a console window open.)
The execution model — build time vs run time
This is the part most likely to surprise you, and it decides how you write the Dockerfile.
Declared volumes exist only when a command runs. docker build never sees them.
| Path | What it is | When |
|---|---|---|
/bosn/<volume-name> |
each declared volume, read-write | command execution only |
/bosn-daemon/heartbeat |
the daemon's liveness file, read-only | command execution only |
That is the entire mount set, and it has three consequences:
Your source tree is not bind-mounted. Your Dockerfile COPYs it in, which is what makes
the content digest meaningful — and it means editing a copied source file rolls a new
generation and rebuilds. That is the model, not a bug.
Anything you want cached during the build needs a BuildKit cache mount
(RUN --mount=type=cache,...), not a bosn volume. bosn does not manage the builder cache
(see Status), so that cache is outside its accounting — it is also the one thing bosn will
never delete out from under you.
Declaring a volume wires it to nothing. The manifest has no env or workdir key, so
the Dockerfile has to point the toolchain at the mount:
ENV CARGO_HOME=/bosn/cargo-reg \
RUSTUP_HOME=/bosn/rustup \
CARGO_TARGET_DIR=/bosn/target
Without those lines the example above creates three volumes and warms none of them.
The image also needs a POSIX shell with date, stat, and sleep: the persistent
container's PID 1 is a shell loop, and bosn shell execs sh. Distroless and scratch
images will not work.
Coming from Docker
What maps to what
| You are used to | bosn | What changes |
|---|---|---|
docker build + docker run |
bosn run -- <cmd> |
rebuilds only if the content digest changed |
docker compose up |
bosn-docker compose up |
service containers get labeled and tracked; volumes do not (see below) |
docker system prune |
(nothing — automatic) | never needed, and bosn never runs it |
docker volume ls + guesswork |
bosn status |
tiers, leases, managed bytes vs ceiling |
| "is this cache still needed?" | bosn gc |
dry-run by default; shows exactly what would be reclaimed |
| deleting a worktree and hoping | bosn done |
marks it finished; its caches become collectable |
If you already have a compose.yaml
Set expectations before you run anything: the front door is a narrow subset, not a compatibility layer.
init is a line parser, not a YAML parser. It wants a top-level services:, service names
at two spaces, and an image: key. A ports:, environment:, volumes:, or
depends_on: key is a hard error — which is most real compose files. Treat it as a
skeleton generator for image-only services, not a migration tool.
bosn-docker init --compose compose.yaml # also: --output
bosn-docker compose -f other.yaml up # also: down, logs, ps
Flags belong before the verb. Anything after it is rejected outright rather than silently
ignored: bosn-docker compose up -d fails with unsupported compose flag or argument '-d'.
There is therefore no detached mode — compose up runs in the foreground.
That refusal is deliberate — silently dropping a flag, or quietly falling back to raw
Docker, would recreate the unlabeled resources bosn exists to eliminate.
Compose volumes and networks are not yet labeled. The overlay labels service containers
only, so volumes declared in your compose file stay outside bosn's accounting entirely —
counted as unlabeled, never given a TTL. Compose volumes are exactly the class of object the
incident was made of, so declare caches in bosn.toml if you want them bounded.
If you already have a pile of unmanaged volumes
That is the 101-volume situation from the incident table. adopt --legacy brings qualifying
volumes under bosn's label contract:
bosn adopt --legacy soldr # plan and report (exits non-zero); also: clud, zccache
bosn adopt --legacy soldr --yes # apply
This buys accounting, not warmth. bosn generates its own volume names and stamps every adopted volume with a sentinel generation that the first ordinary converge supersedes — so your builds will not reuse them. What changes is that volumes which were invisible junk become owned, counted, and collected on a clock, instead of sitting on the disk forever.
Eligibility is label-gated, never name-gated: a volume qualifies only if it carries its
producer's own managed=true label. Unlabeled pre-bosn volumes — much of that 101-volume
population — cannot be adopted and have to be removed by hand. Only volumes are adopted;
matching containers and images are reported and skipped. (zccache is implemented from the
design contract; no producer emitting those labels was found.)
Docker labels are immutable, so this is a staged copy, not a rename: bosn copies the
volume into scratch storage via alpine:3.20, recreates it under the new labels, and copies
it back. Budget roughly double the volume's size in free space, a reachable engine, and
stopped containers. If the disk is already full, free space first.
Adopted resources get a 24-hour quiet period before normal aging resumes.
How the disk savings actually work
Five mechanisms, in descending order of how much they save.
1. Sharing: one cache per machine instead of one per worktree
The incident's dominant multiplier was per-worktree duplication. A cargo registry cache or a rustup toolchain is byte-identical across every checkout — but Docker gives you one volume per name, so ten worktrees meant ten copies.
Every volume declares a scope, which decides what it is keyed on:
| Scope | One per | Use for |
|---|---|---|
spec |
(workspace, stack, digest) | output that must be discarded on a spec edit |
stack |
(workspace, stack) | anything you want warm across edits |
machine |
(family, volume name) |
content-addressed, identical everywhere |
Pick spec carefully. A new digest means a new, empty volume — so a spec-scoped
target/ is a cold build after every source edit, which is the opposite of what you want
from a cache. Use stack for anything whose value is being warm.
That drops the soldr stack from five per-worktree volumes to two. With ten worktrees, 50 volumes become 23 — and the shared ones are the big ones.
family is the sharing key, and it is easy to get wrong. A machine volume is named
from the stack's family, falling back to the stack name when no family is set. So two
repos both declaring family = "rust" share one cargo registry; two repos that omit
family and happen to both name their stack test will also silently share; and two
repos with differently-named stacks and no family will silently not share. Set family
deliberately.
2. Expiry: things actually get deleted, on separate clocks
Docker has no TTL. bosn gives every resource one, tiered by what it costs to recreate:
| Resource | Lifetime | Why |
|---|---|---|
| Container | idle-stop 1 h, removed 24 h | disposable — seconds to recreate from a warm image |
| Warm volume | 72 h | this is the asset: a 20-minute cold build becomes 30 seconds |
| Superseded generation | capped 24 h | the old image after you edited the Dockerfile |
| Machine-shared cache | under pressure, and only once past the warm TTL | most expensive to refill, most widely reused |
Containers are cheap and die fast; caches are expensive and live long. A single global TTL would either delete your caches or keep your junk.
3. Pressure: hard ceilings, and eviction that stops when satisfied
TTLs bound age, not total size. Three ceilings bound size — 1,000 resources, 100 GiB
managed bytes, or under 10 GiB free. Any one puts the machine under pressure, and
eviction then proceeds superseded → done → idle → pressure, taking non-machine resources
before machine-scoped ones. (Only the byte ceiling is configurable, as
shared_cache_ceiling; the other two are constants.)
The part that makes this safe to leave on: after every delete the collector re-measures and re-assesses. Once the machine drops back under the resource-count or byte ceiling, the remaining candidates flip back to "keep" and survive — crossing 100 GiB trims you back under it rather than clearing your cache. It also refuses to declare byte pressure resolved while any resource is still unmeasured, so it cannot talk itself into stopping early.
Two honest limits. Free space is probed once per pass and not re-read between deletes, so a pass triggered by the 10 GiB free-space ceiling cannot de-escalate and will evict every non-machine resource it is allowed to. And machine-scoped caches under pressure are still subject to the warm TTL — if the excess is young machine-scoped cache, GC will clear everything else and remain over the ceiling.
If the backing store's slack were larger than everything GC could reclaim, evicting warm caches would be pure damage, so the design calls for a compaction advisory instead of grinding. That measurement is not implemented: Docker Desktop exposes no reliable guest-used/host-allocated pair through its CLI, so slack is reported as unknown and the advisory is currently unreachable. Reclaiming VHDX slack is a manual, confirmation-gated operation today.
4. Supersession: edits do not accumulate
A generation is a content digest, and it is computed from more than the Dockerfile's
bytes: bosn parses the Dockerfile's logical lines — continuations, heredocs, # escape=,
JSON-form COPY — resolves every COPY/ADD source and RUN --mount=type=bind closure,
applies .dockerignore, and hashes that whole file set. It then folds in the resolved
engine IDs of every external base image, so a moved :latest tag rolls a new generation.
Two consequences worth knowing up front:
- Editing a source file the Dockerfile never copies does not rebuild. That is the point.
- Non-deterministic inputs are refused: a git
ADDneeds a full commit SHA and a remoteADDneeds--checksum, or the command fails. A digest that cannot be reproduced is not an identity.
Edit the Dockerfile and a new generation rolls forward; the old one keeps serving its live leases, then ages out as superseded under the 24-hour cap. Without this, an agent's edit-and-rerun loop leaves one image per edit, forever.
5. Leases: never delete something in use
A lease records the client PID and its process start-time, with a 15-minute TTL. It expires
only when the TTL elapses and a liveness probe says the holder is gone — and the probe
compares PID and process start time, so a recycled PID cannot keep a dead client's lease
alive. It reads /proc/<pid>/stat on Linux, Get-CimInstance Win32_Process on Windows, and
ps -o lstart= under a pinned LC_ALL=C on macOS. If the probe cannot answer, it fails
open and the resource survives.
This is what makes aggressive expiry safe. Without leases the only safe TTL is one longer than the longest build you might ever run, which is no TTL at all.
Before every delete, ownership is re-proven from the engine's own labels, not from the registry — the registry is a hint, the labels are the authority. Anything that fails that check is skipped and logged. Three buckets exist and only one is ever touched:
| Bucket | Meaning | Treatment |
|---|---|---|
| owned | complete label contract, our registry id | eligible for collection |
| foreign | complete contract, someone else's registry id | counted and reported, never deleted |
| unlabeled | incomplete or absent labels | invisible to every decision |
Name prefixes are never ownership proof. This is also why there is no gc --force.
Why an agent needs this specifically
A human runs a build, looks at it, and moves on. An agent runs the same build after every edit, for hours, unattended.
A build must outlive the process that asked for it. bosn run submits the build to the
daemon and streams build output to stderr as it happens. If the agent's timeout fires and
kills the CLI, the build keeps going — re-run the same command and you reattach. A 20-minute
build must not die because a harness gave up at 10.
Failures are machine-readable. Every verb takes --json and returns a stable envelope on
error — {"ok": false, "code": ..., "message": ..., "next": ...} — where next names the
remedy. tasks, gc, and adopt return specific codes (manifest.invalid,
registry.unreadable, daemon.unreachable, policy.invalid); the remaining verbs currently
collapse to a generic command.failed with the diagnostic in message, and are being
migrated one at a time. Argparse errors come back as parse.invalid. An agent gets a
branchable result instead of prose to regex.
Nothing fails silently. A superseded, dropped, or cancelled request exits non-zero with a
message naming what happened: 4 superseded, 5 cancelled, 1 build failed. Note that
bosn run returns your command's exit code, so a test suite that itself exits 4 or 5 is
indistinguishable from these.
Known rough edge: your command's own output is buffered and printed after it exits, and stdout wins over stderr when both are non-empty — so a long test run shows nothing until it finishes. Build output streams; command output does not.
Rapid edits must not pile up — which is the concurrency policy below.
Build jobs and the concurrency policy
Each edit is a new digest against the same stack, so requests neither join (not identical) nor parallelize (same key). Left alone they queue without bound, every entry but the last obsolete on arrival, each pinning volumes against GC.
The rule: per (workspace, stack), at most one running build and one pending request.
| Request arrives while... | What happens |
|---|---|
| nothing is in flight | it builds |
| the same digest is building | it joins — one build, many watchers |
| a different digest is building | it takes the pending slot |
| the pending slot holds the same digest | it joins that |
| the pending slot holds a different digest | it replaces the occupant, which ends as superseded |
The bound is structural rather than tuned, and the worst case is waiting out one obsolete build — which warms BuildKit's layer cache for the one that follows. Cancelling in-flight builds so the newest always wins is more responsive but can livelock: under a fast edit loop every build is killed before finishing and the agent never gets output. Rejecting the second request turns a queue problem into a retry loop, which this project treats as a bug.
Superseding only ever drops a build that has not started, so cancellation never happens
behind your back. Stopping a running build is always deliberate — bosn cancel, daemon
shutdown, or the per-job TTL.
- Distinct keys build in parallel. Two worktrees never block each other.
workspace-idis the resolved manifest root, never the cwd, so two agents in different subdirectories of one worktree correctly share a key. - Total concurrency is capped at
max(2, cpus/2)builds machine-wide (BOSN_MAX_BUILDS). - A cancelled build leaves nothing behind. Registration happens only after
docker buildexits 0, so a killed build cannot leave a generation row implying a usable image. - A hung build cannot pin the daemon. A per-job TTL (default 1 h,
BOSN_BUILD_TTL_SECONDS) reaps anything that stops reporting builder progress. - Job history is bounded too. The 50 most recent finished jobs stay listable; older ids
report
no such job. Build logs keep the last 5,000 lines, so a lateattachsees the tail.
There is deliberately no --detach. Blocking-with-attach is the only mode, because
backgrounding is the easiest way to recreate the pile-up above.
Command reference
bosn unit # run a manifest task by name (converges first)
bosn run -- cargo test # ad-hoc command in the default stack
bosn shell # interactive session in the persistent container
bosn ensure # converge and register without running anything
bosn tasks --json # tasks, stacks, content digests, registration state
bosn status # tiers, leases, bytes vs ceiling, foreign registries
bosn jobs / bosn attach <j> # daemon-owned builds that survive a killed CLI
bosn cancel <j> # stop a build you no longer want
bosn done # this workspace is finished; its caches become collectable
bosn gc # dry-run by default; --apply to reclaim (there is no --force)
bosn doctor # engine reachability, registry integrity, recovery commands
bosn adopt --legacy <family> # import pre-bosn volumes; --yes to apply
bosn __daemon --autostart # install the login launcher (also --no-autostart)
bosn __daemon --stop # stop the daemon — needed after upgrading bosn
status, tasks, and doctor read the sqlite registry directly, so they keep working when
the daemon is wedged. gc and jobs are read-only but go through the daemon and fail closed
if it is unreachable.
After you upgrade bosn, stop the daemon. A running daemon refuses every mutating verb
whose client reports a different version, so the next bosn run fails until you restart it.
What bosn done actually deletes
It marks the workspace finished. At the next GC, every non-machine resource whose generations
are all current is collected immediately. Anything still carrying a superseded
generation — which after any Dockerfile edit means the persistent container, the
stack-scoped volume, and the previous generation's target/ — falls to the conservative
superseded path instead and goes at the 24-hour cap. Machine-scoped caches survive either
way, and live leases outrank all of it, so a running build is never cut off.
bosn accepts only this first-party signal. Deriving "finished" from git state — worktree
missing, branch gone, PR merged — is the integrator's job; clud calls bosn done at worktree
teardown. Dirty work is never destroyed on inference.
Architecture
A bosn CLI and a bosn __daemon singleton, following the soldr/zccache pattern: its own
repository, release cadence, and failure domain. State is sqlite in WAL mode — chosen
over redb for failure shape, since WAL readers never block behind a hung writer. The daemon
is the registry's only writer and the only executor of reap and GC; it is lazily spawned,
autostarted at login, and idle-retired with a scheduled catch-up tick. While the engine is
unreachable it backs maintenance off from 30 s to an hour rather than spinning against a
dead Docker.
The daemon is a loopback TCP listener, authenticated by a 32-byte token written to a
0600 file and compared in constant time; unauthenticated attempts are logged. (The mode bit
is effectively a no-op on Windows.)
One user per machine, in practice. State is already per-user; what collides is the port.
The default state directory always maps to the fixed port 47764, and only BOSN_PORT — or an
explicit --state-dir pointing somewhere other than the resolved default — moves it. Setting
BOSN_STATE_DIR alone does not, because the port lookup resolves its default through that
same variable. More importantly for the disk story: machine-scoped volumes are Docker-global,
but each registry sees the other's as foreign — counted, never collected. bosn cannot bound
another user's growth.
Configuration
Machine policy lives in ~/.config/bosn/config.toml (or $XDG_CONFIG_HOME/bosn/config.toml;
set BOSN_CONFIG to select another file). Values use default < file < environment < CLI flag
precedence, and invalid values stop the command while naming the bad key. The [policy] table
accepts container_idle_stop, container_remove, warm_volume_ttl, superseded_cap,
shared_cache_ceiling (really the total managed-bytes ceiling), run_max_duration (default
8 h, the wall-clock cap on command execution), idle_retire_seconds, build_ttl_seconds, and
max_builds; environment overrides are their uppercase BOSN_ equivalents. bosn status
reports each effective value and its origin.
Enforcement is layered: the daemon is the authority but never the only mechanism. The persistent container's PID 1 watches the bind-mounted daemon heartbeat and exits once it has been stale for 10 minutes, so containers drain even if the daemon dies.
Registry recovery
Run bosn doctor after an unclean shutdown. It reports engine reachability, whether the
autostart launcher is installed, the next maintenance deadline, and registry integrity — and
when the database is damaged it prints the exact non-destructive backup-then-recover commands
for your path, deliberately leaving the original untouched. If it finds complete labels from a
prior registry it prints a source-specific bosn adopt --from-registry <uuid> command. That is
lost-database recovery only: it restores an identity into an empty registry and never replaces
the identity of a registry that already has rows.
Integration
clud ships a thin clud bosn … forwarder that passes argv verbatim to the executable, exactly
as it shells out to soldr, pinned to a bosn release. The second seam is the worktree-teardown
hook that calls bosn done. Both degrade safely when bosn is absent.
Development
git clone https://github.com/zackees/bosn && cd bosn
./install # install uv if absent, then `uv sync --all-groups`
./lint # ruff format --check, ruff check, pyright, KeyboardInterrupt checker
./test # pytest; passthrough args work, e.g. ./test -k registry
./install sets up the development environment; run the CLI from the clone with
uv run bosn. Linting includes an AST checker (ci/lint_kbi.py) that enforces a sibling
except KeyboardInterrupt beside every broad except Exception, because a blind except
otherwise swallows Ctrl-C.
CI runs Linux, Windows, and macOS lanes on every pull request. Docker-backed tests carry a
docker pytest marker and run on Linux only — hosted macOS runners have no Docker daemon and
hosted Windows runners cannot run Linux containers, so real Docker Desktop coverage there
would need a self-hosted runner. The native lanes are advisory until they are added to branch
protection's required status checks.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bosn-0.1.0a1.tar.gz.
File metadata
- Download URL: bosn-0.1.0a1.tar.gz
- Upload date:
- Size: 176.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3c07308df77a607d57b01e5ebb330e6dd1e4b2e526f8a9ce1ccb9674570391c
|
|
| MD5 |
19a9dcd18e17801f67c5e14da164fbd5
|
|
| BLAKE2b-256 |
2f62f09fb3af58940824779a74ba0ca89d2d4694c6bbdafda47cc61a0f70091f
|
File details
Details for the file bosn-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: bosn-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 110.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a32ec96c4a5519551eede3467871bb2b5934ec47a767f236a372acd8638fc13
|
|
| MD5 |
175bab586dbf3c572e7305c32a9abeb9
|
|
| BLAKE2b-256 |
14533e8ff09dbc6d2b6498ee447984049897ab14d84720ae8a8321baab7f5e58
|