Open installed apps, websites, proven Settings destinations, and supported content routes on a paired physical iPad with one Python call. Local-file routes use AirDrop.
Give the repository link to a coding agent on the Mac that will control the iPad:
Install https://github.com/TobyNoSkillSon/ipad-agent on this Mac using the README's
“Default installation: CoreDevice only” instructions. Do not install or configure
Appium, WebDriverAgent, Node.js, npm packages, or Apple signing. Stop for every
human-only gate, and ask before the single Safari test.
The default installation contract uses CoreDevice and installs no automation stack. Appium and WebDriverAgent have a separate, explicitly optional maintenance path.
from ipad_agent import ipadmaps
ipadmaps("open")iPad Agent is a macOS-only alpha. Current semantic integrations use Apple CoreDevice for launches and deep links; local files use AirDrop. Settings and Maps are CoreDevice-only. Optional WebDriverAgent (WDA) infrastructure remains dormant for maintenance and possible future selector integrations. The project is not affiliated with or endorsed by Apple Inc. iPad is an Apple trademark.
Ordinary semantic commands do not jailbreak the iPad, disable its passcode, install apps or profiles, automate protected prompts, or start an iPad-control server. They can launch apps and URLs, create browser history or app state, and import files that remain on the device.
Pairing trust persists beyond one command. Developer Mode belongs to the optional WDA path; Apple states that enabling it reduces device security. WDA adds arbitrary UI inspection, screenshots, taps, swipes, and typing, so it stays dormant unless a person explicitly requests maintenance and accepts that risk. SECURITY.md explains the exact attack surface, shutdown procedure, and manual revocation steps.
| Request | Primary route | What success proves |
|---|---|---|
| Open an installed app | CoreDevice | Launch request accepted |
| Open a website or app deep link | CoreDevice URL payload | URL dispatch accepted |
| Open a proven Settings destination | CoreDevice URL payload | Dispatch accepted for a profile-bound route |
| Open an App Store or Books product | CoreDevice URL payload | Product URL dispatch accepted |
| Transfer a local file | AirDrop | Mac-side sharing callback completed |
CoreDevice acceptance does not prove foreground state or rendering. Validation and discovery can reject a request with certainty before dispatch. Once a devicectl launch subprocess starts, a timeout or nonzero exit is conservatively uncertain because delivery cannot be disproved. Inspect state instead of replaying the action.
Run Python from the checkout root. Each function call performs one requested operation. Application adapters live under integrations/<app>/; shared validation, unlock gates, dispatch, transports, and result projection remain under ipad_agent/.
| Destination | Calls |
|---|---|
| Core | ipadc("open", "Preview") · ipadc("drop", "/absolute/path/report.pdf") · ipadc("status") |
| Preview | ipadpreview("open") · ipadpreview("drop", path) · ipadpreview("show", path) |
| Books | ipadbooks("open") · ipadbooks("item", product) · ipadbooks("drop", path) · ipadbooks("show", path) |
| Files | ipadfiles("open") · ipadfiles("drop", path) · ipadfiles("show", path) |
| Settings | ipadsettings("open") · ipadsettings("general") · ipadsettings("about") · ipadsettings("wifi") · ipadsettings("bluetooth") · ipadsettings("battery") · ipadsettings("accessibility") |
| Clock | ipadclock("open") |
| App Store | ipadappstore("open") · ipadappstore("show", 123456789) |
| Safari | ipadsafari("website", url) · ipadsafari("youtube", url, at=90) |
| Brave, optional | ipadbrave("website", url) · ipadbrave("search", query) · ipadbrave("youtube", url, at=90) |
| Apple Maps | ipadmaps("open") plus the proven map routes below |
| Google Maps | ipadgooglemaps("search", query) · ipadgooglemaps("map", coordinate, ...) · ipadgooglemaps("directions", destination, ...) · ipadgooglemaps("street-view", coordinate, ...) · ipadgooglemaps("link", url) |
| Pages | ipadpages("open") · ipadpages("drop", path) · ipadpages("show", path) |
| Numbers | ipadnumbers("open") · ipadnumbers("drop", path) · ipadnumbers("show", path) |
| Keynote | ipadkeynote("open") · ipadkeynote("drop", path) · ipadkeynote("show", path) |
| Photos | ipadphotos("open") · ipadphotos("drop", path) · ipadphotos("show", path) |
| Messages | ipadmessages("compose") |
Website calls accept explicit HTTP or HTTPS URLs. YouTube timestamps are non-negative seconds. App Store show accepts a positive product ID or canonical apps.apple.com app-product URL. Books item accepts an exact Books asset ID or canonical product URL. File extensions are checked by the destination package before the shared AirDrop policy runs.
The index contains 15 application packages: 14 core packages and optional Brave. Mail is unsupported and is not indexed. App-local SKILL.md files are on-demand instructions, not independently registered Pi skills.
For product iPad17,1, hardware J817AP, iPadOS 26.6.1, build 23G83, Apple Maps has seven proven Unified URL route families: frame, search/show, place, Look Around, directions preview, guides, and validated full links. navigate and report-a-problem remain candidate-gated. Ordinary ipadmaps("open") remains available.
ipadmaps("frame", center=(52.2297, 21.0122), span=(0.05, 0.08), map="explore")
ipadmaps("search", "coffee & breakfast", center=(52.2297, 21.0122))
ipadmaps("place", place_id="opaque-apple-place-id", name="Meeting point")
ipadmaps("look-around", address="Eiffel Tower, Paris")
ipadmaps("directions", "Gdańsk", origin="Warsaw", waypoints=["Łódź", "Toruń"], mode="driving")
ipadmaps("guides")
ipadmaps("link", "https://maps.apple.com/place?place-id=opaque-id")directions stops at route preview. Apple Maps navigation start and report remain maintenance candidates. Google Maps search, map, directions preview, Street View, and canonical links are proven on the same profile; Google Maps navigate remains a candidate. Candidate helpers do not provide an ordinary dispatch shortcut. See SECURITY.md for their authorization boundary.
Brave website and search routes are proven on the recorded profile. Private browsing remains a candidate. IPFS and IPNS are incompatible on that profile.
drop sends one allowlisted local file. Destination packages restrict file types, while private configuration sets allowed roots, extensions, maximum size, and timeout. The macOS picker, or same-Apple-Account auto-accept behaviour, determines the receiver. A person can choose the iPad only when the picker appears.
Mac-side completion does not prove recipient choice, receipt, import, or opening. Exact-profile representative file evidence is limited to:
| Route | Proven scope |
|---|---|
Preview PDF show |
Received and opened in Preview |
Books EPUB show |
Received and opened in Books |
Pages DOCX show |
Received, imported, and opened; no broader content-fidelity claim |
Numbers XLSX show |
Received, imported, and opened; no broader content-fidelity claim |
Keynote PPTX show |
Received, imported, and opened; no broader content-fidelity claim |
Files ZIP drop |
Received and opened in the generic system preview; Files ownership is not proven |
Photos PNG drop |
Persistent import and opening; content fidelity is not proven |
Other admitted formats and Files/Photos show remain unproven. SECURITY.md defines private evidence handling and uncertain-transfer recovery.
Calls return IPadResult, a dictionary with a compact terminal representation.
accepted: CoreDevice accepted the launch or URL request. Visible state is unproved.locked: the iPad requires manual authentication.pending: one stage completed but the requested visible handoff remains unproved.uncertain: the action may have happened before the response was lost. Inspect before any new action.failed: reason: a certain pre-dispatch validation or discovery boundary rejected the request.
package.json exposes only skills/use-ipad. That gateway resolves the requested application through integrations/index.json, then reads only the app-local SKILL.md. Those 15 app-local skills stay out of pi.skills and enter context only when requested. Maintenance guidance remains in app-local WORKFLOWS.md and docs/integrations/.
Installing the Pi package registers this gateway. It does not install the ipad_agent Python package. Python imports work directly from the repository root; there is no pip installation step. To retire the checkout, run pi remove "$PWD" from its root and confirm its absence with pi list before moving or deleting it.
No current semantic app integration starts WDA or Appium. Direct app, URL, Settings, and Maps routes do not need them. Do not install the automation stack during normal setup. The optional maintenance path exists only for automated screenshots, rendered-state checks, and investigation of a new integration when CoreDevice dispatch plus human confirmation is insufficient. See Optional maintenance: Appium and WDA.
This is the normal installation contract for an agent running on the Mac paired with the iPad. It installs no Node.js packages, Appium, WDA, or Apple signing material. Follow the steps in order and keep a short record of each check. Reading files, creating private local configuration, and running doctor do not authorize a device action.
-
Clone the repository and enter its root.
git clone https://github.com/TobyNoSkillSon/ipad-agent.git cd ipad-agentRead
AGENTS.md, this section,SECURITY.md,DEPENDENCIES.md, andconfig.example.tomlbefore changing local state. Confirm thatpackage.jsonexposes only./skills/use-ipad. -
Check prerequisites without installing or upgrading them on the user's behalf.
test "$(uname -s)" = Darwin python3 -c 'import sys; assert sys.version_info >= (3, 11); print(sys.version.split()[0])' xcode-select -p xcodebuild -version xcrun devicectl --version command -v pi
The required host tools are macOS, Python 3.11 or newer, and full Xcode selected through
xcode-select. Pi is required only for skill registration. If any prerequisite is absent, report it and wait for the user to install or approve it. -
Create private configuration. Preserve an existing file and reject unsafe existing state instead of blindly changing it.
python3 - <<'PY' import os, stat from ipad_agent.core.config import DEFAULT_CONFIG_PATH from ipad_agent.core.paths import descriptor_has_extended_acl, private_mkdir, private_write_text private_mkdir(DEFAULT_CONFIG_PATH.parent) if os.path.lexists(DEFAULT_CONFIG_PATH): flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) descriptor = os.open(DEFAULT_CONFIG_PATH, flags) try: info = os.fstat(descriptor) if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): raise RuntimeError("existing config is not a current-user-owned regular file") if descriptor_has_extended_acl(descriptor): raise RuntimeError("existing config carries an extended ACL") if stat.S_IMODE(info.st_mode) & 0o077: raise RuntimeError("existing config grants group or other permissions") os.fchmod(descriptor, 0o600) finally: os.close(descriptor) else: private_write_text(DEFAULT_CONFIG_PATH, open("config.example.toml", encoding="utf-8").read()) print("private config ready") PY stat -f '%Lp %N' .runtime .runtime/config .runtime/config/config.toml
private_mkdirrejects symlinks, non-directories, foreign ownership, extended ACLs, and group or other permissions anywhere in.runtime/config. Stop and inspect any rejection; never follow or chmod an unverified path.Keep device and signing identifiers inside
.runtime/. Do not print the file or copy its private values into chat or reports. A device ID may stay unset when CoreDevice sees exactly one paired iPad. If several are present, ask the user which one to use and store only that choice in the private file. -
Run the read-only readiness check from the repository root.
python3 -m ipad_agent doctor --json
Doctor reports CoreDevice, AirDrop, and optional WDA readiness separately. It reads installed npm package metadata without executing optional Appium or driver code, and its public report does not expose raw CoreDevice discovery output. Treat optional AirDrop or WDA failures as optional unless the user requested those routes.
-
Stop at human trust and unlock gates. Ask the user to pair the iPad, choose Trust This Computer, unlock it, accept Xcode licence or first-launch prompts, or make any other protected choice shown by the tools. The agent must not enter a passcode, use biometrics, enter Apple Account credentials or 2FA, approve signing, or click protected Apple prompts. After the user confirms completion, rerun
doctor; do not infer that a prompt was accepted. -
Register the checkout as a Pi package.
pi install "$PWD" pi listThis adds the local package to Pi settings and exposes the
use-ipadgateway. It is separate from Python setup and does not makeipad_agentimportable outside the checkout. -
Verify Python from the repository root. Do not run
pip install,pip install -e, or an npm install for the Python runtime.python3 -m ipad_agent --help python3 -c 'from ipad_agent import ipadc, ipadsafari, ipadmaps; print("python imports ok")'Every later Python example must run with this repository as the working directory.
-
Configure AirDrop only if the user requests local-file routes. Use a dedicated, current-user-owned directory that is not writable by group or other users. Never allow
/,/Users, the home directory, a shared directory, a broad cloud-sync root, or another aggregate collection. Put only files intentionally available for transfer in that directory. Then set explicit extensions and a byte ceiling in.runtime/config/config.toml, retaining mode0600.[airdrop] allowed_roots = ["/Users/example/iPad-Agent-Transfer"] allowed_extensions = [".pdf", ".epub", ".png"] max_bytes = 524288000 timeout_seconds = 120
Rerun
doctor. The picker or same-Apple-Account auto-accept determines the receiver. A person chooses the recipient only when a picker appears; the agent cannot infer the outcome from the callback. -
Enable Brave only if the user confirms that Brave is installed and requests it.
[addons] enabled = ["brave"]
This private configuration enables semantic Brave commands. It does not install Brave or change the legacy display browser. Leave the addon list empty otherwise.
-
Request authorization for one harmless physical test. State the exact operation and wait for an explicit yes:
ipadc("open", "Safari")once. After approval, run it once from the repository root.python3 - <<'PY' from ipad_agent import ipadc print(ipadc("open", "Safari")) PY
acceptedproves dispatch only, so ask the user whether Safari became visible if visible confirmation matters. If the result islocked, let the user unlock the iPad before any new authorized attempt. If it isuncertain, do not replay it. Inspect with the user and report the unresolved state. -
Verify skill discovery in a fresh Pi session without contacting the device.
pi --no-session "Use the installed use-ipad skill for Safari. Do not contact the iPad. Report the Python call you would run and the app-local skill you loaded."The answer should identify the gateway, resolve Safari through
integrations/index.json, loadintegrations/safari/SKILL.md, and prepare a bare Python call that must run from the checkout root. It must not claim that Pi package installation installed Python. -
Declare the default CoreDevice setup complete only when required prerequisites pass, the private runtime directories are mode
0700, the config is mode0600, CoreDevice readiness is known, the Pi package appears inpi list, root-level Python imports pass, the fresh session finds the gateway and Safari skill, and the single approved test has a recorded certain result. Record WDA as skipped unless the user separately approved the maintenance installation. Mark AirDrop and Brave as enabled, skipped, or blocked. If the user declines the physical test, report setup as configured but not physically verified.
On any failure, stop at the failing step and report the command, a redacted error summary, what remains unknown, and the exact human action or prerequisite needed. Never paste credentials, private paths, device identifiers, raw device output, or physical evidence. Never guess that a prompt succeeded, broaden authorization, or repeat an uncertain device action. SECURITY.md is authoritative for recovery and evidence handling.
Skip this section for normal use. Existing app commands already have their route policies and exact-profile evidence. CoreDevice dispatch plus the user's visual confirmation is enough for a harmless new-route pilot when no captured screenshot is required.
Use the automation stack only when the user explicitly requests automated screenshot evidence, rendered-state observation, or selector research that CoreDevice cannot provide. Ask before installing it. The host phase downloads pinned Appium and XCUITest packages from npm and may execute third-party installation code as the current Mac user. The full path also requires Developer Mode, local Apple Development signing, a locally built WDA, and human handling of every Xcode, keychain, provisioning, trust, and protected-confirmation gate.
After that approval, run each phase separately and stop at any human gate:
python3 -m ipad_agent setup --phase host --apply --json
python3 -m ipad_agent setup --phase wda --apply --json
python3 -m ipad_agent setup --phase verify --apply --jsonSuccessful bounded verification stops its owned Appium server. After a failed verification, or when retiring WDA, inspect the cleanup plan before applying it:
python3 -m ipad_agent cleanup --json
python3 -m ipad_agent cleanup --apply --jsonCleanup is owned-only and can be partial. It first requires receipt-owned Appium to stop, or proves that no unreceipted project Appium state is present, before removing ownership-metadata-matching WDA artifacts. It does not remove host npm packages, private configuration, AirDrop recovery snapshots, the development-signed runner from the iPad, Developer Mode, computer trust, certificates, or profiles. Follow SECURITY.md for full manual shutdown and revocation.
Run the offline release gate from the checkout root:
python3 scripts/release_check.py
python3 -c 'from ipad_agent import ipadc, ipadpreview, ipadbooks, ipadfiles, ipadsettings, ipadclock, ipadappstore, ipadbrave, ipadsafari, ipadmaps, ipadgooglemaps, ipadpages, ipadnumbers, ipadkeynote, ipadphotos, ipadmessages'
python3 -m unittest discover -s tests -p 'test_*.py' -v
python3 -m unittest discover -s integrations -t . -p 'test_*.py' -v
python3 -m compileall -q ipad_agent integrations tests scriptsCI runs the offline test boundaries and never runs physical actions. Integration maintenance and physical evidence procedures live in docs/integrations/authoring.md.
The alpha does not support browser tab control, generated display pages, automatic file association, general UI automation, recipient-bearing Messages, or Mail. Blank Messages compose is proven and cannot send. Current file and map limits are listed above; candidate routes remain gated even when vendor documentation exists.
Runtime configuration, identifiers, signing material, build products, logs, screenshots, and raw physical evidence stay under the Git-ignored .runtime/ directory.
Licensed under Apache-2.0. See LICENSE, THIRD_PARTY.md, and SECURITY.md.