Skip to content

privacy(hid): gate Ulanzi Dial + StreamDeck+ behind opt-in toggles to stop the unconditional macOS Input Monitoring prompt (#3257). Principle III.#3258

Merged
ten9876 merged 2 commits into
mainfrom
aetherclaude/issue-3257
May 29, 2026
Merged

privacy(hid): gate Ulanzi Dial + StreamDeck+ behind opt-in toggles to stop the unconditional macOS Input Monitoring prompt (#3257). Principle III.#3258
ten9876 merged 2 commits into
mainfrom
aetherclaude/issue-3257

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #3257

What was changed

privacy(hid): gate Ulanzi Dial + StreamDeck+ behind opt-in toggles to stop the unconditional macOS Input Monitoring prompt (#3257). Principle III.

Files modified

  • packaging/macos/Info.plist.in
  • src/gui/MainWindow.cpp
  • src/gui/RadioSetupDialog.cpp
 packaging/macos/Info.plist.in |  2 ++
 src/gui/MainWindow.cpp        | 53 +++++++++++++++++++++++++++++++++++++------
 src/gui/RadioSetupDialog.cpp  | 52 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 100 insertions(+), 7 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)


🤖 aethersdr-agent · cost: $18.1449 · model: claude-opus-4-7

… stop the unconditional macOS Input Monitoring prompt (#3257). Principle III.

`MainWindow` started `UlanziDialBackend` and `HidEncoderManager` on every launch.
The Ulanzi macOS backend calls `IOHIDManagerOpen(..., kIOHIDOptionsTypeSeizeDevice)`
and HIDAPI's macOS backend opens with the same flag internally — both trip the
TCC Input Monitoring prompt on first launch even on machines with no supported
hardware. Existing reproduction: K5PTB's screenshot of AetherSDR appearing in
System Settings → Privacy & Security → Input Monitoring with no peripheral
ever connected.

Fix:
- New `UlanziDialEnabled` / `HidEncoderEnabled` AppSettings keys, default
  "False". `MainWindow` ctor gates the `start()` / `loadSettings()` calls
  on them.
- New "USB Control Surfaces" group at the top of the Serial tab in
  `RadioSetupDialog`, with one checkbox per backend. Flipping a checkbox
  on emits `serialSettingsChanged()`, which `MainWindow::
  wireRadioSetupDialogSignals` now handles by kicking `start()` /
  `loadSettings()` — so the TCC prompt fires the moment the user
  affirmatively asks for the feature, with context, instead of on every
  launch.
- `wireRadioSetupDialogSignals` `connect()` moved out of the
  `#ifdef HAVE_SERIALPORT` wrap so the Ulanzi/HID branch runs even in
  builds without serial-port support (the dial/encoder backends exist
  regardless).
- `NSInputMonitoringUsageDescription` added to `packaging/macos/
  Info.plist.in` so the prompt — when it does fire — carries an app-
  supplied rationale explaining it is only for optional USB control
  surfaces.

`start()` and `loadSettings()` are idempotent (each guards re-open), so
re-firing on unrelated settings changes is harmless.

Principle III is the load-bearing citation: the new user-facing labels
("Enable Ulanzi Dial", "Enable HID encoders / StreamDeck+ (RC-28,
PowerMate, ShuttleXpress, …)") match the visible peripheral names users
recognize, and the Info.plist rationale points to "Preferences → Serial"
— the exact tab name registered in `RadioSetupDialog.cpp:472`.

Blast radius: HidEncoderManager::loadSettings risk_score=0.306, 3 high-risk
affected (top: MainWindow::MainWindow, AppSettings::instance,
AppSettings::value); RadioSetupDialog edit risk_score=0.183, 3 high-risk
affected (top: MainWindow::MainWindow, MainWindow::wirePanadapter,
MainWindow::buildMenuBar). All callers are structural bridges to MainWindow's
ctor and the AppSettings singleton; edits are purely additive (new if-gate,
new connect branch, new QGroupBox), no symbol contracts change.

Verification:
- `cmake` is not installed on this host so no local build was possible —
  relying on CI for compile + link.
- No cross-TU symbol motion, no new project-header includes, no
  Q_LOGGING_CATEGORY changes — test-target link audit not applicable.
- Manual UX path to verify post-merge: fresh macOS install, build,
  launch — confirm no Input Monitoring prompt; open Radio Setup →
  Serial, tick "Enable Ulanzi Dial" — confirm prompt fires now with
  rationale text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Without this, the new HidEncoderEnabled=False default in this PR
silently turns off existing users' working HID encoders (RC-28,
PowerMate, ShuttleXpress, StreamDeck+) until they discover the
Radio Setup → Serial checkbox.  Linux + Windows hidraw paths have
no TCC concern, so honouring the prior intent on first launch is
the right default for them.

Mechanism: at MainWindow ctor, if HidEncoderEnabled isn't set at
all (first launch after upgrade), copy HidEncoderAutoDetect's
value into it.  Subsequent toggles in the dialog own the key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Pushed a 15-line migration fixup on top so existing HidEncoderAutoDetect=True users (RC-28 / PowerMate / ShuttleXpress / StreamDeck+ on Linux + Windows) don't silently lose their working controller after upgrade. The mechanism: at MainWindow ctor, if HidEncoderEnabled isn't set at all (first launch after this PR), it inherits HidEncoderAutoDetect's value. Subsequent toggles in Radio Setup → Serial own the key from then on.

The privacy fix in the original commit is unchanged — macOS Input Monitoring still only prompts when the user explicitly opts in.

73, Jeremy KK7GWY & Claude (AI dev partner)

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the privacy-first framing — NSInputMonitoringUsageDescription (matching kIOHIDOptionsTypeSeizeDevice), opt-in defaults, and re-firing start() / loadSettings() from the existing serialSettingsChanged channel are all the right moves. The conditional refactor in wireRadioSetupDialogSignals (moving connect() out of the HAVE_SERIALPORT guard so the lambda still reaches the Ulanzi/HID re-evaluation, while keeping the serial-port and setFlexControlConnectionStatus bodies guarded) reads correctly.

One issue worth fixing before merge, plus a couple of smaller notes.

HidEncoderEnabled migration is effectively always-off (regression)

if (!s.contains("HidEncoderEnabled")) {
    const bool hadAutodetect =
        s.value("HidEncoderAutoDetect", "False").toString() == "True";
    s.setValue("HidEncoderEnabled", hadAutodetect ? "True" : "False");
}

HidEncoderAutoDetect is never written anywhere in the tree — grep only finds two readers (HidEncoderManager.cpp:262 and MainWindow.cpp:7076), both with default "True". No UI writes it, and HidEncoderManager::loadSettings() reads it without ever calling setValue. So in practice the key is absent from AppSettings for essentially every existing user, the migration falls through to the "False" fallback, and every user who had a working RC-28 / PowerMate / Shuttle / StreamDeck+ on the prior build silently loses it on upgrade until they discover the new checkbox.

The stated intent ("Honour that prior intent on first launch") only fires if someone manually edited AppSettings. Two ways to fix:

  1. Match the actual observed default: s.value("HidEncoderAutoDetect", "True") — this preserves prior behavior for the autodetect-by-default world and is a one-character change.
  2. Or use a different evidence-of-prior-use signal: e.g. presence of HidEncoderInvertDir, or any key under a HID prefix, as proof the user has been here before.

Without one of these, the "migration" block is dead code and the PR is a silent regression for anyone currently using a HID encoder — which is a meaningfully larger user base than the Ulanzi case below.

Ulanzi has no migration (acceptable but worth a release-note callout)

There's no prior UlanziDialEnabled key to migrate from, so the same silent-breakage problem applies but is genuinely unavoidable. Probably fine, but worth flagging in release notes so existing Ulanzi users know to re-enable in Preferences → Serial. Consider also a one-shot "we changed this" toast on first launch if other privacy-gated features land later.

Minor: comment on serialSettingsChanged

The new comment ("the 'external-device settings changed' signal in practice") captures the de-facto semantics nicely. If you ever revisit this area, a rename to externalDeviceSettingsChanged would make it self-documenting — not for this PR.

Everything else (AppSettings usage, m_dialBackend / m_hidEncoder null guards, idempotent re-fire of start() / loadSettings(), the Info.plist copy with its "only after you enable" qualifier) looks good. Just the HID default-flip needs sorting and this is ready.


🤖 aethersdr-agent · cost: $8.9725 · model: claude-opus-4-7

@ten9876 ten9876 merged commit 394f50f into main May 29, 2026
7 of 8 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-3257 branch May 29, 2026 04:35
jensenpat pushed a commit that referenced this pull request Jun 9, 2026
…-enables on upgrade (#3493)

Two root causes for StreamDeck+ going dark after the v26.6 opt-in gate (#3258):

1. packaging/linux/60-hid-encoders.rules was missing the StreamDeck+ entry
   (0fd9:0084). Without it hidraw is root-only on Linux, so hid_open() fails
   silently and the device never initialises.

2. The v1 migration in MainWindow read HidEncoderAutoDetect with default "False",
   but the old code treated the key's *absence* as True (implicit default on
   line 8029). Users who never touched that setting had HID implicitly on, yet
   the migration wrote HidEncoderEnabled=False for them. V2 migration detects
   and corrects installs that were already written with the wrong default.

Squashed-from: #3493

Co-authored-by: Tony KX3H <276457091+motoham88@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: jensenpat <patjensen@gmail.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
… stop the unconditional macOS Input Monitoring prompt (aethersdr#3257). Principle III. (aethersdr#3258)

## Summary

Fixes aethersdr#3257

### What was changed

privacy(hid): gate Ulanzi Dial + StreamDeck+ behind opt-in toggles to
stop the unconditional macOS Input Monitoring prompt (aethersdr#3257). Principle
III.

### Files modified

- `packaging/macos/Info.plist.in`
- `src/gui/MainWindow.cpp`
- `src/gui/RadioSetupDialog.cpp`

```
 packaging/macos/Info.plist.in |  2 ++
 src/gui/MainWindow.cpp        | 53 +++++++++++++++++++++++++++++++++++++------
 src/gui/RadioSetupDialog.cpp  | 52 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 100 insertions(+), 7 deletions(-)
```

---
Generated by AetherClaude (automated agent for AetherSDR)

---
<sub>🤖 aethersdr-agent · cost: $18.1449 · model: claude-opus-4-7</sub>

---------

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…-enables on upgrade (aethersdr#3493)

Two root causes for StreamDeck+ going dark after the v26.6 opt-in gate (aethersdr#3258):

1. packaging/linux/60-hid-encoders.rules was missing the StreamDeck+ entry
   (0fd9:0084). Without it hidraw is root-only on Linux, so hid_open() fails
   silently and the device never initialises.

2. The v1 migration in MainWindow read HidEncoderAutoDetect with default "False",
   but the old code treated the key's *absence* as True (implicit default on
   line 8029). Users who never touched that setting had HID implicitly on, yet
   the migration wrote HidEncoderEnabled=False for them. V2 migration detects
   and corrects installs that were already written with the wrong default.

Squashed-from: aethersdr#3493

Co-authored-by: Tony KX3H <276457091+motoham88@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: jensenpat <patjensen@gmail.com>
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.

privacy(hid): UlanziDialMacOSManager unconditionally requests Input Monitoring permission on every macOS launch

1 participant