Skip to content

fix(redaction): pin and passcode are whole-token credentials, scheme-less userinfo is masked, and redact_dict composes redact_nested with no partial reveal (#18194) - #18228

Merged
mrveiss merged 19 commits into
mainfrom
issue-18194-redaction-vocabulary
Oct 11, 2026
Merged

mrveiss merged 19 commits into
mainfrom
issue-18194-redaction-vocabulary

Conversation

@mrveiss

@mrveiss mrveiss commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

Three open redaction defects all live in autobot_shared/secret_redaction.py and its one second recursive redactor, llm_shared/credential_redaction.redact_dict, so they ship as one batch (same file ⇒ one PR). The owner decided #18194 on 2026-10-10: pin and passcode are credentials when they appear as whole words. #18205 asks for one recursive mapping redactor and an end to the partial key reveal. #18206 asks for the password in a scheme-less user:password@host to be masked. It builds on redact_nested from #18219 (merged).

What Changed

  • Vocabulary split (size). The vocabulary and matcher move to autobot_shared/secret_redaction_vocabulary.py. secret_redaction.py re-exports every public name, so no import breaks (594/600 lines, no ratchet entry). redaction_concept_census_test names both files as the one canonical home (CANONICAL_HOMES), and a third file still fails it.
  • security(redaction): "pin" is not a credential noun, so PIN-named fields are never masked #18194: whole-token nouns under both policies. pin, pins, passcode, pincode(s), pin_number(s), pin_codes and pin followed by hash/digest/salt are matched as whole tokens (split on _ - . and camelCase). pin_memory, pinned_revision, pin_length, spinner, opinion and pinyin stay unmasked. They are not added to the substring-matched BROAD_FRAGMENTS. Consequence of the ruling, kept deliberately (confirmed via the coordinator): gpio_pin, led_pin and version_pin are masked. It is recorded in REDACTION_BOUNDARY.md and pinned by a test.
  • security(redaction): scheme-less user:password@host strings are not scrubbed by the canonical redactor #18206: scheme-less userinfo. redact_schemeless_userinfo masks the password in user:password@host[:port][/…]. It splits on the last @ and accepts /, # and : in the password and a bracketed IPv6 host. redact_value and redact_nested apply it. Unchanged: emails (including +), host:port, mailto:/sip:/tel:, and a drive path C:\x@y. It is deliberately not applied to free text, which has no value boundary (note:bob@example.com).
  • tech-debt(redaction): llm_shared credential_redaction.redact_dict is a second recursive redactor that partially reveals keys #18205: one recursive redactor. redact_dict is now redact_nested(data, BROAD, exempt_counts=False, extra_scan=redact_string, mask_credential_urls=True). Effects:
    • Every BROAD credential-named string is masked whole, a URL included, so webhook_secret_url can never come back.
    • redact_nested's own default masks a credential-named URL whole too, except plain endpoint URL names (token_url, auth_url, authorization_url, db_dsn), which keep their host as on main. *_endpoint credential names (oauth_token_endpoint, token_endpoint) are masked whole, as on main, and so are dash-spelled names (db-dsn). Unknown name parts fail closed.
  • tech-debt(redaction): llm_shared credential_redaction.redact_dict is a second recursive redactor that partially reveals keys #18205: partial reveal replaced. redact_dict and redact_api_key (behind redact_string, safe_repr and CredentialRedactionFilter) return the fixed-width REDACTED_PLACEHOLDER instead of value[:4]...value[-4:]. The three other partial-reveal sites outside this module are one class issue, security(redaction): three key maskers outside the canonical redactor still reveal part of the key #18221.
  • Behaviour that changed at redact_dict callers (also in REDACTION_BOUNDARY.md):
    • tuples and sets are walked;
    • None/"" under a credential name is returned as-is;
    • the mask is **********;
    • datetime/date/time/timedelta/UUID/Decimal pass through under ordinary names;
    • bytes fails closed.

redact_dict callers, before and after (call sites unchanged):

Caller Needs
llc/services/replay_service._redact_sensitive credential mask + content scan (redact_string)
llc/services/replay_service._redact_log_dict same, per dict, list item and string
credential_redaction.safe_repr redact_dict
credential_redaction.CredentialRedactionFilter redact_dict + redact_string on record.args

#18206 inventory of fields that could hold a user:pass@host value, recorded in REDACTION_BOUNDARY.md with file:line. All are documented in scheme:// form, and the parsers reject the scheme-less form (SQLAlchemy make_url raises ArgumentError, redis.Redis.from_url raises ValueError). No proxy-credential setting was found:

  • ssot_config: database_url, redis_url, custom_endpoint, ollama_gpu_endpoint, jaeger_endpoint;
  • SLM config: database_url, slm_users_database_url, autobot_users_database_url, redis_url;
  • .env.example: AUTOBOT_REDIS_URL, CELERY_BROKER_URL;
  • ansible templates: *_DATABASE_URL, AUTOBOT_REDIS_URL.

Verification

  • No-shrink, per caller (tech-debt(redaction): two modules in autobot_shared both declare themselves the canonical redactor, and they disagree on 7 of 10 field names #17336 method): the corpus grew by 19 contrast names, and 0 names leave the masked set at any caller. New masks per policy:
    • PRECISE: pin, user_pin, pinCode, card_pin, passcode, pin_hash, pincode, …;
    • BROAD: the same minus passcode (already masked by pass).
    • BROAD callers each go from 2437 to 2449 masked names.
  • redact_dict no-shrink: base f0c9745's real redact_dict masked-name set is pinned as data (redact_dict_base_masked_names_f0c9745f32.txt, 2439 names). The test asserts base ⊆ new across int, float, digit string, JWT, True, an sk- string, a webhook URL and an endpoint URL.
    • Mutation: dropping mask_credential_urls=True fails 5 tests.
  • redact_nested no-shrink: main c5279ee's real redact_nested masked set is pinned as data (redact_nested_base_masked_names_c5279ee366.txt, one line per name: 19,635 (name, value) pairs over 7 values, including a URL with a secret in its path and a URL with userinfo). The test asserts base ⊆ new.
    • Mutation: re-adding _endpoint to the exemption fails the urlpath case.
    • This test also caught a second shrink (db-dsn/token-url dash normalisation), fixed.
  • Over-mask trade-off pinned: a whole value 12:30@site becomes 12:**********@site, by an explicit test.
  • Gate reviews: two Opus gate reviews returned changes, and every finding is fixed in this branch. The second found the _endpoint shrink, a guard(api): a response model with no declared fields and extra:allow passes the secret-field guard while permitting any field #17899 test lost in the rebase (restored verbatim), and the missing security(redaction): scheme-less user:password@host strings are not scrubbed by the canonical redactor #18206 inventory. The first found:
    • HIGH: webhook_secret_url unmasked;
    • MED: scheme-less misses, pin_hash, a no-shrink test blind to URLs;
    • LOW: datetime fail-closed, C:\x@y, the gpio_pin documentation.
  • Probes: redact_dict({'webhook_secret_url': …, 'token_url': …}) masks both. user:p@ss@host gives user:**********@host, and C:\x@y, bob@example.com and host:5432 are unchanged.
  • Tests: autobot_shared/*redact*test*.py, redact_url_credentials_test.py, the census, threat-model anchors, credential_field_name_only_17336, symbol_fork_ratchet_17312 and no_secret_in_response_model_17865 give 499 passed. Backend llm_config_redaction_18193_test, llm_shared/tests/test_credential_redaction.py and llc/tests/test_replay*.py give 107 passed.
  • A 20k-character adversarial input to the scheme-less matcher runs in under 50 ms (asserted).

Risks

  • Over-masking is the direction of every change. Replay exports and logs show ********** where they showed sk-1...cdef, and hardware/version *_pin names are masked.
  • Scheme-less matching on whole values. A whole value shaped word:x@host is masked. This is documented.
  • Rebase history. Built while security(llm): GET /api/llm/config redacts credentials in the nested backend.llm subtree (#18193) #18219 was open, then rebased onto main after it merged. Its mask-all-userinfo rule is kept, as an explicit commit, and re-verified on main: 468 + 107 passed.

Model Used

Anthropic Claude Opus 5.5 (claude-opus-5-5), coordinating and gate review. Anthropic Claude Sonnet 5.5 (claude-sonnet-5-5) subagents for the survey and implementation.

Issue Link

Closes #18194
Closes #18205
Closes #18206

Related: #18219 (merged base), #18221 (partial reveal outside this module), #16941 (redact_string's 32+ character pattern, unchanged here).

Changelog fragment

  • N/A (internal redaction hardening; no user-facing surface beyond masked log/export values)

…er both policies (#18194)

Owner decision on #18194: add pin and passcode to the redaction vocabulary, matched as whole
words. A new WHOLE_TOKEN_NOUNS tuple (not BROAD_FRAGMENTS, which is substring-matched and feeds
credential_redaction.SENSITIVE_KEYS) is consulted by PRECISE and BROAD alike. Names are tokenised on
_ - . and camelCase; the name matches when it ends in pin, pin code or passcode.

Masked: pin, user_pin, pinCode, card_pin, passcode. Unmasked: pin_memory, pinned_revision,
pin_length, spinner, opinion, pinyin.

secret_redaction.py was 599/600 lines, so the vocabulary and matcher (CREDENTIAL_SUFFIXES,
AUTHORIZATION_TERMS, BROAD_*, quantity rule, MatchPolicy, matcher) move to
secret_redaction_vocabulary.py. secret_redaction re-exports every public name; the census guard
treats the pair as one canonical home.

Corpus delta, measured before (parent commit) vs after over autobot_shared/redaction_corpus_17336.txt
plus the 11 appended contrast names, value int / digit string / True, nothing removed anywhere:
- The 3,373 pre-existing names: 0 newly masked under either policy (no name there contains the token
  pin or passcode).
- Appended names newly masked, PRECISE (redact_value, RedactedReprMixin, URL query, portability
  export): pin, user_pin, pinCode, card_pin, passcode.
- Appended names newly masked, BROAD (redact_mapping, redact_dict, cot_events, config_revision,
  redact_nested): pin, user_pin, pinCode, card_pin. passcode was already masked under BROAD (substring
  pass).
…ost[:port] value (#18206)

The canonical redactor only scrubbed userinfo when a value carried scheme://. A bare
user:pw@host:5432 (DSN, proxy or endpoint field) passed through. redact_schemeless_userinfo masks the
password of a WHOLE value of that shape to user:**********@host:5432 and is applied by redact_value
and redact_nested (any depth, any name, over a URL-shaped name too).

No false positives, each pinned by a contrast test: name@host.com (no colon), host:port (no @),
mailto: style addresses, prose with whitespace, and scheme URLs (still handled by the URL path).
Not applied in redact_text/redact_content: free text has no value boundary, so note:bob@example.com
would read as userinfo; the boundary doc says so.

Masked set per caller: unchanged for every name (the change is value-side and only widens), asserted
over the full corpus.
…_nested, with a fixed-width mask (#18205)

redact_nested is now the one recursive mapping redactor. redact_dict is a thin composition over it:
- exempt_counts becomes an explicit keyword on redact_nested (default True, unchanged for GET /config
  and /current); redact_dict passes False because this layer always masked a count of a credential
  noun (max_tokens: 4096), and no name may leave a caller's masked set.
- extra_scan is a new optional hook run over every string left unmasked; redact_dict passes
  redact_string so the API_KEY_PATTERNS content scan (incl. the 32+ char generic key) that
  replay_service, safe_repr and the log filter rely on is kept.
- The partial reveal value[:4]...value[-4:] (and the *** for other types) is replaced by the
  fixed-width REDACTED_PLACEHOLDER: eight characters of a key are a disclosure. redact_api_key keeps
  the partial form only for redact_string's pattern matches, reported on the issue.
- _redact_list is gone (dead after the composition).

Behaviour changes, both widening or neutral: tuples/sets are now walked; None or "" under a credential
name is returned as-is instead of ***.

Proof the masked set does not shrink: test_no_name_leaves_redact_dicts_masked_set runs the frozen
main implementation against the new one over the whole 3,384-name corpus for int, float, digit string,
JWT, True and an sk- string: zero names unmasked. The #17336 policy test's redact_dict caller entry
(BROAD, no count exemption) is unchanged and still green.
…o no llm_shared path reveals part of a key (#18205)

redact_string's API_KEY_PATTERNS matches (CredentialRedactionFilter,
safe_repr) still showed value[:4]...value[-4:]. #18205 asks for the
partial reveal to be justified or replaced; nothing justifies it, so
it is replaced. THREAT_MODEL anchors follow the vocabulary split.
… under both policies; gpio_pin consequence recorded (#18194)
… # in the password and an IPv6 host, spares a drive path (#18206)
…act_nested scrubs only plain endpoint names, scalars pass through, no-shrink test pins base as data (#18205)
…se, and re-point THREAT_MODEL anchors (#18205)

Rebasing onto #18219's 0037917 replayed _redact_string_leaf with
mask_username=cred; #18219 masks ALL URL userinfo (a key can sit in
the user slot). Restored here, with the docstring to match. The
redact_text/redact_mapping anchors follow the vocabulary split.
… on main, redact_nested joins the per-caller no-shrink test (#18205)
…15 lines instead of 19,640 (#18205)

Same 19,635 (name, value) pairs, now `<name><TAB><value ids>`. Three
of the seven values (jwt, sk, userinfo) are masked under every corpus
name, so the pair-per-line form was mostly repetition. Mutation
re-adding "_endpoint" to the exemption still fails the urlpath case.
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 41 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 22fc8426-f8dd-4d21-942e-4e09b44833dc

📥 Commits

Reviewing files that changed from the base of the PR and between f2e93ea and a172bf4.


📒 Files selected for processing (15)
  • autobot-backend/llm_shared/credential_redaction.py
  • autobot-backend/llm_shared/tests/redact_dict_base_masked_names_f0c9745f32.txt
  • autobot-backend/llm_shared/tests/test_credential_redaction.py
  • autobot_shared/redact_nested_base_masked_names_c5279ee366.txt
  • autobot_shared/redaction_corpus_17336.txt
  • autobot_shared/secret_redaction.py
  • autobot_shared/secret_redaction_nested_18193_test.py
  • autobot_shared/secret_redaction_nested_no_shrink_18205_test.py
  • autobot_shared/secret_redaction_policy_17336_test.py
  • autobot_shared/secret_redaction_schemeless_18206_test.py
  • autobot_shared/secret_redaction_vocabulary.py
  • docs/developer/REDACTION_BOUNDARY.md
  • docs/developer/THREAT_MODEL.md
  • repo_tests/redaction_concept_census_test.py
  • repo_tests/test_agent_venv_isolation_14278.py

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@mrveiss

mrveiss commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mrveiss
mrveiss marked this pull request as ready for review October 11, 2026 08:50
@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.

…ule_names, not a hand-kept set (#18205)

The hand-kept _STDLIB lacked decimal, so the first module on the
agent's import path to use it read as an uninstalled third-party
package. The interpreter's own list replaces it; a contrast pair pins
that decimal/asyncio need no install and yaml/httpx do.
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