-
Notifications
You must be signed in to change notification settings - Fork 0
The Safety Invariant
LULL's README and CLAUDE.md both say the "one rule" — horror by permission,
not violation — is "enforced in code." That's true, but it's true of one
half of the guarantee and not the other. This page states precisely which
half is which, because overclaiming here is exactly the kind of thing App
Review (and any careful reader) will catch.
- "LULL cannot even name a forbidden sensor." — Photos, contacts, location, and health data.
- "The camera never runs without consent." — The one sensor LULL does use, gated correctly every time.
These are different kinds of guarantee, verified differently.
In LULLKit/Sources/LULLKit/Consent.swift:
public enum Sensor: String, CaseIterable, Sendable, Codable {
case camera
case microphone
case notifications
case haptics
case motion
}Sensor is a closed Swift enum. There is no case for photos, contacts,
location, or health data. ConsentLedger.grant(_:), .mayUse(_:), and every
other consent API take a Sensor as their parameter type — so no line of
Swift anywhere in this codebase can express "ask for contacts" or "read
location." It isn't refused at runtime; it can't be written, full stop. To
add such a sensor, someone would have to edit Consent.swift itself, in a
diff any reviewer would immediately see for what it is.
This is a real type-system guarantee, not a policy or a runtime check. It's the strongest kind of promise a codebase can make.
The camera is allowed to exist (Sensor.camera is a valid case), so the
type system can't forbid using it — it can only help make sure it's used
correctly. Today, "correctly" is enforced by a hand-written sequence, not by
a type that makes the wrong order impossible.
Trace the actual call path in
app/Sources/GameModel.swift:
func allowTheEye() async {
consent.grant(.camera)
guard await camera.requestAccess() else {
consent.revoke(.camera)
eye.consent(false)
return
}
eye.consent(true)
await camera.start()
startClock()
}This is the only place in the app that calls camera.start(), and the
sequencing — grant consent, then ask the OS, then start — is correct as
written and matches the "in-app first, OS prompt second" rule from
CLAUDE.md. But look at
CameraController
and EyeSession
themselves:
-
CameraController.start()has no internal check againstConsentLedgerorEyeSession.wantsCamera. It will start the capture session if you call it — from anywhere, at any time. It trusts its caller. -
EyeSession.wantsCamerais a derived, read-only property the app is expected to consult ("the app asks this and nothing else," per the doc comment) — but nothing in the types forcesGameModelto actually check it before callingcamera.start().GameModelhappens to only callstart()right aftereye.consent(true), but that's the current code being well-behaved, not a constraint the compiler would catch if a future call site skipped it. - Nothing stops a hypothetical new call site (a new view, a debug button, a
future feature) from constructing its own
CameraControllerand calling.start()directly, bypassingConsentLedgerandEyeSessionentirely. The compiler would accept that code.
So: the shape of the guarantee that exists today is "one reviewed function,
GameModel.allowTheEye(), does the right thing, and it's the only place that
touches the camera." That's a real, verifiable property of the current
codebase — but it's a convention upheld by there being one call site and a
small team, not an invariant the type system defends against a second call
site being added carelessly.
closeTheEye() (the panic switch) has the same shape: it's correct today —
it cancels the clock, calls eye.release(), consent.revokeAll(), and
camera.stop() — but nothing types-check that every path capable of
starting the camera is paired with a path that can stop it.
Not implemented — noted here as the honest "what a stronger version would look like," not as a roadmap commitment:
- Have
CameraController.start()(or a wrapper) take aConsentLedgeror anEyeSessionsnapshot as a required argument, so the compiler refuses to compile a call site that doesn't supply proof of consent. - Make
CameraGateconformances only obtainable through a factory that requires a grantedConsentLedger, soCameraControllercan't be constructed and started independently of the consent flow.
The consent logic itself — independent of whether every call site respects
it — is unit-tested in
ConsentTests.swift
and
EyeSessionTests.swift:
default-deny, per-sensor scoping, immediate revoke, the panic switch clearing
everything, denial being respected and terminal from any phase, and the
camera clock being unable to fast-forward past a backgrounded app. These are
real, green, and pinned. What they don't and can't test is "every future call
site in the app will remember to check consent first" — that's the part that
currently relies on the app being small enough that one person can review
every call to camera.start() by eye.
- Forbidden sensors: genuinely impossible to name in code. Compiler-level.
-
Consent gates the camera: correctly wired today, verified by reading
the single call site — but held up by code review and a small surface
area, not by the type system. Worth re-checking any time a new call site
touching
CameraControlleris added.
Home · Concept & Design · The Safety Invariant · Architecture · Building & Running · Roadmap / Ideas
testtest126/LULL · MIT license
you can close this tab now. it will still be listening.