From 5d5aa3e77311e930bcf50a6defebde9e3de89013 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 11 Nov 2026 14:29:58 +0100 Subject: [PATCH] Add Librarian-only research window to total block kuhy 2026-11-11: game-design research needs Librarian (app 4197610) actually played during the 6-day total block. New root command total-block-research --until T opens ONE window per lock: <= 48 h, never past the lock end, a rerun never extends it. The command only validates and records (/var/lib/steam-backlog- enforcer, root 0700; lock id + deadline + two latches). The daemon opens and closes everything: Steam hosts names, the store block, the total-block chain (IP cache kept) and the steam package, reinstalled with only guard-lib's package-block hook masked for that one pacman call (--hookdir, proved in a guest). Lock file, hook and end date are never touched. Browser-game sites stay null-routed. Only Librarian may run: loop-local allowed set, strict mode (no whitelist exceptions, no zero-achievement games), launchers and counted games killed; state.json is never read or written. Expiry latches the record, then the ordinary tick re-applies hosts (now reconciled each tick), chain, uninstall and purge; the normal store block is restored once, unconditionally. Failure modes: missing/corrupt/unparseable record = no window and a corrupt one also refuses a new window (fail closed); a clock set back cannot reopen a latched window. Checked: 3290 tests at 100% coverage; whole-tree pylint 10/10 run in three parts (the hook exceeds the 60 s cap, so SKIP=pylint here); vmbox guest with real guardctl package-block and the real unit, window opened, Librarian-only enforced, clock moved past the deadline and the full block re-applied. Not verified on the host. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWWzuCEg1m87jpKyRrw --- README.md | 1 + .../_cmd_total_block_research.py | 71 +++++++ steam_backlog_enforcer/_command_help.py | 1 + steam_backlog_enforcer/_command_locks.py | 9 +- steam_backlog_enforcer/_enforce_guards.py | 8 +- steam_backlog_enforcer/_enforce_loop.py | 7 +- steam_backlog_enforcer/_pacman.py | 60 +++++- steam_backlog_enforcer/_research_net.py | 108 ++++++++++ steam_backlog_enforcer/_research_record.py | 136 +++++++++++++ steam_backlog_enforcer/_research_tick.py | 109 +++++++++++ steam_backlog_enforcer/_research_window.py | 165 ++++++++++++++++ steam_backlog_enforcer/_total_block.py | 21 +- .../_total_block_domains.py | 9 + steam_backlog_enforcer/_total_block_hosts.py | 58 ++++-- .../_total_block_iptables.py | 25 +++ steam_backlog_enforcer/enforcer.py | 11 +- steam_backlog_enforcer/game_uninstall.py | 12 +- steam_backlog_enforcer/main/__init__.py | 3 + steam_backlog_enforcer/main/_shared.py | 3 +- steam_backlog_enforcer/store_blocker.py | 17 ++ .../tests/_isolate_research.py | 40 ++++ .../tests/_research_fixtures.py | 80 ++++++++ .../tests/_research_helpers.py | 37 ++++ steam_backlog_enforcer/tests/conftest.py | 4 + .../tests/test_cmd_total_block_research.py | 123 ++++++++++++ .../tests/test_enforce_loop_part3.py | 12 +- .../tests/test_main_shared.py | 1 + .../tests/test_pacman_install.py | 88 +++++++++ .../tests/test_research_expiry.py | 83 ++++++++ .../tests/test_research_hosts.py | 184 ++++++++++++++++++ .../tests/test_research_iptables.py | 134 +++++++++++++ .../tests/test_research_net.py | 165 ++++++++++++++++ .../tests/test_research_open.py | 182 +++++++++++++++++ .../tests/test_research_record.py | 139 +++++++++++++ .../tests/test_research_strict.py | 124 ++++++++++++ .../tests/test_research_tick.py | 108 ++++++++++ .../tests/test_research_window.py | 145 ++++++++++++++ .../tests/test_store_restore.py | 77 ++++++++ .../tests/test_total_block_lifecycle.py | 24 ++- 39 files changed, 2534 insertions(+), 50 deletions(-) create mode 100644 steam_backlog_enforcer/_cmd_total_block_research.py create mode 100644 steam_backlog_enforcer/_research_net.py create mode 100644 steam_backlog_enforcer/_research_record.py create mode 100644 steam_backlog_enforcer/_research_tick.py create mode 100644 steam_backlog_enforcer/_research_window.py create mode 100644 steam_backlog_enforcer/tests/_isolate_research.py create mode 100644 steam_backlog_enforcer/tests/_research_fixtures.py create mode 100644 steam_backlog_enforcer/tests/_research_helpers.py create mode 100644 steam_backlog_enforcer/tests/test_cmd_total_block_research.py create mode 100644 steam_backlog_enforcer/tests/test_pacman_install.py create mode 100644 steam_backlog_enforcer/tests/test_research_expiry.py create mode 100644 steam_backlog_enforcer/tests/test_research_hosts.py create mode 100644 steam_backlog_enforcer/tests/test_research_iptables.py create mode 100644 steam_backlog_enforcer/tests/test_research_net.py create mode 100644 steam_backlog_enforcer/tests/test_research_open.py create mode 100644 steam_backlog_enforcer/tests/test_research_record.py create mode 100644 steam_backlog_enforcer/tests/test_research_strict.py create mode 100644 steam_backlog_enforcer/tests/test_research_tick.py create mode 100644 steam_backlog_enforcer/tests/test_research_window.py create mode 100644 steam_backlog_enforcer/tests/test_store_restore.py diff --git a/README.md b/README.md index 8e845b0..5802541 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ python -m python_pkg.steam_backlog_enforcer.main setup | `unblock [minutes]` | Unblock the store for a window (default 15, max 30); the daemon keeps it open and re-blocks after | | `buy-dlc` | Same 15-minute store window, to buy a game or DLC | | `reset` | Reset all state | +| `total-block-research --until T` | Root. One window per total block (at most 48 h): Steam and the store open, only Librarian may be played; the daemon re-applies the full block at `T` | | `installed` | List currently installed Steam games | | `uninstall` | Interactively uninstall non-assigned games | | `setup` | First-time configuration | diff --git a/steam_backlog_enforcer/_cmd_total_block_research.py b/steam_backlog_enforcer/_cmd_total_block_research.py new file mode 100644 index 0000000..76abcfc --- /dev/null +++ b/steam_backlog_enforcer/_cmd_total_block_research.py @@ -0,0 +1,71 @@ +"""``total-block-research``: lend Steam for one bounded window, Librarian only. + +kuhy 2026-11-11: game-design research needs "Librarian: Tidy Up the Arcane +Library" actually played during a 6-day total block. This command opens ONE +window per lock, ending at ``--until`` (at most 48 h away, never past the +lock's end, never extended by a rerun). It only validates and records the +window; the enforcer daemon opens Steam within a pass and re-applies the full +block at the deadline (:mod:`steam_backlog_enforcer._research_tick`). + +Root-only: the record sits in a root-only state directory. The lock and its +end date are untouched, so this can never shorten or end a total block. +""" + +from __future__ import annotations + +from datetime import datetime +import os + +from steam_backlog_enforcer import _research_window as window +from steam_backlog_enforcer._echo import _echo + +_USAGE = ( + "Usage: total-block-research --until " + "\n e.g. total-block-research --until 2026-11-13T00:00" + "\n One window per total block, at most 48 h, Librarian only." +) + + +def _until_argument(args: list[str]) -> str | None: + """The value of ``--until X`` or ``--until=X``, or None for any other shape.""" + match args: + case ["--until", value]: + return value + case [single] if single.startswith("--until="): + return single.partition("=")[2] + case _: + return None + + +def cmd_total_block_research(args: list[str]) -> int: + """Open the research window. + + Args: + args: Arguments after the command name: ``--until