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:
Guardrail screens every non-streaming Completion 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
Provider (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 check
returns the same Guardrail verdict type as the output side.
Implementers are auto-collected through the nr_ DI tag —
separate from nr_ 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
Input runs the input guardrails over the message list inside
Llm — 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
Guardrail / Guardrail 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 —
chat, chat,
stream — which is where chat / stream
funnel once a default configuration resolves (ADR-034). The raw-string
complete (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 Chat and legacy array messages; a message with no string
content (an assistant tool-call turn) passes through untouched.
Secret ships as the reference implementation,
applying the same secret masking to the prompt that Secret
applies to the response (shared via Redacts). They are separate
classes because a single class cannot implement both Guardrail and
Input — their TAG_ 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
Provider— was rejected to keep the context payload-free (ADR-026); the trade is two locations instead of one.Call Context - Screening covers both the configuration-driven surface and the ad-hoc
pinned-provider-key path. The ad-hoc branches of
chat/() complete/() chat/With Tools () stream(an explicit provider, no DB configuration — the ADR-034 escape hatch) screen the prompt as well, so every message-carrying send path is covered.Chat () - Input guardrails support ALLOW / REDACT / DENY / REQUIRE_APPROVAL. RETRY is ignored on the input side.