feat(ai): @dotcms/ai agentic runtime + mcp-server migration#36309
Conversation
…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>
|
Claude finished @fmontes's task in 1m 38s —— View job Rollback Safety Analysis
Result: ✅ Safe to RollbackAfter analyzing the full diff against every category in the rollback-unsafe reference, no unsafe patterns were found. What this PR changes
Category-by-category verdict
Note on M-3 (REST API)The Java changes add This change is forward-only: rolling back to N-1 simply reverts to Label applied: AI: Safe To Rollback |
🤖 Bedrock Review —
|
There was a problem hiding this comment.
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-toolswith publishable@dotcms/aiand 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-serverandai-evalsconsumers onto the new@dotcms/ai/*subpaths and wiressdk-ai:generate-specas 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. |
🤖 Bedrock Review —
|
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>
There was a problem hiding this comment.
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.jsonis “already committed” and instructs contributors to commit regenerated output, butlibs/sdk/ai/src/generated/is gitignored and generated via NxdependsOn. 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 incore-web/apps/mcp-server(tools/config) andcore-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.jsonThen commit the updated spec.json. CI does not need a live dotCMS instance to build.
</details>
… (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>
🤖 Bedrock Review —
|
🤖 Bedrock Review —
|
|
Tick the box to add this pull request to the merge queue (same as
|
What & why
Productionizes the internal agentic runtime as
@dotcms/ai(libs/agentic-tools→libs/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/airuntime@dotcms/ai/runtime(createRuntime— the front door),/sandbox(generic engine, lint-enforced no-dotCMS),/adapter(dotCMS-wired),/spec(opt-in OpenAPI spec). Bare@dotcms/aiis a pure namespace.request()(direct) andrun(code)(sandboxed) both route through a singlerequestCore— one auth path, one allow-list, one error model, so they can't drift.defineAdapterwith Zod input/output for typed, named operations; typed error hierarchy that round-trips the worker boundary.fetch/require/dynamicimport()blocked;process.envemptied; workerresourceLimits+ wall-clock timeout;AbortSignalaborts in-flight host work on timeout.private, dual ESM/CJS,exportsmap; spec is build-generated (git-ignored) viasdk-ai:generate-spec, wired through consumers'dependsOn.MCP server
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/pathparsing footgun).execute/searchmigrated ontocreateRuntime;execute's file-upload guidance redirects to the dedicated tools.DOTCMS_SPEC_URL=… pnpm nx build mcp-serverregenerates the spec from a local instance in one command (env vars flow throughdependsOn; CLI args don't). README switched topnpm.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
@dotcms/agentic-toolswasprivate/internal, so the rename + consumer migration land together.import()block, ESM-safe worker factory, doc accuracy. (+json-as-text kept — intentional and tested.)Testing
37
sdk-aiunit tests (sandbox confinement/routing/abort,defineAdapter, runtime, http-client); lint + typecheck clean;mcp-serverbuilds end-to-end and serves all four tools;ai-evalsbuilds.🤖 Generated with Claude Code
This PR fixes: #36304