Adds windows/, the Python client for the kids' Windows computers, and proves both halves of the Phase 4 definition of done on the real box (DESKTOP-TE2RHV2). The client: - subscribes, retained, to its own config on the 8883 TLS device plane with its own broker account (FR-3, FR-6); - keeps the last valid config on disk and loads it before connecting, so a broker outage changes nothing (FR-14); - terminates every process on the blocklist while the device is effectively locked, with the allowlist winning and the client's own tree plus critical system processes protected (FR-4, FR-16); - publishes presence and daily per-app usage; the day's totals are held on disk and republished when the link returns (FR-7); - shows an always-on-top warning window naming what is blocked, why and until when (FR-9). install-scheduled-task.ps1 registers the logon Scheduled Task for the kid's account with an interactive token, and grants that account Modify on the state directory: a file created from an elevated shell inherits Administrators only, and the limited task is otherwise refused with Access denied. Evidence (windows/EVIDENCE.md, from the machine's own log): notepad.exe killed inside a scheduled window (pid 16600, then 9724), the device online in the Control Service with locked/until/reason, a telemetry row for the device (app=690s), enforcement continuing with the broker unreachable (192.168.20.13:8884), the warning window on the interactive desktop, and the Scheduled Task registered at logon as New Comp. Domains are deliberately not reported: Windows has no cheap per-process DNS attribution, so the client sends "domains": [] and the gap is documented rather than guessed. 54 unit tests, run with `make windows-test` (no broker, no Windows, no psutil). AGENTS.md §11 is now 10 of 10.
10 KiB
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):
- a real Windows machine blocks a listed process during a scheduled window;
- 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:
{"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:
{"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:
{"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):
{"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:
{"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 endpythonw.exeand 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 Clientis 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.exestays 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: "".