Reproducible from-source Nix flake for the mcp-chrome browser extension #1

Closed
opened 2026-05-16 17:22:39 +03:00 by oleks · 2 comments
Owner

Goal

Package the mcp-chrome Chrome MCP extension (~/projects/mcp-chrome, an upstream GitHub checkout — pnpm + WXT monorepo) as a reproducible, from-source Nix flake. Motivation: the native-messaging bridge kept failing in non-reproducible ways (stale manifest, missing fastify, GC'd node paths, two competing mcp-chrome-bridge installs). A flake makes the build deterministic and cacheable, and pins a path-independent extension ID so native messaging stays stable.

Flake design (flake.nix at repo root of the checkout, staged not committed)

  • nixpkgs = nixos-unstable; flake-utils.
  • wasm-bindgen-cli built in-flake, pinned to 0.2.121 — the locked wasm-bindgen crate is 0.2.121 (forced by web-sys 0.3.98, which hard-pins =0.2.121); nixpkgs ships only 0.2.117 and a CLI/crate mismatch corrupts wasm output.
  • wasm-simd (packages/wasm-simd) compiled from source, no wasm-pack (avoids its network fetch): cargo build --target wasm32-unknown-unknown (needs lld for wasm-ld) → wasm-bindgen --target webwasm-opt -O3. This reproduces wasm-pack --release offline. A pinned Cargo.lock was generated (none existed upstream).
  • Extension: pnpm_10.fetchDeps { fetcherVersion = 2; } offline deps → build chrome-mcp-sharedwxt preparewxt build; the from-source wasm is copied into app/chrome-extension/workers/ so nothing prebuilt is reused.
  • chromeExtensionKey parameter → injects CHROME_EXTENSION_KEY for a stable, path-independent extension ID (today's ID hbdgbgagpkpjffpklnamcljpakneikee is path-derived; a store path would change it and break the pinned native-messaging allowed_origins).
  • nix run .#build app + build.sh: nom-governed build for a separate terminal, with env overrides (TARGET, OUT_LINK, SUBS, CHROME_EXTENSION_KEY).

Pinned FOD hashes

  • wasm-bindgen-cli src: sha256-ZOMgFNOcGkO66Jz/Z83eoIu+DIzo3Z/vq6Z5g6BDY/w=
  • wasm-bindgen-cli cargoHash: sha256-DPdCDPTAPBrbqLUqnCwQu1dePs9lGg85JCJOCIr9qjU=
  • pnpmDeps: sha256-fBCfTzwp/MYabu9duz9FQK+HbSiyQWD8rzJhndFO+Xk=

Root cause investigated: ~70-min build

The extension build hung for 70+ min in Vite/rollup. Root cause: app/chrome-extension/utils/semantic-similarity-engine.ts:1 did a static value import of @xenova/transformers (~44 MB dist/, transitively pulling onnxruntime-web@1.14). That module was statically reachable from 4 WXT entrypoints (background/index.ts, background/semantic-similarity.ts, offscreen/main.ts, popup/App.vue). WXT builds each entrypoint as its own sequential rollup pass, so ≥4 passes each parsed/tree-shook the entire transformers+onnx graph, single-threaded, minify:false. Not a flake defect — reproducible anywhere.

Fix applied (behaviour-neutral lazy-split)

In semantic-similarity-engine.ts: replaced the static import { AutoTokenizer, env as TransformersEnv } with a memoized dynamic import('@xenova/transformers') (loadTransformers()), destructured at the two already-async init methods. The type-only import is kept (zero bundle cost). The engine is only ever initialized behind async/conditional paths, so behaviour is unchanged; rollup now code-splits transformers into a lazy chunk instead of inlining it into 4 entrypoints. (Edits the upstream checkout — coordinate with the localization work happening in parallel in the same tree; target files were verified clean of those edits.)

Environment note: NCPS cache outage

All Nix builds during this work had to bypass the cluster substituters: nix-cache-custom.oleks.space unreachable and nix-cache-mirror.oleks.space's narinfo DB (postgresql.infra.svc.cluster.local:5432) refusing connections → HTTP 500. Workaround baked into build.sh/apps.build: --option substituters https://cache.nixos.org (set SUBS="" once NCPS recovers). Separate cluster incident — see cluster:storage.

Status / next steps

  • wasm-bindgen-cli 0.2.121 builds from source (FOD hashes pinned)
  • wasm-simd builds from source (valid wasm output verified)
  • All 3 FOD hashes pinned; deps resolve; pipeline reaches wxt build
  • Root cause of slow build identified
  • Lazy-split patch applied
  • Confirm green $out post-fix and measure build time (build in progress)
  • Decide upstreaming the lazy-split (PR to hangwin/mcp-chrome) vs carrying as a flake patch
  • Pin CHROME_EXTENSION_KEY and re-register native-messaging allowed_origins to the key-derived ID
  • Decide where the flake lives long-term (integrate into flake-hub vs dedicated repo)
  • Restore cluster substituters (NCPS) and drop the SUBS workaround
## Goal Package the **mcp-chrome** Chrome MCP extension (`~/projects/mcp-chrome`, an upstream GitHub checkout — pnpm + WXT monorepo) as a reproducible, from-source Nix flake. Motivation: the native-messaging bridge kept failing in non-reproducible ways (stale manifest, missing `fastify`, GC'd node paths, two competing `mcp-chrome-bridge` installs). A flake makes the build deterministic and cacheable, and pins a path-independent extension ID so native messaging stays stable. ## Flake design (`flake.nix` at repo root of the checkout, staged not committed) - `nixpkgs` = `nixos-unstable`; `flake-utils`. - **`wasm-bindgen-cli` built in-flake, pinned to 0.2.121** — the locked `wasm-bindgen` crate is 0.2.121 (forced by `web-sys 0.3.98`, which hard-pins `=0.2.121`); nixpkgs ships only 0.2.117 and a CLI/crate mismatch corrupts wasm output. - **`wasm-simd` (`packages/wasm-simd`) compiled from source**, no `wasm-pack` (avoids its network fetch): `cargo build --target wasm32-unknown-unknown` (needs `lld` for `wasm-ld`) → `wasm-bindgen --target web` → `wasm-opt -O3`. This reproduces `wasm-pack --release` offline. A pinned `Cargo.lock` was generated (none existed upstream). - **Extension**: `pnpm_10.fetchDeps { fetcherVersion = 2; }` offline deps → build `chrome-mcp-shared` → `wxt prepare` → `wxt build`; the from-source wasm is copied into `app/chrome-extension/workers/` so nothing prebuilt is reused. - `chromeExtensionKey` parameter → injects `CHROME_EXTENSION_KEY` for a stable, path-independent extension ID (today's ID `hbdgbgagpkpjffpklnamcljpakneikee` is path-derived; a store path would change it and break the pinned native-messaging `allowed_origins`). - `nix run .#build` app + `build.sh`: `nom`-governed build for a separate terminal, with env overrides (`TARGET`, `OUT_LINK`, `SUBS`, `CHROME_EXTENSION_KEY`). ### Pinned FOD hashes - `wasm-bindgen-cli` src: `sha256-ZOMgFNOcGkO66Jz/Z83eoIu+DIzo3Z/vq6Z5g6BDY/w=` - `wasm-bindgen-cli` cargoHash: `sha256-DPdCDPTAPBrbqLUqnCwQu1dePs9lGg85JCJOCIr9qjU=` - `pnpmDeps`: `sha256-fBCfTzwp/MYabu9duz9FQK+HbSiyQWD8rzJhndFO+Xk=` ## Root cause investigated: ~70-min build The extension build hung for 70+ min in Vite/rollup. Root cause: `app/chrome-extension/utils/semantic-similarity-engine.ts:1` did a **static value import** of `@xenova/transformers` (~44 MB `dist/`, transitively pulling `onnxruntime-web@1.14`). That module was statically reachable from **4 WXT entrypoints** (`background/index.ts`, `background/semantic-similarity.ts`, `offscreen/main.ts`, `popup/App.vue`). WXT builds each entrypoint as its own sequential rollup pass, so ≥4 passes each parsed/tree-shook the entire transformers+onnx graph, single-threaded, `minify:false`. Not a flake defect — reproducible anywhere. ## Fix applied (behaviour-neutral lazy-split) In `semantic-similarity-engine.ts`: replaced the static `import { AutoTokenizer, env as TransformersEnv }` with a memoized dynamic `import('@xenova/transformers')` (`loadTransformers()`), destructured at the two already-`async` init methods. The type-only import is kept (zero bundle cost). The engine is only ever initialized behind async/conditional paths, so behaviour is unchanged; rollup now code-splits transformers into a lazy chunk instead of inlining it into 4 entrypoints. (Edits the upstream checkout — coordinate with the localization work happening in parallel in the same tree; target files were verified clean of those edits.) ## Environment note: NCPS cache outage All Nix builds during this work had to bypass the cluster substituters: `nix-cache-custom.oleks.space` unreachable and `nix-cache-mirror.oleks.space`'s narinfo DB (`postgresql.infra.svc.cluster.local:5432`) refusing connections → HTTP 500. Workaround baked into `build.sh`/`apps.build`: `--option substituters https://cache.nixos.org` (set `SUBS=""` once NCPS recovers). Separate cluster incident — see `cluster:storage`. ## Status / next steps - [x] wasm-bindgen-cli 0.2.121 builds from source (FOD hashes pinned) - [x] `wasm-simd` builds from source (valid wasm output verified) - [x] All 3 FOD hashes pinned; deps resolve; pipeline reaches `wxt build` - [x] Root cause of slow build identified - [x] Lazy-split patch applied - [ ] Confirm green `$out` post-fix and measure build time (build in progress) - [ ] Decide upstreaming the lazy-split (PR to hangwin/mcp-chrome) vs carrying as a flake patch - [ ] Pin `CHROME_EXTENSION_KEY` and re-register native-messaging `allowed_origins` to the key-derived ID - [ ] Decide where the flake lives long-term (integrate into flake-hub vs dedicated repo) - [ ] Restore cluster substituters (NCPS) and drop the `SUBS` workaround
oleks added the type/featuredomain/craftactivity/buildprovisional-activity labels 2026-05-16 17:32:28 +03:00
oleks added domain/cross-build and removed domain/craft labels 2026-05-16 17:52:32 +03:00
oleks added this to the oleks — state board project 2026-05-16 17:52:39 +03:00
oleks added this to the oleks — domain board project 2026-05-16 17:52:46 +03:00
oleks added this to the oleks — activity board project 2026-05-16 17:52:53 +03:00
Author
Owner

Refiled to the correct repo: oleks/mcp-chrome#1 (oleks/mcp-chrome#1). The flake now lives on oleks/mcp-chrome main (d7c097a). Closing here — track there.

Refiled to the correct repo: oleks/mcp-chrome#1 (https://git.oleks.space/oleks/mcp-chrome/issues/1). The flake now lives on `oleks/mcp-chrome` `main` (`d7c097a`). Closing here — track there.
oleks closed this issue 2026-05-17 16:25:24 +03:00
oleks removed this from the oleks — state board project 2026-05-17 20:21:18 +03:00
oleks removed this from the oleks — domain board project 2026-05-17 20:21:20 +03:00
oleks removed this from the oleks — activity board project 2026-05-17 20:21:21 +03:00
Author
Owner

Final diagnosis & resolution (scope change)

The full-extension from-source target is not viable. Transformers was ruled out as the cause.

Evidence chain (nixbuild.net build IDs via the cluster:ci nixbuild skill + one local emmett run):

fix attempted derivation outcome
lazy dynamic import() dbcwm26… 9141232 out_of_memory 17.7 GB; 9141631 client_disconnect 30.6 GB / 141 min
(retry, same drv) dbcwm26… 9143034 client_disconnect 21.8 GB / 58.9 GB alloc / 146 min
Vite resolve.alias → prebuilt dist vpxswgn26… 9143364 ran >70 min, 3 cores pegged, killed (WXT 0.20 does not propagate user vite.resolve.alias into its per-entrypoint rollup builds)
externalize: runtime-URL loader + vendor prebuilt + onnx wasmPaths bry87qzsf… 9144045 ran 25+ min remote, then 72 min locally on emmett, identical profile

The externalize postPatch used substituteInPlace --replace-fail, so it provably applied (a missing anchor aborts postPatch immediately; instead it ran for over an hour). With transformers genuinely externalized the build's CPU/time/memory shape was unchanged → transformers was never the dominant cost.

Root cause is intrinsic to upstream wxt build: ~12 sequential MV3 entrypoints, 32 MB of committed onnxruntime wasm shuffled by vite-plugin-static-copy, Vue + @vue-flow + markstream across many entrypoints, minify:false. No config-level patch carried by the flake removes that; it does not complete on nixbuild.net (auto-scaled to 58 GB) or locally (OOM-killed nix-daemon + k3s pods on the first unguarded attempt).

Delivered (finalized flake.nix + build.sh)

  • packages.default / packages.wasm-simd — Rust→wasm, ~22 s, reproducible, verified green (build 9143033 etc.)
  • packages.wasm-bindgen-cli — 0.2.121, FOD-pinned, matches the locked crate
  • apps.build / build.sh default TARGET.#default (the viable target); nix build can no longer land on the broken derivation
  • ⚠️ packages.chrome-mcp-extension — kept but documented KNOWN-BROKEN / upstream-pathological; speculative transformers postPatch removed as proven-dead complexity
  • Behaviour-neutral lazy-split kept in semantic-similarity-engine.ts purely as a runtime-startup improvement (orthogonal; not a build fix)
  • NCPS recovered mid-investigation; SUBS workaround is now opt-in only

Spun-off / not regressed

  • oleks/claude-plugin-cluster#14 (nixbuild-build-introspection skill) — delivered v0.41.0, used here to get the per-build memory/status that finally proved the OOM loop.
  • Recommend an upstream issue/PR to hangwin/mcp-chrome for the WXT/rollup pipeline cost — out of scope for this repo.

Closing as completed (resolved with scope change): the flake is finalized for the reproducible subset; the extension bundle is conclusively an upstream build-architecture problem, not a packaging gap.

## Final diagnosis & resolution (scope change) **The full-extension from-source target is not viable. Transformers was ruled out as the cause.** Evidence chain (nixbuild.net build IDs via the `cluster:ci` nixbuild skill + one local emmett run): | fix attempted | derivation | outcome | |---|---|---| | lazy dynamic `import()` | dbcwm26… | 9141232 `out_of_memory` 17.7 GB; 9141631 `client_disconnect` 30.6 GB / 141 min | | (retry, same drv) | dbcwm26… | 9143034 `client_disconnect` 21.8 GB / 58.9 GB alloc / 146 min | | Vite `resolve.alias` → prebuilt dist | vpxswgn26… | 9143364 ran >70 min, 3 cores pegged, killed (WXT 0.20 does not propagate user `vite.resolve.alias` into its per-entrypoint rollup builds) | | externalize: runtime-URL loader + vendor prebuilt + onnx `wasmPaths` | bry87qzsf… | 9144045 ran 25+ min remote, then **72 min locally on emmett, identical profile** | The externalize `postPatch` used `substituteInPlace --replace-fail`, so it provably applied (a missing anchor aborts `postPatch` immediately; instead it ran for over an hour). With transformers genuinely externalized the build's CPU/time/memory shape was **unchanged** → transformers was never the dominant cost. **Root cause is intrinsic to upstream `wxt build`:** ~12 sequential MV3 entrypoints, 32 MB of committed onnxruntime wasm shuffled by `vite-plugin-static-copy`, Vue + @vue-flow + markstream across many entrypoints, `minify:false`. No config-level patch carried by the flake removes that; it does not complete on nixbuild.net (auto-scaled to 58 GB) or locally (OOM-killed `nix-daemon` + k3s pods on the first unguarded attempt). ### Delivered (finalized `flake.nix` + `build.sh`) - ✅ `packages.default` / `packages.wasm-simd` — Rust→wasm, **~22 s, reproducible, verified green** (build 9143033 etc.) - ✅ `packages.wasm-bindgen-cli` — 0.2.121, FOD-pinned, matches the locked crate - ✅ `apps.build` / `build.sh` default `TARGET` → `.#default` (the viable target); `nix build` can no longer land on the broken derivation - ⚠️ `packages.chrome-mcp-extension` — kept but documented **KNOWN-BROKEN / upstream-pathological**; speculative transformers `postPatch` removed as proven-dead complexity - ✅ Behaviour-neutral lazy-split kept in `semantic-similarity-engine.ts` purely as a runtime-startup improvement (orthogonal; not a build fix) - NCPS recovered mid-investigation; `SUBS` workaround is now opt-in only ### Spun-off / not regressed - `oleks/claude-plugin-cluster#14` (nixbuild-build-introspection skill) — delivered v0.41.0, used here to get the per-build memory/status that finally proved the OOM loop. - Recommend an upstream issue/PR to hangwin/mcp-chrome for the WXT/rollup pipeline cost — out of scope for this repo. Closing as **completed (resolved with scope change)**: the flake is finalized for the reproducible subset; the extension bundle is conclusively an upstream build-architecture problem, not a packaging gap.
Sign in to join this conversation.