"""Secret redaction for tool results (Phase F). Tool results are fed back to the LLM (in-app agent loop) or returned to an external MCP client, so they must never leak credentials. ``read_file`` and ``read_file_raw`` already redact through the shared file service, but other tools (``diff_backup``, ``search_*``) return content that has not been through the redactor. This module applies :func:`backend.secret_redactor.redact` to every string in a tool payload, recursively. """ from __future__ import annotations from typing import Any from backend.secret_redactor import redact _MAX_DEPTH = 12 def redact_payload(payload: Any, _depth: int = 0) -> Any: """Return a copy of *payload* with every string secret-redacted. Walks dicts, lists and tuples; scalars are returned unchanged. Strings are run through :func:`backend.secret_redactor.redact`. A recursion cap avoids pathological/cyclic structures (tool results are JSON-serialisable, so cycles should not occur, but the guard keeps this safe). """ if _depth > _MAX_DEPTH: return payload if isinstance(payload, str): redacted, count = redact(payload) return redacted if count else payload if isinstance(payload, dict): return {key: redact_payload(value, _depth + 1) for key, value in payload.items()} if isinstance(payload, list): return [redact_payload(item, _depth + 1) for item in payload] if isinstance(payload, tuple): return tuple(redact_payload(item, _depth + 1) for item in payload) return payload