BUG-HUNT: [data-loss] ActionRepository.update() silently discards safety_profile changes #6449

Open
opened 2026-04-09 21:04:16 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: Data Loss — ActionRepository.update() does not persist safety_profile changes

Severity Assessment

  • Impact: Updating an Action's safety_profile via ActionRepository.update() has no effect. The new value is silently discarded. Any caller that updates an action's safety profile (e.g. agents action edit) will write to the domain model but the change will never reach the database.
  • Likelihood: Triggered any time a user or service updates an action with a non-default safety_profile. This is a complete data-loss path: no error, no warning, data just disappears.
  • Priority: High

Location

  • File: src/cleveragents/infrastructure/database/repositories.py
  • Function/Class: ActionRepository.update()
  • Lines: ~1060–1185 (the field-assignment block starting at ~line 1086)

Description

LifecycleActionModel has a safety_profile_json column (defined in models.py at line 287). The from_domain() classmethod correctly serializes it (models.py lines 439–460):

safety_profile_json: str | None = None
if getattr(action, "safety_profile", None) is not None:
    safety_profile_json = action.safety_profile.model_dump_json()
model = cls(
    ...
    safety_profile_json=safety_profile_json,
    ...
)

However, ActionRepository.update() iterates through all mutable fields and never assigns row.safety_profile_json. The complete list of fields that ARE updated (repositories.py ~lines 1090–1114) includes:

row.namespace = ns.namespace
row.name = ns.name
row.description = action.description
row.long_description = action.long_description
row.definition_of_done = action.definition_of_done
row.strategy_actor = action.strategy_actor
row.execution_actor = action.execution_actor
row.estimation_actor = action.estimation_actor
row.review_actor = action.review_actor
row.apply_actor = getattr(action, "apply_actor", None)
row.invariant_actor = getattr(action, "invariant_actor", None)
row.automation_profile = getattr(action, "automation_profile", None)
row.inputs_schema_json = inputs_json
row.state = ...
row.reusable = action.reusable
row.read_only = action.read_only
row.created_by = action.created_by
row.tags_json = _json.dumps(action.tags)
row.updated_at = datetime.now().isoformat()

row.safety_profile_json is absent from this list.


Evidence

From models.py:

# LifecycleActionModel
safety_profile_json = Column(Text, nullable=True)  # line 287

@classmethod
def from_domain(cls, action: Any) -> LifecycleActionModel:
    ...
    safety_profile_json: str | None = None
    if getattr(action, "safety_profile", None) is not None:
        safety_profile_json = action.safety_profile.model_dump_json()  # lines 439-441
    model = cls(
        ...
        safety_profile_json=safety_profile_json,  # line 460
        ...
    )

From repositories.py (ActionRepository.update(), ~lines 1090–1114):

# All field assignments shown here — safety_profile_json is MISSING
row.namespace = ns.namespace
row.name = ns.name
...
row.inputs_schema_json = inputs_json      # <-- present
# row.safety_profile_json = ???          # <-- MISSING
row.state = ...
row.reusable = action.reusable

Expected Behavior

When ActionRepository.update(action) is called with an Action that has a safety_profile, the safety_profile_json column should be serialized and written to the database, exactly as from_domain() does.

Actual Behavior

safety_profile_json is never touched in update(). The database retains the previous value (or NULL if this is the first time a profile is set on an action that was created before safety profiles existed). The in-memory Action object has the new safety profile, but the DB does not.


Suggested Fix

Add the missing assignment inside ActionRepository.update() after the existing row.inputs_schema_json = inputs_json line:

# Serialize safety profile
_safety_json: str | None = None
if getattr(action, "safety_profile", None) is not None:
    _safety_json = action.safety_profile.model_dump_json()
row.safety_profile_json = _safety_json  # type: ignore[assignment]

Category

data-loss / type-safety / spec-alignment


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: Data Loss — `ActionRepository.update()` does not persist `safety_profile` changes ### Severity Assessment - **Impact**: Updating an `Action`'s `safety_profile` via `ActionRepository.update()` has **no effect**. The new value is silently discarded. Any caller that updates an action's safety profile (e.g. `agents action edit`) will write to the domain model but the change will never reach the database. - **Likelihood**: Triggered any time a user or service updates an action with a non-default `safety_profile`. This is a complete data-loss path: no error, no warning, data just disappears. - **Priority**: High ### Location - **File**: `src/cleveragents/infrastructure/database/repositories.py` - **Function/Class**: `ActionRepository.update()` - **Lines**: ~1060–1185 (the field-assignment block starting at ~line 1086) --- ### Description `LifecycleActionModel` has a `safety_profile_json` column (defined in `models.py` at line 287). The `from_domain()` classmethod **correctly serializes** it (models.py lines 439–460): ```python safety_profile_json: str | None = None if getattr(action, "safety_profile", None) is not None: safety_profile_json = action.safety_profile.model_dump_json() model = cls( ... safety_profile_json=safety_profile_json, ... ) ``` However, `ActionRepository.update()` iterates through all mutable fields and **never assigns** `row.safety_profile_json`. The complete list of fields that ARE updated (repositories.py ~lines 1090–1114) includes: ```python row.namespace = ns.namespace row.name = ns.name row.description = action.description row.long_description = action.long_description row.definition_of_done = action.definition_of_done row.strategy_actor = action.strategy_actor row.execution_actor = action.execution_actor row.estimation_actor = action.estimation_actor row.review_actor = action.review_actor row.apply_actor = getattr(action, "apply_actor", None) row.invariant_actor = getattr(action, "invariant_actor", None) row.automation_profile = getattr(action, "automation_profile", None) row.inputs_schema_json = inputs_json row.state = ... row.reusable = action.reusable row.read_only = action.read_only row.created_by = action.created_by row.tags_json = _json.dumps(action.tags) row.updated_at = datetime.now().isoformat() ``` **`row.safety_profile_json` is absent from this list.** --- ### Evidence From `models.py`: ```python # LifecycleActionModel safety_profile_json = Column(Text, nullable=True) # line 287 @classmethod def from_domain(cls, action: Any) -> LifecycleActionModel: ... safety_profile_json: str | None = None if getattr(action, "safety_profile", None) is not None: safety_profile_json = action.safety_profile.model_dump_json() # lines 439-441 model = cls( ... safety_profile_json=safety_profile_json, # line 460 ... ) ``` From `repositories.py` (`ActionRepository.update()`, ~lines 1090–1114): ```python # All field assignments shown here — safety_profile_json is MISSING row.namespace = ns.namespace row.name = ns.name ... row.inputs_schema_json = inputs_json # <-- present # row.safety_profile_json = ??? # <-- MISSING row.state = ... row.reusable = action.reusable ``` --- ### Expected Behavior When `ActionRepository.update(action)` is called with an `Action` that has a `safety_profile`, the `safety_profile_json` column should be serialized and written to the database, exactly as `from_domain()` does. ### Actual Behavior `safety_profile_json` is never touched in `update()`. The database retains the **previous value** (or `NULL` if this is the first time a profile is set on an action that was created before safety profiles existed). The in-memory `Action` object has the new safety profile, but the DB does not. --- ### Suggested Fix Add the missing assignment inside `ActionRepository.update()` after the existing `row.inputs_schema_json = inputs_json` line: ```python # Serialize safety profile _safety_json: str | None = None if getattr(action, "safety_profile", None) is not None: _safety_json = action.safety_profile.model_dump_json() row.safety_profile_json = _safety_json # type: ignore[assignment] ``` --- ### Category `data-loss` / `type-safety` / `spec-alignment` --- ### 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
Author
Owner

Verified — Critical data-loss bug. Safety profile changes are silently discarded on update. MoSCoW: Must Have — data loss in safety-critical field.


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

✅ **Verified** — Critical data-loss bug. Safety profile changes are silently discarded on update. **MoSCoW: Must Have** — data loss in safety-critical field. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
HAL9000 added this to the v3.5.0 milestone 2026-04-17 08:44:20 +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#6449
No description provided.