BUG-HUNT: [error-handling] wrap_unexpected mutates CleverAgentsError.details attribute in-place #7740

Open
opened 2026-04-12 03:22:28 +00:00 by HAL9000 · 3 comments
Owner

Bug Report: Error Handling — wrap_unexpected Mutates Exception Object In-Place

Severity Assessment

  • Impact: When wrap_unexpected is called on an existing CleverAgentsError, it reassigns exc.details on the live exception object. If the same exception instance is caught-and-re-raised through multiple error handling layers, each layer's context bleeds into the exception, corrupting the error state. Stored exception references also see unexpected mutations.
  • Likelihood: Medium — occurs whenever a CleverAgentsError is caught, wrapped with context, and re-raised in nested error handlers.
  • Priority: Medium

Location

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

Description

The wrap_unexpected function is designed to wrap exceptions with additional context. For CleverAgentsError instances, it merges extra_context into the error's details. The code comment claims it does NOT mutate the original details object — but it does mutate the exception object itself by reassigning exc.details.

The original dict object referenced by exc.details is not modified in-place (correct), but the exc.details attribute on the exception instance IS overwritten. This is still mutation of the exception object, just one level up from the dict.

This becomes a problem in multi-layer error handling:

  1. Layer A catches exc, calls wrap_unexpected(exc, context={'layer': 'A'})exc.details now contains {'layer': 'A'}
  2. Layer A re-raises exc
  3. Layer B catches the same exc object, calls wrap_unexpected(exc, context={'layer': 'B'})exc.details now contains {'layer': 'A', 'layer': 'B'} (merged, with B's context overwriting A's if keys collide)

Context from layer A unexpectedly appears in layer B's error reporting, and vice versa.

Evidence

# src/cleveragents/core/error_handling.py lines 294-298
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: exc.details IS mutated
    return exc

The comment is misleading: it correctly notes a new dict object is created ({**exc.details, **extra_context}), but then assigns that new dict back to exc.details, which mutates the exception object's attribute.

Expected Behavior

wrap_unexpected should not modify the original exception object at all. It should either:

  • Return the exception as-is (without injecting context) if it's already a CleverAgentsError, OR
  • Create a new CleverAgentsError wrapping the original with merged details

Actual Behavior

The exc.details attribute of the passed-in exception object is overwritten, mutating a shared mutable object. Multiple exception handling layers accumulate and overwrite each other's context on the same exception instance.

Suggested Fix

Create a new exception or return a wrapper that does not modify the original:

if isinstance(exc, CleverAgentsError):
    if extra_context:
        # Return a new exception with merged details; do not mutate exc.
        merged = {**exc.details, **extra_context}
        new_exc = CleverAgentsError(exc.message, details=merged)
        new_exc.__cause__ = exc
        return new_exc
    return exc

Or, if mutation is intentional, the misleading comment should be removed and the API clearly documented as mutating.

Category

error-handling

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD.


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

## Bug Report: Error Handling — wrap_unexpected Mutates Exception Object In-Place ### Severity Assessment - **Impact**: When `wrap_unexpected` is called on an existing `CleverAgentsError`, it reassigns `exc.details` on the live exception object. If the same exception instance is caught-and-re-raised through multiple error handling layers, each layer's context bleeds into the exception, corrupting the error state. Stored exception references also see unexpected mutations. - **Likelihood**: Medium — occurs whenever a `CleverAgentsError` is caught, wrapped with context, and re-raised in nested error handlers. - **Priority**: Medium ### Location - **File**: `src/cleveragents/core/error_handling.py` - **Function/Class**: `wrap_unexpected` - **Lines**: 294–298 ### Description The `wrap_unexpected` function is designed to wrap exceptions with additional context. For `CleverAgentsError` instances, it merges `extra_context` into the error's details. The code comment claims it does NOT mutate the original details object — but it **does** mutate the exception object itself by reassigning `exc.details`. The original `dict` object referenced by `exc.details` is not modified in-place (correct), but the `exc.details` **attribute** on the exception instance IS overwritten. This is still mutation of the exception object, just one level up from the dict. This becomes a problem in multi-layer error handling: 1. Layer A catches exc, calls `wrap_unexpected(exc, context={'layer': 'A'})` → `exc.details` now contains `{'layer': 'A'}` 2. Layer A re-raises exc 3. Layer B catches the same exc object, calls `wrap_unexpected(exc, context={'layer': 'B'})` → `exc.details` now contains `{'layer': 'A', 'layer': 'B'}` (merged, with B's context overwriting A's if keys collide) Context from layer A unexpectedly appears in layer B's error reporting, and vice versa. ### Evidence ```python # src/cleveragents/core/error_handling.py lines 294-298 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: exc.details IS mutated return exc ``` The comment is misleading: it correctly notes a new dict object is created (`{**exc.details, **extra_context}`), but then assigns that new dict back to `exc.details`, which mutates the exception object's attribute. ### Expected Behavior `wrap_unexpected` should not modify the original exception object at all. It should either: - Return the exception as-is (without injecting context) if it's already a `CleverAgentsError`, OR - Create a new `CleverAgentsError` wrapping the original with merged details ### Actual Behavior The `exc.details` attribute of the passed-in exception object is overwritten, mutating a shared mutable object. Multiple exception handling layers accumulate and overwrite each other's context on the same exception instance. ### Suggested Fix Create a new exception or return a wrapper that does not modify the original: ```python if isinstance(exc, CleverAgentsError): if extra_context: # Return a new exception with merged details; do not mutate exc. merged = {**exc.details, **extra_context} new_exc = CleverAgentsError(exc.message, details=merged) new_exc.__cause__ = exc return new_exc return exc ``` Or, if mutation is intentional, the misleading comment should be removed and the API clearly documented as mutating. ### Category error-handling ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-12 03:46:40 +00:00
Author
Owner

Verified — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: wrap_unexpected mutates error details in-place. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
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#7740
No description provided.