---
title: "ADR-061: Skill trust levels, signed manifests, injection scanning"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-061@0.35"
source: "Adr/Adr061SkillTrustEgressAndAudit.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-061: Skill trust levels, signed manifests, injection scanning

---

-   *Status:* Accepted
-   *Date:* 2026-07-14
-   *Authors:* Netresearch DTT GmbH

## Context

The skill and tool subsystems already have a solid, layered base:

-   **Ingest** ([ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.35)) fetches `SKILL.md` behind a GitHub
    host allow-list, resolves refs to an immutable commit SHA, checksums each
    body, and arrives `enabled = false` for repo/marketplace skills.
-   **Injection** ([ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.35)) re-verifies the checksum fail-closed
    at compose time, never uses the system role, and fences the block behind a
    guard preamble.
-   **Tools** ([ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.35), [ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.35)) enforce a
    fail-closed allow-list, per-tool admin gating against the acting backend
    user, and a group enable-cascade.

Four gaps remain. The SHA-pin binds *bytes to a URL* but not *bytes to a
publisher identity*. The ingest inspects structure but never looks at the prose
for prompt-injection payloads. The instruction/data boundary rests on a guard
sentence and message role, without an explicit data delimiter. And there is no
immutable record of *who* imported or enabled *which* skill *when* — the sync
overwrites its own state. Separately, network egress is decided per tool
(`probe_url` hard-codes its own site allow-list) rather than declared per tool
group, so a new or third-party tool's egress is governed by nothing.

## Decision

1.  **Publisher trust levels, separate from support status.**
    `SkillTrustLevel` (`untrusted` < `community` < `verified` <
    `first_party`) classifies a `SkillSource`'s *provenance*. It is
    explicitly **not** `support_status` (which is not a safety signal,
    [ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.35)). The level is denormalised onto each `Skill`
    at ingest exactly as `support_status` is, and an unknown/legacy stored
    value reads as the lowest level (`fromStringOrUntrusted`). Injection and
    the allowed-tools union are gated against a configurable **minimum** trust
    level (`skills.minTrustLevel`, default `untrusted` = accept every enabled
    skill): `SkillComposer::effectiveSkills()` — the single source of truth
    for both paths — drops any skill below the floor. This composes with, and
    does not replace, the existing `enabled = false` default: `untrusted`
    still requires an explicit admin enable.
1.  **Manifest fingerprint over a full public-key signature.** A source may
    declare an `expected_fingerprint`: the sha256 its whole discovered skill
    set must hash to (a canonical, order-independent digest over the
    `identifier → body-checksum` pairs, degenerating to one entry for a
    single-file source). `SkillManifestVerifier` recomputes it after collect
    and verifies with `hash_equals` **before** any skill is materialised; a
    mismatch fails closed — no upsert, no orphaning — leaving the last known-good
    skills untouched, and is audited. We chose a declared expected digest over a
    detached public-key signature deliberately: it binds a publisher-declared
    identity to the exact reviewed bytes (beyond the URL SHA-pin) with **no
    key-management infrastructure**. Full detached signatures over a signed
    manifest listing per-skill digests are a documented follow-up (see
    [consequences](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-061-consequences@0.35)).
1.  **Prompt-injection scanning at ingest.** `PromptInjectionScanner` runs
    a data-driven, auditable pattern set over each body and records the findings
    on the skill. Tiering is conservative to avoid over-blocking legitimate
    prose: only **high-confidence** jailbreak markers (instruction override,
    role reset, DAN/developer-mode personas, chat-template control tokens)
    **force-disable** the skill fail-closed at import — even a single-file source
    that would otherwise default enabled. Medium/low findings (secret-exposure
    verbs, guardrail-bypass wording, covert behaviour, long encoded blobs) only
    *flag* the record for review, since the repo/marketplace skills they most
    often appear on already arrive disabled.
1.  **Explicit instruction/data channel separation.** `SkillComposer` keeps
    the trusted guard preamble and additionally fences the untrusted bodies
    between explicit `BEGIN/END UNTRUSTED SKILL DATA` markers that label them
    as reference data the model must not execute as instructions. Message role
    remains defence-in-depth, **not** a trust boundary ([ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.35)); the markers give the model an unambiguous boundary in-band.
1.  **Immutable import audit trail.** `tx_nrllm_skill_audit` records who / when
    / which source / which SHA / which checksum / which trust level / which scan
    result for every ingest outcome, enable, disable and fail-closed rejection.
    It is **append-only by construction**: `SkillAuditRepository` exposes
    `record()` and read helpers and offers no update or delete path, and the
    table carries no soft-delete column. Any purge is a separate, documented
    retention operation — never the regular write path. The trail is additive to
    the existing sync; no backend UI (UI-less log, like `tx_nrllm_tool_state`).
1.  **Per-group egress policies.** `EgressPolicyService` declares a
    network-egress scope per tool group ([ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.35)) as data,
    fail-closed: a group with no entry resolves to
    `ToolEgressScope::NONE` and may make no outbound request. The one
    positive scope, `own_site`, resolves the instance's own site hosts through
    `SiteFinder` — the allow-listing `probe_url` previously hard-coded, now
    lifted to the group boundary and consulted by the tool through the shared
    gate. There is no free-form "any host" scope, so a new or mis-declared group
    can never egress to an arbitrary target.

## Consequences

-   ● Publisher identity is now a first-class, fail-closed control: trust gates
    injection and tool grants, a declared fingerprint binds the reviewed bytes to
    the publisher, and an unknown trust value reads as the lowest.
-   ● Prompt-injection payloads are caught at the door: high-confidence jailbreaks
    never reach an enabled state, and every body's scan result is retained for
    review and audit.
-   ● The audit trail makes the provenance of any skill that can reach a prompt
    reconstructable after the fact, and cannot be rewritten through the app.
-   ● Egress is declared per group and fail-closed; the network reach of the tool
    surface is now one auditable table, not scattered per-tool constants.
-   ◐ The audit table grows monotonically (one row per ingest/enable/disable). At
    the expected admin-driven cadence this is negligible; a retention command is a
    documented follow-up if ever needed.
-   ◐ Egress scope is keyed by the existing coarse group taxonomy, so `system`
    (which carries `probe_url`) is granted `own_site` even though it also
    holds non-egressing diagnostics. Those tools never call the gate, so the grant
    does not loosen them; a dedicated network group is a possible follow-up.
-   ✕ **Deferred:** full detached public-key signatures over a signed per-skill
    manifest (the fingerprint is a declared expected digest, not a PKI
    signature); an admin-review queue UI for medium/low injection findings; and a
    dedicated egress group split. Each is additive to the fail-closed base
    delivered here.

See [ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.35) and [ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.35) for the ingest /
injection base, [ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.35) and [ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.35) for the
tool runtime and groups, and [the administration guide](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills@0.35) for operation.
