Repository navigation
Route every still-image reader through the privacy chokepoint (W04.1 follow-on) - #458
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, persource), 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
seeendpoint (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