windows-keeper-unelevated¶
On an unelevated Windows box, the modern scheduled-task API says "access denied" where the 1990s one says "sure" — and a GUI-subsystem Python can't print, so a keeper that seems silent may just be mute.
Toolchain facts (each cost a debugging loop)¶
Register-ScheduledTask(CIM) fails unelevated with0x80070005for both S4U and Interactive logon types on this box. The legacyschtasks.exe /Createregisters per-user tasks unelevated without complaint. The installer (scripts/install-keeper.ps1) usesschtasks.- PowerShell quoting into
/TRis a trap: the first attempt put literal\"into the Task-To-Run, which the scheduler reported only as Last Result0x80070002(file not found). Space-free absolute paths need no quotes at all — the fix was deleting the quoting, not escaping harder. pythonw.exeis the GUI subsystem:printis a no-op. Anything a scheduled keeper must say goes to a file (.dev/keeper.log), never stdout.- A scheduled task rots invisibly unless it proves liveness somewhere a
human looks. The keeper touches
.dev/keeper.heartbeatevery tick;/healthzreports the heartbeat's age; the launcher footer renders it ("keeper: 3m ago"). A forgotten keeper now announces itself on the page the maintainer looks at daily. - Port sweeps must know their tenants:
tailscaledlegitimately listens ontailnet-ip:8123to proxy the serve route — a naive "kill whatever is on my port" sweeps it.scripts/dev.py downmatches only0.0.0.0:PORT/127.0.0.1:PORTbinds and skips tailscale-named processes.
The transferable lesson¶
Unattended infra earns trust only by being observable at the surface the human already watches. The heartbeat-in-the-footer pattern generalizes: any background healer should surface "when did I last run" inside the thing it heals, because a dead healer otherwise looks identical to a healthy one right up until the day it was needed.