# Copying code on purpose when applications can’t share a crate

Author: Chris Chabot
Canonical URL: https://chabot.dev/blog/copying-code-on-purpose/
Published: 2026-11-03

When an application may link only one crate, shared code has to be copied. A canonical source tree, declared mounts, file hashes and a drift check keep the deployed copies aligned.

In August 2026, an application deployed on Ferrite, my Rust application platform, could depend on exactly one crate. That crate was `ferrite-app`, the SDK, and the platform's code gate treated every other entry under `[dependencies]` as a blocking violation. The rule is deliberate: the surface injected into an application is that application's authority, so an application that could link a second crate could reach the world through it. For the same reason the gate matches on the dependency's name and never asks where it resolves from, which means a path dependency inside your own workspace is refused exactly as a crate from a registry would be.

The consequence is unavoidable. Two applications that need the same subsystem cannot share a crate, so the only thing left for them to share is source.

When I wrote the design document for what came next, they were already sharing it, by hand. Two applications each carried a copy of the same WebAssembly interpreter, 35 files apiece, and 7 of those files had drifted. Two agent runtimes had 25 file paths in common, and 15 of those no longer matched. One difference between the runtimes came to 1,888 lines in a single tools module, and that one wasn't drift at all: one application ran its agent work in process, the other went through a cloud queue, and that file was where the difference had to live.

Copying was never the mistake, since under this rule there is nothing else to do. The trouble with a hand copy is that it records nothing: where it came from, which revision it was taken at, whether a given difference is a fix that never traveled back or a choice its host made on purpose. A diff shows you 1,888 lines either way.

## Mounts, locks and a check

So the package system I designed doesn't try to abolish the copy. It gives each shared subsystem one canonical source tree, has each application declare where that tree is mounted, records a SHA-256 for every mounted file in a lockfile, and runs a check that works out what each mount should contain, compares it with what is there and exits non-zero on any difference.

A package is a directory with a manifest and a `src/` tree, and it is deliberately neither a crate nor a service. Composing one into an application writes its Rust files into a module of the application's source tree, stamps each with a generated-by provenance header and rewrites the package's references to itself. Package source refers to itself as `crate::<root>`, so when it is mounted under another name those paths have to follow, and the rewrite is bounded on both sides so that it can't touch an identifier that merely contains the same text. When the mount and the root have the same name there is nothing to rewrite, and the result is the header plus the verbatim body. Files that aren't Rust are copied as they are, so a prompt pulled in with `include_str!` still finds its text.

The composed package remains source inside the application. It adds modules, not another dependency entry, so the application's Cargo manifest still has the single permitted crate.

Without the check, all of this would be a convention. It reports a file that is missing, a file that has drifted, a file no package produces and a hash in the lockfile that has gone stale, it verifies the lock instead of merely rewriting it, and it opens every other source file in the application to look for the generated marker. It also compares unmarked Rust files outside mounts with same-named files at a package's source root, using overlap between non-trivial lines to flag likely hand copies. A deliberate fork can be declared with a reason and stays visible in the report.

## Drift and seams

The 1,888-line file is the reason the design has seams. A package that needs something only its host can supply declares a host import, which appears as an edge in the package graph. An application that has to own a file inside a mount declares it as a host file, and the tool refuses a pattern that would match a file the package provides or that tries to climb out of the directory. A real difference between hosts then gets written down where the tool can see it, and any other difference can be called drift without an argument.

Reuse also turned out to be a way of finding bugs. When the ACP package gained a second consumer, that consumer found three defects: a capped scan, a retry cache keyed on the prompt and a row that was being shared. That is the case for a canonical tree in miniature, since a defect found through one application gets fixed in the place every other copy comes from. ACP and MCP also show that a package doesn't have to begin as duplicated code, because neither was extracted from anything in the tree. The usual SDKs need tokio, futures and chrono, and ACP spawns processes, so neither could simply be linked into a deployed application, and a source package was the way to have them at all.

## Outside the monorepo

All of that assumes you live in the monorepo, and a developer with one application doesn't. `ferrite add` is the standalone half of the model. It loads the published index, resolves a package's dependencies transitively while declaring only the package you asked for, fetches each source file and writes it with the same provenance header the in-repo composer stamps. The header has to match byte for byte, because the check recomputes content and compares, so a project that later joins the workspace is already in the expected shape. Neither path needs a checkout of the platform's core beside it, although Cargo still has to resolve the one permitted dependency from somewhere.

Installing source from an index means letting a file from the network name paths on your disk, and the code treats the index accordingly. A hostile or corrupted index must not be able to name `../..`, so relative paths and module identifiers are validated before use, and symlinked destinations are refused. Each file is checked against its digest, and a mismatch names the expected and the actual value; a legacy index with no digests still installs, but under an UNVERIFIED warning that prints even in quiet mode. A conflict writes nothing without `--force`, and the changes that do go ahead are planned first, staged in a temporary location and applied with backups that are restored on error. The index also carries a monotonic serial, and a client refuses anything lower than the serial it has cached. The field saying which commit the index was generated from is reported and, as the code comment puts it, never trusted.

Those checks protect integrity, which is a narrower property than it sounds. A SHA-256 digest says the bytes match what the index says and doesn't say who wrote the index, and the serial stops only the rollback a client can observe, which leaves a fresh machine with nothing to compare against. Authenticating the publisher is the harder problem that projects such as [The Update Framework](https://theupdateframework.io/) exist to solve, and this system doesn't attempt it.

## What checking costs

A drift check runs in CI and shares its file walkers with every sync, so it has to stay cheap. In late August, over 28 packages, 41 consumers, 117 mounts and 1,287 vendored Rust files, it took about a second. You might expect a tool that reads and hashes that much source to spend its time hashing. The profile said otherwise: 64.5 percent of self time was in `readdir`, and re-hashing for the lock check came to about 7 percent of the run. The workload was bound by filesystem metadata, not computation.

An experimental optimization developed largely by a coding agent went after exactly that with cached package bytes, bounded reads, digest reuse and bounded parallel I/O. On one host with a warm cache, the median run of that experimental version fell from 1,050.6 ms over 20 samples to 500.5 ms over 40, a reduction of about 52 percent. The oracle was byte-identical output and exit status from the tool's commands, plus its 36 tests, and an oracle like that is what makes it reasonable to hand this kind of work to an agent. The tree had four pre-existing problems, so the expected exit status was 1 before and after. A faster check doesn't make a workspace clean, and since those four complaints were part of the golden output, the optimized version had to go on reporting them, byte for byte, in half the time.

By the end of August the registry listed 28 packages, mounted across applications, websites and tools, and none of the generated copies was a separate implementation. That was the point of the whole arrangement: however many applications carry a subsystem's source, there is one implementation of it to fix.
