Skip to content

fix(execute): bundle backend so prod runs under Node + isolated-vm#433

Merged
buremba merged 1 commit into
mainfrom
fix/execute-runtime-bundled
Apr 28, 2026
Merged

fix(execute): bundle backend so prod runs under Node + isolated-vm#433
buremba merged 1 commit into
mainfrom
fix/execute-runtime-bundled

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 28, 2026

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.node fails to dlopen under Bun:

undefined symbol: v8::ValueSerializer::Delegate::HasCustomHostObject

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'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.

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 bun condition 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.jsonbuild: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.shexec node /app/.../dist/server.bundle.mjs instead of exec 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:

basic:    {success: true, returnValue: 3}
chaining: {success: true, returnValue: {slug: 'atlas', ok: true}}

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

  • Build adds ~5 s for the esbuild bundle.
  • Hoisted layout is npm-style flat: install size similar, but matches what Node expects natively.
  • If a native dep was wrongly bundled, it'll surface at boot (not silently) — easier to triage.

Test plan

  • CI passes (build-test, sandbox-runtime test, all gates)
  • After merge: build-images succeeds, deploy rolls cleanly, both app pods Running on the new tag
  • Smoke: MCP execute returns a real result instead of RuntimeUnavailable

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.
@github-actions github-actions Bot added the triage:needs-human Triage agent escalated for human review label Apr 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Triage decision: needs-human

Reasons:

  • File changes under docker/ infra path (docker blast radius per .github/triage-config.yml)
  • Modified files: docker/app/Dockerfile, docker/app/start.sh
  • Infra changes require human review due to deployment impact

Next: @buremba assigned for manual review — Docker bundling changes for Node + isolated-vm compatibility in production

@buremba buremba merged commit 20afae5 into main Apr 28, 2026
14 checks passed
@buremba buremba deleted the fix/execute-runtime-bundled branch April 28, 2026 00:08
@buremba buremba restored the fix/execute-runtime-bundled branch May 12, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:needs-human Triage agent escalated for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant