Shipping an SDK that does not depend on the developer’s laptop
What has to be true before a private source tree can become a usable public SDK: fresh snapshots, a private manifest, a checker that never runs exported code, clean-container consumers and generated notices.

On this page
A package that builds in your own source tree has told you very little about whether it will build for anyone else. A path dependency resolves because the sibling directory happens to be there, a test reaches two folders up for a helper, a README command works because you are standing at the repository root with the right toolchain on your path, and a generated file that nothing produces any more is still sitting where an earlier build left it. None of this produces a warning, because on your machine nothing is wrong. The first person to find out is a stranger who has just decided to give your SDK a try.
Ferrite is an application platform built on an upstream core, and the parts I own are developed in a private tree. What an outside developer needs from that tree is an SDK, an index of source packages and a CLI. The rule we adopted for getting them out is that public repositories are opened from scrubbed fresh snapshots with fresh initial commits, never from the private history and never as a partial mirror of the workbench. A filtered history is a claim that nothing awkward exists anywhere in thousands of commits, and a partial mirror has to go on making that claim with every sync. A snapshot only has to be right once, at a moment when a machine can check it. The machinery for that, which I directed with agents, is mostly about finding everything the development machine had been supplying without anyone asking, and then either shipping it or proving it was not needed.
What leaves and what stays behind
The exporter works from a policy file that names which trees belong in which public repository. It copies them into one directory per repository, applies the recorded rewrites, generates the LICENSE, NOTICE and README.md along with a standalone Cargo workspace or a filtered package index, and writes a manifest. It prepares and checks snapshots and never builds a release. For the three targets meant to be publishable, its build step is also the gate: it exits 1 while any of them is blocked, and a separate mode records a worklist for every target instead of refusing. The examples already build clean and the test suite holds them there, so a blocked examples build now counts as a regression and not as an item on a worklist.
The manifest lists the private material and every blocker, which makes it exactly the file that must never be published, so it sits in the output root beside the repository directories and not inside any of them. The check step then refuses an output whose repository directories contain any file the manifest does not list. That one rule keeps a stray build product from leaving, and it also keeps a copy of the manifest from travelling inside a repository, since the manifest does not list that copy.
Checking raises a trust problem of its own. The packages repository contains its own checker, and the obvious way to validate the repository is to run it, which means executing code from the artifact in order to decide whether to trust the artifact. So the check never executes code from an export. If a target has any integrity failure, nothing runs. Otherwise the check derives the checker from the canonical source the policy names, applies the target’s rewrites, refuses an exported copy that differs from the result and runs only the derived bytes, from a private temporary directory, with a three-variable environment, under Node’s permission model, which permits reads inside the export, no writes and no child processes.
Being the stranger
All of that inspects the export, and whether the export works is a different kind of question. The standalone verification copies it into a fresh rust:1.89-slim-bookworm container with neither the private tree nor the platform core present, then builds, tests and scaffolds from what it finds there. A sibling assumption has nowhere left to resolve, and every step leaves a log and a line in a table of steps. The verification never writes into the export, which raises a small question about the lockfile, since the clean SDK build resolves one and consumers ought to have it. The lockfile stays with the receipts and goes back into a new export build as an input, so the published tree carries it as an ordinary manifested file and not as something that appeared during testing.
The consumer journey goes further, over the same export, in its own fresh container. It installs Node.js, installs the CLI with the exported installer and runs the commands in the SDK’s README and the check in the packages README, so the documentation gets executed as well as read. It then gives every package in the exported index its own new application, runs ferrite add, wires in the mounts the way the CLI instructs, and requires ferrite check to report ok with no gate or spec findings and no compiler errors, followed by a passing ferrite test. One application per package matters, because in a shared application a package could pass by leaning on something another package had brought along. The same journey can be pointed at a published registry and a published CLI download instead of the local export, which carries the test across the last step of a release.
A passing run does not show that the refusals work, so the journey also has to produce three of them, each for its stated reason: a tampered package byte must fail the content check, a tampered release archive must fail the checksum and an index entry named like a path must be refused by name. The reason is part of the assertion, because a step that fails for some unrelated cause, a missing tool for instance, looks exactly like a successful refusal if all you check is that it failed. Secret scanning gets the same treatment. Gitleaks runs with its default rules and ignores inline allow markers, so a file cannot excuse itself, and every finding has to match a triage entry by rule, exported path and the SHA-256 of the matched string, with the recorded number of occurrences. An untriaged finding, a changed count or a stale entry fails the step, and the matched strings never enter the receipts.
Provenance stays, notices travel
Two kinds of record come out of this, and they go in opposite directions. Provenance stays in the private audit, because knowing which private material exists and what was blocking what is useful to the operator and to nobody else, and publishing it would disclose the very material the export exists to leave behind. Notices travel with the software, because they are owed to the people whose code is in it. We generate them and never write them by hand. For the CLI they come from cargo metadata --locked for each release platform, following the package’s normal dependency graph and copying verbatim every licence, copyright and notice file that each linked crate ships, and the export refuses a CLI target whose notices name another version. The archives themselves hold only LICENSE and the binary, because the plugin’s setup and runner accept no other member, so the notices travel beside them. Web bundles get theirs from the lockfile and a tree installed from it, covering every production package plus vite and tailwindcss, which look like build tools but add runtime and base-style code to the bundle, and the export refuses a bundled app whose notices are missing or record another lockfile’s digest. My favourite case is browser-core, which ships the BSD licence of the VP8 reference decoder from RFC 6386 because one of its source files carries that decoder’s constant tables. No lockfile would tell you that, because there is no dependency, only numbers that came from somewhere.
The client people install
The CLI is what people actually install, and a version number alone does not say what they got. The policy names the archives every release must carry, at present darwin-arm64 and linux-x86_64, and when the plugin’s version names a release whose archives are missing, the exporter names each one and says whether the file, its SHA256SUMS line or both are absent. A preflight then installs the pinned CLI into an isolated location and checks its version, the SDK edition it is paired with, its command surfaces, the scaffold and local admission without Cargo. On macOS arm64 the pinned release passed, and a deliberately corrupted archive was rejected by checksum before anything was unpacked or run. The preflight needs no model, login, hosted deployment or package-network call, which is why it can live in CI.
That control shows that corruption gets caught before execution. A checksum says the bytes match a list and does not say who wrote the list, so authenticating the publisher has to come from somewhere else.
Publishing itself still found something. When we published a distribution during a rehearsal, the hashes were exact and the readback failed, because the permissions of our private staged package directories had been preserved on the public server. We corrected the modes on the public packages only, after which reads as the web user passed, and the failed readback stayed in the record. A file mode inherited from a private staging directory is about as laptop-shaped as a dependency gets. A hash could never have seen it, because a hash covers only the bytes. The readback saw it because it asked for the file from the place a stranger would get it.
Chris Chabot, November 2026