Permissions
How Extentos derives Android manifest permissions and iOS Info.plist keys automatically from your AppSpec — every block (capture_photo, capture_video, record_audio, speak_text), trigger (voice_command, wake_word, push_to_talk, location_updated, phone_notification_forwarded, incoming_call_detected), and stream (video_frames, audio_chunks, transcription_incremental) declares its own platform-permission requirements. Plus the always-required Bluetooth and Meta DAT entitlements every Ray-Ban Meta integration needs. Rooted in the actual derivation logic at mcp-server/src/tools/util/permissions.ts.
Permissions are derived, not hand-maintained. Every capability your AppSpec uses (capture_photo, voice_command, record_audio, etc.) declares its own Android manifest permissions and iOS Info.plist keys, and the MCP server's getPermissions tool returns the exact set for your current spec. The agent calls getPermissions, gets the structured output, and writes the manifest and Info.plist for you — no developer needs to memorize that voice_command triggers require NSSpeechRecognitionUsageDescription on iOS or that streams require FOREGROUND_SERVICE on Android. This page is the consolidated map: which capabilities require which keys, plus the always-required Bluetooth foundation and Meta DAT entitlements that every Ray-Ban Meta integration needs.
How permission derivation works
Three layers cooperate:
- The AppSpec declares what your app does — blocks, triggers, streams, actions.
- The derivation logic (in
mcp-server/src/tools/util/permissions.ts) maps each spec primitive to its required permissions per platform. getPermissionsreturns the full set, plus structured details (Android manifest entries, iOS plist key/value/reason tuples, foreground-service declarations, NotificationListenerService stubs).
The agent calls getPermissions({ spec, platform }) after every spec mutation. The MCP server returns:
{
"android": {
"permissions": ["android.permission.CAMERA", "android.permission.BLUETOOTH_CONNECT", "..."],
"manifestEntries": ["<uses-permission android:name=\"android.permission.CAMERA\" />", "..."],
"foregroundService": { "required": true, "types": ["camera"], "declaration": null },
"notificationListener": { "required": false, "declaration": null, "devInstructions": null },
"minimumSdk": 26,
"compileSdk": 35,
"targetSdk": 35
},
"ios": {
"plistKeys": [
{ "key": "NSCameraUsageDescription", "value": "...", "reason": "capture_photo / capture_video block ..." },
{ "key": "NSBluetoothAlwaysUsageDescription", "value": "...", "reason": "Required by Meta DAT SDK ..." }
]
},
"metaDat": {
"scopes": ["dat.scope.camera", "..."],
"registrationRequired": true,
"registrationSteps": ["..."]
},
"summary": "Android, 5 permissions (min SDK 26, target 35). Meta DAT registration required, 3 scopes."
}generateConnectionModule writes the manifest and Info.plist using this output during initial scaffolding. validateIntegration re-checks alignment after every spec change.
At runtime, your installed agent has this live. Once Extentos's MCP server is registered with your agent, the agent calls
getPermissions({ spec, platform })and gets the exact Android permissions, manifest entries, foreground-service declarations, and iOS Info.plist keys scoped to your current spec — no manual lookup needed. The static derivation tables below are the human-readable reference for pre-install evaluation, SEO, and out-of-context lookup; the livegetPermissionsresponse is authoritative when wiring a real project.
Android permissions
Always required
These are the foundation — every Extentos app on Android requests them, regardless of spec contents:
| Permission | Why |
|---|---|
android.permission.BLUETOOTH_CONNECT | Connect to paired Ray-Ban Meta over Bluetooth |
android.permission.BLUETOOTH_SCAN | Discover the paired device on the system bonded list |
android.permission.INTERNET | Backend communication for BrowserSimTransport and telemetry |
Derived from blocks
Each block kind in your spec adds its own permissions:
| Block kind | Android permission(s) | Note |
|---|---|---|
capture_photo | android.permission.CAMERA | |
capture_video | android.permission.CAMERA | + RECORD_AUDIO only if include_audio: true is set explicitly. Defaults to false to avoid over-declaring. |
record_audio | android.permission.RECORD_AUDIO | |
speak_text | (none) | TTS doesn't need a manifest permission — it's mediated through the platform's TextToSpeech engine |
Derived from triggers
| Trigger type | Android permission(s) | Notes |
|---|---|---|
manual_launch | (none) | App-driven entry point |
voice_command | android.permission.RECORD_AUDIO | Phone STT over Bluetooth audio |
wake_word | android.permission.RECORD_AUDIO | Same path as voice_command |
push_to_talk | android.permission.RECORD_AUDIO | Same |
capture_button | (none) | Hardware button press, no app-side permission |
tap / double_tap | (none) | Touchpad input — vendor-mediated |
location_updated | android.permission.ACCESS_FINE_LOCATION | Geofence-style triggers |
phone_notification_forwarded | android.permission.BIND_NOTIFICATION_LISTENER_SERVICE | System-managed; cannot be requested programmatically — see below |
incoming_call_detected | android.permission.READ_PHONE_STATE |
Derived from streams
Streams are continuous data flows. They add permissions and require a foreground service.
| Stream type | Android permission(s) |
|---|---|
video_frames | android.permission.CAMERA + FOREGROUND_SERVICE |
outgoing_video_stream | android.permission.CAMERA + FOREGROUND_SERVICE |
audio_chunks | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE |
transcription_incremental | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE |
outgoing_audio_stream | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE |
Any stream in your spec triggers FOREGROUND_SERVICE because streaming continues across app backgrounding — Android 14+ enforces foreground-service types per stream category (camera, microphone). getPermissions returns the required foregroundServiceType set in the response so your agent can configure the service correctly.
NotificationListenerService for phone notifications
If your spec includes a phone_notification_forwarded trigger, the library's ExtentosNotificationListenerService must be declared in your manifest:
<service
android:name="com.extentos.glasses.core.notifications.ExtentosNotificationListenerService"
android:label="Notification Mirroring"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>getPermissions returns this declaration verbatim under notificationListener.declaration when it's needed. Critically, BIND_NOTIFICATION_LISTENER_SERVICE is a system-managed permission — your app cannot request it via the runtime permission API. The user must enable it manually:
Settings → Notifications → Notification access → toggle your app on
Use NotificationAccessHelper.openSettings(context) from the library to deep-link the user to the right screen.
SDK version requirements
| Field | Default | Source |
|---|---|---|
minimumSdk | 26 (Android 8.0) | VERSION_INFO.android.minimumSdk |
compileSdk | 35 | VERSION_INFO.android.compileSdk |
targetSdk | 35 | VERSION_INFO.android.targetSdk |
These come from a single source of truth (mcp-server/src/tools/data/version.ts) so library bumps don't drift. Don't hardcode them in your manifest — use getPermissions.
iOS Info.plist keys
Always required
| Key | Privacy string default | Why |
|---|---|---|
NSBluetoothAlwaysUsageDescription | "Used to connect to your Meta Ray-Ban glasses." | Required by Meta DAT SDK for all glasses connections |
Derived from blocks
| Block kind | iOS plist key | Privacy string |
|---|---|---|
capture_photo / capture_video | NSCameraUsageDescription | "Used to capture photos and video from your glasses." |
record_audio | NSMicrophoneUsageDescription | "Used to capture audio from your glasses for voice commands." |
capture_video with include_audio: true | also NSMicrophoneUsageDescription | (same as record_audio) |
speak_text | (none) | TTS via AVSpeechSynthesizer — no entitlement needed |
Derived from triggers
| Trigger type | iOS plist key(s) | Notes |
|---|---|---|
manual_launch | (none) | |
voice_command / wake_word / push_to_talk | NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription | Phone-side recognition via SFSpeechRecognizer |
capture_button / tap / double_tap | (none) | Hardware-mediated |
location_updated | NSLocationWhenInUseUsageDescription |
Derived from streams
| Stream type | iOS plist key(s) |
|---|---|
video_frames / outgoing_video_stream | NSCameraUsageDescription |
audio_chunks / transcription_incremental / outgoing_audio_stream | NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription |
Privacy strings
getPermissions returns each plist key with three fields: key, value (the default privacy string), reason (which spec primitive triggered it). Defaults from mcp-server/src/tools/util/permissions.ts:
| Key | Default privacy string |
|---|---|
NSCameraUsageDescription | "Used to capture photos and video from your glasses." |
NSMicrophoneUsageDescription | "Used to capture audio from your glasses for voice commands." |
NSSpeechRecognitionUsageDescription | "Used to transcribe voice commands from your glasses." |
NSLocationWhenInUseUsageDescription | "Used to deliver location-aware glasses experiences." |
NSBluetoothAlwaysUsageDescription | "Used to connect to your Meta Ray-Ban glasses." |
You can override these strings in your Info.plist — the App Store review process strongly prefers app-specific reasons over generic ones.
Meta DAT-specific iOS Info.plist keys
These are not derived from the spec — they're required by every Meta Ray-Ban integration regardless of capabilities used. generateConnectionModule writes them once during initial scaffolding.
MWDAT dictionary
<key>MWDAT</key>
<dict>
<key>MetaAppID</key><string>YOUR_META_APP_ID</string>
<key>ClientToken</key><string>YOUR_CLIENT_TOKEN</string>
<key>TeamID</key><string>YOUR_APPLE_TEAM_ID</string>
<key>AppLinkURLScheme</key><string>yourapp</string>
</dict>AppLinkURLScheme is a custom URL scheme (e.g. yourapp://), not a universal link. Meta DAT uses it for the registration callback from the Meta companion app.
CFBundleURLTypes
Must include the same scheme:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>yourapp</string></array>
</dict>
</array>LSApplicationQueriesSchemes
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fb-viewapp</string>
</array>Required to query whether the Meta AI / Meta View companion app is installed.
UISupportedExternalAccessoryProtocols
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.meta.ar.wearable</string>
</array>Declares Meta's external-accessory protocol — required for the BLE link.
UIBackgroundModes
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>Required to maintain the glasses connection across app backgrounding.
Worked example — a voice-trigger photo-capture app
An AppSpec with one voice_command trigger that captures a photo and speaks the result:
Spec primitives used:
- trigger: voice_command
- block: capture_photo
- block: speak_text
- action: ai_call (vision handler)getPermissions({ spec, platform: "android" }) returns:
| Permission | Source |
|---|---|
android.permission.BLUETOOTH_CONNECT | always |
android.permission.BLUETOOTH_SCAN | always |
android.permission.INTERNET | always |
android.permission.CAMERA | capture_photo block |
android.permission.RECORD_AUDIO | voice_command trigger |
→ 5 permissions. foregroundService.required: false (no streams). notificationListener.required: false.
getPermissions({ spec, platform: "ios" }) returns these plist keys:
| Key | Source |
|---|---|
NSBluetoothAlwaysUsageDescription | always |
NSCameraUsageDescription | capture_photo block |
NSMicrophoneUsageDescription | voice_command trigger |
NSSpeechRecognitionUsageDescription | voice_command trigger |
→ 4 plist keys. Plus the always-required Meta DAT-specific keys (MWDAT dict, CFBundleURLTypes, LSApplicationQueriesSchemes, UISupportedExternalAccessoryProtocols, UIBackgroundModes).
Runtime permission requests vs manifest declarations
Declaring permissions in the manifest / Info.plist isn't the same as having them granted at runtime:
- Android:
CAMERA,RECORD_AUDIO,ACCESS_FINE_LOCATION,READ_PHONE_STATE, andBLUETOOTH_CONNECT(on Android 12+) are dangerous permissions — declared in the manifest, but also requested viaActivityCompat.requestPermissionsat runtime. The library'sExtentosConnectionPagehandles the runtime permission flow during pairing. - iOS: plist keys declare the permission and provide the privacy string the OS shows to the user. The OS prompts on first use (e.g., the first time the camera is accessed). The library handles this; you don't need a separate request flow.
BIND_NOTIFICATION_LISTENER_SERVICEon Android is special: declared in the manifest, but cannot be requested programmatically. The user must toggle it on manually in system Settings. UseNotificationAccessHelper.openSettings(context)to deep-link them there.
Common gotchas
Android 12 (API 31) Bluetooth split
Pre-Android 12, BLUETOOTH and BLUETOOTH_ADMIN were normal permissions auto-granted at install. Android 12 split them into runtime-requested BLUETOOTH_CONNECT and BLUETOOTH_SCAN. Extentos targets API 26+ but always uses the new permissions; the library handles the user prompt during pairing.
iOS NSBluetoothAlwaysUsageDescription even if your app doesn't expose Bluetooth in the UI
Meta DAT's BLE connection requires this key. Even if your app appears purely visual to the user, the absence of this key causes the DAT registration flow to crash silently on iOS 13+.
include_audio on capture_video
Adding RECORD_AUDIO (Android) / NSMicrophoneUsageDescription (iOS) for every video capture would force microphone permission on apps that only record silent video. The default is include_audio: false — only set include_audio: true if your app actually needs the audio track.
Foreground service types on Android 14+
Android 14 enforces foregroundServiceType="camera" or ="microphone" per stream. getPermissions returns the right type set; if you write the manifest by hand, make sure the service declaration includes the matching type attribute.
Frequently asked questions
Why does Extentos derive permissions instead of letting me write them?
Three reasons. (1) Permissions drift the moment a spec changes — adding a voice_command trigger requires NSSpeechRecognitionUsageDescription, and forgetting to add it manifests as a confusing runtime crash. (2) Platform requirements evolve (Android 12 BT split, Android 14 foreground-service types) — centralized derivation lets the library track them. (3) AI agents can't reliably memorize platform permission rules; getPermissions is a deterministic primitive they can call.
Can I add permissions Extentos doesn't derive?
Yes. The getPermissions output is the minimum set Extentos needs for the spec. Your app can add more permissions for features outside the spec (analytics, push notifications, etc.) by editing the manifest / Info.plist directly. validateIntegration flags missing required permissions but doesn't complain about extras.
What's the difference between BLUETOOTH_CONNECT and BLUETOOTH_SCAN on Android?
BLUETOOTH_CONNECT is for connecting to an already-paired device. BLUETOOTH_SCAN is for discovering nearby devices. Extentos requests both because the pairing flow scans for the glasses on first run, then connects to the bonded device on subsequent runs.
Why is NSBluetoothAlwaysUsageDescription required and not NSBluetoothPeripheralUsageDescription?
Apple deprecated NSBluetoothPeripheralUsageDescription in iOS 13 in favor of NSBluetoothAlwaysUsageDescription. Meta DAT v0.6+ requires the modern key.
Does my app need to declare every key Meta's documentation lists?
Yes — the Meta DAT-specific keys (MWDAT dict, LSApplicationQueriesSchemes, UISupportedExternalAccessoryProtocols, UIBackgroundModes) are mandatory regardless of which capabilities your spec uses. They're the foundation of the connection itself, not a per-capability requirement.
What happens if I miss a permission?
- Android dangerous permissions (CAMERA, RECORD_AUDIO, etc.) — the runtime request fails silently; the library logs a
permission.deniedevent into the structured event log; the affected capability fails withTransportError.PermissionDenied. - iOS plist keys — the OS terminates your app the moment it tries to access the gated API. There's no graceful fallback — fix the plist.
BIND_NOTIFICATION_LISTENER_SERVICE— declared but not granted meansphone_notification_forwardedtriggers never fire. The library logs the gap; the user has to enable it in system Settings.
validateIntegration checks alignment between your spec and your manifest / Info.plist, and flags missing keys before testing.
Related concepts
- Architecture — how the AppSpec, library, and platform fit together; permissions are part of the boundary
- Capabilities — the vocabulary that drives permission derivation
- Vendors: Meta Ray-Ban — the Meta DAT-specific Info.plist keys, registration flow, audio architecture
getPermissionstool — the MCP tool the agent calls to retrieve the current permission set- Quickstart with an AI agent — how the agent wires permissions during initial scaffolding
Sessions
Lifecycle of an Extentos session — connect, run, disconnect, error states, and reconnection behavior.
Transport vs app simulation
Meta's Mock Device Kit simulates the transport layer — BLE, framing, codec, the SDK plumbing. Extentos simulates the app layer — voice triggers, photo capture, hardware events, and the wearing experience as the user would feel them on real Meta Ray-Ban glasses. Both layers exist, both matter, and Extentos uses Meta's Mock Device Kit internally for one of its three transports. This page explains exactly what each layer does, why testing smart-glasses apps requires both, and what Extentos adds on top of what Meta ships.