From 01cf53d8493e09793ab03e30a3899165c6b96418 Mon Sep 17 00:00:00 2001 From: Alex Markevich Date: Mon, 2 Nov 2026 16:49:24 -0600 Subject: [PATCH 1/6] fix(sanitize): catch Stripe/Slack-app/SendGrid/PyPI keys and non-http URL credentials The diagnose/probe-adapter egress guard let identifier-shaped credentials through when every alnum run was shorter than the entropy arm's 32 chars (Stripe sk_/rk_ live+test keys, Slack xapp- tokens, SendGrid SG. keys, PyPI upload tokens), and _URL_CRED_RE only matched http(s), so postgres://, redis://, mongodb+srv://, amqp:// and ssh:// userinfo passed. Add anchored vendor-format prefix arms and widen the URL rule to any RFC 3986 scheme. Pin both with a canary corpus plus near-miss slugs and userinfo-without-password cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ft1VHSpvtJuygqQbCvsoTT --- CHANGELOG.md | 4 ++++ src/bmad_loop/sanitize.py | 16 ++++++++++++++-- tests/test_sanitize.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94aef5520..51ff1cf85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -237,6 +237,10 @@ breaking changes may land in a minor release. - Pin the run journal and `state.json` to the run dir's recorded identity: writes and re-reads refuse a run dir swapped for a link or another directory instead of acting outside the run, and Windows verify-stream logs refuse junctions. +- Catch more credential shapes in the `diagnose` / `probe-adapter` egress guard: Stripe + `sk_`/`rk_` live and test keys, Slack app-level `xapp-` tokens, SendGrid `SG.` keys and + PyPI upload tokens are now redacted, and URL credentials are flagged for any scheme + (`postgres://`, `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`), not only http(s). ## [0.13.2] — 2026-10-09 diff --git a/src/bmad_loop/sanitize.py b/src/bmad_loop/sanitize.py index a0a4c6768..73c518ca1 100644 --- a/src/bmad_loop/sanitize.py +++ b/src/bmad_loop/sanitize.py @@ -77,7 +77,16 @@ _SECRET_PREFIX_RE = re.compile( r"^(?:" r"sk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|glpat-|gss_|" - r"xox[baprs]-|AKIA|ASIA|AIza|ya29\.|AGAPP|hf_|npm_|dop_v1_|sk-ant-" + r"xox[baprs]-|AKIA|ASIA|AIza|ya29\.|AGAPP|hf_|npm_|dop_v1_|sk-ant-|" + # Shapes the entropy arm below cannot see: each splits at `.`/`-`/`_` into + # runs shorter than `_SECRET_RUN_MIN`, so without a prefix they pass as plain + # identifiers. Stripe secret/restricted keys (legacy ones carry a 24-char + # tail), Slack app-level tokens (`xapp--…`, every segment short), SendGrid + # (`SG.<22>.<43>`, the 43 often split by `-`), and PyPI upload tokens, whose + # macaroon always opens `pypi-AgE`. Each arm is anchored on enough of the + # vendor format that an ordinary slug (`sk_util`, `pypi-mirror`, `SG.1`) + # does not trip it. + r"[sr]k_(?:live|test)_|xapp-\d+-|SG\.[A-Za-z0-9_-]{16,}\.|pypi-AgE" r")" ) _JWT_RE = re.compile(r"^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+") @@ -89,7 +98,10 @@ # Token shape used by assert_no_leak to re-scan rendered output for secrets. _LEAK_TOKEN_RE = re.compile(r"[A-Za-z0-9._/+-]{6,}") -_URL_CRED_RE = re.compile(r"https?://[^/\s]*:[^/@\s]+@") +# Any RFC 3986 scheme, not only http(s): connection strings (`postgres://`, +# `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`) carry `user:password@` +# userinfo the same way and are as likely to reach a log tail or `--help` blob. +_URL_CRED_RE = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/\s]*:[^/@\s]+@") # The guard constructs whose match can straddle a truncation point AND whose # leading fragment would still be sensitive once the rest is clipped away. # `scrub_text`'s cut retracts out of these -- see `_truncate_line`. This sits diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index c550fb4cd..0deccb737 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -236,6 +236,15 @@ def test_scrub_event_payload_is_scrub_json(home): "glpat-xxxxxxxxxxxxxxxxxxxx", # gitlab pat "AIzaSyA0000000000000000000000000000000", # google api key "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", # 40-char high-entropy hex secret + # Shapes whose every alnum run is shorter than _SECRET_RUN_MIN, so only a + # prefix arm can catch them. Low-entropy canaries in the vendors' formats, + # not live keys. + "sk_live_CANARYxxxxxxxxxxxxxxxx", # stripe secret, legacy 24-char tail + "sk_test_CANARYxxxxxxxxxxxxxxxx", # stripe test secret + "rk_live_CANARYxxxxxxxxxxxxxxxx", # stripe restricted key + "xapp-1-A0CANARY01-0000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", # slack app-level + "SG.CANARYxxxxxxxxxxxxxxx.CANARYxxxx-xxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx", # sendgrid + "pypi-AgEIcHlwaS5vcmcCANARY", # pypi upload token ], ) def test_looks_like_secret_catches_credentials(value): @@ -252,6 +261,12 @@ def test_looks_like_secret_catches_credentials(value): "01234567-89ab-cdef-0123-456789abcdef", # UUID: short runs at hyphens "DW-1", "1.2-add-logging", + # Near-misses for the vendor arms: each shares a prefix's first letters + # but not the format the arm is anchored on. + "sk_util", + "pypi-mirror", + "SG.1", + "xapp-name", ], ) def test_looks_like_secret_passes_safe_slugs(value): @@ -311,6 +326,12 @@ def test_assert_no_leak_clean_text(): [ ("contact me@example.com", "email"), ("see https://user:pass@host/x", "url-credentials"), + # Not only http(s): connection strings carry userinfo the same way. + ("dsn postgres://app:hunter2@localhost:5432/db", "url-credentials"), + ("REDIS_URL=redis://default:hunter2@cache:6379", "url-credentials"), + ("uri mongodb+srv://bob:hunter2@cluster0.example.net/x", "url-credentials"), + ("broker amqp://guest:guest@rabbit/vhost", "url-credentials"), + ("ssh://deploy:hunter2@host/repo.git", "url-credentials"), ("path /home/alice/x", "absolute-home-path"), # Every arm of _ABS_HOME_RE, not just the Linux one. The macOS and root # arms were correct but unpinned; so was the drive-letter-plus-FORWARD- @@ -352,6 +373,18 @@ def test_assert_no_leak_fires(text, rule): assert rule in sanitize.assert_no_leak(text) +@pytest.mark.parametrize( + "text", + [ + "ssh://git@github.com:22/org/repo.git", # user, no password + "git@github.com:org/repo.git", # scp-style, no scheme + "see https://host:8443/path", # a port is not a password + ], +) +def test_assert_no_leak_url_rule_needs_a_password(text): + assert "url-credentials" not in sanitize.assert_no_leak(text) + + def test_assert_no_leak_sees_wsl_unc_through_the_json_render(): """#512's literal reproduction, asserted on BOTH encodings. From f788d70f747b9729ee7c4fd6f68d27d386275e83 Mon Sep 17 00:00:00 2001 From: Alex Markevich Date: Mon, 2 Nov 2026 17:24:53 -0600 Subject: [PATCH 2/6] fix(sanitize): redact URL userinfo before emails; stop URL rule at ?/# Address CodeRabbit review on #1027: - scrub_text ran the email rule first, so for redis://bob:hunter2!x@example.com it took the x@example.com tail and left hunter2! behind with the @ gone; the guard's URL rule then passed it. Redact the whole user:password@ first, with a marker that does not itself read as userinfo. - _URL_CRED_RE's halves could cross ? and #, so redis://cache:6379?client=user@cluster made the guard refuse a report with no credentials in it. Neither half now crosses /, ? or #; the password half runs to the last @ in the authority, so user:p@ss@host is redacted whole. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ src/bmad_loop/sanitize.py | 19 ++++++++++++++++--- tests/test_sanitize.py | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ff1cf85..e7da559d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,6 +241,9 @@ breaking changes may land in a minor release. `sk_`/`rk_` live and test keys, Slack app-level `xapp-` tokens, SendGrid `SG.` keys and PyPI upload tokens are now redacted, and URL credentials are flagged for any scheme (`postgres://`, `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`), not only http(s). + `scrub_text` now redacts a URL's whole `user:password@` before emails, so an + email-shaped tail can no longer leave part of the password behind, and an `@` after a + port's `?`/`#` is no longer mistaken for credentials. ## [0.13.2] — 2026-10-09 diff --git a/src/bmad_loop/sanitize.py b/src/bmad_loop/sanitize.py index 73c518ca1..bb2d6d13a 100644 --- a/src/bmad_loop/sanitize.py +++ b/src/bmad_loop/sanitize.py @@ -101,7 +101,12 @@ # Any RFC 3986 scheme, not only http(s): connection strings (`postgres://`, # `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`) carry `user:password@` # userinfo the same way and are as likely to reach a log tail or `--help` blob. -_URL_CRED_RE = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/\s]*:[^/@\s]+@") +# Neither half crosses an authority delimiter (`/`, `?`, `#`), so a port plus a +# query that happens to hold an `@` (`redis://cache:6379?client=user@cluster`) is +# not mistaken for a password. The password half may hold `@` and runs to the +# LAST one in the authority, as URL parsers split it, so `user:p@ss@host` is +# matched — and redacted by scrub_text — whole rather than up to its first `@`. +_URL_CRED_RE = re.compile(r"(?P[A-Za-z][A-Za-z0-9+.-]*://)[^/?#\s]*:[^/?#\s]+@") # The guard constructs whose match can straddle a truncation point AND whose # leading fragment would still be sensitive once the rest is clipped away. # `scrub_text`'s cut retracts out of these -- see `_truncate_line`. This sits @@ -148,6 +153,10 @@ _REDACTED_STR = "" _REDACTED_SECRET = "" # nosec B105 - redaction marker, not a credential _REDACTED_EMAIL = "" +# Replaces a URL's whole `user:password@`, the `@` included: a marker followed by +# `@` would itself read as `:@` to `_URL_CRED_RE` and make the +# guard refuse the very output that was just made safe. +_REDACTED_USERINFO = "" _REDACTED_DEPTH = "" @@ -258,8 +267,8 @@ def scrub_text(s: str, *, max_lines: int | None = None, max_chars: int | None = """Sanitize free text (a CLI's ``--help`` / ``--version`` / a log tail). Less aggressive than :func:`scrub_json` — help text is the CLI's own and - flag lines must survive — so we only redact the home dir and any emails, - then optionally cap the line count and each line's length. + flag lines must survive — so we only redact the home dir, URL credentials + and any emails, then optionally cap the line count and each line's length. ``max_chars`` bounds each line's **content** at ``max_chars``; a truncated line is emitted as at most ``max_chars`` characters plus the @@ -279,6 +288,10 @@ def scrub_text(s: str, *, max_lines: int | None = None, max_chars: int | None = of the redaction step. """ s = redact_home(s) + # URL credentials go BEFORE emails: the email rule would otherwise take the + # `x@example.com` tail of `redis://bob:hunter2!x@example.com`, leaving + # `hunter2!` behind with the `@` gone, so the guard's URL rule no longer sees it. + s = _URL_CRED_RE.sub(lambda m: m.group("scheme") + _REDACTED_USERINFO, s) s = _EMAIL_RE.sub(_REDACTED_EMAIL, s) if max_lines is None and max_chars is None: return s diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index 0deccb737..c2abb5283 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -379,12 +379,34 @@ def test_assert_no_leak_fires(text, rule): "ssh://git@github.com:22/org/repo.git", # user, no password "git@github.com:org/repo.git", # scp-style, no scheme "see https://host:8443/path", # a port is not a password + "redis://cache:6379?client=user@cluster", # `@` past the authority + "redis://cache:6379#user@cluster", ], ) def test_assert_no_leak_url_rule_needs_a_password(text): assert "url-credentials" not in sanitize.assert_no_leak(text) +@pytest.mark.parametrize( + "text", + [ + # Email-shaped tail: the email rule alone would leave `hunter2!` behind. + "redis://bob:hunter2!x@example.com", + "dsn postgres://app:hunter2@db.example.com:5432/db", + # `@` inside the password: redaction must run to the last `@`. + "redis://bob:p@ss@localhost:6379", + "amqp://bob:a@b@mq.example.com", + ], +) +def test_scrub_text_redacts_url_credentials_whole(text): + out = sanitize.scrub_text(text) + for fragment in ("hunter2", "p@ss", "a@b", "bob:", "app:"): + assert fragment not in out + assert "" in out + # The redacted output must not re-trip the guard on its own marker. + assert sanitize.assert_no_leak(out) == [] + + def test_assert_no_leak_sees_wsl_unc_through_the_json_render(): """#512's literal reproduction, asserted on BOTH encodings. From ab64fcc715698674e5540853d72c35e8235461c2 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 4 Nov 2026 22:40:53 -0800 Subject: [PATCH 3/6] fix(sanitize): keep the URL-credential scan linear _URL_CRED_RE has no literal anchor, and its scheme ran unbounded (`[A-Za-z0-9+.-]*://`), so every position of a long alphanumeric run started a scan to the run's end. That is quadratic on a base64 blob or a minified line, and scrub_text (`.sub`, plus _truncate_line through _TRUNCATION_HAZARD_RES) and assert_no_leak all scan such lines. Two properties make the scan linear: - the scheme is capped at 32 characters (`{0,31}`). A longer scheme still matches, starting later, and scrub_text keeps the scheme group either way; - the user half stops at the first `:` (`[^/?#\s:]*`, RFC 3986's user/password split), so `a:a:a:...` no longer backtracks across every colon. The whole userinfo is still redacted. _URL_CRED_RE.search on one 80k-char line, old -> new: "a"*80000 2.677s -> 0.003s 80k base64 without "/" 2.391s -> 0.003s 80k random base64 0.005s -> 0.003s (its "/" ends each scan) "redis://" + "a:"*40000 19.963s -> 0.001s Pinned by deterministic behavioral tests, not wall-clock ones (docs/testing.md rule 4): - test_url_cred_scheme_is_bounded: the match on "a"*40+"://u:p@h" starts at 8. Restoring the unbounded `*` scheme fails it (match.start() is 0). - test_url_cred_user_half_stops_at_first_colon: group("user") on "redis://a:b:c@h" is "a". Restoring `[^/?#\s]*` for the user half fails it (yields "a:b"). Each ablation failed only its own test. sanitize/diagnostics/probe suites: 374 passed. --- src/bmad_loop/sanitize.py | 8 +++++++- tests/test_sanitize.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/bmad_loop/sanitize.py b/src/bmad_loop/sanitize.py index bb2d6d13a..91b859f6f 100644 --- a/src/bmad_loop/sanitize.py +++ b/src/bmad_loop/sanitize.py @@ -106,7 +106,13 @@ # not mistaken for a password. The password half may hold `@` and runs to the # LAST one in the authority, as URL parsers split it, so `user:p@ss@host` is # matched — and redacted by scrub_text — whole rather than up to its first `@`. -_URL_CRED_RE = re.compile(r"(?P[A-Za-z][A-Za-z0-9+.-]*://)[^/?#\s]*:[^/?#\s]+@") +# The scheme is capped at 32 characters and the user half stops at the first `:` +# (RFC 3986's user/password split). The pattern has no literal anchor, so an +# unbounded scheme restarts a scan at every position of a long alphanumeric run. +# That is quadratic on a base64 blob or a minified line, and scrub_text, +# _truncate_line and assert_no_leak all scan such lines. A longer scheme still +# matches, starting later; scrub_text keeps the scheme group either way. +_URL_CRED_RE = re.compile(r"(?P[A-Za-z][A-Za-z0-9+.-]{0,31}://)(?P[^/?#\s:]*):[^/?#\s]+@") # The guard constructs whose match can straddle a truncation point AND whose # leading fragment would still be sensitive once the rest is clipped away. # `scrub_text`'s cut retracts out of these -- see `_truncate_line`. This sits diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index c2abb5283..2d7a24f0b 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -387,6 +387,33 @@ def test_assert_no_leak_url_rule_needs_a_password(text): assert "url-credentials" not in sanitize.assert_no_leak(text) +def test_url_cred_scheme_is_bounded(): + """The scheme is capped at 32 characters, one of the two properties that keep + the unanchored URL-credential scan linear on a long alphanumeric run. A longer + scheme still matches, starting later. + + Ablation: restoring the unbounded ``*`` scheme makes the match start at 0. + """ + text = "a" * 40 + "://u:p@h" + m = sanitize._URL_CRED_RE.search(text) + assert m is not None + assert m.start() == 8 + assert "url-credentials" in sanitize.assert_no_leak(text) + + +def test_url_cred_user_half_stops_at_first_colon(): + """The user half stops at the first ``:`` (RFC 3986's user/password split), + the other property that keeps the scan linear. The whole userinfo is still + redacted. + + Ablation: restoring ``[^/?#\\s]*`` for the user half yields ``"a:b"``. + """ + m = sanitize._URL_CRED_RE.search("redis://a:b:c@h") + assert m is not None + assert m.group("user") == "a" + assert sanitize.scrub_text("redis://a:b:c@h") == "redis://h" + + @pytest.mark.parametrize( "text", [ From 8b0f75c582c0dc32179fbfc020402378b33adb38 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 4 Nov 2026 22:41:27 -0800 Subject: [PATCH 4/6] fix(sanitize): see credential prefixes after a URL path separator CodeRabbit's outside-diff finding on f788d70f ("Detect Stripe keys at URL path boundaries"): _LEAK_TOKEN_RE kept `/` inside a token while _SECRET_PREFIX_RE is `^`-anchored, so a key after a path segment (`https://host.test/sk_live_...`, `path/rk_live_...`, `./SG....`) passed assert_no_leak. `a/sk-ant-...` was missed on main too. Drop `/` from _LEAK_TOKEN_RE so it separates tokens. The entropy arm already splits on `/` via _ALNUM_RUN_RE, JWTs are base64url (no `/`), and the URL-credential rule is a separate regex. test_assert_no_leak_finds_prefixed_keys_in_url_paths covers all four shapes. Restoring `/` in _LEAK_TOKEN_RE failed every row (4 failed). sanitize/diagnostics/probe suites, #481 truncation tests included: 378 passed. --- src/bmad_loop/sanitize.py | 4 +++- tests/test_sanitize.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/bmad_loop/sanitize.py b/src/bmad_loop/sanitize.py index 91b859f6f..7a062b0ed 100644 --- a/src/bmad_loop/sanitize.py +++ b/src/bmad_loop/sanitize.py @@ -97,7 +97,9 @@ _SECRET_ENTROPY_MIN = 3.5 # bits/char; pure hex ~4.0, base64 ~6.0, prose/slug well below # Token shape used by assert_no_leak to re-scan rendered output for secrets. -_LEAK_TOKEN_RE = re.compile(r"[A-Za-z0-9._/+-]{6,}") +# `/` separates tokens, so an anchored prefix arm sees a key that follows a path +# segment; the entropy arm already splits on `/` via _ALNUM_RUN_RE. +_LEAK_TOKEN_RE = re.compile(r"[A-Za-z0-9._+-]{6,}") # Any RFC 3986 scheme, not only http(s): connection strings (`postgres://`, # `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`) carry `user:password@` # userinfo the same way and are as likely to reach a log tail or `--help` blob. diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index 2d7a24f0b..3db484c0f 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -387,6 +387,23 @@ def test_assert_no_leak_url_rule_needs_a_password(text): assert "url-credentials" not in sanitize.assert_no_leak(text) +@pytest.mark.parametrize( + "text", + [ + # Each key follows a `/`. The prefix arms are `^`-anchored, so they see + # the key only when `/` ends the token before it. Low-entropy canaries in + # the vendors' formats, not live keys. + "https://host.test/sk_live_123456789012345678901234", # stripe secret in a URL path + "path/rk_live_abcdefABCDEF0123456789", # stripe restricted key + "./SG.CANARYxxxxxxxxxxxxxxx.CANARYxxxx-xxxx", # sendgrid + "a/sk-ant-xxxxxxxxxxxxxxxxxxxxx", # anthropic + ], +) +def test_assert_no_leak_finds_prefixed_keys_in_url_paths(text): + """Ablation: restoring `/` in ``_LEAK_TOKEN_RE`` fails every row.""" + assert "secret" in sanitize.assert_no_leak(text) + + def test_url_cred_scheme_is_bounded(): """The scheme is capped at 32 characters, one of the two properties that keep the unanchored URL-credential scan linear on a long alphanumeric run. A longer From 110ca518023a21469446e40c9045922ddcffa846 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 4 Nov 2026 22:41:49 -0800 Subject: [PATCH 5/6] docs(changelog): tighten the egress-guard entry --- CHANGELOG.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7da559d8..ec7af8e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -238,12 +238,9 @@ breaking changes may land in a minor release. re-reads refuse a run dir swapped for a link or another directory instead of acting outside the run, and Windows verify-stream logs refuse junctions. - Catch more credential shapes in the `diagnose` / `probe-adapter` egress guard: Stripe - `sk_`/`rk_` live and test keys, Slack app-level `xapp-` tokens, SendGrid `SG.` keys and - PyPI upload tokens are now redacted, and URL credentials are flagged for any scheme - (`postgres://`, `redis://`, `mongodb+srv://`, `amqp://`, `ssh://`), not only http(s). - `scrub_text` now redacts a URL's whole `user:password@` before emails, so an - email-shaped tail can no longer leave part of the password behind, and an `@` after a - port's `?`/`#` is no longer mistaken for credentials. + `sk_`/`rk_` keys, Slack `xapp-`, SendGrid `SG.` and PyPI tokens, including inside URL + paths, and `user:password@` URL credentials under any scheme, not only http(s). + `scrub_text` now redacts a URL's whole userinfo, before emails. ## [0.13.2] — 2026-10-09 From b1a44819893a86ff223f11c06b8f52f8d2d00def Mon Sep 17 00:00:00 2001 From: t Date: Wed, 4 Nov 2026 22:48:53 -0800 Subject: [PATCH 6/6] style(sanitize): wrap the _URL_CRED_RE line for ruff format --- src/bmad_loop/sanitize.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bmad_loop/sanitize.py b/src/bmad_loop/sanitize.py index 7a062b0ed..2c8f3a927 100644 --- a/src/bmad_loop/sanitize.py +++ b/src/bmad_loop/sanitize.py @@ -114,7 +114,9 @@ # That is quadratic on a base64 blob or a minified line, and scrub_text, # _truncate_line and assert_no_leak all scan such lines. A longer scheme still # matches, starting later; scrub_text keeps the scheme group either way. -_URL_CRED_RE = re.compile(r"(?P[A-Za-z][A-Za-z0-9+.-]{0,31}://)(?P[^/?#\s:]*):[^/?#\s]+@") +_URL_CRED_RE = re.compile( + r"(?P[A-Za-z][A-Za-z0-9+.-]{0,31}://)(?P[^/?#\s:]*):[^/?#\s]+@" +) # The guard constructs whose match can straddle a truncation point AND whose # leading fragment would still be sensitive once the rest is clipped away. # `scrub_text`'s cut retracts out of these -- see `_truncate_line`. This sits