Skip to content

feat(spec,middleware): publisher-side subscription filters - #107

Open
ucekmez wants to merge 1 commit into
feat/manifest-event-catalogfrom
feat/subscription-content-filters
Open

feat(spec,middleware): publisher-side subscription filters#107
ucekmez wants to merge 1 commit into
feat/manifest-event-catalogfrom
feat/subscription-content-filters

Conversation

@ucekmez

@ucekmez ucekmez commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 15 of a stacked series. Base is #106. Not for merge without review.

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.

docs/strategy/unmet-needs-map.md answers 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 filter on 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, so subject (from #100) and any eep_* attribute are reachable, not just data.

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.md already 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_types never 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 / prototype is 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

  • Spec / schema only
  • TypeScript package(s)
  • Python package(s)
  • Tests / CI
  • Docs / examples

Checklist

  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md.
  • Tests added or updated where appropriate.
  • Breaking change? No. filter is optional; a subscription without one behaves exactly as before.
  • Documentation updated for user-visible behavior.

Verification

Suite Result
@eep-dev/middleware 229 passed (was 189)
tests/ 191 passed
compliance-cli --fixtures 26 vectors, 0 failed
tests/cross-impl/test_conformance_fixtures.py 28 passed
codegen-schema-types --check no drift

Notes 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.

Copilot AI lite review requested due to automatic review settings August 26, 2026 19:36

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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>
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