Leaving the house was an off switch. The AWAY branch returned early with
packagesToShow = every installed package, so walking 150m from home
restored YouTube -- which is the exact affordance device owner was
provisioned to remove.
always_blocked_packages is exempt from all four branches: AWAY hides
them, the allowlist loop cannot show them, and the workout release skips
them, because otherwise "go for a run" is the off switch instead. Chrome
is included as a second route to the same content; Firefox stays, and
carries the uBlock filters. Play is deliberately left geofenced so apps
can still be installed away from home -- and a hidden package cannot be
reinstalled from Play anyway.
infakt moves from the allowlist to SYSTEM_NEVER_DISABLE. Allowlisting
only held while it stayed in BOTH the day and night lists, and dropping
it from either would silently make it hideable; isAllowed() consults the
never-disable list before the curfew split, so it now holds under every
condition. It is device-paired to a bank over SMS, so losing it strands
the same re-authentication chain a hidden Messages app would.
The exporter now refuses an always-blocked package that is not also
blockable: the runner filters unopted system packages out of
installedPackages, so such an entry would never be hidden and nothing
would say so.
Verified: 128 Python tests at 100% branch coverage, 16 Kotlin tests, the
new ones canary-checked by breaking an assertion and confirming the
suite actually fails. On device, youtube/music/chrome/vending are
hidden=true, infakt hidden=false, uninstall still refused.
The AWAY path itself is unit-tested but NOT verified on hardware: with
no home_location.json every pass decides LOCATION_UNKNOWN, so the branch
is unreachable on this phone until coordinates can be provisioned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC
The Device Owner sweep skipped every FLAG_SYSTEM package, which meant it
could not touch the apps it exists to block: YouTube ships at
/product/app/YouTube as a system app, and so does Chrome.
Deleting the filter is not the fix. Measured on the Pixel 6a, that would
expose 320 system packages of which 243 match no allowlist entry and no
never_disable_prefix -- including com.android.cellbroadcastreceiver
(emergency alerts), com.android.credentialmanager and
com.android.devicelockcontroller. So system apps stay default-deny and
blockable_system_packages is the opt-in.
com.android.vending is on the list deliberately: leaving Play reachable
makes every other removal a one-tap undo.
The list lives in the Python exporter rather than config.sh because it is
a safety boundary, and a shell typo should not be able to widen it. The
exporter subtracts anything already allowed, so an asset can never state
that a package is both allowed and blockable. Kotlin reads the field with
a new optionalStringSet: an asset predating the field parses to the empty
set instead of throwing, because a throw makes decide() skip the pass and
silently disable enforcement.
Verified on-device under a profile owner: hid 1 for YouTube, restored 1
when temporarily allowed, and cellbroadcastreceiver, credentialmanager,
devicelockcontroller and settings all left hidden=false.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC
setApplicationHidden drops a package from the default PackageManager
enumeration even though it stays installed=true, so a sweep querying
with flags 0 could never see anything it had already hidden. That made
packagesToShow permanently empty: measured on device, a pass following
one that hid three apps reported "hid 0, restored 0" and left them
hidden with no way back. Query with MATCH_UNINSTALLED_PACKAGES instead.
Hiding the enforcer itself is the other unrecoverable case, since it
takes the escape hatch and the trigger with it. The exporter now injects
com.kuhy.focus_owner into both the day and night allowlists so the
decision layer cannot propose it, and the runner keeps its point-of-use
skip to cover a policy asset rendered before this fix or hand-edited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J