Skip to content

fix(guards): two assertions that could not hold the property they name (#17673, #17650) - #17678

Merged
mrveiss merged 1 commit into
mainfrom
issue-17673-guard-assertions
Sep 28, 2026
Merged

mrveiss merged 1 commit into
mainfrom
issue-17673-guard-assertions

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

Both issues are the #15826 class — a claim about structure enforced by something that cannot
express structure
— and both were found by fixing a sibling and looking one function further.

#17673. #17667 replaced an order-blind substring assertion on the runner path. The identical
shape survived twelve lines up, on the versioning path, because CI only flagged the one my change
happened to break
. That is the mechanism by which #15826's population persists: a class gets
fixed at whichever instance fails, and its siblings inherit nothing.

The instruction I was given was to mirror the runner's positional assertion. Checking first showed
that would assert the wrong thing.
The runner's property is positional within one function —
redact before the extract loop. This one is aliasing: _apply_version_to_fact receives
target_version from its caller and must write the redacted value back through a subscript on
that dict
, because revert_to_version reads the same dict afterwards to record the revert. A
local-only redaction satisfies every reasonable reading of "it redacts" and leaves the second write
raw — the regression #13708's review actually found. So two assertions, not a mirror of one.

#17650. The brief said to change len(uncovered) == MAX_UNCOVERED_READS to <=. Reading the
issue and RATCHET_BASELINES.md showed that would be a regression: the doc holds exact congruence
in both directions
up as "the only one of the five satisfying staleness and blindness", and
the test's own message gives the reason — "headroom under a ceiling is room for a new bypass to
appear with nothing failing"
. The issue never asked for <= either; it says "the equality pin is
genuinely good and should stay"
.

The real defect is the other half of the convention stated at
python_filter_uncovered_reads.py:23: "and only ever goes down". That half was prose. Equality
makes MAX track the count in either direction, so a new bypass is made green by raising the
number — which is how it went 27 → 38 → 39 → 41 → 42 → 45.

What Changed

#17673 — repo_tests/kb_content_redaction_chokepoint_guard_test.py

Replaced assert 'target_version["content"] = redact_content(...)' in source with two AST
assertions: the redacting call's result is assigned back through a subscript on target_version
(not a local), and revert_to_version calls _apply_version_to_fact before create_version.

mutation new guard old substring
local-only redaction (the real regression) FAIL FAIL
redaction removed FAIL FAIL
record before applying — order swapped FAIL PASS
equivalent rewrite via a temp variable PASS FAIL

Wrong in both directions on two of four. It passed the order swap it was named for, and failed
an equivalent rewrite that preserved the property. A guard that fails a correct rewrite does not
merely under-detect — it punishes the right change, which is the argument for replacing it
rather than widening it.

#17650 — repo_tests/python_filter_covers_its_guards_test.py + python_filter_uncovered_reads.py

Kept ==. Added test_the_ceiling_may_not_rise_above_the_base_revision: MAX may not exceed its
value on the base revision, unless ACKNOWLEDGED_RAISE names exactly the current value and
cites an issue. That is the escape #17650's own AC2 asks for — "the criterion is that the
justification is recorded, not that raising is forbidden"
— and it is checked in both directions, so
a record that no longer matches MAX fails as stale and lowering MAX cannot leave a standing
permission behind it.

Verified across five states: bare raise fails · matching record passes · stale record fails · record
without an issue reference fails · restored passes.

Neither RATCHET_BASELINES.md (no exception mechanism; its checklist has no "how to raise" entry)
nor ARCHITECTURE_EXCEPTIONS.md (which records code-structure deviations, not baseline movements)
defines an allowance path, so the test carries the record itself. The preferred route is still the
one the ratchet exists to force — widen the filter until the read is covered, as #16442 did
("filter entry instead of a raised MAX").

A git-based check that cannot resolve the base SKIPS with a stated reason rather than passing.
Building a guard that reports clean having examined nothing, inside the fix for a guard that could
not hold its own direction, would be a pointed own goal.

The file split is payback, not a ceiling move. kb_content_redaction_chokepoint_guard_test.py
reached 629 lines, over the 600 ceiling, so the placement helpers moved to the new
repo_tests/_redaction_placement.py: 629 → 488, plus a 176-line module. No ceiling was raised
and no baseline entry was added — repo_tests/ is not in EXCLUDED_PREFIXES and the file is not in
KNOWN_LARGE.

Verification

  • Full repo_tests/ suite against the committed state: 4704 passed, 26 skipped, 3 xfailed, 0
    failed
    (9m35s). The baseline before this change was 4703; the delta is the new direction check.
  • Every mutation above was executed, not reasoned — each one applied to the real source, run, and
    reverted, with the file restored and re-verified green afterwards.
  • black, isort, flake8 clean on all four touched files.
  • The earlier run of this suite was discarded rather than cited: it started before the file split
    and the ACKNOWLEDGED_RAISE work, so its result described a tree that no longer existed.

Local counts are labelled local; CI is the authority.

One note for the next handover

A delta patch applied to this branch failed on first attempt, and the reusable form is: the patch
was built against c08bf1b45b and the worktree was not at c08bf1b45b.
Pre-commit's
end-of-file fixer had trimmed a trailing blank line after the earlier commit staged the file, so
the working tree diverged from its own HEAD by exactly the byte the patch used as trailing context.
Any handover into a worktree carrying an unnoticed fixer edit hits this.

Model Used

Claude Opus 5 (1M context)

Closes #17673
Closes #17650
Refs #15826, #17667, #13708, #16442, #17353

Summary by CodeRabbit

  • Tests
    • Strengthened automated checks to verify that sensitive content is redacted before processing and that reverted versions are redacted before being recorded.
    • Added safeguards to ensure any increase in the uncovered-read limit is explicitly acknowledged and remains consistent with the project’s baseline.

#17673, #17650)

Both are the #15826 class: a claim about structure enforced by something that
cannot express structure.

#17673 -- the versioning redaction assertion was the sibling of the one #17667
replaced, and survived because CI only flagged the one my change broke. Its
property is NOT the runner path's: that one is positional within a function, this
one is aliasing. `_apply_version_to_fact` receives `target_version` from its
caller and must write the redacted value back through a subscript on that dict,
because `revert_to_version` reads the same dict afterwards to record the revert.
A local-only redaction reads as correct and leaves the second write raw -- the
regression #13708's review found. So two assertions rather than a mirror of the
runner's one: the write goes through the caller's dict, AND the apply call
precedes the record call.

Mutation table, new guard vs the substring it replaces:

    local-only redaction (the real regression)   new FAIL   old FAIL
    redaction removed                            new FAIL   old FAIL
    record before applying (order swapped)       new FAIL   old PASS
    equivalent rewrite via a temp variable       new PASS   old FAIL

Wrong in both directions on two of four -- blind to a regression it names, loud
about a refactor that preserved the property.

#17650 -- `test_the_uncovered_record_only_shrinks` pins equality. The equality is
correct and documented: RATCHET_BASELINES.md holds exact congruence in both
directions up as the one exemplar satisfying staleness AND blindness, and headroom
under a ceiling is room for a new bypass to appear with nothing failing. The
missing half is the rest of the stated convention -- "and only ever goes down" --
which was prose. Equality makes MAX track the count in EITHER direction, so a new
bypass is made green by raising the number: 27 -> 38 -> 39 -> 41 -> 42 -> 45.

Added the direction check with the escape #17650's own criterion asks for
(justified, not forbidden): MAX may not exceed the base revision's value unless
`ACKNOWLEDGED_RAISE` names exactly the current value and cites an issue. Checked
in both directions -- a record that no longer matches MAX fails as stale, so
lowering MAX cannot leave a standing permission behind it. Verified across five
states: bare raise fails, matching record passes, stale record fails, record
without an issue reference fails, restored passes.

Neither RATCHET_BASELINES.md nor ARCHITECTURE_EXCEPTIONS.md defines an allowance
path for a baseline raise, so this test carries the record itself. The preferred
route is still the one the ratchet exists to force: widen the filter until the
read is covered, as #16442 did.

A git-based check that cannot resolve the base SKIPS with a stated reason rather
than passing -- building a guard that reports clean having examined nothing,
inside the fix for a guard that could not hold its own direction, would be a
pointed own goal.

`kb_content_redaction_chokepoint_guard_test.py` reached 629 lines, over the 600
ceiling, so the placement helpers moved to `repo_tests/_redaction_placement.py`
rather than the ceiling moving: 629 -> 488, plus a 176-line module.

Refs #17673, #17650, #15826, #17667, #13708, #16442
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The changes add AST-based checks for redaction placement in extract and version-revert flows. They also add a test that compares the uncovered-read limit with its value on a configured base revision and validates any recorded acknowledgement.

Changes

Redaction guard checks

Layer / File(s) Summary
Structural redaction checks
repo_tests/_redaction_placement.py, repo_tests/kb_content_redaction_chokepoint_guard_test.py
AST checks verify that redaction occurs before the extract loop, that version content is redacted in place, and that version application occurs before create_version. The guard test imports and uses these checks instead of the removed local helper and substring assertion.

Uncovered-read limit guard

Layer / File(s) Summary
Baseline limit comparison
repo_tests/python_filter_uncovered_reads.py, repo_tests/python_filter_covers_its_guards_test.py
The test reads MAX_UNCOVERED_READS from origin/main or main. It skips if it cannot obtain a base value. An acknowledged increase must match the current limit and include an issue reference; without an acknowledgement, the current limit must not exceed the base value.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 48468

Production code is unchanged. However, the new guard tests can pass in situations they are meant to catch, so later regressions in secret redaction or an increase in the uncovered-read limit could go unnoticed. Tightening these checks is recommended, either before merge or as a prompt follow-up.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 48468

The changes strengthen security checks without changing production behavior. The new limit check can be skipped when a base revision is unavailable, so its protection is not assured in every build environment.

Retained concerns

  • Low · security · inferred: The new shrink-only check does not enforce its direction rule when neither base ref resolves. It also accepts the first resolvable ref without establishing that it is the intended current base; CI ref guarantees are unknown.
Security review details

Security Blast Radius

  • inferred — The identified PR-specific exposure is limited to enforcement of repository security guards. No new attacker-facing runtime entrypoint or expanded access to the Redis-backed version store is established.

Security Findings and Attack Paths

  • inferred — A raised uncovered-read limit can evade the new direction check in a checkout with no resolvable base ref; a stale or untrusted resolvable ref can make its comparison misleading. The existing exact-count check still applies, and neither CI condition nor a resulting bypass is established for this PR.

Trust Boundaries and Controls

  • observed — The Git call fixes the candidate ref names, uses the repository root, and scrubs ambient Git environment variables. Those controls do not themselves authenticate the refs or prevent the test from skipping when they are absent.

Resilience and Maintainability Implications

  • observed — The unchanged revert path writes the live projection before recording its new version and does not inspect the recording result. Its partial-failure and retry consistency gap predates these test changes; the new guards address content ordering, not transition atomicity.

Hardening Proposals

  • proposed — Make the direction check fail when it cannot obtain an authenticated, current base revision, or establish and document an equivalent CI guarantee for the selected ref.
🚥 Pre-merge checks | ✅ 4 | ❓ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The implementation satisfies the main coding objectives. For #17673, _redaction_placement.py uses AST checks for in-place redaction and call order. It also documents the positional limitation of `RE… Provide the sweep results for #17673. State which matching assertions were found, and fix or justify each one. Record the result on the issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the pull request. It identifies guard assertions that did not enforce their stated properties, which matches the redaction-placement and uncovered-read baseline changes.
Out of Scope Changes check ✅ Passed The changed files support the two linked objectives. The helper extraction supports the structural redaction guards for #17673. The baseline lookup and acknowledgement declaration support the monotoni…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 4 files.
Full details: Linked Issues check

Explanation

The implementation satisfies the main coding objectives. For #17673, _redaction_placement.py uses AST checks for in-place redaction and call order. It also documents the positional limitation of REDACTING_CALLS. The PR summary reports mutation checks for removal, reordering, and equivalent spelling changes. For #17650, the equality pin remains, and the new test compares MAX_UNCOVERED_READS with a base revision and validates ACKNOWLEDGED_RAISE. The available evidence does not establish the required sweep of similar in source assertions across repo_tests/, or its recorded disposition on the issue.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a135081b-d787-4124-8d9d-72a105ac3035

📥 Commits

Reviewing files that changed from the base of the PR and between 7adaca8 and 484689c.

📒 Files selected for processing (4)
  • repo_tests/_redaction_placement.py
  • repo_tests/kb_content_redaction_chokepoint_guard_test.py
  • repo_tests/python_filter_covers_its_guards_test.py
  • repo_tests/python_filter_uncovered_reads.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment on lines +70 to +80
for node in ast.walk(fn):
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
continue
callee = _callee_name(node.value.func)
if callee not in REDACTING_CALLS:
continue
targets = [t for tgt in node.targets for t in (tgt.elts if isinstance(tgt, ast.Tuple) else [tgt])]
if not any(isinstance(t, ast.Name) and t.id == "input_data" for t in targets):
continue
if node.lineno < first_loop:
return True, ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude uncalled nested functions from the extract-path check.

ast.walk(fn) also visits assignments inside nested functions. If the only matching assignment is in an uncalled nested function before the extract loop, this helper returns True although the extract tasks receive unsanitised input_data. Limit the check to statements on the extract path, and add that mutation as a negative fixture. As per path instructions, “Every detector needs a contrast pair: a fixture that SHOULD trip it and one that should not.”

Source: Path instructions

Comment on lines +125 to +135
if _callee_name(node.value.func) not in REDACTING_CALLS:
continue
for target in node.targets:
if (
isinstance(target, ast.Subscript)
and isinstance(target.value, ast.Name)
and target.value.id == "target_version"
and isinstance(target.slice, ast.Constant)
and target.slice.value == "content"
):
return True, ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Check the value passed to the redacting call.

The target and callee checks accept target_version["content"] = redact_content(other_content). The helper then reports in-place redaction even though it replaced the version content with a value from another source. Check that the redacting call receives target_version["content"], or explicitly recognise an equivalent data flow.

Comment on lines +127 to +135
for target in node.targets:
if (
isinstance(target, ast.Subscript)
and isinstance(target.value, ast.Name)
and target.value.id == "target_version"
and isinstance(target.slice, ast.Constant)
and target.slice.value == "content"
):
return True, ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require redaction before the Redis write.

This helper returns True if the assignment moves below the hset call in autobot-backend/knowledge/versioning.py. In that order, hset writes the raw target_version["content"] to the live fact before the assignment runs. Check that the redacting assignment precedes the consuming write, not just that the assignment exists.

Comment on lines +164 to +172
calls = [(c.lineno, _callee_name(c.func)) for c in ast.walk(fn) if isinstance(c, ast.Call)]
applies = [ln for ln, name in calls if name == "_apply_version_to_fact"]
records = [ln for ln, name in calls if name == "create_version"]
if not applies:
return False, "revert_to_version no longer calls _apply_version_to_fact at all"
if not records:
return False, "revert_to_version no longer calls create_version -- re-derive this guard"
if min(applies) < min(records):
return True, ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require the apply call to be awaited before recording.

Removing await from revert_to_version leaves both call line numbers unchanged, so this helper returns True. _apply_version_to_fact then produces a coroutine instead of mutating target_version, and create_version can record the raw content. Check that the relevant apply call is awaited before the record call.

Comment on lines +296 to +297
out = subprocess.run(
["git", "show", f"{ref}:{rel}"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Compare with the branch’s merge-base revision.

git show origin/main:… reads the current branch tip of origin/main, not the revision where this branch diverged. If this branch raises MAX_UNCOVERED_READS from 45 to 46 and origin/main later rises to 47, the test accepts 46 without an acknowledgement. Resolve the merge-base commit and read the value from that commit instead.

🧰 Tools
🪛 ast-grep (0.45.3)

[error] 295-302: Command coming from incoming request
Context: subprocess.run(
["git", "show", f"{ref}:{rel}"],
cwd=repo_root(),
capture_output=True,
text=True,
env=scrubbed_git_env(),
timeout=30,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.6)

[error] 296-296: subprocess call: check for execution of untrusted input

(S603)


[warning] 296-296: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


[error] 297-297: Starting a process with a partial executable path

(S607)

Comment on lines +333 to +335
base, why = _max_on_base()
if base is None:
pytest.skip(f"cannot compare against the base revision, so direction is UNCHECKED: {why}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not skip the direction guard when the base is unavailable.

If neither configured reference resolves, pytest.skip leaves a raised MAX_UNCOVERED_READS unchecked. This can occur in a shallow checkout without a local main reference. Make the gating test fail when it cannot read a base value, and arrange for the checkout to supply the required history.

Comment on lines +344 to +345
assert issue.strip().startswith("#"), f"ACKNOWLEDGED_RAISE must cite an issue, got {issue!r}"
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require an issue identifier in ACKNOWLEDGED_RAISE.

issue.strip().startswith("#") accepts "#" and "#not-an-issue". With either value, the test returns before the direction assertion, so a raise passes without the required issue reference. Validate the whole reference, for example with re.fullmatch(r"#\d+", issue.strip()).

@mrveiss
mrveiss merged commit 1b1777f into main Sep 28, 2026
72 checks passed
@mrveiss
mrveiss deleted the issue-17673-guard-assertions branch September 28, 2026 06:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant