Skip to content

feat(ai): @dotcms/ai agentic runtime + mcp-server migration#36309

Merged
fmontes merged 13 commits into
mainfrom
fmontes/dotcms-ai-sdk
Jun 25, 2026
Merged

feat(ai): @dotcms/ai agentic runtime + mcp-server migration#36309
fmontes merged 13 commits into
mainfrom
fmontes/dotcms-ai-sdk

Conversation

@fmontes

@fmontes fmontes commented Jun 24, 2026

Copy link
Copy Markdown
Member

What & why

Productionizes the internal agentic runtime as @dotcms/ai (libs/agentic-toolslibs/sdk/ai): a model — or any driver — writes code, and the runtime runs it sandboxed against the dotCMS API with auth and policy owned in one place. The consumers (apps/mcp-server, apps/ai-evals) are migrated onto it in the same change, so the new API is proven, not just published.

@dotcms/ai runtime

  • Subpaths as seams: @dotcms/ai/runtime (createRuntime — the front door), /sandbox (generic engine, lint-enforced no-dotCMS), /adapter (dotCMS-wired), /spec (opt-in OpenAPI spec). Bare @dotcms/ai is a pure namespace.
  • One runtime, two verbs: request() (direct) and run(code) (sandboxed) both route through a single requestCore — one auth path, one allow-list, one error model, so they can't drift.
  • defineAdapter with Zod input/output for typed, named operations; typed error hierarchy that round-trips the worker boundary.
  • Confinement (trusted code generators, not adversarial isolation): token never enters the sandbox; fetch/require/dynamic import() blocked; process.env emptied; worker resourceLimits + wall-clock timeout; AbortSignal aborts in-flight host work on timeout.
  • Packaging: drops private, dual ESM/CJS, exports map; spec is build-generated (git-ignored) via sdk-ai:generate-spec, wired through consumers' dependsOn.

MCP server

  • New tools download_assets / upload_assets — transfer dotCMS file assets (themes, VTL, CSS, …) between the server's disk and dotCMS. Bytes stream server-side and never enter the model's context; only a JSON manifest is returned. Descriptions steer the agent to use them (vs. hand-rolling uploads), and a 0-match download/upload now warns instead of silently succeeding (surfaces the //site/path parsing footgun).
  • execute / search migrated onto createRuntime; execute's file-upload guidance redirects to the dedicated tools.
  • Local builds: DOTCMS_SPEC_URL=… pnpm nx build mcp-server regenerates the spec from a local instance in one command (env vars flow through dependsOn; CLI args don't). README switched to pnpm.

Backend

Small REST fix: typed request bodies for site/container/template write endpoints so they appear correctly in the generated OpenAPI spec (regenerated openapi.yaml).

Notes

  • No compat shim — the old @dotcms/agentic-tools was private/internal, so the rename + consumer migration land together.
  • Copilot review addressed: lazy spec load, dynamic-import() block, ESM-safe worker factory, doc accuracy. (+json-as-text kept — intentional and tested.)

Testing

37 sdk-ai unit tests (sandbox confinement/routing/abort, defineAdapter, runtime, http-client); lint + typecheck clean; mcp-server builds end-to-end and serves all four tools; ai-evals builds.

🤖 Generated with Claude Code

This PR fixes: #36304

fmontes and others added 5 commits June 24, 2026 10:23
…rver

Move libs/agentic-tools -> libs/sdk/ai and rename @dotcms/agentic-tools ->
@dotcms/ai, splitting the flat lib into subpath seams:
  - @dotcms/ai          front door: createDotCMSRuntime (request + run)
  - @dotcms/ai/sandbox  generic engine (lint-enforced: no dotCMS imports)
  - @dotcms/ai/adapter  dotcmsAdapter, shared requestCore, context + cache
  - @dotcms/ai/spec     OpenAPI spec (opt-in subpath)

Front door: one runtime, two verbs. request() is direct (no worker); run(code)
is sandboxed for model-written code. Both route through a single requestCore
(one auth path, one allow-list, one error model) so they cannot drift.

defineAdapter with Zod input (trust boundary) + output (tool-contract boundary);
output-less adapters are typed not-model-exposable and withheld from
describeAdapterForLLM.

Typed error hierarchy (DotCMSError + subclasses) that round-trips the worker
boundary with name/code/detail intact.

Hardening: worker resourceLimits, an AbortSignal threaded so a sandbox timeout
aborts in-flight fetch, context cache keyed on (sessionId, url).

Packaging: drop private, real semver, Rollup ESM/CJS, exports map, engines.

Consumers migrated in the same change (no shim): apps/mcp-server uses the front
door; apps/ai-evals uses the /sandbox + /adapter + /spec power-user path. Both
build/test dependsOn sdk-ai:generate-spec.

Simplify pass: extracted the duplicated ~150-line worker harness into one shared
worker-harness.ts (hoisted to a module constant), shared the worker message
types and ResolvedSandboxConfig, fixed the Bun resourceLimits config drift, and
collapsed repeated adapter-config construction in the runtime.

Tests: 34 passing (sandbox confinement/routing/abort, defineAdapter, runtime,
http-client). Lint + typecheck clean; mcp-server builds end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, abort

Codex independent review confirmed the "one requestCore" claim holds for auth,
allow-list, and error model, and flagged three runtime/abort gaps:

- run(): context loading happened before the timeout/onTeardown path existed, so
  a hanging context API call could make run() hang past `timeout`. Now the
  context load shares the run's AbortController and is bounded by a timeout that
  aborts it (loaders degrade to empty context on abort, so run still completes).
- request(): the direct path now accepts an optional { signal } so callers can
  make it abortable (it has no surrounding timeout of its own). Additive — no
  change for existing callers.
- SSRF guard: documented the residual DNS-rebinding limitation (the guard checks
  the literal host, not the resolved IP) — matches the stated threat model
  (capability confinement, not adversarial isolation); a hardened fetcher is the
  consumer's responsibility for untrusted URLs.

Tests: 36 passing (added direct-request abort passthrough + context-load
non-hang). Lint + typecheck clean; mcp-server builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ime → createRuntime

Naming-only change (no logic/behavior/threat-model/requestCore/adapter/packaging
changes beyond moving the export):

- createDotCMSRuntime → createRuntime (function, call sites, error strings). The
  public instance/config types keep their descriptive names (DotCMSRuntime,
  DotCMSRuntimeConfig) — `Runtime`/`RuntimeConfig` are too generic to export.
- The front door is no longer exported from the bare @dotcms/ai. It moves to the
  @dotcms/ai/runtime subpath (source: runtime.ts). The bare @dotcms/ai is now a
  pure namespace with no `.` export — deleted src/index.ts; its curated
  re-exports (defineAdapter, error hierarchy, result/context types) now come from
  runtime.ts so /runtime callers don't reach into lower subpaths. /sandbox,
  /adapter, /spec unchanged.
- package.json exports + typesVersions: drop `.`, add `./runtime`.
  project.json rollup `main` → runtime.ts. tsconfig.base.json + mcp-server
  xmcp.config.ts aliases: drop bare @dotcms/ai, add @dotcms/ai/runtime.
- apps/mcp-server execute.ts/search.ts import createRuntime from
  '@dotcms/ai/runtime'.
- README: removed the "execution runtime, not an AI agent / 'agent' appears
  nowhere" disclaimer (the /runtime subpath name now carries that); updated
  samples + topology table to /runtime.

Why: a bare @dotcms/ai import read as "the AI thing" and set an LLM/agent
expectation the runtime doesn't meet. The /runtime subpath makes the import site
state what it is, so the caveat is no longer needed.

Verified: lib typecheck + lint clean, 36 tests pass, mcp-server builds
end-to-end; a tsc resolution check confirms @dotcms/ai/runtime resolves
createRuntime while bare @dotcms/ai no longer resolves to a front-door export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lead with the differentiator (the model writes code against the whole API, vs a
fixed vendor tool menu) and that dotCMS ships its own MCP server + first-party
agents on this runtime. Add a "Governed by construction" section that front-loads
the three governance properties (token never in sandbox, adapters are the only
way out, you decide the surface), and tie defineAdapter to it as the governed
path in practice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odel)

Clarify the runtime (not "the CMS") runs the code against the dotCMS API; note
it sits beneath any driver — model, agent framework, or automation tool like
n8n — not only an LLM. Minor wording/wrap cleanups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries labels Jun 24, 2026
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @fmontes's task in 1m 38s —— View job


Rollback Safety Analysis

  • Read rollback-unsafe categories reference
  • Get full PR diff
  • Analyze diff against unsafe categories
  • Apply appropriate label

Result: ✅ Safe to Rollback

After analyzing the full diff against every category in the rollback-unsafe reference, no unsafe patterns were found.

What this PR changes

Area Changes
core-web/libs/sdk/ai/ New TypeScript library (@dotcms/ai) — frontend/SDK only
core-web/libs/agentic-tools/ Deleted — TypeScript only, no Java or DB involvement
core-web/apps/mcp-server/ MCP server tools rewritten — TypeScript only
core-web/apps/ai-evals/ Eval app updated — TypeScript only
dotCMS/src/main/java/ Three REST resources with @Consumes(MediaType.APPLICATION_JSON) added and OpenAPI @RequestBody annotations
dotCMS/…/openapi.yaml Content-type narrowed from '*/*' to application/json in request body schema

Category-by-category verdict

Category Verdict
C-1 Structural data model change ✅ No DB schema change of any kind
C-2 Elasticsearch mapping change ✅ No ES mapping code touched
C-3 contentlet_as_json model version bump ✅ Not touched
C-4 DROP TABLE / DROP COLUMN ✅ None
H-1 One-way data migration / backfill ✅ No runonce tasks
H-2 RENAME TABLE / RENAME COLUMN ✅ None
H-3 PK restructuring ✅ None
H-4 New ContentType field type ✅ None
H-5 Binary storage provider change ✅ None
H-6 DROP PROCEDURE / FUNCTION ✅ None
H-7 NOT NULL column without default ✅ None
H-8 VTL Viewtool contract change ✅ No viewtools modified
M-1 Non-broadening column type change ✅ None
M-2 Push publishing bundle format change ✅ None
M-3 REST/GraphQL API contract change ✅ See note below
M-4 OSGi interface breakage ✅ No OSGi interfaces changed

Note on M-3 (REST API)

The Java changes add @Consumes(MediaType.APPLICATION_JSON) to POST/PUT endpoints for Site, Container, and Template, and narrow the OpenAPI requestBody.content key from '*/*' to application/json. This is a documentation/strictness tightening — callers sending Content-Type: application/json (which is the standard) are unaffected. Callers sending a non-JSON content type would have been accepted before and now receive a 415 Unsupported Media Type.

This change is forward-only: rolling back to N-1 simply reverts to '*/*' acceptance, which is more permissive. No client that was working on N will break on N-1. Not a rollback risk.

Label applied: AI: Safe To Rollback

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/apps/mcp-server/src/tools/execute.ts:91createRuntime() is called with timeout from process.env.SANDBOX_TIMEOUT, but the createRuntime function signature in the new @dotcms/ai/runtime module does not accept a timeout parameter — this causes a runtime error when the sandbox executes. The PR replaces createExecutor() with createRuntime() but fails to pass timeout correctly, and the new createRuntime expects timeout as a top-level option, not as a separate arg. This is a breaking change without adaptation.
  • 🔴 Critical: core-web/apps/mcp-server/src/tools/search.ts:53 — Same issue: createRuntime() is called with timeout: 10000 as a positional arg, but the new createRuntime expects timeout as a named option in the config object. This will cause undefined timeout and potentially infinite execution.
  • 🔴 Critical: core-web/apps/mcp-server/src/tools/execute.ts:91 and core-web/apps/mcp-server/src/tools/search.ts:53createRuntime() is called with sessionId and token/url, but the new createRuntime requires policy and includeSpec as explicit options — missing includeSpec: true in execute.ts breaks search tool behavior (spec not injected), and timeout is mispassed as above. This breaks core functionality silently.
  • 🟠 High: core-web/apps/mcp-server/src/tools/execute.ts:91dotcms.run(code) is called without handling AbortError or TimeoutError from requestCore — the old Executor had timeout handling, but the new createRuntime().run() may throw uncaught AbortError or TimeoutError from requestCore when the AbortSignal is triggered, crashing the MCP server process. No try/catch or error mapping exists.
  • 🟠 High: core-web/libs/sdk/ai/src/adapter/http-client.ts:101dotcmsAdapter().request() wraps adapter.methods.get('request')?.execute but does not validate that execute is a function — if the adapter is misconfigured or the method is missing, this returns undefined and Promise.resolve(undefined) is returned, silently corrupting the response. Must throw if method is not found.
  • 🟡 Medium: core-web/libs/sdk/ai/src/adapter/http-client.ts:101Promise.resolve(request?.(options)) does not catch or rethrow errors from request — if request throws synchronously (e.g. due to invalid options), it will not be caught and will crash the process. Must wrap in try/catch.
  • 🟡 Medium: core-web/apps/mcp-server/xmcp.config.ts:16resolve.alias maps @dotcms/ai/runtime to ../../libs/sdk/ai/src/runtime.ts — but runtime.ts is not an ES module entry point; it exports createRuntime as a default, but the alias points to the source file, not the compiled output. This breaks in production builds where source files are not available. Must point to dist/libs/sdk/ai/runtime.js or use package.json exports.

Existing

  • 🟡 Medium: core-web/libs/sdk/ai/src/adapter/http-client.ts:101 — Prior finding: request method may be undefined — still present. The code does not validate adapter.methods.get('request') exists before calling execute.

Resolved

  • core-web/apps/ai-evals/project.json:10dependsOn updated from agentic-tools to sdk-ai — now correct.
  • core-web/apps/mcp-server/README.md:301agentic-tools:generate-spec replaced with sdk-ai:generate-spec — now correct.
  • core-web/apps/mcp-server/project.json:10dependsOn updated from agentic-tools to sdk-ai — now correct.
  • core-web/apps/mcp-server/project.json:23dependsOn updated from agentic-tools to sdk-ai — now correct.
  • core-web/apps/mcp-server/project.json:31dependsOn updated from agentic-tools to sdk-ai — now correct.
  • core-web/apps/mcp-server/src/tools/execute.ts:1 — Import path updated from @dotcms/agentic-tools to @dotcms/ai/runtime — now correct.
  • core-web/apps/mcp-server/src/tools/search.ts:1 — Import path updated from @dotcms/agentic-tools to @dotcms/ai/runtime and @dotcms/ai/spec — now correct.
  • core-web/apps/mcp-server/xmcp.config.ts:13 — Alias paths updated to @dotcms/ai/* subpaths — now correct.
  • core-web/libs/agentic-tools/ — Entire directory deleted — no longer referenced.

Run: #28119087375 · tokens: in: 21262 · out: 1486 · total: 22748

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR productionizes the in-repo “agentic runtime” as @dotcms/ai under core-web/libs/sdk/ai, introduces subpath entrypoints (/runtime, /sandbox, /adapter, /spec), and migrates apps/mcp-server and apps/ai-evals to the new API while deleting the prior libs/agentic-tools prototype.

Changes:

  • Replaces @dotcms/agentic-tools with publishable @dotcms/ai and updates TS/Rspack path aliasing accordingly.
  • Adds a new front-door runtime (createRuntime) plus shared request core, sandbox engine, adapter utilities, error model, and spec generation tooling.
  • Migrates mcp-server and ai-evals consumers onto the new @dotcms/ai/* subpaths and wires sdk-ai:generate-spec as a build/test dependency.

Reviewed changes

Copilot reviewed 47 out of 51 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
core-web/tsconfig.base.json Updates TS path aliases to new @dotcms/ai/* subpaths.
core-web/libs/sdk/ai/tsconfig.spec.json Adjusts spec build output path; enables JSON module resolution for tests.
core-web/libs/sdk/ai/tsconfig.lib.json Adjusts library build output path.
core-web/libs/sdk/ai/tsconfig.json Fixes extends path after moving library location.
core-web/libs/sdk/ai/src/spec/spec.ts Adds getSpec() backed by generated spec.json.
core-web/libs/sdk/ai/src/spec/index.ts Introduces /spec subpath barrel export + documentation.
core-web/libs/sdk/ai/src/sandbox/worker-harness.ts Centralizes shared Node/Bun worker harness and resource limits.
core-web/libs/sdk/ai/src/sandbox/types.ts Defines sandbox/adapters/types + shared worker protocol types.
core-web/libs/sdk/ai/src/sandbox/sandbox.spec.ts Adds confinement + routing tests for sandbox worker behavior.
core-web/libs/sdk/ai/src/sandbox/node-worker.ts Implements Node worker_threads sandbox backend with limits and error serialization.
core-web/libs/sdk/ai/src/sandbox/interface.ts Fixes interface import path to local sandbox types.
core-web/libs/sdk/ai/src/sandbox/index.ts Adds /sandbox subpath barrel exports and createSandbox API.
core-web/libs/sdk/ai/src/sandbox/factory.ts Adds runtime-detecting worker sandbox factory (Node vs Bun).
core-web/libs/sdk/ai/src/sandbox/executor.ts Switches executor default sandbox factory to the new worker factory.
core-web/libs/sdk/ai/src/sandbox/errors.ts Adds typed error hierarchy + serialization used across runtime and sandbox.
core-web/libs/sdk/ai/src/sandbox/define-adapter.ts Adds defineAdapter with Zod input/output validation and LLM tool descriptions.
core-web/libs/sdk/ai/src/sandbox/define-adapter.spec.ts Adds unit tests for adapter definition/validation/LLM tool description.
core-web/libs/sdk/ai/src/sandbox/bun-worker.ts Implements Bun Web Worker sandbox backend consistent with Node behavior.
core-web/libs/sdk/ai/src/runtime.ts Adds createRuntime front door that unifies direct requests + sandboxed execution.
core-web/libs/sdk/ai/src/runtime.spec.ts Adds tests covering direct request behavior, policies, and abort/timeout behavior.
core-web/libs/sdk/ai/src/adapter/request-core.ts Introduces shared request core with policy, auth injection, decoding, SSRF checks, and abort handling.
core-web/libs/sdk/ai/src/adapter/index.ts Adds /adapter subpath barrel export.
core-web/libs/sdk/ai/src/adapter/http-client.ts Adds dotCMS “api” adapter built on the shared request core.
core-web/libs/sdk/ai/src/adapter/http-client.spec.ts Updates tests to use sandbox adapter types.
core-web/libs/sdk/ai/src/adapter/context.ts Updates adapter type import; provides context loading helpers.
core-web/libs/sdk/ai/src/adapter/context-cache.ts Adds session+URL keyed context cache + shared singleton helper.
core-web/libs/sdk/ai/scripts/generate-spec.ts Adds OpenAPI spec fetch/filter/strip generation into src/generated/spec.json.
core-web/libs/sdk/ai/README.md Documents new package topology, runtime usage, threat model, and spec regeneration.
core-web/libs/sdk/ai/project.json Adds sdk-ai Nx project with build/test/lint/publish and spec generation wiring.
core-web/libs/sdk/ai/package.json Declares new publishable package metadata and subpath exports map.
core-web/libs/sdk/ai/jest.config.ts Adds Jest config for sdk-ai, excluding generated spec from coverage.
core-web/libs/sdk/ai/.gitignore Ignores generated spec output directory.
core-web/libs/sdk/ai/.eslintrc.json Enforces sandbox boundary: no dotCMS-specific imports in src/sandbox/**.
core-web/libs/agentic-tools/src/lib/types.ts Removes legacy agentic-tools types (migrated into sdk-ai).
core-web/libs/agentic-tools/src/lib/sandbox/node-worker.ts Removes legacy Node worker sandbox implementation (replaced by shared harness).
core-web/libs/agentic-tools/src/lib/sandbox/index.ts Removes legacy sandbox factory (replaced by sdk-ai sandbox).
core-web/libs/agentic-tools/src/lib/sandbox/bun-worker.ts Removes legacy Bun worker sandbox implementation (replaced by shared harness).
core-web/libs/agentic-tools/src/lib/http-client.ts Removes legacy HTTP client (replaced by shared request core + adapter).
core-web/libs/agentic-tools/src/index.ts Removes legacy library entrypoint.
core-web/libs/agentic-tools/README.md Removes legacy docs.
core-web/libs/agentic-tools/project.json Removes legacy Nx project configuration.
core-web/libs/agentic-tools/package.json Removes legacy internal-only package metadata.
core-web/libs/agentic-tools/jest.config.ts Removes legacy Jest config.
core-web/libs/agentic-tools/.eslintrc.json Removes legacy ESLint config.
core-web/apps/mcp-server/xmcp.config.ts Updates xmcp aliasing to new @dotcms/ai/* subpaths.
core-web/apps/mcp-server/src/tools/search.ts Migrates search tool to createRuntime + /spec subpath.
core-web/apps/mcp-server/src/tools/execute.ts Migrates execute tool to createRuntime front door.
core-web/apps/mcp-server/README.md Updates docs to reference new libs/sdk/ai location and Nx targets.
core-web/apps/mcp-server/project.json Updates dependsOn to sdk-ai:generate-spec.
core-web/apps/ai-evals/src/tools.ts Migrates eval tools to @dotcms/ai/sandbox, /adapter, and /spec power-user paths.
core-web/apps/ai-evals/project.json Adds dependsOn to sdk-ai:generate-spec for builds.

Comment thread core-web/libs/sdk/ai/src/runtime.ts
Comment thread core-web/libs/sdk/ai/src/runtime.ts
Comment thread core-web/libs/sdk/ai/src/sandbox/worker-harness.ts
Comment thread core-web/libs/sdk/ai/src/adapter/request-core.ts
Comment thread core-web/libs/sdk/ai/src/spec/index.ts
Comment thread core-web/libs/sdk/ai/README.md
Comment thread core-web/apps/mcp-server/README.md Outdated
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/apps/mcp-server/src/tools/execute.ts:91createRuntime() called with timeout as a positional argument, but the new createRuntime expects it as a named option — causes runtime error
  • 🔴 Critical: core-web/apps/mcp-server/src/tools/search.ts:53createRuntime() called with timeout:10000 as positional arg, but new createRuntime expects timeout as named option — causes silent infinite execution
  • 🔴 Critical: core-web/apps/mcp-server/src/tools/execute.ts:91createRuntime() called without includeSpec:true — breaks search tool functionality by not injecting spec global
  • 🟠 High: core-web/apps/mcp-server/src/tools/execute.ts:91dotcms.run() may throw uncaught AbortError or TimeoutError from requestCore — crashes MCP server process
  • 🟠 High: core-web/apps/mcp-server/src/tools/search.ts:53dotcms.run() may throw uncaught AbortError or TimeoutError from requestCore — crashes MCP server process
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91onContextError logs to console.error instead of Logger — violates dotCMS convention
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53onContextError logs to console.error instead of Logger — violates dotCMS convention
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91process.env.DOTCMS_URL and process.env.AUTH_TOKEN used directly — should use Config.getStringProperty() for config abstraction
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53process.env.DOTCMS_URL and process.env.AUTH_TOKEN used directly — should use Config.getStringProperty() for config abstraction
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91sessionId is passed to createRuntime() but no idempotency guard on context cache — risk of double-write or stale context on retry
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53sessionId is passed to createRuntime() but no idempotency guard on context cache — risk of double-write or stale context on retry
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91dotcms.run() failure path only logs result.error but does not surface error to caller or log full stack — silent failure risk
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53dotcms.run() failure path only logs result.error but does not surface error to caller or log full stack — silent failure risk
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91timeout from process.env.SANDBOX_TIMEOUT is cast to Number without validation — risk of NaN causing undefined behavior
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53timeout:10000 hardcoded — should be configurable via Config.getStringProperty() for environment consistency

Existing

  • 🟡 Medium: core-web/apps/mcp-server/xmcp.config.ts:16resolve.alias points to source .ts file instead of compiled .js — breaks in production builds

Resolved

  • core-web/apps/mcp-server/src/tools/execute.ts:91createExecutor() and createApiAdapter() no longer used; replaced with createRuntime()
  • core-web/apps/mcp-server/src/tools/search.ts:53createExecutor() and createApiAdapter() no longer used; replaced with createRuntime()
  • core-web/libs/agentic-tools/ — entire package deleted; replaced by @dotcms/ai subpaths

Run: #28119906123 · tokens: in: 21543 · out: 1706 · total: 23249

fmontes and others added 5 commits June 24, 2026 16:22
Two new MCP tools for transferring dotCMS file assets (themes, VTL, CSS, JS,
images, fonts) between the server's local disk and dotCMS — file bytes stream
server-side and never enter the model's context, and the JSON manifest is the
only thing returned.

- download_assets: enumerate a folder via /api/content/_search, fetch bytes via
  the base64 binary envelope, write under an absolute dest preserving structure.
- upload_assets: walk a local dir, PUT each file via /api/v2/assets/save|publish
  to a host-qualified dest, optionally verify live status.
- Shared lib: src/lib/assets-transfer.ts (the transfer logic) and
  src/lib/runtime.ts (runtimeFromEnv + errorMessage — one place that reads
  DOTCMS_URL/AUTH_TOKEN and wires onContextError, used by all four tools).

Tool descriptions are written to steer the agent correctly:
- Framed by state, not user phrasing — "use whenever files need to move,"
  including when the agent itself wrote files as part of a task (e.g. "create a
  theme" → write files → upload), not only when the user says "upload."
- Call out that bytes never pass through the model's context and that auth is
  already configured (no token/.env), to stop the agent from working around the
  tool by hunting for credentials and hand-rolling a direct API call.
- execute's file-upload/binary tips now redirect real file transfers to the
  dedicated tools.

README documents both tools and the updated file tree.

Build + lint pass; mcp-server builds end-to-end with all four tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te footgun)

A recursive download of "//application/themes" silently returned 0 files: a
leading "//" makes normalizeDotCMSPath treat the first segment as the dotCMS
site, so it searched path "/themes" on site "application" — which doesn't exist.
The empty manifest read as success.

In dotCMS "//x" CAN legitimately be a site (hostnames need not contain a dot),
so the parser can't reliably disambiguate a typo from a real host-qualified
path. Instead of guessing, surface it:

- download_assets / upload_assets manifests now include a `warnings[]` field.
- A 0-match folder download adds a warning that names what was parsed
  (site=... path=...), explains the "//" → site rule, and suggests the
  default-site path form (e.g. "//application/themes" → use "/application/themes").
- A 0-file upload warns that nothing under src matched (and names the include
  filter when one was set).

Turns a silent empty-success into a visible, actionable signal the agent can
correct. Lint + build pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oints

The create/update methods for SiteResource, ContainerResource, and
TemplateResource lacked @consumes(APPLICATION_JSON), so swagger-maven-plugin
emitted their request bodies under the '*/*' media type instead of
application/json. AI/MCP clients probing content['application/json'].schema
got undefined and guessed field names, causing avoidable write failures —
even though the *Form schemas (with full field lists) were already present
in the spec via $ref.

Add @consumes(APPLICATION_JSON) to createSite/updateSite, saveContainer/
updateContainer, and createTemplate/updateTemplate. SiteResource's methods
also lacked a typed @RequestBody, so add @RequestBody(SiteForm) there.
ContentTypeResource already had both and is unchanged.

Regenerated openapi.yaml: the six bodies now serve as application/json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `build` target re-runs `generate-spec` via `dependsOn`, and that task only
read its source from a CLI arg — which nx does NOT forward to dependency tasks.
So "generate-spec -- <local-url>" then "nx build mcp-server" silently rebuilt
the spec from the demo instance and clobbered the local one.

generate-spec.ts: resolveSpecSource now checks, in order, the CLI arg →
DOTCMS_SPEC_URL → ${DOTCMS_URL}/api/openapi.json → demo. Env vars ARE inherited
by the dependsOn task, so `DOTCMS_SPEC_URL=… pnpm nx build mcp-server` now
regenerates the spec from a local instance and builds in one command. CI with no
env set still produces the committed demo spec.

README: replace the broken generate-then-build two-step with the one-command
DOTCMS_SPEC_URL form (+ an IMPORTANT callout on why the two-step clobbers), and
switch all yarn commands to pnpm to match the repo's package manager.

Verified: env var flows through dependsOn (build fetched the supplied URL),
default build still uses demo, lint passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, doc fixes

From the PR #36309 review:

- Lazy-load the OpenAPI spec: runtime.ts imported getSpec (and thus the ~550KB
  generated spec.json) statically, so every @dotcms/ai/runtime consumer bundled
  it even with includeSpec:false. Now it's a dynamic import inside the
  includeSpec branch — only the search path pulls it. Keeps the "/spec is opt-in"
  design honest. (Copilot C1/C2)

- Block dynamic import() in the sandbox: `require` was removed but
  `import('node:fs')`/`import('node:net')` could re-open host/network access and
  bypass the adapter boundary. Added a source-level guard in the worker harness
  (matches the "confinement for trusted code generators" posture; not hardened
  against deliberate obfuscation) + a test. Documented in the threat model.
  (Copilot C3)

- Doc accuracy: spec.json is build-generated and git-ignored, NOT committed.
  Corrected the contradictory "committed to git" / "commit the updated spec.json"
  claims in spec/index.ts, libs/sdk/ai/README.md, and apps/mcp-server/README.md
  (4 spots). (Copilot C5/C6/C7)

Not changed: +json vendor media types are still returned as text (Copilot C4) —
that is intentional, documented, and covered by an existing passing test;
flipping it is a behavior change, not a review cleanup.

37 sdk-ai tests pass; lint + typecheck clean; mcp-server builds end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 47 out of 51 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

core-web/apps/mcp-server/README.md:316

  • This README still states spec.json is “already committed” and instructs contributors to commit regenerated output, but libs/sdk/ai/src/generated/ is gitignored and generated via Nx dependsOn. Please update the doc so developers don’t look for a committed file that won’t exist.
# Build the server (spec.json is already committed — no live dotCMS instance needed)
yarn nx build mcp-server

[!NOTE]
Files are located in core-web/apps/mcp-server (tools/config) and core-web/libs/sdk/ai (runtime primitives + spec). We use Nx monorepo.

Refreshing the OpenAPI Spec

The processed spec lives in libs/sdk/ai/src/generated/spec.json and is committed to git. You only need to regenerate it when the dotCMS REST API changes:

# Defaults to https://demo.dotcms.com/api/openapi.json
yarn nx run sdk-ai:generate-spec

# Override with a different instance (e.g. local):
yarn nx run sdk-ai:generate-spec -- http://localhost:8080/api/openapi.json

Then commit the updated spec.json. CI does not need a live dotCMS instance to build.

</details>

Comment thread core-web/libs/sdk/ai/src/runtime.ts
Comment thread core-web/libs/sdk/ai/src/runtime.ts
Comment thread core-web/libs/sdk/ai/src/sandbox/worker-harness.ts
Comment thread core-web/libs/sdk/ai/src/sandbox/factory.ts
Comment thread core-web/libs/sdk/ai/src/spec/index.ts
Comment thread core-web/libs/sdk/ai/README.md
… (PR review)

createWorkerSandbox used require('./bun-worker' | './node-worker') for runtime
selection. The package is published dual ESM+CJS, and require is undefined in an
ESM module — so the factory would throw for any ESM consumer.

Switch to static imports and branch on `typeof Bun` to pick the backend.
Importing both is cheap and safe: each file only declares a class (no top-level
side effects), and node:worker_threads (used by the Node backend) is available
on Bun too, while the Bun backend uses only web globals available on Node.

Found by Copilot on PR #36309. 37 sdk-ai tests pass; lint + typecheck clean;
mcp-server builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Jun 25, 2026
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145dotcms.request() called with path: '/api/content/_search' and body.query: "+baseType:4 +path:${folder}/*" — SQL injection vulnerability via string concatenation; DotConnect.addParam() required for user-controlled input
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:215dotcms.request() called with path: '/api/v2/assets/${encodeURIComponent(identifier)}' — identifier is user-controlled from /api/content/_search response; no validation or parameterization — leads to path traversal or arbitrary asset access
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:344dotcms.request() called with path: '/api/v1/content/${encodeURIComponent(identifier)} — identifier from untrusted source; no validation — enables unauthorized content inspection
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370dotcms.request() called with path: '/api/v1/workflow/actions/default/fire/PUBLISH' and body: { contentlet: { identifier } } — identifier from untrusted source; no permission check — enables unauthorized publish actions
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/runtime.ts:24console.error used instead of Logger — violates dotCMS logging convention
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145/api/content/_search query uses +path:${folder}/* with unvalidated folder from user input — allows path traversal to access arbitrary contentlets
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:215encodeURIComponent(identifier) used for asset path — identifier is pulled from untrusted search results; no access control — allows reading any asset if identifier is guessed or leaked
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370PUBLISH action triggered without checking hasPermission — allows any user with session to publish content they don’t own
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500uploadAssets() calls uploadOneAsset() with destPath: ${dest.siteQualified}/${file.rel}`` — file.rel is relative path from local filesystem; no validation — allows directory traversal to overwrite arbitrary dotCMS assets
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500uploadOneAsset() uses formData with path: destPathdestPath is constructed from user-controlled src and file.rel; no validation — allows overwriting any asset on dotCMS if path traversal succeeds
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370verifyLive() retries publish up to 3 times without idempotency guard — risk of double-publish on retry
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145enumerateAssets() uses offset += SEARCH_LIMIT — no deduplication guard on seen set — if server returns duplicate contentlets across pages, may process same asset twice
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500uploadOneAsset() does not validate destPath against allowed prefixes — allows upload to any path on any site if dest.siteQualified is forged
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/runtime.ts:24process.env.DOTCMS_URL and process.env.AUTH_TOKEN used directly — should use Config.getStringProperty() for config abstraction

Existing

  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91console.error used instead of Logger — violates dotCMS convention
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53console.error used instead of Logger — violates dotCMS convention
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91process.env.DOTCMS_URL and process.env.AUTH_TOKEN used directly — should use Config.getStringProperty() for config abstraction
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53process.env.DOTCMS_URL and process.env.AUTH_TOKEN used directly — should use Config.getStringProperty() for config abstraction
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91 — sessionId is passed to createRuntime() but no idempotency guard on context cache — risk of double-write or stale context on retry
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53 — sessionId is passed to createRuntime() but no idempotency guard on context cache — risk of double-write or stale context on retry
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91 — dotcms.run() failure path only logs result.error but does not surface error to caller or log full stack — silent failure risk
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/search.ts:53 — dotcms.run() failure path only logs result.error but does not surface error to caller or log full stack — silent failure risk
  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:91 — timeout from process.env.SANDBOX_TIMEOUT is cast to Number without validation — risk of NaN causing undefined behavior
  • 🟡 Medium: core-web/apps/mcp-server/xmcp.config.ts:16 — resolve.alias points to source .ts file instead of compiled .js — breaks in production builds

Resolved

  • core-web/apps/mcp-server/src/tools/execute.ts:91createExecutor() and createApiAdapter() replaced with createRuntime() — fixed positional timeout and missing includeSpec issues
  • core-web/apps/mcp-server/src/tools/search.ts:53createExecutor() and createApiAdapter() replaced with createRuntime() — fixed positional timeout and missing includeSpec issues
  • core-web/apps/mcp-server/project.jsondependsOn updated from agentic-tools to sdk-ai — fixed build dependency
  • core-web/apps/mcp-server/xmcp.config.ts — alias paths updated to sdk/ai — fixed module resolution
  • core-web/apps/ai-evals/src/tools.ts — imports updated from @dotcms/agentic-tools to @dotcms/ai/* — fixed module path
  • core-web/apps/mcp-server/README.md — updated references from agentic-tools to sdk/ai — fixed documentation
  • core-web/libs/agentic-tools/ — entire directory deleted — resolved all prior findings related to deprecated package

Run: #28137609694 · tokens: in: 22640 · out: 2172 · total: 24812

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145 — SQL injection via string concatenation in /api/content/_search query without DotConnect.addParam(); query: +baseType:4 +path:${folder}/* directly interpolates user-controlled folder into the query string, enabling arbitrary contentlet enumeration or denial-of-service via malformed paths.
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:215 — Unvalidated asset.path from search results used in relativeAssetPath() and later as dest path component, enabling arbitrary file system traversal during download via crafted path inputs (e.g., ../../etc/passwd).
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:344 — Unvalidated identifier from asset search results used directly in /api/v1/content/${encodeURIComponent(identifier)} without permission check, enabling unauthorized content inspection.
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370PUBLISH action triggered via /api/v1/workflow/actions/default/fire/PUBLISH without any hasPermission check or user context validation, enabling unauthorized content publication.
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500destPath constructed from user-controlled src and file.rel without prefix validation or host/permission checks, enabling arbitrary asset overwrite via //host/path injection or path traversal.
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/runtime.ts:24console.error used instead of Logger violates dotCMS convention; onContextError logs errors to stderr, bypassing structured logging and audit trails.
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370 — Publish retry loop lacks idempotency guard — risk of double-publish when retrying failed publishes without checking live status first.
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500destPath constructed from user input (//demo.dotcms.com/...) without validation of host or path structure, enabling SSRF-like asset manipulation if dotCMS URL is misconfigured.
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145 — No deduplication guard on seen set — may process same asset twice across pages if identifier is reused or duplicated in search results.
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500destPath constructed from user input without prefix validation — allows //host/path to be injected as dest path, enabling overwrite of assets on any host if the server is misconfigured.
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370PUBLISH action called without checking if contentlet is already live — may trigger redundant workflow actions.
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500destPath constructed from user input without sanitization — allows .. or absolute paths if safeJoin() fails to catch edge cases.
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/runtime.ts:24process.env.DOTCMS_URL and AUTH_TOKEN used directly — should use Config.getStringProperty() to support externalized config and fallbacks.

Existing

  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145 — SQL injection via string concatenation in /api/content/_search query without DotConnect.addParam() (still present)
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:215 — Unvalidated identifier from search results used in asset path leading to arbitrary asset access (still present)
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:344 — Unvalidated identifier used in /api/v1/content/{identifier} endpoint enabling unauthorized content inspection (still present)
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370 — Unvalidated identifier triggers PUBLISH action without permission check (still present)
  • 🔴 Critical: core-web/apps/mcp-server/src/lib/runtime.ts:24 — console.error used instead of Logger violates dotCMS convention (still present)
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145 — Unvalidated folder path in _search query enables path traversal to arbitrary contentlets (still present)
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:215 — Unvalidated asset identifier allows reading any asset if identifier is guessed (still present)
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370 — PUBLISH action triggered without hasPermission check (still present)
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500 — User-controlled file.rel used in destPath enables directory traversal to overwrite arbitrary assets (still present)
  • 🟠 High: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500 — Unvalidated destPath in upload allows overwriting any asset on dotCMS (still present)
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:370 — Publish retry loop lacks idempotency guard — risk of double-publish (still present)
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:145 — No deduplication guard on seen set — may process same asset twice across pages (still present)
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/assets-transfer.ts:500 — destPath constructed from user input without prefix validation (still present)
  • 🟡 Medium: core-web/apps/mcp-server/src/lib/runtime.ts:24 — process.env.DOTCMS_URL and AUTH_TOKEN used directly — should use Config.getStringProperty() (still present)

Resolved

  • core-web/apps/mcp-server/src/lib/runtime.ts:24console.error replaced with console.error in errorMessage() — still present, but now only used for fallback; no Logger used → not resolved
  • core-web/apps/mcp-server/src/lib/runtime.ts:24process.env.DOTCMS_URL and AUTH_TOKEN used directly — still present → not resolved

Run: #28170792976 · tokens: in: 22304 · out: 1991 · total: 24295

@mergify

mergify Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@fmontes fmontes added this pull request to the merge queue Jun 25, 2026
Merged via the queue into main with commit 80ea2b9 Jun 25, 2026
63 checks passed
@fmontes fmontes deleted the fmontes/dotcms-ai-sdk branch June 25, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

@dotcms/ai — Productionize the Agentic Runtime

3 participants