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,133 @@
|
||||
# Permissions
|
||||
|
||||
Every permission the design requires, per platform, with what it is for, what
|
||||
degrades without it, and how invasive it feels.
|
||||
|
||||
Two permissions ask for more than a user expects. This file says so rather than
|
||||
burying it: **accessibility service** on Android, and **browser extension
|
||||
permissions** on desktop (ADR-7). Both grant the ability to observe and control
|
||||
other applications. There is no way to provide the feature without them.
|
||||
|
||||
Where an optional permission is refused, the documented behaviour is what
|
||||
happens — not a silent failure.
|
||||
|
||||
---
|
||||
|
||||
## Android
|
||||
|
||||
### Required
|
||||
|
||||
| Permission | Why | Without it |
|
||||
|---|---|---|
|
||||
| `BIND_ACCESSIBILITY_SERVICE` | Foreground-app detection. Section 4.1 specifies `TYPE_WINDOW_STATE_CHANGED`, which is event-driven and needs no polling. | **Nothing blocks.** This is the single permission the product cannot work without. |
|
||||
| `SYSTEM_ALERT_WINDOW` | The block screen is an overlay that must appear over whatever the user just opened. | No block screen. Blocking would have to become a kill, which is far more destructive and loses the user's state. |
|
||||
| `PACKAGE_USAGE_STATS` | Usage-limit and launch-count conditions (section 1.2), and historical statistics (section 1.6). Granted via Settings, not at install. | Usage and launch-count conditions are unavailable. Time conditions still work. |
|
||||
| `BIND_DEVICE_ADMIN` | Uninstall protection under Strict Mode (section 1.4). | Strict Mode cannot block uninstall. The other restrictions still apply. |
|
||||
| `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_SPECIAL_USE` | A persistent notification so the service is not killed silently. | The service is far more likely to be killed by the OS, with nothing telling the user why. |
|
||||
| `RECEIVE_BOOT_COMPLETED` | Re-apply blocks after a reboot. | Enforcement does not survive a reboot until the app is opened by hand. |
|
||||
| `POST_NOTIFICATIONS` | The persistent notification itself. | The foreground service runs without its notification, and users cannot tell it is running. |
|
||||
| `QUERY_ALL_PACKAGES` | Enumerating installed apps for allowlist mode (section 1.1) and for resolving a desktop-file id to a name. | Allowlist mode cannot enumerate, so it blocks nothing (fail open, section 1.5.4). Blocklist mode is unaffected. |
|
||||
|
||||
**On `QUERY_ALL_PACKAGES`:** this one is Play-Store-sensitive and needs a policy
|
||||
justification. It is requested because allowlist mode requires knowing what is
|
||||
installed, and there is no narrower permission that does that job.
|
||||
|
||||
### Optional
|
||||
|
||||
| Permission | Why | Without it |
|
||||
|---|---|---|
|
||||
| `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION` | Location conditions (section 1.2). | The condition is hidden in the UI rather than offered and never firing. Mobile-only per section 6.4. |
|
||||
| `ACCESS_WIFI_STATE` | Wi-Fi SSID conditions (section 1.2). | SSID conditions unavailable. Time conditions unaffected. |
|
||||
| `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` | Exemption from Doze, which would otherwise suspend enforcement. | Works, but only while the screen is on, on most OEMs. |
|
||||
|
||||
### Behaviour when location is unavailable
|
||||
|
||||
Section 1.7 makes this configurable. The default is **do not block**: a lost GPS
|
||||
fix should not mean a locked device. The alternative — treat unavailable as
|
||||
present — exists for schedules whose purpose is to block *leaving* somewhere.
|
||||
|
||||
### OEM autostart
|
||||
|
||||
Section 6.5: Xiaomi, Huawei, and Samsung kill background services in
|
||||
model-specific ways that a permissions dialog cannot fix. Autostart is granted in
|
||||
the vendor's own UI, not Android's.
|
||||
|
||||
| Vendor | Where |
|
||||
|---|---|
|
||||
| Xiaomi / Redmi | Settings → Apps → Autostart |
|
||||
| Huawei / Honor | Settings → Battery → App launch, then enable auto-launch, secondary launch, and run in background |
|
||||
| Samsung | Settings → Apps → Blocker → Never sleeping apps |
|
||||
| OnePlus / Oppo | Settings → Battery → App battery management → Allow background activity |
|
||||
|
||||
**Not yet implemented.** The troubleshooting screen arrives in M7. Until then
|
||||
these workarounds are not documented anywhere the user can find.
|
||||
|
||||
---
|
||||
|
||||
## Linux
|
||||
|
||||
The X11 and KWin paths need no special permissions. That is the main advantage
|
||||
over Android and worth stating plainly.
|
||||
|
||||
| Mechanism | Needs | Notes |
|
||||
|---|---|---|
|
||||
| KWin script over `org.kde.KWin /Scripting` | Nothing | Session D-Bus, available to any process in the session. Verified present on KDE Plasma 6.6.6. |
|
||||
| X11 via `x11rb` | Nothing | `DISPLAY` access only. **Blind to native Wayland apps.** |
|
||||
| NetworkManager SSID via D-Bus | Nothing | Session bus. |
|
||||
| `systemd --user` service | `loginctl enable-linger` for post-logout operation | Without linger, the daemon stops when the user logs out, which is usually correct. |
|
||||
| Process watching via `/proc` polling | Nothing | Used only by the degraded fallback. |
|
||||
| Process watching via eBPF | Likely **CAP_BPF / CAP_SYS_ADMIN**, or permissive sysctls | Not verified on the development machine. |
|
||||
|
||||
### What an unprivileged daemon cannot do
|
||||
|
||||
Recorded because it is a platform limit, not a missing feature. See
|
||||
`desktop/platform-linux/src/lib.rs::UNPRIVILEGED_LIMITS`:
|
||||
|
||||
- Prevent a process from launching. It can only react after the fact.
|
||||
- Mask a systemd unit, which needs root. Tamper resistance is `Restart=always`
|
||||
plus a watchdog, and nothing stronger.
|
||||
- Protect its own configuration from the user who owns the files. Lock state
|
||||
lives in the database instead (section 1.5.3).
|
||||
- Read `/proc/<pid>/exe` for processes owned by other users.
|
||||
- Stop a process already running as root.
|
||||
|
||||
**Not verified:** whether eBPF process tracing works unprivileged here. The
|
||||
relevant sysctls (`kernel.unprivileged_bpf_disabled`,
|
||||
`kernel.perf_event_paranoid`) were not read, so the eBPF path is assumed
|
||||
unavailable rather than known to work. `/proc` polling is the assumed fallback.
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
|
||||
Nothing implemented yet, so nothing is measured or verified. From section 4.2:
|
||||
|
||||
| Mechanism | Needs | Notes |
|
||||
|---|---|---|
|
||||
| `SetWinEventHook(EVENT_SYSTEM_FOREGROUND)` | Nothing | Foreground detection. Event-driven, no polling. |
|
||||
| Minimise / close a window | Nothing | Standard user-session APIs. |
|
||||
| Terminating a process owned by another user | Elevation | Same-user processes need nothing. |
|
||||
| WLAN API notification callback | Nothing | Wi-Fi SSID conditions. |
|
||||
| Windows Service watchdog | Administrator, to install | Restarts the per-user daemon if it dies. |
|
||||
|
||||
---
|
||||
|
||||
## Browser extension (desktop, ADR-7)
|
||||
|
||||
The extension is a separate permission surface from the app, and arguably the most
|
||||
alarming one — it is worth being precise about what it asks for.
|
||||
|
||||
| Permission | Why | Without it |
|
||||
|---|---|---|
|
||||
| Host permissions for `<all_urls>` | Read the URL of every page the user visits, to match domains and keywords. | Nothing blocks in the browser. |
|
||||
| `webRequest` or equivalent URL observation | Same, plus redirect handling. | Domain blocking degrades to what the user navigates directly. |
|
||||
| `nativeMessaging` | Ask the daemon whether to block. | **No blocking at all.** The extension holds no blocking logic of its own, by design (ADR-7). |
|
||||
|
||||
The extension contains no blocking logic. It asks the daemon, which returns a
|
||||
decision from the same engine that decides apps. That is why the permission looks
|
||||
broader than its behaviour: it observes every URL so it can ask about it, but it
|
||||
cannot block anything the engine does not decide to block.
|
||||
|
||||
**The residual gap, stated plainly:** an extension covers browsers it is installed
|
||||
in. `curl`, or a browser without the extension, is not covered. That is section
|
||||
6.7 — friction, not a wall.
|
||||
Reference in New Issue
Block a user