uttrflow / uttrflow/uttrflow-swift
AI suggestions consent is stored under the app's exact-case bundle id but switched off under the lower-cased one, so the capture store never sees the decline
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
Two stores disagree about how a bundle identifier is spelled:
- `SuggestionPreferences.set(_:isOn:)` lower-cases the identifier (`Sources/UttrflowPredict/SuggestionPreferences.swift:158-167`), so `turnedOff` holds, for example, `com.apple.terminal`.
- When a switch changes, `SuggestionCoordinator.follow` forwards those lower-cased ids: `capture.record(.declined, for: application)` (`Sources/Uttrflow/Suggestion/SuggestionCoordinator.swift:132-139`).
- The capture consent map is keyed by the string as given, and looked up with the exact-case id from the field reading: `consent[bundleIdentifier] ?? .unknown` (`Sources/UttrflowPredictCapture/CapturePreferences.swift:38-41`, `:58-60`). `CaptureGate` asks with `reading.bundleIdentifier` (`Sources/UttrflowPredictCapture/CaptureGate.swift:34`).
- The automatic allow records the exact-case id: `capture.record(.allowed, for: snapshot.bundleIdentifier)` (`SuggestionCoordinator.swift:616`).
For any app whose bundle id has a capital letter, `predict-consent.v1.json` ends up with `com.apple.Terminal: allowed` and `com.apple.terminal: declined`. The capture layer's own gate then answers "proceed" for an app the user switched off.
Today the coordinator checks `preferences.isEnabled(in:)` before a turn reaches capture (`SuggestionCoordinator.swift:333`), so no lines are learned. But the second, independent gate that `Docs/predict.md` describes ("One switch, two stores: what may be suggested in is what may be learned from") does not hold for these apps. Any future capture path that skips the coordinator (such as the unwired shell-history import, `CaptureSession.importShellHistory`) would learn from an app the user turned off.
## Why it matters
Consent is a privacy control that should fail closed, and it is currently defended by one check instead of the two the design relies on.
## Acceptance criteria
- `CapturePreferences.record` and `state(of:)` normalise the identifier the same way `SuggestionPreferences` does. Existing mixed-case keys are folded on load, with `declined` winning over `allowed`.
- A test in `Tests/UttrflowPredictCaptureTests` records `.declined` for a lower-cased id and `.allowed` for the mixed-case id, and asserts that `CaptureGate.refusal` for a reading with the mixed-case id is `.consentDeclined`.
## Where to start
- `Sources/UttrflowPredictCapture/CapturePreferences.swift` (about 10 lines) and `Tests/UttrflowPredictCaptureTests`.
- Run `make verify`. See `CONTRIBUTING.md`.
- Size: small.
Contributor guide
Research direction
Start with Sources/UttrflowPredictCapture/CapturePreferences.swift and read the normalization used by SuggestionPreferences. Add or update a test in Tests/UttrflowPredictCaptureTests covering mixed-case and lower-case identifiers, then run make verify. Done means existing keys are folded with declined taking precedence and CaptureGate returns .consentDeclined for the mixed-case reading.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100