utils/crdt-sync/tool/_seeded_apps.py
Krzysztof kuhy Rudnicki aa032c95e4 Drop the gatelock dependency, and actually sync the pool
Two things the pool needed to be real rather than merely correct.

**freedays no longer depends on gatelock.** It imported
gatelock.log_integrity for ~15 lines of HMAC, but importing gatelock runs
its __init__, which imports tkinter for the lock-window machinery. So a
headless policy library had acquired a hard dependency on a GUI toolkit,
and `import freedays` failed anywhere without Tk:

    ImportError: libtk8.6.so: cannot open shared object file

A vmbox run caught it. The host masked it completely -- Tk is installed
here for the lockers -- and so did CI, whose own comment claimed "no
display needed" while the job would have needed libtk. freedays/_hmac.py
now does the signing with hashlib and hmac.

The format is deliberately unchanged (HMAC-SHA256 over compact
sorted-key JSON), so nothing has to be re-signed: all 8 entries already
in the real audit trail, written by gatelock's implementation, verify
against the new one. That was checked, not assumed.

This also dissolves the version diamond that shaped the app rollout:
freedays now pins only crdt-sync, so it can never conflict with whatever
gatelock tag an app carries.

**The pool now syncs on a schedule.** A `sync-quiet` subcommand plus a
systemd user timer every 15 minutes, because "it syncs" was otherwise
only true if you typed `freedays sync` yourself -- a day marked on the
phone would sit there. sync-quiet exits 0 when there is no network or no
credential, on purpose: a unit that goes red every time the laptop is
offline is a unit you learn to ignore, and a real bug still reaches the
journal through the library's logging.

install.sh gives freedays its own venv rather than borrowing an app's --
the pool is shared by five apps, so binding its CLI to one of them would
make "is freedays installed?" depend on something unrelated, and
uninstalling that app would take the command with it.

Verified in vmbox first (systemd), then on this host: the timer is
enabled and scheduled, and the service *finishes* rather than failing
when there is no remote.

seed_session.py gains freedays, without which the one command that can
give the pool a Firebase session would reject it outright (--app is
choices-validated) and the pool would stay local-only with nothing
saying why. That file was already exactly at the 250-line cap, so the
app list and its rationale move to tool/_seeded_apps.py.

freedays: 109 tests, 100% branch coverage. crdt-sync: 433 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:56:42 +02:00

46 lines
2.1 KiB
Python

"""Which apps keep a desktop Firebase session, and why the list is hand-kept.
Split out of ``seed_session.py`` for the 250-line cap. The rationale is worth
more than the tuple: it is what stops the next person from "simplifying" this
into a directory scan.
"""
from __future__ import annotations
from typing import Final
# Every app that calls firebase_client_for/mirror_client_for from the desktop,
# and so keeps a refresh token under ~/.config/<app>/. Named here rather than
# discovered from ~/.config: a missing directory is exactly the case that
# needs seeding, so discovery would skip the app that needs this most.
# The remaining Flutter-only apps (home_inventory, workout_app) are absent on
# purpose: they authenticate in-app on the phone and have no desktop session.
# `todo` used to be grouped with them, but it grew a real desktop wrapper
# (lib/desktop/wrapper_server.dart) with its own Firebase REST client that
# needs a session exactly like the Python daemons do -- see
# lib/sync/firebase_backend.dart and lib/desktop/wrapper_server.dart in
# ~/todo. Its credential cache lives at ~/.config/todo/firebase_auth.json,
# same shape as every entry below, even though the reader is Dart, not
# Python: FirebaseCredentials.fromJson in crdt_sync_dart parses the exact
# {id_token, refresh_token, expires_at} shape credential_store_for writes.
#
# Re-derive rather than guess when adding one; a repo-by-repo sweep missed two
# of these. Grep all of ~ for the string literal passed to firebase_client_for,
# mirror_client_for and credential_store_for across every *.py. That still
# misses call sites passing a constant (wake_alarm._constants.SYNC_APP_NAME,
# home_guard._constants.APP_NAME), so check those by name too. "interop" is
# tool/interop_seed.py's own scratch store, not an app.
DEFAULT_APPS: Final = (
"diet_guard",
"wake_alarm",
"screen_locker",
"byox_ladder",
"leetcode_guard",
"todo",
"home_guard",
# The shared free-day pool (~/utils/freedays), not an app: its systemd
# timer is headless and it has no session of its own to inherit, so
# without this a day marked on the phone never reaches the PC.
"freedays",
)