f05836f528
The parity contract from CLAUDE.md section 7 now exists: 42 cases across 11 scenario groups, with a runner that asserts every case and a coverage check that asserts every mandatory scenario is claimed. **A design change, and why.** ADR-16 originally specified JSON tables, justified by "serde_json will already be in the tree". That was wrong — `rusqlite` does not pull `serde` by default and `uniffi` has not landed, so nothing provided it, and `serde` is not a section 3 dependency (rule 0.3). The tables are therefore a Rust builder DSL: the same declarations, but a typo in a field name is a compile error rather than a test that silently asserts the wrong thing. For a document whose entire job is to be a contract, compile-time checking beats diffability. Converting to JSON later is mechanical if serde is approved. **Two engine bugs the tables caught, both real:** 1. **Overnight windows woke the daemon immediately.** `time_window_boundary` computed the end as `end - minute` and clamped at zero. For a 22:00–06:00 window active at 23:00, "06:00 today" is in the past, so the boundary became *now* — and the daemon would wake, re-evaluate, get the same answer, and wake again, forever. Not a missed block, but a busy loop burning the section 5 idle budget. Fixed by detecting the late half of an overnight window and crossing midnight properly. 2. **An inactive schedule woke a day early.** The next-start search iterated day offsets to test membership but computed the delta as `start - minute` only, wrapping by 1440 without advancing the day. A Monday–Friday schedule evaluated on Saturday reported Monday's start as 21 hours away — Sunday 09:00, a day not in the set. The block still fired, because the next evaluation recomputed it, but the daemon woke a day early for nothing. Fixed by folding the day offset into the delta. **Four of my own table cases were wrong too**, which is the other half of why they are worth having: a UTC+5 offset is 15:00 local and therefore *inside* 09:00–17:00 (I had asserted outside); the same for the DST cases. The engine was right and the test was wrong in all four. **Coverage bookkeeping is checked, not promised.** `coverage::Location` records whether a scenario is covered by a table or by a code fixture — clock tampering and the 24-hour cap are about the passage of time and a bound across lock levels, not a decision at an instant, so they cannot be expressed as data. Both directions fail: an uncovered scenario is an error, and so is a `covers` tag that is not on the mandatory list, which is how a case quietly stops testing what its name claims. **`BlockReason` now carries its target.** A schedule may name twenty targets and they do not all become blocked — some are paused, some belong to a condition that is not currently active, some are on the never-block list. Without carrying which target a reason is about, two blocked apps under one schedule produce two identical reasons and the UI has to re-derive the pairing, which is exactly the drift `BlockReason` exists to prevent. The tables could not assert per-app attribution without it. **The DST gap is recorded, not papered over.** `CivilTime` is an epoch plus an offset with no timezone database behind it, so the tables pin that a fixed local window stays fixed as the offset moves — including across a DST transition — but cannot express a spring-forward time that does not exist or a fall-back time that is ambiguous. Those are section 7 cases that are genuinely not covered, and LIMITATIONS.md now says so. Co-Authored-By: Claude Code <noreply@anthropic.com>
201 lines
9.2 KiB
Markdown
201 lines
9.2 KiB
Markdown
# Limitations
|
|
|
|
CLAUDE.md section 6 is titled "Known hard limits (tell me, don't hide them)". This
|
|
file is that promise, kept in writing.
|
|
|
|
Two rules govern the wording here. Every limitation is stated as a fact about the
|
|
operating system, not as a bug with a workaround. And no entry uses the words
|
|
"unbypassable", "tamper-proof", "guaranteed", or "secure" — section 6.1 and 6.7
|
|
require those claims not be made, and writing around them is how a document
|
|
quietly starts lying.
|
|
|
|
Nothing in this file is aspirational. Where a limitation is not yet implemented,
|
|
it says so.
|
|
|
|
---
|
|
|
|
## The safety guarantees, stated first
|
|
|
|
Before the limits, the guarantees. Section 1.5.4 makes these non-negotiable, and
|
|
they constrain what the limitations below are allowed to be.
|
|
|
|
- **Never-block list.** Phone, dialler, emergency calls and SOS, and emergency
|
|
alerts can never be blocked. On desktop, the OS shell and the Settings basics
|
|
needed to keep the machine usable. Hardcoded in
|
|
`core/src/never_block.rs`, not a setting, with no flag that reverses it. The
|
|
check happens inside the engine after schedule evaluation, so a schedule naming
|
|
a protected target blocks its other targets and cannot block the protected one.
|
|
- **Finite worst case.** The longest possible path to disabling blocking is 24
|
|
hours. No lock exists without an exit. Asserted by
|
|
`worst_case_disable_path_is_capped`.
|
|
- **Recovery is never interfered with.** Factory reset, safe mode, and OS-level
|
|
recovery are not blocked, and never will be. Section 6.7: that is correct
|
|
behaviour, not a gap.
|
|
- **Fail open.** A service crash loop or a corrupt database must never leave a
|
|
device unusable. Recovery renames the damaged file aside and starts clean
|
|
rather than failing to launch.
|
|
- **Opt-in only.** Strict and Stubborn are never enabled by default, and never by
|
|
a schedule template.
|
|
- **Always escapable.** The "How to turn this off" documentation is reachable
|
|
*without* disabling anything, including under Strict Mode.
|
|
|
|
---
|
|
|
|
## 1. Desktop Strict Mode cannot be made tamper-proof against root or admin
|
|
|
|
**Status:** fundamental. Not being worked around.
|
|
|
|
On desktop, a user with administrative rights, or root, can stop the service, edit
|
|
its files, delete it, or inspect its memory. No design prevents this. Section 6.1
|
|
requires this be documented honestly and that we not claim otherwise.
|
|
|
|
Mitigations actually implemented:
|
|
|
|
- A watchdog service that restarts the daemon if it dies.
|
|
- File ACLs, and where the filesystem supports it, an immutable flag on config.
|
|
- Restart-on-kill, so a kill is not durable.
|
|
- A tamper log, so the attempt is recorded even though it succeeds.
|
|
|
|
What none of this changes: a determined user with root removes the app. The goal
|
|
is to outlast an impulse, not to defeat a determined person.
|
|
|
|
## 2. Desktop website blocking has no single clean mechanism
|
|
|
|
**Status:** decided (ADR-7), with a residual gap.
|
|
|
|
Shipping a Firefox extension over native messaging. That makes keyword and
|
|
path-level blocking work, and unlike DNS or a hosts file it is not defeated by
|
|
DoH, because it inspects the URL in the browser rather than the network path.
|
|
|
|
The costs are real: the user must install an extension and grant permissions that
|
|
look alarming, and Firefox-only for now.
|
|
|
|
**The residual gap:** an extension covers browsers it is installed in. A
|
|
determined user can `curl`, or use a browser without it. This is section 6.7
|
|
territory — friction, not a wall.
|
|
|
|
DNS/hosts-only blocking was rejected because it cannot do keyword or path-level
|
|
matching and cannot defeat DoH. Doing both was rejected because two mechanisms
|
|
means two sources of truth, and this project has a strong interest in exactly one
|
|
decision path.
|
|
|
|
## 3. Wayland focused-window detection is compositor-dependent
|
|
|
|
**Status:** partially implemented (ADR-14), partly not.
|
|
|
|
There is no portable API for "which window has focus" on Wayland. Each compositor
|
|
has its own mechanism, and none of them is a standard.
|
|
|
|
Implemented:
|
|
|
|
- **KDE Plasma** — a script loaded over `org.kde.KWin /Scripting`. Verified
|
|
available on the development machine.
|
|
- **X11** — `x11rb` on `_NET_ACTIVE_WINDOW`. Works on any X11 session, and on
|
|
XWayland clients under Wayland. **Blind to native Wayland apps**, which is why
|
|
it is a fallback rather than the primary.
|
|
- **Process-based fallback** — no idea what has focus, acts on process creation,
|
|
and cannot show a meaningful block screen.
|
|
|
|
Not implemented, still in M7:
|
|
|
|
- **wlroots** compositors, via `foreign-toplevel-management`.
|
|
- **GNOME**, via an extension.
|
|
|
|
A further degradation on this machine specifically: `/proc/connectors` does not
|
|
exist on its kernel, so the netlink proc-connector process watcher is
|
|
unavailable. The process fallback must use eBPF or `/proc` polling instead, both
|
|
of which cost CPU and none of which can produce a proper block screen.
|
|
|
|
## 4. Location schedules are mobile-only
|
|
|
|
**Status:** implemented as a deliberate absence on desktop.
|
|
|
|
There is no reliable desktop equivalent. The engine carries the condition and
|
|
validation *rejects* it on non-Android platforms
|
|
(`ValidationError::LocationOnDesktop`), and the desktop UI hides it.
|
|
|
|
This is a design choice rather than an omission. A condition that is offered but
|
|
never fires is worse than one that is not offered: it teaches the user the app
|
|
does not work.
|
|
|
|
## 5. Android OEMs kill background services aggressively
|
|
|
|
**Status:** not yet implemented. M3-Android.
|
|
|
|
Xiaomi, Huawei, Samsung and others kill background services in ways that vary by
|
|
model and by version. An accessibility-service-based blocker is exactly the kind
|
|
of process these vendors target.
|
|
|
|
The plan is a troubleshooting screen covering autostart and battery settings per
|
|
OEM. **Not yet written**, and no OEM-specific workaround exists yet.
|
|
|
|
## 6. Android accessibility URL reading breaks when browser UIs change
|
|
|
|
**Status:** not yet implemented. M5.
|
|
|
|
Reading a browser's URL bar through the accessibility tree means depending on that
|
|
browser's internal view structure, which changes without notice. Every browser
|
|
needs its own resource-id table, and those tables rot.
|
|
|
|
The plan is a per-browser table with a maintenance path. **No table exists yet**,
|
|
and there is no mechanism for a user to report a broken browser other than an
|
|
issue. This will be a recurring support cost, not a one-time fix.
|
|
|
|
## 7. Friction is not a wall
|
|
|
|
**Status:** by design. Section 1.5.4 and 6.7.
|
|
|
|
A user with ADB, root, safe mode, a factory reset, or administrative rights on
|
|
desktop can always remove this app. Blocking ADB, safe mode, or OS recovery is
|
|
not attempted and never will be — ADR-11.
|
|
|
|
What the friction ladder does buy is time, in the moment, against an impulse. A
|
|
user who decides calmly and deliberately still gets through, within a bounded 24
|
|
hours.
|
|
|
|
Stating this plainly matters for a reason beyond honesty: a product that claims to
|
|
be unremovable is a product whose users cannot trust its other claims either.
|
|
|
|
## 8. Play Store policy is hostile to apps that resist uninstall
|
|
|
|
**Status:** accepted. Distribution decision not yet finalised.
|
|
|
|
Accessibility and Device Admin use require policy justification, and both are used
|
|
here for core functionality. An app that resists uninstall is close to
|
|
categorically unpublishable.
|
|
|
|
The plan is to keep the strongest friction behind an explicit opt-in and treat a
|
|
sideload or F-Droid flavour as the primary distribution for it. **Not yet built.**
|
|
|
|
---
|
|
|
|
## Limits specific to this implementation
|
|
|
|
Not platform limits — known shortfalls in the current code, recorded so they are
|
|
not mistaken for platform facts.
|
|
|
|
- **`ProcessBackend` is not implemented.** ADR-14 lists three backends; the KWin
|
|
and X11 paths land in M2-Linux, and the process fallback is weaker than the
|
|
spec assumes because of the `/proc/connectors` gap above.
|
|
- **`rusqlite` vs system-linked SQLite is unresolved** (ADR-4). Bundled is built
|
|
and costs ~1.1 MB of the 5 MB daemon budget. System-linking would reclaim most
|
|
of that but adds a runtime dependency on the host's libsqlite3.
|
|
- **Lock state is not yet persisted or enforced.** `core/src/lock.rs` has the
|
|
timers, the escalation, and the wall-clock correction, all tested. The state
|
|
machine is not yet driven from a UI, and `LockState::evaluate` currently returns
|
|
the Normal level unconditionally.
|
|
- **DST coverage is partial, and the boundary is stated.** `CivilTime` is an
|
|
epoch value plus a UTC offset, with no IANA timezone database behind it — the
|
|
`time` crate is not a section 3 dependency (ADR-16). The golden tables pin the
|
|
observable rule, that a fixed local window stays fixed as the offset moves,
|
|
including across a DST transition. What they **cannot** express is a local time
|
|
that does not exist because the clock sprang forward, or one that is ambiguous
|
|
because it fell back. Those are cases section 7 lists that are not yet
|
|
covered. This is the strongest argument for a timezone-aware time type.
|
|
- **`LockState` still always evaluates to `Normal`** — persistence works, but no
|
|
UI drives the lock state machine yet.
|
|
- **`Condition::UsageLimit` and `LaunchCount` sum across all targets** rather
|
|
than the schedule's own target set, until targets carry usage keys.
|
|
- **Windows is entirely unimplemented** and cannot be compiled or tested on the
|
|
development machine, so writing enforcement now would mean shipping unverified
|
|
code. |