@dotcms/ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @dotcms/ai
2
+
3
+ Every other CMS hands an AI a *fixed menu of tools* — it can only do what the vendor pre-built. `@dotcms/ai` does the opposite: the model writes code, and the runtime runs it in a sandbox against the whole dotCMS API, with auth and policy owned in one place. The ceiling isn't a tool list; it's the API itself.
4
+
5
+ You bring whatever drives it — a model, an agent framework, an automation tool like n8n. This is the execution layer beneath them: no LLM inside, it only runs the code, safely.
6
+
7
+ It's also the layer dotCMS's own MCP server and first-party agents run on. We ship on it, not just publish it.
8
+
9
+ ### Governed by construction
10
+
11
+ Safety isn't a setting you turn on; it's the shape of the runtime:
12
+
13
+ - **Your token never enters the sandbox.** Auth is injected on the host side; the executing code cannot read it.
14
+ - **Adapters are the only way out.** Sandbox code reaches the network/host *only* through an adapter you grant — direct `fetch`/`require`/`process.env` are removed.
15
+ - **You decide the surface.** An allow-list (or typed `defineAdapter` operations) bounds what any code — model-written or not — can reach. Expose `scan` and `read`; never expose `delete`.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @dotcms/ai
21
+ ```
22
+
23
+ ## The front door — one runtime, two verbs
24
+
25
+ ```ts
26
+ import { createRuntime } from '@dotcms/ai/runtime';
27
+
28
+ const dotcms = createRuntime({
29
+ url, // dotCMS instance URL
30
+ token, // dotCMS auth token — NEVER enters the sandbox
31
+ allow, // optional allow-list/policy (string[] of path prefixes, or a predicate)
32
+ sessionId, // context-cache + isolation key
33
+ includeSpec, // inject the `spec` global for the search use case
34
+ timeout // sandbox wall-clock timeout (ms)
35
+ });
36
+
37
+ await dotcms.request(opts); // DIRECT — you write the call. No worker.
38
+ await dotcms.run(code); // SANDBOXED — a model wrote `code`.
39
+ ```
40
+
41
+ **The one rule that keeps the surface small:** `request` is the default. `run` is only for code you did **not** write (a model did). If you write the call yourself, you never need `run`. `run(code)` is implemented *as* "spin a worker whose `api.request` forwards to `dotcms.request`" — the two verbs share one adapter, one auth path, one allow-list, one error model, and cannot drift.
42
+
43
+ ## Package topology — one package, subpaths as seams
44
+
45
+ | Subpath | Audience | Contains | Generic? |
46
+ |---|---|---|---|
47
+ | `@dotcms/ai/runtime` | Most callers — the front door | `createRuntime`, `defineAdapter`, errors | dotCMS-wired |
48
+ | `@dotcms/ai/sandbox` | Power users / custom adapters | `createSandbox`, `defineAdapter`, `Executor`, types, errors | **fully generic, lint-enforced** |
49
+ | `@dotcms/ai/adapter` | Power users | `dotcmsAdapter`, `requestCore`, context loading + cache | dotCMS-specific |
50
+ | `@dotcms/ai/spec` | The search use case | the OpenAPI spec (opt-in; keeps the ~550KB off the default path) | dotCMS-specific |
51
+
52
+ `@dotcms/ai` is a pure namespace — there is no bare import; everything is reached through a subpath. It is an **umbrella** for growth: future AI surfaces (RAG, embeddings, custom agents, harness) land as new subpaths under the same package.
53
+
54
+ ## Custom, typed operations — `defineAdapter`
55
+
56
+ Instead of permitting paths on a generic `request`, expose **named operations** an LLM can call by name, with Zod-validated input and a declared output contract. This is the governed path in practice — the model sees `scan`, not `/api/**`:
57
+
58
+ ```ts
59
+ import { defineAdapter, createSandbox } from '@dotcms/ai/sandbox';
60
+ import { z } from 'zod';
61
+
62
+ const a11y = defineAdapter({
63
+ name: 'a11y',
64
+ methods: {
65
+ scan: {
66
+ description: 'Scan a page URL; returns axe findings',
67
+ input: z.object({ url: z.string().url() }),
68
+ output: z.object({ findings: z.object({ violations: z.array(z.any()) }).loose() }),
69
+ handler: ({ url }, { request }) =>
70
+ request({ method: 'POST', path: '/api/v1/page-scanner/a11y/check', body: { url } })
71
+ }
72
+ }
73
+ });
74
+
75
+ const sandbox = createSandbox({
76
+ adapters: [a11y],
77
+ timeout: 120_000,
78
+ request: (opts) => dotcms.request(opts) // host capability; the runtime provides one
79
+ });
80
+ await sandbox.run(`return (await a11y.scan({ url: 'https://demo.dotcms.com/' })).findings.violations;`);
81
+ ```
82
+
83
+ - **`input` is mandatory** — it is the *trust* boundary (args come from model code; validate before the handler runs).
84
+ - **`output` is required for any model-facing adapter** — it is the *tool-contract* boundary (the result schema the LLM plans against; becomes the auto-generated tool definition). Use **loose/passthrough** output schemas so a new REST field doesn't break the contract. Adapters *without* `output` are typed as not model-exposable and are withheld from the auto-generated tool descriptions (`describeAdapterForLLM`).
85
+
86
+ ## Error model
87
+
88
+ A single typed hierarchy, surfaced identically from `request()` and `run()` (one `requestCore`): `ValidationError`, `PolicyError`, `HttpError` (carries status + body), `TimeoutError`, `AbortError`, `SandboxError`, `RuntimeError` — all subclasses of `DotCMSError`, each with a stable `code` and a serializable `toJSON()`. The model-facing string an MCP tool builds is *formatting on top of* this model.
89
+
90
+ ```ts
91
+ import { isDotCMSError, HttpError } from '@dotcms/ai/runtime';
92
+ try { await dotcms.request({ path: '/api/v1/site' }); }
93
+ catch (e) { if (e instanceof HttpError) console.error(e.status, e.body); }
94
+ ```
95
+
96
+ ## Threat model — capability confinement, NOT adversarial isolation
97
+
98
+ The governance above is **capability confinement for trusted code generators** — it stops your own model from doing something it shouldn't, not an attacker from breaking out.
99
+
100
+ - **Stops accidental egress:** `fetch`/`XMLHttpRequest`/`WebSocket`/`EventSource`/`sendBeacon` throw; `require` removed; dynamic `import()` is blocked at the source level (so `import('node:fs')`/`import('node:net')` can't re-open host access); `process.env` emptied; worker spawned with `env:{}`.
101
+ - **Stops runaway cost:** wall-clock timeout, `resourceLimits` memory/stack caps, and an `AbortSignal` threaded to adapter calls so a timeout aborts in-flight host work.
102
+ - **Does NOT stop hostile code.** User code runs via `new AsyncFunction(code)` in the same V8 isolate as the worker harness — hostile code can reach shared globals, and the `import()` block is a source-level guard (not hardened against deliberate obfuscation). The intended threat is "our own model hallucinates a `DELETE` or an infinite loop," not "an attacker submits malicious JS."
103
+
104
+ **If you must run genuinely untrusted code, bring your own process/microVM isolation.**
105
+
106
+ ## Support matrix
107
+
108
+ - **Node** ≥ 20, **Bun** (native Web Workers). Both worker backends behave identically.
109
+ - **OpenAPI spec ↔ server version:** `@dotcms/ai/spec` is generated from a *specific* dotCMS instance (see "Regenerating the spec"). It is a filtered snapshot, not a live contract — regenerate it against your target server if its REST surface differs from the one you built against.
110
+ - **Semver:** subpaths are part of the public API; a breaking change to any subpath is a major.
111
+
112
+ ## Regenerating the spec
113
+
114
+ `src/generated/spec.json` is **build-generated and git-ignored** — it is NOT committed. The
115
+ `build`/`test`/`serve` targets run `sdk-ai:generate-spec` automatically (via `dependsOn`), so
116
+ you rarely run it by hand; do so only to refresh the local copy or inspect the output.
117
+
118
+ ```bash
119
+ # Defaults to https://demo.dotcms.com/api/openapi.json
120
+ pnpm nx run sdk-ai:generate-spec
121
+
122
+ # Override with a different instance (URL or local file path):
123
+ pnpm nx run sdk-ai:generate-spec -- http://localhost:8080/api/openapi.json
124
+ ```
125
+
126
+ The script filters the spec to the endpoints in `ALLOWED_PREFIXES` (see `scripts/generate-spec.ts`),
127
+ dereferences `$ref`s, and strips response schemas to keep the file small. Because the spec is
128
+ regenerated at build time, there is nothing to commit.
129
+
130
+ ## Commands
131
+
132
+ ```bash
133
+ pnpm nx run sdk-ai:build # Build (ESM + CJS, dual)
134
+ pnpm nx run sdk-ai:test # Run tests
135
+ pnpm nx run sdk-ai:lint # Lint src + scripts
136
+ pnpm nx run sdk-ai:generate-spec -- <url-or-path> # Refresh spec.json
137
+ ```
@@ -0,0 +1 @@
1
+ exports._default = require('./index.cjs.js').default;
package/index.cjs.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ var index = require('./spec.cjs.js');
4
+
5
+
6
+
7
+ exports.getSpec = index.getSpec;
package/index.cjs.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export * from './index.cjs.js';
2
+ export { _default as default } from './index.cjs.default.js';
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/spec/index";
package/index.esm.js ADDED
@@ -0,0 +1 @@
1
+ export { getSpec } from './spec.esm.js';
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@dotcms/ai",
3
+ "version": "0.1.0",
4
+ "description": "The dotCMS agentic runtime — run model-written or human-written code safely against a dotCMS instance, with auth and policy owned in one place.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dotCMS/core.git#main",
8
+ "directory": "core-web/libs/sdk/ai"
9
+ },
10
+ "dependencies": {
11
+ "tslib": "^2.3.0",
12
+ "zod": "^4.1.9"
13
+ },
14
+ "engines": {
15
+ "node": ">=20.0.0"
16
+ },
17
+ "exports": {
18
+ "./package.json": "./package.json",
19
+ "./runtime": "./src/runtime.ts",
20
+ "./sandbox": "./src/sandbox/index.ts",
21
+ "./adapter": "./src/adapter/index.ts",
22
+ "./spec": "./src/spec/index.ts",
23
+ ".": {
24
+ "module": "./runtime.esm.js",
25
+ "types": "./runtime.d.ts",
26
+ "import": "./runtime.cjs.mjs",
27
+ "default": "./runtime.cjs.js"
28
+ },
29
+ "./index": {
30
+ "module": "./index.esm.js",
31
+ "types": "./index.d.ts",
32
+ "import": "./index.cjs.mjs",
33
+ "default": "./index.cjs.js"
34
+ }
35
+ },
36
+ "typesVersions": {
37
+ "*": {
38
+ "runtime": [
39
+ "./src/runtime.d.ts"
40
+ ],
41
+ "sandbox": [
42
+ "./src/sandbox/index.d.ts"
43
+ ],
44
+ "adapter": [
45
+ "./src/adapter/index.d.ts"
46
+ ],
47
+ "spec": [
48
+ "./src/spec/index.d.ts"
49
+ ]
50
+ }
51
+ },
52
+ "keywords": [
53
+ "dotCMS",
54
+ "CMS",
55
+ "AI",
56
+ "agent",
57
+ "agentic",
58
+ "runtime",
59
+ "sandbox",
60
+ "MCP",
61
+ "model-context-protocol"
62
+ ],
63
+ "author": "dotcms <dev@dotcms.com>",
64
+ "license": "MIT",
65
+ "bugs": {
66
+ "url": "https://github.com/dotCMS/core/issues"
67
+ },
68
+ "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/ai/README.md",
69
+ "module": "./runtime.esm.js",
70
+ "main": "./runtime.cjs.js",
71
+ "types": "./runtime.d.ts"
72
+ }
@@ -0,0 +1 @@
1
+ exports._default = require('./runtime.cjs.js').default;
package/runtime.cjs.js ADDED
Binary file
@@ -0,0 +1,2 @@
1
+ export * from './runtime.cjs.js';
2
+ export { _default as default } from './runtime.cjs.default.js';
package/runtime.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/runtime";
package/runtime.esm.js ADDED
Binary file