# iOS Computer-Use — Honest Scope
Apple does not let third-party apps drive other apps. There is no equivalent
to Android's `MediaProjection` for full-screen capture without a user prompt,
no `ScreenCaptureKit` (that is macOS only), and no system-wide accessibility
API that a sandboxed iOS app can use to inject events into other apps.
This document spells out what *is* possible on iOS, exactly how each surface
behaves, and the manual validation checklist that has to be run on a real
device before any of this ships.
## What works
### 1. ReplayKit foreground capture (own app)
`RPScreenRecorder.shared().startCapture(handler:completionHandler:)` returns
`CMSampleBuffer`s of *the host app's* window. Useful for "show me what's on
the Eliza screen right now."
```swift
RPScreenRecorder.shared().startCapture(handler: { sampleBuffer, type, error in
// Receive frames for own-app capture only.
}, completionHandler: { error in
// Setup result; ReplayKit shows a system permission prompt on first use.
})
```
Constraints:
- Frame delivery rate is the display refresh rate; the bridge throttles to a
caller-supplied `frameRate` (default 1Hz).
- Hard cap of 30 seconds per session. Past that, the work has to move to a
`BGProcessingTask`.
### 2. ReplayKit broadcast extension (system-wide capture)
A separate target with a ~50MB memory ceiling. Streams frames over an App
Group shared container into the main app. The user must start the broadcast
themselves via the system share-sheet picker — apps cannot programmatically
launch the extension.
```swift
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer,
with sampleBufferType: RPSampleBufferType) {
// Compress + dump into the App Group container.
}
```
**iOS 26 / 26.1 beta regression**: extensions are killed within ~3 seconds
even when memory headroom is fine. We surface this as
`extension_died` from `broadcastExtensionHandshake` so callers can fall back
to foreground capture. Track Apple's feedback status before shipping this
target on iOS 26.
### 3. Apple Vision OCR
`VNRecognizeTextRequest` is on-device, free, supports ~30 languages, and
typically runs sub-300ms on modern devices.
```swift
let request = VNRecognizeTextRequest { request, error in
let observations = request.results as? [VNRecognizedTextObservation]
// Top candidate per observation; each has bounding box, text, confidence.
}
request.recognitionLevel = .accurate
request.recognitionLanguages = ["en-US"]
try VNImageRequestHandler(cgImage: cg).perform([request])
```
This is the OCR provider the WS6 scene-builder will pick up on iOS. The
provider interface is in
`eliza/plugins/plugin-computeruse/src/mobile/ocr-provider.ts`.
### 4. App Intents
The only sanctioned way to drive other apps. Each target app must expose
intents (Shortcuts-style). We support invocation via x-callback URL schemes
for the system apps in the static registry
(`ios-app-intent-registry.ts`):
- Mail — `mailto:` with `subject` / `body` / `cc` / `bcc`
- Messages — `sms:` with `body`
- Maps — `http://maps.apple.com/?daddr=...&dirflg=...`
- Safari — open URL
For richer intents (Notes append, Reminders add, Music play with a query)
the user has to donate the action via Shortcuts; we can then invoke via
`AppIntent` on iOS 16+. The bridge's `appIntentList` returns the runtime
list of donated intents this app sees.
### 5. UIAccessibility (own-app reading only)
`accessibilitySnapshot` walks the key window's view hierarchy and returns
`accessibilityLabel` / `accessibilityValue` / role. iOS gives us no way to
read another app's UIAccessibility tree.
### 6. Apple Foundation Models (iOS 26+)
Apple ships an on-device LLM under the `FoundationModels` framework when
Apple Intelligence is enabled. We expose this as an *opportunistic*
fast-path. If unavailable, the existing llama-cpp-capacitor local Eliza-1
vision path stays as the default.
Entitlement and Info.plist updates are listed below.
## What does NOT work
Stock iOS does not allow any of the following from a third-party app, and
we do not pretend otherwise:
- **Driving other apps' UI**. No cross-app input synthesis, no
`MediaProjection` / `ScreenCaptureKit` equivalent, no system-wide
accessibility event injection.
- **Listing other running apps' processes**. The kernel hides this from
third-party apps; there is no `ps`-equivalent.
- **Persistent background inference past ~30s**, except via
`BGProcessingTask` (opportunistic, OS-scheduled, not guaranteed).
If a feature spec calls for any of the above, escalate it as not feasible
on iOS rather than inventing a workaround that will get the app rejected
or silently broken by Apple in the next OS update.
## Entitlements + Info.plist
Add the following to `apps/app/ios/App/App/App.entitlements`:
```xml
com.apple.developer.kernel.increased-memory-limit
com.apple.developer.kernel.extended-virtual-addressing
```
Add the following to `apps/app/ios/App/App/Info.plist`:
```xml
NSScreenCaptureDescription
Captures the Eliza app window when you ask it to see the screen.
NSAppleEventsUsageDescription
Sends Shortcuts to other apps when you authorize an action.
```
The screen capture string is shown on ReplayKit's first-launch system
prompt. The Apple Events string is shown only if the host (this app) ever
needs to drive AppleScript via the Catalyst surface; iOS apps that only
use `UIApplication.shared.open(url:)` to invoke x-callback intents do not
need it, but it costs nothing to include for forward compatibility with
Mac Catalyst builds.
## Validation checklist
The iOS bridge contract is covered by TypeScript tests, but physical iOS
device behavior is not proven by this repository alone. The required evidence
manifest is `docs/ios-device-validation.json`; keep it in
`requires_device_evidence` until a real device run records device/build
metadata, artifacts, and per-method results. Release gates that require
physical proof should run:
```bash
bun run --cwd plugins/plugin-computeruse validate:ios-device-evidence -- --require-complete
```
Without `--require-complete`, the same command validates that the manifest
still tracks every required method and evidence field. Before shipping any
iOS bridge release, complete the manifest for:
### Simulator vs device
| Surface | Simulator | Real device |
| -------------------------------- | --------- | ----------- |
| ReplayKit foreground capture | Partial | Required |
| Broadcast extension | No | Required |
| Vision OCR | Yes | Required |
| App Intents (x-callback) | Partial | Required |
| Accessibility snapshot | Yes | Required |
| Foundation Models | No | Required |
| Memory pressure probe | Limited | Required |
### Per-method checklist
For each method below, run on at least one A14-or-later iPhone running the
target iOS (currently iOS 26.1, with iOS 17.6 as the floor):
1. **`probe()`** — assert `data.platform === "ios"`, `osVersion` matches the
device, all six capability bits are present.
2. **`replayKitForegroundStart`** — assert the system permission prompt is
shown on first use; on accept, `replayKitForegroundDrain` returns frames
with non-empty `jpegBase64` and the device's screen resolution.
3. **`broadcastExtensionHandshake`** — bundle the extension target via Xcode,
tap the share-sheet broadcast picker, and assert `broadcastActive`
transitions to `true`. On iOS 26 betas, verify `regression.observed`
eventually flips to `true` after the extension dies.
4. **`visionOcr`** — render a known PNG with the string `"WS9 OCR SMOKE"`
into the app, run OCR with `recognitionLevel: "accurate"`, and assert
`fullText` contains the source string (case-insensitive).
5. **`appIntentList`** — verify the returned list is empty for a fresh
install and grows after the user donates intents via Shortcuts.
6. **`appIntentInvoke`** with `com.apple.mobilesafari.open-url` — assert
Safari opens to the provided URL. With `com.apple.MobileSMS.send-message`
— assert Messages opens with the recipient and body pre-filled.
7. **`accessibilitySnapshot`** — assert the returned tree's top-level node
has `role !== "labeled"` and `children.length > 0` on a populated screen.
8. **`foundationModelGenerate`** — on a device with Apple Intelligence
enabled, assert a short prompt returns non-empty text. With AI disabled,
assert the call resolves with `foundation_model_unavailable`.
9. **`memoryPressureProbe`** — invoke `UIApplication.shared.performMemoryWarning()`
(debug-only) and assert the next probe call returns
`severity >= 0.7` with `lastWarningAt` set.
## Hand-off to other workstreams
- **WS1 (memory-pressure arbiter)** consumes `MemoryPressureSample` via the
shared `IPressureSignal` contract in `ios-bridge.ts`. The bridge is the
producer; the arbiter is the consumer. WS1 will subscribe to push events
via the bridge once that channel lands; for now it polls `memoryPressureProbe`.
- **WS6 (scene-builder OCR)** picks up the iOS Vision provider through
`selectOcrProvider()` in `ocr-provider.ts`. Register the provider at app
boot when running on iOS:
```ts
registerOcrProvider(
createIosVisionOcrProvider(() => Capacitor.Plugins.ComputerUse),
);
```
## Apple documentation references
- ReplayKit — https://developer.apple.com/documentation/replaykit
- Vision Text Recognition — https://developer.apple.com/documentation/vision/recognizing_text_in_images
- App Intents — https://developer.apple.com/documentation/appintents
- Foundation Models — https://developer.apple.com/documentation/foundationmodels (iOS 26)
- App Groups — https://developer.apple.com/documentation/xcode/configuring-app-groups
- `os_proc_available_memory()` — https://developer.apple.com/documentation/foundation/process_info/3743117-os_proc_available_memory
- Background Tasks — https://developer.apple.com/documentation/backgroundtasks