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.
This commit is contained in:
199
windows/EVIDENCE.md
Normal file
199
windows/EVIDENCE.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 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: ""`.
|
||||
Reference in New Issue
Block a user