From a4d01eeb84af4c082961cdb384a36902e5417a19 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 17:21:58 +0530 Subject: [PATCH 1/5] fix(macos/plist): add NSBluetoothAlwaysUsageDescription to prevent Gmeet sign-in crash (#1288) Embedded Chromium probes CoreBluetooth via WebAuthn caBLE / hybrid transport when Google sign-in offers cross-device passkey authentication (phone-as- authenticator). macOS TCC enforces hard SIGABRT (not graceful denial) when a non-sandboxed Hardened Runtime app accesses a privacy-sensitive resource without the matching usage-description key. The same Bluetooth probe is also reachable via getUserMedia() / enumerateDevices() audio peripheral listing on later macOS versions, so the description covers both surfaces. Adding NSBluetoothAlwaysUsageDescription so the first probe prompts the user instead of crashing the production app. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src-tauri/Info.plist b/app/src-tauri/Info.plist index f212e7d4fec..35065714ac6 100644 --- a/app/src-tauri/Info.plist +++ b/app/src-tauri/Info.plist @@ -8,6 +8,8 @@ OpenHuman uses the camera for video calls and huddles inside embedded apps (Google Meet, Discord, Slack). NSAppleEventsUsageDescription OpenHuman uses Apple Events to read the focused text field for inline autocomplete suggestions and to detect the active app for context-aware features. You can manage this in System Settings > Privacy & Security > Automation. + NSBluetoothAlwaysUsageDescription + OpenHuman uses Bluetooth for cross-device passkey sign-in (for example, when signing in to Google with your phone as an authenticator) and to detect connected audio devices (AirPods, headsets) in voice and video calls inside embedded apps (Google Meet, Discord, Slack). CFBundleURLTypes From 8de9aff4493c4c6fe450cda7bf3e034219b05879 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 17:22:11 +0530 Subject: [PATCH 2/5] feat(macos/plist): add Tier 2 privacy descriptions for embedded webview safety (#1288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit revealed six sibling privacy keys exposed to the same SIGABRT class as NSBluetoothAlwaysUsageDescription: any embedded webview probe (Gmeet/Slack/ Discord/Telegram) into Location, Documents/Downloads/Desktop folders, Contacts, or Calendars on macOS 11+ would crash a non-sandboxed Hardened Runtime app without the matching usage-description key. macOS only renders the system prompt when the resource is actually probed, so adding the keys does NOT pre-emptively annoy users — the prompt path exists only when a webview triggers it. Keys added: - NSLocationWhenInUseUsageDescription - NSDocumentsFolderUsageDescription - NSDownloadsFolderUsageDescription - NSDesktopFolderUsageDescription - NSContactsUsageDescription - NSCalendarsUsageDescription Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/Info.plist | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src-tauri/Info.plist b/app/src-tauri/Info.plist index 35065714ac6..e70c0aed7f1 100644 --- a/app/src-tauri/Info.plist +++ b/app/src-tauri/Info.plist @@ -10,6 +10,18 @@ OpenHuman uses Apple Events to read the focused text field for inline autocomplete suggestions and to detect the active app for context-aware features. You can manage this in System Settings > Privacy & Security > Automation. NSBluetoothAlwaysUsageDescription OpenHuman uses Bluetooth for cross-device passkey sign-in (for example, when signing in to Google with your phone as an authenticator) and to detect connected audio devices (AirPods, headsets) in voice and video calls inside embedded apps (Google Meet, Discord, Slack). + NSLocationWhenInUseUsageDescription + OpenHuman shares your approximate location only when an embedded app (Google Meet, Discord) requests it — for example, to suggest nearby meeting rooms or set a default region. You can deny this without affecting other features. + NSDocumentsFolderUsageDescription + OpenHuman accesses the Documents folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram). + NSDownloadsFolderUsageDescription + OpenHuman accesses the Downloads folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram). + NSDesktopFolderUsageDescription + OpenHuman accesses the Desktop folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram). + NSContactsUsageDescription + OpenHuman accesses Contacts only when an embedded app (Slack, Google Meet) explicitly asks to import or invite people from your address book. + NSCalendarsUsageDescription + OpenHuman accesses your calendar only when an embedded app (Google Meet) asks to read or create events on your behalf. CFBundleURLTypes From e10031a334c5e13c3258b615f6a79f71043c672d Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 17:22:16 +0530 Subject: [PATCH 3/5] chore(macos/entitlements): add bluetooth device entitlement under Hardened Runtime (#1288) Belt-and-braces companion to NSBluetoothAlwaysUsageDescription in Info.plist. Harmless under non-sandboxed Hardened Runtime (the current bundle config), future-safe if Apple tightens the sandbox model in a later macOS release. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/entitlements.sidecar.plist | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src-tauri/entitlements.sidecar.plist b/app/src-tauri/entitlements.sidecar.plist index c49111c8c73..cdad97f19fe 100644 --- a/app/src-tauri/entitlements.sidecar.plist +++ b/app/src-tauri/entitlements.sidecar.plist @@ -32,5 +32,14 @@ consent dialog. --> com.apple.security.automation.apple-events + + com.apple.security.device.bluetooth + From 1e95a9bb8363129a7646bef19b8e87f1b65eba93 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 17:22:23 +0530 Subject: [PATCH 4/5] test(plist): assert required macOS privacy keys present (#1288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vitest unit test reads `app/src-tauri/Info.plist`, parses XML via DOMParser, asserts all 10 required NS*UsageDescription keys exist with non-empty descriptions ≥ 30 chars (cheap placeholder check). Catches accidental removal of any privacy key in future PRs — would have caught #1288 had it been in place pre-incident. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/test/info-plist-required-keys.test.ts | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/test/info-plist-required-keys.test.ts diff --git a/app/test/info-plist-required-keys.test.ts b/app/test/info-plist-required-keys.test.ts new file mode 100644 index 00000000000..d424f9489e4 --- /dev/null +++ b/app/test/info-plist-required-keys.test.ts @@ -0,0 +1,66 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const INFO_PLIST_PATH = path.resolve(HERE, '..', 'src-tauri', 'Info.plist'); + +const REQUIRED_PRIVACY_KEYS = [ + 'NSMicrophoneUsageDescription', + 'NSCameraUsageDescription', + 'NSAppleEventsUsageDescription', + 'NSBluetoothAlwaysUsageDescription', + 'NSLocationWhenInUseUsageDescription', + 'NSDocumentsFolderUsageDescription', + 'NSDownloadsFolderUsageDescription', + 'NSDesktopFolderUsageDescription', + 'NSContactsUsageDescription', + 'NSCalendarsUsageDescription', +] as const; + +const MIN_DESCRIPTION_LEN = 30; + +function parsePlistKeyValuePairs(xml: string): Map { + const doc = new DOMParser().parseFromString(xml, 'application/xml'); + const parserError = doc.querySelector('parsererror'); + if (parserError) { + throw new Error(`Info.plist XML parse failed: ${parserError.textContent ?? 'unknown'}`); + } + + const root = doc.querySelector('plist > dict'); + if (!root) { + throw new Error('Info.plist missing top-level '); + } + + const map = new Map(); + const children = Array.from(root.children); + for (let i = 0; i < children.length; i++) { + const node = children[i]; + if (node.tagName !== 'key') continue; + const keyName = (node.textContent ?? '').trim(); + const valueNode = children[i + 1]; + if (!valueNode || valueNode.tagName !== 'string') continue; + map.set(keyName, (valueNode.textContent ?? '').trim()); + } + return map; +} + +describe('app/src-tauri/Info.plist macOS privacy keys', () => { + const xml = readFileSync(INFO_PLIST_PATH, 'utf8'); + const pairs = parsePlistKeyValuePairs(xml); + + it.each(REQUIRED_PRIVACY_KEYS)('declares %s with non-empty user-facing copy', key => { + const value = pairs.get(key); + expect(value, `Missing required privacy key '${key}' in Info.plist`).toBeDefined(); + expect( + (value ?? '').length, + `Privacy key '${key}' description must be ≥ ${MIN_DESCRIPTION_LEN} chars (placeholder check)` + ).toBeGreaterThanOrEqual(MIN_DESCRIPTION_LEN); + }); + + it('has the same key count as REQUIRED_PRIVACY_KEYS or strictly more', () => { + const declared = REQUIRED_PRIVACY_KEYS.filter(k => pairs.has(k)).length; + expect(declared).toBe(REQUIRED_PRIVACY_KEYS.length); + }); +}); From c1da0c78696183202eb67181b642903709c80019 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 17:22:28 +0530 Subject: [PATCH 5/5] docs(release): extend macOS smoke checklist with privacy prompts (#1288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new line items under the macOS section covering the privacy prompts gated on the keys added in #1288: - Bluetooth prompt on first Gmeet sign-in or audio device enumeration (regression watch — hard fail mode is SIGABRT before prompt renders). - Location no-crash invariant on Gmeet room-finder probe. - File picker no-crash invariant for Documents/Downloads/Desktop. These line items make the issue's "Regression safety" acceptance criterion concrete in the release-cut checklist. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/RELEASE-MANUAL-SMOKE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/RELEASE-MANUAL-SMOKE.md b/docs/RELEASE-MANUAL-SMOKE.md index fcce367075d..d45fdbcc2d9 100644 --- a/docs/RELEASE-MANUAL-SMOKE.md +++ b/docs/RELEASE-MANUAL-SMOKE.md @@ -29,6 +29,9 @@ Applies to every release, all platforms. - [ ] **Input Monitoring prompt fires on first hotkey use** — Press the registered global hotkey for the first time. Expected: `Input Monitoring` prompt; granting it makes the hotkey trigger; denying it does not crash the app. - [ ] **Screen Recording prompt fires on first screen-share** — Use the screen-share skill or `getDisplayMedia` shim. Expected: `Screen Recording` prompt; granted → picker shows windows + screens; denied → in-app message explaining the requirement. - [ ] **Microphone prompt fires on first voice capture** — Start a voice session. Expected: standard mic prompt; granted → capture begins; denied → fallback message, no panic. +- [ ] **Bluetooth prompt fires on first Gmeet call (regression watch — see #1288)** — Open the Google Meet webview account and join a meeting from a fresh install. Expected: macOS prompts `OpenHuman would like to use Bluetooth` the first time the device picker enumerates audio peripherals; granted → AirPods/headsets appear in the picker; denied → fallback to built-in mic, no crash. Hard fail mode (key absent) is a SIGABRT before the prompt can render. +- [ ] **Location prompt does not crash on Gmeet room-finder probe** — If Gmeet surfaces nearby-room suggestions, the first probe should trigger `OpenHuman would like to use your current location`; granting or denying must NOT crash the app. (Probe path is webview-driven; only verify the no-crash invariant here.) +- [ ] **File picker does not crash on Documents/Downloads/Desktop selections** — From an embedded app (Slack, Discord, Telegram), trigger a file upload and pick a file from `Documents`, `Downloads`, and `Desktop` in turn. Expected: macOS prompts `OpenHuman would like to access files in your folder` the first time per folder; deny + retry must not crash. ### Windows