BUG-HUNT: [consistency] redact_dict() returns a shallow copy when show_secrets=True — caller mutations to nested objects silently corrupt the original dict #6613

Open
opened 2026-04-09 22:17:36 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Consistency — redact_dict() Shallow Copy Contract Violation

Severity Assessment

  • Impact: When show_secrets=True (debug mode / --show-secrets flag), redact_dict() returns dict(data) — a shallow copy. Callers that modify nested objects in the returned dict unknowingly mutate the original data structure. This can corrupt in-memory domain objects (e.g., plan metadata, error details, config dicts) if the returned "safe" dict is ever modified downstream.
  • Likelihood: Low in today's call sites (current callers iterate the result read-only), but the broken contract is a latent correctness hazard for any future caller that treats the return value as a fully independent copy.
  • Priority: Priority/Backlog

Location

  • File: src/cleveragents/shared/redaction.py
  • Function: redact_dict
  • Line: 166

Description

redact_dict() documents that it returns "a new dict" (Returns: A new dict with sensitive values replaced by ***REDACTED***.). This contract holds when show_secrets=False (the normal path) because _redact_dict_inner builds a completely new dictionary tree. However, when show_secrets=True, the function takes a fast path:

# src/cleveragents/shared/redaction.py, line 165–166
if show_secrets:
    return dict(data)   # ← shallow copy only!

dict(data) creates a new top-level dict object, but all values remain shared references to the original objects. Mutations to nested objects in the returned dict affect the original.

Evidence

from cleveragents.shared.redaction import redact_dict, set_show_secrets

set_show_secrets(True)   # e.g. --show-secrets CLI flag

original = {
    "plan_id": "p-123",
    "metadata": {"cost": 0.042, "provider": "openai"},
}

result = redact_dict(original)   # caller thinks this is a safe copy

# Caller modifies the returned dict (perfectly reasonable):
result["metadata"]["provider"] = "MUTATED"

# Original is now corrupted:
assert original["metadata"]["provider"] == "MUTATED"   # True — BUG

The same issue arises at any call site that modifies the returned dict's nested structure:

# src/cleveragents/cli/commands/auto_debug.py, line 251
safe = redact_dict(e.details)
for key, value in safe.items():      # read-only today — safe
    console.print(...)

If any future caller does safe["nested"]["field"] = x, the original e.details dict (which may be stored in the exception object or audit log) is silently mutated.

Expected Behavior

redact_dict() should always return a fully independent copy of the input dict. Mutations to the returned dict must never affect the original, regardless of the show_secrets flag.

Actual Behavior

When show_secrets=True, redact_dict() returns a shallow copy. Mutating nested dicts or lists in the returned value mutates the original data.

Inconsistency Note

The show_secrets=False path via _redact_dict_inner does produce a deep independent copy (it builds result = {} from scratch recursively). Only the show_secrets=True fast path is broken.

Suggested Fix

Replace the shallow dict(data) fast path with a proper deep copy:

import copy

if show_secrets:
    return copy.deepcopy(data)

Or, if performance is a concern for large dicts:

if show_secrets:
    # Build a true independent copy using the same recursive machinery
    # but without any redaction substitutions
    return _copy_dict_deep(data)

Category

consistency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Consistency — `redact_dict()` Shallow Copy Contract Violation ### Severity Assessment - **Impact**: When `show_secrets=True` (debug mode / `--show-secrets` flag), `redact_dict()` returns `dict(data)` — a **shallow copy**. Callers that modify nested objects in the returned dict unknowingly mutate the original data structure. This can corrupt in-memory domain objects (e.g., plan metadata, error details, config dicts) if the returned "safe" dict is ever modified downstream. - **Likelihood**: Low in today's call sites (current callers iterate the result read-only), but the broken contract is a latent correctness hazard for any future caller that treats the return value as a fully independent copy. - **Priority**: Priority/Backlog ### Location - **File**: `src/cleveragents/shared/redaction.py` - **Function**: `redact_dict` - **Line**: 166 ### Description `redact_dict()` documents that it returns **"a new dict"** (`Returns: A new dict with sensitive values replaced by ***REDACTED***.`). This contract holds when `show_secrets=False` (the normal path) because `_redact_dict_inner` builds a completely new dictionary tree. However, when `show_secrets=True`, the function takes a fast path: ```python # src/cleveragents/shared/redaction.py, line 165–166 if show_secrets: return dict(data) # ← shallow copy only! ``` `dict(data)` creates a new top-level dict object, but **all values remain shared references** to the original objects. Mutations to nested objects in the returned dict affect the original. ### Evidence ```python from cleveragents.shared.redaction import redact_dict, set_show_secrets set_show_secrets(True) # e.g. --show-secrets CLI flag original = { "plan_id": "p-123", "metadata": {"cost": 0.042, "provider": "openai"}, } result = redact_dict(original) # caller thinks this is a safe copy # Caller modifies the returned dict (perfectly reasonable): result["metadata"]["provider"] = "MUTATED" # Original is now corrupted: assert original["metadata"]["provider"] == "MUTATED" # True — BUG ``` The same issue arises at any call site that modifies the returned dict's nested structure: ```python # src/cleveragents/cli/commands/auto_debug.py, line 251 safe = redact_dict(e.details) for key, value in safe.items(): # read-only today — safe console.print(...) ``` If any future caller does `safe["nested"]["field"] = x`, the original `e.details` dict (which may be stored in the exception object or audit log) is silently mutated. ### Expected Behavior `redact_dict()` should always return a fully independent copy of the input dict. Mutations to the returned dict must never affect the original, regardless of the `show_secrets` flag. ### Actual Behavior When `show_secrets=True`, `redact_dict()` returns a shallow copy. Mutating nested dicts or lists in the returned value mutates the original data. ### Inconsistency Note The `show_secrets=False` path via `_redact_dict_inner` **does** produce a deep independent copy (it builds `result = {}` from scratch recursively). Only the `show_secrets=True` fast path is broken. ### Suggested Fix Replace the shallow `dict(data)` fast path with a proper deep copy: ```python import copy if show_secrets: return copy.deepcopy(data) ``` Or, if performance is a concern for large dicts: ```python if show_secrets: # Build a true independent copy using the same recursive machinery # but without any redaction substitutions return _copy_dict_deep(data) ``` ### Category `consistency` ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 22:25:28 +00:00
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#6613
No description provided.