autodudes / cheddi
ChEddi: AI assistant for the TYPO3 backend. A chat drawer on every backend page where editors work on pages, content, translations, metadata and other data in plain language.
Package info
Type:typo3-cms-extension
pkg:composer/autodudes/cheddi
Requires
- php: ^8.1
- autodudes/ai-suite: ^12.23.0 || ^13.17.0 || ^14.5.0
- autodudes/ai-suite-mcp: ^0.8
- phpoffice/phpspreadsheet: ^3.10
- phpoffice/phpword: ^1.4
- smalot/pdfparser: ^2.12
- symfony/clock: ^6.4 || ^7.0 || ^8.0
- typo3/cms-scheduler: ^12.4.11 || ^13.4.1 || ^14.3.0
- typo3/cms-workspaces: ^12.4.11 || ^13.4.1 || ^14.3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
π§ͺ Beta (
0.2.2). The feature set and the HTTP contract (routes, theTurnResultresponse envelope, the tool-severity classification) are stabilising, but smaller changes are still possible between minor versions while the extension matures. Pin a version and review the changelog before upgrading.
An AI assistant that lives inside the TYPO3 backend. cheddi puts a resizable chat
drawer on every backend page, and editors say in plain language what they need
("translate this page into English", "write a meta description for the news list", "which
pages have no image?"). ChEddi then works on the real records: page tree, content elements,
translations, metadata, images, record CRUD, always as the logged-in backend user and with
exactly that user's permissions.
Nothing changes behind the editor's back. Read-only steps run on their own, every write is surfaced inline and needs a click, destructive ones a second click. In the default workspace write mode the result lands in a draft the editor can publish or discard from the drawer.
AI Suite is the technical foundation this builds on: it ships
the extension infrastructure, the backend-group permission model, the TYPO3
version-compatibility layer and the shared services, while
ai_suite_mcp contributes the MCP ToolRegistry whose tools ChEddi
drives from inside the backend instead of from an external MCP client. Both are hard
dependencies and Composer pulls them in for you.
What you can do with it
- π¬ Chat from anywhere in the backend: a floating bubble opens a drawer on top of the current module. The drawer is resizable and its size/open-state persist per browser.
- π§° Work on real records, not suggestions: the assistant calls the same MCP tools an external client would (page tree, content generation, translation, metadata, image generation, record CRUD), gated to exactly what the current BE user is allowed to do.
- β Confirm before anything changes: read-only tools auto-run; write tools require a click; destructive tools require a second confirming click. Decisions are recorded inline in the conversation as an audit trail.
- π§ Location-aware: when the editor has a record open, the drawer names its table and uid with the turn, so the assistant knows what is being looked at. The table is checked against the TCA before anything is read with it.
- π Attach documents: PDF, Word, spreadsheets and plain text are uploaded into FAL and
their text is pulled in on demand via the
readAttachmentTexttool. The upload answer says per file whether it can be read at all, so the editor learns that before sending. Images are not an accepted attachment type β see Limitations. - π§ Research the web (experimental): two separate capabilities behind the
enable_web_researchbackend-group flag.searchWebruns through the AI Suite Server on a US-hosted search provider and costs credits;readWebPagefetches one URL the editor named directly from this installation, with no AI provider in the path and no credits. Both feed their sources into the drawer so the editor can check the answer against them. See Limitations before enabling it. - π§Ύ Review what changed: when writes land in a draft workspace (the default), the actions menu lists exactly the records this conversation touched and offers Publish or Discard per record or for all of them.
- ποΈ Conversation history: sessions are stored per BE user, auto-titled from the first message, listed in a panel, and can be reopened, switched or deleted.
- π° Credit + token feedback: remaining credits show in the header and turn amber below a threshold; the input locks when credits are exhausted.
- π Bring your own keys: on BYOK tariffs the turn runs on the provider keys stored under Bring your own keys in the AI Suite extension configuration, and models without a stored key never appear in the picker. On credit tariffs the AI Suite Server's own keys are used and every turn is billed against your credits. Either way the request goes through the AI Suite Server; the chat never calls a provider directly.
- π Markdown rendered safely: assistant Markdown is rendered with
markedand sanitised withDOMPurify(both vendored locally) before it ever touchesinnerHTML. - π§Ή Self-cleaning: a scheduler command soft-deletes idle sessions and hard-deletes them after a grace period.
Limitations & accepted trade-offs
These are deliberate design decisions, not bugs, and worth knowing before you deploy:
- Every turn goes through the AI Suite Server. The chat never calls a model provider directly, so the server has to be reachable even on BYOK tariffs, where it forwards the request using your own keys.
- Images are not supported as attachments. Messages travel to the server as plain strings, so no image content could reach the model anyway; image extensions are therefore not in the upload allowlist and are rejected. Documents (PDF, Word, spreadsheets, plain text) are read, as text, on demand.
- Write reversibility comes from the workspace draft, not from the chat. With the default
workspacewrite mode a confirmed change lands in a draft you can publish or discard from the drawer; inlivemode a confirmed change is immediate. Note that the underlyinguploadMediaandgenerateImagetools write to live in every mode (see theai_suite_mcpREADME). - Web research is experimental. π§ It is off by default (
enable_web_research) and its behaviour and configuration may change between minor versions; treat it as a preview, not as a stable interface. Three consequences worth knowing before you switch it on:searchWebsends the editor's query out of the installation, to a US-hosted search provider by way of the AI Suite Server, and it costs credits.chatForceGdpablocks it outright for that reason.readWebPageis unaffected by that block: it is an HTTP GET from your server to a page the editor already named, which is the same request they could make with a browser.- Which provider is used is a server-side decision, not a setting in this extension. On a bring-your-own-key plan the search authenticates with your own key for whichever provider the server researches with; without that key the tool is withheld from the model entirely, so a turn cannot fail halfway through a search it could never run.
- Page text is extracted heuristically. Scripts, styles and boilerplate are stripped and the result is truncated, so the model sees a cleaned-up excerpt rather than the page.
- No streaming. A turn is a blocking request; the thinking indicator stands in for token streaming.
Requirements
| Dependency | Constraint | Notes |
|---|---|---|
| TYPO3 CMS | 12.4.11 β 14.3.x |
Backend extension; targets v12/v13/v14 in parallel branches |
| PHP | ^8.1 |
|
autodudes/ai-suite |
12.23.0 β 14.x |
Provides SendRequestService, BackendUserService, ModelService, SettingsFactory, UuidService, AbstractRepository |
autodudes/ai-suite-mcp |
0.8.0 β 1.0.0 |
Provides the ToolRegistry, McpPermissionService, McpUserContext |
typo3/cms-workspaces |
12.4.11 β 14.3.x |
Required; the default workspace write mode routes confirmed changes through a draft |
typo3/cms-scheduler |
12.4.11 β 14.3.x |
Required. The session auto-delete command also runs straight from the CLI, but the dependency is hard |
smalot/pdfparser, phpoffice/phpword, phpoffice/phpspreadsheet |
^2.12, ^1.4, ^3.10 |
Text extraction for attached PDF / Word / spreadsheet documents |
The chat extension does not talk to AI providers directly. It posts each turn to the AI Suite Server
/api/chatTurnendpoint (the same server AI Suite already uses), which holds the credentials and runs the LLM. The server URL and API key come from the AI Suite ext-conf (aiSuiteServer/aiSuiteApiKey), merged with per-BE-group overrides.
Installation
composer require autodudes/cheddi
Then in the TYPO3 backend:
- Activate the extension (it ships its DB tables via
ext_tables.sql). - Grant the Enable Chat Interface permission to the relevant BE groups
(Access β custom permission option
tx_aisuite_features:enable_cheddi_interface). Without it the drawer is not injected and every chat AJAX route returns403. - Make sure each BE group also holds the per-model permissions
(
tx_aisuite_models:<Model>) for at least one chat-capable model; otherwise the model dropdown is empty and the input area shows a hint.
Permissions & security model
Access is gated in two layers, both reusing the existing AI Suite permission system:
- Feature flag:
tx_aisuite_features:enable_cheddi_interface. Gates both the drawer injection (InjectChatDrawerListener) and every AJAX route (ChatController::guardPermission). A second, independent flagtx_aisuite_features:enable_web_researchgates the experimental web research tools (WebResearchPolicy); it is off unless granted, andchatForceGdpaadditionally blockssearchWebregardless of the flag. - Per-model + per-tool: the model dropdown only lists models the BE group holds
tx_aisuite_models:<Model>for. TheToolBridgefilters the MCP tool catalogue throughMcpPermissionServiceso the LLM only ever sees tools the user may run, and re-validates on execution. Tool calls run under anMcpUserContextseeded from the BE user's own permission-derived scopes (admins bypass via TYPO3'sBackendUserAuthentication::check()).
Tool risk classification
ToolPolicyResolver assigns each tool a Severity, which decides whether confirmation is
required before it runs:
| Severity | Behaviour | Resolved from |
|---|---|---|
ReadOnly |
Runs automatically, no confirmation | readOnlyHint |
Write |
Inline confirm (one click) | Default catch-all |
Destructive |
Inline confirm + a second "really?" click | destructiveHint |
Resolution order: destructiveHint wins, then readOnlyHint, otherwise Write. Only the
tool's own MCP behavioural hints decide β the same hints every MCP client sees. There is no
scope or name heuristic: a tool that annotates nothing is treated as a write and asks for
confirmation, so an unannotated tool can never mutate records silently.
One exception, in ToolBridge::isModelDiscoveryCall(): a generate* / translate* / image /
workflow tool called without a model argument is only listing the models it could use, so it
counts as ReadOnly and does not spend credits.
How a turn works
Browser (chat-drawer.js) ChatController ChatService / ToolBridge / ChatRequestService
ββββββββββββββββββββββββ βββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββ
user types βββΊ POST /cheddi/turn ββΊ startTurnAction ββββββββΊ startTurn()
ββ resolve/create ChatSession (per BE user)
ββ persist user message, auto-title session
ββ collect system context (page id, module)
ββ ChatRequestService ββΊ server /api/chatTurn
ββ inspect the assistant's tool calls:
β’ ReadOnly β execute now, status=continuing
β’ Write/Destr β status=needsConfirm
βββββββββββββββββ TurnResult (JSON) ββββββββββββββββββββββββββββ
status == continuing ββΊ POST /chat/turn/continue ββΊ continueTurnAction ββΊ continueTurn() (loop, capped)
status == needsConfirm ββΊ render inline confirm UI
user approves/declines ββΊ POST /chat/confirm ββΊ applyConfirmationsAction ββΊ applyConfirmations()
ββ run approved tools, then run next turn
status == final ββΊ done
Other terminal/secondary statuses a TurnResult can carry: error, creditsExhausted,
and aborted (e.g. the tool-call cap was hit).
Tool-call cap
To stop runaway tool loops, ChatService counts tool calls in the current user-turn
sequence (reset by each new user message):
- soft warning at
TOOL_CAP_SOFT_WARNING = 20: attaches a notice the drawer surfaces; - hard abort at
TOOL_CAP_HARD_LIMIT = 40: returnsaborted/toolCapReached.
The client carries its own higher ceiling (MAX_AUTO_CONTINUES = 50 in chat-drawer.js)
purely as a backstop in case the server ever forgets to terminate a loop.
History summarisation
When the server returns a historySummary block, ChatMessageRepository::replaceWithSummary()
hard-deletes the listed old messages and inserts a single role=summary row inside a
transaction, so the stored history mirrors the LLM's compacted view. The drawer re-renders
a collapsible summary separator in the same position.
AJAX routes
All routes are backend AJAX routes (Configuration/Backend/AjaxRoutes.php), all guarded by
the feature flag:
| Route identifier | Path | Action |
|---|---|---|
cheddi_status |
/cheddi/status |
Everything the drawer needs on open: models, operating context, credits, templates, attachment limits |
cheddi_turn |
/cheddi/turn |
Start a turn (creates the session on first call) |
cheddi_turn_continue |
/cheddi/turn/continue |
Fire the next turn after auto-run read-only tools |
cheddi_confirm |
/cheddi/confirm |
Apply Write/Destructive confirmations |
cheddi_sessions_list |
/cheddi/sessions |
List the BE user's sessions |
cheddi_session_load |
/cheddi/session/load |
Load full session detail + message history |
cheddi_session_delete |
/cheddi/session/delete |
Soft-delete a session |
cheddi_attachment_upload |
/cheddi/attachment/upload |
Store an attachment in FAL (extension allowlist + size cap + folder write permission) |
cheddi_ws_changes |
/cheddi/workspace/changes |
List the draft records this session changed |
cheddi_ws_publish |
/cheddi/workspace/publish |
Publish selected drafts |
cheddi_ws_discard |
/cheddi/workspace/discard |
Discard selected drafts |
cheddi_help |
/cheddi/help |
Help text for the drawer's help modal |
cheddi_turn_progress |
/cheddi/turn/progress |
Poll which tool the running turn is on (no rate limit, by design) |
cheddi_summarize |
/cheddi/summarize |
Roll the conversation up into one summary, on the editor's request |
Validation errors and permission denials use 4xx with { "error": { "message": ... } }
so the frontend can branch on HTTP status. Domain-level errors from the orchestrator come
back as 422 with a TurnResult envelope (never 5xx, they are user-correctable).
Architecture
Classes/
βββ Controller/
β βββ ChatController.php AJAX entry points; permission guard; JSON envelopes
βββ Service/Chat/
β βββ ChatService.php Turn orchestrator (sessions, messages, tool routing, caps)
β βββ ChatRequestService.php HTTP to the AI Suite Server /api/chatTurn (1 retry on 5xx/connect)
β βββ ToolBridge.php Adapter to the MCP ToolRegistry (filter / project / execute)
β βββ ToolPolicyResolver.php Maps a tool to a Severity
β βββ ContextCollector.php Builds the per-turn system-context preamble
β βββ ChatSessionAutoDeleter.php Two-stage retention logic (IO-free, testable)
βββ Command/
β βββ AutoDeleteChatSessionsCommand.php Scheduler/CLI entry point for retention
βββ EventListener/
β βββ InjectChatDrawerListener.php Injects the drawer mount point + CSS/JS into the BE
βββ Domain/
β βββ Model/{ChatSession,ChatMessage}.php Plain row objects (fromRow factories)
β βββ Model/Dto/{TurnResult,ChatTurnAnswer}.php Typed turn in/out envelopes
β βββ Repository/{ChatSession,ChatMessage}Repository.php extend AI Suite AbstractRepository
β βββ Enum/Severity.php ReadOnly | Write | Destructive (derived from MCP hints)
Resources/Public/
βββ JavaScript/chat-drawer.js The drawer: turn loop, messages, confirmations, model state
βββ JavaScript/{sessions-panel,templates-dropdown,navigation-targets,drawer-resize}.js Self-contained pieces of the surface
βββ JavaScript/vendor/{marked.esm.js,dompurify.es.mjs} Locally vendored, CVE-audited
βββ Css/chat-drawer.css Drawer styling
Frontend
chat-drawer.js is a dependency-free ES module registered via
Configuration/JavaScriptModules.php. It mounts on the [data-cheddi-drawer] div
the event listener appends to the backend <body>. It owns the bubble/drawer, model
selector, session panel, the auto-continue loop, inline confirm UI, credit display, and
safe Markdown rendering. State persisted to localStorage: drawer size/open-state, the
active session UUID, and the chosen model.
marked and DOMPurify are vendored locally (not pulled from a CDN) to keep the chat
self-contained; the import map in JavaScriptModules.php records the audited versions and
the CVEs they patch; re-run the audit and update that comment when bumping them.
Database tables
| Table | Purpose |
|---|---|
tx_cheddi_session |
One row per conversation: session_uuid, be_user, title, model, last_activity, soft-delete deleted flag |
tx_cheddi_message |
One row per message: session, sort, role (user/assistant/tool/summary), content, tool_calls, tool_call_id, tool_status, attachments (file references, never bytes), provider_items |
tx_cheddi_change |
Audit trail of the records a session mutated: session, tablename, record_uid, workspace_record_uid, workspace, page_id, action. Only filled in workspace write mode |
There are no FK constraints between them; the auto-deleter cascades message, change and attachment deletion in application code.
Session retention
Two-stage, handled by ChatSessionAutoDeleter and invoked by the
cheddi:auto-delete-sessions command:
- Soft-delete sessions whose
last_activityis older thanchatSessionLifetimeDays(ext-conf, default 20 days). The row stays (deleted=1) so a future restore window is possible. - Hard-delete sessions soft-deleted for at least
HARD_DELETE_GRACE_DAYS(7 days), together with their messages.
Run it from the CLI:
vendor/bin/typo3 cheddi:auto-delete-sessions
β¦or register it as a recurring task in the TYPO3 scheduler module (requires
typo3/cms-scheduler). A daily cron is recommended.
Configuration
Extension configuration (ext_conf_template.txt):
| Key | Default | Description |
|---|---|---|
chatSessionLifetimeDays |
20 |
Days of inactivity before a session is soft-deleted. |
chatForceGdpa |
inherit |
Restrict the chat to GDPR-compliant models, and block searchWeb (experimental; readWebPage stays available, it contacts no AI provider). |
chatWriteMode |
inherit |
Where writes land: draft workspace or live. |
chatAllowRawHtmlWrite |
inherit |
Whether the chat may store raw markup in code editor fields. |
chatExcludedTables |
(empty) | Tables ChEddi must never read or write. |
chatExcludedTools |
(empty) | MCP tool names ChEddi must never offer, on top of the built-in list. |
chatSearchAdditionalTables |
(empty) | Extra tables searchContent sweeps in the chat. |
chatExcludeAdditionalTablesFromSearch |
(empty) | Auto-detected child tables to keep out of chat searches. |
chatLogVerbose |
inherit |
Whether the INFO trace goes to var/log/cheddi.log. |
chatLogRedactionPatterns |
(empty) | Extra regex patterns redacted in the chat log. |
Every key with a counterpart in ai_suite_mcp (mcpWriteMode, mcpAllowRawHtmlWrite,
mcpExcludedTables, mcpSearchAdditionalTables, mcpExcludeAdditionalTablesFromSearch,
mcpLogVerbose, mcpLogRedactionPatterns) or in ai_suite (forceGdpa) defaults to
inherit: left empty or on inherit, ChEddi behaves exactly like the surface it inherits
from. A configured value may only tighten: the table lists are added to the MCP ones and
never shorten them, chatAllowRawHtmlWrite = Yes cannot re-enable what MCP forbids, and
chatForceGdpa = No cannot take the chat out of an installation-wide GDPR requirement.
ChatSettingsService resolves this and pushes what the shared MCP services need into
SurfaceSettingOverrides (ai_suite_mcp), which the MCP transport itself leaves empty.
The server endpoint and API key are not configured here; they are read from the parent
AI Suite ext-conf (aiSuiteServer, aiSuiteApiKey) via
SettingsFactory::mergeExtConfAndUserGroupSettings(), so per-BE-group overrides apply
identically to the rest of AI Suite. The chat* keys above are read from this extension's own
configuration and have no per-BE-group override.
Without aiSuiteApiKey the chat does not start a turn at all: the drawer shows the same hint the
AI Suite module shows ("Please enter your AI Suite API key β¦") as a system message and keeps the
input locked, and a turn requested anyway comes back with chatErrorCode: apiKeyMissing instead
of a raw HTTP 401 from the server.
Development
Unit tests live under Tests/Unit/, functional tests under Tests/Functional/. Both run in
one call from the parent repository:
ddev tests-cheddi # unit + functional ddev tests-cheddi unit # one suite ddev tests-cheddi functional --filter someTest
The raw invocations behind it:
ddev exec .Build/bin/phpunit -c Extensions/cheddi/Tests/UnitTests.xml ddev exec .Build/bin/phpunit -c Extensions/cheddi/Tests/FunctionalTests.xml
Static analysis uses the extension-local phpstan.neon. Code style follows the repo-wide
PHP-CS-Fixer setup.
License
GPL-2.0-or-later. Β© AutoDudes, https://www.autodudes.de/