shared/redaction: _redact_dict_inner() doesn't detect circular references — infinite recursion on self-referential dicts #10579

Open
opened 2026-04-18 17:38:02 +00:00 by HAL9000 · 0 comments
Owner

Metadata

Commit: HEAD (current working tree)
Branch: main

Background and Context

The _redact_dict_inner() function in src/cleveragents/shared/redaction.py (lines 170-189) recursively processes nested dictionaries without tracking visited objects. If a dictionary contains a circular reference (e.g., a dict that references itself), the function will infinitely recurse and eventually crash with a RecursionError.

This is a critical issue for any code path that processes user-supplied or dynamically-generated data structures that might contain circular references.

Expected Behavior

The redact_dict() and _redact_dict_inner() functions should gracefully handle circular references without:

  • Causing infinite recursion
  • Raising RecursionError
  • Losing data or producing incorrect output

When a circular reference is encountered, the function should either:

  1. Return an empty dict for the circular reference, or
  2. Raise a clear, informative ValueError indicating a circular reference was detected

Acceptance Criteria

  • _redact_dict_inner() tracks visited objects using a visited set keyed by id()
  • Circular references are detected before recursion occurs
  • No RecursionError is raised when processing self-referential dicts
  • The reproduction case below completes without crashing
  • Unit tests cover circular reference scenarios (self-reference, mutual references, deep cycles)
  • Behavior is documented in docstrings

Subtasks

  • Add visited: set[int] | None = None parameter to _redact_dict_inner()
  • Initialize visited = set() in redact_dict() before calling _redact_dict_inner()
  • Add circular reference detection logic at the start of _redact_dict_inner()
  • Update recursive calls to pass visited set
  • Write unit tests for self-referential dicts
  • Write unit tests for mutually-referential dicts
  • Update function docstrings

Reproduction

from cleveragents.shared import redact_dict

# Create a circular reference
data = {"key": "value"}
data["self"] = data  # Self-reference

# This will cause infinite recursion and RecursionError
try:
    result = redact_dict(data)
    print("Success:", result)
except RecursionError as e:
    print(f"Crashed with RecursionError: {e}")

Proposed Fix

def redact_dict(
    data: dict[str, Any],
    *,
    show_secrets: bool | None = None,
) -> dict[str, Any]:
    if show_secrets is None:
        show_secrets = get_show_secrets()
    if show_secrets:
        return dict(data)
    return _redact_dict_inner(data, visited=set())

def _redact_dict_inner(
    data: dict[str, Any],
    visited: set[int] | None = None,
) -> dict[str, Any]:
    if visited is None:
        visited = set()
    
    # Check for circular reference
    obj_id = id(data)
    if obj_id in visited:
        return {}  # or raise ValueError("Circular reference detected")
    
    visited.add(obj_id)
    result: dict[str, Any] = {}
    
    for key, value in data.items():
        if isinstance(value, dict):
            result[key] = _redact_dict_inner(value, visited=visited)
        elif isinstance(value, list):
            result[key] = _redact_list_inner(value, visited=visited)
        else:
            result[key] = value
    
    visited.discard(obj_id)  # Remove from visited after processing
    return result

Definition of Done

  • Code changes merged to main
  • All unit tests pass (including new circular reference tests)
  • Test coverage remains ≥ 97%
  • No RecursionError occurs on circular reference inputs
  • Docstrings updated with circular reference handling notes
  • Issue closed and linked to PR

Automated by CleverAgents Bot
Agent: new-issue-creator

## Metadata **Commit:** HEAD (current working tree) **Branch:** main ## Background and Context The `_redact_dict_inner()` function in `src/cleveragents/shared/redaction.py` (lines 170-189) recursively processes nested dictionaries without tracking visited objects. If a dictionary contains a circular reference (e.g., a dict that references itself), the function will infinitely recurse and eventually crash with a `RecursionError`. This is a critical issue for any code path that processes user-supplied or dynamically-generated data structures that might contain circular references. ## Expected Behavior The `redact_dict()` and `_redact_dict_inner()` functions should gracefully handle circular references without: - Causing infinite recursion - Raising `RecursionError` - Losing data or producing incorrect output When a circular reference is encountered, the function should either: 1. Return an empty dict for the circular reference, or 2. Raise a clear, informative `ValueError` indicating a circular reference was detected ## Acceptance Criteria - [ ] `_redact_dict_inner()` tracks visited objects using a `visited` set keyed by `id()` - [ ] Circular references are detected before recursion occurs - [ ] No `RecursionError` is raised when processing self-referential dicts - [ ] The reproduction case below completes without crashing - [ ] Unit tests cover circular reference scenarios (self-reference, mutual references, deep cycles) - [ ] Behavior is documented in docstrings ## Subtasks - [ ] Add `visited: set[int] | None = None` parameter to `_redact_dict_inner()` - [ ] Initialize `visited = set()` in `redact_dict()` before calling `_redact_dict_inner()` - [ ] Add circular reference detection logic at the start of `_redact_dict_inner()` - [ ] Update recursive calls to pass `visited` set - [ ] Write unit tests for self-referential dicts - [ ] Write unit tests for mutually-referential dicts - [ ] Update function docstrings ## Reproduction ```python from cleveragents.shared import redact_dict # Create a circular reference data = {"key": "value"} data["self"] = data # Self-reference # This will cause infinite recursion and RecursionError try: result = redact_dict(data) print("Success:", result) except RecursionError as e: print(f"Crashed with RecursionError: {e}") ``` ## Proposed Fix ```python def redact_dict( data: dict[str, Any], *, show_secrets: bool | None = None, ) -> dict[str, Any]: if show_secrets is None: show_secrets = get_show_secrets() if show_secrets: return dict(data) return _redact_dict_inner(data, visited=set()) def _redact_dict_inner( data: dict[str, Any], visited: set[int] | None = None, ) -> dict[str, Any]: if visited is None: visited = set() # Check for circular reference obj_id = id(data) if obj_id in visited: return {} # or raise ValueError("Circular reference detected") visited.add(obj_id) result: dict[str, Any] = {} for key, value in data.items(): if isinstance(value, dict): result[key] = _redact_dict_inner(value, visited=visited) elif isinstance(value, list): result[key] = _redact_list_inner(value, visited=visited) else: result[key] = value visited.discard(obj_id) # Remove from visited after processing return result ``` ## Definition of Done - [ ] Code changes merged to main - [ ] All unit tests pass (including new circular reference tests) - [ ] Test coverage remains ≥ 97% - [ ] No `RecursionError` occurs on circular reference inputs - [ ] Docstrings updated with circular reference handling notes - [ ] Issue closed and linked to PR --- **Automated by CleverAgents Bot** Agent: new-issue-creator
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#10579
No description provided.