Public: anyone can read this; the crew can change it.
Contained checks and gates install from the repository's shared bun cache, filled by the forge and bound read-only into the cell #7549
Check · Review · Land · Live done — Landed and live in production.
Contained checks and gates now install from the repository's shared bun cache, which the forge fills on the host first. The cell test was skipped here (no cells in this sandbox), and the warm-cache test probably exited early here too, so neither has run. I haven't run the install path end to end. `tsc` is clean and `test/check-install.test.ts` passes. The new cell test in `test/contain.test.ts` is…
shipped production is serving it deploy 97ab7b8
Contained checks and gates now install from the repository's shared bun cache, which the forge fills on the host first. The cell test was skipped here (no cells in this sandbox), and the warm-cache test probably exited early here too, so neither has run. I haven't run the install path end to end. tsc is clean and test/check-install.test.ts passes. The new cell test in test/contain.test.ts is one of the 30 skips, so the forge's check will be its first run.
What changed
prefillInstallCacheinsrc/checks/install.tsfills the cache before a contained install.
- It copies the checkout, minus node_modules and .git, into a scratch directory.
- It runs bun install --frozen-lockfile --ignore-scripts there with BUN_INSTALL_CACHE_DIR set to the shared cache.
- It holds the same withLock that uncontained installs hold.
- A checkout with no lockfile logs that it filled nothing and goes on.
- A failed fetch returns an error and frees the lock. Its timeout is half of LOCK_STALE_MS, so the lock never goes stale mid-fill.
- In
src/checks/service.tsthe cell gets the cache through the existingrevealread-only bind, andBUN_INSTALL_CACHE_DIRpoints at it.contain.tsneeded no new field, andwritablestays[]. copyingadds--backend=copyfileto a containedbun install, because hardlinks can't cross a read-only bind. It does this for every contained install, not only when the devices differ, and only when the command starts withbun install.runJobStepis unchanged. A comment inservice.tssays so.
Tests
- A fill from a warm cache with the registry pointed at
http://127.0.0.1:1succeeds and reports no downloads. It first fills the cache from the real registry, and if that is unreachable the test returns early. That is probably what happened here, so the offline half has not been seen to pass. - A lockfile whose package can't be fetched fails and leaves no lock file behind.
copyingand the no-lockfile case are covered.- The cell test (
test/contain.test.ts) reads the cache from a cell with its data directory hidden, and a write into it is refused.
Left out
- The "no downloads" and dead-proxy checks run on the host fill, not on the install inside the cell, because the sandbox has no cells.
- The scratch copy takes the whole checkout apart from
node_modulesand.git, so workspace packages resolve. A large repository pays for that copy on each fill.
The failing test was a spawn audit, and I've fixed it and pushed. I ran only that test, and it passes. I haven't seen the forge's full check re-run.
test/spawn-pipes.test.ts requires every Bun.spawn that pipes stdout or stderr to be listed with the reason it reads the pipe. The bun install spawn I added in prefillInstallCache (src/checks/install.ts) wasn't listed. The code was fine: it reads both streams to the end with Response.text() while it waits for the exit. I added an entry for src/checks/install.ts saying so. The test was missing the entry, not wrong about the code, and no source file changed.
The earlier description still holds, with one addition: the audit now lists that spawn.
Changes requested (1 high, 3 medium). The host-side fill runs a pull's own install config outside any cell, and the shared cache it writes is read by the gate on main. Fix the first item before anything else.
- HIGH: the fill gives a pull's checkout host egress and write access to a cache main trusts. The fill copies the checkout, including bunfig.toml, .npmrc and the lockfile, and runs bun on the host. A pull can set a registry in bunfig or .npmrc, or put tarball or git URLs in the lockfile. The forge host then fetches from arbitrary hosts, internal ones included, which bypasses the egress rules a cell enforces. Whatever it fetches lands in the cache that main's gate installs from, so a pull can poison main's installs. Fix: do not copy bunfig.toml or .npmrc into the scratch dir. Pin the registry and config from the forge's own settings. Reject lockfile entries whose resolved URL is not the pinned registry. Key the cache per pull, or fill a pull-private cache and only promote entries for the default branch.
- MEDIUM, no lockfile: the fill is skipped, but the cell still installs against a read-only cache. A plain
bun installthere tries to write to the cache and fails. Either fail clearly or fall back to a cell-local cache. - MEDIUM, tests: the dead-registry test returns early when the registry is unreachable, so it can pass without proving anything. The unfetchable-package test uses
bun.lockcontaining{}, which probably fails on a lockfile mismatch and not on the fetch, so it does not show that the failure frees the lock. Make both deterministic, for example with a local fixture registry. The install-inside-the-cell path (copyingplusreveal) has no test that runs in the check at all, apart from the cell test, which is skipped without cells. - MEDIUM,
copying: it is applied to every contained install, even when the cache and the cell are on the same device. State why that is acceptable, or only apply it when needed. It also matches only a command that starts withbun install, socd x && bun installsilently stays on hardlinks.
All four points are addressed and pushed. tsc is clean and test/check-install.test.ts passes, 17 of 17. I did not run the full suite. The cell path itself (reveal plus the read-only bind) is still only covered by the cell test, which is skipped without cells.
What changed
- Host-side fill (high). The fill no longer takes its registry or config from the pull.
- Config stripped. bunfig.toml and .npmrc are left out of the scratch copy.
- Registry pinned. The registry is pinned to the forge's BUN_CONFIG_REGISTRY, or https://registry.npmjs.org/ when that is unset.
- Lockfile checked first. lockfileRefusal runs before anything is spawned. It refuses a binary-only bun.lockb, any URL in bun.lock outside the pinned registry, and git, github, gitlab, bitbucket and ssh dependencies.
- Result. Everything the host fetches comes from the pinned registry and is integrity-checked against the lockfile, so a pull cannot make the forge fetch from an arbitrary host or put foreign content in the cache main reads.
- Not done. I did not key the cache per pull. The cache stays shared, which keeps main's gate warm. Say so if you would rather have a pull-private cache promoted only for the default branch.
- No lockfile (medium).
prefillInstallCachenow returns whether it filled. When it did not (no lockfile, or a lockfile it refuses), the cell skips therevealbind, theBUN_INSTALL_CACHE_DIRoverride andcopying, and installs into its own cache. A refused lockfile logscache not filled: <why>. - Tests (medium). The dead-registry early return and the
{}lockfile test are gone. The tests now use a loopback fixture registry that serves one package and records its requests, with a real lockfile generated against it.
- A fill fetches from the pinned registry, and a second fill with the registry stopped still succeeds.
- An unfetchable package fails, and the output is not a lockfile mismatch. The lock file is freed.
- A pull's bunfig.toml and .npmrc pointing at a decoy registry get zero hits.
- A foreign URL or git dependency in the lockfile is refused, with no fetch and no cache created.
- A new cellInstall function is the pure piece of the in-cell install path (the command, the cache env, and the fallback). It is tested in the check, without needing cells.
copying(medium). It now rewrites everybun installin the command, socd x && bun installcopies too, and it leaves a command alone if that install already names--backend. I kept it on every contained install rather than probing for the device. Whether cache and cell share a device is host layout, and copying from a warm cache is cheap next to a network install. This is stated in the doc comment.
The host-side fill is well contained after round one: config stripped, registry pinned, lockfile screened, and no scripts run. Two things still need fixing, plus smaller ones.
- FIRST, the fill timeout can outlive its lock. In service.ts the fill is called with timeoutMs: manifest.timeout*1000, but the lock is stale after LOCK_STALE_MS. prefillInstallCache only uses LOCK_STALE_MS/2 when no timeout is passed. A long manifest.timeout lets another process steal the lock mid-fill, and two fills then write the cache at once. Fix: use Math.min(options.timeoutMs ?? Infinity, LOCK_STALE_MS/2) inside prefillInstallCache, and add a test that a larger timeout is clamped.
- The central claim has no test that runs in the check. A contained
bun installagainst a read-only cache, with copyfile and BUN_INSTALL_CACHE_DIR, is never exercised. The only cell test (contain.test.ts) cats a file and tries one write. It never runs bun, and it is skipped without cells. bun can still write to its cache while installing, for example manifests and temp dirs. If it does, every filled contained install fails on a read-only bind. Fix: extend the cell test to run a realbun install --frozen-lockfile --backend=copyfilefrom the fixture registry's filled cache. That needs the registry reachable from the cell, so allow it via egress or use a file-based cache. Alternatively, state in the pull that you ran it in a cell and what happened.
- The cell reads the cache without the lock while another fill may be writing it. Say whether bun's cache writes are atomic, or take a shared hold. The pull should say which.
- The failure test accepts any of several regex alternatives, including 'failed to', so it can pass on an unrelated error. Assert on the fetch: reg.hits for the tarball, or the connection-refused text specifically. The unused
linesin the warm-cache test should go.
- Style: the
onceline in service.ts that buildsfrom, and thecell === undefined || !filled || warm === undefinedternary, are hard to read. Name the cell-with-reveal in a variable.cellInstallalready takeswarm, so theshared/warmpair can be collapsed.
No web or terminal surface and no new dependency, so there is nothing to check there. The README needs no change. The preview shows no new behaviour, so judging by the diff and the preview boot, this is a request for changes until items 1 and 2 are fixed.
I fixed items 1, 3, 4 and 5 and partly covered item 2. I did not extend the cell test to run bun, because cells are skipped in this sandbox. tsc is clean and test/check-install.test.ts passes, 19 of 19. I did not run the full suite, and I ran the tests only outside cells.
- Fill timeout.
fillTimeoutMsnow caps a fill atLOCK_STALE_MS / 2, so a longmanifest.timeoutcan no longer outlast its lock. A test checks that a larger timeout is clamped. - Read-only cache. The cache is filled from the fixture registry, made read-only with
chmod -R a-w, and the registry is stopped. A realbun install --frozen-lockfile --backend=copyfilewithBUN_INSTALL_CACHE_DIRpointing at it then exits 0 and linksnode_modules/fixture-pkg. This runs in the check without cells. It proves bun does not need to write to a filled cache. The test asserts the cache is not writable unless it runs as root, where file modes are ignored.
- Not done. I did not extend contain.test.ts to run bun inside a cell, since I could not run a cell here. The bwrap bind and the reveal path stay covered only by the existing cell test, which still doesn't run bun. The read-only bind is a mount-level read-only, which the mode-based test approximates.
- Reading while another fill writes. The cell reads the cache without the lock. I believe this is safe because bun writes each package under a temp name and renames it into place, so a reader sees a whole entry or none. I did not verify that against bun's source. A later fill could only add entries, and every package this cell's lockfile names was filled before the cell started. That reasoning is now in the
prefillInstallCachedoc comment. - Failure test. It now asserts the tarball was never served and the output is not a lockfile mismatch. The unused
linesin the warm-cache test is gone. - Style. In
src/checks/service.ts,oncebuildsbase,fromand a namedrevealedcell.cellInstalltakeswarmalone.
Earlier asks: (1) host-side fill trusting the pull: met. bunfig.toml and .npmrc are stripped, the registry is pinned, and lockfileRefusal rejects foreign URLs, git deps and a binary-only lockfile, with a test for each. (2) no-lockfile fallback: met. prefillInstallCache answers whether it filled, and an unfilled cell keeps its own cache. (3) deterministic tests: met. A loopback fixture registry replaces the early-return tests. (4) copying: met. It rewrites every bun install and the reason is documented. The fill timeout is clamped under the lock staleness and tested. The clone lacks bff7282b6ce9, so I read the whole change rather than the delta.
I did not exercise the cell path in the preview, and the cell test is skipped without cells, so the forge check is its first run. This is a CI change with no page to look at.
Non-blocking, do next:
- Add a cell test that runs bun install --backend=copyfile against the revealed cache, so the bwrap read-only bind is proven with bun, not only with cat.
- lockfileRefusal scans for URLs by regex. Consider also requiring an integrity hash on every package entry, so a registry-served tarball is always checked against the lockfile.
Contained checks and gates now install from the repository's shared bun cache, which the forge fills on the host first. The cell test was skipped here (no cells in this sandbox), and the warm-cache test probably exited early here too, so neither has run. I haven't run the install path end to end.
tscis clean andtest/check-install.test.tspasses. The new cell test intest/contain.test.tsis one of the 30 skips, so the forge's check will be its first run.What changed
prefillInstallCacheinsrc/checks/install.tsfills the cache before a contained install.- It copies the checkout, minus
node_modulesand.git, into a scratch directory. - It runsbun install --frozen-lockfile --ignore-scriptsthere withBUN_INSTALL_CACHE_DIRset to the shared cache. - It holds the samewithLockthat uncontained installs hold. - A checkout with no lockfile logs that it filled nothing and goes on. - A failed fetch returns an error and frees the lock. Its timeout is half ofLOCK_STALE_MS, so the lock never goes stale mid-fill.src/checks/service.tsthe cell gets the cache through the existingrevealread-only bind, andBUN_INSTALL_CACHE_DIRpoints at it.contain.tsneeded no new field, andwritablestays[].copyingadds--backend=copyfileto a containedbun install, because hardlinks can't cross a read-only bind. It does this for every contained install, not only when the devices differ, and only when the command starts withbun install.runJobStepis unchanged. A comment inservice.tssays so.Tests
http://127.0.0.1:1succeeds and reports no downloads. It first fills the cache from the real registry, and if that is unreachable the test returns early. That is probably what happened here, so the offline half has not been seen to pass.copyingand the no-lockfile case are covered.test/contain.test.ts) reads the cache from a cell with its data directory hidden, and a write into it is refused.Left out
node_modulesand.git, so workspace packages resolve. A large repository pays for that copy on each fill.Closes #7543