Files
internet_use_control/windows/EVIDENCE.md
Sam Rolfe 1275b1baaa Phase 4: kids' Windows computer client (AUD-97)
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.
2026-10-14 15:00:22 +11:00

10 KiB
Raw Blame History

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:

{"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 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: "".