testsAndMisc/docs/superpowers/contracts/focus-owner-geofence-and-observability-2026-08-14.json
Krzysztof kuhy Rudnicki 82e206116a
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Make the geofence tell the truth, and the phone explain itself
The enforcer failed in both directions: nothing blocked at home, and
everything blocked at the office. The comparison was never inverted --
`metres <= threshold` was always right. Three separate causes:

Only ACCESS_COARSE_LOCATION was declared. Android 12+ fuzzes coarse to a
~1-2 km grid against a 150 m fence, so a fix taken at home routinely landed
outside it, read as AWAY, and restored every app the geofence exists to hide.
FINE is now declared and self-granted by the device owner, along with coarse
and background, re-asserted every pass so a reinstall cannot silently drop it.

There were three states but two behaviours. LOCATION_UNKNOWN applies the
identical sweep to AT_HOME -- correct fail-closed design, indistinguishable
from outside. And nothing ever requested a fix: the pass read whatever another
app happened to cache, so at the office with no navigation running the cache
was empty. Passes now acquire a fix inside a location-typed foreground
service, bounded at 20 s, and treat anything older than 30 min as no fix. That
window is deliberately longer than the 15-minute cadence; a shorter one marks
the newest cache stale on every pass of an idle phone and blocks everything.

None of it was diagnosable. `adb logcat -d -s FocusOwner:V` came back empty
while dumpsys showed 82 alarms had fired, and `run-as` is refused on the
release build device owner requires. So the pass now writes a ring-buffered
JSONL record read back in-app: reason, distance vs the fence, fix
age/provider/accuracy, curfew state, and every hidden app with the rule that
hid it. Coordinates are never recorded -- a distance is enough to debug a
geofence, and this is a home address.

Also fixed along the way:

- Hysteresis keyed off `isEnforcing`, which is true under AWAY too because
  YouTube stays hidden, so the threshold was permanently radius+hysteresis.
  It now keys off whether the previous pass was inside the fence.
- The scheduler's curfew-boundary alarms were documented but never scheduled;
  both callers invoked `scheduleNext()` bare, so 05:00 could land 15 min late.
- Installing over the app cancelled its pending alarms, so `adb install -r`
  silently stopped enforcement until someone opened the app. MY_PACKAGE_REPLACED
  now re-arms it, verified on device without launching the app.
- A wrong-typed log record threw instead of being skipped, and the exception
  escaped far enough to leave the page stuck busy -- which disables the
  release-device-owner button, the one control that must work when everything
  else has gone wrong.
- `lib/enforcement.dart` was a second decision layer with no always-blocked
  concept, cited by KDoc as pinned by a `PolicyParityTest` that never existed.
  It would have made the new screen report YouTube visible while it is hidden.
  Deleted; the UI renders the Kotlin record rather than re-deciding.
- Anchoring home accepted a stale or unknown-precision fix. A wrong home
  offsets the fence everywhere and forever, which is what produced a 766 m
  reading while standing at home.

Policy: Play Store is allowed by day (not during curfew) so infakt can be
installed and updated; the sweep is default-deny for third-party packages, so
anything installed from Play is hidden on the next at-home pass. Tachiyomi's
per-source extension apks are covered by a new prefix allowlist rather than an
exact list that goes stale whenever an extension is installed.

`export.py` stopped subtracting the allowlist from `blockable_system_packages`.
Sweepable is not hideable: that list decides which system packages are
eligible for a decision, and `isAllowed` is what protects them. Removing an
allowed system package from the sweep freezes it in its last state -- it would
have stranded Play hidden permanently the moment it was allowlisted.

Verified on the Pixel 6a: FINE granted POLICY_FIXED, `applied AT_HOME` with a
0 m distance from a +/-11 m GPS fix, YouTube/Chrome/Firefox absent from
`pm list packages -e`, Play Store, infakt, RunnerUp and all five Tachiyomi
packages present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjxL9kdAzZmGqQA5tMNrZ8
2026-08-14 13:43:05 +02:00

25 lines
3.3 KiB
JSON

{
"title": "focus_owner: correct the geofence input and make every decision explainable",
"objective": "The Device Owner enforcer failed in both directions on the Pixel 6a: nothing blocked at home (a coarse, fuzzed fix read as AWAY and restored the browser) and everything blocked at the office (an empty last-known cache produced LOCATION_UNKNOWN, which applies the identical sweep to AT_HOME). Neither was diagnosable, because logcat rotates and run-as is refused on the release build device owner requires. Success means the geofence classifies correctly from a precise, actively-acquired fix; the phone states on screen why it is enforcing, how far it believes it is from home, whether curfew is active, and which apps are hidden with the reason for each; and a durable in-app record answers the same questions hours later. Play Store and Tachiyomi's per-source extensions become available without weakening the YouTube block.",
"acceptance_criteria": [
"ACCESS_FINE_LOCATION is declared and self-granted by the device owner; on-device dumpsys shows granted=true.",
"Each pass actively acquires a fix inside a location-typed foreground service, bounded by a timeout, and treats a fix older than the freshness window as no fix (fail-closed).",
"The freshness window is strictly longer than the enforcement cadence, so an idle phone does not fall to LOCATION_UNKNOWN every pass.",
"Fix selection gates on freshness first and prefers accuracy only within the fresh set; a recent coarse fix beats an old precise one.",
"LOCATION_UNKNOWN and no-home-configured are each rendered distinctly on screen, never as AT_HOME.",
"Hysteresis widens the threshold only when the previous pass placed the phone inside the fence.",
"A durable, size-bounded record of each pass is written and readable in-app; it never contains latitude or longitude.",
"A malformed or wrong-typed log record is skipped and can never disable the release-device-owner escape hatch.",
"Installing over the app re-arms the alarm chain without the app being opened.",
"YouTube, YouTube Music and Chrome remain hidden in every state including AWAY; Play Store, inFakt, RunnerUp and all installed Tachiyomi packages are available at home.",
"Play Store is allowed by day and hidden during curfew.",
"The generated policy asset reproduces exactly from config.sh with home coordinates redacted."
],
"out_of_scope": [
"Making the DNS/VPN layer actually filter youtube.com. private_dns_host is empty, port 853 is not forwarded, and VPN lockdown bypasses system Private DNS, so a browser still reaches YouTube when AWAY. At home and during curfew every browser is hidden, which is what closes it there.",
"Removing the rooted-era phone_focus_mode shell daemon, which still parses the same config.sh but does not consume the new prefix lists.",
"The unrelated poker-stakes work present in the same working tree."
],
"verifier": "bash ~/.claude/scripts/finish.sh gate (pre-commit + suites); python3 -m pytest python_pkg/focus_policy/tests/ (137); cd focus_owner && flutter analyze && flutter test (94); cd focus_owner/android && JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :app:testDebugUnitTest (40); on-device: adb install -r, tap Run enforcement now, read the status card, then adb shell pm list packages -e to confirm the hidden/available sets."
}