0069. Web filtering is set per host on two axes: enforcement and history¶
- Status: accepted
- Boundary moved: the storage and observability model of web activity. What kenny gathers from a host and what it keeps is now a per-host decision the server makes and the agent obeys, instead of a fixed property of the telemetry pipeline: a host can be told not to collect at all, and the server can keep only the matches against the list.
- Amends: ADR-0024, ADR-0055
- Date: 2026-09-29
Context and Problem Statement¶
ADR-0024 gave each host two booleans, enabled and block_mode. Neither governs what is
recorded. The web_activity collector runs on every Windows host, reads every user's
browser history as LocalSystem, and the server stores every observed domain — in
web_activity_events and again inside the stored telemetry snapshot — whether or not the
feature is on for that host. enabled only decides whether matches are flagged. A host with
parental controls "off" therefore keeps a full 30-day browsing profile, which is exactly the
record an operator switching the feature off would assume does not exist.
The obvious fix — keep only the matches — takes something away: the full profile is how an operator discovers sites they did not know to list. Existing installations use it, and it must stay available. The question is how to express "what does the filter do" and "what is kept" without either losing that or collapsing two questions into one control.
Considered Options¶
- A single logging level (
off | flagged | full) beside the existing booleans. Rejected: it leavesenabled/block_modemeaning something subtly different from the level, and admits combinations such as "alarm without collection". - A single enforcement level with a fourth rung for full history. Rejected: the two questions do not order. "Protect, matches only" and "Log only, full history" have no natural rank, so a single scale either needs five or six rungs, each saying two things at once, or drops a combination someone needs.
- Two independent settings — enforcement and history. Chosen.
Decision Outcome¶
Each host has:
enforcement∈off | log_only | protect— what the filter does.offmatches nothing.log_onlymatches observed domains against the host's effective list, records the matches, and raises the alarm.protectdoes the same and also blocks (webfilter_apply). It replacesenabled/block_mode, which remain readable as its derived view (enabled⇔ notoff,block_mode⇔protect), so existing API callers keep working; a storedblock_modewithoutenabledisoff, as it always behaved.history∈violations | full— what the server keeps.violationsstores only the matches: unmatched domains are dropped on insert, and the stored snapshot'sdomainsis emptied, leaving the matches inflagged.fullkeeps every observed domain, as before.
Collection follows from both. The agent collects web_activity only when enforcement is
not off or history is full; under off + violations nothing is gathered. The agent is
told this over the policy frame's per-host collect map (protocol v0.20) and learns only
that boolean — never the enforcement level, never the list — so ADR-0024's split stands: the
server is the matcher, the agent a dumb collector and enforcer. The server does not trust the
gate. It discards on insert whatever it did not ask for, which covers agents predating the
field and any snapshot taken before the first policy frame arrived. If enrichment fails,
the snapshot is stored without domains rather than unfiltered.
log_only and protect both record matches: monitoring stays the guarantee and blocking
the best effort, as ADR-0024 requires. Under protect a recorded match therefore means the
block was circumvented, or the entry is a watch entry. For that to hold, the collector drops
DNS-cache entries that resolve to a sinkhole (0.0.0.0, ::): Windows preloads the hosts
file into the cache, so every blocked name would otherwise read as reached.
Migration changes nothing that is recorded. Every host with stored web activity is
materialised with history: full and the enforcement level its booleans already meant. A host
heard from for the first time gets off + the default history — violations unless the live
setting KENNY_WEBFILTER_DEFAULT_HISTORY says otherwise — and keeps it: the default is written
into the host's configuration at first contact, so changing the setting later affects new hosts
only and never rewrites what a known host records. An update must not
silently change what is recorded in either direction; removing a capability without telling
anyone is as wrong as widening surveillance without telling anyone.
Asymmetry. Raising enforcement or turning history to full widens what is recorded, so on
the AI surfaces it goes through webfilter_set's existing confirm-gate. Turning history to
violations purges that host's unmatched events and empties domains in its stored
snapshots immediately — otherwise "no browsing profile" would stay false for up to 30 days.
A level change is configuration: it surfaces as drift and is applied explicitly, like any
other edit. ADR-0055's schedule loop now runs only under protect, and a window still cannot
raise the level.
Consequences¶
- Good, because a host with filtering off can now actually be unobserved, and the promise holds on the host, on the wire and in storage rather than only in the dashboard.
- Good, because the privacy-preserving mode (
violations) keeps the alarm fully working — the alarm only ever needed the matches. - Good, because nothing an existing installation relied on is removed, and the full profile is now an explicit, visible choice instead of an invisible side effect.
- Bad, because under
violationsevery observed domain still crosses the wire: the agent holds no list, so data minimisation happens at the server's insert, not at the source. Onlyoff+violationsis enforced on the host. - Bad, because existing hosts without filtering keep recording a full profile until someone turns it off. Accepted as the price of not changing recorded state silently; the dashboard shows the history setting beside the enforcement level so it is not hidden.
- Bad, because the purge rewrites that host's stored snapshots and is irreversible; the dashboard asks before doing it.
More Information¶
- Contract:
docs/protocol.md§policy.collect, § Telemetry sections (web_activity), § Versioning (0.20); fixturedocs/fixtures/policy_collect.json. - Code (server):
kenny_server/webfilter.py,store.py(WebFilterStore,TelemetryStore),tunnel.py(per-hostpolicyframe, insert-time filtering),tools.py,webui/__init__.py,config.py. - Code (agent):
src/protocol.rs,src/tunnel.rs,src/telemetry/collectors/web_activity.rs.