Permissions
Extentos derives Android permissions and iOS Info.plist keys from the capabilities your handler uses, plus the always-required Bluetooth and Meta DAT keys.
Permissions are derived, not hand-maintained. Every SDK capability your handler subscribes to (capture_photo, transcription_incremental, 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 declared capability list. The agent calls getPermissions, gets the structured output, and writes the manifest and Info.plist for you — no developer needs to memorize that transcription_incremental requires NSSpeechRecognitionUsageDescription on iOS or that continuous-capture flows 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:
extentos.manifest.json'scapabilitiesarray declares what SDK primitives your handler uses —capture_photo,transcription_incremental,speak,video_frames, etc.- The derivation logic (in
mcp-server/src/tools/util/permissions.ts) maps each capability name 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({ capabilities, platform }) whenever the declared capability list changes. 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": "<service android:name=\".CameraForegroundService\" … />", "devInstructions": "subclass GlassesForegroundService …" },
"notificationListener": { "required": false, "declaration": null, "devInstructions": null },
"minimumSdk": 31,
"compileSdk": 35,
"targetSdk": 34
},
"ios": {
"plistKeys": [
{ "key": "NSCameraUsageDescription", "value": "...", "reason": "capture_photo / capture_video block ..." },
{ "key": "NSBluetoothAlwaysUsageDescription", "value": "...", "reason": "Required by Meta DAT SDK ..." }
]
},
"metaDat": {
"scopes": ["glasses.connection", "glasses.camera.photo", "..."],
"registrationRequired": true,
"registrationSteps": ["..."]
},
"summary": "Android, 5 permissions (min SDK 31, target 34). 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 change to your declared capabilities.
At runtime, your installed agent has this live. Once Extentos's MCP server is registered with your agent, the agent calls
getPermissions({ capabilities, platform })and gets the exact Android permissions, manifest entries, foreground-service declarations, and iOS Info.plist keys scoped to your declared capabilities — 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 which capabilities you declare:
| Permission | Why |
|---|---|
android.permission.BLUETOOTH_CONNECT | Connect to paired vendor glasses over Bluetooth. com.extentos:glasses declares it in its own manifest for every app — you don't add it. A voice-only app on the system-audio transport never exercises it (the OS routes the audio). A camera or display app must have it granted before ExtentosGlasses.create(...): the vendor arm of TransportChoice.Auto reads the bonded-device list, which this permission gates. Once the Meta transport is running the SDK requests it at runtime through your activityProvider, but that is too late to influence the transport already chosen. |
android.permission.BLUETOOTH_SCAN | Discover the paired device on the system bonded list. Declared by the library. |
android.permission.INTERNET | The managed gateway, the browser simulator, and telemetry. You declare this one — unlike the Bluetooth pair above, it is not in the library's manifest. An app that runs entirely on local models and sends no telemetry can omit it after the weights are on the device — the download itself is a network call, so an app that ships without INTERNET can never fetch a model in the first place. |
Derived from camera primitives
| Capability | Android permission(s) | Note |
|---|---|---|
capture_photo | android.permission.CAMERA | |
capture_video | android.permission.CAMERA | + RECORD_AUDIO only if your handler's VideoConfig requests audio. |
video_frames | android.permission.CAMERA + FOREGROUND_SERVICE + FOREGROUND_SERVICE_CAMERA | Continuous stream; survives backgrounding only with a foreground service. FOREGROUND_SERVICE_CAMERA is required on Android 14+. |
Derived from audio primitives
| Capability | Android permission(s) | Notes |
|---|---|---|
record_audio | android.permission.RECORD_AUDIO | One-shot capture |
audio_chunks | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE + FOREGROUND_SERVICE_MICROPHONE | Continuous raw audio stream. FOREGROUND_SERVICE_MICROPHONE is required on Android 14+. |
transcription_incremental | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE + FOREGROUND_SERVICE_MICROPHONE | Continuous STT stream. FOREGROUND_SERVICE_MICROPHONE is required on Android 14+. |
assistant_runtime | android.permission.RECORD_AUDIO + FOREGROUND_SERVICE + FOREGROUND_SERVICE_MICROPHONE | The Phase-4 assistant holds the mic for the life of a session — same set as continuous transcription. |
speak | (none) | TTS doesn't need a manifest permission — mediated through the platform's TextToSpeech engine |
Derived from hardware events
Hardware events your handler subscribes to via glasses.runtime.events and pattern-matches the variants from:
| Event | Android permission(s) | Notes |
|---|---|---|
thermal_warning, hinges_closed, audio_route_changed, app_lifecycle_changed, connection_state_changed | (none) | Platform / library mediated, no app-side permission |
location_updated | android.permission.ACCESS_FINE_LOCATION | Geofence-style flows |
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 |
Foreground-service requirement for continuous capture
Microphone-type and camera-type services differ, and getPermissions reflects that. The library ships GlassesForegroundService with foregroundServiceType="microphone" and declares it in its own manifest — for a mic-only footprint you don't declare anything, you just call GlassesForegroundService.start(context). A camera-type service is yours: subclass GlassesForegroundService (or write your own) and declare it, because the library can't declare a camera service on behalf of apps that don't use the camera. That's why getPermissions returns a declaration stub for camera footprints and none for mic-only ones.
Capabilities that capture continuously (video_frames, audio_chunks, transcription_incremental) require FOREGROUND_SERVICE because the capture continues across app backgrounding — Android 14+ enforces foreground-service types per category (camera, microphone). getPermissions returns the required foregroundServiceType set in the response so your agent can configure the service correctly. See searchDocs(topic: 'concurrency_modes') for the background-survival model.
NotificationListenerService for phone notifications
If your handler subscribes to phone_notification_forwarded events, 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 | 31 (Android 12) | VERSION_INFO.android.minimumSdk |
compileSdk | 35 | VERSION_INFO.android.compileSdk |
targetSdk | 34 | 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. minSdk and compileSdk are real floors — take them from getPermissions rather than hardcoding a guess. targetSdk is not a requirement: the library declares none, so the 34 above is the value this advice was derived against, not a value to copy into your build. Yours is your own choice — see install.
iOS Info.plist keys
Always required
| Key | Privacy string default | Why |
|---|---|---|
NSBluetoothAlwaysUsageDescription | "Required to connect to your glasses over Bluetooth." | Required by Meta DAT SDK for all glasses connections |
Derived from camera primitives
| Capability | iOS plist key | Privacy string |
|---|---|---|
capture_photo / capture_video | NSCameraUsageDescription | "Used to capture photos and video from your glasses." |
video_frames | NSCameraUsageDescription | (same) |
capture_video requesting audio | also NSMicrophoneUsageDescription | (same as record_audio) |
Derived from audio primitives
| Capability | iOS plist key(s) | Notes |
|---|---|---|
record_audio | NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription | One-shot silence-VAD capture; returns raw audio (no auto-STT — the speech key is derived for the whole audio-capture family) |
audio_chunks | NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription | Continuous raw audio (the speech key is derived for the whole audio-capture family) |
transcription_incremental | NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription | Phone-side recognition via SFSpeechRecognizer |
assistant_runtime | NSMicrophoneUsageDescription, NSSpeechRecognitionUsageDescription | The Phase-4 assistant. Add UIBackgroundModes: ["audio"] if it must keep listening while backgrounded. |
speak | (none) | TTS via AVSpeechSynthesizer — no entitlement needed |
Derived from hardware events
| Event | iOS plist key(s) | Notes |
|---|---|---|
location_updated | NSLocationWhenInUseUsageDescription | Geofence-style flows |
| All others (thermal, hinges, audio-route, lifecycle, call, notifications) | (none) | Platform / library mediated |
Privacy strings
getPermissions returns each plist key with three fields: key, value (the default privacy string), reason (which SDK capability 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 | "Required to connect to your glasses over Bluetooth." |
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 your declared capabilities — they're required by every app that uses a vendor transport, regardless of which camera/display capabilities it declares. generateConnectionModule writes them once during initial scaffolding.
A voice-only app needs none of them: it has no Meta DAT dependency at all, and its microphone and speaker are reached through the phone's own Bluetooth audio routing. Skip this whole section if you're on the voice path.
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>
<key>DAMEnabled</key><false/>
</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.
Set DAMEnabled explicitly — leaving it out breaks camera and audio apps on
real hardware. Meta's default when the key is missing is true, and DAM
runs an on-glasses readiness probe that reaps the device session about ten
seconds after it starts on any model without the display web-app runtime —
which is every current non-display model. Set it true only if you use the
display; false otherwise. generateConnectionModule emits the right value
from your declared capabilities. Full detail: Info.plist reference.
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>Those three are the vendor-transport set — a camera or display app needs them. A voice-only app needs none of them, but if the assistant must keep running while the app is backgrounded it needs audio instead:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>Add audio to the array as well if your app is both.
Required to maintain the glasses connection across app backgrounding.
Worked example — a wake-phrase photo-capture handler
A handler that subscribes to audio.transcriptions() for the wake phrase, calls camera.capturePhoto(), forwards to a vision LLM, and audio.speak()s the result. Capability declarations in the manifest:
manifest.capabilities = [
"transcription_incremental", // subscribe to audio.transcriptions()
"capture_photo", // camera.capturePhoto()
"speak" // audio.speak()
]getPermissions({ capabilities, platform: "android" }) returns:
| Permission | Source |
|---|---|
android.permission.BLUETOOTH_CONNECT | always |
android.permission.BLUETOOTH_SCAN | always |
android.permission.INTERNET | always |
android.permission.CAMERA | capture_photo |
android.permission.RECORD_AUDIO | transcription_incremental |
android.permission.FOREGROUND_SERVICE | transcription_incremental (continuous) |
android.permission.FOREGROUND_SERVICE_MICROPHONE | transcription_incremental (Android 14+) |
→ 7 permissions. foregroundService.required: true (continuous transcription). notificationListener.required: false.
getPermissions({ capabilities, platform: "ios" }) returns these plist keys:
| Key | Source |
|---|---|
NSBluetoothAlwaysUsageDescription | always |
NSCameraUsageDescription | capture_photo |
NSMicrophoneUsageDescription | transcription_incremental |
NSSpeechRecognitionUsageDescription | transcription_incremental |
→ 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 flow during pairing, and once a vendor transport is running the SDK requestsBLUETOOTH_CONNECTandCAMERAitself through youractivityProvider. Neither covers an app with no connection page — a voice app owns itsRECORD_AUDIOrequest outright, and a camera app must grantBLUETOOTH_CONNECTbeforeExtentosGlasses.create(...)for the vendor arm to resolve at all. See the voice-assistant bootstrap. - 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 31+ — the new permissions are required (no legacy fallback); 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+.
Audio on capture_video
VideoConfig lets you request an audio track on the captured video; if you do, RECORD_AUDIO (Android) / NSMicrophoneUsageDescription (iOS) is required too. The default is video-only — only request audio if your handler actually needs it.
Foreground service types on Android 14+
Android 14 enforces foregroundServiceType="camera" or ="microphone" per continuous-capture flow. 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 handler changes which capability it subscribes to — adding transcription_incremental 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 declared capabilities. Your app can add more permissions for features outside the SDK (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?
It depends on the path. The Meta DAT-specific keys (MWDAT dict, LSApplicationQueriesSchemes, UISupportedExternalAccessoryProtocols, UIBackgroundModes) are mandatory for any app that uses a vendor transport — that is, one declaring camera or display. A voice-only app has no DAT dependency at all and needs none of them; its microphone and speaker are reached through the phone's own Bluetooth audio routing.
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's result is a typed failure. On the capture path that failure isCaptureError.PlatformError, notPermissionDenied—CaptureError.PermissionDeniedis declared but not currently emitted, so don't hang your "ask for the camera again" recovery on it (see photo capture fails). Audio calls do returnAudioError.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_forwardedevents never reach your handler. The library logs the gap; the user has to enable it in system Settings.
validateIntegration checks alignment between your declared capabilities and your manifest / Info.plist, and flags missing keys before testing.
Related concepts
- Architecture — how the SDK, 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
Related
Architecture
How Extentos fits together — AI agent, MCP server, native Kotlin/Swift SDK, four transports (system audio, Meta DAT, browser sim, local in-memory sim), and the backend.
Capabilities
The Extentos capability vocabulary — the vendor-agnostic SDK primitives (audio, camera, voice, assistant, display, hardware events) your handler subscribes to.
Meta smart glasses (Meta DAT)
Meta smart glasses developer guide: Wearables Device Access Toolkit (DAT 0.8.0) capabilities, supported models (Ray-Ban Meta, Oakley Meta, Ray-Ban Display), 2026 distribution state, and how Extentos abstracts the toolkit.
Quickstart with an AI agent
Install the Extentos MCP server and let your AI agent scaffold Meta Ray-Ban smart-glasses capabilities into a native iOS or Android app. Free to start.
Implementation Guidance tools
The Extentos MCP server's guidance tools — getVoiceCommandGuidance (analyze proposed wake / command phrases for UX issues) and getPermissions (derive exact platform permissions, Meta DAT requirements, and foreground-service needs from the declared SDK capability list). Side-quest helpers the agent calls during composition.
Sessions
How an Extentos session works — the glasses connection-state machine, what persists across backgrounding and reconnects, and the three browser-simulator roles.
Projects
How Extentos identifies a project across Android and iOS — the Extentos app id (config.appId) decoupled from the store bundle id, and how the same app id shares one project across platforms.