You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge origin/main into the milli-unit telemetry branch
Main landed the observed-enforcement gate (#79) and the quick-action
buttons (#77) while this branch was waiting on CI.
Two textual conflicts, both the same shape and both in test setup where
main added ChargingRepository's evidenceStore/buildIdentity arguments on
the exact line this branch added BatteryReader's unitCalibration
argument. Both sides are wanted, so both were kept.
A third break was semantic rather than textual, so git merged it cleanly
and the compiler caught it: ChargingRepositoryRestoreGateTest arrived
with main as a new file and constructs BatteryReader without the
calibration argument this branch added. Same fix as the other five call
sites.
Verified after resolution: 1345 tests across both flavors. Checked
specifically that main's adapter-gating rework did not invalidate this
branch's UnmappedDeviceSettingsIntentTest, which asserts a device matches
no adapter at all; LineageOS now matches on the provider rather than a
codename allowlist, but a bare Robolectric device still selects nothing.
Copy file name to clipboardExpand all lines: .claude/rules/privileged-access.md
+63-13Lines changed: 63 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -142,9 +142,11 @@ the qualification ledger (`device-qualification` skill).
142
142
143
143
### LineageOS
144
144
145
-
LineageOS control requires **all** of: LineageOS (`DeviceInfo.isLineageOs`), a **physically-qualified
146
-
device codename** (`Build.DEVICE` ∈ `LineageChargingAdapter.QUALIFIED_CODENAMES`), the `lineagesettings` provider
147
-
present, and the system user.
145
+
LineageOS control requires **all** of: LineageOS (`DeviceInfo.isLineageOs`), the `lineagesettings` provider
146
+
present, the system user, and clearing the **enforcement evidence gate** for this exact build (a maintainer
147
+
codename, or the user's explicit opt-in on an unconfirmed build, and no refutation — see "Enforcement evidence gate"
148
+
below). The adapter *matches* every provider-carrying LineageOS build; what it may *do* there is decided by the
149
+
gate, not by a codename.
148
150
149
151
**Detect LineageOS with the `org.lineageos.android` system feature, never `ro.lineage.build.version`.** All five
150
152
`ro.lineage.*` properties are labelled `u:object_r:custom_version_prop:s0`, which SELinux denies to
@@ -154,14 +156,17 @@ adapter (verified on oriole / LineageOS 23.2 / Android 16). `hasSystemFeature` n
154
156
permission. `lineageOsVersion` remains a **secondary identity signal** — `isLineageOs` ORs it in so derivatives that
155
157
relabel the property still match — and is normally null on real hardware; it is not "diagnostics only".
156
158
157
-
**Blocker before the first codename is added to `QUALIFIED_CODENAMES`:** the live gate is codename-scoped, but HAL
158
-
capability is *build*-scoped — oriole exposed the LIMIT mode bit on LineageOS 20 and dropped it on 23.2 on identical
159
-
hardware. A bare codename entry would therefore claim more than any single qualification run proves. Scope the entry
160
-
by codename **plus** the qualified Lineage generation / API level (and treat a property-only or derivative match as
161
-
insufficient for the live gate), or qualify every build you intend to cover. It is **manufacturer-agnostic** (LineageOS runs on many OEMs), so the Lineage
162
-
live/lab adapters are ordered **before all OEM adapters** in `AdapterRegistry` — a LineageOS build on Samsung/
163
-
Xiaomi/OnePlus hardware must never be swallowed by a manufacturer-based lab adapter. Unqualified LineageOS builds
164
-
fall to `LineageLabAdapter` (diagnostics/contribution).
159
+
`QUALIFIED_CODENAMES` survives as the **maintainer fast path** only (still empty): a codename there is the ONLY
160
+
route to the confirmed tier, since nothing Amply can observe confirms a cap (see below). It is deliberately no
161
+
longer what makes the adapter reachable, because HAL capability is
162
+
*build*-scoped — oriole exposed the LIMIT mode bit on LineageOS 20 and dropped it on 23.2 on identical hardware —
163
+
so a bare codename entry claims more than any single qualification run proves. Add one only with a qualified device
164
+
plus a ledger row, and only when you mean every build on that device.
165
+
166
+
LineageOS is **manufacturer-agnostic** (it runs on many OEMs), so the Lineage live/lab adapters are ordered
167
+
**before all OEM adapters** in `AdapterRegistry` — a LineageOS build on Samsung/Xiaomi/OnePlus hardware must never
168
+
be swallowed by a manufacturer-based lab adapter. A build **without** the settings provider does not match the live
169
+
adapter at all and falls to `LineageLabAdapter` (diagnostics/contribution), which keeps that ordering for it.
165
170
166
171
The three keys live in the private `content://lineagesettings/system` provider — **NOT** any AOSP `settings`
167
172
namespace. Modeled as `SettingNamespace.LINEAGE_SYSTEM`: **reads are unprivileged** (`LineageSettingsClient`,
@@ -174,11 +179,56 @@ Writable keys/domains (`LineageSettingWritePolicy`, independent of the adapter):
174
179
175
180
**Crucial gate rationale:** the setting can be written while the `vendor.lineage.health.IChargingControl` HAL never
176
181
actually limits (the `mIsLimitSet:false` class of bug) — setting readback does **not** prove hardware enforcement.
177
-
That is why the gate is a qualified-codename allowlist, not "any LineageOS device": a device must be physically
178
-
proven (see the qualification protocol) before its codename is added. The adapter also **refuses** (reads
182
+
That is why control is never granted on "any LineageOS device": either the maintainer proved the hardware (a ledger
183
+
row), or the user accepted an explicitly unconfirmed build that Amply keeps watching for a refutation. The adapter also
184
+
**refuses** (reads
179
185
`Unknown(unrecognizedValue=true)`) any native state it cannot restore exactly — AUTO/CUSTOM schedule modes, off-tick
180
186
limits, or an absent/malformed `enabled` — so a temporary session never clobbers the user's own choice.
181
187
188
+
## Enforcement Evidence Gate
189
+
190
+
For adapters that set `ChargingAdapter.enforcementEvidenceRequired` (LineageOS today), a settings read-back is
191
+
**not** a licence to offer control: it proves the ROM stored the value, not that the charging hardware acts on it.
192
+
`AdapterRegistry.select()` therefore takes an explicit `EnforcementEvidenceState` — **no default**, so "the caller
193
+
forgot", "not read yet" and "genuinely nothing stored" cannot collapse into control-enabled — and resolves a tier in
194
+
this order: **REFUTED** (or a corrupt record, which may be one) → control off, contribution wanted; **CONFIRMED**
195
+
(a maintainer codename, and nothing else) → control as probed; **UNVERIFIED** (the user accepted the unconfirmed
196
+
build) → control as probed, but no surface may claim the cap is proven; otherwise **CANDIDATE** → control off.
197
+
Callers that only need adapter *capabilities* pass `EnforcementEvidenceState.Loading`, which can never enable
198
+
control. A probe that **already** refused control (secondary user, missing provider) short-circuits the whole
199
+
resolution: enforcement stays null and no surface offers an opt-in that could not change anything.
200
+
201
+
The gate governs **new control only**. A restore the user is already owed — the session restore, its rollback, boot
202
+
recovery of one — goes through `ChargingRepository.restorePersistent()`, which applies every adapter precondition
203
+
but not the evidence tier: an OTA mid-session changes the build identity, and refusing the owed protective write
204
+
would strand the device in the session's Unrestricted state.
205
+
206
+
Pending recovery work is **not** automatically an owed restore, so every recovery target carries a persisted
207
+
`RecoveryOrigin` and `writeRecoveryTarget()` dispatches on it: `SESSION_RESTORE` takes the ungated path,
208
+
`USER_REQUEST` (a widget/tile persistent choice, which `setPersistentPolicy` persists *before* its write) stays on
209
+
the gated `reapplyPersistent()`. A fresh user write — `Unrestricted` included — must not reach a build the gate
210
+
refuses just because a process death turned it into recovery work. The field's default is the gated
211
+
`USER_REQUEST`, so a record from a build without it cannot bypass the gate either.
212
+
213
+
Evidence is produced by `charging/core/enforcement/`: a pure `EnforcementVerdictEngine` over the monitor's battery
214
+
ticks, persisted by `EnforcementEvidenceStore`. Three properties are load-bearing and must not be relaxed:
215
+
216
+
-**Observation can only refute, never confirm — `EnforcementVerdict` has exactly one value.** No passively
217
+
observable signal distinguishes a cap hold from a thermal or weak-supply pause. `EXTRA_CHARGING_STATUS` == 4
218
+
looked like one, but it is *session-scoped*: measured on a Pixel 6 / LineageOS 23.2, the extra read 4 while the
219
+
device was actively charging at level 70 under an 80% cap — it means "limit mode is enabled for this plug
220
+
session", exactly as `StatsLimitHitDetector`'s KDoc documents for Pixel. The only field that differs between a
221
+
cap hold and a thermal pause is `EXTRA_STATUS`, which both produce. So Amply never claims a cap is verified from
222
+
observation; the confirmed tier comes solely from physical qualification. Earning a real confirmation would take
223
+
a **guided two-cap challenge** (write a cap below the current level and watch charging cut, raise it and watch it
224
+
resume, cut again) — known, and deliberately not implemented. **REFUTE keys on an upward level trend** through
225
+
the cap from any starting level, needs no hardware signal, and deliberately ignores the reported battery status,
226
+
which a ROM can misreport while charging past the limit.
227
+
- Evidence is scoped to a **composite build identity** (fingerprint + incremental + build time + provider version
228
+
code, hashed). `Build.FINGERPRINT` alone is useless here: LineageOS spoofs it to stock.
229
+
- A refutation is **terminal** for its scope, and a corrupt record is treated as a refutation. Both are fail-closed
230
+
on purpose — the one error this gate exists to prevent is claiming protection that isn't there.
231
+
182
232
## Foreground Service Requirement
183
233
184
234
The temporary override uses a `specialUse` foreground service because dormant apps cannot reliably receive
0 commit comments