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:
90
windows/install-scheduled-task.ps1
Normal file
90
windows/install-scheduled-task.ps1
Normal file
@@ -0,0 +1,90 @@
|
||||
# Install (or remove) the Internet Use Control kids' client as a Scheduled Task
|
||||
# that starts at logon (AGENTS.md §11 Phase 4).
|
||||
#
|
||||
# The task runs as the signed-in kid's account, in that account's interactive
|
||||
# session, which is where the child's apps and the warning window live. No
|
||||
# password is stored: the task uses an interactive logon token, so it starts
|
||||
# when the child logs on and the child cannot see a service window.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File install-scheduled-task.ps1 `
|
||||
# -ClientDir "C:\ProgramData\WebRestrict\client" `
|
||||
# -Config "C:\ProgramData\WebRestrict\client.json"
|
||||
#
|
||||
# Remove it again with:
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File install-scheduled-task.ps1 -Remove
|
||||
#
|
||||
# The equivalent one-line schtasks form (what the phase proof used) is:
|
||||
#
|
||||
# schtasks /Create /TN "WebRestrict Kids Client" /SC ONLOGON /F ^
|
||||
# /TR "\"<pythonw.exe>\" -m webrestrict --config \"C:\ProgramData\WebRestrict\client.json\""
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$TaskName = "WebRestrict Kids Client",
|
||||
[string]$Account = "$env:USERDOMAIN\$env:USERNAME",
|
||||
[string]$Config = "$env:ProgramData\WebRestrict\client.json",
|
||||
[string]$ClientDir = "$env:ProgramData\WebRestrict\client",
|
||||
[string]$StateDir = "$env:ProgramData\WebRestrict\state",
|
||||
[string]$Python = "",
|
||||
[switch]$SkipAcl,
|
||||
[switch]$Remove
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ($Remove) {
|
||||
schtasks /Delete /TN $TaskName /F
|
||||
Write-Host "removed scheduled task '$TaskName'"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# The state directory holds the log, the cached config and the usage roll-up.
|
||||
# The task runs with a limited interactive token, so the account needs an
|
||||
# explicit Modify grant: a file created from an elevated shell inherits only
|
||||
# Administrators, and the limited task is then refused with "Access denied".
|
||||
if (-not $SkipAcl) {
|
||||
if (-not (Test-Path $StateDir)) { New-Item -ItemType Directory -Force -Path $StateDir | Out-Null }
|
||||
icacls $StateDir /grant "${Account}:(OI)(CI)M" /T /C | Out-Null
|
||||
Write-Host "granted Modify on $StateDir to $Account"
|
||||
}
|
||||
|
||||
function Resolve-Python {
|
||||
param([string]$Explicit)
|
||||
if ($Explicit) { return $Explicit }
|
||||
foreach ($candidate in @("pythonw.exe", "python.exe")) {
|
||||
$found = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||
if ($found) {
|
||||
$path = $found.Source
|
||||
# pythonw.exe has no console, so a logon start shows no window.
|
||||
$windowless = Join-Path (Split-Path $path) "pythonw.exe"
|
||||
if (Test-Path $windowless) { return $windowless }
|
||||
return $path
|
||||
}
|
||||
}
|
||||
throw "python was not found on PATH; pass -Python <path to pythonw.exe>"
|
||||
}
|
||||
|
||||
$pythonExe = Resolve-Python -Explicit $Python
|
||||
$arguments = "-m webrestrict --config `"$Config`""
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute $pythonExe -Argument $arguments -WorkingDirectory $ClientDir
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $Account
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-RestartCount 3 `
|
||||
-RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
|
||||
-MultipleInstances IgnoreNew
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $Account -LogonType Interactive -RunLevel Limited
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||
-Settings $settings -Principal $principal -Force | Out-Null
|
||||
|
||||
Write-Host "registered '$TaskName'"
|
||||
Write-Host " account : $Account (interactive, at logon)"
|
||||
Write-Host " command : $pythonExe $arguments"
|
||||
Write-Host " workdir : $ClientDir"
|
||||
schtasks /Query /TN $TaskName /V /FO LIST
|
||||
Reference in New Issue
Block a user