@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 +137 -0
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +7 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -0
- package/index.esm.js +1 -0
- package/package.json +72 -0
- package/runtime.cjs.default.js +1 -0
- package/runtime.cjs.js +0 -0
- package/runtime.cjs.mjs +2 -0
- package/runtime.d.ts +1 -0
- package/runtime.esm.js +0 -0
- package/spec.cjs.js +16090 -0
- package/spec.esm.js +16088 -0
- package/src/adapter/context-cache.d.ts +35 -0
- package/src/adapter/context.d.ts +42 -0
- package/src/adapter/http-client.d.ts +37 -0
- package/src/adapter/index.d.ts +12 -0
- package/src/adapter/request-core.d.ts +77 -0
- package/src/runtime.d.ts +70 -0
- package/src/sandbox/bun-worker.d.ts +10 -0
- package/src/sandbox/define-adapter.d.ts +88 -0
- package/src/sandbox/errors.d.ts +91 -0
- package/src/sandbox/executor.d.ts +27 -0
- package/src/sandbox/factory.d.ts +13 -0
- package/src/sandbox/index.d.ts +44 -0
- package/src/sandbox/interface.d.ts +8 -0
- package/src/sandbox/node-worker.d.ts +10 -0
- package/src/sandbox/types.d.ts +111 -0
- package/src/sandbox/worker-harness.d.ts +15 -0
- package/src/spec/index.d.ts +7 -0
- package/src/spec/spec.d.ts +1 -0
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
package/index.cjs.mjs
ADDED
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
|
package/runtime.cjs.mjs
ADDED
package/runtime.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/runtime";
|
package/runtime.esm.js
ADDED
|
Binary file
|