feat: onboarding framework, MCP client, settings UI, analytics + misc#195
feat: onboarding framework, MCP client, settings UI, analytics + misc#195
Conversation
…ation - documents.ts: getCurrentOwnerEmail() now reads from AsyncLocalStorage via getRequestUserEmail() instead of process.env (concurrent-safe) - db.ts: migrations v5-v8 are now no-ops — the tables in v1-v4 already include owner_email, and ALTER TABLE ADD COLUMN IF NOT EXISTS doesn't work on SQLite, causing fresh installs to fail
- enterprise-workspace.md: use `create` (not `create-workspace`), show multi-select picker, document `add-app`, add unified-deploy section with same-origin auth/A2A benefits, add shared-env section, refresh "out of scope" now that cross-subpath SSO is solved by deploy. - getting-started.md: lead with workspace + multi-select, mention `--standalone` and `add-app`. - deployment.md: add "Workspace Deploy: One Origin, Many Apps" section at the top; document `APP_BASE_PATH` + workspace env layering.
✅ Deploy Preview for nutritrack-daily-calories ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for agent-native-fw ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Deploying agent-native-mail with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e045a423.agent-native.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native.pages.dev |
Deploying agent-native-calendar with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://81dfcdb5.agent-native-calendar.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-calendar.pages.dev |
Deploying agent-native-forms with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://fd890bd0.agent-native-forms.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-forms.pages.dev |
Deploying agent-native-analytics with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b1d670d0.agent-native-analytics.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-analytics.pages.dev |
Deploying agent-native-content with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://17dab5be.agent-native-67d.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-67d.pages.dev |
Deploying agent-native-videos with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://384f3f77.agent-native-videos.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-videos.pages.dev |
Deploying agent-native-slides with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9f9e21fa.agent-native-slides.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-slides.pages.dev |
Deploying agent-native-dispatcher with
|
| Latest commit: |
5589cd8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://850d3ff0.agent-native-dispatcher.pages.dev |
| Branch Preview URL: | https://updates-90.agent-native-dispatcher.pages.dev |
There was a problem hiding this comment.
Builder has reviewed your changes and found 2 potential issues.
Review Details
Code Review Summary
I've reviewed PR #195, which updates three user-facing docs (getting-started, deployment, enterprise-workspace) and refactors the Cloudflare Pages build process for multi-app workspace deployment (Risk: Standard).
Architectural Approach
The PR successfully pivots the framework from CLI-first onboarding to workspace-by-default, with the agent-native deploy command consolidating multiple apps behind a single origin. The unified deploy story is sound and solves real problems (shared auth sessions, zero-config cross-app A2A). The Cloudflare build refactor is complex but well-intentioned — using esbuild --splitting to reduce entry point size and applying surgical post-build patches to all generated chunks.
Key Findings
Critical Issues Found:
- 🔴 HIGH — Database migrations v5–v8 replaced with no-ops, breaking upgrades for older content databases
- 🟡 MEDIUM — Timer module-scope shim not applied to esbuild-split chunks
- 🟢 LOW — Getting-started docs advertise
pnpm devbut workspace scaffold doesn't support it
What Works Well:
- ✅ Data scoping correctly uses
getRequestUserEmail()from request context - ✅ Documentation is generally accurate with good cross-page anchor setup
- ✅ Post-build patch loop thoughtfully applies patches to all JS files (with one exception noted)
🧪 Browser testing: Skipped — PR modifies documentation and build tooling only, no frontend/UI changes.
Code review by Builder.io
Add a complete ad-hoc analysis workflow: the agent gathers data from multiple sources (HubSpot, Gong, Slack, BigQuery, etc.), synthesizes findings into a Markdown report, and saves a reusable artifact with re-run instructions. Anyone can hit "Re-run" to get fresh results delegated back to the agent. - Actions: save-analysis, get-analysis, list-analyses, delete-analysis - API routes: GET/DELETE /api/analyses, /api/analyses/:id - UI: /analyses list page + /analyses/:id detail with re-run button - Sidebar: Analyses nav link with IconReportAnalytics - Navigation: analyses view support in navigate action + nav state hook - Skill: .agents/skills/adhoc-analysis/SKILL.md with full workflow guide - AGENTS.md: updated with analysis actions, common tasks, skill ref - Also includes dev-all.ts fix for templates.ts port config path
- Restore v5-v8 ALTER TABLE migrations in content template that were incorrectly replaced with SELECT 1 — breaks upgrade path for databases created before owner_email was added to CREATE TABLE statements. - Apply CF Workers setInterval shim to all JS chunks, not just entry — with ESM code splitting, chunks evaluate before entry module body, so the shim must be present in every file.
There was a problem hiding this comment.
Builder has reviewed your changes and found 4 potential issues.
Review Details
Incremental Review: PR #195 Updated
The PR has been updated with new analytics template code. I've reviewed the new additions and found 4 new issues (3 high, 1 medium) — critical security vulnerabilities in the analytics Markdown renderer and data scoping in the new HTTP action.
Previous Issues (Still Unfixed)
The three issues from my initial review remain:
- 🔴 HIGH — Database migrations v5–v8 replaced with no-ops (breaks upgrades)
- 🟡 MEDIUM — Timer shim only on entry file, not split chunks
- 🟢 LOW — Docs advertise
pnpm devwithout scaffold support
NEW Issues Found
Security-Critical:
- 🔴 HIGH — Markdown XSS: Link URLs directly inserted into href without escaping (javascript: protocol attacks)
- 🔴 HIGH — Markdown XSS: Code block language attribute not escaped (event handler injection)
- 🔴 HIGH — list-analyses HTTP action leaks analyses from all users/orgs (uses
getAllSettings()unscoped) - 🟡 MEDIUM — Analyses saved in local mode not migrated on sign-in (remain globally visible after upgrade)
What Works Well
✅ Data scoping helpers in the new handlers are correct
✅ Authorization checks are properly implemented
✅ Settings hierarchy (org → user → global) follows the pattern
🧪 Browser testing: Skipped — new analytics code is backend/component logic, visual/UX verification deferred to specific analytics testing phase.
Code review by Builder.io
| .replace(/`([^`]+)`/g, "<code>$1</code>") | ||
| // Links | ||
| .replace( | ||
| /\[([^\]]+)\]\(([^)]+)\)/g, |
There was a problem hiding this comment.
🔴 XSS vulnerability: Link URLs not escaped in href attribute
The markdown link handler directly inserts user-controlled URLs into href without HTML entity escaping. An attacker can use markdown like [click](javascript:alert('XSS')) to execute arbitrary JavaScript. The resultMarkdown comes from the AI agent and persists in the database (stored XSS).
React with 👍 or 👎 to help me improve.
| codeLines.push(escapeHtml(lines[i])); | ||
| i++; | ||
| } | ||
| i++; // skip closing ``` |
There was a problem hiding this comment.
🔴 XSS vulnerability: Code block language attribute not escaped
The code block language attribute is directly inserted into the class attribute without escaping. A fence like ```" onclick="alert('XSS') becomes <code class="language-" onclick="...>, allowing event handler injection through markdown.
React with 👍 or 👎 to help me improve.
| const all = await getAllSettings(); | ||
| const analyses: Record<string, unknown>[] = []; | ||
|
|
||
| // Collect from all scopes (org > user > global) | ||
| for (const [key, value] of Object.entries(all)) { | ||
| if (!key.includes(KEY_PREFIX)) continue; |
There was a problem hiding this comment.
🔴 HTTP list-analyses action returns analyses from every user and org
The action uses getAllSettings() which scans the entire settings table unscoped, then filters only by key substring. In multi-user/multi-org deployments, any authenticated caller can enumerate other users' saved analyses via GET /_agent-native/actions/list-analyses.
React with 👍 or 👎 to help me improve.
| if (orgId) { | ||
| await putOrgSetting(orgId, key, analysis); | ||
| } else if (email !== "local@localhost") { | ||
| await putUserSetting(email, key, analysis); | ||
| } else { | ||
| await putSetting(key, analysis); |
There was a problem hiding this comment.
🟡 Analyses saved in local mode not migrated on sign-in
Analyses created with AGENT_USER_EMAIL=local@localhost are stored in global settings scope. The existing local-to-user migration route does not include the new adhoc-analysis- prefix, so these analyses never move to the signed-in user and remain globally visible after upgrade.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Builder has reviewed your changes and found 4 potential issues.
Review Details
Incremental Review Update: PR #195 Status
The PR has been updated with fixes for 2 of the 6 previously reported issues. 4 critical security issues remain unfixed.
✅ Issues FIXED
Both deployment-related issues have been properly addressed:
-
Database migrations v5–v8 — Restored proper ALTER TABLE statements that add
owner_emailcolumns to pre-existing tables. Upgrade path is now correct. -
Timer shim on split chunks — The setInterval/setTimeout workaround now applies to ALL .js files in the worker output directory, not just the entry. Solves the Cloudflare Pages ESM chunk evaluation timing issue.
❌ Critical Issues REMAINING (4 issues)
All security vulnerabilities in the analytics template are still present:
XSS Vulnerabilities (2):
- Markdown link URLs directly inserted into href without escaping —
javascript:protocol attacks possible - Markdown code block language attribute unescaped — event handler injection (
onclick=...) possible - Both are stored XSS since resultMarkdown persists in the database
Data Leak:
list-analysesHTTP action usesgetAllSettings()unscoped — any authenticated user can enumerate ALL users' saved analyses across the entire system
Local Mode Migration:
- No migration added for analyses saved in
AUTH_MODE=local— they remain in global scope after sign-in, potentially visible to other users
These 4 issues must be resolved before this PR can be merged. The Markdown XSS vulnerabilities are particularly critical as they enable arbitrary JavaScript execution in analysis viewers' browsers.
Code review by Builder.io
| .replace(/`([^`]+)`/g, "<code>$1</code>") | ||
| // Links | ||
| .replace( | ||
| /\[([^\]]+)\]\(([^)]+)\)/g, |
There was a problem hiding this comment.
🔴 XSS vulnerability: Link URLs not escaped in href attribute
The markdown link handler directly inserts user-controlled URLs into href without HTML entity escaping. An attacker can use markdown like [click](javascript:alert('XSS')) to execute arbitrary JavaScript. The resultMarkdown comes from the AI agent and persists in the database (stored XSS).
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Fixed in 10527d8 — URLs are now run through sanitizeUrl() (blocks javascript:/data:/vbscript:/file: protocols) and escapeHtml() before insertion into href attributes.
| codeLines.push(escapeHtml(lines[i])); | ||
| i++; | ||
| } | ||
| i++; // skip closing ``` |
There was a problem hiding this comment.
🔴 XSS vulnerability: Code block language attribute not escaped
The code block language attribute is directly inserted into the class attribute without escaping. A fence like ```" onclick="alert('XSS') becomes <code class="language-" onclick="...>, allowing event handler injection through markdown.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Fixed in 10527d8 — code fence language hints are now stripped to safe characters ([a-zA-Z0-9_+#.-]) and HTML-escaped before insertion into class attribute.
| const all = await getAllSettings(); | ||
| const analyses: Record<string, unknown>[] = []; | ||
|
|
||
| // Collect from all scopes (org > user > global) | ||
| for (const [key, value] of Object.entries(all)) { | ||
| if (!key.includes(KEY_PREFIX)) continue; |
There was a problem hiding this comment.
🔴 HTTP list-analyses action returns analyses from every user and org
The action uses getAllSettings() which scans the entire settings table unscoped, then filters only by key substring. In multi-user/multi-org deployments, any authenticated caller can enumerate other users' saved analyses via GET /_agent-native/actions/list-analyses.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Fixed in 10527d8 — list-analyses now uses listOrgSettings(orgId, prefix) for org scope and exact u:<email>: prefix matching for user scope instead of unscoped getAllSettings() with substring filter.
| if (orgId) { | ||
| await putOrgSetting(orgId, key, analysis); | ||
| } else if (email !== "local@localhost") { | ||
| await putUserSetting(email, key, analysis); | ||
| } else { | ||
| await putSetting(key, analysis); |
There was a problem hiding this comment.
🟡 Analyses saved in local mode not migrated on sign-in
Analyses created with AGENT_USER_EMAIL=local@localhost are stored in global settings scope. The existing local-to-user migration route does not include the new adhoc-analysis- prefix, so these analyses never move to the signed-in user and remain globally visible after upgrade.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Fixed in 10527d8 — local-mode analyses now use putUserSetting(email, key) which stores under u:local@localhost:adhoc-analysis-*. The existing migrateLocalUserData flow renames these to the real account on sign-in.
Another agent's refactor removed the dev auto-local fallback from isLocalModeEnabled(). Without it, dev mode required real auth (Better Auth or Google OAuth), causing constant logouts on server restart since the session cookie pointed to a DB that may not persist. Restored: in dev mode (NODE_ENV=development) without ACCESS_TOKEN or custom auth, getSession() returns local@localhost automatically. This is the expected dev experience — no auth setup needed.
Adds the "Connect Gmail via Builder" onramp so new users can reach mail's
aha moment without ever opening Google Cloud Console. When
BUILDER_PRIVATE_KEY is set and the user completes the Builder-hosted
OAuth flow, Gmail/People/Calendar calls go through
ai-services.builder.io/google/* instead of calling Google directly.
Agent-native side (this PR):
- credential-provider.ts — generic helpers for Builder-proxy auth:
FeatureNotConfiguredError, hasBuilderPrivateKey, getBuilderAuthHeader,
getBuilderProxyOrigin. Shaped so the LLM gateway layer can reuse it.
- google-proxy.ts — resolveGoogleTarget + per-workspace ledger of
accounts connected via Builder (stored in the shared settings table
so every sibling app in a workspace sees the same accounts).
- builder-browser.ts — getBuilderGoogleConnectUrl for the cli-auth
handoff with the full gmail/calendar/contacts scope set.
- core-routes-plugin.ts — adds /_agent-native/builder/google/status
(banner data) and /_agent-native/builder/google/callback (records
the connected account after Builder finishes the OAuth exchange).
- templates/mail/server/lib/google-api.ts — gmail/people/calendar
functions now accept GoogleAuth = string | GoogleProxyTarget. Direct
string tokens keep the existing Google endpoints; proxy targets
rewrite to ai-services.../google/{api}/*.
- templates/mail/server/lib/google-auth.ts — getClient / getClients /
getAuthStatus / isConnected all now surface Builder-connected
accounts alongside direct-OAuth accounts, de-duped by email.
- templates/mail/app/components/GoogleConnectBanner.tsx — hero layout
gets a prominent "Connect Gmail via Builder" primary CTA (visible
when BUILDER_PRIVATE_KEY is set) with the BYO-keys wizard below it
as a secondary option. "Free while in beta" copy.
Builder side (separate PRs): multi-tenant Google OAuth app, token
storage in organizations_private, and /google/* proxy in ai-services
still to land. The agent-native side degrades to BYO-keys until they do.
Plan: /Users/steve/.claude/plans/sorted-yawning-castle.md
…rding Removes the Builder-hosted multi-tenant Gmail OAuth proxy design (resolveGoogleTarget / builder-google-accounts / GoogleConnectBanner "Connect Gmail via Builder" CTA / /google/* proxy scaffolding). Why: Google's CASA review for "restricted" Gmail scopes (readonly, send, modify) would very likely reject a multi-tenant passthrough where one verified OAuth client proxies tokens for arbitrary downstream apps. Rather than bet Gmail availability on that review, we're pivoting to a per-user BYO-keys flow, optionally bootstrapped by the agent running a browser-automation onboarding (separate PR) so users never have to touch Google Cloud Console by hand. Kept: - packages/core/src/server/credential-provider.ts — the generic FeatureNotConfiguredError / hasBuilderPrivateKey / getBuilderAuthHeader / getBuilderProxyOrigin helpers are still useful for the LLM gateway and future Builder-hosted credential integrations. JSDoc updated to drop the Google-proxy framing. - The existing Builder browser / cli-auth flow (getBuilderBrowserConnectUrl, /builder/callback, /builder/status). - Another agent's /builder/agents-run route, which was unstaged on this branch at revert time. Removed: - packages/core/src/server/google-proxy.ts (entire file). - BUILDER_GOOGLE_* constants and getBuilderGoogleConnectUrl() in builder-browser.ts. - /_agent-native/builder/google/status and /_agent-native/builder/google/callback in core-routes-plugin.ts. - Proxy re-exports in packages/core/src/server/index.ts. - GoogleProxyTarget / GoogleAuth plumbing in templates/mail/server/lib/google-api.ts and google-auth.ts; Gmail/People/Calendar functions go back to taking a plain accessToken: string. - "Connect Gmail via Builder" button + builderStatus fetch in templates/mail/app/components/GoogleConnectBanner.tsx.
- New MCP client module and settings UI components - AgentPanel, CodeRequiredDialog, ResourcesPanel improvements - Macros agent-chat plugin updates
Introduces a shared onboarding checklist surfaced in the agent chat sidebar of every template. Steps are registered via a module-level registry and served over auto-mounted `/_agent-native/onboarding/*` routes. Templates can register app-specific steps (Gmail, Slack, etc.) while the framework ships three defaults (LLM, database, auth). - New subpath exports: `@agent-native/core/onboarding` (types, registry, plugin) and `@agent-native/core/client/onboarding` (useOnboarding hook, OnboardingPanel, OnboardingBanner). - Auto-mounts alongside the other framework plugins via the bootstrap path in framework-request-handler. - OnboardingPanel is lazy-mounted inside AgentPanel — it hides itself once required steps are complete or the user dismisses. - Four method kinds: link, form, builder-cli-auth, agent-task. builder-cli-auth reuses the existing builder-status endpoint and polls for credentials to appear; agent-task uses sendToAgentChat. - State (overrides + dismissal) lives in application-state so it scopes per user and syncs across tabs.
Registers a "Connect Gmail" step with the framework-level onboarding registry so it appears in the agent sidebar's setup checklist. The step offers two completion methods: - Manual wizard (primary): links to the app root where the existing GoogleConnectBanner auto-detects missing credentials and guides the user through Google Cloud Console setup. - Agent task (beta): hands the task to the agent, which drives the browser or falls back to step-by-step instructions and writes the resulting credentials to .env via /_agent-native/env-vars. Completion is detected when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are both set in the environment.
- Log when no config is found and no server is auto-detectable, so users can debug 'why isn't my MCP tool showing up' without tailing the whole startup. - Tighten the auto-detect success log to explicitly mention registration. - Add autoDetectMcpConfig unit tests (PATH discovery, miss, opt-out). - Add mcpToolsToActionEntries unit tests covering agent-only http, text flattening, error-flagged results, and graceful degrade on underlying call failures.
…rdening Inline UseBuilderCard/ManualSetupCard/LLMSectionInner into SettingsPanel to remove unused separate component files. Make Setup tab icon-only. Analytics actions now use scoped settings (org or user) exclusively, closing a data leakage vector where list-analyses could expose other users' saved analyses via unscoped getAllSettings iteration. Markdown renderer hardens against XSS: sanitizeUrl blocks javascript:/ data:/vbscript: protocols, escapes single quotes, and normalizes code fence language hints.
Deploy-time route discovery references defaultOnboardingPlugin as a default plugin, but the onboarding plugin's exports were never added to @agent-native/core/server. Cloudflare Pages builds failed for every template with 'No matching export for import "defaultOnboardingPlugin"'. Also includes in-flight calendar EventDescription extraction and settings panel cleanups from concurrent work on this branch.
With Cloudflare Pages' nodejs_compat_v2 (implicit for compatibility_date ≥ 2024-09-23), plain 'fs' imports are rejected — only 'node:fs' works. Before code splitting, the banner was only in the entry; main's CF deploys happened to pass. Once splitting was turned on, every chunk got the same banner and Pages rejected them with No such module 'node:fs' imported from chunks/... Change the banner to emit 'node:$mod' imports and drop the post-build pass that stripped the prefix. Also carries in-flight settings panel props (builderEnabled/comingSoon) so UseBuilderCard typechecks against its call sites.
Code splitting produced chunks with a banner that imported node:fs etc. Cloudflare Pages rejected those chunks at deploy-time: 'No such module "node:fs" imported from chunks/..' even with nodejs_compat + compat_date 2025-01-01 in wrangler.toml. Reverting to a single-file bundle matches what main shipped and gets CF Pages green again. The entry grows back to ~9MB (same total size we had split across 300 chunks — no new code, just not split).
The prior regex only matched `from "module"`; with the node: prefix now preserved (needed for nodejs_compat_v2), esbuild emits `from "node:module"` instead. That slipped past the patch and the resulting bundle called createRequire(undefined) at worker init, which crashed CF Pages deploy with: The argument 'path' must be a file URL object, ... Received 'undefined'
Without splitting, the single-file bundle hit Cloudflare Pages' 25 MiB Functions size limit (26.3 MiB). With splitting back on, we need to strip the `node:` prefix from every import because Pages Functions runs under nodejs_compat v1 (bare names only) and rejects `node:fs` at worker init with 'No such module "node:fs"'. Workers-on-the-edge with a 2024-09-23+ compat date use v2 (prefix required); Pages Functions lags behind. Strip handles both cases — esbuild-emitted imports and dependency code — and the require shim banner also emits bare names to match.
…calls
The static-import regex missed dynamic `import("node:fs")` calls that
Nitro/h3 emit in some chunks. CF Pages' loader walks every chunk and
fails on the first unresolved node: specifier regardless of whether
the import is static or dynamic.
Extend the post-build strip to cover both dynamic imports and require().
Anthropic SDK source has `import('node:buffer').File` inside an error
string. esbuild preserves the single quotes, so the outer double-quoted
literal stays valid. My earlier strip rewrote to `import("buffer")`
and the inner double quotes terminated the outer string, producing:
Uncaught SyntaxError: Unexpected identifier 'buffer'
at CF Pages deploy time.
Fix: capture the original quote char and preserve it in the replacement.
Verified locally with `wrangler pages dev` — chunk now reads
`import('buffer').File` and the worker boots (no syntax error).
CF Pages' Functions loader scans chunks for "node:*" string literals
and pre-resolves them as module specs — whether or not the string is
ever passed to import()/require() at runtime. Keeping "node:fs" as
an object key in the require shim, or as a string arg to a minified
wrapper like Ut("node:fs"), still triggered:
Uncaught Error: No such module "node:fs"
Replace the targeted import/require regexes with a single pass that
rewrites "node:X" -> "X" for X in a known-builtins list (fs, path,
os, crypto, http, ..., async_hooks, sqlite, worker_threads). Scoping
to known builtins avoids touching user strings that happen to start
with 'node:'.
Also drop the 'node:NAME' keys from the require shim's lookup table
since no runtime lookup uses them anymore.
Verified locally with `wrangler pages dev dist` — no module resolution
errors; no "node:*" strings remain in any chunk.
Switched from post-build string strip to esbuild's --alias flag so node: prefix never lands in chunks in the first place. CF Pages Functions (wrangler 3.x, nodejs_compat v1) rejects `import from "node:fs"` in non-entry chunks, and the post-build regex strip was apparently not reliable across build environments — CF's produced chunks with node:fs intact while the identical local run produced bare names. --alias operates at bundle resolution, not text replacement, so it's environment-independent. Keep the post-build strip as belt & suspenders for any dependency that emits node: through an unusual path (dynamic imports inside template literals, etc.).
esbuild doesn't apply --alias to specifiers that are already matched by --external. With both `--external:node:fs` and `--alias:node:fs=fs`, the external wins and esbuild emits the node: prefix verbatim. Remove the node:* externals so the alias fires first, resolving every `from "node:fs"` to the bare `fs` external. Verified: chunks now contain `import from "fs"` — no node: prefix anywhere.
…ons, and agents Add the ability to manage workspace-wide skills, instructions, and reusable agent profiles from the dispatcher control plane. Resources can be scoped to "all apps" (pushed everywhere) or "selected" (granted per-app), mirroring the vault secret grant model. New tables: workspace_resources, workspace_resource_grants New store: workspace-resources-store.ts (CRUD, grants, sync via /_agent-native/resources) 9 new actions for resource management and cross-app sync New workspace.tsx UI with tabbed skills/instructions/agents view
…antics Since compat_date >= 2024-09-23, `nodejs_compat` implies v2 behavior (node: prefix required). But CF Pages Functions' wrangler pipeline keeps rejecting deploys with: No such module "node:fs" imported from chunks/... Local builds emit bare 'fs' imports (v1 form), wrangler pages dev accepts them, yet CF Pages' production validator fails. Pages Functions has lagged behind Workers on v2 adoption; pinning to the day before v2 (2024-09-22) keeps us on v1 where bare names are the expected form, matching what esbuild emits here. Long-term we should move back to 2025-01-01 once Pages v2 support stabilizes — or once we move off `_worker.js/` chunk output to a single-file bundle or Workers (not Pages).
Subagent-validated: wrangler 3.x with nodejs_compat flag needs date
>= 2024-09-23 for bare builtin imports ('fs', 'path', etc.) to
auto-resolve. Pre-2024-09-23 errors out at bundle time with:
Could not resolve 'util'. Make sure to prefix the module name
with 'node:' or update your compatibility_date to 2024-09-23 or later.
Previous commit pinned to 2024-09-22, which was the wrong direction
for our bare-name strategy — wrangler was 1 day off v2 and forced us
back to v1 (node: prefix required).
Local `wrangler pages dev` now returns HTTP 200 cleanly.
…ndle CF Pages' deploy validator consistently rejects chunked _worker.js/ bundles with 'No such module "node:fs" imported from chunks/...', even when the chunks contain only bare-name imports (which local `wrangler pages dev` + workerd accept fine — HTTP 200). Same chunk hash 'chunk-2UYMX2L7.js' appeared in the failing error across 5 pushes, suggesting CF's upload cache/dedup by content-hash combined with Pages' wrangler 3.101.0 bundler doing something different than local wrangler does. Rather than continue fighting it, revert to single-file bundle like main. Local CF build: 9.1 MB total — well under the 25 MiB limit. Local wrangler pages dev: HTTP 200, no module-resolution errors. Keeps the node: prefix strip + alias + require shim infrastructure in place as belt-and-suspenders, but without splitting none of that is load-bearing anymore.
Slides' single-file CF Pages bundle was 19 MB locally / 26 MB after CF's wrangler polyfills — over the 25 MiB Pages Functions limit. The bloat came from mermaid, @excalidraw/excalidraw, pdf-parse, and @google/genai being bundled into the SSR server chunk via `ssr.noExternal`. These only render in the browser or run from Node action scripts — SSR never executes them — so marking them external cuts the bundle to 8.7 MB without runtime impact. Local wrangler pages dev still returns HTTP 200.
There was a problem hiding this comment.
Builder has reviewed your changes and found 2 potential issues.
Review Details
Code Review: PR #195 Bundle Optimization
What changed: This PR externalizes 5 heavy libraries (mermaid, @excalidraw/excalidraw, @excalidraw/mermaid-to-excalidraw, pdf-parse, @google/genai) from the Cloudflare Pages SSR bundle to reduce the slides template's bundle size from 26MB to 8.7MB.
Risk Assessment: 🔴 HIGH — Bundle configuration change affecting production deployments to Cloudflare Pages. Incorrect externalization can cause runtime failures in deployed apps.
Architectural Concern: The PR correctly identifies a real problem (exceeding CF Pages' 25MB Functions limit), but the solution is unsafe for two of the five externalized libraries:
🔴 Critical Issues Found
Issue 1: pdf-parse is incorrectly externalized from the recruiting template
- Root Cause: pdf-parse is statically imported in
templates/recruiting/actions/filter-candidates.ts(line 5), which imports it fromtemplates/recruiting/server/lib/resume-filter.ts(line 2) - Impact: When the
filter-candidatesaction executes in the CF Pages worker, it will fail with a module-not-found error when attempting to parse PDF resumes (line 89 of resume-filter.ts:const pdf = new PDFParse(...)) - Why it matters: Resume filtering is a core recruiting feature; it will silently fail in production whenever a resume PDF needs to be analyzed
Issue 2: mermaid is incorrectly externalized from the slides template
- Root Cause: mermaid is statically imported in
templates/slides/app/components/deck/MermaidRenderer.tsx(line 2), which is imported bySlideRenderer.tsx(line 6). SlideRenderer is used throughout the application (deck cards, presentation view, slide editor) and is part of the React Router route import graph - Impact: Although MermaidRenderer only executes in the browser, the static import means mermaid is part of the SSR bundle at compile time. Externalizing it will cause the esbuild bundler to emit a bare import in the worker, leading to module-not-found errors if Mermaid slides are encountered during SSR or route resolution
- Why it matters: Any slide with Mermaid diagrams could fail to load properly in production
✅ Acceptable Externalization
@excalidraw/excalidraw and @excalidraw/mermaid-to-excalidraw appear safe — they're only imported via dynamic await import() calls in UI components, keeping them outside the static import graph.
@google/genai is dynamically imported in action handlers, which is safer, though it still carries risk if any code path references it statically.
Recommended Fix
- Keep pdf-parse bundled in the recruiting template — it's required by the filter-candidates action
- Keep mermaid bundled in the slides template — it's part of the SlideRenderer component graph
- Consider alternative bundle-size reduction strategies:
- Lazy-load only the specific features (mermaid rendering, PDF parsing) when actually needed
- Split the worker bundle into multiple chunks if CF Pages supports it
- Trim unused dependencies from package.json
🧪 Browser testing: Skipped — PR touches build configuration only, no UI files modified. Testing should focus on verifying that both the recruiting filter-candidates action and slides presentation views work correctly after deployment.
Code review by Builder.io
Previous approach used `ssr.external`, which tells vite to leave import specifiers untransformed in the server bundle. Wrangler then re-resolved them from node_modules at deploy time and pulled the full mermaid/excalidraw/pdf-parse source into the CF Pages Functions bundle — inflating slides to 34 MiB (over the 25 MiB limit). Replace with a vite plugin that intercepts SSR resolution for these packages and returns a tiny Proxy-based stub instead. The stub answers any property access with another stub, so dead import/re-export chains parse cleanly, but nothing real ships in the server bundle. Client build is unaffected — only SSR hits the plugin. Slides local build: 8.7 MB. wrangler pages dev: HTTP 200.
Register docs-search as an agent tool so every app's agent can look up framework documentation. Docs are bundled in @agent-native/core and read via fs at runtime — always the right version, no network needed. Bump minor version for the new docs-search tool, workspace-management doc, and docs-as-source-of-truth move to core.
Hardcoding mermaid/excalidraw/pdf-parse in the framework's vite helper
was wrong — those are application-specific concerns and the core should
never know their names.
Expose 'ssrStubs: string[]' on defineConfig instead. Templates that have
heavy browser-only deps (currently only slides) opt in by listing them:
defineConfig({
ssrStubs: ['mermaid', '@excalidraw/excalidraw'],
})
Slides opts in for mermaid + excalidraw + mermaid-to-excalidraw.
Removes pdf-parse and @google/genai from the list entirely — pdf-parse
is used server-side by recruiting's filter-candidates action and must
resolve normally; stubbing it would break production PDF parsing.
Framework core no longer mentions any package name.
Summary
Bundles a batch of features and fixes that landed on
updates-90sincemain:Onboarding framework (new)
Shared first-run setup flow for every template — replaces ad-hoc per-template onboarding with one step-by-step checklist pinned to the agent sidebar. Templates register steps with their own completion detection and method choices (paste a key / connect Builder / ask the agent).
packages/core/src/onboarding/— registry + plugin + default steps (LLM, DB, auth)packages/core/src/client/onboarding/—<OnboardingPanel>,useOnboardinghookGET /_agent-native/onboarding/steps,POST /steps/:id/complete,POST /dismisslink/form/builder-cli-auth/agent-task(sends a prompt to the agent chat — the agent can drive setup the same way it drives everything else)MCP client support (new)
Agent-native can now consume tools from user-configured local MCP servers, not just expose its own. Drop an
mcp.config.json(workspace-root or per-app) and every MCP tool shows up in the agent's tool registry with themcp__<server>__prefix.packages/core/src/mcp-client/— config, manager, stdio transportclaude-in-chromeif installed; opt out withAGENT_NATIVE_DISABLE_MCP_AUTODETECT=1Settings UI + AgentPanel refactor
packages/core/src/client/settings/— new modular settings panel (AgentsSection, LLMSection, BrowserSection, BackgroundAgentSection, ComingSoonSection)AgentPanel.tsxslimmed down (~635 → leaner),CodeRequiredDialog,ResourcesPanelcleanupsAnalytics — ad-hoc analysis
New reusable analysis artifact system in the analytics template (separate from pre-built dashboards). Lets users ask the agent one-off questions and pin the result.
Content template
Auth
Docs
CF / deploy
setTimeoutshims work across the full bundleTest plan
pnpm -w run prepgreen locallymcp.config.jsonpointing atclaude-in-chrome, agent tool list includesmcp__claude-in-chrome__*owner_emailacross sessionsAUTH_MODEset → still logged in aslocal@localhostDesign notes:
/Users/steve/.claude/plans/sorted-yawning-castle.md