# Phase 4 evidence — Windows client on the real machine **Date:** 2026-10-14 (Australia/Sydney, UTC+11) **Machine:** `DESKTOP-TE2RHV2` (`192.168.20.31`), account `New Comp` (local administrator, task runs with a limited interactive token) **Client version:** `webrestrict-windows 1.0.0-phase4` (`windows/`) **Device:** `Finn's computer`, Control Service device id 2, topic `restrict/finn_s_computer_2/config` **Broker:** Mosquitto on `.13:8883`, TLS, account `finn_s_computer_2` (topic ACL, `%u`) **Control Service:** `.13:8791`, dashboard `https://webrestrict.lab.audasmedia.com.au` The Definition of Done (AGENTS.md §11 Phase 4): 1. a real Windows machine blocks a listed process during a scheduled window; 2. the client keeps its last state when the broker is unavailable. Both are shown below from the machine's own log. The client was installed as a Scheduled Task and started through that task, so the evidence is produced by the deployment, not by a hand-run interpreter. --- ## 1. The configuration that caused the block Set through the dashboard (device 2): blocklist entry `notepad.exe`, and a temporary weekly window for Wednesday 14:00–23:59. The Control Service published the retained config, read back from the broker with the device's own account: ```json {"v":1,"device_id":2,"name":"Finn's computer","platform":"windows","locked":true, "until":"2026-10-14T23:59:00+11:00","allow":[],"block":["notepad.exe"], "reason":"schedule","generated_at":"2026-10-14T03:49:31.401827999Z"} ``` ## 2. A listed process is killed inside the scheduled window Before the client starts, the process is running on the interactive desktop: ``` Image Name PID Session Name Session# Mem Usage ========================= ======== ================ =========== ============ notepad.exe 16600 Console 2 13,396 K INFO: No tasks are running which match the specified criteria. <- pythonw.exe: client not running ``` The Scheduled Task is started (`schtasks /Run /TN "WebRestrict Kids Client"`). Machine log `C:\ProgramData\WebRestrict\state\client.log`: ``` 2026-10-14 14:59:46,301 INFO webrestrict no cached config yet; seeded an unlocked config (FR-14) 2026-10-14 14:59:46,301 INFO webrestrict startup config locked=False until=None block=0 allow=0 cached_at=None 2026-10-14 14:59:46,414 INFO webrestrict.overlay warning window is available 2026-10-14 14:59:46,498 INFO webrestrict.mqtt mqtt connecting to 192.168.20.13:8883 as finn_s_computer_2 (topic restrict/finn_s_computer_2/config) 2026-10-14 14:59:46,500 INFO webrestrict client running: poll=5s telemetry=30s status=30s device=finn_s_computer_2 state_dir=C:\ProgramData\WebRestrict\state 2026-10-14 14:59:47,131 INFO webrestrict.mqtt mqtt connected, subscribing to restrict/finn_s_computer_2/config 2026-10-14 14:59:47,131 INFO webrestrict link up: publishing presence and today's usage 2026-10-14 14:59:47,140 INFO webrestrict config applied locked=True until=2026-10-14T23:59:00+11:00 reason=schedule block=['notepad.exe'] allow=[] generated_at=2026-10-14T03:49:31.401827999Z 2026-10-14 14:59:47,169 INFO webrestrict blocked notepad.exe (pid=16600, rule=notepad): terminated 2026-10-14 14:59:47,169 INFO webrestrict enforcement killed 1 process(es): notepad.exe(16600) ``` After, on the same machine: ``` INFO: No tasks are running which match the specified criteria. <- notepad.exe, gone Image Name PID Session Name Session# Mem Usage pythonw.exe 12608 Console 2 47,904 K ``` The cached config on disk, which is what a broker-less restart uses: ```json {"allow": [], "block": ["notepad.exe"], "device_id": 2, "generated_at": "2026-10-14T03:49:31.401827999Z", "grant_until": null, "locked": true, "name": "Finn's computer", "platform": "windows", "reason": "schedule", "until": "2026-10-14T23:59:00+11:00", "v": 1} ``` ## 3. The dashboard shows the device online with the right state `GET /api/v1/admin/devices/2` on `.13:8791` while the client was running: ```json {"ok":true,"device":{"id":2,"name":"Finn's computer","child":"Finn","platform":"windows", "topic":"restrict/finn_s_computer_2/config","online":true,"last_seen":"2026-10-14T04:01:17Z", "locked":true,"until":"2026-10-14T23:59:00+11:00","grant_until":null,"reason":"schedule"}} ``` `online: true` comes from the client's retained status report; `locked`/`until`/`reason` are the schedule that is in force. The machine log shows the presence heartbeat: ``` 2026-10-14 15:00:17,228 INFO webrestrict status published locked=True blocking=True broker=connected cached_config=True 2026-10-14 15:00:47,323 INFO webrestrict telemetry day=2026-10-14 apps=10 top=[{"name": "ApplicationFrameHost.exe", "seconds": 30}, ...] 2026-10-14 15:01:17,417 INFO webrestrict telemetry day=2026-10-14 apps=10 top=[{"name": "ApplicationFrameHost.exe", "seconds": 60}, ...] ``` ## 4. A telemetry row lands in the Control Service Control Service, same day (`docker exec webrestrict-control /app/control usage`): ``` day=2026-10-14 source=rollup Harry's phone Harry app=5466 domain=4920 screen=5466 Finn's computer Finn app=690 domain=0 screen=690 total_screen=6156 ``` `app=690` is `Finn's computer`'s daily total in seconds, from the client's `restrict/finn_s_computer_2/telemetry` reports. `domain=0` is the documented gap: Windows has no cheap per-process DNS attribution, so the client sends `"domains": []` instead of a guess (see `README.md`). The on-device roll-up shows the attribution, and confirms the client no longer credits itself (the overlay's own window is excluded): ```json {"days": {"2026-10-14": {"ApplicationFrameHost.exe": 60, "Discord.exe": 60, "EADesktop.exe": 60, "Overwolf.exe": 60, "Spotify.exe": 60, "TextInputHost.exe": 60, "chrome.exe": 60, "cmd.exe": 60, "explorer.exe": 60, "powershell.exe": 60}}} ``` ## 5. Keep last state when the broker is unavailable (FR-14) The client was stopped, then started again with a copy of the config whose broker port points at a dead listener (`192.168.20.13:8884`) and the **same** state directory, so it loads the cached config. A new `notepad.exe` was started afterwards. Machine log: ``` 2026-10-14 15:01:41,347 INFO webrestrict startup config locked=True until=2026-10-14T23:59:00+11:00 block=1 allow=0 cached_at=2026-10-14T03:49:31.401827999Z 2026-10-14 15:01:41,539 INFO webrestrict.mqtt mqtt connecting to 192.168.20.13:8884 as finn_s_computer_2 (topic restrict/finn_s_computer_2/config) 2026-10-14 15:01:47,045 WARNING webrestrict.mqtt broker 192.168.20.13:8884 is unreachable; enforcing the cached config only (FR-14) 2026-10-14 15:01:47,045 WARNING webrestrict broker link lost (broker unreachable): enforcing the cached config (FR-14) block=['notepad.exe'] until=2026-10-14T23:59:00+11:00 2026-10-14 15:01:52,207 INFO webrestrict blocked notepad.exe (pid=9724, rule=notepad): terminated ``` `notepad.exe` (PID 9724) was running in session 2 immediately before that line, and `tasklist /FI "IMAGENAME eq notepad.exe"` reported no matching task afterwards. The 8884 listener does not exist, so nothing was received from the broker during that run. The same resilience covers usage: on the next connected start the client republished the day's totals held on disk (`telemetry republished day=2026-10-14 apps=10`). ## 6. The warning window (FR-9) The client runs in the child's session (`pythonw.exe`, session 2) and shows an always-on-top Tkinter window. A window enumeration from the same session, while a block was in force: ```json {"hwnd": 3278938, "visible": true, "titlelen": 11, "title": "Screen time", "pid": 9908, "class": "TkTopLevel", "proc": "pythonw.exe"} ``` Screenshot of the interactive desktop taken by a helper task in the same session: `screen-offline-run.png` (attached to AUD-97). The bottom-right region of the image is a large light-grey panel over a dark desktop — the overlay, placed bottom-right of the primary screen. ## 7. The Scheduled Task ``` Folder: \ HostName: DESKTOP-TE2RHV2 TaskName: \WebRestrict Kids Client Status: Ready Logon Mode: Interactive only Run As User: New Comp Task To Run: C:\Users\New Comp\AppData\Local\Programs\Python\Python314\pythonw.exe -m webrestrict --config "C:\ProgramData\WebRestrict\client.json" Start In: C:\ProgramData\WebRestrict\client Schedule Type: At logon time ``` Installed by `install-scheduled-task.ps1` (which also grants the account Modify on the state directory), or by the one-line `schtasks /Create /TN "WebRestrict Kids Client" /SC ONLOGON /F ...` form in `README.md`. Every step above was produced by starting this task (`schtasks /Run`), not by a shell. ## 8. What was not proven, and the honest gaps * **Domains in telemetry.** Not implemented on Windows, by design and by decision. The client publishes `"domains": []`; there is no FR-18 site filter for Windows in this phase. * **Blocking another account's processes.** The task runs with a limited (non-elevated) token, so it blocks the child's own processes. A process owned by a different Windows account would need the task to run at highest privilege. * **Tamper detection is presence, not a watchdog.** A force-stop or a logoff drives the broker's Last Will to `{"online": false}`, which the Control Service turns into the offline alert (FR-13). There is no separate on-device watchdog process; a determined child can end `pythonw.exe` and is visible as an offline device seconds later. * **A process started mid-window is killed on the next tick** (≤ `poll_seconds`, 5 s by default). ## 9. End state left on the machine * `WebRestrict Kids Client` is registered (at logon, `New Comp`, interactive) and running. * The temporary proof window (Wednesday 14:00–23:59) was **cleared**; no invented schedule is in force. `notepad.exe` stays on the blocklist, so the rule is ready for the next window — it does nothing while the device is unlocked. * Proof scaffolding (helper tasks, helper scripts, the offline config copy, screenshots) was removed from the machine. Only `C:\ProgramData\WebRestrict\{client,client.json,ca.crt,state}` remains. * Final device state: `online: true`, `locked: false`, `reason: ""`.