Skip to content

fix(mcp): bound intent and parameter capture work, redact feedback extra keys and numbers - #1217

Open
lucasheriques wants to merge 6 commits into
mainfrom
lucas/mcp-capture-bounds
Open

lucasheriques wants to merge 6 commits into
mainfrom
lucas/mcp-capture-bounds

Conversation

@lucasheriques

@lucasheriques lucasheriques commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Three fixes to what posthog.mcp captures, ported from PostHog/posthog-js#5607. Each one is shown below as before (main) and after (this PR). The outputs come from running one scratch script against the merge base (2c35f04) and this branch. Timings are single runs on a laptop.

On main:

  1. $mcp_intent is redacted in full before truncation keeps 2048 characters, so a long intent stalls capture for seconds.
  2. Tool arguments are sanitized in full before the 100-entry and depth-10 limits cut them. A deeply nested argument raises RecursionError, and a top-level argument named password is captured whole.
  3. $mcp_feedback_<name> object extras send emails and card numbers that sit in keys or in number values.

This PR adds no personal-data redaction to $mcp_parameters, $mcp_response or error text. It does not change which identifiers redact_pii matches.

What changes, by example

1. Free text is redacted only as far as truncation can keep it

sanitize_free_text (used for $mcp_intent, the send_feedback text fields and keys, and unknown tool names) follows the sdk-specs bounded scan. Text up to 3072 characters (the 2048 limit plus a 1024 margin) is redacted whole. Longer text is redacted twice, once cut at 3072 and once cut at 2048. Capture keeps the common prefix of the two results and appends .... A match split by the 2048 cut differs between the two results. A match split by the 3072 cut lies past the end of the second result. So neither reaches the capture, and the cost no longer grows with the input.

| $mcp_intent | main | This PR |
| "1-1." * 2048 (8 KB) | not measured separately | 14 ms, 1-1.1-1.1-1.… (2048 chars) |
| "1-1." * 250_000 (1 MB) | 3.6 s, 1-1.1-1.1-1.… (2048 chars) | 14 ms, same output |
| "1-1." * 3_000_000 (12 MB) | 41.2 s, 1-1.1-1.1-1.… (2048 chars) | 14 ms, same output |
| "1-1." * 3_000_000 (12 MB) | 41.2 s, 1-1.1-1.1-1.… (2048 chars) | under 0.1 ms, [redacted] |
| "lorem ipsum " * 87_382 (1 MB) | 218 ms, lorem ipsum lorem… (2048 chars) | 0.7 ms, same output |
| "s" * 5000, "s" * 9000 or "请帮我查询" * 1800 | first 2048 chars | same |
| "s" * 5000 or "请帮我查询" * 900 | first 2048 chars | same |
| "s" * 9000 (one 9000-char word) | sss… (2048 chars) | [redacted] |
| Refund for jane.doe@acme.com, card 4111 1111 1111 1111 | Refund for [redacted], card [redacted] | same |

Tests pin the bounded scan. A card, an SSN, a phone number (one with an NBSP before a space too) or a URL with userinfo straddles the 2048 cut or the 3072 cut, after a start that shrinks by 1500 characters when redacted. None of their digits or credentials reach the capture. Dropping either of the two redactions fails three of those cases.

2. Tool arguments are limited before they are sanitized

build_captured_mcp_parameters now strips the injected arguments (context, conversation_id, and the SDK-owned llm_model). Then it applies the 100-entry and depth-10 limits through normalize, with strings kept whole. Only after that does it sanitize. A credential-named key is written as [redacted] without its value being read. Read counts come from a dict subclass that counts entries read through items().

Tool call main This PR
100 arguments 100 reads, 100 kept 100 reads, 100 kept
100,000 arguments 100,000 reads, 24 ms, 100,000 kept before truncation 101 reads, 0.1 ms, 100 kept + "...": "[MaxProperties ~]"
one argument nested 1000 deep RecursionError {"deep": {"child": … "[Object]"}}, cut at depth 10 of $mcp_parameters
{"password": "hunter2", "q": "x", "context": "…"} {"password": "hunter2", "q": "x"} {"password": "[redacted]", "q": "x"}
{"context": …, "conversation_id": …, a0 … a99} 100 arguments kept 100 arguments kept (stripped names don't count toward the 100)

On main the credential-key rule only ran inside nested objects, so a top-level argument named password, token or api_key reached $mcp_parameters whole.

3. Feedback object extras redact identifiers in keys and numbers

A host can declare extra report fields for the send_feedback tool. An object extra is sent as JSON text, and main sanitized only its string values. Now each string and number inside it gets the same free-text pass as $mcp_intent. A number whose text that pass changes becomes the string "[redacted]", so the JSON stays valid. Keys get the token pass, the known-format credential check (sk-proj-…, PEM and the other vendor formats), and the PII and URL passes. They skip only the entropy detector. Otherwise names like Get_Organization_Memberships read as credentials, and two such keys collapse into one [redacted] entry. The extra is also limited to 100 entries and depth 10 before it is sanitized.

Declared extras meta (object) and score (number), sent as:

{
  "meta": {"ada@example.com": "x", "card\n4111111111111111": 1, "n": 4111111111111111},
  "score": 4111111111111111
}
Property main This PR
$mcp_feedback_meta {"ada@example.com": "x", "card\n4111111111111111": 1, "n": 4111111111111111} {"[redacted]": "x", "card\n[redacted]": 1, "n": "[redacted]"}
$mcp_feedback_score 4111111111111111 4111111111111111 (top-level numbers are sent as is, per spec)
meta nested 5000 deep RecursionError, whole $mcp_feedback event dropped {"child": … "[Object]"}, cut at depth 10
meta = {"Get_Organization_Memberships": 1, "ISO_8601_timestamp_UTC": 5} same as input same as input
meta = {"n": 10**5000} extra dropped (json.dumps fails) {"n": "[redacted]"}

Keys are sanitized before the object is stringified. Sanitizing the JSON text instead would miss the first key: in card\n4111… the escape's n is a word character, so the card has no word boundary.

What does not change

Identifier matching in redact_pii stays as it is, including card and SSN overlap. Both runs printed the same output:

redact_pii input main and this PR
4111 1111 1111 1111 12/30 [redacted] 12/30
SSN 078-05-1120 SSN [redacted]
4111-1111-1111-1111 and 078-05-1120 [redacted] and [redacted]
id 4111111111111112 (fails Luhn) id 4111111111111112
ip 10.0.0.1, +44 20 7946 0958 ip [redacted], [redacted]
  • $mcp_parameters still gets no personal-data redaction. {"email": "ada@example.com", "note": "call 415-555-0142"} is captured as is on both.
  • An intent of up to 3072 characters is redacted in full, as on main. Longer intents keep their start, as on main.
  • Large string leaves in $mcp_parameters and responses are still scanned in full, and sanitize_event sanitizes parameters a second time, as on main. A 12 MB string argument costs about 3 s across the two passes. This PR bounds entries and depth only. Bounding captured strings the way posthog-js redactCapturedHead does is a follow-up.

Tradeoffs

  • A non-JSON value inside an object extra (a set, bytes, a datetime) is now captured as its text, as $mcp_parameters does, where main dropped the extra.
  • When redaction shrinks the head below 2048 characters, the capture is shorter than truncation would allow. It never holds more than redacting the whole string would.
  • Inside an object extra, a number that passes as a card (for example some 13-digit millisecond timestamps) is sent as "[redacted]". Two keys that both redact to [redacted] collapse into the last one.

Where to look

File What to check
posthog/mcp/_sanitization.py _redact_bounded and _SCAN_MARGIN (the bounded scan), and _sanitize_free_text_key with _is_known_secret (keys)
posthog/mcp/_sanitization.py build_captured_mcp_parameters, _pick_captured_fields, _kept_arguments, limit_entries: pick fields, strip, limit, then sanitize
posthog/mcp/_sanitization.py _sanitize_value_with with its leaf and key functions; _sanitize_free_text_leaf (the number rule), _sanitize_free_text_key
posthog/mcp/_truncation.py normalize's optional redacts_key, and _Walk carrying it
posthog/mcp/feedback.py _capture_extra_value: scalars as is, objects limited and then walked
posthog/test/mcp/test_pipeline.py, test_feedback.py the failing-first parameterized cases

💚 How did you test it?

Each fix has a parameterized test that failed on main first. The test commit lands before the fix.

  • pytest posthog/test/mcp posthog/test/test_exception_capture.py: 756 passed, 11 skipped, after merging main.
  • ruff format --check ., ruff check ., and mypy --no-site-packages --config-file mypy.ini . | mypy-baseline filter are all clean, using the tool versions pinned in uv.lock.
  • The straddle tests fail when either of the two redactions in _redact_bounded is dropped (3 cases each).
  • The before/after tables above come from one uncommitted scratch script run against both checkouts.

📝 Checklist

  • All commits are signed
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file (.sampo/changesets/mcp-capture-bounds.md)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Opus 5.5) wrote this from a brief that came out of the posthog-js#5607 review. Those review rounds rejected every change to card and SSN overlap handling, so redact_pii matching is left alone here.

The first version cut at an ASCII space that no pattern matches across. Review pointed to the sdk-specs bounded scan (cut at limit + 1024 and at the limit, keep the common prefix), and this PR now follows it. The space rule turned long space-free text into [redacted], and it depended on a hand-kept list of characters a number continues across, which the NBSP leak showed is easy to get wrong. Four Sonnet reviewers made a simplify pass. The interrogate gate ran an Opus adversary and a Sonnet adversary, each in its own checkout. Both reproduced an NBSP phone-number leak at the cut and the loss of long space-free text. The Sonnet adversary also found key collisions from the entropy detector and a 4300-digit int that dropped the whole feedback event. All four are fixed in the last fix commit, each with a test, and the adversaries' scripts now print the required output. Their differential fuzzer (head redaction against whole-string redaction) finds 0 mismatches on four seeds. Both also flagged that large string arguments are still scanned in full; that is out of scope here and listed above.

…edback extras

Each fails on main: a 1MB separated-digit intent scans for seconds, captured
parameters read every argument and recurse past the depth limit (a 1000-deep
argument raises RecursionError), a top-level credential-named argument is
captured whole, and object feedback extras keep identifiers in keys and numbers.

Tested: pytest posthog/test/mcp/test_pipeline.py -k 'intent_scan or
captured_parameters' and posthog/test/mcp/test_feedback.py -k 'extras_redact or
scalar_extras'. The new cases fail; the guard cases pass.
…tra keys and numbers

- sanitize_free_text redacts only a head of 2x the intent limit (4096 chars),
  cut at a space no pass matches across: not after a digit or a card/SSN/phone
  separator. A head with no such space is one long word whose entropy verdict
  depends on all of it, so it is captured as [redacted]. 12MB of "1-1." went
  from ~41s to under 1ms.
- build_captured_mcp_parameters strips the injected arguments, applies the
  100-entry and depth-10 limits (normalize, strings whole), then sanitizes. A
  credential-named key is redacted without reading its value. This also redacts
  a top-level argument named e.g. "password", which main captured whole, and
  fixes a RecursionError on deeply nested arguments.
- Feedback object extras: keys and numbers get the free-text pass, a number
  whose text changes becomes "[redacted]", and the extra is limited before
  sanitizing (a 5000-deep extra raised RecursionError). Top-level number and
  boolean extras are captured as sent.

Tested: pytest posthog/test/mcp (711 passed, 11 skipped); ruff format --check,
ruff check, mypy | mypy-baseline filter all clean. The straddle tests fail when
the cut ignores digits and separators.

Reviewer notes: two existing tests now use 4000-char words instead of 5000 so
they stay below the scanned head; a no-space intent over 4096 chars is
[redacted] rather than truncated. Card and SSN matching in redact_pii is
unchanged. Large string leaves in $mcp_parameters are still scanned in full.
@lucasheriques lucasheriques self-assigned this Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

posthog-python Compliance Report

Date: 2026-11-12T14:58:04.037509+00:00
Duration: 254545ms

✅ All Tests Passed!

270/270 tests passed


Capture_V1 Tests

✅ 95/95 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 521ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 511ms
Required Headers.Has Authorization Bearer Header ✅ 511ms
Required Headers.Has Content Type Json ✅ 510ms
Required Headers.Has Posthog Sdk Info Format ✅ 511ms
Required Headers.Has Posthog Attempt Header ✅ 510ms
Required Headers.Has Posthog Request Id ✅ 511ms
Required Headers.Has Posthog Request Timestamp ✅ 510ms
Required Headers.Has User Agent ✅ 512ms
Body Format.Body Has Created At And Batch ✅ 510ms
Body Format.No Api Key In Body ✅ 511ms
Body Format.No Sent At In Body ✅ 511ms
Event Format.Event Has Required Root Fields ✅ 511ms
Event Format.Event Uuid Is Valid ✅ 511ms
Event Format.Event Timestamp Is Rfc3339 ✅ 510ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 528ms
Event Format.Distinct Id Is String ✅ 511ms
Event Format.Distinct Id At Root Not Properties ✅ 511ms
Event Format.Custom Properties Preserved ✅ 511ms
Event Format.Set Properties Preserved ✅ 511ms
Event Format.Set Once Properties Preserved ✅ 510ms
Event Format.Groups Properties Preserved ✅ 510ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 511ms
Event Format.Event Has Required Root Fields Batch ✅ 514ms
Event Format.Event Uuid Is Valid Batch ✅ 514ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 514ms
Event Format.Distinct Id Is String Batch ✅ 514ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 514ms
Event Format.Custom Properties Preserved Batch ✅ 514ms
Event Format.Set Properties Preserved Batch ✅ 514ms
Event Format.Set Once Properties Preserved Batch ✅ 514ms
Event Format.Groups Properties Preserved Batch ✅ 514ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 514ms
Batch Behavior.Multiple Events In Single Batch ✅ 519ms
Batch Behavior.Batch Envelope Smoke ✅ 516ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 507ms
Batch Behavior.Flush At Triggers Batch ✅ 1012ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 511ms
Deduplication.Generates Unique Uuids ✅ 518ms
Deduplication.Different Events Same Content Different Uuids ✅ 513ms
Deduplication.Preserves Uuid On Retry ✅ 6519ms
Deduplication.Preserves Timestamp On Retry ✅ 6520ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 6524ms
Deduplication.No Duplicate Events In Batch ✅ 519ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 510ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 13529ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 6519ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 3019ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 6520ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 511ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 516ms
Response Format Validation.Success No Retry After When All Ok ✅ 513ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 1516ms
Response Format Validation.Success No Retry After When Drop Only ✅ 513ms
Response Format Validation.Response Echoes Request Id ✅ 510ms
Retry Behavior.Retries On 408 ✅ 6520ms
Retry Behavior.Retries On 500 ✅ 6521ms
Retry Behavior.Retries On 503 ✅ 8519ms
Retry Behavior.Retries On 504 ✅ 6520ms
Retry Behavior.Retries On 404 ✅ 6522ms
Retry Behavior.Retries On 429 ✅ 6516ms
Retry Behavior.Retries On 502 ✅ 6519ms
Retry Behavior.Retryable Errors Have Retry After ✅ 3517ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 11517ms
Retry Behavior.Does Not Retry On 400 ✅ 2513ms
Retry Behavior.Does Not Retry On 401 ✅ 2512ms
Retry Behavior.Does Not Retry On 402 ✅ 2515ms
Retry Behavior.Does Not Retry On 413 ✅ 2514ms
Retry Behavior.Does Not Retry On 415 ✅ 2513ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 2515ms
Retry Behavior.Implements Backoff ✅ 19530ms
Partial Batch Handling.Handles 200 Full Success ✅ 2512ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3519ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3517ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 3516ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 6522ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 6518ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 6524ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 6522ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 6520ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 6523ms
Partial Batch Handling.Respects Retry After On Partial ✅ 8521ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3517ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 3520ms
Compression.Sends Gzip Content Encoding ✅ 511ms
Compression.No Content Encoding When Disabled ✅ 511ms
Compression.Compressed Body Is Decompressible ✅ 510ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 2513ms
Event Options.Cookieless Mode Override ✅ 512ms
Event Options.Disable Skew Correction Override ✅ 510ms
Event Options.Process Person Profile Override ✅ 510ms
Event Options.Product Tour Id Override ✅ 510ms
Event Options.Unset Options Omitted ✅ 510ms
Event Options.Options Override In Batch ✅ 514ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 511ms

Capture_Ai Tests

✅ 5/5 tests passed

View Details
Test Status Duration
Routing.Capture Ai Posts To Ai Endpoint ✅ 513ms
Routing.Capture Does Not Reroute Ai Named Events ✅ 510ms
Identity.Capture Ai Event Has Uuid ✅ 510ms
Identity.Capture Ai Keeps Supplied Uuid ✅ 510ms
Timestamp Format.Non Utc Event Timestamp Is Converted To Utc ✅ 512ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 12ms
Request Payload.Flags Request Uses V4 Query Param ✅ 9ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 9ms
Request Payload.Flags Request Omits Authorization Header ✅ 9ms
Request Payload.Token In Flags Body Matches Init ✅ 10ms
Request Payload.Groups Round Trip ✅ 9ms
Request Payload.Groups Default To Empty Object ✅ 10ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 9ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 10ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 9ms
Request Lifecycle.No Flags Request On Init Alone ✅ 3ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 15ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 9ms
Retry Behavior.Retries Flags On 502 ✅ 313ms
Retry Behavior.Retries Flags On 504 ✅ 314ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 512ms

Feature_Flags_V4 Tests

✅ 149/149 tests passed

View Details
Test Status Duration
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Complete Split] ✅ 11ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Partial Split Missing Config Version] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Partial Split Missing Rule Type] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Partial Split Missing Rule Id] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Partial Split Missing Variant Key] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Wrong Type Config Version] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Wrong Type Rule Type] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Wrong Type Rule Id] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Wrong Type Experiment Id] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Extra Experiment Id] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Split Wrong Type Variant Key] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Wrong Rule Type Literal] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Unknown Fields] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Old Server Variant] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Old Server Boolean] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Old Server Null Variant] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Old Server False] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.V4 Value Takes Precedence] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Missing Flag] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Targeting Match Targeted Release] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Targeting Match Percentage Rollout] ✅ 11ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Rollout Miss Percentage Rollout] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Rollout Miss Experiment] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Experiment Paused] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Holdout] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Holdout Variant Key] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.No Rule Match] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Missing Group Key] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Dependency Error] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Error] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Error With Value] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Malformed Sibling Isolation] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Partial Split String Value] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.V1 At V4] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.V1 Failed] ✅ 9ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Integral Json Number Context] ✅ 10ms
Legacy Getters.Legacy Getter Reads Reader Fixture[Readers.Flag Key Seed] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Target] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Rollout Hit] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Miss Return Default] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Miss Return Default Null] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Miss Continue] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.No Match Default] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.No Match Null] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Order First Match] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Order Targeting Miss] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Target] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Rollout Hit] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Miss Return Default] ✅ 11ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Miss Return Default Null] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Miss Continue] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.No Match Default] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.No Match Null] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Order First Match] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Order Targeting Miss] ✅ 23ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Target] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Rollout Hit] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Miss Return Default] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Miss Return Default Null] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Miss Continue] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.No Match Default] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.No Match Null] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Order First Match] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Order Targeting Miss] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Unicode] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.String.Looks Boolean] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Zero] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Negative Decimal] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Short Decimal] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Exponent] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Max Safe Integer] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Number.Min Safe Integer] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Empty] ✅ 10ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Nested] ✅ 9ms
Legacy Getters.Legacy Getter Reads Typed Value[V2 Value.Object.Max Depth] ✅ 10ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Complete Split] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Partial Split Missing Config Version] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Partial Split Missing Rule Type] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Partial Split Missing Rule Id] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Partial Split Missing Variant Key] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Wrong Type Config Version] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Wrong Type Rule Type] ✅ 8ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Wrong Type Rule Id] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Wrong Type Experiment Id] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Extra Experiment Id] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Split Wrong Type Variant Key] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Wrong Rule Type Literal] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Unknown Fields] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Old Server Variant] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Old Server Boolean] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Old Server Null Variant] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Old Server False] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.V4 Value Takes Precedence] ✅ 8ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Missing Flag] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Targeting Match Targeted Release] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Targeting Match Percentage Rollout] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Rollout Miss Percentage Rollout] ✅ 8ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Rollout Miss Experiment] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Experiment Paused] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Holdout] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Holdout Variant Key] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.No Rule Match] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Missing Group Key] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Dependency Error] ✅ 6ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Error] ✅ 8ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Error With Value] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Malformed Sibling Isolation] ✅ 6ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Partial Split String Value] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.V1 At V4] ✅ 8ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Integral Json Number Context] ✅ 7ms
Legacy Payloads.Legacy Payload Reads Reader Fixture[Readers.Flag Key Seed] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Target] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Rollout Hit] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Miss Return Default] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Miss Return Default Null] ✅ 6ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Miss Continue] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.No Match Default] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.No Match Null] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Order First Match] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Order Targeting Miss] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Target] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Rollout Hit] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Miss Return Default] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Miss Return Default Null] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Miss Continue] ✅ 9ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.No Match Default] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.No Match Null] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Order First Match] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Order Targeting Miss] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Target] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Rollout Hit] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Miss Return Default] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Miss Return Default Null] ✅ 6ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Miss Continue] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.No Match Default] ✅ 8ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.No Match Null] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Order First Match] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Order Targeting Miss] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Unicode] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.String.Looks Boolean] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Zero] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Negative Decimal] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Short Decimal] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Exponent] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Max Safe Integer] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Number.Min Safe Integer] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Empty] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Nested] ✅ 7ms
Legacy Payloads.Legacy Payload Is The Typed Value[V2 Value.Object.Max Depth] ✅ 7ms

Feature_Flags_Local_Evaluation Tests

✅ 4/4 tests passed

View Details
Test Status Duration
Versioned Boolean Matching.Matching Version Missing ✅ 67ms
Versioned Boolean Matching.Matching Version 1 ✅ 69ms
Versioned Boolean Matching.Matching Version 2 ✅ 70ms
Versioned Boolean Matching.Version Only Reload 1 2 1 2 Missing ✅ 38ms

…ck extras

- The head cut read the character before an ASCII space, but redact_pii first
  turns Unicode spaces into ASCII ones, so "+44<NBSP> 7911..." was cut mid-number
  and its digits leaked once earlier redactions shrank the head. The check now
  reads the normalized text.
- Free text up to 8192 chars (4x the limit) is redacted whole, as on main, so
  CJK, newline- or tab-separated text and short prose before a long path keep
  their content. The head cut applies only above that; worst case is bounded.
- Feedback extra keys skip the entropy detector, as resource names do: it read
  names like Get_Organization_Memberships as credentials, and two such keys
  collapsed into one "[redacted]" entry. Keys still get the token, PII and URL
  passes, bounded the same way.
- An int past Python's 4300-digit str() limit inside an extra is "[redacted]"
  instead of raising, and _capture_extra_value keeps sanitizing inside its
  try, so one extra can no longer drop the whole $mcp_feedback event.

Tested: pytest posthog/test/mcp (717 passed, 11 skipped); ruff and mypy
baseline clean; the adversaries' repro scripts print the required output and
their differential fuzzer finds 0 head-vs-whole mismatches on seeds 1, 3, 5, 7.
@lucasheriques
lucasheriques marked this pull request as ready for review November 9, 2026 15:17
@lucasheriques
lucasheriques requested a review from a team as a code owner November 9, 2026 15:17
@veria-ai

veria-ai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

PR overview

The PR bounds intent and parameter capture work in MCP sanitization and redacts additional feedback keys and numbers.

One issue has been addressed, but an open scan-boundary gap can leave sensitive text unredacted in captured intent. Long signed JWTs with credential-bearing claims and URL userinfo can escape recognition when their identifying delimiters fall beyond both scan limits, allowing secrets to remain in $mcp_intent.

Open issues (1)

Fixed/addressed: 1 · PR risk: 5/10

Reviews

Comment thread posthog/mcp/_sanitization.py Resolved
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

SDK performance: posthog-python

Benchmarks completed. No changes flagged for review.

Flags require at least a 5% change in the median and all six pairs agreeing on direction. This is a noise filter, not proof of a performance change or a CI gate.

View all results
Test Measurement Before PR With PR Change Assessment
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (32-byte payload, steady)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (32-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (4,096-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocated memory per operation 113.98 KiB/op 113.98 KiB/op <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocations per operation 1,104.6 count/op 1,104.61 count/op <0.1% more Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 81.67 µs/op 82.38 µs/op 0.9% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 36.13 MiB 36.13 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 37.83 MiB 37.85 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 37.83 MiB 37.85 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 81.21 µs/op 81.98 µs/op 1% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 40.37 MiB 40.37 MiB No observed change Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation 166.54 KiB/op 166.54 KiB/op <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocations per operation 1,097.6 count/op 1,097.61 count/op <0.1% more Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 109.56 µs/op 108.7 µs/op 0.8% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 40.88 MiB 41 MiB 0.3% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 45.93 MiB 46.03 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 45.93 MiB 46.03 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 100.17 µs/op 99.16 µs/op 1% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 50.78 MiB 50.83 MiB 0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation 118.12 KiB/op 118.12 KiB/op <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocations per operation 1,144.55 count/op 1,144.54 count/op <0.1% fewer Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 94.86 µs/op 93.03 µs/op 1.9% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 36.01 MiB 36.03 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 36.1 MiB 36.12 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 36.1 MiB 36.12 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 92.17 µs/op 90.35 µs/op 2% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 40.37 MiB 40.37 MiB No observed change Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation 171 KiB/op 171 KiB/op <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocations per operation 1,137.38 count/op 1,137.38 count/op <0.1% more Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 124.83 µs/op 118.7 µs/op 4.9% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 40.86 MiB 40.82 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 40.86 MiB 40.82 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 40.85 MiB 40.81 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 116.45 µs/op 110.35 µs/op 5.2% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 41.19 MiB 41.2 MiB <0.1% more memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Caller-thread CPU time per operation 61.94 µs/op 59.69 µs/op 3.6% less time Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 61.93 µs/op 59.68 µs/op 3.6% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Caller-thread CPU time per operation 61.86 µs/op 62.38 µs/op 0.8% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 64.41 µs/op 62.65 µs/op 2.7% less time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation 200.98 KiB/op 200.98 KiB/op No observed change Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocations per operation 2,339.33 count/op 2,339.33 count/op No observed change Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 88.29 µs/op 89.01 µs/op 0.8% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 88.28 µs/op 89.01 µs/op 0.8% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation 1.93 MiB/op 1.93 MiB/op No observed change Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocations per operation 23,013.56 count/op 23,013.56 count/op No observed change Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 863.14 µs/op 865.76 µs/op 0.3% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 863.18 µs/op 865.76 µs/op 0.3% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation 19.24 MiB/op 19.24 MiB/op No observed change Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocations per operation 229,654.94 count/op 229,654.94 count/op No observed change Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 8.62 ms/op 8.61 ms/op <0.1% less time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 8.62 ms/op 8.61 ms/op <0.1% less time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 42.38 MiB 42.51 MiB 0.3% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 42.67 MiB 42.77 MiB 0.2% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 42.67 MiB 42.77 MiB 0.2% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 42.59 MiB 42.68 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 42 MiB 42.08 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 42.07 MiB 42.15 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 42.05 MiB 42.15 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 42.33 MiB 42.47 MiB 0.3% more memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation 15.06 KiB/op 15.06 KiB/op No observed change Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocations per operation 173.76 count/op 173.76 count/op No observed change Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 9.75 µs/op 9.75 µs/op <0.1% more time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 9.75 µs/op 9.75 µs/op <0.1% more time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation 23.14 KiB/op 23.14 KiB/op No observed change Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocations per operation 270.27 count/op 270.27 count/op No observed change Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 13.05 µs/op 12.94 µs/op 0.9% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 13.06 µs/op 12.94 µs/op 0.9% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation 35.73 KiB/op 35.73 KiB/op No observed change Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocations per operation 387.27 count/op 387.27 count/op No observed change Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 18.56 µs/op 18.74 µs/op 1% more time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 18.56 µs/op 18.74 µs/op 1% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation 53.61 KiB/op 53.61 KiB/op No observed change Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocations per operation 576.27 count/op 576.27 count/op No observed change Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 26.33 µs/op 26.32 µs/op <0.1% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 26.33 µs/op 26.32 µs/op <0.1% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation 52.16 KiB/op 52.16 KiB/op No observed change Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocations per operation 556.77 count/op 556.77 count/op No observed change Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 25.04 µs/op 24.92 µs/op 0.5% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 25.04 µs/op 24.92 µs/op 0.5% less time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation 17.85 KiB/op 17.85 KiB/op No observed change Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocations per operation 198.27 count/op 198.27 count/op No observed change Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 10.84 µs/op 10.85 µs/op <0.1% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 10.84 µs/op 10.85 µs/op <0.1% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation 19.24 KiB/op 19.24 KiB/op No observed change Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocations per operation 210.77 count/op 210.77 count/op No observed change Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 11.61 µs/op 11.57 µs/op 0.3% less time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 11.61 µs/op 11.57 µs/op 0.4% less time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation 14.05 KiB/op 14.05 KiB/op No observed change Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocations per operation 160.76 count/op 160.76 count/op No observed change Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 9.18 µs/op 9.1 µs/op 0.8% less time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 9.18 µs/op 9.1 µs/op 0.8% less time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation 15.22 KiB/op 15.22 KiB/op No observed change Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocations per operation 177.77 count/op 177.77 count/op No observed change Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 10.17 µs/op 10.13 µs/op 0.3% less time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 10.17 µs/op 10.13 µs/op 0.4% less time Not flagged

Values are medians of paired measurements. Not flagged does not mean unchanged. Unavailable means a measurement could not be provided, not zero. Estimated allocation measurements retain their instrumentation uncertainty. RSS measures the whole process, not just SDK allocations; observed peaks are sampled checkpoints. Paired ranges in the diagnostics describe variability, not confidence intervals.

Each shard uses its own runner; compare Before PR against With PR within a row, not across shards.

Compared 7eabca81234311dfd86e516c4354c5322f73bfc5 → d526a20de727f45a596e808cb0672ee124021168.

Full report and diagnostics

102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in comparison.json, including per-runner environments, resource limits, measurement methods, samples and checkpoints.

@lucasheriques lucasheriques added the stamphog Request stamphog review label Nov 9, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-add the stamphog label to request another review once you have addressed this.

The credential-disclosure concern from @veria-ai remains unaddressed despite the reported no-findings review. Retain known credential-format checks for feedback keys, add regression coverage, and request review from Georgis Andonis or a security-aware teammate before re-requesting.

  • Author wrote 69% of the modified lines and has 9 merged PRs in these paths (familiarity MODERATE).
  • PostHog Review (Standard) reviewed the current head: no findings.
  • @veria-ai on posthog/mcp/_sanitization.py: the feedback-key sanitizer still applies only the PostHog-specific token pattern, personal-data redaction, and URL sanitization, bypassing the broader credential checks used for string values. Non-PostHog credential-shaped keys can therefore remain in outbound feedback metadata. The discussion from @veria-ai[bot] repeats this still-unfixed concern.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 272L, 3F substantive, 469L/7F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (469L, 7F, two-areas, fix)
stamphog 2.7.0 .stamphog/policy.yml @ 311d11b · reviewed head 311d11b

@stamphog stamphog Bot removed the stamphog Request stamphog review label Nov 9, 2026

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bounded traversal and feedback key/number redaction fixes are verified. One intentional long-text retention change still needs reconciliation with the current MCP Payload privacy contract; details inline.

AI-assisted review.

Comment thread posthog/mcp/_sanitization.py Resolved
@dustinbyrne
dustinbyrne requested a review from a team November 9, 2026 19:08
…scan; keep known credential formats in feedback keys

Long free text (intent, feedback fields, feedback keys) now follows the
sdk-specs bounded scan: redact the text cut at limit+1024 and at the limit,
keep their common prefix. Text with no ASCII-space boundary (CJK, one long
word) keeps its start instead of becoming [redacted]. Feedback keys keep the
known-format credential checks (sk-proj-..., PEM) and still skip entropy.

Tested: pytest posthog/test/mcp + test_exception_capture (756 passed,
11 skipped); straddle tests fail under either one-scan mutation; ruff, mypy
baseline clean.
@lucasheriques lucasheriques added the stamphog Request stamphog review label Nov 12, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed d526a20: 1 Must fix.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment on lines +433 to +437
kept = os.path.commonprefix(
[
redact(value[: _MAX_USER_INTENT_LENGTH + _SCAN_MARGIN]),
redact(value[:_MAX_USER_INTENT_LENGTH]),
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · Redact URL credentials that extend past both scan cuts

With intent "https://admin:" + "p" * 4000 + "@localhost/x", both slices end before @, so neither URL pass recognizes userinfo. _redact_bounded retains their raw common prefix and captures roughly 2 KB of the password in $mcp_intent; the previous full pass removes it. Preserve the privacy contract (AGENTS.md:21) by redacting incomplete URLs at the scan boundary.

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-add the stamphog label to request another review once you have addressed this.

The current-head review raises a security concern that the code still exhibits; the discussion bot's all-clear claim does not resolve it. Fix @PostHog's inline finding in posthog/mcp/_sanitization.py, add a regression test for credentials extending beyond both scan cuts, and request fresh review from a teammate or agent; Georgis Andonis or Aleksander Błaszkiewicz are suggested reviewers.

  • Author wrote 61% of the modified lines and has 9 merged PRs in these paths (familiarity MODERATE).
  • PostHog Review (Standard) reviewed the current head: 1 finding, posted on the PR.
  • posthog[bot] reviewed the current head.
  • @PostHog, posthog/mcp/_sanitization.py: The URL-credential exposure remains unfixed. _redact_bounded sanitizes only the first 2048 and 3072 characters and retains their common prefix. For an HTTPS URL with username admin and a 4000-character repeated-letter password, both cuts precede the userinfo delimiter; the URL sanitizer leaves these incomplete authorities unchanged, retaining password text in captured intent. The full URL would have its userinfo removed.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 309L, 4F substantive, 500L/8F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (500L, 8F, two-areas, fix)
stamphog 2.8.0 .stamphog/policy.yml @ d526a20 · reviewed head d526a20

@stamphog stamphog Bot removed the stamphog Request stamphog review label Nov 12, 2026
return redact(value)
kept = os.path.commonprefix(
[
redact(value[: _MAX_USER_INTENT_LENGTH + _SCAN_MARGIN]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Incomplete prefix scans can bypass redaction

Both scans can agree on raw sensitive text: a verified 1,631-character signed JWT starting at offset 1,550 has its signature separator at 3,137, so neither prefix recognizes it, and the final $mcp_intent contains a decodable db_password claim that the pre-change sanitizer redacted. Fail closed on unfinished credential-like spans at the scan boundary rather than treating their common prefix as safe; URL userinfo whose @ falls beyond both cuts is affected too.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants