From c3f3770ea68c91d409fc0ce9a8c361ca4ef61a2a Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Mon, 29 Dec 2025 12:38:38 -0500 Subject: [PATCH] Clean up README structure --- CONTRIBUTING.md | 78 +++ README.md | 162 +++-- .../2025-12-26 001 initial shepherdd.md | 622 ++++++++++++++++++ docs/ai/history/2025-12-26 002 initial ui.md | 369 +++++++++++ setup-cgroups.sh | 34 - 5 files changed, 1168 insertions(+), 97 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/ai/history/2025-12-26 001 initial shepherdd.md create mode 100644 docs/ai/history/2025-12-26 002 initial ui.md delete mode 100755 setup-cgroups.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c724e45 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# For developers + +## Build for development + +### tl;dr + +You need a Wayland-capable Linux system, Rust, and a small set of system dependencies. Once installed, `./run-dev` will start a development instance. + +### Requirements + +1. **Linux with Wayland** + + * Any modern Wayland compositor is sufficient. For Ubuntu, this means 25.10 or higher. + * Optional (but recommended for realistic testing): TPM-based full disk encryption and a BIOS/UEFI password to prevent local tampering. + +2. **System dependencies** + + * Platform-specific packages are required. + * For Ubuntu, see the [`SYSTEM_DEPS` section in the CI configuration](./.github/workflows/ci.yml). + +3. **Rust toolchain** + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + +### Running in development + +Start a development instance: + +```sh +./run-dev +``` + +#### Adjusting the time + +To avoid having to adjust the system clock or wait for timeouts, development +builds can mock the time with `SHEPHERD_MOCK_TIME`: + +```sh +SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev +``` + +Time and activity history are maintained in a SQLite database, which +`./run-dev` places at `./dev-runtime/data/shepherdd.db`. Edit this database +using a tool like [DB Browser for SQLite](https://sqlitebrowser.org/) while the +service is not running to inject application usage. [TODO: Link to schema docs] + +### Testing and linting + +Run the test suite: + +```sh +cargo test +``` + +Run lint checks: + +```sh +cargo clippy +``` + + +## Contribution guidelines + +`shepherd-launcher` is licensed under the GPLv3 to preserve end-users' rights. +By submitting a pull request, you agree to license your contributions under the +GPLv3. + +Contributions written in whole or in part by generative AI are allowed; +however, they will be reviewed as if you personally authored them. I highly +recommend adding substantial prompts and design docs provided to agents to +[docs/ai/history/](./docs/ai/history/) along with the PRs and commit hashes +associated with them. + +The authors of `shepherd-launcher` do not condone software or media piracy. +Contributions that explicitly promote or facilitate piracy will be rejected. +Please support developers and creators by obtaining content legally. diff --git a/README.md b/README.md index 099b184..c5ba425 100644 --- a/README.md +++ b/README.md @@ -10,58 +10,39 @@ not software or hardware vendors, by providing: * access to any application that can be run, emulated, or virtualized in desktop Linux * with granular access controls inspired by and exceeding those in iOS Screen Time -While this repository provides some recipes for existing software packages -(including non-free software), `shepherd-launcher` is *non-prescriptive*: as -the end user, you are free to use them, not use them, or write your own. +While this repository provides some examples for existing software packages +(including non-free software and abandonware), `shepherd-launcher` is +*non-prescriptive*: as the end user, you are free to use them, not use them, +or write your own. ## Screenshots -TODO: +### Home screen -* home screen at different times showing different applications -* modern proprietary application showcase (Minecraft, individual Steam games) -* emulated application showcase (ScummVM games, 90s edutainment on Win9x, Duolingo via Waydroid) -* externally managed Chrome container for access to school resources -* media showcase (local storage and individual titles from streaming services) -* time limit popup -* "token" system +TODO: home screen at different times (bedtime vs afternoon) showing different applications -## Installation +### Time limits -tl;dr: +TODO: GIF or video of GCompris a few seconds from closing, emphasizing: +* Countdown clock +* Warning messaging +* Automatic close at end of time +* Icon deliberately missing afterwards -- cooldown -1. any Linux with Wayland (optional: TPM-based FDE plus BIOS password to prevent tampering) -2. System dependencies: - - **Ubuntu/Debian**: `apt install build-essential pkg-config libglib2.0-dev libgtk-4-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf-xlib-2.0-dev libwayland-dev libx11-dev libxkbcommon-dev libgirepository1.0-dev libgtk4-layer-shell-dev librust-gtk4-layer-shell-sys-dev sway swayidle` -3. Rust (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`) -4. Set up cgroups for process management (one-time, requires root): - ```bash - sudo ./setup-cgroups.sh - ``` -5. binaries (TODO: deployable package that depends on Sway and installs the config) -6. test session on login -7. configure auto-login to this session +### "access to any application that can be run, emulated, or virtualized" -### cgroups Setup +TODO: show the following running with some subset of the above features highlighted: +* Minecraft +* Steam games (World of Goo, Portal 2) +* ScummVM games (Putt Putt, Secret of Monkey Island) +* Media -The shepherd daemon uses Linux cgroups v2 to reliably terminate all processes -when a session ends. This is essential for applications like Minecraft that -spawn child processes which may escape traditional process group signals. - -Run the setup script once after installation: - -```bash -sudo ./setup-cgroups.sh -``` - -This creates `/sys/fs/cgroup/shepherd` with appropriate permissions for your -user. The directory is not persistent across reboots on most systems, so you -may want to add this to your system startup (e.g., in `/etc/rc.local` or a -systemd unit). - -## Usage - -TODO: open lid; play +Contributions are welcome for improvements and not yet implemented backends, +such as: +* Pre-booted Steam to improve launch time [TODO: link to issue] +* Android apps via Waydroid, including pre-booting Android if necessary [TODO: link to issue] +* Legacy Win9x via DOSBox, QEMU, or PCem, including scripts to create a boot-to-app image [TODO: link to issue] +* Chrome, including strict sandboxing and support for firewall rules [TODO: link to issue] ## Core concepts @@ -71,32 +52,87 @@ TODO: open lid; play * **Wrappers, not patches**: existing software is sandboxed, not modified * **Revocable access**: sessions end predictably and enforceably -## Recipes +## Non-goals -TODO +* Modifying or patching third-party applications +* Circumventing DRM or platform protections +* Replacing parental involvement with automation -* `.desktop` syntax plus rules (time allowance, time-of-day restrictions, network whitelist, etc.) -* wrappers for common applications -* how to write a custom wrapper or custom application +## Installation + +`shepherd-launcher` is pre-alpha and in active development. As such, end-user +binaries and installation instructions are not yet available. + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to run in development. + +Contributions are welcome for: +* a CI step that generates production binaries [TODO: link to issue] +* an installation script [TODO: link to issue] + +## Example configuration + +For the Minecraft example shown above: + +```toml +# Minecraft via mc-installer snap +# Ubuntu: sudo snap install mc-installer +[[entries]] +id = "minecraft" +label = "Minecraft" +icon = "minecraft" + +[entries.kind] +type = "snap" +snap_name = "mc-installer" + +[entries.availability] +[[entries.availability.windows]] +days = "weekdays" +start = "15:00" +end = "18:00" + +[[entries.availability.windows]] +days = "weekends" +start = "10:00" +end = "20:00" + +[entries.limits] +max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days) +daily_quota_seconds = 3600 # 1 hour per day +cooldown_seconds = 600 # 10 minute cooldown + +[[entries.warnings]] +seconds_before = 600 +severity = "info" +message = "10 minutes left - start wrapping up!" + +[[entries.warnings]] +seconds_before = 120 +severity = "warn" +message = "2 minutes remaining - save your game!" + +[[entries.warnings]] +seconds_before = 30 +severity = "critical" +message = "30 seconds! Save NOW!" +``` + +See [config.example.toml](./config.example.toml) for more. ## Development -tl;dr: -* Run in development: `./run-dev` -* Set the `SHEPHERD_MOCK_TIME` environment variable to mock the time, - such as `SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev` -* Run tests: `cargo test` -* Lint: `cargo clippy` +See [CONTRIBUTING.md](./CONTRIBUTING.md) -## Contributing +## Written in 2025, responsibly -`shepherd-launcher` is licensed under the GPLv3 to preserve end-users' rights. -By submitting a pull request, you agree to license your contributions under the -GPLv3. +This project stands on the shoulders of giants in systems software and +compatibility infrastructure: -Contributions written in part or in whole by generative AI are allowed; -however, they will be reviewed as if you personally authored them. +* Wayland and Sway +* Rust +* Snap +* Proton and WINE -The authors of `shepherd-launcher` do not condone software or media piracy. -Contributions that explicitly promote or facilitate piracy will be rejected. -Please support developers and creators by obtaining content legally. +This project was written with the assistance of generative AI-based coding +agents. Substantial prompts and design docs provided to agents are disclosed in +[docs/ai](./docs/ai/) diff --git a/docs/ai/history/2025-12-26 001 initial shepherdd.md b/docs/ai/history/2025-12-26 001 initial shepherdd.md new file mode 100644 index 0000000..479831e --- /dev/null +++ b/docs/ai/history/2025-12-26 001 initial shepherdd.md @@ -0,0 +1,622 @@ +This is the initial prompt to create `shepherdd` and its supporting crates, provided to Claude Opus 4.5 via VSCode Agent mode with nothing other than a Rust "hello world". + +It was produced by ChatGPT after some back-and-forth [in this conversation](https://chatgpt.com/share/6952b1fb-35e4-800b-abca-5f212f9d77e5). + +The output was committed as ac2d2abfed39e015aed6c2400f4f4609a2823d6d. + +----- + +Implement `shepherdd` as described below. + +### Prelude: What is `shepherdd`, and why does it exist? + +`shepherdd` is the **authoritative policy and enforcement daemon** for a child-focused, parent-defined computing environment. Its job is not to be a launcher UI, a window manager, or a media player. Its job is to **decide what is allowed, when it is allowed, for how long it is allowed, and to enforce that decision reliably**—regardless of what user interface, operating system, or legacy application happens to be in use. + +The problem it solves is simple to describe but hard to implement correctly: + +> Parents want to give children access to *real software*—including legacy games, emulators, virtual machines, browsers, and media—without relying on opaque vendor-controlled ecosystems or fragile UI-level restrictions. Time limits must be enforced. Warnings must be accurate. Expiry must be non-negotiable. And the system must remain understandable, inspectable, and extensible over time. + +`shepherdd` is the **root of trust** for this environment. Everything else—launchers, overlays, shells, admin apps—is replaceable. + +--- + +### Core philosophy + +1. **Policy, not UI, is the authority** + `shepherdd` decides *what happens*. User interfaces only request actions and display state. If a UI crashes, disconnects, or is replaced entirely, enforcement continues. + +2. **Real software, unmodified** + The system must be able to supervise arbitrary third-party programs: + emulators (e.g. ScummVM), virtual machines (e.g. Windows 9x games), browsers, media players, and other legacy or closed software that cannot be instrumented or embedded. Enforcement cannot rely on cooperation from the application. + +3. **Time is enforced, not suggested** + Sessions have fixed deadlines computed at launch. Warnings occur at defined thresholds. When time expires, the session is terminated—gracefully if possible, forcefully if necessary. + +4. **Portability over cleverness** + Desktop operating systems have fundamentally different control models. Linux can kill process groups; Android cannot. macOS requires MDM for true kiosk mode; Windows uses job objects and shell policies. `shepherdd` must acknowledge these differences honestly through a capability-based host interface rather than pretending all platforms are equivalent. + +5. **Low-level, but not reckless** + `shepherdd` is designed to run as a background service with elevated privileges where appropriate, but it avoids OS-specific assumptions in its core. Platform-specific behavior lives behind explicit adapters. + +6. **Open, inspectable, and extensible** + Policies are human-readable. Decisions are explainable (“why is this unavailable?”). Actions are logged. The architecture is intended to invite future contributors—especially for additional platforms—without requiring them to understand the entire system. + +--- + +### What `shepherdd` is *not* + +* It is **not** a graphical launcher. +* It is **not** a window manager or compositor. +* It is **not** a parental surveillance tool. +* It does **not** depend on cloud services. +* It does **not** assume a particular desktop environment or vendor ecosystem. + +--- + +### How the system is expected to be used + +* On **Linux/Wayland**, `shepherdd` runs as a service. + It starts or coordinates with a compositor (e.g. Sway), launches supervised applications, and communicates with a launcher UI and an always-on HUD overlay via local IPC. + +* On **Windows**, a future adapter may integrate with a custom shell or kiosk configuration while preserving the same policy and enforcement logic. + +* On **macOS**, `shepherdd` may operate in “soft kiosk” mode or integrate with MDM / Autonomous Single App Mode for hard lockdowns, using the same core. + +* On **Android**, the same policy engine could back a managed launcher and device-owner workflow, even though enforcement primitives differ. + +In all cases, **the daemon is the entry point**: shells connect to it; admin tools manage it; enforcement flows from it. + +--- + +### What you, the coding agent, are building + +You are not building a UI. +You are building a **policy engine and enforcement service** that: + +* loads and validates a parent-defined policy, +* evaluates availability and time limits, +* tracks and supervises running sessions, +* emits warnings and state changes, +* terminates sessions when required, +* exposes a stable IPC API for multiple frontends, +* and does so in a way that can survive OS differences and future expansion. + +If this daemon is correct, the rest of the system can evolve freely. If it is wrong, no amount of UI polish will fix it. + +That is why `shepherdd` exists. + +----- + +# `shepherdd` library requirements document + +This document specifies the **libraries (crates/modules)** that a coding agent must implement to build `shepherdd` from scratch. It assumes **zero prior context** and defines the architecture, responsibilities, APIs, and non-functional requirements needed to support a portable “policy + enforcement” daemon with replaceable frontends and host adapters. + +The implementation language is assumed to be **Rust** (recommended for portability + service-style reliability), but the requirements are written so the design could be ported. + +--- + +## 0. Glossary + +* **Daemon (`shepherdd`)**: The authoritative service that loads policy, decides what’s allowed, tracks time, issues warnings, and enforces expiry. +* **Shell / Frontend**: UI applications (e.g., Wayland launcher grid, HUD overlay, Windows shell, macOS kiosk UI) that display state and send commands. Shells do not enforce policy. +* **Host Adapter**: Platform-specific integration that can spawn/stop apps and (optionally) manage focus/fullscreen/lockdown. +* **Entry**: A whitelisted launchable unit (command, VM recipe, media collection). +* **Session**: A running instance of an Entry with start time, deadline, warnings, and enforcement actions. + +--- + +## 1. Top-level library set + +Implement the following libraries (Rust crates/modules), each testable in isolation: + +1. **`shepherd-core`** + Pure, platform-agnostic policy engine and session state machine. + +2. **`shepherd-host-api`** + Capability-based trait interfaces for platform adapters (Linux/Windows/macOS/Android). No platform code. + +3. **`shepherd-host-linux`** (initial implementation target) + Linux host adapter: spawn/kill process trees, optional cgroups, optional Sway IPC integration hooks. + +4. **`shepherd-config`** + Config schema, parsing, validation, and hot-reload support. + +5. **`shepherd-store`** + Persistence abstraction + at least one concrete backend for: + + * audit log + * usage/accounting + * current state recovery + +6. **`shepherd-ipc`** + Local privileged IPC transport and protocol implementation (Unix domain socket for Linux). Authentication/authorization for local clients. + +7. **`shepherd-api`** + Protocol types (request/response/event structures) shared by daemon and clients. Must be stable and versioned. + +8. **`shepherd-remote`** (optional but designed-in; may be stubbed initially) + Remote management plane over HTTPS with pairing/auth. Must not be required for local operation. + +9. **`shepherd-util`** + Shared utilities (time, IDs, error types, rate limiting helpers, etc.). + +The `shepherdd` binary will mostly wire these together. This document focuses on the libraries. + +--- + +## 2. System-level goals (must be supported by libraries) + +### 2.1 Core capabilities + +* Maintain a **whitelist** of allowed entries. +* Restrict entries by: + + * **time windows** (day-of-week + time-of-day windows) + * **max run duration per launch** + * optional **daily quotas** and **cooldowns** +* Provide system status needed by shells: + + * current session, time remaining, upcoming warnings + * “why is this entry unavailable?” +* Emit warnings at configured thresholds. +* Enforce expiry by terminating the running session via host adapter. +* Support “media playback entries” using the same policy primitives. + +### 2.2 Portability and extensibility + +* Core logic must not assume Wayland, process-killing, or fullscreen exist. +* Enforcement must be expressed in terms of **capabilities**. +* Protocol must support multiple shells and admin tools simultaneously. +* The daemon must run as a service; shells connect over local IPC. + +### 2.3 Safety and determinism + +* The daemon is the **sole authority** for timing and enforcement. +* Must be resilient to UI crashes/disconnects. +* Timing must use **monotonic time** for countdowns (wall-clock changes must not break enforcement). +* All commands must be auditable. + +--- + +## 3. `shepherd-core` requirements + +### 3.1 Responsibilities + +* Parse validated config objects (from `shepherd-config`) into internal policy structures. +* Evaluate policy at a given time (“what entries are visible/launchable now?”). +* Implement session state machine: + + * `Idle` + * `Launching` + * `Running` + * `Warned` (can be multiple warning levels) + * `Expiring` (termination initiated) + * `Ended` (with reason) +* Compute: + + * allowed duration if started “now” (min of entry max duration, time-window end, quota remaining) + * warning schedule times +* Produce a deterministic **Enforcement Plan**: + + * what to do on launch + * when to warn + * when/how to expire + +### 3.2 Data structures + +#### 3.2.1 Entry model + +Minimum fields: + +* `entry_id: EntryId` (stable) +* `label: String` +* `icon_ref: Option` (opaque reference; shell interprets) +* `kind: EntryKind`: + + * `Process { argv: Vec, env: Map, cwd: Option }` + * `Vm { driver: String, args: Map }` (generic “VM recipe”) + * `Media { library_id: String, args: Map }` + * `Custom { type_name: String, payload: Value }` +* `availability: AvailabilityPolicy` +* `limits: LimitsPolicy` +* `warnings: WarningPolicy` + +#### 3.2.2 AvailabilityPolicy + +* `time_windows`: list of windows: + + * days-of-week mask + * start time-of-day (local time) + * end time-of-day (local time) +* semantics: entry is allowed if “now” is in at least one window. + +#### 3.2.3 LimitsPolicy + +* `max_run: Duration` +* optional: + + * `daily_quota: Duration` + * `cooldown_after_run: Duration` + * `min_gap_between_runs: Duration` (alias of cooldown) +* Must support future extension. + +#### 3.2.4 WarningPolicy + +* list of thresholds in seconds before expiry (e.g., 300, 60, 10) +* each threshold has: + + * `message_template` (optional) + * `severity` (info/warn/critical) +* warnings must not be emitted twice for the same session. + +### 3.3 Public API + +* `CoreEngine::new(policy: Policy, store: StoreHandle) -> CoreEngine` +* `CoreEngine::list_entries(now) -> Vec` + + * `EntryView` includes: + + * enabled/disabled + * reasons list (structured codes) + * if enabled: `max_run_if_started_now` +* `CoreEngine::request_launch(entry_id, now) -> LaunchDecision` + + * Returns either `Denied { reasons }` or `Approved { session_plan }` +* `CoreEngine::start_session(approved_plan, host_session_handle, now)` +* `CoreEngine::tick(now_monotonic) -> Vec` + + * Emits scheduled warnings and expiry events. +* `CoreEngine::notify_session_exited(exit_info, now) -> CoreEvent` +* `CoreEngine::stop_current(request, now) -> StopDecision` + + * request may be user stop vs admin stop vs policy stop. +* `CoreEngine::get_state() -> DaemonStateSnapshot` + +### 3.4 Event model + +Core emits events for IPC and host enforcement: + +* `SessionStarted` +* `Warning { threshold, remaining }` +* `ExpireDue` +* `SessionEnded { reason }` +* `EntryAvailabilityChanged` (optional; used for UI updates) +* `PolicyReloaded` (optional) + +### 3.5 Time rules + +* All “countdown” logic uses monotonic time. +* Availability windows use wall-clock local time. +* If wall-clock jumps, session expiry timing remains based on monotonic time, but **no extension** should be granted by clock rollback. + +### 3.6 Testing requirements + +* Unit tests for: + + * time window evaluation (DST boundaries included) + * quota accounting + * warning schedule correctness + * state machine transitions +* Property tests recommended for “no double warning / no negative remaining / no orphan state”. + +--- + +## 4. `shepherd-host-api` requirements + +### 4.1 Responsibilities + +Define the interface between core daemon logic and platform integration. + +### 4.2 Capability model + +`HostCapabilities` must include (at minimum): + +* `spawn_kind_supported: Set` (Process/Vm/Media/Custom) +* `can_kill_forcefully: bool` +* `can_graceful_stop: bool` +* `can_group_process_tree: bool` (process group / job object) +* `can_observe_exit: bool` +* `can_observe_window_ready: bool` (optional) +* `can_force_foreground: bool` (optional) +* `can_force_fullscreen: bool` (optional) +* `can_lock_to_single_app: bool` (MDM/Assigned Access style, optional) + +### 4.3 SessionHandle abstraction + +`HostSessionHandle` must be opaque to core: + +* contains platform-specific identifiers (pid/pgid/job object, bundle ID, etc.) +* must be serializable if you plan crash recovery; otherwise explicitly “not recoverable”. + +### 4.4 Host API trait + +Minimum methods: + +* `capabilities() -> HostCapabilities` +* `spawn(entry: Entry, spawn_opts: SpawnOptions) -> Result` +* `stop(handle, mode: StopMode) -> Result<()>` + + * `StopMode`: `Graceful(Duration)` then `Force` +* `subscribe() -> HostEventStream` (async) + + * `HostEvent`: `Exited(handle, exit_status)`, optionally `WindowReady(handle)`, `SpawnFailed`, etc. +* Optional (feature-gated): + + * `set_foreground(handle)` + * `set_fullscreen(handle)` + * `ensure_shell_visible()` (return to launcher) + * `start_shell_process()` / `start_compositor()` (Linux-specific convenience, but keep generic naming) + +--- + +## 5. `shepherd-host-linux` requirements (initial adapter) + +### 5.1 Spawn/kill semantics + +* Must spawn `Process` entries without invoking a shell: + + * `execve(argv[0], argv, env)` behavior +* Must place spawned process in its own **process group**. +* Must be able to terminate entire process group: + + * `SIGTERM` then `SIGKILL` after grace timeout. +* Must handle “VM recipes” as process spawns (e.g., launching qemu) using same group semantics. + +### 5.2 Optional containment (extensible) + +Design hooks for future: + +* cgroups v2 creation per session (cpu/mem/io limits optional) +* namespacing (optional) +* environment sanitization and controlled PATH + +### 5.3 Observability + +* Must provide exit notifications for sessions. +* Should capture stdout/stderr to logs (file per session or journald). + +### 5.4 Linux compositor management (optional module) + +Provide an optional module that: + +* can start Sway (or connect to existing) +* can issue minimal compositor commands (focus/fullscreen) +* can subscribe to compositor events (window created/destroyed) + This module must be optional so the daemon can run headless or under another compositor. + +--- + +## 6. `shepherd-config` requirements + +### 6.1 Config format + +* Must support TOML (recommended). YAML acceptable but TOML preferred for strictness. +* Provide versioned schema with explicit `config_version`. + +### 6.2 Validation + +Must reject invalid config with clear errors: + +* duplicate entry IDs +* empty argv +* invalid time windows +* warning thresholds >= max_run +* negative durations +* unknown kinds/drivers (unless `Custom` allows unknown) + +### 6.3 Hot reload + +* Must support reload on SIGHUP (Linux) or an API call. +* Reload must be atomic: + + * either new policy fully applied, or old remains. +* On reload, current session continues with old plan unless explicitly configured otherwise. + +--- + +## 7. `shepherd-store` requirements + +### 7.1 Responsibilities + +Persist: + +* audit events (append-only) +* usage accounting (per entry/day) +* cooldown tracking +* last-known daemon snapshot (optional) + +### 7.2 Backends + +Minimum viable: + +* SQLite backend (recommended) + OR +* append-only JSON lines log + periodic compacted summary + +### 7.3 API + +* `Store::append_audit(event)` +* `Store::get_usage(entry_id, day) -> Duration` +* `Store::add_usage(entry_id, day, duration)` +* `Store::get_cooldown_until(entry_id) -> Option