Managing skills
Skills are GitHub-hosted SKILL. files — a YAML front-matter block
with a name and description plus a markdown body — that nr-llm can
ingest, review, and (from Plan 1b) inject into prompts. You add a skill
source that points at GitHub, sync it, and then enable the individual
skills you want.
Skill management is admin-only. It lives in AI > Authoring > Skills and is not delegated to other backend groups: a skill body becomes prompt context, so the two skill tables are treated as a privilege-escalation surface.
Note
Ingest — adding sources, syncing and reviewing — is described by ADR-035. Attaching enabled skills to tasks and configurations and injecting them into text-generation prompts is described by ADR-036 and the Attaching skills section below.
Source types
A source has one of three types:
single_file - One
SKILL.at a fixed path in a repository. A single, explicit admin act — its skill may default to enabled.md repo- A whole repository. Every
SKILL.under the repo root,md skills/<name>/,.claude/orskills/<name>/ <plugin>/is discovered. Discovered skills arrive disabled for review.skills/<name>/ marketplace- An Anthropic
marketplace.index that lists plugins pointing at further repositories. Each entry is expanded with thejson repoflow. All discovered skills arrive disabled.
Adding a source
- Navigate to AI > Authoring > Skills.
- Click New Skill Source.
-
Fill in the fields:
- Title
- Display name for the source list.
- Type
single_,file repoormarketplace(see above).- URL
- The GitHub URL the type expects (the
SKILL.URL, the repository URL, or themd marketplace.URL).json - Ref
- A branch or tag (for example
mainorv1.). It is resolved once to an immutable commit SHA at sync time; all bodies are then fetched by that SHA, never by the moving branch.2. 0
- Click Save.
The pinned_, sync_, sync_ and last_
fields are managed by the sync run and shown read-only.
Sync errors shown by the module and stored on the source mask credential query parameters and URL userinfo passwords. Fatal-error diagnostics retain the exception class, source uid and sanitized message and source URL; they do not retain the raw exception object.
GitHub token and rate limits
Unauthenticated GitHub API access is limited to 60 requests per hour,
which is quickly exhausted by a repo or marketplace sync. Add a
personal access token (a read-only, public-repo token is enough) to raise
the limit and to read private repositories.
- The token is set through the Set token action on a source, not typed into a FormEngine field. The source stores a vault reference; nr-vault stores the token with envelope encryption. The plaintext token is never stored in the source record.
- When a sync hits the rate limit (HTTP 403 with no remaining quota), the
source is set to
sync_carrying the reset time; state is not partially corrupted. Add a token and re-sync.status = error
Host-allowlist prerequisite
nr-llm enforces an app-level GitHub allowlist on every skill request:
the scheme must be https and the host must be one of github.,
raw., api. or
codeload.. This is separate from, and in
addition to, the nr-vault SSRF guard.
On hardened instances that restrict outbound HTTP through the global
HTTP/ SSRF setting, those four GitHub hosts must be on
that list, otherwise every sync fails closed. This is a deliberate
prerequisite — nr-llm never silently bypasses the SSRF guard.
Syncing and the review flow
The Skills module — the Sources table (type, sync status,
last synced, per-source actions) above the discovered Skills
with their partial / full support badge and enabled state.
- On a source, click Sync. The source moves through
never_→synced syncing→ok/partial/error. Thesyncingstate also acts as a lock: a second concurrent sync on the same source is refused. partialmeans the per-sync file-count or wall-time bound was reached (large marketplaces); the skills fetched so far are stored.- Discovered skills from
repoandmarketplacesources are created disabled by default. Review each one, then toggle it on with Enable. - Re-sync never silently changes an enabled skill. If a re-sync
brings a different version of an enabled skill, nr-llm auto-disables
it so you re-confirm before it is used again. A version is the body
and the frontmatter fields the model reads — name, description,
support status,
allowed-and thetools processmarker — so a frontmatter-only change upstream, such as a widenedallowed-, disables the skill as well (ADR-214).tools - A skill that disappeared upstream is marked orphaned and disabled, never silently dropped, so attachments (Plan 1b) do not vanish.
Deleting a source cascade-deletes its skills.
The partial support badge
Each skill carries a support badge:
full- The skill is plain front-matter and prose.
partial- The body or front-matter references scripts,
references/,assets/or anallowed-declaration.tools
Warning
partial is not a "safer content" badge. It only signals that
the referenced scripts and assets are not executed by nr-llm
(which is true for every skill in this release). The prose itself is
fully untrusted regardless of the badge. Asset references are stripped
from injected prose purely to avoid dangling instructions, not as a
security control.
See ADR-035 for the full design and security rationale.
Attaching skills and injecting them into prompts
Enabled, non-orphaned skills can be attached to a Task and/or an
LLM configuration via the Skills field on those records
(only enabled skills are offered). At execution time, for text-generation
operations only — completion, translation and task execution; never
embeddings, vision or speech — nr-llm composes unapproved attached skills into a
delimited block and prepends it to the user prompt. Approved instruction
versions use the system-message channel described below; the stored
configuration system_ is never modified.
A configuration has two skill fields:
- Skills loaded on demand. An agent run lists the attached skills
whose current version is approved as an instruction (and which are not
process skills) by uid, name and description in the description of the
builtin tool
load_. When the task matches, the model loads one: its section joins the run's system message before the next call, the tool result confirms the load without the body, and loading the same version again appends nothing. The load re-checks the attachment, the record and the approval at that moment, and refuses a section that would leave less than 10 % of the model's context budget. A skill attached here that is not approved is neither listed nor composed. Tasks, translation and the other one-shot paths never load on demand.skill - Skills loaded always. Composed into every run, as described below. Every attachment that existed before the load modes is one of these. Process skills cannot be attached here.
Both fields grant the skills' declared tools (allowed-) from the
start of a run; loading a skill grants nothing.
Note
An attachment loaded always is eager and complete. The whole skill
body — the entire SKILL. prose after the front-matter, not just
the name/description — is written into the prompt before the model
runs. Nothing loads a skill's references/ / scripts/ / assets/
(those lines are stripped from partial skills, and the files are never
executed). Such a skill therefore always costs its full body in tokens on
every run (subject to the budget below).
Composition rules:
- Precedence. Configuration skills are the baseline, task skills are additive; the set is the union deduped by source + identifier (the configuration wins on a duplicate). The configuration block renders first.
- Budget. The untrusted user-message block is bounded by a byte budget; when it is exceeded, task-additive skills are dropped before configuration-baseline skills and each drop is logged. Approved instruction sections are kept in full and count toward the context-window estimate. A request that does not fit even then is still sent, with a logged warning: the chat path first drops older turns, the completion path sends the prompt unchanged. No instruction section is dropped to make it fit.
- Integrity. Each skill's version digest — over the body and the frontmatter fields the model reads — is recomputed from the stored fields at injection time; a mismatch (tampering or a stale row) drops that skill — it is never injected. A row synced before the digest existed is checked against its body checksum until the upgrade wizard or a sync writes its digest. Name, description and body are therefore read-only in FormEngine.
- Process skills. A skill whose frontmatter sets
process: trueis never composed from an attachment; it is skipped with a warning. - Untrusted output. Skill prose is third-party text; output produced under its influence is treated as untrusted and escaped/sanitized where it is stored or rendered. Message role is defense-in-depth, not a trust boundary.
See ADR-036 for the injection design.
Writing skills in the backend
A source of the type Written in the backend holds skills that are authored in TYPO3 instead of fetched from GitHub (ADR-214).
- Create a skill source and choose the type Written in the
backend. Set its trust level: it decides whether its skills are admitted
(
skills.) and whether an approved version may instruct (min Trust Level skills.).instruction Trust Level - In the Sources table, click New skill on that
source. Write the name, an identifier unique within the source, the
description and the body.
Allowed toolstakes a JSON list of tool names; leave it empty for no declaration,[]allows no tool. - Enable the skill and attach it like any other. Approve a version as described below to make it an instruction.
Skills written here are edited through FormEngine and the DataHandler, so every change is in the record history with a diff and a rollback. Every edit changes the version: an approved version stops instructing at the next run until the edited version is approved. Until a version is approved the skill grants no tools — it counts as a declared empty list — and afterwards it grants the tools of the approved version it holds, or of its most recent approved version while it holds an unapproved one; never the ones in the field. Every unrevoked approval keeps vouching: an author who restores the fields of an older approved version gets that version back, its tools included. Revoke an older version when a newer one is meant to replace it.
A skill attached to a configuration or a task keeps restricting the tools of its runs even when it stops instructing: an orphaned skill — one whose identifier the sync no longer finds — grants no tools and still restricts, as does a skill whose source is gone. A skill that is still attached cannot be deleted; detach it on the configuration or the task first.
Only an administrator enables or disables a skill. A skill the sync disabled — its version changed, the scanner found an injection, or it was orphaned — keeps restricting the runs it is attached to until an administrator enables it again or detaches it; a skill an administrator disabled drops out of those runs. Hiding a skill takes it out of every run like an administrator's disable: a hidden skill neither instructs nor restricts. Like the enable flag, hiding and unhiding an existing skill is an administrator's, even where a group is granted the field. A new skill, a copy included, is stored disabled unless an administrator enables it, and such a skill restricts the runs it gets attached to — a copied page's configuration, for example — until an administrator enables it; a new skill an administrator stores with the administrator's mark — a copy of a skill an administrator disabled — stays out.
A workspace publish request that would delete a page holding an attached skill, on it or below it, is refused as a whole: no workspace action in that request is applied. "Publish all" works in batches, so batches before the refused one are published. The single publish and "publish all" show the refusal; publishing or sending a selection to a stage, and the scheduled auto-publish, write it to the system log only — a staged delete over an attached skill then holds back every auto-publish of its workspace until the skill is detached or the delete discarded.
A backend user who is not an administrator writes skills only with the fields an administrator granted: the source, the allowed tools and the data class are excluded fields (Allowed excludefields of the backend group). Without the source grant a new skill is stored without a source and is never composed. The enable flag is an administrator's, even when a group is granted it: a new skill stays disabled until an administrator enables it. The New skill button creates the record on the root page, which only administrators may write; a backend user who is not an administrator creates the skill with Create new record on a page in their web mount.
A skill does not change its kind by changing its source: a skill that a sync wrote keeps being checked against what the sync wrote, also on a backend source, so an edit after such a move takes it out instead of becoming a new version.
The same rule protects every skill's tool declaration: a skill whose source is missing, hidden or disabled, and a synced skill edited after its sync count as a declared empty list. They restrict a run's tools instead of widening them. A process skill counts only in a run that invokes it; attached without an invocation it adds neither tools nor a restriction.
A backend source cannot be synced.
Approving a skill version as an instruction
By default every skill is untrusted reference data: it is fenced in the user message and the model is told not to follow it as instructions. An administrator can approve one version of a skill so that it is composed into the system message as an instruction of the installation, behind the configuration's own prompt and snippets — or appended to the caller's system message when the caller sends one (ADR-214).
- In the Skills list, click the check-mark action of a skill. The review page shows the current version, its version digest, the source's trust level and the instruction trust level, and — once a version was approved — the diff against the most recent approved version.
- Click Approve this version. The form sends the digest it showed; if the skill changed in the meantime, the approval is refused and the page shows the new state.
- To withdraw an approval, click Revoke this version in the approval history. A revoked version becomes an instruction again only through a new approval, never because a sync brought the same text back.
A version instructs only while both hold:
- an unrevoked approval names exactly this version (digest) of this skill from its current source — any change to the body or a covered frontmatter field, or moving the skill to another source, needs a new approval; and
- the source record's trust level is at or above
skills.(extension configuration, defaultinstruction Trust Level verified). A value belowskills.is read as that minimum; a value nobody can read is read asmin Trust Level first_. The level is read from the source record at every composition, so re-classifying a source takes effect at once. A hidden or disabled source vouches for nothing.party
A run that waits for a human — for a write approval or for typed input —
keeps the approved versions it was composed with. Before it continues, it
checks each of them again: if the approval was revoked, the skill was deleted,
disabled, hidden or orphaned, or the source fell below skills. in the
meantime, the run stops with an error that names the skill and the reason, and
no pending call executes. Start a new run to continue without that
instruction.
The fields that decide this — on a skill its source, trust level, checksums, version digest, enabled and hidden flags, allowed tools, support status, orphan flag and stored frontmatter; on a source its type, trust level, hidden and enabled flags, URL, ref, pinned SHA, expected fingerprint and access token — are exclude fields. A non-admin group reaches them only when an administrator grants them under Allowed excludefields.
A version that contains characters a browser renders as nothing but a model reads — Unicode tag characters, zero-width and direction marks, no-break spaces and similar — cannot be approved: the approval would bind to text the approver did not see. The review page lists each such character with its field, line and code point. Remove them at the source and sync again.
Approving and revoking is restricted to administrators. Every approval, revocation and refused approval is written to the skill audit trail with the version digest it concerns. A skill without a version digest yet (synced before the upgrade, and not yet migrated) cannot be approved: run the upgrade wizard Write the version digest onto existing skills or sync its source first.
An approval snapshot keeps an omitted tool declaration distinct from a declared empty list. A malformed or non-list JSON declaration in a stored snapshot is read as an empty list, so object values cannot become tool names.
Warning
An approved skill is an instruction with the acting user's reach: it can steer which tools the model calls, within the run's allow-list and the user's TYPO3 permissions. Write tools still need a human approval for every call; no skill can switch that off.
Isolation controls: trust, fingerprint, injection scan, audit
On top of the SHA-pin and checksum controls above, each source and skill carries the isolation controls introduced in ADR-061.
Publisher trust level. Every source is classified — untrusted (the
default, for anonymous public GitHub content), community, verified or
first_ (operator-controlled). This is provenance, independent of the
partial support badge. Each synced skill denormalises its source's level, so
re-classifying a source takes effect on the next sync. The instance-wide floor
skills. (extension configuration, default untrusted) gates
use: a skill below the floor is dropped from both prompt injection and the
allowed-tools union. Raising the floor to verified therefore hides every
community/untrusted skill without deleting it. Trust is separate from the
enabled = false default — an untrusted skill still needs an explicit
enable. An unreadable or mistyped floor falls back to untrusted; the value
actually in force is shown under Effective policy.
Skill-block byte budget. skills. (extension configuration,
default 24000) caps the composed block that is prepended to the user
prompt. The measure is bytes, not tokens — a deliberate over-estimate, since no
tokenizer is available. When the block exceeds the budget, skills are dropped
from the tail first: task-additive skills go before the configuration baseline,
and every drop is logged as a warning. Lower the value to reserve more of the
model's context window for the conversation itself; raise it if a large
configuration baseline is being trimmed. An empty, non-numeric or zero value
falls back to 24000 — the cap cannot be switched off, so an emptied field
never puts an unbounded block on the wire. The budget is instance-wide and
independent of the model's context window; the per-request window bound is
handled separately by the context-window manager (ADR-107).
The budget bounds the prompt block only, not the allowed- union
(Gating tools). The union is computed
over the effective skills before the block is assembled, so a skill dropped
for the budget still grants its tools while its usage rules stay out of the
prompt. That is deliberate — a budget-aware union would widen the gate,
because dropping the last declaring skill removes the restriction entirely.
Watch the drop warnings when lowering the value: they name every skill whose
prose stopped shipping while its tools kept being offered.
Manifest fingerprint (optional). A source may declare an
expected_: the sha256 its whole skill set must hash to. When set,
the digest is recomputed at sync and verified before anything is materialised; a
mismatch fails closed (no skill is imported, the source goes to error) and
leaves the last known-good skills untouched. Leave it empty to rely on the
commit-SHA pin alone. This binds the reviewed bytes to a publisher-declared
identity beyond "these bytes from this URL"; it is a declared digest, not a
public-key signature.
Prompt-injection scan. Each body is scanned at ingest for known injection
signatures. A high-confidence jailbreak marker (e.g. "ignore all previous
instructions", role reset, chat-template control tokens) force-disables the
skill at import — even a single-file source that would otherwise default
enabled — and must be re-reviewed before enabling. Lower-confidence findings are
recorded on the skill (Injection scan findings) for review without blocking.
Immutable audit trail. Every ingest, enable, disable and fail-closed
rejection is written to tx_ with who / when / source / SHA /
checksum / trust level / scan result. Normal ingest and approval operations
never update or delete existing audit rows. The explicit
nrllm: command removes rows older than
privacy.; provenance remains reconstructable within
that configured retention window. See Data retention & purge.