The closed build#
The claim from the security model — an artifact that is not in the lock cannot enter a classpath — is worth more than words. This page shows the code that enforces each part of it.
No manifest on the hot path#
Every command that builds a classpath (test, run, nrepl, exec,
build, …) goes through the same two Go steps:
- Staleness check —
lockfile.Stalere-hashes thedeps.ednof each module as raw bytes and compares it to themanifest_sha256of the lock. No EDN parsing: the Go binary has no EDN reader in the hot path at all (the one bounded literal reader is for CLI arguments). A changed manifest is a changed plan: the command exits 3 and points atrig lock. - Classpath assembly —
classpath.Buildreadsmodules[].classpath(or the classpath of the alias) from the lock and expands each entry. It expands an artifact id against theartifactslist of the lock, and a{"local": …}reference against themodulesof the lock. An entry that resolves to nothing is an error. The classpath order is exactly the order of the lock.
The manifest can add nothing: the build never reads it.
The lock is validated, not trusted#
lockfile.Validate runs before anything uses the lock:
- the schema version must match.
- every
mvnartifact carries a well-formedsha256, and everygitdep a commit sha. - every classpath entry of every module and alias must reference an artifact id or a module that exists in the lock.
- a pinned JVM version must satisfy the requested one.
A hand-edited or corrupted lock fails validation and the command exits before touching the network.
Only the Go binary writes hashes#
The resolve response of the kernel has no hashes. After it returns,
completeLock in the Go binary:
- fetches every
mvnartifact throughfetch.GetNew, in parallel, and computes the sha256 over the exact bytes. - stamps the shas into the lock.
- runs
Validatebefore it writesdeps.lock.
From then on, the sha256 in the lock is a fact about bytes the Go
binary held — not an assertion from a repository, a POM, or a
pre-existing ~/.m2.
Where bytes come from — and the gate at each source#
For a pinned artifact, fetch.Get tries sources in order; each source
has its own gate:
| Source | Gate |
|---|---|
Rig cache (artifacts/<sha256>) |
the entry is re-hashed before use — a content-addressed file that does not hash to its own name is a mismatch, not a cache hit |
~/.m2/repository copy |
accepted only when its bytes hash to the locked sha. A poisoned m2 copy is skipped, never used |
Download from the lock's url |
the body is hashed as it is written to the cache; a difference from the locked sha aborts with exit 4 — no self-heal, no retry with a different sha |
At first lock, the sha does not exist yet, so the gates differ: nothing
exists to hash against. Rig tries the url→sha recorded by the cache
(re-verified), then the m2 copy only when self-consistent, then a
fresh download. Self-consistent means its content matches the Maven
.sha1 sidecar stored beside it. Whatever source wins, Go computes the
sha256 over the bytes it keeps.
Rig pins the URL too. Go fixes artifacts[].url at lock time
(repo attribution), so a hot build
only ever downloads from a URL that appears in a reviewed lock diff.
Changing a repository changes the lock.
check stage 1: tampering surfaces, never heals#
rig check's first stage uses VerifyGet, the security-gate variant of
the fetch. If a cache entry fails its hash, check reports a
mismatch. It never re-fetches the entry: a re-fetch could replace
tampered bytes with clean ones and hide the incident. check obtains
missing entries from m2 or the repository, and checks them the same way.
That is the difference between "the build works" and "the build proves
what it ran."
Git dependencies: commit-pinned, provisioned on demand#
A git dep is an artifact pinned by commit sha — the full 40-hex
sha — expanded from ~/.gitlibs/libs/<group>/<name>/<sha> (or
$GITLIBS/libs/...). The hot path checks for the checkout. When it is
missing and the command is online, the command provisions it. The
kernel op git-procure runs the same tools.gitlibs procure the lock
runs (the mirror clone, plus a worktree at the sha). Provisioning is
idempotent: it adopts an existing checkout, and never re-clones one. Under
--offline, a missing checkout fails and points at the gitlibs cache.
Git sync stays serialized behind one JVM-wide lock
(rig.resolver.git-sync): tools.deps expands subtrees in parallel,
and the tools.gitlibs check-then-act cache is not thread-safe. So the
kernel wraps its entry points.
Offline and the exit codes#
| Flag / state | Effect | Exit |
|---|---|---|
--offline |
no network, ever: cache, m2, and existing gitlibs checkouts only | 1 when an artifact is in neither |
| stale lock | the hot commands fail and point at rig lock; check reports the stale module instead |
3 (check: 1) |
| hash mismatch | abort, no self-heal | 4 |
| cooldown refused everything | rig lock fails; retry with --force |
5 |
| git checkout missing, command online | the hot command provisions it (the kernel op git-procure) |
0 |
git checkout missing, --offline |
the command fails and points at the gitlibs cache | 1 |
--offline applies to the kernel too. rig lock --offline refuses
rather than probe repositories. rig outdated --offline is a straight
error when the lock has pins to compare, because it needs the metadata.
Drift without re-resolving: rig check#
rig check never re-resolves and never writes the lock. Its kernel stage
compares the manifests against the lock:
| Problem | Severity |
|---|---|
stale-lock — a requirement the lock's pin does not satisfy, or a manifest whose bytes changed since the lock was written |
error |
conflict — two modules require mutually unsatisfiable versions |
error |
drift — a module's requirement differs from the workspace's shared one |
warn |
floating-version — a manifest still says RELEASE/LATEST |
error |
git-ref — a git coordinate without a full 40-hex :git/sha |
error |
no-lib — publish enabled without a :rig/lib coordinate |
error |
unknown-repo — the lock pins an artifact via a repo no manifest declares |
error |
Rig checks version ranges ([1.0,2.0)) with the same Maven comparator
as resolution. So a range requirement and its pin agree or disagree the
way they do everywhere else in the ecosystem.