BUG-HUNT: [error-handling] wrap_unexpected() mutates the input CleverAgentsError exception's .details attribute #6386

Open
opened 2026-04-09 20:59:46 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Error Handling — wrap_unexpected() Mutates Exception Object

Severity Assessment

  • Impact: Any code that passes a CleverAgentsError to wrap_unexpected() more than once (e.g., in a layered exception-handling chain) will have its .details dict silently accumulate context from each call. Downstream code that inspects or logs exc.details may see stale or duplicated context, leading to misleading diagnostics and potential data leakage of context values across error boundaries.
  • Likelihood: Medium — exception re-wrapping at multiple layers is a common pattern in service code. Any call stack where the same exception object is passed to wrap_unexpected() more than once will reproduce this.
  • Priority: Medium

Location

  • File: src/cleveragents/core/error_handling.py
  • Function: wrap_unexpected
  • Lines: 291–298

Description

The wrap_unexpected() docstring states:

"Create a new dict so the original details dict object is not mutated."

While a new dict object is indeed constructed, the function then reassigns exc.details on the original exception object:

if isinstance(exc, CleverAgentsError):
    if extra_context:
        # Create a new dict so the original details object is not mutated.
        exc.details = {**exc.details, **extra_context}   # ← BUG: mutates exc
    return exc

The dict value is not mutated in-place (correct), but the exception object itself has its .details attribute overwritten. If the same exception object travels up through multiple wrap_unexpected() calls (each adding different context), the details dict accumulates all layers of context:

# Demonstration
exc = CleverAgentsError("test", {"key": "original"})

# Layer 1 (e.g., repository layer)
wrap_unexpected(exc, context={"repo": "plans"})
# exc.details is now {"key": "original", "repo": "plans"}

# Layer 2 (e.g., service layer)
wrap_unexpected(exc, context={"service": "plan_service"})
# exc.details is now {"key": "original", "repo": "plans", "service": "plan_service"}
# ↑ Context from layer 1 is now permanently embedded in exc.details

This also means that if exc is caught by a third-party error handler that inspects .details, it will see context from every intermediate layer — breaking the isolation of diagnostic information.

Expected Behavior

Each call to wrap_unexpected() should be non-destructive. A new CleverAgentsError should be returned (or the exception should not be mutated), consistent with the docstring's stated intent.

Actual Behavior

The .details attribute of the input CleverAgentsError is overwritten on every call to wrap_unexpected() that supplies context. The exception object is mutated despite documentation claiming otherwise.

Suggested Fix

Return a new CleverAgentsError instance instead of mutating the existing one:

if isinstance(exc, CleverAgentsError):
    if extra_context:
        # Return a NEW exception with merged details to avoid mutating the original.
        new_details = {**exc.details, **extra_context}
        new_exc = CleverAgentsError(exc.message, details=new_details)
        new_exc.__cause__ = exc
        return new_exc
    return exc

Category

error-handling

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: Error Handling — `wrap_unexpected()` Mutates Exception Object ### Severity Assessment - **Impact**: Any code that passes a `CleverAgentsError` to `wrap_unexpected()` more than once (e.g., in a layered exception-handling chain) will have its `.details` dict silently accumulate context from each call. Downstream code that inspects or logs `exc.details` may see stale or duplicated context, leading to misleading diagnostics and potential data leakage of context values across error boundaries. - **Likelihood**: Medium — exception re-wrapping at multiple layers is a common pattern in service code. Any call stack where the same exception object is passed to `wrap_unexpected()` more than once will reproduce this. - **Priority**: Medium ### Location - **File**: `src/cleveragents/core/error_handling.py` - **Function**: `wrap_unexpected` - **Lines**: 291–298 ### Description The `wrap_unexpected()` docstring states: > *"Create a new dict so the original details dict object is not mutated."* While a **new dict object** is indeed constructed, the function then **reassigns `exc.details`** on the original exception object: ```python if isinstance(exc, CleverAgentsError): if extra_context: # Create a new dict so the original details object is not mutated. exc.details = {**exc.details, **extra_context} # ← BUG: mutates exc return exc ``` The dict **value** is not mutated in-place (correct), but the **exception object itself** has its `.details` attribute overwritten. If the same exception object travels up through multiple `wrap_unexpected()` calls (each adding different `context`), the details dict accumulates all layers of context: ```python # Demonstration exc = CleverAgentsError("test", {"key": "original"}) # Layer 1 (e.g., repository layer) wrap_unexpected(exc, context={"repo": "plans"}) # exc.details is now {"key": "original", "repo": "plans"} # Layer 2 (e.g., service layer) wrap_unexpected(exc, context={"service": "plan_service"}) # exc.details is now {"key": "original", "repo": "plans", "service": "plan_service"} # ↑ Context from layer 1 is now permanently embedded in exc.details ``` This also means that if `exc` is caught by a third-party error handler that inspects `.details`, it will see context from every intermediate layer — breaking the isolation of diagnostic information. ### Expected Behavior Each call to `wrap_unexpected()` should be non-destructive. A new `CleverAgentsError` should be returned (or the exception should not be mutated), consistent with the docstring's stated intent. ### Actual Behavior The `.details` attribute of the input `CleverAgentsError` is overwritten on every call to `wrap_unexpected()` that supplies `context`. The exception object is mutated despite documentation claiming otherwise. ### Suggested Fix Return a new `CleverAgentsError` instance instead of mutating the existing one: ```python if isinstance(exc, CleverAgentsError): if extra_context: # Return a NEW exception with merged details to avoid mutating the original. new_details = {**exc.details, **extra_context} new_exc = CleverAgentsError(exc.message, details=new_details) new_exc.__cause__ = exc return new_exc return exc ``` ### Category `error-handling` ### 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 21:09:21 +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#6386
No description provided.