Skip to content

Route every still-image reader through the privacy chokepoint (W04.1 follow-on) - #458

Merged
straff2002 merged 3 commits into
mainfrom
feat/compliance-p5b
Sep 10, 2026
Merged

straff2002 merged 3 commits into
mainfrom
feat/compliance-p5b

Conversation

@straff2002

Copy link
Copy Markdown
Owner

Closes the gap #451 recorded in roadmap task W04.1 in EU (finding F03): about 22 call sites read the camera's latest frame directly and handed the bytes to a model, a file, another process or the Photos library without passing through the privacy filter.

One accessor. CameraService.filteredStill(for:source:) is now the only way to obtain a still for anything that leaves the process: it reads the latest frame (or captures one, per source), runs it through the filter and returns either the filtered still or an explicit unavailable reason. Never the source pixels. The filter is wired once at the composition root; unwired reports as unavailable rather than passing raw.

Routed. Structured vision, safety assessment, assistive mode, navigation, live coach, the direct-mode smart capture (a live bypass three functions from an already-filtered sibling), the capture, photo-log and money tools, the MCP see endpoint (503 when unavailable) and the dwell-capture Photos crop. Twelve on-device OCR and scanning tools go through the same accessor with an on-device scope so the roster names them. Size and liveness checks use two new non-pixel accessors. Five new filtered scopes, 24 roster additions.

Enforcement. The consumer scrape now also matches raw still reads and the chokepoint calls: a raw read needs a roster case with a raw tap, any file that obtains camera pixels and calls a model, Photos or export sink must contain a chokepoint call, and every new scope must have a real call site. 23 routing tests drive each family with a canary still through filtered and unavailable cases.

Evidence. Full OpenGlassesTests 5222 executed, 0 failures, 13 skipped. Release build succeeded.

Deliberately not routed, needs a product decision. CameraService.capturePhoto(), the deliberate shutter, saves every capture to Photos unfiltered and about six app paths send those bytes to a model. Blurring a photograph the wearer deliberately framed is a product call rather than wiring; it is named as owed in the rows. Device captures and CPU pressure remain owed.

🤖 Generated with Claude Code

straff2002 and others added 3 commits September 10, 2026 12:06
The bystander blur covered the camera-rate consumers: one shared
OutboundFrameRelay, one subscription, and a roster test that fails the
build when someone taps the raw publisher instead. It did not cover the
way this app actually sends most of its pixels — one still at a time.

About two dozen call sites read CameraService.latestFrame, JPEG-encoded
it, and handed the bytes to a cloud model, a session log, the Photos
library or an HTTP client. Six calls to filtered(_:for:) existed in the
whole app, all of them in AppState. Structured vision, the HECA safety
assessment, assistive mode, navigation assist, the live coach, the
identification and lookup and scanning and capture tools, and the local
MCP server's see_glasses all took the unfiltered path — and so did
"smart camera" in AppState, three functions below a sibling that had
been filtered since the blur was first wired up.

CameraService.filteredStill(for:source:) is now the only way to get a
still. It replaces the latestFrame-then-capturePhoto fallback that was
copied into every reader, and folds the privacy decision into it, so the
two cannot drift apart again. The scope is required rather than
defaulted: a reader that has not decided what its still is for has not
decided whether a bystander's face may travel with it. Every step fails
closed — no frame, no filter wired, or a filter that cannot run right
now each return .unavailable rather than the source pixels.

Five scopes name the newly covered purposes (visionAssessment,
assistiveGuidance, toolPhotoCapture, photoLibrary, remoteFrameRequest).
The on-device readers — Vision OCR, barcode and QR decoding, colour
sampling — request their stills through the same accessor under an
on-device scope, so the classification is written at the call site
instead of inferred from a missing filter call.

Dwell capture keeps its raw saliency subscription and its raw on-screen
preview, and filters the crop before it reaches the Photos library: a
library shared with every app the wearer has granted it to, and synced
off the device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two guards, matching the two the frame-publisher surface already has.

The source scrape in OutboundFrameConsumerTests now also reads raw
latestFrame accesses and filteredStill(for:) requests, so the roster
covers the still half of the app: a type that reads raw pixels has to
own a roster case with a raw tap saying why, and a file that both
obtains camera pixels and calls a model, a Photos write, a session log
or an HTTP response has to contain a chokepoint call. One file is
exempt, and it is the accessor's own home.

FilteredStillRoutingTests drives each rerouted family through a fake
provider, twice: once returning a filtered still and once returning
.unavailable. The first case asserts the sink receives the filtered
pixels — a two-tone canary survives the JPEG round trip and is checked
after it. The second is the one that matters, because the failure being
guarded against is a path that, unable to filter, sends the raw frame
anyway: the model is never called, no image reaches the tool result,
nothing is written to the Photos library, and the wearer is not told a
capture was saved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
W04.1 and F03 both carried the same two owed items: route the still
readers that took CameraService.latestFrame straight to a model, and
settle the dwell-capture crop. Both are done, so both come off — and
what replaces them is narrower and honest: a deliberate shutter capture
is still written to the Photos library and sent to the model unfiltered,
and whether the wearer's own framed photograph should be blurred is a
product decision rather than a wiring one.

Device captures of real background/lock transitions and of the blur
under CPU/thermal pressure stay owed, unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@straff2002
straff2002 merged commit 0223f0a into main Sep 10, 2026
9 checks passed
@straff2002
straff2002 deleted the feat/compliance-p5b branch September 10, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant