fix(execute): bundle backend so prod runs under Node + isolated-vm#433
Merged
Conversation
Execute MCP tool has been broken in prod since launch: the backend runs under Bun (JSC, partial V8 ABI shim) but isolated-vm is a V8 native addon that fails to dlopen under Bun. Returns RuntimeUnavailable to every caller. #430 tried 'exec node --import tsx src/server.ts' to switch the runtime. The runtime swap mechanism worked end-to-end (verified isolated-vm + runScript() in the live image), but the actual server boot crashed because Node's cjs-module-lexer couldn't detect named exports of @lobu/core's CJS dist barrel when reached via the full server.ts import chain. Reverted in #431. Right fix per second-opinion review: bundle the backend at build time. esbuild resolves all workspace imports inline (using the 'bun' condition to pick up TS source rather than CJS dist), so Node never has to bind named imports against cross-package CJS barrels — the failure mode that broke #430 simply doesn't exist for the bundle. Native addons and packages with require-in-the-middle hooks (Sentry, OpenTelemetry, pino) stay external so their runtime hooks keep working. Implementation: - packages/owletto-backend/scripts/build-server-bundle.mjs: esbuild config that bundles workspace + relative imports, externalizes every bare specifier. ~2.4 MB self-contained ESM, ~100 ms build. - packages/owletto-backend/package.json: 'build:server' npm script. - docker/app/Dockerfile: switch 'bun install' to 'bun install --linker=hoisted' so node_modules is flat (Node's resolver doesn't understand bun's .bun/<pkg>@<ver>/ isolated layout). Add a 'bun run build:server' step in the builder stage. - docker/app/start.sh: 'exec node /app/.../dist/server.bundle.mjs' instead of 'exec bun src/server.ts'. Verified end-to-end against a locally built docker image: basic: {success: true, returnValue: 3} chaining: {success: true, returnValue: {slug: 'atlas', ok: true}} The CI sandbox-runtime test added in #430 stays as the regression guard - it'll keep failing until the runtime can actually load isolated-vm. Tradeoffs: - Adds ~5 s to the docker build (esbuild bundle). - Hoisted layout is npm-style flat: increases build-stage install time slightly but matches what Node expects natively. - Native addons and Sentry/OpenTelemetry/pino externalized; if any of those moves are wrong they'll surface at boot, not silently.
Contributor
|
Triage decision: Reasons:
Next: @buremba assigned for manual review — Docker bundling changes for Node + isolated-vm compatibility in production |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Execute MCP tool has been broken in prod since launch: the backend runs under Bun (JSC, partial V8 ABI shim) but isolated-vm is a V8 native addon.
isolated_vm.nodefails to dlopen under Bun:PR #427 caught this and returned
RuntimeUnavailable. The hardening worked — the tool itself never ran.PR #430 tried
exec node --import tsx src/server.ts. The runtime swap mechanism worked end-to-end (verified isolated-vm + runScript() in the live image). But the actual server boot crashed because Node'scjs-module-lexercouldn't detect named exports of@lobu/core's CJS dist barrel when reached via the fullserver.tsimport chain. Reverted in #431.What
Per a second-opinion review, the cleaner fix is to bundle the backend at build time. esbuild resolves all workspace imports inline (using the
buncondition to pick up TS source rather than the CJS dist), so Node never has to bind named imports against cross-package CJS barrels — the failure mode that broke #430 simply doesn't exist for the bundle.packages/owletto-backend/scripts/build-server-bundle.mjs— esbuild config that bundles workspace + relative imports, externalizes every bare specifier. ~2.4 MB self-contained ESM, ~100 ms build.packages/owletto-backend/package.json—build:servernpm script.docker/app/Dockerfile— switchbun installtobun install --linker=hoistedso node_modules is flat (Node's resolver doesn't understand bun's.bun/<pkg>@<ver>/isolated layout). Add abun run build:serverstep in the builder stage.docker/app/start.sh—exec node /app/.../dist/server.bundle.mjsinstead ofexec bun src/server.ts.Native addons stay external
isolated-vm,
@sentry/*,@opentelemetry/*, pino, etc. — externalized. They need to load via Node's normal resolver because they use native bindings or require-in-the-middle hooks. The hoisted node_modules layout makes them findable from the bundle's location.Verified
Built a local docker image (
docker build -f docker/app/Dockerfile -t lobu-app-spike:bundled .) and exercised runScript via a small bundled test entry:Both are real V8 isolate executions inside the container, not RuntimeUnavailable.
Regression guard
The CI sandbox-runtime test added in #430 (
packages/owletto-backend/src/__tests__/integration/sandbox/run-script-runtime.test.ts) stays in place. It fails loudly when isolated-vm can't load — so any future regression of the runtime contract blocks merge.Tradeoffs
Test plan
executereturns a real result instead of RuntimeUnavailable