ADR 089 · Accepted
Guardrail boundary completeness — reasoning, system prompt, vision
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.