From 54e6162901846a049435e54c754ba1ec20458c23 Mon Sep 17 00:00:00 2001 From: Lewis Edginton Date: Wed, 18 Mar 2026 18:11:17 +0000 Subject: [PATCH 1/2] fix: gracefully skip web_fetch tool when jsdom fails to load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jsdom has filesystem dependencies (browser/default-stylesheet.css) that break when bundled with esbuild for the Docker runtime — __dirname resolves to the bundle location instead of jsdom's package directory. This caused ENOENT crashes during streamMessage tool assembly, followed by cascading "p is not a function" errors on every retry attempt as jsdom's corrupted module cache returned undefined exports. Wrap the dynamic import in a try/catch so the rest of the toolset still works. Anthropic models already replace web_fetch with a provider-native tool, so the impact is minimal. Co-Authored-By: Claude Opus 4.6 --- src/common/utils/tools/tools.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index a93bc68a82..69a2efb9ba 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -334,9 +334,21 @@ export async function getToolsForModel( const wrap = (tool: Tool) => wrapWithInitWait(tool, workspaceId, initStateManager); - // Lazy-load web_fetch to avoid loading jsdom (ESM-only) at Jest setup time - // This allows integration tests to run without transforming jsdom's dependencies - const { createWebFetchTool } = await import("@/node/services/tools/web_fetch"); + // Lazy-load web_fetch to avoid loading jsdom (ESM-only) at Jest setup time. + // jsdom has filesystem dependencies (browser/default-stylesheet.css) that break + // when bundled with esbuild — the __dirname-relative path resolves incorrectly + // in the Docker runtime. Catch import failures so the rest of the toolset still + // works; Anthropic models already replace web_fetch with a provider-native tool. + let createWebFetchTool: + | typeof import("@/node/services/tools/web_fetch")["createWebFetchTool"] + | undefined; + try { + ({ createWebFetchTool } = await import("@/node/services/tools/web_fetch")); + } catch (error) { + log.warn("Failed to load web_fetch tool (jsdom dependency issue), skipping", { + error: error instanceof Error ? error.message : String(error), + }); + } // Runtime-dependent tools need to wait for workspace initialization // Wrap them to handle init waiting centrally instead of in each tool @@ -367,7 +379,7 @@ export async function getToolsForModel( bash_background_list: wrap(createBashBackgroundListTool(config)), bash_background_terminate: wrap(createBashBackgroundTerminateTool(config)), - web_fetch: wrap(createWebFetchTool(config)), + ...(createWebFetchTool ? { web_fetch: wrap(createWebFetchTool(config)) } : {}), }; // Non-runtime tools execute immediately (no init wait needed) From fdc7c36a5001c620f743403fdca944a0fd3c0fdd Mon Sep 17 00:00:00 2001 From: Lewis Edginton Date: Wed, 18 Mar 2026 18:13:13 +0000 Subject: [PATCH 2/2] fix: externalize jsdom from server bundle for Docker runtime jsdom reads browser/default-stylesheet.css from disk via a __dirname-relative path. When esbuild bundles jsdom into server-bundle.js, __dirname resolves to the bundle directory instead of jsdom's package directory, causing ENOENT in the Docker runtime. Externalize jsdom from the esbuild server bundle (like ssh2 and agent-browser) so it keeps its original filesystem layout. Add a dependency-tracing step in the Dockerfile builder stage and copy the full transitive tree into the runtime image. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 30 ++++++++++++++++++++++++++++++ Makefile | 4 +++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f60a16bd3a..8c7ec0be21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,25 @@ RUN git init && \ # Thread RELEASE_TAG through to scripts/generate-version.sh when CI provides it. RUN RELEASE_TAG="${RELEASE_TAG}" make verify-docker-runtime-artifacts +# Trace jsdom's full transitive dependency tree so the runtime stage can copy +# exactly the packages it needs. jsdom is externalized from the esbuild bundle +# because it reads browser/default-stylesheet.css from disk via __dirname. +RUN node -e " \ + const fs = require('fs'), path = require('path'); \ + const seen = new Set(); \ + function trace(name) { \ + if (seen.has(name)) return; \ + seen.add(name); \ + try { \ + const pkgPath = require.resolve(name + '/package.json'); \ + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); \ + for (const d of Object.keys(pkg.dependencies || {})) trace(d); \ + } catch {} \ + } \ + trace('jsdom'); \ + fs.writeFileSync('/tmp/jsdom-deps.txt', [...seen].join('\n')); \ +" + # ============================================================================== # Stage 2: Runtime # ============================================================================== @@ -100,6 +119,17 @@ COPY --from=builder /app/node_modules/bcrypt-pbkdf ./node_modules/bcrypt-pbkdf COPY --from=builder /app/node_modules/tweetnacl ./node_modules/tweetnacl # - @1password/sdk + sdk-core: externalized; contains native WASM for secret resolution COPY --from=builder /app/node_modules/@1password ./node_modules/@1password +# - jsdom + transitive deps: externalized because it reads CSS from disk via __dirname. +# Copy the traced dependency tree built in the builder stage. +COPY --from=builder /tmp/jsdom-deps.txt /tmp/jsdom-deps.txt +RUN --mount=from=builder,source=/app/node_modules,target=/mnt/node_modules \ + while IFS= read -r pkg; do \ + src="/mnt/node_modules/$pkg"; \ + if [ -d "$src" ]; then \ + mkdir -p "node_modules/$pkg" && cp -a "$src/." "node_modules/$pkg/"; \ + fi; \ + done < /tmp/jsdom-deps.txt && \ + rm /tmp/jsdom-deps.txt # Copy frontend/static assets from least to most volatile for better cache reuse. # Vite outputs JS/CSS/HTML directly to dist/ (assetsDir: "."). diff --git a/Makefile b/Makefile index afb378fa6a..dfd93b4ec5 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,9 @@ ESBUILD_CLI_FLAGS := --bundle --format=esm --platform=node --target=node20 --out # Common esbuild flags for server runtime Docker bundle. # Place runtime bundles under dist/runtime so frontend dist/*.js layers remain stable. # External native modules (node-pty, ssh2) and electron remain runtime dependencies. -ESBUILD_SERVER_FLAGS := --bundle --platform=node --target=node22 --format=cjs --outfile=dist/runtime/server-bundle.js --external:@lydell/node-pty --external:node-pty --external:electron --external:ssh2 --external:@1password/sdk --external:@1password/sdk-core --external:agent-browser --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --minify +# jsdom is externalized because it reads browser/default-stylesheet.css from +# disk via a __dirname-relative path that breaks when bundled. +ESBUILD_SERVER_FLAGS := --bundle --platform=node --target=node22 --format=cjs --outfile=dist/runtime/server-bundle.js --external:@lydell/node-pty --external:node-pty --external:electron --external:ssh2 --external:@1password/sdk --external:@1password/sdk-core --external:agent-browser --external:jsdom --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --minify # Common esbuild flags for tokenizer worker bundle used by server-bundle runtime. ESBUILD_TOKENIZER_WORKER_FLAGS := --bundle --platform=node --target=node22 --format=cjs --outfile=dist/runtime/tokenizer.worker.js --minify