Skip to content

fix(chat): keep the eval-based dynamic import out of client bundles - #3421

Merged
kwakayama merged 3 commits into
mainfrom
fix/chat-client-eval-reachability
Aug 6, 2026
Merged

fix(chat): keep the eval-based dynamic import out of client bundles#3421
kwakayama merged 3 commits into
mainfrom
fix/chat-client-eval-reachability

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

Project pages are served with script-src 'self' 'nonce-...' https://esm.sh. That has no 'unsafe-eval', so the new Function("specifier", ...) in platform/compat/dynamic-import.ts throws EvalError in the browser. Hydration dies before first paint and the page sits on its skeleton loaders forever.

Observed on support-agent-vkmo7d.production.veryfront.com:

[Veryfront] Client initialization error: EvalError: Evaluating a string as JavaScript
violates the following Content Security Policy directive because 'unsafe-eval' is not
an allowed source of script: script-src 'self' 'nonce-...' https://esm.sh
    at new Function (<anonymous>)
    at .../_vf/assets/bfbed417...js:1:9
[Veryfront] Hydration failed signal received

document.querySelectorAll('*').length is 61 and the page text is Loading messages....

This is independent of #3420. That PR fixed release-asset 503 shedding, which was also breaking this page. Page loads that shed zero assets still died on this EvalError, so both fixes are needed for the page to render.

What changed

Two barrel imports pulled a server-only helper into the veryfront/chat client entry. Both need only getHostEnv, but importing the platform/compat/process.ts barrel also drags in its runCommand re-export from process/command.ts, which imports dynamic-import.ts:

chat/index.ts -> chat/stream-watchdog.ts
              -> agent/streaming/lifecycle/watchdog-compat-adapter.ts
              -> platform/compat/process.ts -> process/command.ts -> dynamic-import.ts

chat/index.ts -> react/components/chat/agent-card.tsx -> ... ->
                 react/components/chat/missing-renderer-warning.ts
              -> platform/environment.ts
              -> platform/compat/process.ts -> process/command.ts -> dynamic-import.ts

Both now import #veryfront/platform/compat/process/env.ts directly, where getHostEnv is defined. That is already the convention — see security/sandbox/deno-sandbox.ts and utils/logger/logger.ts.

The new Function is deliberately left alone

It is load-bearing on the server: it keeps the import non-literal so neither deno compile nor the release-asset rewriter traces into the specifier.

I tried removing it first. deno compile tolerates the bare form, but the release-asset builder does not:

specifier=veryfront/chat  sourceKey=platform/compat/dynamic-import
error="Release module contains a non-literal dynamic import"
→ dependency-transform-failed:veryfront  module-rewrite-failed:app

That broke scaffolded-project-build.test.ts across 5 scaffolds. The fix is to keep the eval and keep it off the client. The doc comment on that helper mentions only deno compile, which is what made this look safe — worth a follow-up to correct.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Checklist

  • I have added tests that prove my fix is effective

Two guards, both confirmed red before the fix and green after:

1. Shipped bytes (scaffolded-project-build.test.ts) — asserts no published JS asset contains new Function. This is ground truth, and it is the assertion that matters: the build succeeds either way, and every pre-existing assertion in that file passed while the eval was shipping.

Measured by probing actual uploads:

scaffold before after
minimal 0 0
ai-agent 1 (bfbed417dd16) 0
docs-agent 1 (bfbed417dd16) 0
agentic-workflow 0 0
multi-agent-system 1 (bfbed417dd16) 0
coding-agent 1 (bfbed417dd16) 0
saas-starter 1 (bfbed417dd16) 0

bfbed417dd16 is byte-identical to the chunk failing in production. Uploads per scaffold drop 269 → 263, so the whole server-only subtree leaves the client bundle rather than just the one chunk.

2. Import reachability (dynamic-import.test.ts) — walks value imports (not import type, which is erased) from the six client entries in PLATFORM_UTILITY_PATHS and fails with the offending chain.

This guard earned its place immediately: it found the second chain above, which manual tracing missed because a BFS only reports the shortest path. Without it I would have shipped a fix that cut one chain and left the other.

Verification: src/release-assets/ 15 passed / 220 steps; src/platform/ src/chat/ src/agent/streaming/ 307 passed / 3245 steps; deno check, deno lint, deno fmt --check clean. Rebased onto 540509c5 (#3420 merged).

Notes for review

  • Only chat/index.ts ever leaked. The other five client entries are clean, and mdx/workflow only appeared to leak under a regex that ignored import type.
  • The barrel platform/compat/process.ts is a recurring hazard: any client-reachable module importing it inherits runCommand. The reachability test now catches that class of mistake rather than this one instance.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented CSP-related hydration errors caused by incompatible browser bundle code.
    • Ensured scaffolded release builds reject JavaScript assets containing unsafe dynamic evaluation.
  • Tests

    • Added checks to detect client-bundle paths that could include incompatible dynamic import code.
    • Added regression coverage across scaffolded release builds.
  • Documentation

    • Updated source references throughout the chat API documentation.

Project pages are served with `script-src 'self' 'nonce-...' https://esm.sh`.
That has no 'unsafe-eval', so the `new Function("specifier", ...)` in
platform/compat/dynamic-import.ts throws EvalError in the browser. Hydration
dies before first paint and the page sits on its skeleton loaders forever.

Two barrel imports were pulling that server-only helper into the
`veryfront/chat` client entry. Both need only `getHostEnv`, but importing the
`platform/compat/process.ts` barrel also drags in its `runCommand` re-export
from process/command.ts, which imports dynamic-import.ts:

  chat/index.ts -> chat/stream-watchdog.ts
                -> agent/streaming/lifecycle/watchdog-compat-adapter.ts
                -> platform/compat/process.ts -> process/command.ts

  chat/index.ts -> react/components/chat/agent-card.tsx -> ... ->
                   react/components/chat/missing-renderer-warning.ts
                -> platform/environment.ts
                -> platform/compat/process.ts -> process/command.ts

Both now import `platform/compat/process/env.ts` directly, which is where
getHostEnv is defined and which is already the convention elsewhere (see
security/sandbox/deno-sandbox.ts, utils/logger/logger.ts).

The `new Function` itself is deliberately left alone. It is load-bearing on the
server: it keeps the import non-literal so neither `deno compile` nor the
release-asset rewriter traces into the specifier. Removing it is not an option
— the release-asset builder then rejects the module outright with "Release
module contains a non-literal dynamic import". The fix is to keep the eval and
keep it off the client.

Two regression guards, both confirmed red before the fix and green after:

- scaffolded-project-build asserts no *published* JS asset contains
  `new Function`. This is the ground truth: before the fix, 5 of 7 scaffolds
  published chunk bfbed417dd16 — byte-identical to the asset failing in
  production — and every existing assertion in that file still passed.
- dynamic-import.test walks value imports (not `import type`) from the six
  client entries in PLATFORM_UTILITY_PATHS and fails with the offending chain.
  This one found the second chain above, which manual tracing had missed.

Verified: uploads per scaffold drop 269 -> 263, so the whole server-only
subtree leaves the client bundle rather than just the one chunk.
@kwakayama
kwakayama requested a review from kojiwakayama as a code owner August 6, 2026 08:33
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e90a6c9b-2eaa-4c65-bade-a382d64371ae

📥 Commits

Reviewing files that changed from the base of the PR and between adf66e0 and 1ae9d63.

📒 Files selected for processing (2)
  • docs/api-reference/veryfront/chat.md
  • src/platform/compat/dynamic-import.test.ts

📝 Walkthrough

Walkthrough

The change replaces two barrel imports with direct environment imports. It adds client-bundle reachability tests and scaffold build assertions that reject JavaScript assets containing new Function(...).

Changes

CSP-safe client bundles

Layer / File(s) Summary
Direct environment imports
src/agent/streaming/lifecycle/watchdog-compat-adapter.ts, src/platform/environment.ts, docs/api-reference/veryfront/chat.md
Both modules import getHostEnv directly from process/env.ts. Chat API source links now point to updated watchdog declarations.
Bundle regression checks
src/platform/compat/dynamic-import.test.ts, src/release-assets/scaffolded-project-build.test.ts
Tests resolve client imports, trace dependency chains, and reject scaffolded JavaScript assets containing new Function(...).

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: kojiwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the fix that keeps eval-based dynamic imports out of client bundles.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-client-eval-reachability

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/compat/dynamic-import.test.ts`:
- Around line 68-72: Update the candidate list in resolveSpecifier to include
the directory fallback `${base}/index.tsx` alongside the existing index.ts
option, so extensionless imports resolving to index.tsx return their resolved
candidate.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e51b032-7c62-4d57-a597-dd6224a98de0

📥 Commits

Reviewing files that changed from the base of the PR and between 540509c and adf66e0.

📒 Files selected for processing (4)
  • src/agent/streaming/lifecycle/watchdog-compat-adapter.ts
  • src/platform/compat/dynamic-import.test.ts
  • src/platform/environment.ts
  • src/release-assets/scaffolded-project-build.test.ts

Comment thread src/platform/compat/dynamic-import.test.ts Outdated
…rence

Two `ci (lint)` tasks, both caught locally against the same chain CI runs:

- lint:test-typecheck — regex capture groups are `string | undefined` under
  the repo's strict indexing, so guard the pushes instead of indexing raw.
- docs:api-reference:check — the explanatory comment added to
  watchdog-compat-adapter.ts shifts its line numbers, and the generated
  reference pins source links by line. Regenerated with `deno task docs`;
  the chat.md diff is line-number movement only, no API surface change.
…ty guard

resolveSpecifier checked `${base}/index.ts` but not `${base}/index.tsx`, so an
extensionless value import resolving through a .tsx directory module returned
null and the traversal stopped there — a silent pass.

That matters here specifically: the guard walks React subtrees, and
src/react/components/chat/chat/index.tsx is a directory module inside
chat/index.ts, the one entry that actually leaked.

No live blind spot today (nothing currently reaches those three .tsx directory
modules through a relative or #veryfront extensionless import), so this closes
a false-negative class rather than fixing a miss. Guard still fails on the
pre-fix imports.
@kwakayama
kwakayama added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 666bd52 Aug 6, 2026
31 checks passed
@kwakayama
kwakayama deleted the fix/chat-client-eval-reachability branch August 6, 2026 09:10
@kwakayama kwakayama mentioned this pull request Aug 6, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant