Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@
<string>OpenHuman uses the camera for video calls and huddles inside embedded apps (Google Meet, Discord, Slack).</string>
<key>NSAppleEventsUsageDescription</key>
<string>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 &gt; Privacy &amp; Security &gt; Automation.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>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).</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>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.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>OpenHuman accesses the Documents folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram).</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>OpenHuman accesses the Downloads folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram).</string>
<key>NSDesktopFolderUsageDescription</key>
<string>OpenHuman accesses the Desktop folder only when you pick or save a file there from inside an embedded app (Slack, Discord, Telegram).</string>
<key>NSContactsUsageDescription</key>
<string>OpenHuman accesses Contacts only when an embedded app (Slack, Google Meet) explicitly asks to import or invite people from your address book.</string>
<key>NSCalendarsUsageDescription</key>
<string>OpenHuman accesses your calendar only when an embedded app (Google Meet) asks to read or create events on your behalf.</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
9 changes: 9 additions & 0 deletions app/src-tauri/entitlements.sidecar.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,14 @@
consent dialog. -->
<key>com.apple.security.automation.apple-events</key>
<true/>
<!-- Required so embedded Chromium can enumerate Bluetooth audio devices
(AirPods, headsets) inside getUserMedia() / enumerateDevices() calls
on macOS 11+. The companion `NSBluetoothAlwaysUsageDescription` in
`Info.plist` supplies the user-facing text shown in the consent
dialog; the entitlement is belt-and-braces under the Hardened
Runtime — harmless when non-sandboxed, future-safe if Apple
tightens the sandbox. Tracked in #1288. -->
<key>com.apple.security.device.bluetooth</key>
<true/>
</dict>
</plist>
66 changes: 66 additions & 0 deletions app/test/info-plist-required-keys.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
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 <plist><dict>');
}

const map = new Map<string, string>();
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);
});
});
3 changes: 3 additions & 0 deletions docs/RELEASE-MANUAL-SMOKE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> folder` the first time per folder; deny + retry must not crash.

### Windows

Expand Down
Loading