feat: add Research Edition privacy filter for window titles#130
feat: add Research Edition privacy filter for window titles#130TimeToBuildBob wants to merge 12 commits into
Conversation
Adds an opt-in transform that classifies browser window titles into study categories and drops non-browser titles entirely, replicating the ERC fork approach without maintaining a separate fork. - New module `research_filter.py`: `is_browser()`, `classify_title()`, `transform()` — all pure functions, no dependencies beyond stdlib - Config: `research_enabled = false` (default off) + TOML table `[aw-watcher-window.research_category_map]` for the researcher's map - CLI: `--research` flag mirrors the config option - main.py: `heartbeat_loop` applies the transform after existing `exclude_title`/`exclude_titles` steps - 19 tests covering is_browser, classify_title, and transform (including input-immutability, url/incognito preservation, macOS JXA extra fields) macOS note: the `swift` strategy delegates to the compiled Swift helper and bypasses this Python transform. Use `--strategy jxa` or `--strategy applescript` on macOS for Research Edition support. Part of ActivityWatch Research Edition for the Lund/IIIEE study.
Greptile SummaryThis PR adds an opt-in Research Edition privacy filter that classifies browser window titles into configurable study categories and drops non-browser app titles entirely. The previous blocking concerns — silent macOS Swift bypass, double
Confidence Score: 5/5Safe to merge; all critical path changes are covered by tests and the previous blocking concerns have been resolved. The Swift binary now correctly handles research mode end-to-end via its own --research/--research-category argument parsing, the Python transform uses is None (not falsiness) so an empty category map correctly applies the filter, non-browser URLs are stripped in both paths, and config loading was consolidated into parse_args. The remaining findings are cosmetic: one Swift log line renders the optional wrapper literally, and the two browser-app lists are duplicated across Swift and Python with no sync guard. aw_watcher_window/macos.swift — one error-log interpolation of the now-optional data.title will print Optional(...). Both browser-app lists (BROWSER_APPS in Python and researchBrowserApps in Swift) need to be updated together when new browsers are added. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Window event captured] --> B{research_enabled?}
B -- No --> C{exclude_title or\nexclude_titles match?}
C -- Yes --> D[title = 'excluded']
C -- No --> E[Send event as-is]
D --> E
B -- Yes --> F{macOS + swift strategy?}
F -- Yes --> G[Pass --research + --research-category\nflags to Swift binary]
G --> H{isResearchBrowser?}
H -- Yes --> I[classifyResearch\nURL-first then title]
I --> J{Pattern match?}
J -- Yes --> K[title = category name]
J -- No --> L[title = 'excluded']
K --> M[Send event: app + title, url stripped]
L --> M
H -- No --> N[Send event: app only\ntitle + url dropped]
F -- No --> O[Python heartbeat_loop\nresearch_transform called]
O --> P{is_browser app?}
P -- Yes --> Q[classify_title\nURL-first then title]
Q --> R{Pattern match?}
R -- Yes --> S[title = category name]
R -- No --> T[title = 'excluded']
S --> U[Send event: app + title, url stripped]
T --> U
P -- No --> V[Send event: app only\ntitle + url dropped]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Window event captured] --> B{research_enabled?}
B -- No --> C{exclude_title or\nexclude_titles match?}
C -- Yes --> D[title = 'excluded']
C -- No --> E[Send event as-is]
D --> E
B -- Yes --> F{macOS + swift strategy?}
F -- Yes --> G[Pass --research + --research-category\nflags to Swift binary]
G --> H{isResearchBrowser?}
H -- Yes --> I[classifyResearch\nURL-first then title]
I --> J{Pattern match?}
J -- Yes --> K[title = category name]
J -- No --> L[title = 'excluded']
K --> M[Send event: app + title, url stripped]
L --> M
H -- No --> N[Send event: app only\ntitle + url dropped]
F -- No --> O[Python heartbeat_loop\nresearch_transform called]
O --> P{is_browser app?}
P -- Yes --> Q[classify_title\nURL-first then title]
Q --> R{Pattern match?}
R -- Yes --> S[title = category name]
R -- No --> T[title = 'excluded']
S --> U[Send event: app + title, url stripped]
T --> U
P -- No --> V[Send event: app only\ntitle + url dropped]
Reviews (8): Last reviewed commit: "fix(research): prioritize research trans..." | Re-trigger Greptile |
dict | None union syntax requires Python 3.10+; replace with Optional[dict] from typing to fix typecheck + runtime errors on py3.9.
|
Pushed Root cause: Fix: Replace |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
Privacy Fix: Strip URLs from Browser EventsI've identified and fixed the privacy gap identified by Greptile's P1 finding: browser URLs were being leaked to the ActivityWatch server despite the privacy filter being active. The IssueOn macOS JXA (the required strategy for Research Edition support), browser events carry full URLs that can expose search queries, authentication tokens, and other sensitive path components. The previous implementation used The Fixif is_browser(app):
# Browser: replace title with a study category
title = window.get("title", "")
category = classify_title(title, category_map)
# Don't spread window dict — URLs must not be exposed for privacy
result = {"app": app, "title": category}
# Preserve incognito flag if present (metadata, not a privacy concern)
if "incognito" in window:
result["incognito"] = window["incognito"]
return resultVerificationTested locally:
The fix maintains the privacy guarantee of Research Edition mode on macOS JXA by ensuring raw browser URLs are never uploaded to the ActivityWatch server, while still preserving the incognito flag as non-sensitive metadata. Commit: |
On macOS JXA, browser events carry full URLs that can expose search queries,
authentication tokens, and other sensitive path components. The previous
implementation used `{**window, 'title': category}` which preserved the url
field via dict spread, leaking URLs to the ActivityWatch server despite the
privacy filter being active.
Changed to explicitly construct the result dict, stripping the URL while
preserving the incognito flag as non-sensitive metadata. Non-browser apps
(e.g. Finder) continue to preserve the url field since file:// URLs are not
a privacy concern.
Updated test to verify URLs are NOT included in browser results.
Fixes Greptile ActivityWatch#130 P1 privacy finding.
Privacy Fix Applied: URLs Stripped from Browser EventsI've pushed commit The IssueThe original implementation used The FixChanged to explicitly construct the result dict:
Test UpdateUpdated the test CI is running now — tests should pass with this fix. |
|
@greptileai review |
Non-browser apps on macOS JXA can expose sensitive URLs via file:// and mailbox:// schemes (e.g., file:///Users/jdoe/sensitive-folder/ or mailbox://user@corp.com/inbox/). The privacy filter must strip these just as it strips browser URLs to prevent leaking sensitive path components to the ActivityWatch server. Changes: - Remove 'url' field from non-browser preserved fields - Keep 'incognito' flag preserved (non-sensitive metadata) - Update test to verify URL stripping for non-browser apps Fixes Greptile P1 security finding.
P1 Security Fix Applied: Non-browser URLs StrippedI've pushed commit The IssueThe privacy filter was correctly stripping URLs from browser events (search queries, auth tokens), but Greptile caught that non-browser apps on macOS JXA can also expose sensitive URLs:
These were being preserved and would leak to the ActivityWatch server. The FixChanged the non-browser branch to drop the result = {"app": app}
# Preserve incognito flag if present (metadata, not a privacy concern)
if "incognito" in window:
result["incognito"] = window["incognito"]
# Note: URL is NOT preserved for non-browser appsTest CoverageUpdated All Greptile findings now appear addressed:
Ready for final review verification. |
|
@greptileai review |
…able The Swift strategy can capture browser URLs via accessibility APIs, giving more reliable classification than page titles (which change mid-load or show generic text like "New Tab"). Classify by URL first, fall back to title. Both Python transform() and Swift applyResearchFilter() now implement this: - classify_title() accepts optional url= param, tries URL patterns first - Swift classifyResearch() takes url parameter, checks it before title URL is still stripped from output in both implementations for privacy.
|
@greptileai review |
|
@greptileai review |
Summary
Adds an opt-in Research Edition mode that classifies browser window titles into study categories and drops non-browser titles entirely — replicating the approach from the ERC research fork without the overhead of maintaining a separate fork, code-signing pipeline, or notarization setup.
This is the watcher-side half of the ActivityWatch Research Edition for the Lund/IIIEE study. The CI build variant (which bakes in
research_enabled = trueat package build time) is a separate PR to the mainactivitywatchbundle repo.What changed
aw_watcher_window/research_filter.py(new) — pure-Python, no external deps:is_browser(app)— case-insensitive lookup against a known browser app listclassify_title(title, category_map)— first-match substring classifier; returns"excluded"when nothing matchestransform(window, category_map)— applies the full ERC transform: browser titles → category, non-browser titles → dropped entirely (onlyapprecorded)aw_watcher_window/config.py— addsresearch_enabled = falsedefault and[aw-watcher-window.research_category_map]TOML table; exposes--researchCLI flagaw_watcher_window/main.py—heartbeat_loopappliesresearch_transform()after the existingexclude_title/exclude_titlesstep when Research Edition is enabledtests/test_research_filter.py(new) — 19 tests coveringis_browser,classify_title, andtransform(input immutability, url/incognito field preservation, macOS JXA extra fields, edge cases)Behaviour at a glance
macOS note
The
swiftstrategy (the default on macOS) delegates to the compiled Swift helper and bypasses this Python transform. Use--strategy jxaor--strategy applescripton macOS if Research Edition support is needed there.Test plan
python3 -m unittest tests/test_research_filter.py— all 19 tests passresearch_enabled = trueand a category map: browser titles classified, non-browser titles dropped--strategy jxa+--research: titles transformed correctly