Repository navigation
Keep rules: wallet tokens ship as sent (0.17.0, #159487) - #71
Conversation
The known-clean set is keyed on the content rules alone, and what is clean depends on the value rules too: a string masked clean with no value rule configured came back unmasked once one was. configure_masking_value_rules and _reset_masking_config now empty it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as sent (ECSCTX_MASK_VALUE_RULES) A keep rule (`KeepRule`, or anything whose `keep` is True with a callable `matches`) goes in the same ECSCTX_MASK_VALUE_RULES list as the label rules; rules are asked in list order and the first match wins. A value a keep rule matches -- a mapping the key walk meets, or JSON text -- is left as sent under any key, a credential's or a card's included, and the walk does not descend into it. Two things it cannot override: - the floor: nothing is kept under a CVV or SAD key, inside a CVV or SAD container (a key the service lists there included), or as the value of a pair labelled as one; such a value is masked as it would be without the rule; - the guard: a match holding a card, CVV or SAD key, or a card number, at any depth, JSON text inside it included, is walked as if nothing matched. A string leaf is a card number when pan_shaped reads it as one and its digits pass int_is_pan: pan_shaped alone reads Google Pay's 13-digit keyExpiration as a card number. A keep matcher that raises counts as no match; a label rule keeps 0.16.0's contract and leaves the marker. A rule's hints must be a collection of strings, or configuring it raises: anything else raised where text is masked. JSON text under a hashing key that holds a kept value deeper is still hashed whole. The key walk asks each mapping once, where it asked twice, and each rule's kind is cached on the rule tuple's identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s text mask_by_patterns sets aside each JSON object a keep rule ships -- in a message, a repr, JSON in a JSON string -- as a `[KEPT-<letters>-MASKED]` placeholder (stash_kept), runs the rules over the rest, and puts each placeholder still there back as it was written. A label's shape with no digit, so every rule leaves it; one a rule destroys takes the value with it. Text that already holds `[KEPT-` is masked as before. kept_spans reads rule 2's objects top down: a label match is no span and is not looked into, an object nothing matches is read member by member, and nothing right after a CVV or SAD key (a quoted key, `key=`, an XML start tag) is kept. A credential before it (`token=`, `"token":`, `Bearer`, `<password>`) does not stop it. Rule 2 and mask_objects run only for label rules, and a rule is asked only about text holding one of its hints, the key walk of JSON text included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
redact_body labels what a label rule matches first (mask_objects), then
sets aside what a keep rule ships (stash_kept), runs its body rules over
the rest and puts the kept values back as written. A credential's JSON
value, an extra secret key such as Connect's `signature`, and a form
value no longer read into one: `"token": "{\"version\"…}"` keeps its
quotes.
An element's entity-encoded JSON is read as it decodes (_encoded_leaf),
and when it keeps a value the element's text is set aside too: base64's
`==` after a run holding `cvv` read as a CVV form field, and about 1 in
250 entity-encoded Apple Pay tokens lost a padding byte to the label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s after ecsctx's A service that masks again after ecsctx (Connect does) must leave what ecsctx ships as sent as it was. is_kept(value, key=None) says whether a mapping or JSON text is one: a keep rule in force matches it first, the guard lets it through, and the key it sits under is no CVV or SAD key. mask_outside_kept(text, mask) runs a service's text masker with each kept value set aside as a `[KEPT-<letters>-MASKED]` placeholder, putting back each one the masker left. Both never raise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…T_SAD_RULES label them as 0.16.0 did Decided for #159487 on 2026-10-01: an Apple Pay or Google Pay token is single-use, and the token as it was sent is what debugs a wallet payment. WALLET_RULES are now keep rules with strict matchers -- an Apple Pay PKPaymentToken, its `paymentData` (EC_v1, RSA_v1), a Google Pay payment method token -- and nothing besides the keys each writes, so a token with a key of its own is walked as before (a PKPaymentToken keeps only its paymentData). Saved-card tokens stay masked; nothing under a CVV or SAD key is kept. Samsung Pay (a JWE) is not matched: no captured token pins its shape yet. WALLET_SAD_RULES = (APPLE_PAY_RULE, GOOGLE_PAY_RULE) are 0.16.0's label rules, unchanged. The golden test runs with them, so masking_0156.json stays byte-identical; with WALLET_RULES every golden input that holds no wallet hint masks exactly as the file says. BREAKING CHANGE: a service listing `ECSCTX_MASK_VALUE_RULES = [*WALLET_RULES]` logs wallet tokens as sent instead of `[SAD-MASKED]`. List WALLET_SAD_RULES to keep 0.16.0's behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: a "Keep rules" subsection under "Value rules" -- the protocol, where rules are asked and in what order, the floor and the guard, why a credential or card key above a kept value does not stop it, the JSON-text and text-floor limits, is_kept and mask_outside_kept, and the cost. The Ottu paragraph carries the #159487 decision and WALLET_SAD_RULES; the pack and type tables, the redact_body and text-rule paragraphs say what a keep rule does there; ECSCTX_REDACT_BODY_LOG_CAP's default 4096 cuts a roughly 5 KB Apple Pay token. CLAUDE.md, ecsctx/CLAUDE.md and the patterns docstring follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te key or a repr comment hides no card data The matcher and the guard read the parsed object, but a keep ships the text as written. json.loads keeps the last of two duplicate keys and ast.literal_eval drops a comment, so a PAN or a CVV in the shadowed key or the comment shipped past the guard, with Ottu's own WALLET_RULES (review C1). A keep decision on text now rests on a parse that refuses a key written twice (json_as_written): JSON text that names one is not kept, in the key walk (text_ruling; the walk of its parse keeps nothing either), in text and in bodies (kept_spans), and by is_kept. A JSON-text leaf naming a key twice inside a match is card data to the guard. A Python repr is kept only where its parse renders back to it exactly. Label rules read text as in 0.16.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing matches _mask_object left any keep match as written and did not look into it, so where the guard refused one -- or it was written otherwise than it parses -- a label rule's match inside it shipped unlabelled, in text and in redact_body, while the key walk and kept_spans walked it (review I1). It now reads such a match member by member, as the brief's guard says: walked as if nothing matched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard read keys and whole leaves, so a CVV named by a pair's
identifier -- {"name": "cvv", "value": "123"} -- rode through a keep
match, even KeepRule(lambda v: True), where the key walk masks it by
the pair (review I2). A mapping with a `value` key and an identifier
(filters._PAIR_IDENTIFIERS) that the key rules read as a card, a CVV or
other SAD is now card data to the guard. The README says what it reads:
keys, pairs and whole leaves; a CVV written inside a longer string is
the matcher's to keep out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…af is text
The matchers checked each object's keys, not its leaves' types, so a
container or a number in a slot -- a `{name, value}` pair labelled cvv in
`signature`, `paymentMethod.displayName` or `signedMessage` -- rode
through a matched wallet (review I2). Every leaf a wallet writes is a
string: Apple Pay's `signature`, `paymentMethod` fields and
`transactionIdentifier`, Google Pay's `signature`, `signedMessage` and
`signedKey`, and `signatures` a list of strings. Anything else is no
wallet's, and is walked as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…umber's shape A string leaf was card data to the guard only when pan_shaped read it and its digits passed int_is_pan, so a card number with a 1, 7, 8 or 9 prefix, or one failing Luhn, shipped inside a kept value (review M1). The guard now reads every pan_shaped leaf as a card number, as the brief has it, but for thirteen bare digits from a 1: epoch milliseconds, Google Pay's keyExpiration, without which no Google Pay token is ever kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading text as written cost a second, strict parse on every KEEP: a wallet-bearing event masked in 683 µs where 0.16.0's label rules took 555. Where keep rules are asked, text_ruling and kept_spans now parse strictly first and read the text as json.loads does only where a key is written twice: 651 µs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…65, not 2033 Google Pay's keyExpiration starts with a 2 from 18 May 2033, and the exemption read only thirteen digits from a 1: every Google Pay token would then have been refused and masked whole. It now reads a 1 or a 2; no card number is thirteen digits from either. A 13-digit run from a 3 still blocks keeping. From the re-review of the keep rules (N1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @jab3z's task in 25s —— View job Review of #71: keep rules for wallet tokens (0.17.0)I read the What I checked
Findings
VerdictNo blocking correctness problems in what I read. Finding 1 is a small performance fix worth doing before Connect adopts this. I'd confirm CI is green on 3.10 and 3.12 before merging, since I couldn't run it here. I can't approve PRs. |
|
@claude review |
|
Claude finished @jab3z's task in 24s —— View job Review of #71: keep rules for wallet tokens (0.17.0)I read the full Checked in
|
… each time `applicable()` built a new tuple whenever a text held some rules' hints and not others (Ottu's WALLET_RULES on a line with an Apple Pay token, say). `_split()` caches on a tuple's identity, so each text added an entry, and the 64th emptied the cache, the configured rules' entry with it: 100 such lines split the rules 305 times. Each selection of a rule tuple now has one tuple, so its split is cached once. On a wallet line masking takes about 82 µs where it took 88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard data is read in a kept value A keep match the guard refuses is walked as if nothing had matched, so a label rule listed after it for the same shape is never asked (a test now pins it). And the guard reads card data only: PII or a credential inside a kept value ships with it, which the README now says beside the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dacian's decision on #159487 (2026-10-01): wallet tokens (Apple Pay, Google Pay) ship in logs exactly as sent. They are single-use, and debugging needs them. Saved-card tokens stay masked.
In 0.16.0,
ECSCTX_MASK_VALUE_RULEScould only replace a shape with its label. Turning Ottu'sWALLET_RULESoff would not have shipped the tokens either:tokencontainer rule masks every leaf of a token held undertoken;signatureandephemeralPublicKeyread as secrets;transactionIdreads as a payment id;signaturein bodies.What this adds
Keep rules:
KeepRule(matches, hints), listed in the sameECSCTX_MASK_VALUE_RULES. The first rule to match wins. A kept value is copied as written, and the walk does not descend into it.Floor: nothing is kept under a CVV or SAD key, or inside a CVV/SAD container (in the key walk, and the key right before an object in text).
Guard: a match is not kept when it holds, at any depth:
{name, value}pair that reads as card, CVV or SAD;So even
KeepRule(lambda v: True)cannot ship card data. One exemption: 13-digit epoch milliseconds (from a 1 or a 2, so until 2065), which is Google Pay'skeyExpiration.Text and bodies: a kept span is set aside behind a
[KEPT-…-MASKED]placeholder before any content rule runs, then restored. This applies inmask_by_patternsandredact_body, entity-encoded bodies included.Helpers for a service's own masking pass:
is_kept(value, key=)andmask_outside_kept(text, mask).Ottu contrib, breaking (
feat(contrib)!):WALLET_RULESare now strict keep rules: the Apple Pay token and itspaymentData, and Google Pay. Exact key sets, version values, and text leaves only.WALLET_SAD_RULESkeep 0.16.0's labels, for any service that wants them.[*WALLET_RULES], so they flip on upgrade, as intended.Fix: configuring value rules clears the strings cached as clean.
Docs: README "Keep rules", the Ottu paragraph, the
redact_bodyand text-rules paragraphs, CLAUDE.md files and docstrings.Verification
golden/masking_0156.jsonis byte-identical underWALLET_SAD_RULES. UnderWALLET_RULES, every golden input without a wallet hint still masks as the file says.perf, 21774f4): 100 wallet lines split the rules 305 times, and now once.docs, a403054): a keep match the guard refuses asks no later rule (a test pins it), and the guard reads card data only, so PII or a credential inside a kept value ships with it.WALLET_RULES, on lines without a wallet: 1.5–4.6% slower.<udf9>): about 88 µs down to 82.Parked (follow-up ticket, none blocking)
[KEPT-. A probe shows that ships the leaf's entity-encodedpasswordin clear.pci. This fails closed.field_typeandkeeploads as a keep rule.Release
Minor: 0.17.0. The release note must name the breaking change: Connect and Ottu PG ship wallet tokens as sent on upgrade. The
feat(contrib)!commit subject carries it into the changelog. Connect and Ottu PG adopt it next, pinning>=0.17.0. Connect also adoptsis_kept/mask_outside_keptin its own pass, and raises its body log cap so a whole Apple Pay token fits.Refs #159487
🤖 Generated with Claude Code