BUG-HUNT: [domain-model-invariant] GuardrailAuditTrail _allowed_count/_denied_count declared as class variables instead of PrivateAttr #7750

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

Bug Report: Domain Model Invariant — GuardrailAuditTrail Counter Not Declared as PrivateAttr

Severity Assessment

  • Impact: _allowed_count and _denied_count are declared as class-level variables (not Pydantic PrivateAttr), meaning all instances share the class-level value before model_post_init assigns instance-level overrides. In Pydantic v2, class attributes starting with _ that are not PrivateAttr are NOT managed as instance attributes — they risk shared-state bugs between instances.
  • Likelihood: Medium
  • Priority: Medium

Location

  • File: src/cleveragents/domain/models/core/autonomy_guardrails.py
  • Class: GuardrailAuditTrail
  • Lines: ~100-120

Description

The GuardrailAuditTrail class declares _allowed_count: int = 0 and _denied_count: int = 0 as plain class-level attributes, not PrivateAttr. In Pydantic v2, attributes with underscore prefix that are not PrivateAttr are treated as ClassVar-like, not as per-instance private attributes. While model_post_init assigns to them per-instance, between __init__ and model_post_init any access to these attributes reads the class-level zero value. Furthermore, model_copy() triggers model_post_init on the copy, resetting counters from current entries — losing any historical counts of evicted entries.

Evidence

class GuardrailAuditTrail(BaseModel):
    entries: list[GuardrailAuditEntry] = Field(default_factory=list, ...)
    max_entries: int = Field(default=_DEFAULT_MAX_AUDIT_ENTRIES, ...)
    _allowed_count: int = 0    # CLASS-LEVEL, not PrivateAttr
    _denied_count: int = 0     # CLASS-LEVEL, not PrivateAttr

Expected Behavior

Pydantic PrivateAttr should be used for _allowed_count and _denied_count so they are properly managed as per-instance private attributes.

Actual Behavior

Declaration as class-level attributes risks shared-state bugs and is not the correct Pydantic v2 pattern for private per-instance state.

Suggested Fix

from pydantic import PrivateAttr

class GuardrailAuditTrail(BaseModel):
    entries: list[GuardrailAuditEntry] = Field(default_factory=list)
    max_entries: int = Field(default=_DEFAULT_MAX_AUDIT_ENTRIES, ge=1, le=1_000_000)
    _allowed_count: int = PrivateAttr(default=0)
    _denied_count: int = PrivateAttr(default=0)

Category

domain-model-invariant

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: Domain Model Invariant — GuardrailAuditTrail Counter Not Declared as PrivateAttr ### Severity Assessment - **Impact**: `_allowed_count` and `_denied_count` are declared as class-level variables (not Pydantic `PrivateAttr`), meaning all instances share the class-level value before `model_post_init` assigns instance-level overrides. In Pydantic v2, class attributes starting with `_` that are not `PrivateAttr` are NOT managed as instance attributes — they risk shared-state bugs between instances. - **Likelihood**: Medium - **Priority**: Medium ### Location - **File**: `src/cleveragents/domain/models/core/autonomy_guardrails.py` - **Class**: `GuardrailAuditTrail` - **Lines**: ~100-120 ### Description The `GuardrailAuditTrail` class declares `_allowed_count: int = 0` and `_denied_count: int = 0` as plain class-level attributes, not `PrivateAttr`. In Pydantic v2, attributes with underscore prefix that are not `PrivateAttr` are treated as ClassVar-like, not as per-instance private attributes. While `model_post_init` assigns to them per-instance, between `__init__` and `model_post_init` any access to these attributes reads the class-level zero value. Furthermore, `model_copy()` triggers `model_post_init` on the copy, resetting counters from current `entries` — losing any historical counts of evicted entries. ### Evidence ```python class GuardrailAuditTrail(BaseModel): entries: list[GuardrailAuditEntry] = Field(default_factory=list, ...) max_entries: int = Field(default=_DEFAULT_MAX_AUDIT_ENTRIES, ...) _allowed_count: int = 0 # CLASS-LEVEL, not PrivateAttr _denied_count: int = 0 # CLASS-LEVEL, not PrivateAttr ``` ### Expected Behavior Pydantic `PrivateAttr` should be used for `_allowed_count` and `_denied_count` so they are properly managed as per-instance private attributes. ### Actual Behavior Declaration as class-level attributes risks shared-state bugs and is not the correct Pydantic v2 pattern for private per-instance state. ### Suggested Fix ```python from pydantic import PrivateAttr class GuardrailAuditTrail(BaseModel): entries: list[GuardrailAuditEntry] = Field(default_factory=list) max_entries: int = Field(default=_DEFAULT_MAX_AUDIT_ENTRIES, ge=1, le=1_000_000) _allowed_count: int = PrivateAttr(default=0) _denied_count: int = PrivateAttr(default=0) ``` ### Category domain-model-invariant ### 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:43:59 +00:00
Author
Owner

Verified — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Domain model bug: GuardrailAuditTrail counts declared as class variables instead of PrivateAttr. 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#7750
No description provided.