Repository navigation
v0.5.4
Fixed
- Strict mode admitted unreviewed discoveries — the mode advertises "whitelist only", but the poll loop only reached the warn-then-kill path when a process matched no pattern at all. A process matching a pattern in
discoveredstate — a row discovery created automatically the first time it crossed the CPU threshold — ran unchallenged. On a host that had been innormalmode for a while, that covered most of what a user actually launches, so switching tostrictchanged nothing for them. Admission is now an explicit allowlist ofactiveandignored discover disallowon a browser domain did nothing — the CLI wrotedisallowedto the row and printed "will be terminated on detection", but the kill path operates on PIDs and browser domains are handled in a separate block that only records runtime. A domain could sit disallowed indefinitely and remain reachable. ADR-001 listed this as a stretch goal; it was never built
Added
- Browser managed-policy generation (ADR-003) — playtimed writes Chrome-family and Firefox policy files from its own
browser_domainpatterns, so the browser enforces at navigation time across every tab and window rather than playtimed reacting to whichever tab has focus.normalmode generates a blocklist from disallowed domains;strictgenerates an allowlist of permitted domains with everything else blocked;passthroughwithdraws playtimed's rules playtimed browser-policyshows what would be written,--syncwrites it. Generation also runs at daemon startup, on SIGHUP, and after any CLI command that changes a domain's state or the daemon mode- For Chrome-family browsers playtimed writes exactly one file,
playtimed.json, and never reads or overwrites others in the merged policy directory. Firefox has no such directory, so there playtimed owns thepolicies.WebsiteFilterkey inside the sharedpolicies.jsonand leaves the rest of the document intact - An
activegaming-category domain is written into the blocklist rather than the allowlist.activemeans "counted against the limit", and playtimed cannot close a tab when a domain's budget expires — so on a host that had been tracking YouTube time, readingactiveas "permitted" would have generated an allowlist of exactly the sites a lockdown exists to block
Changed
ActivityDB.get_patterns()now returns onlypattern_type = 'process'rows by default. Abrowser_domainpattern holds a hostname, but process matching is an unanchored regex search over process name and command line, so a domain row in that scan meant any process whose cmdline contained the string was treated as that pattern — and a disallowed one was killed. Passpattern_type=Nonefor every typeget_browser_patterns()now honours theenabledcolumn, which it previously ignored- The systemd unit grants write access to each browser's configuration root under
/etc.ProtectSystem=strictmade all of/etcread-only, so daemon-side policy writes would have failedEROFSand been logged as a warning while the feature appeared to work
Security
- Browser database copies used
tempfile.mktemp()— the workers copy Chrome'sHistoryand Firefox'splaces.sqlitesomewhere unlocked before reading them, andmktempreturns a path without creating it. The daemon runs as root, the source is a file the monitored user owns and controls the contents of, and/tmpis world-writable, so that user could plant a symlink at the predicted path and have root write bytes of their choosing to a path of their choosing.PrivateTmp=truein the unit blocks it for the packaged daemon;playtimed runstarted by hand is not covered. Both workers now call one helper inbrowser/base.py, built onmkstemp, which creates the file itself with mode 0600. The helper removes the file if the copy raises: the caller has no name for it until the helper returns, so nothing else can — and a directory left at the profile path satisfies the caller's.exists()guard, making the copy fail on every poll - The content goes through the descriptor
mkstempreturned rather than being written back by path. A write by name follows a symlink at the destination and carries noO_EXCL, so reopening would have reopened the same question, leaving only/tmp's sticky bit between root and a user-directed write - The copy is a descriptor write rather than
shutil.copy2. Browsers leave those profile files world-readable, andcopy2would preserve the source mode — widening the 0600 back out and exposing the user's browsing history to every local account for as long as the copy exists
Changed
make checkpasses. It never had: ruff's default rule selection grows between minor releases, so the gate went red on code no commit touched, reaching 244 findings. ruff is now pinned in the dev extra, three rule groups are switched off with the reason recorded beside each (naive datetimes, which are deliberate for a daemon whose budgets are per local calendar day; blind excepts, which are all log-and-continue probes; and the iteration idiom for entries that vanish mid-scan), and the rest are fixedplaytimed message listprints the urgency of each template. The colour was computed and discarded
Note
Chrome reloads managed policy on launch and periodically thereafter, not immediately. A domain disallowed while the browser is open stays reachable until it reloads — the process layer is instantaneous and the browser layer is not.