Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0+2026.01.22T18.35.16.272Z.0cc53c87.berickson.20260122.pmss.rulesets.argument
0.1.0+2026.01.26T17.51.31.713Z.b83cda8e.berickson.20260126.blacklist.by.domain
31 changes: 31 additions & 0 deletions docs/concepts/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,34 @@ authentication, safety checks, and analytics while keeping the event format
itself lightweight. Clients only need to agree on the JSON structure of events,
while the server handles durability and routing responsibilities on their
behalf.

## Configuring domain-based event blacklisting

Incoming events can be blacklisted through PMSS rules so that specific domains
either continue streaming, are told to retry later, or drop events entirely.
The `blacklist_event_action` setting controls the action and defaults to
`TRANSMIT`. Define rules under the `incoming_events` namespace and include a
`domain` attribute to scope the behavior per organization. When the action is
`MAINTAIN`, the `blacklist_time_limit` setting controls whether the client
should wait a short time or stop sending forever.

```pmss
incoming_events {
blacklist_event_action: TRANSMIT;
}

incoming_events[domain="example.org"] {
blacklist_event_action: DROP;
}

incoming_events[domain="pilot.example.edu"] {
blacklist_event_action: MAINTAIN;
blacklist_time_limit: DAYS;
}
```

When a client connects, the server extracts a candidate domain from the event
payload and uses it to resolve the `blacklist_event_action` setting. If
a rule returns `DROP`, the client is instructed to stop sending events.
`MAINTAIN` asks the client to retain events and retry after a delay (as defined
by `blacklist_time_limit`), while `TRANSMIT` streams events normally.
7 changes: 7 additions & 0 deletions docs/reference/system_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ runtime.
| `event_auth.hash_identify` | Enables hash-based identity hints (e.g., `/page#user=alice`) for one-off experiments. | disabled | [`learning_observer/learning_observer/auth/events.py`](../../learning_observer/learning_observer/auth/events.py) |
| `event_auth.testcase_auth` | Allows automated tests to tag events with deterministic user IDs. | disabled | [`learning_observer/learning_observer/auth/events.py`](../../learning_observer/learning_observer/auth/events.py) |

### Incoming events blacklist (`incoming_events` namespace)

| YAML path | Description | Default | Used in |
| --- | --- | --- | --- |
| `incoming_events.blacklist_event_action` | Action to take for incoming events (`TRANSMIT`, `MAINTAIN`, or `DROP`) when blacklist rules match. | `TRANSMIT` | [`learning_observer/learning_observer/blacklist.py`](../../learning_observer/learning_observer/blacklist.py) |
| `incoming_events.blacklist_time_limit` | Time limit to return when `blacklist_event_action` is `MAINTAIN` (`PERMANENT`, `MINUTES`, or `DAYS`). | `MINUTES` | [`learning_observer/learning_observer/blacklist.py`](../../learning_observer/learning_observer/blacklist.py) |

## Modules

Modules can define their own PMSS namespaces under `modules.<module_name>`.
Expand Down
2 changes: 1 addition & 1 deletion learning_observer/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0+2026.01.22T18.35.16.272Z.0cc53c87.berickson.20260122.pmss.rulesets.argument
0.1.0+2026.01.26T17.51.31.713Z.b83cda8e.berickson.20260126.blacklist.by.domain
70 changes: 66 additions & 4 deletions learning_observer/learning_observer/blacklist.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import pmss

import learning_observer.settings
import learning_observer.util

# TODO:
# ACTIONS = enum.StrEnum("ACTIONS", names=('TRANSMIT', 'MAINTAIN', 'DROP'))
#
Expand All @@ -16,6 +21,56 @@ class TIME_LIMITS:
[setattr(TIME_LIMITS, limit, limit) for limit in time_limits]


def _parse_blacklist_time_limit(value):
if isinstance(value, str):
normalized = value.strip().upper()
if normalized in time_limits:
return normalized
try:
numeric_value = float(normalized)
except ValueError as exc:
raise ValueError(f"Value '{value}' is not a valid blacklist time limit.") from exc
if numeric_value.is_integer():
return int(numeric_value)
return numeric_value
if isinstance(value, bool):
raise ValueError(f"Value '{value}' is not a valid blacklist time limit.")
if isinstance(value, (int, float)):
return int(value) if isinstance(value, float) and value.is_integer() else value
raise ValueError(f"Value '{value}' is not a valid blacklist time limit.")


# Register blacklist settings
pmss.parser('blacklist_event_action', parent='string', choices=action_modes, transform=None)
pmss.register_field(
name='blacklist_event_action',
type='blacklist_event_action',
description='How to treat incoming events for blacklist checks.',
default=ACTIONS.TRANSMIT
)
pmss.parser('blacklist_time_limit', transform=_parse_blacklist_time_limit)
pmss.register_field(
name='blacklist_time_limit',
type='blacklist_time_limit',
description='Time limits for MAINTAINing blacklist responses. One of PERMANENT / MINUTES / DAYS or a number of milliseconds.',
default=TIME_LIMITS.MINUTES
)


def _extract_event_domain(event):
'''Find the domain of a user via the event
HACK we do not include the user's email throughout the incoming
event pipeline. We might do that in the future. For now, we look
in the places where an email *might* be to extract the domain.
'''
if not isinstance(event, dict):
return None
email = None
if 'chrome_identity' in event:
email = event['chrome_identity'].get('email')
return learning_observer.util.get_domain_from_email(email)


def get_blacklist_status(event):
'''Return the blacklist status of a given event
Returns should look like:
Expand All @@ -26,16 +81,23 @@ def get_blacklist_status(event):
message
}`
'''
# TODO add in the logic to check our blacklist
if False:
event_domain = _extract_event_domain(event)
attributes = {'domain': event_domain} if event_domain else {}
action_type = learning_observer.settings.pmss_settings.blacklist_event_action(types=['incoming_events'], attributes=attributes)

if action_type == ACTIONS.MAINTAIN:
# tell client to keep messages around
time_limit = learning_observer.settings.pmss_settings.blacklist_time_limit(
types=['incoming_events'],
attributes=attributes
)
return {
'status': 'blocklist',
'action': ACTIONS.MAINTAIN,
'time_limit': TIME_LIMITS.MINUTES, # Note this could (and usually would) be a number, but we have a few defaults like 'MINUTES' defined
'time_limit': time_limit,
'message': 'You are blocked for now'
}
if False:
if action_type == ACTIONS.DROP:
# tell client to drop messages
return {
'status': 'blocklist',
Expand Down
Loading