Concepts

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:

  1. The AppSpec declares what your app does — blocks, triggers, streams, actions.
  2. The derivation logic (in mcp-server/src/tools/util/permissions.ts) maps each spec primitive 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({ 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 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 spec contents:

PermissionWhy
android.permission.BLUETOOTH_CONNECTConnect to paired Ray-Ban Meta over Bluetooth
android.permission.BLUETOOTH_SCANDiscover the paired device on the system bonded list
android.permission.INTERNETBackend communication for BrowserSimTransport and telemetry

Derived from blocks

Each block kind in your spec adds its own permissions:

Block kindAndroid permission(s)Note
capture_photoandroid.permission.CAMERA
capture_videoandroid.permission.CAMERA+ RECORD_AUDIO only if include_audio: true is set explicitly. Defaults to false to avoid over-declaring.
record_audioandroid.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 typeAndroid permission(s)Notes
manual_launch(none)App-driven entry point
voice_commandandroid.permission.RECORD_AUDIOPhone STT over Bluetooth audio
wake_wordandroid.permission.RECORD_AUDIOSame path as voice_command
push_to_talkandroid.permission.RECORD_AUDIOSame
capture_button(none)Hardware button press, no app-side permission
tap / double_tap(none)Touchpad input — vendor-mediated
location_updatedandroid.permission.ACCESS_FINE_LOCATIONGeofence-style triggers
phone_notification_forwardedandroid.permission.BIND_NOTIFICATION_LISTENER_SERVICESystem-managed; cannot be requested programmatically — see below
incoming_call_detectedandroid.permission.READ_PHONE_STATE

Derived from streams

Streams are continuous data flows. They add permissions and require a foreground service.

Stream typeAndroid permission(s)
video_framesandroid.permission.CAMERA + FOREGROUND_SERVICE
outgoing_video_streamandroid.permission.CAMERA + FOREGROUND_SERVICE
audio_chunksandroid.permission.RECORD_AUDIO + FOREGROUND_SERVICE
transcription_incrementalandroid.permission.RECORD_AUDIO + FOREGROUND_SERVICE
outgoing_audio_streamandroid.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

FieldDefaultSource
minimumSdk26 (Android 8.0)VERSION_INFO.android.minimumSdk
compileSdk35VERSION_INFO.android.compileSdk
targetSdk35VERSION_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

KeyPrivacy string defaultWhy
NSBluetoothAlwaysUsageDescription"Used to connect to your Meta Ray-Ban glasses."Required by Meta DAT SDK for all glasses connections

Derived from blocks

Block kindiOS plist keyPrivacy string
capture_photo / capture_videoNSCameraUsageDescription"Used to capture photos and video from your glasses."
record_audioNSMicrophoneUsageDescription"Used to capture audio from your glasses for voice commands."
capture_video with include_audio: truealso NSMicrophoneUsageDescription(same as record_audio)
speak_text(none)TTS via AVSpeechSynthesizer — no entitlement needed

Derived from triggers

Trigger typeiOS plist key(s)Notes
manual_launch(none)
voice_command / wake_word / push_to_talkNSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescriptionPhone-side recognition via SFSpeechRecognizer
capture_button / tap / double_tap(none)Hardware-mediated
location_updatedNSLocationWhenInUseUsageDescription

Derived from streams

Stream typeiOS plist key(s)
video_frames / outgoing_video_streamNSCameraUsageDescription
audio_chunks / transcription_incremental / outgoing_audio_streamNSMicrophoneUsageDescription + 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:

KeyDefault 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:

PermissionSource
android.permission.BLUETOOTH_CONNECTalways
android.permission.BLUETOOTH_SCANalways
android.permission.INTERNETalways
android.permission.CAMERAcapture_photo block
android.permission.RECORD_AUDIOvoice_command trigger

→ 5 permissions. foregroundService.required: false (no streams). notificationListener.required: false.

getPermissions({ spec, platform: "ios" }) returns these plist keys:

KeySource
NSBluetoothAlwaysUsageDescriptionalways
NSCameraUsageDescriptioncapture_photo block
NSMicrophoneUsageDescriptionvoice_command trigger
NSSpeechRecognitionUsageDescriptionvoice_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, 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 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_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 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.denied event into the structured event log; the affected capability fails with TransportError.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 triggers 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.

  • 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
  • getPermissions tool — 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