---
title: "ADR-061: Skill trust levels, manifest fingerprints, injection scanning"
manual: "LLM Foundation"
version: "0.44"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-061@0.44"
source: "Adr/Adr061SkillTrustEgressAndAudit.rst"
rendered: "2026-11-12T17:35:48+00:00"
---

# ADR-061: Skill trust levels, manifest fingerprints, injection scanning {#adr-061}

## and per-group egress policies {#adr-061-audit-clarification}

-   *Status:*

    Accepted (item 6: a filtered external egress scope now exists — see [ADR-202](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-202@0.44);
    items 1 and 4: the trust level also decides whether an approved version
    is fenced or an instruction — decided by [ADR-214](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-214@0.44),
    implemented — and a backend-authored skill is admitted by its source's
    level — decided there, implemented)

-   *Date:* 2026-07-14
-   *Amended:*

    2026-09-23 by [ADR-202](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-202@0.44) (item 6: a filtered external scope now exists);
    2026-10-08 by [ADR-214](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-214@0.44) (items 1 and 4)

-   *Authors:* Netresearch DTT GmbH

> [!NOTE]
> Manifest verification is a declared SHA-256 fingerprint check, not a
> public-key signature. Its assurance depends on how the administrator obtained
> the expected digest. Egress checks constrain cooperating tool paths, not
> arbitrary PHP. See [Managing skills](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills@0.44) and [Network egress policy per group](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-egress@0.44).

## Context {#adr-061-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.44)) 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.44)) 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.44), [ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.44)) 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 {#adr-061-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.44)). 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 checks identifiers and body
    checksums against an expected digest obtained by the administrator. It does
    not authenticate the publisher independently of that digest's delivery
    channel, and does not cover frontmatter, trust or approval metadata. 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.44)).
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.44)); 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.44)) 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. The gate has no unfiltered any-host scope. A custom PHP tool must
    call it; this does not prevent a separate HTTP client from using the network.

## Consequences {#adr-061-consequences}

-   ● Operator-declared trust gates injection and tool grants. A fingerprint
    checks expected identifiers/body checksums; it is not a public-key signature
    or independent publisher authentication. Unknown trust 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.44) and [ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.44) for the ingest /
injection base, [ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.44) and [ADR-043](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-043@0.44) for the
tool runtime and groups, and [the administration guide](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills@0.44) for operation.
