[AUTO-BUG-5] merge_invariants() ignores non_overridable flag on GLOBAL invariants — plan-level invariants silently bypass security constraints #10468

Open
opened 2026-04-18 10:00:06 +00:00 by HAL9000 · 0 comments
Owner

Metadata

Field Value
Branch bugfix/m3-merge-invariants-non-overridable
Commit Message fix(domain/invariant): enforce non_overridable flag in merge_invariants() to prevent plan-level override of GLOBAL security invariants
Milestone v3.2.0

Background and Context

The Invariant domain model in src/cleveragents/domain/models/core/invariant.py defines a non_overridable field (lines 86–92) with the following documented contract:

non_overridable: bool = Field(
    default=False,
    description=(
        "When True and scope is GLOBAL, this invariant cannot be "
        "overridden by lower-scope invariants during reconciliation"
    ),
)

This field is intended to protect critical system-wide security constraints (e.g., "no external network access", "no credential exfiltration") from being silently bypassed by plan-level or project-level invariants.

However, the merge_invariants() function (lines 166–197) never checks this flag. It iterates plan → project → global in precedence order and uses a seen set to de-duplicate by text. A plan-level invariant with the same text as a non_overridable=True GLOBAL invariant will silently override it — the global version is excluded from the result.

Current Behavior

# src/cleveragents/domain/models/core/invariant.py, lines 185–197
seen: set[str] = set()
result: list[Invariant] = []

for inv_list in (plan_invariants, project_invariants, global_invariants):
    for inv in inv_list:
        if not inv.active:
            continue
        key = inv.text.strip().lower()
        if key not in seen:  # ← non_overridable is NEVER checked
            seen.add(key)
            result.append(inv)

return result

Reproduction:

from cleveragents.domain.models.core.invariant import Invariant, InvariantScope, merge_invariants

global_inv = Invariant(
    text="no external network access",
    scope=InvariantScope.GLOBAL,
    non_overridable=True,
    source="security-policy",
)
plan_inv = Invariant(
    text="no external network access",
    scope=InvariantScope.PLAN,
    non_overridable=False,
    source="plan-override",
)

result = merge_invariants(
    plan_invariants=[plan_inv],
    project_invariants=[],
    global_invariants=[global_inv],
)

# BUG: result contains plan_inv (scope=PLAN), not global_inv (scope=GLOBAL, non_overridable=True)
# The non_overridable GLOBAL invariant was silently dropped
assert result[0].scope == InvariantScope.GLOBAL  # FAILS

Expected Behavior

When merge_invariants() encounters a GLOBAL invariant with non_overridable=True, it must ensure that invariant is included in the result regardless of whether a plan-level or project-level invariant with the same text exists. The non_overridable GLOBAL invariant should take precedence over lower-scope invariants with the same text.

The corrected logic should:

  1. First collect all GLOBAL invariants with non_overridable=True and add them to seen and result unconditionally
  2. Then process plan → project → global in normal precedence order, skipping any invariant whose text is already in seen

Impact

  • Security bypass: Critical system-wide constraints (e.g., network isolation, credential protection) can be silently overridden by plan-level invariants, defeating the purpose of the non_overridable flag
  • Spec violation: The specification requires "Aggregate roots must enforce invariants" and "Business rules must be enforced in domain layer, not application layer" — the non_overridable contract is a domain business rule that is not being enforced
  • Silent failure: No exception is raised; the bug is invisible to callers

Acceptance Criteria

  • merge_invariants() checks the non_overridable flag on GLOBAL invariants
  • A GLOBAL invariant with non_overridable=True is always included in the result, even if a plan-level or project-level invariant with the same text exists
  • A GLOBAL invariant with non_overridable=False continues to follow normal plan > project > global precedence
  • The TDD scenario from issue #10379 passes after the fix is applied
  • All existing invariant merge tests continue to pass
  • Coverage ≥ 97%

Subtasks

  • Read merge_invariants() in src/cleveragents/domain/models/core/invariant.py (lines 166–197)
  • Refactor merge_invariants() to first add all GLOBAL non_overridable=True invariants to seen and result
  • Then process plan → project → global in normal precedence order, skipping already-seen texts
  • Remove @tdd_expected_fail tag from the TDD scenario in issue #10379 after the fix is applied
  • Run nox -s unit_tests to confirm all tests pass
  • Verify coverage ≥ 97% via nox -s coverage_report
  • Run nox (all default sessions), fix any errors

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch bugfix/m3-merge-invariants-non-overridable.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.

Duplicate Check

Searched open issues (pages 1–3, 50 per page) and closed issues (pages 1–3, 50 per page) for keywords: non_overridable, non-overridable, invariant override, merge_invariants, GLOBAL invariant. No existing issues found covering this specific bug.


Automated by CleverAgents Bot
Agent: new-issue-creator

## Metadata | Field | Value | |---|---| | **Branch** | `bugfix/m3-merge-invariants-non-overridable` | | **Commit Message** | `fix(domain/invariant): enforce non_overridable flag in merge_invariants() to prevent plan-level override of GLOBAL security invariants` | | **Milestone** | v3.2.0 | ## Background and Context The `Invariant` domain model in `src/cleveragents/domain/models/core/invariant.py` defines a `non_overridable` field (lines 86–92) with the following documented contract: ```python non_overridable: bool = Field( default=False, description=( "When True and scope is GLOBAL, this invariant cannot be " "overridden by lower-scope invariants during reconciliation" ), ) ``` This field is intended to protect critical system-wide security constraints (e.g., "no external network access", "no credential exfiltration") from being silently bypassed by plan-level or project-level invariants. However, the `merge_invariants()` function (lines 166–197) **never checks this flag**. It iterates plan → project → global in precedence order and uses a `seen` set to de-duplicate by text. A plan-level invariant with the same text as a `non_overridable=True` GLOBAL invariant will silently override it — the global version is excluded from the result. ## Current Behavior ```python # src/cleveragents/domain/models/core/invariant.py, lines 185–197 seen: set[str] = set() result: list[Invariant] = [] for inv_list in (plan_invariants, project_invariants, global_invariants): for inv in inv_list: if not inv.active: continue key = inv.text.strip().lower() if key not in seen: # ← non_overridable is NEVER checked seen.add(key) result.append(inv) return result ``` **Reproduction:** ```python from cleveragents.domain.models.core.invariant import Invariant, InvariantScope, merge_invariants global_inv = Invariant( text="no external network access", scope=InvariantScope.GLOBAL, non_overridable=True, source="security-policy", ) plan_inv = Invariant( text="no external network access", scope=InvariantScope.PLAN, non_overridable=False, source="plan-override", ) result = merge_invariants( plan_invariants=[plan_inv], project_invariants=[], global_invariants=[global_inv], ) # BUG: result contains plan_inv (scope=PLAN), not global_inv (scope=GLOBAL, non_overridable=True) # The non_overridable GLOBAL invariant was silently dropped assert result[0].scope == InvariantScope.GLOBAL # FAILS ``` ## Expected Behavior When `merge_invariants()` encounters a GLOBAL invariant with `non_overridable=True`, it must ensure that invariant is included in the result regardless of whether a plan-level or project-level invariant with the same text exists. The `non_overridable` GLOBAL invariant should take precedence over lower-scope invariants with the same text. The corrected logic should: 1. First collect all GLOBAL invariants with `non_overridable=True` and add them to `seen` and `result` unconditionally 2. Then process plan → project → global in normal precedence order, skipping any invariant whose text is already in `seen` ## Impact - **Security bypass**: Critical system-wide constraints (e.g., network isolation, credential protection) can be silently overridden by plan-level invariants, defeating the purpose of the `non_overridable` flag - **Spec violation**: The specification requires "Aggregate roots must enforce invariants" and "Business rules must be enforced in domain layer, not application layer" — the `non_overridable` contract is a domain business rule that is not being enforced - **Silent failure**: No exception is raised; the bug is invisible to callers ## Acceptance Criteria - [ ] `merge_invariants()` checks the `non_overridable` flag on GLOBAL invariants - [ ] A GLOBAL invariant with `non_overridable=True` is always included in the result, even if a plan-level or project-level invariant with the same text exists - [ ] A GLOBAL invariant with `non_overridable=False` continues to follow normal plan > project > global precedence - [ ] The TDD scenario from issue #10379 passes after the fix is applied - [ ] All existing invariant merge tests continue to pass - [ ] Coverage ≥ 97% ## Subtasks - [ ] Read `merge_invariants()` in `src/cleveragents/domain/models/core/invariant.py` (lines 166–197) - [ ] Refactor `merge_invariants()` to first add all GLOBAL `non_overridable=True` invariants to `seen` and `result` - [ ] Then process plan → project → global in normal precedence order, skipping already-seen texts - [ ] Remove `@tdd_expected_fail` tag from the TDD scenario in issue #10379 after the fix is applied - [ ] Run `nox -s unit_tests` to confirm all tests pass - [ ] Verify coverage ≥ 97% via `nox -s coverage_report` - [ ] Run `nox` (all default sessions), fix any errors ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch `bugfix/m3-merge-invariants-non-overridable`. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done. ## Duplicate Check Searched open issues (pages 1–3, 50 per page) and closed issues (pages 1–3, 50 per page) for keywords: `non_overridable`, `non-overridable`, `invariant override`, `merge_invariants`, `GLOBAL invariant`. No existing issues found covering this specific bug. --- **Automated by CleverAgents Bot** Agent: new-issue-creator
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#10468
No description provided.