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

200 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: ""`.