---
title: "ADR-087: Input-side guardrails — screening and redacting the outgoing prompt"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-087@0.35"
source: "Adr/Adr087InputGuardrails.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-087: Input-side guardrails — screening and redacting the outgoing prompt

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

## Context

ADR-085 established the guardrail pipeline for provider *responses*:
`GuardrailMiddleware` screens every non-streaming `CompletionResponse` and a
guardrail returns an ALLOW / REDACT / DENY / RETRY / REQUIRE_APPROVAL verdict.
It screens output only, and said so: the prompt payload is not on the
`ProviderCallContext` (ADR-026 keeps the context payload-free), so a
middleware cannot see — let alone rewrite — the outgoing messages.

The prompt is untrusted too. A user can paste a secret (an API key, a
credential-bearing URL) that should not be forwarded verbatim to a third-party
provider, and an operator may want to block a prompt on policy before spending a
call.

## Decision

**An input guardrail is an \`\`InputGuardrailInterface\`\`** whose `checkInput()`
returns the same `GuardrailResult` verdict type as the output side.
Implementers are auto-collected through the `nr_llm.input_guardrail` DI tag —
separate from `nr_llm.guardrail` so an output-only guardrail (e.g. the
provider content-filter, which reacts to a response attribute) is not forced to
implement a meaningless input check.

**Screening runs on the send path, not in the pipeline.** An
`InputGuardrailScreener` runs the input guardrails over the message list inside
`LlmServiceManager` — before the pipeline, where the messages are reachable.
This is the key difference from the output side: because the screener holds the
payload, a **REDACT verdict rewrites the prompt in place** (a middleware could
not). A DENY / REQUIRE_APPROVAL throws the same
`GuardrailViolationException` / `GuardrailApprovalRequiredException` the
output side uses, so a caller handles both identically; RETRY (re-ask the
provider) has no meaning before the call and is ignored.

Screening is applied at the configuration-driven entry points —
`chatWithConfiguration`, `chatWithToolsForConfiguration`,
`streamChatWithConfiguration` — which is where `chat()` / `streamChat()`
funnel once a default configuration resolves (ADR-034). The raw-string
`completeWithConfiguration` (where `complete()` funnels) takes a prompt
string rather than a message list, so it screens through a string variant that
wraps, screens, and unwraps the prompt. For streaming, screening runs before the
opener captures the messages, so a redaction reaches the provider and a DENY
throws at call time, not on first iteration. The screener handles both
typed `ChatMessage` and legacy array messages; a message with no string
content (an assistant tool-call turn) passes through untouched.

`SecretRedactionInputGuardrail` ships as the reference implementation,
applying the same secret masking to the prompt that `SecretRedactionGuardrail`
applies to the response (shared via `RedactsSecretsTrait`). They are separate
classes because a single class cannot implement both `GuardrailInterface` and
`InputGuardrailInterface` — their `TAG_NAME` constants would collide.

## Consequences

-   The prompt is screened and can be redacted before it leaves the extension —
    the input complement of ADR-085.
-   Guardrail execution is deliberately split: output in the pipeline (ADR-085),
    input on the send path (here). The alternative — threading the payload onto
    `ProviderCallContext` — was rejected to keep the context payload-free
    (ADR-026); the trade is two locations instead of one.
-   Screening covers both the configuration-driven surface and the ad-hoc
    pinned-provider-key path. The ad-hoc branches of `chat()` / `complete()` /
    `chatWithTools()` / `streamChat()` (an explicit provider, no DB
    configuration — the ADR-034 escape hatch) screen the prompt as well, so every
    message-carrying send path is covered.
-   Input guardrails support ALLOW / REDACT / DENY / REQUIRE_APPROVAL. RETRY is
    ignored on the input side.
