--- status: resolved opened: 2026-09-15 located-in: [mesh-tools, mesh-host, mesh-sdk] fixed-by: mesh-tools b618057 (the SDK resolved by version from the registry, one pin); mesh-host 7986306; mesh-controller 4b9bc50. Caveat: the base image still builds with npm install and no lock, so the build is not reproducible amended-design: --- # 053 — The SDK is pinned twice, and the two disagree ## Symptom The module that carries the tool runtime names the SDK two ways, and they are not the same thing: ``` package.json @novox/mesh-sdk -> git+https:///mesh-sdk.git# package-lock.json @novox/mesh-sdk -> ../mesh-sdk ``` The manifest names a commit in a repository any machine can reach. The lock names a **sibling directory**, which exists on the workstation the lock was generated on and nowhere else. It builds anyway, because the recipe runs `npm install` — which tolerates a lock that disagrees with its manifest and re-resolves from the manifest. It is the one command that hides this. ## Why this matters **A lock file exists to make a build reproducible, and this one describes one machine.** `npm ci` — the command for exactly the case a lock is for — fails here, or worse, succeeds against whatever happens to be at that path. **It is the first thing a fresh mesh builds.** The toolchain image carries the SDK, and everything with code of its own is compiled inside it. A dependency resolved differently on the build machine than on a workstation is a difference in every module the mesh will ever build, arriving as a compile error or a runtime mismatch far from here. **And it is about to be copied.** Each language's toolchain will carry that language's SDK the same way ([ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md)). Whatever this repository does, the Rust and Python ones will do, so the shape is worth getting right before there are four of them. ## It is a violation, not an open question [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) already decided this, and was accepted: > Each module is an independent package that declares its dependencies and **consumes them from > the private registry, including the mesh's own shared library.** A cross-package change is > therefore two steps: publish the producer, then consume it. So a git dependency at a pinned commit is not an alternative mechanism under consideration. It is the mesh's own shared library being consumed by a means the record rules out, and the lock naming a sibling directory is what that looks like when nobody publishes. **Which makes the fix a direction rather than a discussion**: publish the SDK to the private registry, consume it by version, and the lock stops being able to name a path that exists on one machine. ## Open questions - **Which private registry, and does the bootstrap have one?** The catalogue holds `verdaccio`; a git host typically serves package registries too. ADR 0014 says *the* private registry as though there is one, and today a fresh mesh has neither until something installs it — so the first SDK build happens before the registry the record assumes. - **Or should the SDK be a published package?** The catalogue holds a private registry module, and a published package is how the rest of the world does this — at the cost of a mesh needing that registry up before it can build anything, which is a bootstrap problem where there is currently none. - **What generates the lock, and on what?** A lock produced on a workstation with sibling checkouts will keep saying this. A lock produced the way the image builds would not. ## How this would be checked | Rule | Checked by | |---|---| | A build does not depend on the machine it runs on | The toolchain image builds with `npm ci` rather than `npm install`, which refuses a lock that disagrees with its manifest. | | The SDK a module compiles against is the one named | The commit baked into the toolchain image is compared with the one the manifest pins. |