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

Open Beginner friendly
#668 0 comments 0 reactions 0 assignees View on GitHub
area:ai-suggestions bug good first issue P0 security
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.