Repository navigation
[DO NOT MERGE] CI timing for the redactor floor (see Consiliency/pmcp#234) - #307
Closed
ViperJuice wants to merge 38 commits into
Closed
ViperJuice wants to merge 38 commits into
ViperJuice wants to merge 38 commits into
Conversation
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Rev 4: the architecture changes so both defect classes close by construction. Every pass now reads the original text and returns spans; overlaps are merged and all replacements land in one step, so no pass can consume a boundary a later pass needs (the round-3 URL-eats-escape leak). process_output redacts BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut never lands inside a credential the redactor has not seen whole (the round-3 backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value rule are gone. Property tests replace hand-picked cases: 3000 random printable passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in place) and M26 (truncate then redact) are red under the property tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Rev 4: the architecture changes so both defect classes close by construction. Every pass now reads the original text and returns spans; overlaps are merged and all replacements land in one step, so no pass can consume a boundary a later pass needs (the round-3 URL-eats-escape leak). process_output redacts BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut never lands inside a credential the redactor has not seen whole (the round-3 backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value rule are gone. Property tests replace hand-picked cases: 3000 random printable passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in place) and M26 (truncate then redact) are red under the property tests. Rev 5: key coverage as a property. The lead's own property test found keys the redactor names but did not redact in every form (passwd/pwd via the policy default only, quoted Authorization stopping at a space, private_key/ credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a quoted Authorization value is redacted whole, a bare value may start with punctuation, and an existing [REDACTED] is inert to every span. The new test derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks); M27 drops passwd from the strong set and the property goes red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Rev 4: the architecture changes so both defect classes close by construction. Every pass now reads the original text and returns spans; overlaps are merged and all replacements land in one step, so no pass can consume a boundary a later pass needs (the round-3 URL-eats-escape leak). process_output redacts BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut never lands inside a credential the redactor has not seen whole (the round-3 backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value rule are gone. Property tests replace hand-picked cases: 3000 random printable passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in place) and M26 (truncate then redact) are red under the property tests. Rev 5: key coverage as a property. The lead's own property test found keys the redactor names but did not redact in every form (passwd/pwd via the policy default only, quoted Authorization stopping at a space, private_key/ credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a quoted Authorization value is redacted whole, a bare value may start with punctuation, and an existing [REDACTED] is inert to every span. The new test derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks); M27 drops passwd from the strong set and the property goes red. Rev 6: three rules inside the span design leaked, two of them regressions against main, and are replaced. The URL pass yields component spans (userinfo, secret query values, fragment) instead of a rewritten URL that swallowed every keyed and shape span inside its query; a literal [REDACTED] in the input is a span that merges with what touches it instead of a shield that disarmed the rule next to it; and process_output keeps only the cap plus the tail of any span that starts before it, so the window's far edge is never emitted. `redact_auth_url` is byte-identical to main again. New properties: every span the redactor collected is absent from the output (over the corpora and generators for query strings and literal markers), the window edge never emits an unredacted value, and a 42-input corpus of what main redacts still vanishes. Authorization gains the plain-word gate and same-line separator; `==` is a comparison. Mutation rows M28-M30, M32, M33. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Rev 4: the architecture changes so both defect classes close by construction. Every pass now reads the original text and returns spans; overlaps are merged and all replacements land in one step, so no pass can consume a boundary a later pass needs (the round-3 URL-eats-escape leak). process_output redacts BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut never lands inside a credential the redactor has not seen whole (the round-3 backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value rule are gone. Property tests replace hand-picked cases: 3000 random printable passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in place) and M26 (truncate then redact) are red under the property tests. Rev 5: key coverage as a property. The lead's own property test found keys the redactor names but did not redact in every form (passwd/pwd via the policy default only, quoted Authorization stopping at a space, private_key/ credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a quoted Authorization value is redacted whole, a bare value may start with punctuation, and an existing [REDACTED] is inert to every span. The new test derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks); M27 drops passwd from the strong set and the property goes red. Rev 6: three rules inside the span design leaked, two of them regressions against main, and are replaced. The URL pass yields component spans (userinfo, secret query values, fragment) instead of a rewritten URL that swallowed every keyed and shape span inside its query; a literal [REDACTED] in the input is a span that merges with what touches it instead of a shield that disarmed the rule next to it; and process_output keeps only the cap plus the tail of any span that starts before it, so the window's far edge is never emitted. `redact_auth_url` is byte-identical to main again. New properties: every span the redactor collected is absent from the output (over the corpora and generators for query strings and literal markers), the window edge never emits an unredacted value, and a 42-input corpus of what main redacts still vanishes. Authorization gains the plain-word gate and same-line separator; `==` is a comparison. Mutation rows M28-M30, M32, M33. The CLI seats' supplementary cases (userinfo plus a non-secret query key, an operator pattern in a URL path, a window shrink from ordinary keyed values) are named tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a. The redactor is keyword-anchored, so it redacts the word after `token`, `secret`, `session`, `password` and `bearer` in prose while letting keyword-less credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan specifies separator-anchored keyword rules with the key as the identifier's last segment, a value gate for whitespace-separated keywords and for `Bearer`, an OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed tokens, class-transition scoring of opaque runs with a 256-char payload bound), a padding-safe split in `redact_secrets`, and a truncation cut that never ends inside a token. Both directions are acceptance criteria: a prose corpus that must survive byte-identically and a 29-class credential corpus that must not. The inherited spike was measured, not trusted: ten defects were found and fixed (elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer` boundary, `0x` addresses, image-sized base64, pod names, newline-spanning separators, `param=value` after a keyword, base64-padding split). Its tests/test_auth.py edit is dropped: both original fixtures pass unchanged. Rev 2: the board found three blocking defects in rev 1, all reproduced and fixed with tests and mutation rows M18-M20 -- the payload bound was applied after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation back-up was bounded on the whole trailing run (a credential after a long path kept its prefix; offset 0 kept a partial token), and quoted values stopped at an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`). Rev 3: two more board defects, both reproduced and fixed with tests and mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a value class that ate the closing quote (`{"password": "hunter2 Bearer x"}` left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and a truncation cut inside a quoted multi-word password leaked its first word (128 of 1452 swept cuts). Keyed values are now redacted first, every looser value class stops at quotes and brackets, and an unterminated quoted value runs to the end of its line; the sweep is 0 of 1452 with coverage asserted. mypy src/ is clean and in the verification. Rev 4: the architecture changes so both defect classes close by construction. Every pass now reads the original text and returns spans; overlaps are merged and all replacements land in one step, so no pass can consume a boundary a later pass needs (the round-3 URL-eats-escape leak). process_output redacts BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut never lands inside a credential the redactor has not seen whole (the round-3 backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value rule are gone. Property tests replace hand-picked cases: 3000 random printable passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in place) and M26 (truncate then redact) are red under the property tests. Rev 5: key coverage as a property. The lead's own property test found keys the redactor names but did not redact in every form (passwd/pwd via the policy default only, quoted Authorization stopping at a space, private_key/ credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a quoted Authorization value is redacted whole, a bare value may start with punctuation, and an existing [REDACTED] is inert to every span. The new test derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks); M27 drops passwd from the strong set and the property goes red. Rev 6: three rules inside the span design leaked, two of them regressions against main, and are replaced. The URL pass yields component spans (userinfo, secret query values, fragment) instead of a rewritten URL that swallowed every keyed and shape span inside its query; a literal [REDACTED] in the input is a span that merges with what touches it instead of a shield that disarmed the rule next to it; and process_output keeps only the cap plus the tail of any span that starts before it, so the window's far edge is never emitted. `redact_auth_url` is byte-identical to main again. New properties: every span the redactor collected is absent from the output (over the corpora and generators for query strings and literal markers), the window edge never emits an unredacted value, and a 42-input corpus of what main redacts still vanishes. Authorization gains the plain-word gate and same-line separator; `==` is a comparison. Mutation rows M28-M30, M32, M33. The CLI seats' supplementary cases (userinfo plus a non-secret query key, an operator pattern in a URL path, a window shrink from ordinary keyed values) are named tests. Rev 7: the differential against main is the never-worse test. The board seat's 4000-row corpus is regenerated in-test and main @ 860636a's recorded removals are a committed oracle fixture (beside this plan; the implementer copies it to tests/fixtures/); a row where rev 7 keeps a piece main removed fails unless it is in an accepted class decided from the row's own key, separator and value (0 unaccepted; 226 row-surfaces in 5 classes; 1852 rows better than main). Four regressions against main are fixed (a quote before Bearer; httpie := and ==; percent-encoded query values; Ruby =>), the serialisation layer is in scope -- structured results are redacted as structure before json.dumps -- and M28 is a two-rule mutant red for its stated reason. Mutation rows M34-M38. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Work-in-progress snapshot for the move from claw to dev0. UNVERIFIED: not measured, reviewed or panel-approved. See HANDOFF-dev0.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gtj1apYGGFYzjYgL26KHKC
Reference code for plan revision 8 (PR #288). Not for merge: the plan PR embeds this code; an executor applies it later. - Keyed values in quotes are redacted INSIDE the quotes, so a redacted JSON document stays JSON and a dict result round-trips as a dict. The rev-8 spike returned 120/400 dict corpus rows as str (main: 30); now 14, a strict subset of main's. JSON-valid corpus rows broken: 0/258 on both surfaces (main 7). - New keyword-list pass: `"password": ["hunter2"]` (pretty-printed as a string) is redacted per element inside its quotes; it leaked on main. - Oracle fixture regenerated from main 9ca081e (auth/policy unchanged since 860636a); string/dict removals identical to the prior fixture; adds main's per-row result type (`dict_types`) so the dict differential also asserts the type is never worse. - Tests for items 3 (depth bound fails closed) and 4 (operator pattern on a percent-encoded query value), each proven by a mutant. Full suite -m 'not live': 4213 passed, 3 skipped, 25 deselected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8FnHrdkBnAGRWfDkbtUti
…uments (see #234) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8FnHrdkBnAGRWfDkbtUti
The keyed-list comment still called a list a stated residual (the new _keyword_list_spans redacts it); the accepted-class docstring said rev 7; `not inner` in _keyword_list_spans could never change output (an empty element makes a zero-length span, which apply_redaction_spans drops); the plan-folder copy of the oracle now matches the fixture byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…d found narrowed (see #234) Board round on plan rev 8 (#288), claude seat, all reproduced on main vs rev 8 before fixing: - B1 a JSON literal after a quoted key stays JSON: null/true/false are kept; a number becomes the string "[REDACTED]" (dict stays a dict). - B2 the Authorization bare value never starts on `[`/`{`; JSON literals after a quoted Authorization key get B1's rule. - B3 `.` no longer blocks a key (`self.password =`); `:` still does (arn). - B4 the camelCase qualifier may start with a capital (`AccessToken=`). - B5 whitespace is `[^\S\r\n]` everywhere rev 8 enumerated `[ \t\xa0]`, on both surfaces (U+3000, U+2003, \v, \f ...). - N1 suffixed keys (`password2`, `passwordHash`, `secret_value`) are secrets when the value is credential-shaped and not a URL/ARN; declared compound keys (`secret_access_key`) are exempt; the whitespace-separator rule gets the `_key`/`_id` and camelCase key shapes. - N2 the keyed-list pass skips lists of objects. - N3 after an UNQUOTED key, a quote whose content starts with `,:]}` closes the enclosing string and is not a value. - N4 the key qualifier is bounded to 8 segments (`"a_"*33000`: 0.12 s). - Policy defaults: value never ends on a backslash and never starts on a quote (the engine redacts quoted values inside the quotes on that surface), which also removes rev 8's opening-quote residual. Seat's JSON fuzz (6000 docs): rev 9 breaks 6/6 (engine/policy), main 1005/1276; the only docs broken here and not on main carry escaped quotes (JSON inside a string leaf, #290). Generator widening, oracle regeneration and new tests: next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
grok, codex and gemini on plan rev 8 (#288); each reproduced on main vs this tree before fixing: - `==` then whitespace is a separator on the engine (main's `[\s:=]+`): `password == hunter2` is redacted; a non-credential-shaped value is a comparison (`if token == expected:` survives). - A percent-encoded query KEY is decoded and the `key=value` pair asked of the engine (`?api%2Dkey=hunter2`). - `Bearer` continues onto an unindented line only with a credential-shaped value that is not a flag/bullet (`Bearer\r\nhunter2`). - The whitespace-rule and Authorization bare values never end on a backslash (a serialised leaf's `\"` stays escaped; dict stays dict). - The keyed-list body is quote-aware (`["first]", "hunter2"]` -- leaked on main too) and honours the `code` qualifier exemption (`error_codes: [...]`). - Dropped the `&` accepted-class arm: it read the output and could not fire (gemini). Seat JSON fuzz: see commit message of the next plan revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…234) After an unquoted key inside a string leaf (`{"a": "token: [x", "b": "]"}`), the quote-aware list body scanned past the string's closing quote to a `]` in a later value and redacted across the boundary: invalid JSON on both surfaces where main left it valid (rev 8 did the same). The list body is now a literal array -- quoted strings or JSON scalars separated by commas, nothing bare -- so that text is not an array, while `["first]", "hunter2"]` still is. Seat JSON fuzz (6000 docs, seed 7): 4/4 broken (engine/policy), main 1005/1276, and 0 documents broken here that main keeps valid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…the whitespace rule (see #234) Found by the widened differential (both pre-existing in rev 8; main keeps them valid / redacted): - A: process_output serialises with json.dumps, which spells non-ASCII whitespace ` `; the shape pass started a span at the `u` and left a lone backslash (invalid JSON, dict -> str: 54 key x whitespace cases). widen_over_escapes() gives a span that follows an escaping backslash the backslash too, on both surfaces. Sweep: 0 of 117 break. - B: the whitespace-separator rule now takes the suffixed-key shape (`password2 abc123def456`, `passwordHash hunter2`, `secret_value\t...`); it already gates on a credential-shaped value, and now excludes URL/ARN values like the separator form. `secret-bearer hunter2` is redacted as on main; `secret-bearer failed` still survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
Defect C from the widened differential: `CLIENTSECRET=`, `dbpassword=`, `Accesstoken\t…`, `mypassword=` leaked (main redacts all; rev 8 too) -- the camelCase qualifier needs a case boundary a case-folded key does not have. Keys may now carry a prefix glued on with no boundary (`glued`, <= 24 chars, never starting right after a backslash, so a JSON `\n` escape's `n` is not a prefix). A glued key is a secret only when the value is credential-shaped and not a URL/ARN; on the whitespace rule it must also be a single-case word (`Ed25519PrivateKey X509Cert` is prose). The `code` qualifier check sees the glued prefix (`errorcode=` stays). The straddle guard also skips a quoted value after an unquoted key that contains the OTHER quote character (`…password='x"], "k": "y'` runs across a JSON string boundary); main never redacts a value that starts on a quote. casing sweep: 0 leaks (main-equal); escape sweep 0/117; seat fuzz 4/4, 0 documents broken here that main keeps valid; "a_"*33000 < 0.3 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…see #234) Regression tests, one per finding class of the board's second round (B1-B5, N1-N4, G1, C2, C4, C6, C7, the policy defaults) and of round 3 (A: no span starts inside a JSON escape; B: suffixed keys on the whitespace rule; C: case-folded compound keys; the literal-array list body; the other-quote straddle; the flag/bullet guard). Each is red on rev 8 (56d7f80) and green here; the round-3 fixes are each pinned by a mutant that the tests kill. The differential is widened along every axis the findings found: dotted, PascalCase/camelCase, suffixed, glued and --flag keys; every str.isspace() separator but CR/LF, alone and after `:`, plus spaced `==` and a quoted key before a bare value; JSON literals and values starting on `,`/`}`; JSON documents with mixed types, nested objects and lists of objects (8 000 rows, 800 dict rows; an axis test asserts each axis is present). The oracle is regenerated from main @ 9ca081e (auth.py/policy.py unchanged since 860636a) and now also records process_output's type over the committed JSON fuzz. Every row worse than main falls in a class decided from its own key, separator, value and wrap: 0 unaccepted. The JSON fuzz (seed 7, 1 500 objects x 4 serialisations) is a property test: wherever the input has no backslash-escaped quote (#290's scope, decided from the input), the output parses on both surfaces, and process_output returns a dict wherever main's did. N4 gets a 2 s timing guard. One production change, authorised by the coordinator: the flag/bullet guard skips only a `--` flag or a lone `-*#>` marker followed by whitespace, on the whitespace rule, the keyed and Authorization continuations and the Bearer next-line check. A marker glued to a value (`token -abc123def`, `password:\n -hunter22`) is part of it and is redacted, as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…ow (see #234) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…s (see #234) A literal U+2028 in a parametrize list made the review tooling refuse the plan bundle for every external seat ("brokered review input contains a transport-active control character"). The whitespace list now uses escapes, and a test fails on any literal Cc/Cf/Zl/Zp character (tab aside) or U+0085 in auth.py, policy.py or this file; appending a literal U+2028 to auth.py fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…sion test (see #234) Every family below was worse than main on the rev-10 grammar audit. Each one has a regression test that fails on rev 9 (60ca193) and passes here, and a mutant of its production change that those tests kill. The families share the keyword regexes, so they land together; each has its own section here. F5/F6 (the maintainer's decision 1). process_output({"token": NaN}) came back as a str; {"max_tokens": 1024} became "[REDACTED]". Now every scalar json.dumps emits (null true false, numbers, NaN, +-Infinity) is left alone after a quoted key, as on main (_JSON_SCALAR_RE). The two test_b1 number rows now assert that. Numeric secrets move to #290. F3. process_output({"t": "\u00a0password=hunter22x"}) kept the value, because the serialiser's \uXXXX (and \b, \f, control, non-ASCII) tail read as a glued prefix. Now every JSON escape is a key boundary: the engine keyword, whitespace and Bearer rules plus the secret/token policy defaults. Follow-up: an escaped space between Bearer/Authorization: and the value (Cookie: session=bearer \u2006(tok)) is part of the separator (_JSON_SPACE_ESCAPE). F4. password:\n\nhunter22x, password:\rX, token\r\r\nX and password\n: X leaked. Now a line break is any \r/\n run, before or after the operator; the unindented-break gate is kept. F2. X-Auth: Bearer hunter22x and token: Bearer abc123def456 leaked. The Bearer (?<![=:]) lookbehinds are gone: the lookahead and plain-word gate already keep token_type=Bearer expires_in=3600. F9. The PEM rule was quadratic: 5.6 s on 264 KB of BEGIN lines. It now has a tempered body, and a timing guard holds it under 2 s. F1. Database:Password=hunter22x leaked. A single : now starts a key; :: does not, and nothing inside an arn:/urn: name does (_RESOURCE_NAME_RE). F8. C:\secret=, DOMAIN\password=, \password hunter22x and \0WM9qpSecret=X leaked. Now a backslash is a boundary. Only a JSON escape's tail (the u00a0 of \u00a0, the n of \n) may never start a qualifier or glued prefix. N1. password===X, password:: X and password=:X leaked. The operator is now a run of up to four :/=. A run holding == or :: is gated on a credential-shaped value. N2. password_=X, password__=X and id_token__Xv=X leaked. A suffix may now hold joiner runs and a trailing joiner. N5. password="hunter22x (unterminated) leaked. Now the run after the quote is the value; when it ends at a double quote it must be credential-shaped. N6/N7. Bearer "x", Bearer (x), Authorization: [x] and Authorization: Bearer "x" leaked. Now the inside of a wrapped value is redacted and the wrapping kept, except after a quoted key. N8. otp_code=abc123def456, mfa_code=... and verification_code=... leaked. Now code under a qualifier that is not a status name (_STATUS_CODE_QUALIFIERS) is a weak key, gated on a credential-shaped value. N9. java -jar x.jar -password hunter22x leaked, as did _token X and ---secret X. The whitespace rule now takes any -/_ flag run and any number of joined segments. Its start is anchored, so that stays linear. N4b. PGPassword abc123def456 and DBPassword X leaked. An acronym glued to a Titlecase key now counts on the whitespace rule. Ed25519PrivateKey X509Cert and random-case gby3zPassword x stay prose (the maintainer's decision 4). Also: the false-positive guard list (71 strings) is pinned as a table of exact engine, policy and dict-leaf outputs. Four entries changed from rev 9, each commented: token::Type and secret::new() lose the engine false positive, and Authorization: [required] / (none) now redact the inside. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…see #234) Revs 8 and 9 each passed a hand-listed differential with 0 unaccepted rows, and a board then blocked each one on an axis the list never produced. The differential is now derived from the oracle's grammar instead: tests/_redaction_grammar.py samples main's keyword, Authorization and Bearer rules, the policy keys, and every escape and scalar json.dumps emits. The docstring lists every axis and the sampling density. - Surfaces: 12 per text row (E and P on raw text and on four json.dumps spellings, process_output(str) and process_output(dict)), plus the result type. Each scalar row is checked through process_output(dict). - Tier 1 (11 335 rows) runs in the default suite, in about 7 s. - Tier 2, the full set (145 983 rows), runs under the new `slow` marker. It is excluded by default via addopts, following `live`, and runs one block per case. The oracle is regenerated from main @ 9ca081e by regen_fixture.py. It is deterministic (gzip mtime=0, sorted keys, base64 wrapped at 76) and carries a fingerprint of each tier's corpus, so a corpus change without a new fixture fails. The fixture and the plan copy are cmp-identical. The accepted classes now implement the stated ones. Each is decided from the row as written (pre, qual, name, suffix, sep, value) and the surface, never from the output: - N3: a glued prefix or suffix of more than 24 alphanumerics; - N10: arn:/urn:; - C3/C3a: D2 whitespace; - N11; - N4: the maintainer's decision 4; - C4: == or :: with a plain value; - C5: suffixed; - C6: glued, any case; - C7; - C8: any unindented line break with a plain value; - N6b: a quoted value on a serialised surface (#290, decision 3); - C10: weak key; qualified code with a non-credential value; - C11: Authorization/Bearer with a plain word, possibly wrapped; - N4c: an inner key word straddling name and suffix. Result: 0 unaccepted on both tiers, and on a second seed of tier 2. The oracle is regenerated by tests/fixtures/regen_redaction_main_oracle.py (now committed; it reproduces the fixture byte for byte). The hand-listed string differential and its word-list classifier are removed. The structured-result differential and the JSON fuzz keep their corpora and now use the grammar classifier. An axis test asserts every claimed axis, escape, scalar and surface is produced. The no-control- characters guard also covers the new module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…alifier axes (see #234) This commit applies the coordinator's decisions on the rev-10 open items. N8. key_code=abc123def456 kept a credential-shaped value that main redacted, because `key` was on the status-qualifier list. `key`, `promo`, `coupon` and `discount` are removed: none of them names a status. Those keys are now weak keys and redact such a value, as on main. The 37 qualifiers that remain are the new stated class C12 ("a `code` key qualified by a diagnostic or descriptive word keeps its value"). It is decided from the qualifier's last segment alone and listed explicitly in the grammar module and in the plan. A test pins the class set equal to _STATUS_CODE_QUALIFIERS. Three new axes in the grammar-derived differential: - a `code` qualifier axis: an exhaustive block of every diagnostic and 13 credential qualifiers, plus sampling in the name-focus and mix rows; - a wrap axis: bare, prose, or the pair in each position main's URL rule reads (path, query pair, query value, fragment); - a URL block derived from main's URL rule and redact_auth_url: userinfo; hosts, including one whose port does not parse; query keys in and out of AUTH_SECRET_QUERY_KEYS, in any case, percent-encoded or not, with credential, plain, encoded or empty values; and the fragment. A piece now counts as present when it appears as written or percent-decoded, so main's re-encoding of what it keeps is not counted as a removal. The N11 class reads the value together with whatever follows it in the row. Found by the URL axis: process_output({"t": 'see https://:pw@h.example:99999?SID=%69abc"'}) came back as a str on rev 9 and on the previous rev-10 commit. Inside a serialised leaf the URL runs up to the backslash of the closing \", and the query-value span ate it. A URL now hands back a trailing backslash, as it does trailing punctuation. Tier 1 has 12 535 rows and tier 2, the full set, 166 183. Both have 0 unaccepted rows, as does tier 2 on a second seed. The slow tier's docstring says how to run it (-m slow) and that CI does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…(see #234) main's sanitize_auth_diagnostic, redact_auth_url and redact_secrets, copied from origin/main (1fb36f2) so the floor replay can be compared with them byte for byte. Test-only: main's keyword rule is quadratic on joiner-rich runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
) The redactor no longer re-implements main's rules. pmcp.redaction_floor replays them -- URL, authorization, bearer, keyword, JWT, then the effective policy patterns, each on the previous one's output as main ran them -- with every character of the intermediate text tracking the input position it came from. What main removed is known exactly, as spans over the input, and every such span is applied unless a named suppression predicate drops it. Rev 10's rules only add. * Main's keyword rule is quadratic on joiner-rich runs (8 KB of 'a-' takes 3 s on main), so it is replayed by main_keyword_matches, a linear matcher. * redact_auth_url is called for real and aligned with its input per component; an alignment that does not reproduce main's output makes the whole URL one floor span. * A floor span over a JSON document is applied inside the string it starts in; only syntax is kept. * The predicates are the stated classes (C3 C3a C4 C5 C6 C7 C8 C10 C11 C12 N3 N4 N10 N11), each decided from the match's own key, separator and value, plus four content-free syntax predicates (separator_syntax, policy_keyword_word, scheme_word, wrapper_syntax). * DEFAULT_REDACTION_PATTERNS is main's list again; rev 10's forms are _ADDITIVE_DEFAULT_PATTERNS, applied beside each effective default. Rev 10 tests that pinned a redaction main made and no stated class covers (a flag or a bullet after a keyword, '-bearer <credential>', a policy value across a JSON string boundary) now pin main's redaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
… time (see #234) B3 of rev 10's board: the split re-stripped the rest of the match at every position, which is quadratic in a run of ':=' (58 s on 264 KB) and reachable from redact_secrets callers with no window (a task's status_message and raw payload, _scrub_sensitive_text). 'A value after it' is now one comparison with where the trailing separator run starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…, B1-B5 (see #234) * Fidelity: the replay's text is main's output byte for byte (vendored main) on tier 1 (tier 2 slow), the JSON fuzz and random text, with no URL fallback; the linear keyword matcher yields the same match tuples as main's real regex on random fragments, adversarial joiner runs (main's regex in a worker under a per-input timeout) and the grammar tiers. * Construction: on every surface of tier 1 (tier 2 slow), the board's multi-pair and wide-separator grids and the JSON fuzz, every unsuppressed floor span is removed and the JSON adjustment keeps syntax only; every piece main removes and this keeps is explained by a named predicate that fired on it. No hand classification. * One case per suppression predicate: fires on its false positive, keeps none of a credential-shaped neighbour. * B1-B5 of rev 10's board, red on e79d75f and green here, each with a mutant that turns it red; timing guards (66 KB, under 1 s, every surface). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…key reading (see #234) * pmcp.keyword_matcher holds main's keyword rule in linear time with no dependency on the rest of pmcp (redact_keyword_values is a drop-in for main's re.sub), so it can be lifted into a fix for main on its own. * The matcher scanned the value run after every separator before asking whether the run held a key, which re-scanned one long value run from every run inside it: 6 s on 66 KB of 'a='. It asks for the key first. * A key of 11 000 'token-' has 11 000 readings: a key over 256 characters is read no way, so no key-reading predicate fires and main's redaction stands (30 s -> 0.6 s). * Timing guards for both, and for 'a-b=' and 'password=b'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…t (see #234) The board's URL probes (userinfo, fragments, a port main cannot parse and its 400-character arm, every query key main redacts, encoded keys) and the .env and YAML multi-pair rows join the construction grid; the URL step's labels get a direct test. The slow tier's adversarial matcher runs stop at 1 500 repetitions (main's regex takes seconds on each). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…budget (see #234) A running regex cannot be cancelled, so the executor-based comparison waited for main's quadratic regex on its longest inputs at shutdown and hung the slow tier. The comparison now uses a pool it terminates, reports what it did not reach as a TIMEOUT, and a test holds that to a 2 s budget on 66 KB of 'a-'. The slow tier's adversarial runs stop at 1 000 repetitions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #234) Main's URL rewrite gives a bare query key an '=' ('?urn:x:tok.' becomes '?urn%3Ax%3Atok=.'), which the piece metric reads as a removal of 'tok.'. Part B of the construction test now checks main's own output for the piece with characters inserted into it before asking the floor for a predicate, and counts those apart. Found by the tier-2 mix block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ntial-shaped value (see #234) The maintainer's decision after rev 11's report: a name=value token after a keyword and whitespace is its own pair only when its value is not credential-shaped (D2), and never directly after Bearer or Authorization, whose value it is. 'x Bearer abcdef=SECRETPART' is redacted as main redacts it; 'token expires_in=3600' and 'token code=404' stay prose. The predicate's look-ahead grows from 8 to 256 characters so a long pair value is judged whole. Main's behaviour wins over an unapproved suppression, so four lines that rev 10 kept because of N11-after-Bearer now read as main reads them: 'token_type=Bearer expires_in=3600', 'Bearer realm="api"', the WWW-Authenticate challenge and 'scheme=Bearer scope=read' (the false-positive table and test_bearer_is_a_scheme_not_a_word pin main's output). The prose corpus drops those two shapes, and its generator never ends a clause on the word 'bearer'. They can come back as a separately reviewed class. Test: red on d3595a0, green here; restoring the old N11 kills it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
One keyword matcher: main's src/pmcp/keyword_matcher.py is taken as it is (the floor's copy differed only in docstrings), and the floor calls its public interface unchanged; tests/test_keyword_matcher.py passes untouched. sanitize_auth_diagnostic stays span-based: main's sequential pipeline is what pmcp.redaction_floor replays, so the floor's URL step now hands back trailing punctuation with main's one-pass rstrip, and so does the additive URL rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
The floor module, the vendored-main helper and the floor tests now describe pmcp.keyword_matcher as the matcher main runs, and the timeout test of the regex comparison uses a stand-in reference instead of a long input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ame, N3 glued only, short plain numbers (see #234) * B-1: the closing wrapping of a Bearer/Authorization value is found by one right-to-left scan (a search anchored at the end restarted at every closer). B-1b: an unindented line break is one reverse search, in the floor's C8 and in the additive rules. B-1c: the predicates are asked once per part of a match, not once per span (one match can split around many of main's markers and each span rescanned the shared value). * A generated timing sweep (tests/_redaction_shapes.py): every unit of every redactor regex, repeated after each trigger word and before each failing tail, 46 096 shapes, through every entry point; the default tier holds the board's shapes and the families it found at 16 KB -> 64 KB with a linear-growth assertion, the slow tier the whole sweep. * B-2: N10 fires only on a key that is a segment of an arn:/urn: name, with its separator and value inside the name -- not on a key after the name across & , ; ( ) ? =. * N-1: N3 counts only the run glued to the key word, never across joiners. * N-2: a plain number is at most 10 decimal digits; longer runs and 0x hex follow main. * N-4: every URL position is kept by a piece or labelled; what nothing covers (an empty, doubled or trailing '&') is labelled url.normal. Each check is red on 3e49b95 and green here, with a mutant restoring rev 11's mechanism that turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
One case over 46 096 shapes ran past the 700 s per-test timeout in the slow tier; each chunk now takes about 90 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…isected resource names, camelCase joins words; linearity on a work count (see #234) * B-3: a marker the URL step synthesises stands for the input it replaced (a redacted query value) or for nothing (an added '='), not for the whole URL, and _Tracked.source emits each input character once, left to right. A later match over many such markers built a value of k x the URL. * B-4: _in_resource_name answers with a binary search over the sorted, disjoint names instead of asking every name for every keyword match. widen_over_escapes reads backslash runs from one precomputed pass. * N-5: N3 treats a case change as a joiner, so camelCase and SHOUTING multi-word keys are keys, as _/- joined ones are (N-1). * T-1: linear time is asserted on a deterministic work count (pmcp.redaction_floor.WORK, test-only; every loop and string build adds what it iterates over or copies), at 16 KB -> 64 KB -> 256 KB on every entry point for every shape both boards found and the composition families. What a count cannot see, a regex's own backtracking, keeps a wall-clock check with a 64x bound over 16x the input (linear 16x, quadratic 256x). The remaining default-tier clock tests are smoke caps (10 s) or use the same ratio bound. * tests/_redaction_shapes.py adds compositions derived from the loops: two leads, interleaved units, markers inside markers, nested JSON and header contexts (24 120 shapes); the slow tier screens all 70 216 on the work count and the regex-derived ones on the clock. Each finding is red on b7b071d and green here, with a mutant restoring the earlier mechanism. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…rk charges; memory linearity; shapes inside atomised pieces (see #234) * B-5 (B-3's class): a re-encoded URL component is one piece standing for all of itself, and every later match inside it copied the whole piece. Now a step spells out each input character once: a later match of the same step reads those characters as the intermediate text (_Tracked.source keeps a per-step frontier). floor_spans copies the removed text only for the syntax parts its predicates read, and applies a span over one input range once. * Work is charged exactly where it is copied (source, the removed text). * S-1: the 256-character windowed searches (_context, the glued tail, the resource-name segment) are backward walks; key readings are computed once per decision. * A peak-memory (tracemalloc) linearity check beside the work count: 8 KB -> 128 KB in the default tier, 16 KB -> 256 KB in the slow tier. * tests/_redaction_shapes.py adds repeats of each match-producing unit inside each context the replay atomises (a re-encoded query key or value, a re-cased host, the ValueError arm, a header value, a JSON string): 476 shapes; on 4b5cac8 156 of them were super-linear, here none. * The seat's repros join LINEAR_SHAPES; a mutant restoring rev 13's source is killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Collaborator
Author
|
Closing without merging. This draft existed only to time CI for the replay-based design; plan revision 15 replaces that design with additive-only rules on top of main's redactor. See #288. 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft opened only to measure CI time for the reference implementation of plan revision 14 (#288) on GitHub runners. It will be closed without merging; the implementation lands through its own PR after the plan is approved.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.