---
title: "ADR-089: Guardrail boundary completeness — reasoning, system prompt, vision"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-089@0.35"
source: "Adr/Adr089GuardrailBoundaryCompleteness.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-089: Guardrail boundary completeness — reasoning, system prompt, vision

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

## Context

ADR-085/087 guard the model's answer (output) and the user turns (input). An
adversarial audit of the merged layer found the secret-redaction guardrail — a
defense against a model echoing a secret it was given, or a user pasting one —
missed three reachable boundaries, so the same secret masked in one place leaked
in another:

-   the model's **reasoning / \`\`thinking\`\` block** (surfaced in the playground
    glass-box, ADR-040) — screened on the answer, not the reasoning;
-   the **system prompt** — added after input screening, so it reached the
    provider unscreened on every path except `completeForConfiguration`;
-   the **vision** text prompt — `vision()` forwarded its text items to the
    provider without screening, on both sides.

## Decision

**Cover every boundary the same secret can cross.**

-   **Reasoning.** `GuardrailResult` gains an optional `redactedThinking`;
    `SecretRedactionGuardrail` masks both the content and the `thinking` block,
    and `GuardrailMiddleware` rebuilds the response with the redacted reasoning
    (null = leave as-is). Tool-call **arguments are deliberately not redacted** —
    they are functional parameters the tool consumes, and masking them would break
    the call.
-   **System prompt.** The six `applySystemPrompt()` call sites in
    `LlmServiceManager` route through `applyAndScreenSystemPrompt()`, which
    screens the *final assembled* message list — so the prepended system turn is
    screened too. Re-screening the already-screened user turns is an idempotent
    no-op.
-   **Vision.** `vision()` screens the text items of its `VisionContent` before
    dispatch, matching the chat/tool paths.

## Consequences

-   A secret is now masked (or the prompt blocked) across answer, reasoning,
    user turn, system prompt and vision text — the "enumerate all boundaries"
    completion of ADR-085/087.
-   `embed()` remains unscreened: its input is embedding text, not a chat
    prompt; screening/redacting it would corrupt the vector semantics. Documented
    as out of scope.
-   Redacting reasoning depends on the provider populating `thinking`
    (Ollama `message.thinking`, ADR-016); providers that fold reasoning into
    the content are covered by the content redaction already.
-   On the streaming paths the system prompt is screened inside the lazy opener
    (that is where `applySystemPrompt` runs), so a REDACT is applied before the
    provider send, but a DENY / REQUIRE_APPROVAL triggered *only* by system-prompt
    content would throw on first drain rather than at call time — unlike the eager
    user-turn screening. Latent: the shipped `SecretRedactionInputGuardrail` only
    REDACTs, which is correct here; a future DENY-returning input guardrail would
    need the effective system prompt hoisted and screened before the opener.
