docs: add ADR log, architecture, limitations, permissions, perf, testing
The documentation set CLAUDE.md sections 2, 6, 8 and 10.1 call for, written as records of decisions actually made rather than as a description of intent. **DECISIONS.md** — 17 ADRs. Two are deliberately left open and say so: ADR-6 (the desktop UI toolkit, which section 3 requires be decided on measurement and which needs the owner's call, partly because Slint is GPL-3.0) and ADR-17 (the licence, which is a legal commitment nobody should make on the owner's behalf). Where CLAUDE.md gives a reason for a decision, that reason is recorded; where it does not, the file says the reason is unrecorded rather than inventing one. **LIMITATIONS.md** — all eight section 6 limits plus the section 1.5.4 safety guarantees, stated as facts about the operating systems. The words "unbypassable", "tamper-proof", "guaranteed" and "secure" appear nowhere, per sections 6.1 and 6.7. Ends with a separate list of shortfalls in the *current code* — storage has no I/O, the golden tables cannot yet be parsed, LockState always returns Normal — so a known gap in this implementation is never mistaken for a platform limit. **ARCHITECTURE.md** — the layering, the purity contract and its mechanical enforcement, why `next_wakeup` must be armed over inactive schedules too, the overnight weekday-of-start rule, and the end-to-end flow for one block event. **PERF.md** — targets and results as separate tables, so an aspiration is never read as a measurement. The one real number recorded (0.279 MB stripped daemon) is labelled as nearly meaningless, since the daemon is two print statements. Lists the common causes of idle-wakeup regressions. **PERMISSIONS.md** — per platform, what each permission is for and what degrades without it. Flags the two invasive ones (Android accessibility, extension host permissions) rather than burying them, and records that unprivileged eBPF is *assumed* unavailable here because the sysctls were never read. **TESTING.md** — the golden-table contract, the add-the-case-first rule, and why clock tampering needs code fixtures instead of data. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# 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` has not been added.** The schema is written and reviewed in
|
||||
`core/src/storage.rs` (ADR-4) but storage has no I/O behind it yet, so nothing
|
||||
persists. A restart loses all state.
|
||||
- **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.
|
||||
- **The golden tables cannot be parsed.** The format is specified in
|
||||
`tests/src/golden.rs`; parsing needs serde, which is a dependency decision
|
||||
awaiting sign-off (ADR-13, ADR-16). The behavioural coverage that exists today
|
||||
is hand-written unit tests in the same spirit.
|
||||
- **`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.
|
||||
Reference in New Issue
Block a user