Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,47 @@ npx @swmansion/argent init

---

## Physical iOS devices (experimental)

Argent can drive a **physical iPhone** — no app installed on the device — over Apple's
CoreDevice "remote control" services (the same path Xcode's device window uses), via
[`pymobiledevice3`](https://github.com/doronz88/pymobiledevice3). Supported interactions:
`screenshot`, `gesture-tap`, `gesture-swipe`, `button`, and `launch-app`. The device shows
up in `list-devices` with `kind: "device"`.

**Requirements**

- **iOS 27 or later** — Apple gates host-driven input ("remote control") to iOS 27+; on
earlier versions the device reports `CoreDeviceError 9021` and only screenshots work.
- macOS with Xcode, and `pymobiledevice3` installed (e.g. `pipx install pymobiledevice3`).
- The iPhone connected, unlocked, trusted, with **Developer Mode** on.

**Setup**

1. Enable the feature flag:
```sh
argent enable physical-ios-devices
```
2. Connect the iPhone (unlocked, trusted, Developer Mode on).

`list-devices` then includes the iPhone, and the supported tools work against its UDID.
The first interaction (or `boot-device`) starts the required CoreDevice tunnel
automatically: Argent shows a standard macOS authorization prompt — branded as Argent,
with Touch ID / password — to launch `pymobiledevice3 remote tunneld` as root (creating the
tunnel interface needs root once; every other command runs unprivileged). No manual `sudo`.

If the prompt is declined or there's no GUI session (headless), start the tunnel manually
and leave it running: `sudo pymobiledevice3 remote tunneld`.

**Limitations / notes**

- Not supported on physical iOS yet: keyboard/typing, pinch & rotate (multi-touch),
`open-url`, and `reinstall-app` — these return a clear "not supported" error.
- Overrides: `ARGENT_PYMOBILEDEVICE3` (path to the binary), `ARGENT_PMD3_TUNNELD_PORT`
(defaults to `49151`).

---

## Installation

#### Prerequisites
Expand Down
32 changes: 32 additions & 0 deletions packages/argent/scripts/bundle-tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ const AX_TCP_BIN_SRC = path.resolve(BIN_SRC_ROOT, "darwin/tcp/ax-service");
const BIN_DIR = path.resolve(__dirname, "../bin");
const AX_BIN_DEST = path.resolve(BIN_DIR, "darwin/ax-service");
const AX_TCP_BIN_DEST = path.resolve(BIN_DIR, "darwin/tcp/ax-service");
// argent-device-auth: macOS host helper for the branded physical-iOS tunnel
// auth prompt. Best-effort — only present once argent-private publishes the
// signed binary; until then physical iOS falls back to the osascript prompt.
const DEVICE_AUTH_BIN_SRC = path.resolve(BIN_SRC_ROOT, "darwin/argent-device-auth");
const DEVICE_AUTH_BIN_DEST = path.resolve(BIN_DIR, "darwin/argent-device-auth");
// Argent icon shown in that prompt (committed under native-devtools-ios/assets).
const DEVICE_ICON_SRC = path.resolve(
WORKSPACE_ROOT,
"packages/native-devtools-ios/assets/argent-icon.png"
);
const DEVICE_ICON_DEST = path.resolve(__dirname, "../assets/argent-icon.png");
// Host platform keys (see hostPlatformKey() in @argent/native-devtools-ios):
// darwin is a universal binary; Linux ships one single-arch ELF per key.
const SUPPORTED_HOST_PLATFORMS = ["darwin", "linux", "linux-arm64"];
Expand Down Expand Up @@ -169,6 +180,27 @@ const ASSETS = [
copiedLabel: "ax-service (tcp) binary",
missLabel: "ax-service (tcp) binary",
},
// macOS host helper for the branded physical-iOS tunnel auth prompt.
// Best-effort: present once argent-private publishes the signed binary;
// until then physical iOS falls back to the (unbranded) osascript prompt.
{
kind: "file",
src: DEVICE_AUTH_BIN_SRC,
dest: DEVICE_AUTH_BIN_DEST,
mode: 0o755,
required: false,
copiedLabel: "argent-device-auth binary",
missLabel: "argent-device-auth binary",
},
// Argent icon shown in the device-auth prompt (committed; best-effort copy).
{
kind: "file",
src: DEVICE_ICON_SRC,
dest: DEVICE_ICON_DEST,
required: false,
copiedLabel: "device-auth icon",
missLabel: "device-auth icon",
},
// Android host-side Perfetto trace processor: the third-party WASM engine
// (trace_processor.wasm + emscripten glue + the EngineBase decoder + LICENSE).
// wasm-trace-processor.ts resolves these via
Expand Down
5 changes: 5 additions & 0 deletions packages/configuration-core/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export const FLAG_REGISTRY: readonly FlagDefinition[] = [
name: "artifacts-list-endpoint",
description: "Expose GET /artifacts for remote artifact inventory consumers.",
},
{
name: "physical-ios-devices",
description:
"Discover and control physical iOS devices (iOS 27+) over Apple's CoreDevice tunnel via pymobiledevice3. Requires a running `sudo pymobiledevice3 remote tunneld`. Supports screenshot, tap, swipe, and hardware buttons.",
},
];

// Look up a flag's definition — exported for consumers that want the
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions packages/native-devtools-ios/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import * as fs from "node:fs";
const DYLIB_DIR = process.env.ARGENT_NATIVE_DEVTOOLS_DIR ?? path.join(__dirname, "..", "dylibs");
const BIN_DIR = process.env.ARGENT_SIMULATOR_SERVER_DIR ?? path.join(__dirname, "..", "bin");
const DYLIB_TCP_DIR = process.env.ARGENT_NATIVE_DEVTOOLS_TCP_DIR ?? path.join(DYLIB_DIR, "tcp");
// Committed static assets (e.g. the Argent icon shown in the device-auth prompt).
const ASSETS_DIR =
process.env.ARGENT_NATIVE_DEVTOOLS_ASSETS_DIR ?? path.join(__dirname, "..", "assets");

// iOS Simulator only runs on macOS, so the dylibs that get injected into it
// and the ax-service that gets `simctl spawn`d into it are only ever usable
Expand Down Expand Up @@ -115,3 +118,24 @@ export function axServiceBinaryPathTcp(): string {
requireDarwin("ax-service (tcp)");
return requireBinIn(platformTcpBinDir(), "ax-service");
}

// argent-device-auth is the macOS host helper that pops the branded macOS
// authorization modal (password / Touch ID) and runs a command as root —
// Argent uses it to start the physical-iOS CoreDevice tunnel without a manual
// `sudo`. Unlike the resolvers above it returns null (rather than throwing)
// when absent, so callers can fall back to a less-branded escalation path.
// Override the binary with ARGENT_DEVICE_AUTH_HELPER (absolute path).
export function deviceAuthHelperPath(): string | null {
if (process.platform !== "darwin") return null;
const override = process.env.ARGENT_DEVICE_AUTH_HELPER;
if (override) return fs.existsSync(override) ? override : null;
const p = path.join(platformBinDir(), "argent-device-auth");
return fs.existsSync(p) ? p : null;
}

// Path to the Argent icon shown in the device-auth prompt, or null if missing.
// Override with ARGENT_DEVICE_ICON (absolute path to a PNG/icns).
export function argentIconPath(): string | null {
const p = process.env.ARGENT_DEVICE_ICON ?? path.join(ASSETS_DIR, "argent-icon.png");
return fs.existsSync(p) ? p : null;
}
8 changes: 8 additions & 0 deletions packages/tool-server/src/blueprints/ax-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,14 @@ export const axServiceBlueprint: ServiceBlueprint<AXServiceApi, DeviceInfo> = {
`${AX_SERVICE_NAMESPACE} is iOS-only. The target '${device.id}' classifies as ${device.platform} — describe uses uiautomator on Android and the CDP DOM walker on Chromium, neither of which needs this service.`
);
}
if (device.kind === "device") {
// ax-service uses `xcrun simctl spawn`, which only works on simulators.
// Physical iPhones are driven over CoreDevice and have no accessibility/
// describe path yet.
throw new Error(
`${AX_SERVICE_NAMESPACE} is iOS-simulator-only. The physical device '${device.id}' is driven over CoreDevice; describe/accessibility is not supported on physical iOS yet.`
);
}
// Reject before spawning. An undefined `device.id` slips through when an
// inner tool is invoked via a wrapper that doesn't re-validate the inner
// schema. Without this guard `getSocketPath(undefined).slice` would crash
Expand Down
Loading