---
title: Permissions
description: Extentos derives Android permissions and iOS Info.plist keys from the capabilities your handler uses, plus the always-required Bluetooth and Meta DAT keys.
type: concept
platform: all
related:
  - /docs/concepts/architecture
  - /docs/concepts/capabilities
  - /docs/vendors/meta
  - /docs/getting-started/with-agent
  - /docs/mcp-server/tools/guidance
---

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:

1. **`extentos.manifest.json`'s `capabilities` array declares what SDK primitives your handler uses** — `capture_photo`, `transcription_incremental`, `speak`, `video_frames`, etc.
2. **The derivation logic** (in `mcp-server/src/tools/util/permissions.ts`) maps each capability name to its required permissions per platform.
3. **`getPermissions`** returns 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:

```json
{
  "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 })`](/docs/mcp-server/tools/guidance) 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 live `getPermissions` response 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:

```xml
<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](/docs/sdk/android/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](/docs/getting-started/choose-your-path).

### `MWDAT` dictionary

```xml
<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.

<Callout type="warn">
  **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](/docs/sdk/ios/info-plist).
</Callout>

### `CFBundleURLTypes`

Must include the same scheme:

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array><string>yourapp</string></array>
    </dict>
</array>
```

### `LSApplicationQueriesSchemes`

```xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>fb-viewapp</string>
</array>
```

Required to query whether the Meta AI / Meta View companion app is installed.

### `UISupportedExternalAccessoryProtocols`

```xml
<key>UISupportedExternalAccessoryProtocols</key>
<array>
    <string>com.meta.ar.wearable</string>
</array>
```

Declares Meta's external-accessory protocol — required for the BLE link.

### `UIBackgroundModes`

```xml
<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:

```xml
<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:

```text
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`, and `BLUETOOTH_CONNECT` (on Android 12+) are **dangerous permissions** — declared in the manifest, but also requested via `ActivityCompat.requestPermissions` at runtime. The library's `ExtentosConnectionPage` handles the runtime flow during pairing, and once a vendor transport is running the SDK requests `BLUETOOTH_CONNECT` and `CAMERA` itself through your `activityProvider`. **Neither covers an app with no connection page** — a voice app owns its `RECORD_AUDIO` request outright, and a camera app must grant `BLUETOOTH_CONNECT` before `ExtentosGlasses.create(...)` for the vendor arm to resolve at all. See the [voice-assistant bootstrap](/docs/guides/voice-assistant#1-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_SERVICE` on Android** is special: declared in the manifest, but **cannot be requested programmatically**. The user must toggle it on manually in system Settings. Use `NotificationAccessHelper.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.denied` event into the structured event log; the affected capability's result is a typed failure. **On the capture path that failure is `CaptureError.PlatformError`, not `PermissionDenied`** — `CaptureError.PermissionDenied` is declared but not currently emitted, so don't hang your "ask for the camera again" recovery on it (see [photo capture fails](/docs/troubleshooting/photo-capture-fails)). Audio calls do return `AudioError.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 means `phone_notification_forwarded` events 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](/docs/concepts/architecture)** — how the SDK, library, and platform fit together; permissions are part of the boundary
- **[Capabilities](/docs/concepts/capabilities)** — the vocabulary that drives permission derivation
- **[Vendors: Meta Ray-Ban](/docs/vendors/meta)** — the Meta DAT-specific Info.plist keys, registration flow, audio architecture
- **[`getPermissions` tool](/docs/mcp-server/tools/guidance)** — the MCP tool the agent calls to retrieve the current permission set
- **[Quickstart with an AI agent](/docs/getting-started/with-agent)** — how the agent wires permissions during initial scaffolding
