feat(spec,middleware): publisher-side subscription filters - #107
Open
ucekmez wants to merge 1 commit into
Open
Conversation
Filtering was `event_types` glob only, and `subscription.request.json` permits `*` solely in the final segment. A subscriber interested in one field of one object still received every event of that type and discarded the rest — after paying full delivery cost. For an LLM agent that cost is tokens, which is precisely the "context bloat" `docs/strategy/unmet-needs-map.md` claims EEP answers with "subscribers choose what and when". The mechanism was thinner than the claim. New §5.1.3 and an optional `filter` on the subscription request: a flat list of comparisons over dotted paths into the envelope, combined with `all` or `any`. Operators are `eq`, `ne`, `in`, `nin`, `prefix`, `exists`, `gt`, `lt`. Three design decisions worth stating: - **No regex operator, and not Turing-complete.** The filter is evaluated by the publisher, on the delivery hot path, against every candidate event. A richer language would let a subscriber hand the publisher a catastrophically backtracking pattern to run at the publisher's expense. Conditions (20) and path depth (8) are bounded for the same reason. `ROADMAP.md` already plans ReDoS fuzzing on gate-config patterns; this avoids opening a second such surface. - **A malformed filter is rejected with 400, not ignored.** A subscriber that believes it is filtering but is not receives traffic it thought it had asked to be spared, and cannot detect the difference from its own side. - **Filters narrow, never widen**, and are not access control. Gates are applied before filters, and a filter cannot reach anything the subscriber's tier does not already grant. An event that fails the filter is not delivered and does not count toward the subscription's failure counter — it was never a candidate, so treating it as a failed delivery would eventually pause a healthy subscription. Path traversal into `__proto__` / `constructor` / `prototype` is rejected at validation, and the reader ignores inherited properties, so a subscriber-supplied path can never reach a prototype lookup. Webhook batching, the other half of audit finding O4, is deliberately left to a separate change: it alters the delivery envelope and the signing unit, which deserves its own review. Refs: EEP audit 2026-08 finding O4 (filters) Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
ucekmez
force-pushed
the
feat/manifest-event-catalog
branch
from
August 26, 2026 19:39
ca5719c to
a741bb4
Compare
ucekmez
force-pushed
the
feat/subscription-content-filters
branch
from
August 26, 2026 19:39
aded512 to
a336653
Compare
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.
Summary
PR 15 of a stacked series. Base is #106. Not for merge without review.
Filtering was
event_typesglob only — andsubscription.request.jsonpermits*solely in the final segment. A subscriber interested in one field of one object still received every event of that type and discarded the rest, after paying full delivery cost. For an LLM agent that cost is tokens.docs/strategy/unmet-needs-map.mdanswers the "context bloat" pain with "Subscribers choose what and when". The mechanism was thinner than the claim.What changed
New §5.1.3 and an optional
filteron the subscription request:{ "event_types": ["com.example.entity.updated"], "filter": { "match": "all", "conditions": [ { "path": "subject", "op": "prefix", "value": "listing/" }, { "path": "data.status", "op": "in", "value": ["published", "archived"] } ] } }Operators:
eq,ne,in,nin,prefix,exists,gt,lt. Paths address the whole envelope, sosubject(from #100) and anyeep_*attribute are reachable, not justdata.Three design decisions worth reviewing
1. No regex operator, and deliberately not Turing-complete. The filter runs on the publisher's delivery hot path against every candidate event. A richer language would let a subscriber hand the publisher a catastrophically backtracking pattern to evaluate at the publisher's expense. Conditions (20) and path depth (8) are bounded for the same reason.
ROADMAP.mdalready plans ReDoS fuzzing on gate-config patterns — this avoids opening a second such surface rather than adding one to the list. There's a negative conformance vector using^(a+)+$to pin it.2. A malformed filter is rejected with
400, not ignored. A subscriber that believes it is filtering but is not receives traffic it thought it had asked to be spared — and cannot detect the difference from its own side.3. Filters narrow, never widen, and are not access control. Gates are applied before filters; a filter cannot reach anything the subscriber's tier does not already grant. A test pins that a permissive filter cannot deliver an event whose type
event_typesnever selected.One subtlety: an event that fails the filter is not counted as a failed delivery. It was never a candidate — treating it as a failure would eventually pause a perfectly healthy subscription.
Security
Path traversal into
__proto__/constructor/prototypeis rejected at validation, and the reader ignores inherited properties — so a subscriber-supplied path can never reach a prototype lookup even if validation were bypassed. Both layers are tested.Scope
Checklist
filteris optional; a subscription without one behaves exactly as before.Verification
@eep-dev/middlewaretests/compliance-cli --fixturestests/cross-impl/test_conformance_fixtures.pycodegen-schema-types --checkNotes for reviewers
Webhook batching — the other half of audit finding O4 — is deliberately not here. It changes the delivery envelope and the signing unit (an HMAC over a batch rather than a single event), which deserves its own review rather than riding along with a filtering change. Coming separately.
No Python middleware parity for filter evaluation — flagging rather than skipping silently. The schema and spec are language-neutral; only the TS dispatcher enforces it today.