Skip to content

refactor(redaction): name the redaction boundary and pin the concept census (#16688) - #17335

Merged
mrveiss merged 11 commits into
mainfrom
issue-16688-fork-collapse
Sep 24, 2026
Merged

mrveiss merged 11 commits into
mainfrom
issue-16688-fork-collapse

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

#16688 asks which of four parallel redaction implementations is canonical. I measured before choosing, and the measurement changed the question twice.

There are seven, not four. A census rule run over all 3,392 tracked production sources found three modules that independently decide what a secret looks like and that no issue had counted:

module own vocabulary size
chat_workflow/cot_events.py _SENSITIVE_KEY_FRAGMENTS 13 nouns
llc/services/portability.py _SECRET_LIKE_KEYS 13 nouns
services/config_revision_service.py _SECRET_SUBSTRINGS 5 nouns

A fourth uncounted one, autobot_shared/security/redaction.py, also declares itself the one canonical redactor (#12242, "a redactor is a security control, so there is exactly ONE implementation of it"). Two modules in autobot_shared/ each claiming to be the single canonical redactor is the fork problem in its sharpest form.

And one of the original four is not an implementation at all. security/chat_message_safety.py declares no detector — it composes pii_pipeline.scrub_outbound and the shared injection detector. It is the shape the other seven should converge on, so the guard must not count it. #17312's table scored it at "5 sensitive categories" by counting keyword mentions in its prose; that file itself flags the method as indicative rather than proof, and reading the source is where it comes apart.

So there is no single canonical redactor, and there should not be one. What was missing was never a winner — it was the boundary: one module per shape of the problem. Six independent answers to one question ("is this key name sensitive?") with word lists of 19, 13, 13, 8, 5 and 8 meant which module a caller imported decided whether client_secret or bearer_token reached a log.

The decision I did not make

secret_redaction.is_credential_field matches by suffix; security/redaction.redact_mapping matches by substring. On a 10-name sample they disagree on 7 — substring fires on monkey_patch, tls_key_path, password_hash_algorithm, private_ip, passenger_count, certainty_score, tokenizers_parallelism. Each rule is a documented ruling in its own file, and each is right for its call sites: over-masking is free in a log line and destroys the diagnostic in SSOT config's __repr__. Merging them reverses one recorded ruling and silently changes behaviour at the other's call sites, so this PR documents the split and leaves it open rather than guessing. Filed separately.

What Changed

New — docs/developer/REDACTION_BOUNDARY.md (AC 1). The census table, a which-one-do-I-call table keyed by what you're holding, where to add a new detector, and why the duplication guard cannot see any of this. Indexed from CLAUDE.md; every module in the census points at it except portability.py, which sits on its grandfathered 936-line ceiling (#5060) and would be pushed over by the note — recorded in the doc rather than raising the ceiling.

New — repo_tests/redaction_concept_census_test.py (AC 3). Pins the seven.

Deliberate deviation from the AC's wording: it asks for a grep-based guard. This is AST-based. A text grep for a secret noun matches comments, docstrings and event names — it produced 12 hits on first measurement, including a Redis key prefix ("chat:tokens:") and a placeholder (_REDACTED = "<redacted>"). The rule counts a module only when it both exposes a redact/scrub/mask entry point and declares a detector — a compiled pattern naming a secret, a ≥3-noun vocabulary, or a secret/PII enum. That last clause is load-bearing: pii_pipeline keeps its vocabulary in PIIType and builds regexes inside _build_detectors(), so a module-level-assignment rule misses the single largest implementation in the tree.

Changed — two behaviours, both strictly widening. A redaction merge that narrows is a silent under-redaction, so direction was the constraint throughout:

  • secret_redaction._JWT_RE required eyJ on the payload as well as the header, making it narrower than a2a/pii_pipeline.py, which anchors the header alone. A payload starts eyJ only when its JSON begins exactly {"; a serializer emitting a space after the brace gives eyAi. Such tokens were blocked on the A2A path and passed through here — in the module llm_shared.credential_redaction delegates its log redaction to. Now header-anchored, matching pii_pipeline.
  • credential_redaction.SENSITIVE_KEYS no longer hand-copies the credential nouns; it derives from secret_redaction.CREDENTIAL_SUFFIXES. As a union, not a replacement — that vocabulary carries no authorization terms, and a straight swap would have stopped redacting Authorization, auth_header and x_auth. Verified: 0 lost, 7 gained.

Verification

Pre-push suite, green (this is what gates the push; [pre-push OK] pytest: all relevant tests pass):

222 passed  ->  after fixture fix: all relevant tests pass

The census rule, executed against the real tree (its own functions loaded from the guard file, not a paraphrase):

swept 3392 sources -> detected 7
NEW (would fail the guard): none
STALE (in census, no longer detected): none
unparseable: none

The rule fires on drift, not on similarity (#17312 AC 4 — a planted fork sharing no 8-line run with any real module):

PASS  drifted fork (shares no text with any real module)   expected=True  got=True
PASS  pure composition                                     expected=False got=False
PASS  detector vocab, no redaction API                     expected=False got=False
PASS  real chat_message_safety.py counted?                 expected=False got=False

The union direction, measured:

lost from pre-16688 set: NONE
sensitive keys NOT matched: NONE      (15 probed, incl. Authorization/auth_header/x_auth)
ordinary keys wrongly matched: NONE   (5 probed)

A failure worth reporting. The first version of the JWT test asserted pattern == "jwt" on a fixture using {} → e30. That is 3 characters — below the {8,} this scanner requires and below pii_pipeline's {10,}, so neither rule matched it and high_entropy claimed the span. The pre-push suite caught it. The divergence is real, the example was not; the fixture is now a token verified matched by the new rule and unmatched by the old one. The loose assertion (assert matches) would have passed throughout — the precise one is what surfaced it, and the source comment carried the same wrong example and is corrected with it.

Why MAX_DUP_LINES will not move. jscpd -k 70 -l 8 finds zero clone lines across these modules: they are forks, and a fork shares no 8-line run. The pin neither rises when an eighth lands nor falls when one is collapsed — that blind spot is #17312, and the census is the answer to it for this concept.

Not verified locally: this machine sits below the declared dependency floors (104 of 209), so the full CI suite is the authority on anything the pre-push subset does not cover.

Model Used

Claude Opus 5 (1M context)


All three acceptance criteria delivered, with the AC-3 deviation stated above.

Closes #16688
Refs #17312
Refs #12242

Summary by CodeRabbit

  • Bug Fixes

    • Improved JWT detection so valid tokens with payloads that use a different prefix are also recognised and redacted.
    • Kept credential-key redaction aligned with shared sensitive-key patterns while preserving protection for authorisation-related keys.
  • Documentation

    • Added guidance on where redaction rules are managed and how to identify the appropriate redaction capability.
    • Updated threat model references to match the current code locations.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4971a3fa-dfee-4de0-9ce7-9067f49daedc

📥 Commits

Reviewing files that changed from the base of the PR and between df4273e and 2b8b20f.

📒 Files selected for processing (8)
  • autobot-backend/a2a/pii_pipeline.py
  • autobot-backend/chat_workflow/cot_events.py
  • autobot-backend/llm_shared/credential_redaction.py
  • autobot-backend/security/chat_message_safety.py
  • autobot-backend/services/config_revision_service.py
  • autobot_shared/secret_redaction.py
  • autobot_shared/secret_redaction_test.py
  • autobot_shared/security/redaction.py

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


📝 Walkthrough

Walkthrough

The changes document redactor ownership, update credential-key matching to use a shared vocabulary, widen JWT payload matching, and add a repository test that checks the census of detector-owning modules.

Changes

Redaction boundary and detection

Layer / File(s) Summary
Document redactor ownership
docs/developer/REDACTION_BOUNDARY.md, CLAUDE.md, autobot-backend/a2a/pii_pipeline.py, autobot-backend/chat_workflow/cot_events.py, autobot-backend/llm_shared/credential_redaction.py, autobot-backend/security/chat_message_safety.py, autobot-backend/services/config_revision_service.py, autobot_shared/secret_redaction.py, autobot_shared/security/redaction.py, docs/developer/THREAT_MODEL.md
The new boundary document describes detector ownership and redactor selection. Navigation and module documentation link to it. The Threat Model references updated function line numbers.
Update credential-key and JWT detection
autobot-backend/llm_shared/credential_redaction.py, autobot-backend/llm_shared/tests/test_credential_redaction.py, autobot_shared/secret_redaction.py, autobot_shared/secret_redaction_test.py
Credential-key matching derives from shared credential suffixes and retains authorization terms. JWT matching accepts payload segments that do not start with eyJ. Tests cover both changes.
Pin the detector-owning module census
repo_tests/redaction_concept_census_test.py
The new repository test scans tracked production Python files, checks the pinned detector census, and tests classification and parse-failure cases.

Priority: ➖ Normal

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning #16688 requires three coding outcomes. docs/developer/REDACTION_BOUNDARY.md documents ownership by problem shape. repo_tests/redaction_concept_census_test.py pins seven implementations with an AST… Migrate at least one directly affected module or caller onto the selected redactor. Add the required grep-based guard, or record an approved change to that acceptance criterion. Update the boundary document and tests to reflect the complete…
Out of Scope Changes check ⚠️ Warning #16688 states that redesigning detector patterns is out of scope. autobot_shared/secret_redaction.py widens _JWT_RE and adds JWT matching and redaction tests. This changes detector behaviour rathe… Remove the JWT pattern change and its related tests from this pull request, or link the change to a coding requirement that covers JWT detector behaviour.
Docstring Coverage ⚠️ Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 23 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main changes: naming the redaction boundary and adding a concept census guard.
Full details: Linked Issues check

Explanation

#16688 requires three coding outcomes. docs/developer/REDACTION_BOUNDARY.md documents ownership by problem shape. repo_tests/redaction_concept_census_test.py pins seven implementations with an AST-based guard. However, no redaction module or caller is migrated onto another redactor. llm_shared/credential_redaction.py still declares API_KEY_PATTERNS, and the census still counts it. The guard is AST-based, not the required grep-based guard.

Resolution

Migrate at least one directly affected module or caller onto the selected redactor. Add the required grep-based guard, or record an approved change to that acceptance criterion. Update the boundary document and tests to reflect the completed migration and guard.

Full details: Out of Scope Changes check

Explanation

#16688 states that redesigning detector patterns is out of scope. autobot_shared/secret_redaction.py widens _JWT_RE and adds JWT matching and redaction tests. This changes detector behaviour rather than documenting ownership or consolidating callers.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 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.

@mrveiss

mrveiss commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

python-suite shards 5/6/8/10 addressed — pushed as f58793659b

Posting so the sweeps stop re-reporting it.

Cause — in this branch's diff, not base

threat_model_anchors_resolve_test failed. The boundary pointer this PR adds to autobot_shared/security/redaction.py's module docstring inserts five lines above every definition in the file, and the citations in docs/developer/THREAT_MODEL.md still pointed at the old numbers.

Base agrees with itself — checked before assuming:

git show origin/main:autobot_shared/security/redaction.py | grep -n "def redact_provider_error"   -> 140
git show origin/main:docs/developer/THREAT_MODEL.md       | grep -n redact_provider_error         -> 116: ... (:140)

Four anchors moved, not one

The failing test names only the anchor whose shard reached it first, so fixing the reported one would have left three and produced another red run under a different name. Every citation in the document was re-resolved with the test's own rule — AST definition lookup, not a line regex:

anchor was now
redact_text :97 :102
redact_mapping :109 :114
redact_cloud_identifiers :126 :131
redact_provider_error :140 :145

Only redact_provider_error was reported; redact_text sits on the preceding line of the doc and would not have been caught by checking the reported line alone.

Two anchors that look broken and are not

A definition-only probe also flagged import_module (:550) and spec_from_file_location (:619) in loader.py. Those are stdlib call sites, and the test has an explicit branch for anchors with no definition in the file — both are named in its comment. The probe was wrong, not the doc; "fixing" them would have broken two correct anchors.

Scope check

This PR adds a docstring pointer to eight modules, so the sweep covered all 38 anchors rather than only redaction.py. The other seven are unaffected — either unanchored or anchored above the insertion point.

Verification

repo_tests/threat_model_anchors_resolve_test.py ... 41 passed

THREAT_MODEL.md stays at 140 lines, which is its own ceiling (test_document_exists_and_is_short_enough_to_read_every_review asserts <= 140) — this change edits numbers in place and adds none.

Root-caused by autobot-ai-f8 while sweeping, including the base-vs-branch attribution check. The widening from one anchor to four is this commit.

@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: 9


ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5d8aa3b3-7a76-46a7-9faa-40f5bcf85364

📥 Commits

Reviewing files that changed from the base of the PR and between 3131039 and f587936.

📒 Files selected for processing (14)
  • CLAUDE.md
  • autobot-backend/a2a/pii_pipeline.py
  • autobot-backend/chat_workflow/cot_events.py
  • autobot-backend/llm_shared/credential_redaction.py
  • autobot-backend/llm_shared/tests/test_credential_redaction.py
  • autobot-backend/security/chat_message_safety.py
  • autobot-backend/services/config_revision_service.py
  • autobot_shared/secret_redaction.py
  • autobot_shared/secret_redaction_test.py
  • autobot_shared/security/redaction.py
  • docs/developer/REDACTION_BOUNDARY.md
  • docs/developer/THREAT_MODEL.md
  • repo_tests/audio_extension_allowlist_test.py
  • repo_tests/redaction_concept_census_test.py

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

Comment on lines +56 to +58
_AUTHORIZATION_KEYS = frozenset({"bearer", "auth", "authorization"})

SENSITIVE_KEYS = frozenset(CREDENTIAL_SUFFIXES) | _AUTHORIZATION_KEYS

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 | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '35,120p' autobot-backend/llm_shared/credential_redaction.py
sed -n '138,205p' autobot-backend/llm_shared/tests/test_credential_redaction.py
sed -n '65,110p' autobot_shared/secret_redaction.py

Repository: mrveiss/AutoBot-AI

Length of output: 7692


Separate legacy fragments from suffix matching.

redact_dict currently uses substring matching for every SENSITIVE_KEYS entry. Since key is now in CREDENTIAL_SUFFIXES, ordinary keys such as keyboard, keyframe, and monkey can have their values redacted. Apply suffix matching only to newly introduced suffixes. Retain the legacy literal matching so auth_header, apikey, and authtoken remain covered.

Suggested fix
+_LEGACY_SENSITIVE_KEYS = frozenset(
+    {"apikey", "token", "bearer", "password", "secret", "credential", "auth", "authorization"}
+)
+_SUFFIX_SENSITIVE_KEYS = frozenset(CREDENTIAL_SUFFIXES) - _LEGACY_SENSITIVE_KEYS
+
 _AUTHORIZATION_KEYS = frozenset({"bearer", "auth", "authorization"})
 
-SENSITIVE_KEYS = frozenset(CREDENTIAL_SUFFIXES) | _AUTHORIZATION_KEYS
+SENSITIVE_KEYS = (
+    frozenset(CREDENTIAL_SUFFIXES)
+    | _LEGACY_SENSITIVE_KEYS
+    | _AUTHORIZATION_KEYS
+)
...
-        key_lower = key.lower().replace("_", "").replace("-", "")
-        if any(sensitive in key_lower for sensitive in SENSITIVE_KEYS):
+        key_lower = key.lower()
+        key_normalized = key_lower.replace("_", "").replace("-", "")
+        if (
+            any(sensitive in key_normalized for sensitive in _LEGACY_SENSITIVE_KEYS | _AUTHORIZATION_KEYS)
+            or any(
+                key_lower == sensitive or key_lower.endswith(f"_{sensitive}")
+                for sensitive in _SUFFIX_SENSITIVE_KEYS
+            )
+        ):

Also add regression cases for keyboard, keyframe, and monkey, while retaining coverage for apikey and authtoken.

Comment on lines +107 to +108
* a new **credential** pattern or noun → `secret_redaction.py` (reaches
`credential_redaction` and every `redact_content` caller automatically)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Inspect the vocabulary and free-text pattern declarations without running repository code.
rg -n -C 5 'CREDENTIAL_SUFFIXES|_CREDENTIAL_PHRASE_RE|_KNOWN_PREFIX_RE|def scan_content_for_credentials|def redact_content' autobot_shared/secret_redaction.py

Repository: mrveiss/AutoBot-AI

Length of output: 5178


Separate field-name and free-text redaction updates.

CREDENTIAL_SUFFIXES feeds is_credential_field, but scan_content_for_credentials uses fixed regular expressions, including _CREDENTIAL_PHRASE_RE. A new noun added only to CREDENTIAL_SUFFIXES can therefore reach credential_redaction without making redact_content recognise that noun in free text. Update the guidance to distinguish these changes.

Suggested documentation fix
-* a new **credential** pattern or noun → `secret_redaction.py` (reaches
-  `credential_redaction` and every `redact_content` caller automatically)
+* a new **credential field-name** pattern or noun → `secret_redaction.py`
+  (reaches `credential_redaction` automatically); update the free-text
+  patterns separately when `redact_content` must recognise the noun
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* a new **credential** pattern or noun → `secret_redaction.py` (reaches
`credential_redaction` and every `redact_content` caller automatically)
* a new **credential field-name** pattern or noun → `secret_redaction.py`
(reaches `credential_redaction` automatically); update the free-text
patterns separately when `redact_content` must recognise the noun

Comment on lines +112 to +113
A detector added anywhere else is an eighth implementation, and
`repo_tests/redaction_concept_census_test.py` fails on it.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Limit the census guarantee to new detector-owning modules.

The census definition on Lines 21-25 counts modules, not detectors. Adding a detector to an existing counted module does not create an eighth module or necessarily fail the census. State that limit here so reviewers do not treat a passing census as proof that every new detector was placed in the correct module.

#:
#: Measured on THIS rebased tree, twice and by two methods, not carried from
#: another session's message: `git ls-files "*.py"` = 6151 and
#: `git ls-tree -r HEAD` = 6151 (origin/main = 6150 both ways).

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '90,170p' repo_tests/audio_extension_allowlist_test.py
git ls-tree -r HEAD --name-only | wc -l
git ls-tree -r HEAD --name-only -- '*.py' | wc -l

Repository: mrveiss/AutoBot-AI

Length of output: 4316


🏁 Script executed:

set -eu
printf '%s\n' '--- current comment and declaration ---'
sed -n '126,160p' repo_tests/audio_extension_allowlist_test.py
printf '%s\n' '--- correctly scoped tree counts ---'
printf 'all paths: '
git ls-tree -r --name-only HEAD | wc -l
printf 'python paths: '
git ls-tree -r --name-only HEAD -- '*.py' | wc -l
printf '%s\n' '--- changed lines against the stated PR base ---'
git diff --unified=3 31310392dfe359bcdd4738ef97953407781e11ef -- repo_tests/audio_extension_allowlist_test.py | sed -n '/6151/,+20p'

Repository: mrveiss/AutoBot-AI

Length of output: 3219


🏁 Script executed:

set -eu
printf '%s\n' '--- representative HEAD tree paths ---'
git ls-tree -r --name-only HEAD | sed -n '1,5p'
printf '%s\n' '--- representative tracked Python paths ---'
git ls-files '*.py' | sed -n '1,5p'
printf '%s\n' '--- Python count from unfiltered HEAD tree ---'
git ls-tree -r --name-only HEAD | grep -E '\.py$' | wc -l
printf '%s\n' '--- pathspec variants ---'
printf 'HEAD then pathspec: '
git ls-tree -r --name-only HEAD -- '*.py' | wc -l
printf 'pathspec before HEAD: '
git ls-tree -r --name-only -- '*.py' HEAD | wc -l
printf 'globstar pathspec: '
git ls-tree -r --name-only HEAD -- '**/*.py' | wc -l

Repository: mrveiss/AutoBot-AI

Length of output: 751


Filter the ls-tree cross-check to Python files.

git ls-tree -r HEAD lists 10,761 paths. It does not support the stated count of 6,151 Python files. Record the filtered command instead.

Suggested fix
-#: `git ls-files "*.py"` = 6151 and
-#: `git ls-tree -r HEAD` = 6151 (origin/main = 6150 both ways).
+#: `git ls-files "*.py"` = 6151 and
+#: `git ls-tree -r --name-only HEAD | grep -E '\.py$' | wc -l` = 6151
+#: (origin/main = 6150 both ways).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#: `git ls-tree -r HEAD` = 6151 (origin/main = 6150 both ways).
#: `git ls-tree -r --name-only HEAD | grep -E '\.py$' | wc -l` = 6151
#: (origin/main = 6150 both ways).

single literal first argument.
"""
for call in ast.walk(node):
if not (isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute) and call.func.attr == "compile"):

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Recognise directly imported regex compilers.

from re import compile as regex_compile produces an ast.Name call, not the ast.Attribute required here. A module that defines PASSWORD_PATTERN = regex_compile(r"password=\S+") and redact_values is reported as having no detector. Resolve imports of re.compile, including aliases, before excluding name-style calls. Add a fixture for this import form.

continue
for arg in call.args:
for const in ast.walk(arg):
if isinstance(const, ast.Constant) and isinstance(const.value, str) and _mentions_secret(const.value):

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Detect PII patterns named outside their regex text.

_compiles_secret_pattern checks the regex text but not its assignment name. A new module with EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}") and redact_email therefore escapes the census. The regex contains none of _SECRET_NOUNS. Check detector names or equivalent PII evidence, and add an email-only fixture. The documented census includes PII implementations.

Comment on lines +176 to +179
if not isinstance(coll, (ast.Tuple, ast.List, ast.Set)):
continue
strings = [e.value for e in coll.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)]
if len(strings) >= 3 and sum(1 for s in strings if _mentions_secret(s)) >= 3:

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Include dictionary-based secret vocabularies.

A module can define SENSITIVE_FIELDS = {"password": True, "token": True, "api_key": True} and use it in redact_mapping. This walker excludes ast.Dict, so the new implementation passes the census. Inspect string keys in dictionary literals and add positive and negative dictionary fixtures.

Comment on lines +229 to +233
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
lowered = node.name.lower()
if any(verb in lowered for verb in _REDACTION_VERBS):
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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Recognise redaction methods on classes with other names.

A module with a secret vocabulary and class CredentialFilter exposing def redact(self, value) defines a redaction API. _defines_redaction_api checks only the top-level class name, so the module passes the census. Inspect methods on top-level classes and add a fixture for this class shape.


# A detector vocabulary with no redaction entry point — e.g. an audit rule list.
_PLANTED_DETECTOR_ONLY = """
INTERESTING = ("password", "token")

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '160,255p' repo_tests/redaction_concept_census_test.py
sed -n '330,390p' repo_tests/redaction_concept_census_test.py
rg -n '_declares_secret_vocabulary|_declares_secret_enum|_PLANTED_DETECTOR_ONLY|contrast pair|Every detector needs' repo_tests CLAUDE.md

Repository: mrveiss/AutoBot-AI

Length of output: 8945


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- relevant files ---'
rg -l '_declares_secret_vocabulary|_declares_secret_enum|is_redaction_implementation|PLANTED_.*(DETECTOR|ENUM|COLLECTION)|secret vocabulary|secret/PII|detector' repo_tests --glob '*.py' | sort
printf '%s\n' '--- relevant symbols and assertions ---'
rg -n '_declares_secret_vocabulary|_declares_secret_enum|is_redaction_implementation|PLANTED_DETECTOR_ONLY|PLANTED_.*(ENUM|COLLECTION)|expected|assert .*redaction|assert .*detector|secret vocabulary|secret/PII' repo_tests --glob '*.py'
printf '%s\n' '--- current module outline and focused source ---'
ast-grep outline repo_tests/redaction_concept_census_test.py
sed -n '1,170p' repo_tests/redaction_concept_census_test.py
sed -n '255,390p' repo_tests/redaction_concept_census_test.py
printf '%s\n' '--- base/head status ---'
git show --stat --oneline f58793659b2fa282b7deac8e8bbec4ea09486a31 -- repo_tests/redaction_concept_census_test.py
git diff --stat 31310392dfe359bcdd4738ef97953407781e11ef f58793659b2fa282b7deac8e8bbec4ea09486a31 -- repo_tests/redaction_concept_census_test.py

Repository: mrveiss/AutoBot-AI

Length of output: 42043


Make the detector-only fixture exercise the collection detector.

_PLANTED_DETECTOR_ONLY contains only two secret strings, but _declares_secret_vocabulary requires at least three. Add a third secret noun and assert _declares_secret_vocabulary(ast.parse(_PLANTED_DETECTOR_ONLY)) is True before asserting that is_redaction_implementation returns False. Add separate positive and negative contrast fixtures for the collection and enum detector paths.

mrveiss added a commit that referenced this pull request Sep 23, 2026
…rebased tree (#17331, #17332)

CI failed shard 12/12 on reach_declarations_test for audio-extension-allowlist and
hooks-path-override. Both floors sat 2 below their lower bound, on a branch whose own
contribution is 2 counting files -- main had spent the rest of the allowance before this
branch existed. #17142's shape, for the sixteenth time on one of these.

Measured on THIS tree after rebasing onto current main, not copied from any other
branch or message:

  audio-extension-allowlist   population 6152   completed 5765
      verify_floor needs floor >= 6152-(400+300) = 5452
      completed    needs floor <= 5765
      window [5452, 5765]; was 5450; now 5700

  hooks-path-override         population 6939   completed 6938
      verify_floor needs floor >= 6939-(400+1) = 6538
      completed    needs floor <= 6938
      window [6538, 6938]; was 6536; now 6800

A floor has TWO bounds and only the first has been checked at every previous re-pin.
`verify_floor` fails when population-floor exceeds growth+skips; `completed` fails when
the count the guard actually FINISHED processing drops below the floor. `completed` is
smaller than population and shrinks as repo_tests grows, so a floor pinned from
population alone can pass one check and fail the other.

Every previous re-pin took the lower bound as the value, which is why there have been
fifteen: at the minimum, the next file main adds is another red. These take the middle
of the window -- 452 of 700 allowance and 65 files of completed headroom for audio, 139
and 138 for hooks. Both are true claims: 5765 and 6938 files really are opened.

5700 is the same value e5 pins in #17335, deliberately rather than inventing a third
independent number for one measurement.

This does not fix #17142. A 400-file growth allowance is 5.8% of a 6939-file tree and
this tree spends it in under a week; the allowance is the thing that is wrong, not the
floors. Sixteenth data point.

Verified: 209 tests pass across reach_declarations_test, audio_extension_allowlist_test
and hooks_path_override_15961_test on this tree. Local passes are not CI (#17144) --
this red came from CI and the re-measurement is what answers it.

Refs #17142
@mrveiss
mrveiss force-pushed the issue-16688-fork-collapse branch 2 times, most recently from 0c9174f to d26c9b2 Compare September 23, 2026 23:31

@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: 1


ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bfe65c7d-60c6-4b6a-841b-51f6b724b531

📥 Commits

Reviewing files that changed from the base of the PR and between f587936 and 0e52857.

📒 Files selected for processing (11)
  • autobot-backend/a2a/pii_pipeline.py
  • autobot-backend/chat_workflow/cot_events.py
  • autobot-backend/llm_shared/credential_redaction.py
  • autobot-backend/security/chat_message_safety.py
  • autobot-backend/services/config_revision_service.py
  • autobot_shared/secret_redaction.py
  • autobot_shared/secret_redaction_test.py
  • autobot_shared/security/redaction.py
  • repo_tests/audio_extension_allowlist_test.py
  • repo_tests/hooks_path_override_15961_test.py
  • repo_tests/redaction_concept_census_test.py

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

Comment on lines +223 to +232
for node in tree.body:
if isinstance(node, (ast.Assign, ast.AnnAssign)):
if node.value is not None and (
_compiles_secret_pattern(node.value) or _declares_secret_vocabulary(node.value)
):
return True
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if _compiles_secret_pattern(node) or _declares_secret_vocabulary(node):
return True
return _declares_secret_enum(tree)

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

Scan class bodies for detectors, not only module-level statements.

_declares_own_detector checks only module-level Assign, AnnAssign, FunctionDef and AsyncFunctionDef nodes. For a ClassDef, it runs only _declares_secret_enum. That check accepts plain string constants only. It does not find compiled patterns or collection literals.

A class-based redactor therefore passes the census. Example:

import re

class SecretRedactor:
    _PATTERNS = [re.compile(r"password=\S+"), re.compile(r"bearer\s+\S+")]

    def apply(self, text):
        ...

_defines_redaction_api matches the class name SecretRedactor. _declares_own_detector returns False, because the class-body assignment and the method bodies are never scanned. A class that holds its patterns is a common shape for a new redactor. This gap lets an eighth implementation land without any test failure.

The same gap applies to module-level if/try blocks, for example a try: import … fallback that defines patterns.

Scan ClassDef bodies and their methods with the same two checks. Add a positive fixture for the class-held pattern. Also add a negative fixture: a class that holds a non-secret regex. The path instruction requires a contrast pair for every detector.

🐛 Proposed fix
     for node in tree.body:
         if isinstance(node, (ast.Assign, ast.AnnAssign)):
             if node.value is not None and (
                 _compiles_secret_pattern(node.value) or _declares_secret_vocabulary(node.value)
             ):
                 return True
-        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.If, ast.Try)):
             if _compiles_secret_pattern(node) or _declares_secret_vocabulary(node):
                 return True
     return _declares_secret_enum(tree)

As per path instructions: "Every detector needs a contrast pair: a fixture that SHOULD trip it and one that should not."

🧰 Tools
🪛 Ruff (0.16.6)

[warning] 229-230: Use a single if statement instead of nested if statements

(SIM102)

Source: Path instructions

@mrveiss
mrveiss force-pushed the issue-16688-fork-collapse branch 2 times, most recently from 59268da to ee2668a Compare September 24, 2026 06:41
@mrveiss

mrveiss commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

Review: approve

Reviewed at head ee2668a37. Two things could have made this an under-redaction, and both check out; one small note.

1. SENSITIVE_KEYS losing apikey is safe — because of how it is consumed, not because of the noun list. Deriving from CREDENTIAL_SUFFIXES drops the literal "apikey" and relies on "key" covering it. That is only true under substring matching, and it is: credential_redaction.py:94 on merged main is

if any(sensitive in key_lower for sensitive in SENSITIVE_KEYS):

so apikey still matches via key. Under exact matching the same diff would have silently stopped redacting a field literally named apikey. test_no_pre_16688_key_stops_being_sensitive pins exactly that, and pinning the old vocabulary rather than asserting the new one is the right direction for it — it fails if a future narrowing of CREDENTIAL_SUFFIXES drops a noun, which is the drift that would otherwise be invisible.

2. The set → frozenset change breaks nothing. SENSITIVE_KEYS has no consumer outside credential_redaction.py and its tests, and nothing anywhere calls .add/.update/.discard on it. Worth stating because a mutable module-level set turning immutable is the kind of change whose failure shows up at import time in one unlucky caller.

The census being MEASURED rather than transcribed is the part worth keeping. #16688 and #17312 both said four; the rule found seven, and the three nobody had counted (cot_events, portability, config_revision_service) each carry their own key-name vocabulary. A guard that had restated the issue's number would have passed while three redactors stayed invisible — and would have read as enforcement.

One note, not blocking: repo_tests/hooks_path_override_15961_test.py is a comment-only edit to a hub file from a redaction PR. The reasoning is sound (per-branch narrative in a shared floor file conflicts on every rebase, and it has already conflicted three times), but a hub-file touch in an unrelated PR is itself a rebase surface for whoever is mid-flight on it. Mentioning it so it is a choice rather than an accident.

Overlap check: this touches autobot-backend/chat_workflow/cot_events.py, and so does my #17375 — a docstring addition at the module header here, _try_publish's body there. Far apart, so no textual conflict, but flagging it since same-file-in-two-PRs is normally the thing we stop.

…census (#16688)

The issue asks which of four parallel redaction implementations is canonical.
Measuring first changed the question twice.

There are seven, not four. A census rule run over all 3,392 tracked production
sources finds three more modules that independently decide what a secret looks
like and that no issue had counted: chat_workflow/cot_events.py
(_SENSITIVE_KEY_FRAGMENTS, 13 nouns), llc/services/portability.py
(_SECRET_LIKE_KEYS, 13) and services/config_revision_service.py
(_SECRET_SUBSTRINGS, 5). A fifth, autobot_shared/security/redaction.py, also
declares itself the one canonical redactor (#12242).

And one of the four is not an implementation at all. security/chat_message_safety.py
declares no detector; it composes pii_pipeline and the injection detector. It is
the shape the other seven should converge on, so the guard must not count it.

There is therefore no single canonical redactor and there should not be one.
What was missing is the boundary: one module per SHAPE of the problem.
docs/developer/REDACTION_BOUNDARY.md states it, with a which-one-do-I-call
table, and each of the modules points at it -- except portability.py, which sits
on its grandfathered 936-line ceiling and would be pushed over by the note.

Two behaviour changes, both strictly widening:

- secret_redaction's JWT pattern required "eyJ" on the payload as well as the
  header, making it narrower than a2a/pii_pipeline.py's. A payload of {} encodes
  to e30 and was matched there and missed here -- in the module
  llm_shared.credential_redaction delegates log redaction to. Anchored on the
  header alone, as pii_pipeline already was.
- credential_redaction.SENSITIVE_KEYS no longer hand-copies the credential
  nouns; it derives from secret_redaction.CREDENTIAL_SUFFIXES. Deliberately a
  UNION: that vocabulary carries no authorization terms, so a straight
  replacement would have stopped redacting Authorization, auth_header and
  x_auth. A redaction merge that narrows is a silent under-redaction, which is
  why both directions are now tested.

repo_tests/redaction_concept_census_test.py pins the seven. It counts
implementations of a concept rather than duplicated text, because jscpd finds
zero clone lines across these modules -- they are forks, and a fork shares no
8-line run, so MAX_DUP_LINES neither rises when an eighth lands nor falls when
one is collapsed (#17312). The rule is proven on a planted fork that shares no
text with any real module, and on the two shapes that must NOT count: pure
composition, and a vocabulary with no redaction entry point.

The two autobot_shared redactors disagree on 7 of 10 sampled field names --
suffix matching vs substring, each a recorded ruling in its own file. Merging
them would reverse one, so this documents the split and leaves it open.

Refs #16688, #17312
…les size (#16688)

The pre-push suite caught the previous commit's fixture asserting the wrong
thing. `{}` base64url-encodes to `e30`, three characters -- below the `{8,}`
this scanner requires for a segment and below the `{10,}` a2a/pii_pipeline.py
requires, so NEITHER rule matched it. The test passed its "something matched"
sibling only because the high-entropy rule claimed the span; the precise
assertion (`pattern == "jwt"`) is what surfaced it, which is the argument for
writing it that way.

The divergence itself is real; the example was not. A payload starts "eyJ" only
when its JSON begins exactly '{"'. A serializer emitting a space after the brace
gives "eyAi" and one padding before it gives "IHsi" -- both long enough, both
matched by pii_pipeline, both missed by this scanner before the widening.
Fixture replaced with such a token, decoding to { "sub": "1234567890", "name": "A" },
verified matched by the new rule and unmatched by the old one. The source
comment carried the same wrong example and is corrected with it.

Refs #16688
…om on main (#16688, #16724)

origin/main sits at EXACTLY zero headroom on this floor: population 6150,
floor 5450, and 6150 - 5450 = 700 = skips 300 + growth 400. Any branch adding
one tracked .py file turns it red. This branch adds one test file, measures
6151, slack 701 -- one over. The floor is stale, not the branch.

Measured on this rebased tree, two methods, not carried from another session:
  git ls-files "*.py"      = 6151   (origin/main 6150)
  git ls-tree -r HEAD      = 6151   (origin/main 6150)

Re-pinned to 5700, NOT the 5751 that population - growth gives. The binding
constraint is completed(), not verify_floor(): the guard's loop continues past
the canonical file and everything under repo_tests/ before reading anything, so
completed = 6151 - 386 - 1 = 5764. A 5751 floor leaves 13 files of margin
against a number that FALLS as repo_tests/ grows -- and this branch grows it.
5700 leaves 64, comparable to the 47 the 5450 pin carried against its measured
completed of 5497, which held from #16702 until now.

  verify_floor: 6151 - 5700 = 451 <= 300 + 400 = 700   ok
  completed:    5764 >= 5700, margin 64                 ok

skips and growth unchanged: neither is short, the floor was stale. #16724
already tracks re-measuring this from CI's own log rather than from a tree walk.

Checked for the duplicated `floor=` keyword a conflict resolution produced
elsewhere today: one occurrence.

Also measured, not changed: hooks-path-override is green here but at ZERO
headroom (population 6937 incl. 31 extensionless shell scripts, floor 6536,
slack 401, allowance 401); origin/main has exactly 1. The next tracked
.sh/.py/.yml/.yaml file anywhere turns it red. Left alone deliberately -- it
passes, it is a hub file four sessions are touching tonight, and #17142 already
tracks it.

Refs #16688, #16724, #17142
…6688)

python-suite shards 5, 6, 8 and 10 were red on
threat_model_anchors_resolve_test. The cause is in this branch's own diff, not
in base: the boundary pointer added to autobot_shared/security/redaction.py's
module docstring inserted five lines above every definition in the file, and the
citations in docs/developer/THREAT_MODEL.md still pointed at the old numbers.

Base agrees with itself, checked before assuming:
  git show origin/main:autobot_shared/security/redaction.py \
    | grep -n "def redact_provider_error"    -> 140
  git show origin/main:docs/developer/THREAT_MODEL.md \
    | grep -n redact_provider_error          -> 116: ... (:140)

FOUR anchors moved, not one. The failing test names only the anchor its shard
reached first, so fixing that one would have left three and produced another red
run. Every citation in the document was re-resolved with the test's own rule
(AST definition lookup, not a line regex) rather than only the reported case:

  redact_text              :97  -> :102
  redact_mapping          :109  -> :114
  redact_cloud_identifiers :126 -> :131
  redact_provider_error    :140 -> :145

The two loader.py anchors that a definition-only check flags are NOT broken:
import_module and spec_from_file_location are stdlib CALL sites, and the test
has an explicit branch for anchors with no definition in the file. A first pass
listed them as failures; that was the probe being wrong, not the doc.

The other seven modules this branch adds a docstring pointer to are unaffected
-- the sweep covered all 38 anchors, not just redaction.py, and they are either
unanchored or anchored above the insertion point.

Verified: repo_tests/threat_model_anchors_resolve_test.py -- 41 passed.
THREAT_MODEL.md stays at 140 lines, which is its own ceiling
(test_document_exists_and_is_short_enough_to_read_every_review asserts <= 140);
this change edits line numbers in place and adds none.

Root-caused by autobot-ai-f8 while sweeping, including the base-vs-branch
attribution check; the widening from one anchor to four is this commit.

Refs #16688
…re-empt a zero-headroom floor (#16688)

Both changes are pre-emptive: this branch was GREEN when they were made. It was
green against a base that has since moved, which is the whole point.

THE CENSUS GUARD WAS A SECOND FORK OF THE ENUMERATION.
redaction_concept_census_test shelled out to `git ls-files` for itself, exactly
as enum_hand_copy_guard did on the #14881 branch.
one_git_enumeration_15926_test::test_the_direct_invocation_count_only_shrinks
caught that one in CI; rebasing this branch onto origin/main 3f0d4b3
reproduces it here -- 34 invocations against a MAX of 33 -- because main reached
33 after this branch's last run. Found by predicting it and checking, not by a red.

A census guard forking the repo's enumeration, in a PR whose subject is forks,
is the joke writing itself. Routed through tools.lint._scan_helpers.tracked_paths,
whose `exclude` becomes a git `:(exclude)` pathspec so git does the matching --
rather than the second Python-side matcher this had, which is how #15510 fired
an exclusion against absolute paths and matched every file. subprocess and
scrubbed_git_env are no longer imported.

HOOKS-PATH-OVERRIDE 6536 -> 6800 WHILE PASSING. Measured on this rebased tree:
population 6937 (glob hits for *.sh/*.py/*.yml/*.yaml plus 31 extensionless
files whose shebang says shell), completed 6936, slack 401 against
growth 400 + skips 1 = exactly 401. That is not "passing", it is "passing until
the next file anyone lands", and four sessions moved main in the hour this
branch sat in review. The cost of pre-empting is one line on a hub file three
other PRs already change to the same value; the cost of not is an hour of CI and
a round trip on a predictable red.

6800 is mid-window of [6536, 6936], not the minimum -- fifteen previous re-pins
took the bottom, which is why there were fifteen. It is deliberately the same
value as autobot-ai-58's #17338 and this session's #17342 and #17346: one
measurement should not produce four numbers, and identical lines cannot
conflict. First to land wins, the rest adopt on rebase.

The other three floors were measured here and left alone, because this branch
does not move their populations: audio-extension-allowlist 6151/5764 against the
5700 pinned earlier on this branch (margin 64),
prompt-injection-detector-strict-mode 3256 (no new backend non-test module),
import-hermeticity 662 (no new module under api/ or autobot_shared/). Two of
those sit at exactly zero headroom on main and will need re-pinning by whichever
branch next adds to their roots -- see #17142.

Verified on this tree:
  one_git_enumeration + redaction_concept_census + threat_model_anchors
  + prompt_injection_detector_strict_mode + reach_declarations ... 274 passed

reach_declarations_test is run with the prompt-injection module in the same
session on purpose: its declaration reaches the sweep only when something else
has imported it, so repo_tests/ alone silently checks 35 of 36 floors.

Refs #16688, #15926, #17142
…ts own (#16688)

Empty commit: the change is in the rebase conflict resolutions, but the commits
carrying them still SAY 6800 and 5700 in their messages because that is what
they proposed when written. Stating it once beats rewriting four messages.

Rebased onto origin/main 19006b3. Both floors adopted, first-to-land wins:

  hooks-path-override        proposed 6800 -> adopted main's 6736 (#17318)
  audio-extension-allowlist  proposed 5700 -> adopted main's 5752 (#17317)

Re-derived on this tree rather than taken on trust:

  audio       pop 6153  completed 5766  pin 5752  slack 401/700  margin 14
  hooks       pop 6939  completed 6938  pin 6736  slack 203/401  margin 202
  prompt-inj  pop 3256                  pin 2956  slack 300/300  ZERO headroom
  import-herm pop  662                  pin  602  slack  60/60   ZERO headroom

The pre-emptive 6800 on this branch was still the right call when made -- it sat
at slack 401 against an allowance of 401, one file from red, while passing --
but main moved first and one measurement should not produce four numbers.

FOR #17142, the measurement that took three branches to produce. audio's pin is
population - growth, the GAP bound, while the bound that fires on this guard is
completed(): its loop continues past the canonical file and all of repo_tests/
before reading anything, so completed sits ~387 below population and falls every
time anyone adds a guard. Main pinned 5752 with an apparent 402 of headroom. The
real figure, measured on this session's three branches:

  issue-13049-lazy-import        margin 16
  issue-14881-enum-hand-copies   margin 15
  issue-16688-fork-collapse      margin 14

Three ordinary branches, each adding one or two repo_tests guards, took it from
16 to 14. That is the allowance being wrong, not the floors, and it is why this
guard has a re-pin history at all.

Verified on this tree:
  prompt_injection_detector_strict_mode + reach_declarations
  + one_git_enumeration + redaction_concept_census
  + threat_model_anchors ................................... 274 passed

Refs #16688, #17142, #17317, #17318
…arried (#16688, #15928)

reach_floor_migration_test::test_no_new_hand_rolled_floor_remains_unmigrated
failed: redaction_concept_census_test declared `_MIN_SOURCE_FILES` as a
hand-rolled reach-sized floor. #15928's rule is that a floor >=100 which binds a
tree sweep goes through _reach.declare(), because a number chosen by feel decays
silently.

IT HAD ALREADY DECAYED, IN THE FOUR HOURS SINCE I WROTE IT. The constant was
1500. Re-measured with a throwaway script, as #15928 requires rather than
carrying the old value across: the real population is 3,393 tracked production
.py files (excluding .worktrees/, docs/, repo_tests/ and test files). 1500 sat
56% BELOW the tree it claimed to bind -- it would have passed over a sweep that
had lost more than half the codebase and still reported "no new redaction
implementations".

Which is the argument #15928 makes, demonstrated on a guard whose own docstring
is about not confusing "nothing found" with "did not look". I wrote the number
by feel in a file about measurement discipline.

  declare("redaction-concept-census", floor=3093, growth=300,
          what="tracked production python files")

growth=300 matches the sibling declaration in
prompt_injection_detector_strict_mode_test over a population of similar size.

Also wired REACH.completed(parsed), which the hand-rolled version had no way to
express: the floor now binds what the census actually PARSED, not merely what it
listed. Without it the sweep could enumerate 3,393 files, fail to parse 3,000 of
them, and report the same clean as a tree with nothing in it -- the exact
distinction this guard exists to make.

discover() takes a root and returns [] on EmptyEnumeration rather than letting
it escape: reach_declarations_test drives every declaration against an empty
repository and demands ReachFloorError specifically, so letting EmptyEnumeration
through means the declaration fails loudly while saying nothing about whether
its floor binds.

Found by autobot-ai-f8 reading the shard-8 log on the sibling #17342 branch,
whose enum_hand_copy_guard_test.py carries the identical defect. Confirmed here
by running the migration guard rather than assuming the sibling's diagnosis
transferred.

Verified on this tree:
  reach_floor_migration + redaction_concept_census
  + reach_declarations ...................................... 206 passed

Refs #16688, #15928, #17312
…he minimum (#16688, #17142)

Rebased onto origin/main e8c7bf9. Main's floors adopted where they hold; the
one this branch created did not.

  audio-extension-allowlist  adopted main's 5757 (NOT 5752 -- superseded)
  hooks-path-override        adopted main's 6736
  prompt-injection-...       adopted main's 2958, green here
  import-hermeticity         adopted main's  604, green here
  redaction-concept-census   3093 -> 3247, MINE, and it was red

THE CENSUS FLOOR WAS MY OWN MINIMUM PIN, AND IT IS THE SECOND ONE TONIGHT.

I created that declaration earlier in this branch, migrating a hand-rolled
floor under #15928, and pinned it at 3093 -- population 3393 minus growth 300,
which is the MINIMUM OF THE WINDOW. It went red on this rebase: population
reached 3397, slack 304 against an allowance of 300. Four files.

The sibling declaration on the #14881 branch (enum-hand-copy-census, pinned
5755 at population 6155) went red the same way at 6165. Ten files. Both were
committed AFTER I filed the comment on #17142 arguing that `population - growth`
is not a floor but a snapshot with zero tolerance by construction.

So: wrote the argument, made the mistake twice, noticed neither time until
reach_declarations_test failed. That is the case for the check being mechanical
rather than a review item, and it is self-reported on #17142 with timestamps
because "people who understand the rule will follow it" is the objection it
removes.

Re-measured on this tree rather than adjusted: 3,397 tracked production .py
files using the guard's own filter. Window [3097, 3397]; 3247 is mid-window and
buys ~150 files of headroom instead of four.

Measured after the re-pin:

  audio       pop 6163  completed 5776  pin 5757  slack 406/700  margin 19
  hooks       pop 6949                  pin 6736  slack 213/401
  prompt-inj  pop 3258                  pin 2958  slack 300/300  ZERO headroom
  import-herm pop  664                  pin  604  slack  60/60   ZERO headroom
  census      pop 3397                  pin 3247  slack 150/300

The two at zero headroom are adopted unchanged because this branch adds nothing
to their roots. #17346 carries prompt-injection at 3100 and #17342 carries
import-hermeticity at 633 -- those are the branches that do.

audio is 5757, not the 5752 quoted to me during review: #17317's value was
superseded while that message was in flight. Read by AST at e8c7bf9 rather
than taken from the message, which is the same discipline this session has been
applying all night -- a value quoted by name is already a snapshot.

Verified on this tree:
  reach_declarations + reach_floor_migration
  + redaction_concept_census + threat_model_anchors .......... 247 passed

Refs #16688, #17142, #15928
… not ship (#16688)

Third instance of the same defect, found by checking rather than by review.
autobot-ai-f8 caught it on #17342 (fixed as acbfbe3) and #17346 (67f977f);
this branch is the third of the three that re-pinned this declaration tonight,
so it was checked before anyone had to report it.

  git diff origin/main...HEAD -- repo_tests/audio_extension_allowlist_test.py \
    | grep -c 'floor='                                    -> 0
  the file ships                                          -> floor=5757
  the block this branch ADDED said                        -> "Ratcheted 5450 -> 5700 (#16688)"

An earlier revision of this branch did propose 5700 and the block described it
correctly then. #17317 landed 5752, later 5757, and the rebase adopted main's
value; the `floor=` hunk was resolved and the prose, living in a different
commit, survived as a pure addition. The branch ended up ADDING a precise
narrative about a change it no longer makes.

Deleted rather than corrected, matching the #17346 fix: main's own comment sits
immediately below explaining 5757, so a correction block would leave two voices
on one floor -- which is the defect, not its remedy. The diff no longer touches
this file.

Note this differs from the #17342 fix, which rewrote its block instead. That
was the right call there and the wrong one to repeat: at the time I had not seen
that main already carried a true account a few lines down, so the correction
became a second voice too. It is deleted there as well.

WHY THIS FILE AND NO OTHER, which is f8's observation: three branches re-pinned
this one declaration tonight. A floor whose comment block is append-only across
concurrent branches accumulates contradictory narratives even when every
individual re-pin is correct, because the rebase resolves the value and nothing
resolves the sentence. The durable record of the mechanism belongs in commit
messages and on #17142 -- not as competing prose in a hub file.

Refs #16688, #17142, #17356
…ments from prose (#16688)

Two distinct problems, both found by grepping this branch's own diff for prose
naming a measurement -- which is the check autobot-ai-f8 asked for after the
orphaned-narrative round.

1. THIS BRANCH WAS SHIPPING TWO STALE FLOORS AND TWO SUPERSEDED COPIES OF MAIN'S
   OWN COMMENTS. audio-extension-allowlist carried 5757 with main's older
   narrative while main is at 5764 with a rewritten one;
   prompt-injection-detector-strict-mode carried 2958's text while main is at
   2960. Neither needs a branch-specific value -- measured on this tree, main's
   floors pass here -- so both files are now byte-identical to origin/main.

   That is worse than an orphaned note of my own: a superseded copy of ANOTHER
   branch's comment silently misattributes a stale account to them, and reads as
   authoritative because it is written in their voice.

2. EVERY MEASUREMENT IS OUT OF THE PROSE THIS BRANCH ADDS. The census
   declaration's migration note, its re-pin note, the hooks adoption note and
   one line of REDACTION_BOUNDARY.md all cited a population or a slack figure.
   Each was correct when written and stale one rebase later.

   The last one is the instructive case: "enumerate 3,393 files, fail to parse
   3,000 of them" was an ILLUSTRATION, not a claim -- and an illustration
   carrying a real population goes stale exactly like a measurement while
   reading as more authoritative for being precise. Now "enumerate the whole
   tree, fail to parse most of it", which stays true.

   Prose that cites no measurement cannot go stale. The arithmetic lives in
   commit messages, which cannot drift from the tree they describe because they
   are not in it.

Verified on this tree:
  reach_declarations + reach_floor_migration
  + redaction_concept_census ................................ 206 passed

Refs #16688, #17142, #17356
@mrveiss
mrveiss force-pushed the issue-16688-fork-collapse branch from ee2668a to df4273e Compare September 24, 2026 07:52

@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: 2


ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 032323a2-cbb2-43b0-863b-87c8dc1a83b6

📥 Commits

Reviewing files that changed from the base of the PR and between 0e52857 and df4273e.

📒 Files selected for processing (10)
  • autobot-backend/a2a/pii_pipeline.py
  • autobot-backend/chat_workflow/cot_events.py
  • autobot-backend/llm_shared/credential_redaction.py
  • autobot-backend/security/chat_message_safety.py
  • autobot-backend/services/config_revision_service.py
  • autobot_shared/secret_redaction.py
  • autobot_shared/secret_redaction_test.py
  • autobot_shared/security/redaction.py
  • docs/developer/REDACTION_BOUNDARY.md
  • repo_tests/redaction_concept_census_test.py

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

``llm_shared/credential_redaction.py`` IS in the census, because it still
declares ``API_KEY_PATTERNS`` of its own on top of the shared vocabulary it now
derives ``SENSITIVE_KEYS`` from (#16688). If those patterns are folded into
``secret_redaction`` later, drop it from ``_CENSUS`` and the count falls to 3.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the projected census count.

_CENSUS has seven entries. Removing credential_redaction.py would leave six, not three. Correct the count so the consolidation guidance matches the pinned census.

Comment on lines +229 to +230
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if _compiles_secret_pattern(node) or _declares_secret_vocabulary(node):

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

Detect inline regex redactors.

If a new module defines redact_text using re.sub(r"password=\S+", "[redacted]", text), this branch reports no detector. The regex is not compiled, and the function declares no collection vocabulary. The new implementation therefore passes the census. Recognise inline regex calls and add one fixture that trips this detector and one that does not.

As per path instructions: “Every detector needs a contrast pair: a fixture that SHOULD trip it and one that should not.”

🧰 Tools
🪛 Ruff (0.16.6)

[warning] 229-230: Use a single if statement instead of nested if statements

(SIM102)

Source: Path instructions

@mrveiss

mrveiss commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

Review currency: the approval was given at a different head, and still applies — checked rather than assumed

This PR was reviewed and approved at ee2668a37a. The head is now 2b8b20f2e2, moved by an automatic rebase. An approval at a head that no longer exists is not an approval of what merges, so here is the comparison.

Both heads' own patches, each taken against its own merge base (a two-dot diff between the two tips would show main's independent commits reversed and answer a different question):

approved  ee2668a37a   14 files, 747 added lines
current   2b8b20f2e2   13 files, 735 added lines

One file left the patch and nothing else changed:

REMOVED: repo_tests/hooks_path_override_15961_test.py

Every one of the remaining 13 files is byte-identical between the two patches — compared hunk-by-hunk, not by counting files:

identical: 13    differing: 0

Why that file left, and why its departure is the reviewer's own recommendation

hooks_path_override_15961_test.py is a hub file this PR had no substantive reason to touch: its floor value was 6736 on both sides, and the diff was prose only. The review flagged exactly that — "a hub touch in an unrelated PR is itself a rebase surface" — and it was immediately proved right when #17342 merged and this PR went CONFLICTING on that one file, taking its green with it.

Resolving that conflict toward main removed the branch's changes to it entirely. So the current head is a strict subset of the approved content, and the subtracted part is precisely what the reviewer objected to.

The approval therefore still covers what would merge. Not because heads are close, but because the delta is one removal in the direction the review asked for.

Floors re-verified on the current head

Measured, not carried:

declaration population floor verdict
prompt-injection-detector-strict-mode 3261 3100 (+300 growth) OK, ceiling 3400
audio-extension-allowlist 6180 5764 gap 416 of 700 — OK
audio-extension-allowlist completed() 5787 5764 OK, 23 files
redaction concept census 6180 tracked .py 3247 OK
tracked-key-material 10798 10000 (+800) OK, 2 files of headroom

That last one is tight and worth naming: main carries only 4 files of headroom on tracked-key-material, and this PR adds 2. #17382 re-pins that floor to 10_400, which gives the whole queue ~395 files instead of four — so if #17382 lands first, this margin stops mattering.

Outstanding: python-suite, the last of the 11 required contexts.

@mrveiss
mrveiss merged commit 41a56fa into main Sep 24, 2026
78 of 83 checks passed
@mrveiss
mrveiss deleted the issue-16688-fork-collapse branch September 24, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

canonical debt: four parallel secret/PII redaction implementations (secret_redaction, pii_pipeline, chat_message_safety, credential_redaction)

1 participant