ADR-061: Skill trust levels, signed manifests, injection scanning
- Status
-
Accepted (item 6: a filtered external egress scope now exists — see ADR-202)
- Date
-
2026-07-14
- Amended
-
2026-09-23 by ADR-202 (item 6: a filtered external scope now exists)
- Authors
-
Netresearch DTT GmbH
Context
The skill and tool subsystems already have a solid, layered base:
- Ingest (ADR-035) fetches
SKILL.behind a GitHub host allow-list, resolves refs to an immutable commit SHA, checksums each body, and arrivesmd enabled = falsefor repo/marketplace skills. - Injection (ADR-036) 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, ADR-043) 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_ 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
- Publisher trust levels, separate from support status.
Skill(Trust Level untrusted<community<verified<first_) classifies aparty Skill's provenance. It is explicitly notSource support_(which is not a safety signal, ADR-035). The level is denormalised onto eachstatus Skillat ingest exactly assupport_is, and an unknown/legacy stored value reads as the lowest level (status from). Injection and the allowed-tools union are gated against a configurable minimum trust level (String Or Untrusted skills., defaultmin Trust Level untrusted= accept every enabled skill):Skill— the single source of truth for both paths — drops any skill below the floor. This composes with, and does not replace, the existingComposer:: effective Skills () enabled = falsedefault:untrustedstill requires an explicit admin enable. - Manifest fingerprint over a full public-key signature. A source may
declare an
expected_: the sha256 its whole discovered skill set must hash to (a canonical, order-independent digest over thefingerprint identifier → body-pairs, degenerating to one entry for a single-file source).checksum Skillrecomputes it after collect and verifies withManifest Verifier hash_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).equals - Prompt-injection scanning at ingest.
Promptruns 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.Injection Scanner - Explicit instruction/data channel separation.
Skillkeeps the trusted guard preamble and additionally fences the untrusted bodies between explicitComposer BEGIN/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); the markers give the model an unambiguous boundary in-band.END UNTRUSTED SKILL DATA - Immutable import audit trail.
tx_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:nrllm_ skill_ audit SkillexposesAudit Repository recordand 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 - Per-group egress policies.
Egressdeclares a network-egress scope per tool group (ADR-043) as data, fail-closed: a group with no entry resolves toPolicy Service Tooland may make no outbound request. The one positive scope,Egress Scope:: NONE own_, resolves the instance's own site hosts throughsite Site— the allow-listingFinder probe_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.url
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 carriesprobe_) is grantedurl own_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.site - ✕ 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 and ADR-036 for the ingest / injection base, ADR-038 and ADR-043 for the tool runtime and groups, and the administration guide for operation.