---
title: "Running tools"
manual: "TYPO3 LLM Extension"
version: "0.36"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools@0.36"
source: "Administration/Tools.rst"
rendered: "2026-09-23T23:37:39+00:00"
---

# Running tools {#administration-tools}

*Tools* are small, admin-curated PHP functions the model may call
mid-generation. Where a normal completion answers in one shot, a **tool run**
is a bounded *agent loop*: the model may ask to call a tool, nr-llm executes
it, feeds the result back, and re-asks — until the model answers or an
iteration cap is reached. The v1 consumer is the interactive
[Tool Playground](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-playground@0.36).

The [Tool Playground](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-playground@0.36) — the only
surface that runs the agent TOOL loop — is **admin-only** (editors run
one-shot tasks and decide approvals in **Web > AI Tasks**, which
never executes tools directly). The runtime itself
applies a two-tier gate: each tool declares `requiresAdmin()`, and
`ToolLoopService` drops admin-only tools when the acting backend user is
not an administrator. Most built-in tools require admin because a tool runs
with full TYPO3 privileges, has no per-record authorization, and its return
value egresses both to the configured LLM provider **and** to the rendered
backend output; only a few read-only, scope-limited tools are offered to
non-admin users.

> [!NOTE]
> The runtime design and its security and cost rationale are recorded in
> [ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.36). Skill ingest and injection — which can steer
> *which* tools a run may use and *what arguments* the model chooses — are
> [ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.36) / [ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.36) and the
> [Managing skills](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills@0.36) guide.

## The built-in tools {#administration-tools-builtin}

nr-llm ships forty-one read-only tools and sixteen writing tools. Each is a
reference implementation of the security contract: model-chosen arguments are
validated and scoped, volumes are capped, and secret-bearing output is either
redacted or gated behind a separate `_raw` variant. Thirty-eight ship
**enabled**; the three unredacted `_raw` variants (`get_env_raw`,
`get_php_info_raw` and `list_be_users_raw`) and all sixteen writing
tools (`update_page_metadata`, `set_page_social_image`,
`set_file_alternative_text`, `update_fal_asset_meta`,
`move_content_element`, `create_content_element_draft`,
`create_page_draft`, `create_translation_draft`,
`attach_file_to_content_element`, `create_record_draft`,
`update_content_element`, `publish_record`, `delete_record`,
`copy_record`, `move_page`, `replace_file_reference`) ship
**disabled** and must be enabled deliberately.
Many require admin; the read-only structure, content
and file tools (`get_pagetree`, `get_tca`, `get_full_tca`,
`get_table_schema`, `get_flexform_schema`, `fluid_resolve`,
`search_records`, `get_page_content`, `read_records`,
`get_record_history`, `resolve_url`, `validate_tca`,
`list_fal_storages`, `browse_fal_folder`, `search_fal_files`,
`get_fal_references`, `find_missing_files`) are offered to
non-admin backend users — those self-enforce the acting user's TYPO3
permissions (page-show rights, `tables_select`) inside the tool, so a
non-admin only ever sees what the backend already grants them (see
[ADR-042](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-042@0.36)).

The two tools below are the fullest illustrations of the contract:

-   **`fetch_logs`**

    Returns the most recent `sys_log` entries, newest first, with an
    optional PSR `level` filter and a `limit` (default 20, **hard-capped
    at 50**). Personally-identifying fields — the client IP, the backend user
    id and the serialized payload — are **redacted by omission**, because the
    result egresses to the external provider.

-   **`read_fal_asset_meta`**

    Returns read-only metadata (file name, MIME type, size, title, alternative
    text) for a single managed file (`sys_file`) by its `uid`. The uid is
    model-chosen and therefore injection-steerable, so the lookup is
    **storage-scoped** (default: the default storage). A uid in a non-permitted
    storage returns the same neutral "not found or not permitted" string as a
    missing uid — the model cannot enumerate arbitrary files.

The remaining tools follow the same pattern:

-   **`list_fal_storages`**

    The file storages this run may touch (uid, name, driver, status flags).
    The effective set is the configured allow-list, intersected for
    non-admins with their file mounts; the server-side base path is never
    part of the output.

-   **`browse_fal_folder`**

    One FAL folder: subfolders (with file count), then files with size and
    MIME type. Storage-relative identifiers only; anything unresolvable
    collapses into one neutral denial. Capped at 100 entries.

-   **`search_fal_files`**

    Substring search over file name and metadata title/alternative within
    the accessible storages. `%`/`_` in the query match literally;
    missing files are excluded.

-   **`get_fal_references`**

    Where a file is used: `sys_file_reference` rows as \``table:uid
    (field)`\`, hidden references marked. Soft references (RTE links, plain
    URLs) are not tracked — stated in the output so "no references" is
    never read as "safe to delete". Non-admins only see references from
    tables they may read.

-   **`find_missing_files`**

    `sys_file` records whose physical file is gone (`missing = 1`) —
    the "broken image" diagnosis. The total count is always reported next
    to the capped listing.

-   **`get_env` / `get_env_raw`**

    Process environment variables. `get_env` redacts secret-looking values
    (password, token, key, secret, salt, DSN, …); `get_env_raw` returns them
    unredacted (database password, encryption key) and ships disabled.

-   **`get_php_info` / `get_php_info_raw`**

    PHP runtime configuration. `get_php_info` is redacted; `get_php_info_raw`
    returns the full, secret-bearing `phpinfo` detail and ships disabled.

-   **`get_pagetree`**

    The backend page tree (uid, title, doktype) as a depth-indented outline;
    deleted and hidden pages are excluded — structure only, no content.

-   **`get_tca`**

    The TYPO3 TCA schema: with no argument it lists the configured table names;
    with a `table` argument it returns that table's field definitions.

-   **`list_be_groups`**

    The backend user groups (uid, title).

-   **`list_be_users` / `list_be_users_raw`**

    Backend users. `list_be_users` omits credentials (password hashes and MFA
    secrets are never included); `list_be_users_raw` returns the full
    non-credential profile columns and ships disabled.

-   **`search_records`**

    Full-text search across the tables that define TCA `searchFields`.
    Returns compact `table:uid` hits with a short excerpt around the match.
    Credential and nr-llm configuration tables are never searched; non-admins
    are limited to their `tables_select` tables and to hits on pages they
    may show.

-   **`get_page_content`**

    One page's header data plus its content elements in column/sorting order
    (uid, colPos, CType, header, a short bodytext excerpt). Non-admins need
    page-show permission; only admins see hidden elements (marked
    `[hidden]`).

-   **`read_records`**

    Generic equality-filtered read of one TCA table — never raw SQL. Fields
    are validated against the TCA and credential-like columns are silently
    dropped; the same table gates as `search_records` apply.

-   **`get_record_history`**

    One record's change history from `sys_history`, newest first: when,
    which backend user, which action, and per modification the changed
    fields as old → new values. Values of credential-like fields are never
    rendered — only the fact that they changed. Same table gates as
    `read_records`, and non-admins additionally need page-show access on
    the record's page.

-   **`resolve_url`**

    Map a URL (or path) of this instance to the page that serves it: site,
    language, page uid/title/slug and route arguments. Routing only — no
    request is sent; foreign hosts cannot match by construction. Non-admins
    need page-show permission on the resolved page.

-   **`get_typoscript`**

    The resolved frontend TypoScript (setup or constants) effective on a page,
    with a dotted `path` drill-down and capped output. Admin-only —
    constants routinely carry API keys — and credential-like values render as
    `[redacted]` on top of that.

-   **`get_tsconfig`**

    The rootline-merged Page TSconfig effective on a page, with the same
    `path` drill-down, output cap and redaction as `get_typoscript`.
    Admin-only.

-   **`get_last_exception`**

    The newest exception/error from the TYPO3 file logs with its parsed
    stack trace and the surrounding source lines of the project frames
    inlined. `index` steps back through older errors, `search` filters
    by message, class or component. Admin-only.

-   **`read_source`**

    A line-numbered range of one project source file. Paths must resolve
    inside the project root; dotfiles, `var/*` (except `var/log`),
    `config/system`, `settings.php`/`additional.php`, key material
    and credential paths are structurally unreadable. Admin-only.

-   **`search_code`**

    Literal-substring (or opt-in regex) search across the project's source
    files, returning `path:line` hits. Vendor, `var` and dot
    directories are never searched; matched credential lines are
    value-redacted. Admin-only.

-   **`probe_url`**

    One GET against a URL of *this* instance: status, key headers, timing
    and a short body excerpt — and on a 5xx the matching exception from the
    TYPO3 logs is appended automatically. Foreign hosts and non-http(s)
    schemes are denied; redirects are reported, not followed. Admin-only.

-   **`get_full_tca`**

    The TCA index: the names and titles of all accessible tables, each with a
    pointer to `get_table_schema`. A navigation aid so the model can traverse
    the schema without the whole (multi-megabyte) TCA being sent at once. The
    same table gates as `get_table_schema` apply. Optional `filter` and
    `extension` narrow the list.

-   **`get_table_schema`**

    One table's schema in a readable form: control settings plus, per field,
    its type and — for relational fields — the foreign table and relation kind
    (the value over `get_tca`). Sensitive tables are denied for every user;
    credential-like columns show name and type only.

-   **`get_flexform_schema`**

    The data structure of a TCA FlexForm field, rendered as sheets and fields.
    When the field selects one of several structures by a pointer, the
    available keys are listed so a follow-up call can pass `ds_pointer`. Same
    table gates as `get_table_schema`.

-   **`fluid_resolve`**

    Which physical Fluid file backs a template, partial or layout name in an
    extension: the candidate paths in override order with an exists flag and
    the winning path — to debug a wrong or missing template. Paths only.
    (Resolves an extension's own `Resources/Private` paths; TypoScript
    override root paths need a live rendering context and are not reflected.)

-   **`validate_tca`**

    Structural TCA checks: `ctrl.label`/`ctrl.type` naming undefined
    columns, `foreign_table` references to unknown tables, `showitem`
    entries referencing undefined columns or palettes. One table or all
    accessible tables; findings name schema keys, never record data.

-   **`check_typoscript`**

    Scans the TypoScript effective on a page (constants **and** setup) for
    syntax errors — invalid lines, unbalanced braces, `@import` matching no
    file — using the same core scanner as the backend's TypoScript module.
    Reports source and line number only, never the offending line's content
    (a constants line may carry an API key). Admin-only.

-   **`list_extensions`**

    The installed (active) extensions: key, version, composer name and
    title — no package paths. Admin-only.

-   **`get_site_config`**

    Without arguments the configured sites (identifier, base, root page);
    with `identifier` that site's configuration flattened to dotted
    `key: value` lines. Credential-like keys (camelCase included, e.g.
    `apiKey`) render as `[redacted]`. Admin-only.

-   **`list_scheduler_tasks`**

    The scheduler tasks with next execution, disabled flag and a
    last-run-failed marker. The serialized task object is **never
    unserialized**; degrades gracefully when EXT:scheduler is absent.
    Admin-only.

-   **`get_system_status`**

    One compact block: TYPO3/PHP/database versions, application context,
    composer mode, OS family, timezone — no paths, no hostnames. Admin-only.

-   **`list_deprecations`**

    The newest distinct messages from the deprecation log, deduplicated with
    a ×count suffix and project paths relativized — the upgrade work list.
    Admin-only.

-   **`list_middlewares`**

    A PSR-15 middleware stack (frontend or backend) in execution order with
    identifiers and classes. Admin-only.

-   **`site_rag_query`**

    Curated evidence about the website's own **public** content for a
    question: source id, title, URL and match excerpt per source, retrieved
    from the best available search index — EXT:solr, ke_search,
    indexed_search or a database fallback — and labelled with the answering
    backend ([ADR-049](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-049@0.36)). Index-level filtering is always
    public-only (what the anonymous visitor could read).

-   **`site_fetch_source`**

    The full indexed text behind a `site_rag_query` source id, capped at
    8000 characters — for reading a promising source beyond its excerpt.

## The writing tools {#administration-tools-writing}

Sixteen tools change anything at all: `update_page_metadata`,
`set_page_social_image`, `set_file_alternative_text`,
`update_fal_asset_meta`, `move_content_element`,
`create_content_element_draft`, `create_page_draft`,
`create_translation_draft`, `attach_file_to_content_element`,
`create_record_draft`, and the six that act on existing pages and content
elements — `update_content_element`, `publish_record`,
`delete_record`, `copy_record`, `move_page` and
`replace_file_reference`. All sixteen write through the TYPO3 DataHandler,
as the acting backend user, in the live workspace only, on **one** record per
call — plus, for a delete, a copy or a page move, what core carries along
with that record, which the approval card counts ([ADR-135](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-135@0.36),
[ADR-146](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-146@0.36), [ADR-180](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-180@0.36),
[ADR-197](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-197@0.36), [ADR-198](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-198@0.36),
[ADR-199](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-199@0.36)).

What holds for all of them:

-   They ship **disabled**, sit in their own `editing` group, and are not
    offered until both the group and the tool are enabled.
-   **Every call pauses for a human decision** ([ADR-134](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-134@0.36)). The
    approval card names the record and the values, together with the values the
    write would REPLACE ([ADR-136](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-136@0.36)); nothing is written until
    somebody presses Approve, and the approver must themselves be permitted to
    run the tool ([ADR-133](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-133@0.36)).
-   Permissions are enforced twice: the tool checks the acting user's own rights
    first, and the DataHandler enforces its own rules on top. A non-admin
    therefore writes only what the backend already lets them write.
-   A field the user lacks the "exclude field" grant for is dropped by the
    DataHandler **without an error**. Every one of them re-reads the record
    afterwards and reports that as a failure rather than as a successful write.
-   A call the tool would refuse is refused **whole** rather than applied in
    part, and a record the acting user may not reach is refused with the same
    words as a record that does not exist — so a refusal never confirms that a
    uid exists.
-   They act on **live rows only**. A workspace draft of a page, a content
    element or a file reference is not there for them: it is not written,
    copied, counted or shown on an approval card, even when its uid is named
    ([ADR-198](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-198@0.36)).
-   `update_page_metadata` and `update_content_element` bind a field's
    before and after to the whole value: two short values in full, otherwise
    the section that differs, where it starts, and the length and a short hash
    of both values — so a change past the visible part, such as an appended
    link, still shows. A character a reader cannot see, such as a zero-width
    space or a direction mark, is written as its code point.

-   **`update_page_metadata`**

    Sets a fixed set of descriptive fields on one page. Editable: `title`,
    `subtitle`, `nav_title`, `abstract`, `description`, `keywords`
    and — when EXT:seo is installed — `seo_title`, `og_title`,
    `og_description`, `twitter_title`, `twitter_description` and
    `twitter_card`. The last one is a select: its value must be one of the
    items the TCA declares, and the refusal names them. Anything
    else (`slug`, `hidden`, `doktype`, `fe_group`, `perms_*`,
    `no_index`, the image relations …) is refused. Authorised by the acting
    user's page-edit right; the DataHandler then enforces `tables_modify` and
    the field-level grants. The two social images have a writer of their own,
    `set_page_social_image`.

-   **`set_page_social_image`**

    Sets the social preview image of one page — `og_image` (Open Graph) or
    `twitter_image` — to an **existing** managed file, identified by its
    `sys_file` uid. Both columns come with EXT:seo; without the extension the
    call is refused and says so. It creates exactly one `sys_file_reference`
    and never uploads, moves or renames a file ([ADR-195](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-195@0.36)).

    Authorised on both ends: the acting user needs page-edit rights on the
    page, and the file has to lie in a permitted storage inside that user's own
    file mounts. Either failure is refused in the same words as a page or file
    that does not exist. Both columns are exclude fields, so a non-admin editor
    also needs the field-level grants `pages:og_image` /
    `pages:twitter_image` — without them the whole call is refused **before**
    anything is written, because the DataHandler would otherwise create the
    reference and drop the page's side of it in silence. A column an
    installation hides from non-admins (`displayCond` of
    `HIDE_FOR_NON_ADMINS`) is refused for every non-admin the same way, and
    the refusal names the condition.

    Three things worth knowing before enabling it:

    -   **The field must accept the file.** Both columns accept the image
        extensions the installation configures (`common-image-types`); a
        `.txt` is refused rather than written.
    -   **An image that is already there stops the call.** The refusal names the
        file referenced now; `replace` is the only way past it and it
        **deletes** that reference first — recoverably (`deleted = 1`) and in
        `sys_log` — and the approval card says so on its own line.
    -   **Default-language pages only.** A translated page is refused and the
        default-language page named; whether a translation follows its parent or
        carries its own image is a page-properties decision
        (`allowLanguageSynchronization`) the tool leaves to the editor. And
        because TYPO3 saves a page's translations along with the page, an editor
        whose allowed languages leave out a language the page is translated into,
        or who may not edit one of its translations — a translation carries page
        permissions of its own — is refused before anything is written; the
        refusal names the translation.

    The page's own reference count is re-read afterwards, because EXT:seo reads
    it before it looks for the file: a reference the page does not count is one
    nothing renders. When that check fails, the new reference is deleted again
    and the page is written back to what it held; the tool then reads the page
    once more and the message says whether it is back as the call found it or
    what is still there, and names anything TYPO3 reported while putting it
    back — on a translated page that can be a translation it could not
    re-synchronise, which the tool reports rather than repairs.

-   **`set_file_alternative_text`**

    Sets the alternative text (`sys_file_metadata.alternative`) of one managed
    file, identified by its `sys_file` uid — the accessibility gap an editor
    most often leaves behind. It writes that one field and nothing else.

    Authorised by the same storage allow-list and file mounts as the read-only
    FAL tools; core's own file-metadata permission check (a **writable** file
    mount) then applies inside the DataHandler, so a read-only mount is refused
    there.

    Two limits worth knowing before enabling it:

    -   It **never creates** a metadata record. A file that carries none is
        refused, in the same words as a file the user may not reach — so the
        model cannot tell "not yours" from "not indexed".
    -   It writes the **live**, **default-language** record only and takes no
        language argument. A translation, and a draft version of the same record
        in a workspace, are both left alone. Translated alternative texts stay a
        backend job ([ADR-135](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-135@0.36)).

    An empty string is accepted and is the correct value for a decorative
    image.

-   **`update_fal_asset_meta`**

    Sets the **title** and the **description** (`sys_file_metadata.title`,
    `sys_file_metadata.description`) of one managed file, identified by its
    `sys_file` uid, and the **copyright** notice where EXT:filemetadata
    provides the column. Any of the fields may be given; at least one is
    required.

    It is a second tool rather than a wider `set_file_alternative_text`
    because the two are deliberately **field-disjoint**: this one does not
    write the alternative text and that one writes nothing else, so no
    file field has two writers and an approver never has to work out which of
    two cards won.

    It shares the alternative-text tool's whole permission surface — the same
    storage allow-list and file mounts, core's writable-mount check inside the
    DataHandler, the same neutral refusal, no metadata record ever created, and
    the live default-language record only.

    Two behaviours worth knowing before enabling it:

    -   A field the call **leaves out keeps its stored value**; an **empty
        string** clears the field it names. The two are not the same, and a call
        that sets the title never touches a description somebody wrote by hand.
    -   Core marks `title` as an **exclude field** and `description` not, so
        a non-admin editor needs the field-level grant
        `sys_file_metadata:title` for the title. Without it the whole call is
        refused **before** anything is written, rather than the DataHandler
        dropping the title in silence and applying the description — which would
        leave the asset described by half of an approved call.

    The title is bounded in **bytes**, not characters: the column is
    `tinytext` and holds 255 bytes, so accented and non-Latin characters
    count for more than one.

-   **`attach_file_to_content_element`**

    References an **existing** managed file from one content element, appended
    to one of its file fields (`image`, `assets` or `media`). It creates
    exactly one `sys_file_reference`. It never uploads, moves, copies or
    renames a file, and never touches `sys_file_metadata` — the file has to be
    there already.

    Authorised on both ends: the acting user needs content-edit rights on the
    element's page, and the file has to lie in a permitted storage inside that
    user's own file mounts. Either failure is refused in the same words as an
    element that does not exist.

    Three things worth knowing before enabling it:

    -   **The field must accept the file.** Each file field declares which
        extensions it takes, and they differ — `image` accepts fourteen,
        `assets` twenty-seven, `media` anything. A `.docx` on `image` is
        refused rather than written, because the FormEngine would reject the
        relation anyway.
    -   **The field is only inferred when it is unambiguous.** When the element's
        type offers exactly one of the three, that one is used; when it offers
        several, the call must name the one it means. The tool does not pick.
    -   **Calling twice attaches the file twice.** That is the declared effect
        (`NON_IDEMPOTENT_WRITE`), not an accident: two references to one image
        are a valid thing to want, so nothing deduplicates behind the caller's
        back.

    The new reference is always appended last, and the element's own reference
    count is re-read afterwards. A write that left the relation inconsistent is
    reported as a failure and the reference is removed again.

-   **`move_content_element`**

    Moves one content element to a page and a column. The element keeps its uid,
    its content, its language, its history and its references — only its place
    changes, which is what makes it the least committal write in the set.

    Both ends are authorised: the acting user needs content-edit rights on the
    **source** page as well as on the target, because moving an element out of a
    page edits that page's content too. An `after_content_uid` anchor must sit
    on the target page and in the same language; a wrong anchor is refused
    rather than silently corrected.

    The destination column is always stated explicitly on the wire, so the
    element lands where the approval card said it would even when the anchor
    element sits in a column the caller did not expect.

-   **`create_content_element_draft`**

    Creates one content element on a page — the first tool that brings a record
    into being. Three things bound it:

    -   It is **always hidden**. There is no argument to switch that off:
        publishing is a separate act with a separate audience, and the approval
        that let the tool run approved a draft.
    -   The content types are read from the installation's TCA at call time,
        under an exclusion rule ([ADR-196](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-196@0.36)). A type is offered
        unless it is denied by name — `list` (the legacy plugin element),
        `html`, `shortcut`, `div`, every `menu_*` — or it is a plugin,
        known by its Extbase registration or by the `plugins` or `forms`
        item group, or its form holds a
        column whose payload is not prose: a FlexForm, inline children, a group
        or folder reference, a slug, a password. File, category and link
        relations do not exclude a type; the draft leaves them empty, so
        `textmedia` is offered and gets its media through
        `attach_file_to_content_element`. The tool description lists what the
        installation offers.
    -   The field set is the type's own scalar columns: headline, body text,
        column, language and position as arguments, and every further `input`,
        `text`, `select` (static items), `check`, `number`, `datetime`,
        `radio`, `color` or `email` column of the chosen type through
        `fields`, validated against its TCA type as the DataHandler reads it.
        A `check` with several items, a `check` limited by
        `maximumRecordsChecked` and an `input` or `email` with `eval`
        `unique` stay in the form and are refused as keys, because TYPO3 would
        change them in silence; so is a column the type's TCA declares
        `readOnly`, and a read-only `header` or `bodytext` refuses the
        call. Identity, position,
        visibility, publication, audience and translation columns are refused by
        name; a wrong key or value refuses the whole call and names the columns
        the type offers. The chosen type is checked against the acting user's
        explicit allow-list, and the exclude-field grants per column — the
        tool's own columns included — before the write; a `datetime` is handed
        over as the integer both supported cores store. The page's TSconfig
        narrows all of this per page, as it narrows the backend form:
        `TCEFORM.tt_content.CType.keepItems` and `removeItems`; `disabled`
        and `config.readOnly` on a `fields` column, on `bodytext` and on
        `header` — a hidden or read-only header refuses the call, since the
        header is required, and like the backend form the tool does not read
        `config.readOnly` for a `radio` column; and
        `keepItems` and `removeItems` on a select column in `fields`, on
        `colPos` for the `column` argument and on `sys_language_uid` for
        the `language` argument — each also under `types.<CType>` — are
        honoured, and the refusal names the rule. `addItems` is not read,
        and a column that is `readOnly` in the TCA stays refused even where
        a page sets `config.readOnly = 0`. This
        is still not a generic record API: the table is fixed and a
        relation is never an argument.

    In a language other than the default one the element is created
    **standalone**, without a translation parent. That is refused on a page
    which already holds connected translations in that language
    ([ADR-193](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-193@0.36)): a standalone element beside them is what the
    page module reports as *Inconsistent content detected*. The refusal sends
    the model to the default language and to `create_translation_draft`. A
    page whose TSconfig sets
    `mod.web_layout.allowInconsistentLanguageHandling` is exempt, as it is
    from core's warning.

    `bodytext` is refused for a type whose form does not show it, such as
    `header`. Otherwise it reaches the DataHandler and its RTE transformation
    exactly as an editor's input does. It is bounded in length and not otherwise filtered — an
    editor may write the same markup by hand, and a tool enforcing a stricter
    rule than the CMS would be enforcing a rule that does not exist.

-   **`create_page_draft`**

    Creates one standard page under a parent page — the first tool that brings a
    page into being ([ADR-180](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-180@0.36)). It is bounded the same way the
    element draft is, and a little further:

    -   It is **always hidden**, with no argument to switch that off. Core's own
        default for a new page is hidden as well; the tool does not rely on it —
        an installation that sets `TCAdefaults.pages.hidden = 0` gets the same
        read-back, and a page that came out visible is deleted again.
    -   It is **always a standard page** in the **default language**. Shortcuts,
        mount points, links, folders and translations are out of reach; a
        translation has its own tool.
    -   The field set is fixed: title, navigation title, position (`parent` and
        an optional `after_page_uid` that must be a subpage of the same parent).
        The URL segment is generated by the DataHandler from the title.

    Authorised by the acting user's *new page* permission on the parent; the
    DataHandler then enforces `tables_modify`, the page-type grant and the
    field-level grants on top.

    It creates the page and **nothing on it** — one record per call, as for
    every writer. The success message hands the model the new uid and names
    `create_content_element_draft` as the tool to put text on it, so "a page
    with an introduction" is two calls and two approvals, each showing one
    record.

-   **`create_translation_draft`**

    Translates one page or content element into another language by running
    core's own `localize` command, so connected-mode translations, inline
    children and every localisation hook behave exactly as they do in the
    backend.

    The result is **hidden**, which is the part core does not do: `localize`
    copies the source's visibility, so a translation of a live page would go live
    the moment it was created.

    An existing translation stops the call and is named in the refusal. The
    `overwrite` argument is the only way past it: it **deletes** that
    translation first — recoverably (`deleted = 1`) and in `sys_log` — and
    the approval card says so on its own line. Whether the target language exists
    for the record's site is core's check, not a second implementation here.

-   **`create_record_draft`**

    Creates one record in a TCA table that has **no dedicated writer** — the
    fallback for extension tables such as a news record
    ([ADR-197](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-197@0.36)). It serves a table by exclusion:

    -   `pages` and `tt_content` have writers of their own and are refused
        by name; `sys_*` tables, the tables no tool may read (`be_users`,
        `sys_log`, the nr_llm and nr_vault tables …) and tables declared
        `adminOnly`, `hideTable` or `readOnly` are refused as well.
    -   A table another **registered creator** declares through
        `RecordCreatorInterface::getCreatedTables()` is refused and the tool
        named — read at call time, so an extension that ships its own creator
        withdraws the fallback the day it is installed. The builtin creators
        declare `tt_content` (`create_content_element_draft`), `pages`
        (`create_page_draft`) and both (`create_translation_draft`). A
        declaration that cannot be read refuses the call and names the tool.
    -   The extension configuration `tools.createRecordDraft.deniedTables`
        (comma-separated table names, empty by default) excludes further tables
        on one installation. It can only narrow the list, never widen it, and a
        configuration that cannot be read refuses every table.
    -   A table without a "disabled" enable column is refused, because nothing
        this extension writes may be visible before a human unhides it.

    Only **scalar** columns can be set — `input`, `text`, `number`,
    `email`, `color`, `datetime` (as a timestamp), `check`, `radio`
    and `select` with static items — and only those the record type's form
    shows as writable on that page: a column the TCA declares `readOnly` is
    refused unless page TSconfig
    `TCEFORM.<table>.<column>.config.readOnly = 0` lifts it, as it does in
    the form (a `radio` excepted, whose `readOnly` the form does not let
    page TSconfig change).
    Relations, files, FlexForms, links and slugs are not arguments;
    `hidden`, `uid`, `pid`, the language, timing, ownership and
    versioning columns are refused by name. Values are checked against the
    record type's TCA — its `columnsOverrides` included — before anything is
    written: the items of a select, `max` and `range`, 0/1 for a check, a
    valid address for an email. The record type is the one the DataHandler
    gives the record — the call's value for the type column, else
    `TCAdefaults` from the page's TSconfig, then from the acting user's, else
    the column's default — and the tool writes it explicitly where the acting
    user may write the type column, leaving it to the DataHandler otherwise; a
    `TCAdefaults`
    value that names no record type of the table is refused. Every column the
    record type marks required must be given. A value the DataHandler would rewrite is refused rather
    than written: a column with an `eval` the DataHandler acts on
    (`upper`, `alphanum`, `unique`, `uniqueInPid`, an extension's
    registered evaluation …; a token it ignores is ignored here too), an
    eight-digit colour on a
    column without `opacity`, a text shorter than its `min`, a decimal
    with more than two decimal places (TYPO3 stores two), a decimal the range
    check would clamp. So is what the page's TSconfig takes out of the
    backend form: a column `TCEFORM.<table>.<column>.disabled` hides, a
    select value `keepItems` or `removeItems` filters out, a column
    `TCEFORM.<table>.<column>.config.readOnly` makes read-only, and a record
    type its type field does not offer on that page — the refusal names the
    rule.

    The record is **always hidden** and **always in the default language**.
    Authorised by the acting user's `tables_modify` grant, the content-edit
    permission on the page or folder, and the field-level grants for every
    column the call sets — asked before the write, because the DataHandler
    drops such a column in silence. What TYPO3 still rewrites — a hook of the
    installation, a grant the pre-check does not model — is read back
    afterwards, the default language included, and the record type unless it
    is core's fallback type and the tool could not write it; a record
    that does not carry what was approved is deleted again and the columns are
    named.

    The approval card names each column with the record type's TCA label — a
    showitem `field;Label`, else a `columnsOverrides` label, else the
    column's own — in English, never in the viewer's language, because the
    card is compared byte for byte when the run resumes:
    `published_at (Published at): "2026-09-10T10:00:00+00:00"`. A page
    TSconfig label override (`TCEFORM.<table>.<column>.label`) is not
    applied, although the backend form shows it: the card must not depend on
    the page or on the viewer's language.

    It declares no editor action — it has no record an editor would select —
    so it is reached through the assistant only, never from a record's
    context menu.

-   **`update_content_element`**

    Changes fields of one existing content element ([ADR-198](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-198@0.36)).
    The field set is the one `create_content_element_draft` offers for a new
    element of the same type — the scalar columns of the type's form, header and
    body text included — checked by the same rules ([ADR-196](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-196@0.36)): a
    value outside a select's items, a number outside its range, a column the
    page's TSconfig hides or makes read-only is refused, and the refusal names
    the columns the type offers. An element whose type the exclusion rule leaves
    out — raw HTML, plugins, menus, shortcuts, a form holding a FlexForm or
    inline children — is refused whole; relations and the identity, position,
    visibility, publication, audience and translation columns are never fields. A
    column a translation takes from its default-language element (\``l10n_mode =
    exclude`\`) is refused and the element to change named. The approval card
    shows every column's change, bound to the whole value. Where some columns
    take and others do not, the answer says which. Authorised by content-edit
    rights on the page, the record-level rights and the field-level grant for
    every column.

-   **`publish_record`**

    Clears the hidden flag of one page or content element — the step after a
    human has reviewed a draft — and changes nothing else. Start and stop
    times and access groups stay as they are; the approval card names them,
    and a hidden default-language record behind a translation, where they still
    keep the record from visitors. A record that is not hidden is not written.
    Needs page-edit rights on a page or content-edit rights on an element's
    page, and the field-level grant for the hidden column.

-   **`delete_record`**

    Deletes one page or content element with core's delete command. The row is
    flagged `deleted` and stays recoverable from the recycler. What core
    deletes with it is counted on the approval card first: the translations of a
    default-language record, and for a page its subpages (their uids, ten and
    then "and N more"), their translations, and the records stored on the pages
    table by table. The counts include records the acting user cannot see; they
    are counts only, never titles. A page with subpages is refused unless the
    call sets `include_subpages`; a branch of more than 50 pages is refused
    outright, and a site root is never deleted. The card also counts the records
    the reference index says still point at the record — links and shortcuts that
    will break. Needs delete rights on a page (and on every page of its branch),
    content-edit rights for an element, and the right to edit every translation
    that goes along; for a page, also `tables_modify` for every table with
    records on it and the languages of the content on it. A delete that would
    take a workspace draft along is refused: core discards the versions and
    new workspace translations of what it deletes for good, and strands the
    other drafts on a deleted page. Publish or discard them in their workspace
    first.

-   **`copy_record`**

    Copies one content element to a page and column, or one page under a
    parent, with core's copy command ([ADR-199](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-199@0.36)). The copy and
    every translation copied with it are **always hidden**, whatever core's
    `hideAtCopy`, the user's preferences and page TSconfig say, and a page is
    **always copied without its subpages**: core would hide only the top page
    of a copied branch. What happens to the translations of a
    default-language record depends on the TYPO3 release and the target's
    **site**: TYPO3 14 and 13.4.25 or later copy none outside a site and,
    inside one, a translation only where the site has its language (for an
    element, where the target page is translated into it); one core refuses
    fails the copy, which is then taken back. Before 13.4.25, core asks no
    site: it copies every page translation, and drops an element translation
    the target page is not translated into without an error. The card states
    the rule of the running core. The answer says how many were copied. A
    page's content is copied with the page. A translation is refused by
    itself, as are a page translation as target, a site root, and a
    standalone element beside connected translations on the target page
    ([ADR-193](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-193@0.36)).

-   **`move_page`**

    Moves one default-language page to a new parent or directly after a
    sibling. Its content, translations and subpages move with it, and it keeps
    its uid and its URL path — core does not regenerate the slug on a move.
    The approval card says so, counts the subpages that move along, and warns
    when the page moves into another site, into a site or out of every site.
    Asks the permissions core asks: delete rights on the page and new-page
    rights on the new parent for a new parent, edit rights within the same
    parent. A site root, a translation, a page translation as parent and a
    target inside the page's own branch are refused.

-   **`replace_file_reference`**

    Points one existing file reference on a content element's `image`,
    `assets` or `media` field at another **existing** file, or removes the
    reference; `attach_file_to_content_element` only appends. A replacement
    takes the old reference's position, and nothing of the old reference is
    carried over — its title, alternative text, description and crop described
    the old file; the card says which texts the new reference will show. The
    file itself is never changed, and the new file must lie in a permitted
    storage inside the acting user's file mounts and carry an extension the
    field accepts. Core deletes the translated references of the old one with
    it: the card names them, the call is refused where the acting user may
    not change the translated element they sit on, and afterwards each
    translated element's reference count is set and read back.

## Registering a tool {#administration-tools-register}

A tool is a PHP class that implements
`Netresearch\NrLlm\Service\Tool\ToolInterface`:

-   **`getSpec(): ToolSpec`**

    Returns the declaration the model receives — a name, a description, and a
    JSON-Schema `parameters` block. Build it with
    `ToolSpec::function($name, $description, $parameters)`.

-   **`execute(array $arguments): string`**

    Runs the tool with the model-provided arguments and returns a plain
    string that is fed back into the conversation as a tool turn.

-   **`getGroup(): string`**

    The tool's *group* — a short, stable identifier used to enable or disable
    whole families of tools at once. Built-ins use `content`, `editing`,
    `structure`, `system`, `accounts` and `configuration`; third-party
    tools declare their own group (recommended: the providing extension's key).
    See [Tool groups](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-groups@0.36).

The interface carries `#[AutoconfigureTag('nr_llm.tool')]`, so a class is
**auto-registered simply by implementing it** — no central registration file
to edit. `ToolRegistry` collects every tagged tool through a DI iterator
and indexes it by spec name; two tools with the **same** `name` is a
developer error and fails fast at container build.

A tool that **creates records** also implements
`Netresearch\NrLlm\Service\Tool\RecordCreatorInterface` and lists the
tables its records land in from `getCreatedTables()`. The generic
`create_record_draft` then steps back from those tables and names your tool
instead ([ADR-197](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-197@0.36)).

When you write a tool, honour the security contract: treat `$arguments` as
attacker-influenced (the model is steerable by injected skill prose),
**validate and scope** every input (cap volumes, scope identifier lookups),
and never return secrets — the result leaves the instance.

## Managing tools {#administration-tools-manage}

The **AI > Operation > Tools** module lists every registered tool
with its global enable state and lets an admin toggle it. A **disabled** tool
is refused on every run, everywhere — the runtime gate is fail-closed, so a
disabled tool can never be offered to the model regardless of a skill's
`allowed-tools` or the per-run selection in the playground. Some built-in
tools (for example `get_env_raw` and `get_php_info_raw`) ship **disabled
by default** because they return unredacted, secret-bearing output; enable
them only deliberately.

![The Tools management module listing each built-in tool with an Enabled or Disabled badge and an Enable/Disable toggle](../Images/ToolsModule.png)

A tool that carries an **editor action** declaration
([ADR-152](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-152@0.36)) reads differently in that list: it shows an icon,
its translated name, one sentence written for a human, and the record types it
addresses — instead of the wire name and the description written for the
language model. Nine of the sixteen writing tools declare one —
`create_record_draft` has no subject record and declares none
([ADR-197](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-197@0.36)), and the six of [ADR-198](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-198@0.36) are
reached through the assistant only — and the wire name stays
visible as the technical detail the toggle acts on. A read-only tool is
unchanged.

The declaration is presentation only. It does not decide whether a tool writes
— that is the tool's declared effect — and it changes nothing about how a call
is fenced, approved or audited.

### What editors see {#administration-tools-editor-actions}

The declaration is what the **Editor Action Center**
([ADR-158](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-158@0.36)) renders. It lives in the editor module
**Web > AI tasks** and appears in two places: as an **AI
actions** catalogue reachable from that module, and as an **AI actions**
entry in the context menu of a record — a page or a content element — which
opens the catalogue narrowed to the actions that address that record.

An editor is offered an action only when all of the following hold, and every
one of them is an administrator's decision:

-   the writing tool is enabled in this module (all sixteen ship
    **disabled**);
-   its group — `editing` — is enabled, and where the default LLM
    configuration restricts tool groups, `editing` is among them;
-   the tool's data class is within the configured provider's trust-zone ceiling;
-   the backend user holds `tasks_use` and has the module ticked in their
    group;
-   the backend user may use the default LLM configuration itself — where that
    configuration restricts **Allowed backend groups**, the user is in
    one of them (see [Backend user permissions](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-permissions@0.36)).

That last point is checked again when the action is started, so an editor
outside those groups cannot start a run by naming the action directly either.

Starting an action creates an ordinary agent run restricted to that one tool.
Because the tool declares a write, the run suspends before it touches anything
and the change appears on an approval card with its preview — the editor is
redirected straight to that inbox. Nothing is written until someone approves.

The record an action is offered on is the record its arguments name, which is
not always the record it writes: **Create content element draft** is
offered on a **page**, because the page is what it must be told, and the
element it creates is the result. Where an action needs something the selected
record cannot supply — **Move content element** needs a target page —
the editor names it in the note, and the approval card shows the destination
that was resolved from it.

Files have no context-menu entry yet: the file list identifies a file by its
combined identifier rather than by uid, so **Set alternative text** is
listed in the catalogue but has no per-record entry point.

### Several records at once {#several-records-at-once}

Once a record is selected — that is, when the catalogue was opened from a
record's context menu — each action there also offers **Run this on
several records** ([ADR-162](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-162@0.36)). The catalogue opened from the
module menu has no record and therefore no bulk entry point either; an action
needs a subject, and this module picks none. That page takes a list of record
numbers from the same table, seeded with the record that was selected, and
shows, before anything starts, which of them the action can run on, which are
skipped and why, and what the batch is expected to cost in requests, tokens and
money.

At most 100 entries of that list are read at all. A longer paste is cut there
and the page says so, because everything past the cut would otherwise become a
table row and a record number in a message no one can read.

Starting it creates **one ordinary run per record**. There is no bulk mode: each
record gets its own approval card with its own preview, and an approver decides
them one at a time. At most 20 records are started in one press, because the runs
execute inside the one backend request.

The AI budget is checked once per run, so a batch can run out of budget partway
through. When that happens the batch stops and names the records the stop kept
from starting — the record it stopped on was run, and is reported separately.
Nothing is left half-written: the runs that did start are proposals awaiting
approval, not changes.

Runs that ended for some other reason are reported by kind — failed, stopped by
a guardrail, cancelled, or simply finished without proposing a change — so a
batch in which everything failed does not read like one in which nothing needed
changing.

The estimate on that page is deliberately rough and says so: it does not count
the system prompt and skills the runtime adds, and its upper price assumes every
request returns the configured token ceiling. It shows no price range at all
unless the model record carries both an input and an output price and the
configuration sets an output ceiling — an absent range means "unknown", which
`0.00` would not. Treat it as an order of magnitude, not an invoice.

## Tool groups {#administration-tools-groups}

Every tool belongs to a **group** (its `getGroup()` value). The built-in
groups carry a translated name in the module header beside their identifier; a
group a third-party extension brings has no translated name and shows its
identifier alone. The built-in taxonomy:

| Group | Tools |
| --- | --- |
| `content` | `search_records`, `get_page_content`, `read_records`, `get_record_history` |
| `structure` | `get_pagetree`, `get_tca`, `read_fal_asset_meta`, `get_full_tca`, `get_table_schema`, `get_flexform_schema`, `resolve_url`, `validate_tca` |
| `system` | `get_env` (+ raw), `get_php_info` (+ raw), `fetch_logs`, `probe_url`, `list_extensions`, `list_scheduler_tasks`, `get_system_status`, `list_deprecations`, `list_middlewares` |
| `accounts` | `list_be_users` (+ raw), `list_be_groups` |
| `configuration` | `get_typoscript`, `get_tsconfig`, `fluid_resolve`, `check_typoscript`, `get_site_config` |
| `code` | `get_last_exception`, `read_source`, `search_code` |
| `files` | `list_fal_storages`, `browse_fal_folder`, `search_fal_files`, `get_fal_references`, `find_missing_files` |
| `rag` | `site_rag_query`, `site_fetch_source` |
| `editing` | `update_page_metadata`, `set_page_social_image`, `set_file_alternative_text`, `update_fal_asset_meta`, `attach_file_to_content_element`, `move_content_element`, `create_content_element_draft`, `create_page_draft`, `create_translation_draft`, `create_record_draft`, `update_content_element`, `publish_record`, `delete_record`, `copy_record`, `move_page`, `replace_file_reference` — the only WRITING group |

Groups can be switched on three levels, and the result cascades
**fail-closed** — a tool is offered only when *every* level permits it:

1.  **Centrally** in the Tools module: each group header carries an
    Enable/Disable group toggle. A disabled group refuses all of its tools —
    including same-group tools installed later — and a per-tool override can
    **not** re-enable a tool inside a disabled group (predictable,
    fail-closed). Per-tool toggles keep working but take effect only once the
    group is enabled again.
1.  **Per configuration**: the **Allowed tool groups** field on an
    LLM configuration restricts agent runs with that configuration to tools of
    the selected groups (empty = all groups). This intersects with a skill's
    `allowed-tools` declaration.
1.  **Per run** in the playground: the tool checkboxes are grouped, and each
    group checkbox (de)selects its children.

Third-party extensions declare their own group per tool; the recommended
value is the extension key, so an admin can disable an extension's whole
tool family with one toggle. The design is recorded in
[ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.36).

## Network egress policy per group {#administration-tools-egress}

Network egress is governed **per tool group** and is **fail-closed**
([ADR-061](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-061@0.36)). Each group has a declared egress scope; a group
with no declaration may make **no** outbound request:

| Scope | Meaning |
| --- | --- |
| `none` | No outbound network request (the default for every group). |
| `own_site` | Only the instance's own configured site hosts, resolved through `SiteFinder` — the exact allow-listing `probe_url` applies, now lifted to the group boundary. |

Only the `system` group (which carries `probe_url`, the one built-in that
fetches over the network) is granted `own_site`; every other group is
`none`. There is no "any host" scope, so a newly installed or mis-declared
tool group can never egress to an arbitrary target. The diagnostics tools that
share the `system` group (`get_env`, `fetch_logs` …) never make a network
request, so the grant does not loosen them.

## Using the Tool Playground {#administration-tools-playground}

The playground lives in **AI > Operation > Playground** and is
admin-only. It is a sibling of the [Tools](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-manage@0.36) management module: the playground *runs* the
loop, while the Tools module governs *which* tools exist and are enabled.

![The Tool Playground module with the LLM configuration picker, an empty prompt box, the Run button, and the Available tools panel](../Images/ToolPlaygroundShell.png)

> [!TIP]
> **Small local models work best with a narrow tool set.** With every
> group enabled, the model is offered every enabled tool declaration at
> once (several dozen). Small models such as the seeded `qwen3:4b`
> often fail to pick the right tool from a set that large, or reason
> past the token budget without calling any. Untick the groups that are
> irrelevant to the question — restricted to one or two groups, the same
> model picks the right tool. Larger hosted models cope with the full
> set.

1.  Pick an **LLM configuration** from the dropdown. Its vault-stored API key,
    model, temperature and system prompt are what the loop actually runs on —
    the playground never falls back to a default model.
1.  Type a **prompt**. Optionally open the override panels to **force-inject
    skills** (added on top of the configuration's own), **force-add snippets**
    (inserted as leading system messages), override the **system prompt**, cap
    the **max rounds**, or tick **capture raw provider response**.
1.  Click **Run** — or **Dry run** to assemble the prompt and
    inspect exactly what *would* be sent without calling the model.
1.  Read the **inspector** — live from the moment you click Run. A summary
    strip reports rounds, tool calls, the prompt/completion token split,
    estimated cost, wall time and status. The step list is the nr_llm ↔ LLM
    dialog in order: each round opens with a **Context budget** step,
    then its outbound **request** (the messages sent and the tools offered)
    appears the instant it goes out, a waiting indicator shows while the model
    works, then the **response** and each tool execution stream in. Select a
    step to open its detail — requests carry **Messages sent** and
    **Tools offered**; responses carry **Structured**,
    **Raw JSON** and **Thinking**. The model's **final answer**
    closes the run.
1.  The **Context budget** step says where the window went for the round
    that follows it, so you can act on the component that is yours to change
    rather than only learn that history was dropped. It has two tabs:

    -   ****Where the window went****

        The window, the output reserve, the safety margin and the resulting
        budget, then four component lines — transcript, tool schema, system
        prompt (incl. snippets) and skills — that **sum to the estimated total**,
        plus what is left. On this surface the system-prompt line always reads
        *counted in the transcript* and the skills line always reads 0: the agent
        loop builds both into the transcript before the fit measures it, so their
        content is already on the transcript line. The table says so under the
        figures. A send whose reserve exceeds the whole window is handed to the
        provider unmeasured, and the step reports *no accounting* instead of a
        window of zero.

    -   ****Injected context****

        Every snippet and skill this run injects, by **name only**, with the data
        class it declared ([ADR-144](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-144@0.36)) or *not classified*, and the
        strictest class across all of them. The list covers the snippets and
        skills you force-injected for this one run as well as the
        configuration's own, and the input-context gate reads the same list
        ([ADR-164](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-164@0.36)): a source shown here is a source that is
        gated. Forcing a snippet the configuration's trust zone would refuse
        therefore refuses the run, naming that snippet.

![A completed tool run — the summary strip, the ordered step list and the selected step's detail tabs for a two-iteration agent loop](../Images/ToolPlaygroundRun.png)

The **Tools available to this run** list lets you narrow a single run
to a subset of the globally-enabled tools (the full list and the global
enable/disable controls live in the [Tools](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-manage@0.36) module). Raw-response capture is off unless you
tick it, so ordinary runs never retain the provider's raw payload. Every
displayed string — tool arguments, tool results (which may include
`sys_log` content), and the final answer — is rendered escaped; HTML is
only ever shown inside a sandboxed preview, never injected into the page.

Each run is bounded by the iteration cap (default 5) and, when the
configuration's backend user has a budget, by the per-iteration budget
pre-flight. If the cap is hit with tools still pending, a final tool-free
completion synthesises a closing answer and the run is marked *truncated*.
The aggregated **token** usage is reported; the monetary cost is recorded in
the usage table by the middleware pipeline.

## Ollama model-capability dependency {#administration-tools-ollama}

Tool calling depends on the **model**, not just the provider. For Ollama,
only function-calling-capable models — for example `llama3.1`,
`mistral`, `qwen2.5` — return tool calls. A model without function-calling
support simply **answers the prompt directly and never calls a tool**; the
loop ends gracefully on the first plain answer. If a configured Ollama model
never seems to use the available tools, verify it is one of the
function-calling models for your Ollama version.

## Gating tools with `allowed-tools` in a skill {#administration-tools-allowed}

A skill's `SKILL.md` front-matter may carry an `allowed-tools` key that
gates which tools the skills attached to a configuration (or task) grant for a
run. The resolution is **fail-closed on declaration**, computed over the
configuration's *effective* skills (enabled, non-orphaned, at or above the
instance trust floor, deduped):

-   **Absent** (no skill declares `allowed-tools`) — no opinion; all
    registered tools are offered.
-   **Declared list** — the **union** of the declared lists across the effective
    skills; only those tools are offered (intersected with what is actually
    registered, so an unknown name is dropped).
-   **Declared empty** (`allowed-tools: []`) — declares **zero** tools; if no
    other effective skill widens the set, the run gets no tools and is a single
    plain completion.

A disabled or orphaned skill never grants tools. The allow-list is enforced
both when the tools are offered to the model and again when a tool call is
executed, so a prompt injection cannot reach a tool the skills did not grant.

The effective set is *not* the same as what ends up in the prompt. The
skill-block byte budget (`skills.maxBytes`, see
[Managing skills](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills-isolation@0.36)) is applied after the
allow-list has been computed, while the block is assembled. A skill dropped
for the budget therefore still contributes its `allowed-tools`, but its
usage rules never reach the model. That order is deliberate: a budget-aware
union would let the drop of the last *declaring* skill collapse the allow-list
to "no restriction" — every registered tool — which is a looser gate, not a
tighter one. Keep `skills.maxBytes` above the composed size of the skills
you rely on if you want their prose to arrive alongside the tools it
describes; every drop is logged as a warning.

See [ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.36) for the runtime design and security rationale.
