Skip to content

feat(privacy): add watcher-side privacy filter (drop/redact before send) - #135

Open
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/privacy-filter-watcher-side
Open

feat(privacy): add watcher-side privacy filter (drop/redact before send)#135
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/privacy-filter-watcher-side

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

Adds a client-side privacy filter that drops or redacts window events before they are sent to aw-server, so sensitive data never leaves the machine at all.

This mirrors the server-side privacy_filters engine that landed in aw-server-rust#600, allowing users to enforce the same rules at both layers. Previously the watcher only supported dropping titles via exclude_title/exclude_titles (redact to "excluded"). This PR adds explicit drop and redact actions with user-configurable regex patterns and replacement strings.

What changed

  • aw_watcher_window/privacy_filter.py (new) — pure Python, no new deps:
    • compile_privacy_rules(raw) — validates and compiles regex patterns; invalid regexes and unknown actions are skipped with an error log rather than crashing
    • apply_privacy_filters(window, rules) — returns None (drop) or a filtered copy (redact); never mutates the input dict
  • aw_watcher_window/config.py — exposes privacy_filter_rules from [[aw-watcher-window.privacy_filter]] TOML array of tables
  • aw_watcher_window/main.py — privacy filter runs first in transform_window; a None return skips the heartbeat; heartbeat_loop threads the rules through
  • tests/test_privacy_filter.py (new) — 23 tests covering compile, drop, redact, input immutability, edge cases
  • README.md — documents the new config section with examples

Config example

# Drop private-browsing windows entirely
[[aw-watcher-window.privacy_filter]]
pattern = "(?i)private browsing|incognito"
action  = "drop"

# Redact banking titles
[[aw-watcher-window.privacy_filter]]
pattern     = "(?i)bank|my account"
action      = "redact"
replacement = "REDACTED"   # optional; defaults to "excluded"

# Drop by app name (not title)
[[aw-watcher-window.privacy_filter]]
pattern = "(?i)signal"
field   = "app"
action  = "drop"

Rule fields: pattern (Python regex), field (default "title"), action ("drop" or "redact"), replacement (for redact; default "excluded").

Ordering

Privacy filter runs before Research Edition and exclude_title transforms. A "drop" rule exits immediately — subsequent rules are not evaluated for that event.

macOS note

The default swift strategy bypasses this Python transform (same as Research Edition). Use --strategy jxa or --strategy applescript to enable watcher-side privacy filtering on macOS.

Test plan

  • python3 -m pytest tests/test_privacy_filter.py -v — 23 passed
  • python3 -m pytest tests/test_main.py -v — 12 passed (no regressions)
  • Normal run with no config: zero behaviour change
  • drop rule matching title: heartbeat skipped
  • redact rule matching title: heartbeat sent with replacement value
  • Invalid regex in rule: rule skipped with error log, watcher continues

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

Adds watcher-side privacy filtering before window events are sent to the server.

  • Loads privacy-filter rules from TOML configuration.
  • Compiles valid drop and redact rules while skipping malformed rules.
  • Applies filtering before existing research and title-exclusion transforms.
  • Skips heartbeats for dropped events and documents configuration and macOS limitations.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current code catches both regex syntax errors and TypeError from non-string pattern values, logs the malformed rule, and continues startup.

Important Files Changed

Filename Overview
aw_watcher_window/privacy_filter.py Implements rule compilation and immutable drop/redact filtering; the previously reported non-string-pattern crash is fixed by catching TypeError.
aw_watcher_window/main.py Integrates filtering before existing transforms and suppresses heartbeats for dropped events.
aw_watcher_window/config.py Exposes configured privacy-filter tables through parsed arguments.
tests/test_privacy_filter.py Covers rule validation, malformed non-string patterns, filtering order, custom fields, and input immutability.
README.md Documents privacy-filter configuration, behavior, ordering, and the macOS strategy limitation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Read current window] --> B[Apply privacy-filter rules]
    B -->|Drop| C[Skip heartbeat]
    B -->|Pass or redact| D[Apply research/title transforms]
    D --> E[Create window event]
    E --> F[Send heartbeat to aw-server]
Loading

Reviews (2): Last reviewed commit: "fix(privacy): skip non-string regex patt..." | Re-trigger Greptile

Comment thread aw_watcher_window/privacy_filter.py Outdated
Comment on lines +59 to +61
try:
pattern = re.compile(pattern_str)
except re.error as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Non-string patterns crash startup

When a privacy-filter table contains a non-string pattern such as pattern = 123, re.compile raises TypeError, which is not caught here and terminates the watcher during startup instead of logging and skipping the invalid rule.

Suggested change
try:
pattern = re.compile(pattern_str)
except re.error as exc:
try:
pattern = re.compile(pattern_str)
except (re.error, TypeError) as exc:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed in cdf5c3b: compile_privacy_rules now catches TypeError from non-string TOML pattern values and skips the malformed rule. Added a regression test for pattern = 123; all 24 focused privacy-filter tests pass.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob The watcher-side privacy filter should also be configurable via webui (non-technical user-facing central config) similar to the server-side privacy filter (but watcher-only config should also be supported, if e.g. server is untrusted).

Adds a client-side privacy filter that drops or redacts window events
before they are sent to aw-server, so sensitive data never leaves the
machine at all.

New module `aw_watcher_window/privacy_filter.py`:
- `compile_privacy_rules(raw)` — validates and compiles regex patterns;
  invalid regexes and unknown actions are skipped with an error log
- `apply_privacy_filters(window, rules)` — returns None (drop) or a
  filtered copy (redact); never mutates the input dict

Config via `[[aw-watcher-window.privacy_filter]]` TOML tables:
  pattern     = "(?i)private browsing|incognito"
  action      = "drop"           # or "redact"
  field       = "title"          # optional; defaults to "title"
  replacement = "excluded"       # optional; used for redact action

Integration in main.py:
- Privacy filter runs before research and exclude_title transforms
- A None return from transform_window skips the heartbeat entirely
- heartbeat_loop passes privacy_filter_rules through

23 new tests in tests/test_privacy_filter.py; all existing tests pass.

Mirrors the server-side privacy_filters engine in aw-server-rust (#600)
so users can enforce the same rules at both the watcher and server layers.

macOS note: the swift strategy bypasses this Python transform; use
--strategy jxa or --strategy applescript to enable it on macOS.
@TimeToBuildBob
TimeToBuildBob force-pushed the feat/privacy-filter-watcher-side branch from cdf5c3b to 9176770 Compare August 17, 2026 18:33
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Good point. The TOML config in this PR covers the watcher-only path you described (useful when the server is untrusted or the user wants filtering before data ever leaves the machine).

For webui configurability, the shape I'd expect is:

  • aw-server exposes a config endpoint for watcher-side privacy rules (similar to how server-side privacy_filters are managed)
  • aw-webui adds a settings panel for configuring those rules
  • The watcher fetches its config from the server when available, falling back to TOML when disconnected or server is untrusted

That touches aw-server and aw-webui (separate repos), so it's naturally a follow-up rather than something that fits cleanly into this PR. Would you prefer I scope it here (which would expand the PR significantly) or open a follow-up issue to track it?

Also resolved the rebase conflict with master (#136, #137) — the branch is up to date now.

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