Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ==============================================================================
Expand Down Expand Up @@ -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: ".").
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,21 @@ export async function getToolsForModel(
const wrap = <TParameters, TResult>(tool: Tool<TParameters, TResult>) =>
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
Expand Down Expand Up @@ -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)
Expand Down