sandbox_device
Leases an Android emulator or an iPhone simulator for the box, shown on its screen; lists the box's devices, or releases one.
| Input | Output |
|---|---|
id, action (attach, list, release, reconnect or restart), platform (android or ios), model, osVersion, deviceId |
the device's deviceId, serial inside the box, model, osVersion, status; attach also returns bootTimings and egressIp; list returns the box's devices and the device hosts; reconnect returns the device with reconnected: true, restart with restarted: true |
attach leases a phone for the box, on separate hardware, and waits until it has booted: an Android emulator with platform: "android", an iPhone simulator on a Mac with platform: "ios". Its screen is on the box display, so sandbox_shot, recordings, takeover and a box sandbox_review show it, and the person can tap and type on it there. A box can hold up to 4 devices. release gives one back; stopping the box releases all of them. Devices bill by the minute while attached and running. A phone freezes and thaws with its box (Box states): when the box goes 10 minutes without use, the phone is frozen first, then the box, and a frozen phone holds no device slot and does not bill. An Android emulator is saved with its memory, so it comes back exactly where it was; an iPhone simulator is shut down, so its installed apps, their data and the Mac's copy of /work are kept, but apps start again. The call that thaws the box brings the phone back on the same host and attaches it with the same serial before it runs (about 35 seconds for Android, 50 for an iPhone; both come back at the same time). If that host has no free slot then, the phone stays frozen (list shows status: "frozen") and is attached again as soon as a slot frees up while the box keeps being used (a call in the last 10 minutes); release discards it. list does not wake a frozen box. A box review of it waiting for the person does not keep it up: the phone freezes and is released with the box by the usual idle rules, and the person opening the review wakes both. When every device of that platform is taken, attach fails with all N <platform> device slots are in use; try again later or release a device with sandbox_device release. An osVersion the device hosts lack fails at once with 400 osVersion <v> is not available: the <platform> device host has <version>; attach without osVersion to use it, before anything boots: a phone is never quietly given another version. While it waits, progress messages say which step the phone is at (for an iPhone: creating the lease user, creating the simulator, its first boot, warming up Safari), and the result's bootTimings says how long each step took. egressIp in the result is the IP the phone's own internet traffic leaves from: it is not the box's IP, so check IP allowlists and geolocation against it (the phone has no curl, and curl in the box answers with the box's IP).
list returns { "devices": [...], "hosts": [...] }. devices has the box's phones with their status (ready, frozen, offline, restarting and the like), each with a note when it is not simply ready, and those that stopped working in the last hour with status: "failed" and their reason (an emulator that keeps crashing ends up there; attach a new one). hosts says, for each platform, whether a device host is online, how many freeSlots it has, the osVersions it can boot (pass one of them as osVersion, or leave it out for the default) and egressIps, where a phone's own traffic leaves from; when none is online it adds lastHeartbeatAt and a note: attaching that platform fails until it is back, which is on ParallelSandbox's side, not in your project, so plan the test without that phone or try later. Check it before spending time building for a phone; sandbox_start with toolchains: ["android"] says so in its next when no Android host is online.
When the person should try an Android build and no emulator is available, hand them the APK itself to install on their own phone. There is no file entry in sandbox_review; serve it as a page: put the APK in a folder with an index.html that links it (<a href="app-debug.apk" download>Install</a>), serve that folder in the background (cd /work/apk && python3 -m http.server 8090, background: true), and call sandbox_review with open: "web" and port: 8090, saying in what that they open it on their Android phone, download the APK and allow their browser to install apps when Android asks. A debug build installs this way; a phone with a Play Store build of the same app needs that one uninstalled first, since the signatures differ.
An Android device whose adb in the box no longer reaches it comes back by itself. When its tunnel drops, the box notices within about 20 seconds and reconnects it, trying again every 30 seconds until it is back; when the emulator itself crashes or stops answering adb for a minute, its device host restarts the same emulator in place, keeping its serial, installed apps and their data (a cold boot, about a minute), and the box reconnects to it. Meanwhile list shows it as status: "offline" with its adb state (and, once the box has reconnected it, autoReconnects), or status: "restarting"; a device that was restarted shows restarts, restartedAt and restartReason. sandbox_device { "id": "<id>", "action": "reconnect", "deviceId": "<deviceId>" } reconnects it right away and returns it with reconnected: true. An emulator that crashes a fourth time within 15 minutes is not restarted again and is listed as failed: often the app is what brings it down, so read adb logcat before it goes, then attach a new one.
sandbox_device { "id": "<id>", "action": "restart", "deviceId": "<deviceId>" } restarts a phone in place and returns once it has booted (restarted: true), with the same serial, installed apps and their data; apps that were open start again. An Android emulator reboots (adb reboot, then the box reconnects it, waits for the boot to finish and wakes the screen); an iPhone simulator shuts down and boots again, which also applies settings that only take effect on a new boot, such as simctl ui {udid} appearance dark, and clears a system dialog that will not go away. Boxes started before this action existed answer that their image cannot restart a device.
Android (about 35 seconds): Android 15 on a pixel_9 profile unless you pass model (for example pixel_7). Inside the box adb devices lists it under the returned serial (emulator-5554, then emulator-5556), and during a review or takeover the person can tap and type on it.
The box has artemis-adb, deterministic ADB tools with no model inside: hierarchy --out FILE (a JSON array of on-screen elements with text, content-desc, resource-id, package and parsed_bounds; tap the center of parsed_bounds), tap X Y, long-press X Y, swipe X1 Y1 X2 Y2, type X Y TEXT, clear-text X Y, erase, back, key KEYCODE, launch PACKAGE, stop PACKAGE, open URL, screenshot --out FILE.
adb reverse tcp:PORT tcp:PORT works through the tunnel: the phone's localhost:PORT is that port in the box, so a dev server in the box reaches the app the way Metro reaches React Native. Hybrid apps (Capacitor, Cordova, Ionic) built in debug mode, or with WebView.setWebContentsDebuggingEnabled(true), expose their WebView to Chrome DevTools: psbx-webview <package> [port] forwards that app's WebView to 127.0.0.1:<port> in the box (default 9222) and prints each page's webSocketDebuggerUrl (psbx-webview alone lists the apps that have one, psbx-webview chrome is Chrome itself). Connect to a page's URL with raw CDP, chrome-remote-interface or Puppeteer's connect({ browserWSEndpoint: <that URL> }); Playwright's connectOverCDP fails on a WebView with Browser.setDownloadBehavior: Browser context management is not supported. adb exec-out screencap -p > /work/phone.png saves the phone screen at full resolution.
iOS: an iPhone simulator on an Apple silicon Mac with Xcode 26.5, running iOS 26.5 on an iPhone 17 Pro unless you pass model, the name of another simulator device such as "iPhone 17", "iPhone Air", "iPhone 17 Pro Max" or "iPad Pro 11-inch (M5)". list shows the iOS versions the Macs have in hosts[].osVersions; leave osVersion out for the newest. An iPhone takes 1 to 4 minutes to attach, most of it the first boot of a new simulator. serial is the simulator's UDID. The simulator belongs to a macOS user made for this lease alone, and is deleted with everything in it when the device is released, including the Mac's copy of /work and what was built there: after a new attach, sync and build again.
There is no adb for iOS, and Xcode runs only on the Mac, so the app is built there. The box gets psbx-ios on PATH, plus xcodebuild and simctl wrappers that forward to it; run them with sandbox_exec. Each call is an SSH round trip to the Mac (about 3 seconds for the first, under a second after that). The box itself needs no toolchain or extra size for this.
- Every
psbx-iosword runs on the Mac in the lease'swork/folder, a copy of the box's/workthatpsbx-ios syncfills, at the same paths: the box's/work/appiswork/appon the Mac (psbx-ios pwdprints the folder's full path). So give the other words paths relative to/work,xcodebuild -project app/Hello.xcodeproj ...or-workspace app/ios/App/App.xcworkspace, never box paths such as/work/app/...;cdin the box changes nothing there. psbx-ios sync [PATH]copiesPATHfrom the box (default all of/work; a relative path starts from your directory in the box, and a path outside/worklands under its last name) to the same place on the Mac, with or without a trailing slash, and prints progress and the Mac's full path when it is done; run a large one, such as anode_modules, withsandbox_execandbackground: true. It deletes what the box no longer has there, so a file deleted in the box goes away on the Mac too, except what is built or recorded on the Mac:dd/andDerivedData/folders at any depth andbuild/,*.mp4and*.movat the top ofPATHare kept on the Mac and never sent.psbx-ios pull PATH [DEST]copies a file or folder from the Mac'swork/into the box (DESTdefaults to/work/), for what only the Mac produced.xcodebuild -project app/Hello.xcodeproj -scheme Hello -destination "platform=iOS Simulator,id={udid}" -derivedDataPath dd CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual buildbuilds for the leased simulator (about 6 seconds for a small SwiftUI app;{udid}is replaced by this simulator's id, and<serial>works too). The product stays on the Mac, heredd/Build/Products/Debug-iphonesimulator/Hello.app; nothing comes back to the box except each command's output (and what youpsbx-ios pull).lsin the box does not see it;psbx-ios ls dd/Build/Products/Debug-iphonesimulatordoes. KeepCODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual(ad hoc signing, no certificate needed): withCODE_SIGNING_ALLOWED=NOthe app runs but its launch screen stays black, because the simulator refuses an unsigned launch storyboard.psbx-ios xcodegen DIR [ARGS]runs XcodeGen inwork/DIRon the Mac (defaultgenerate), making the.xcodeprojfrom itsproject.yml, so a new app needs no hand-written project file:psbx-ios sync app, thenpsbx-ios xcodegen app.psbx-ios install dd/Build/Products/Debug-iphonesimulator/Hello.app(a path on the Mac; for one that is not there, such as a box path, it says that paths start from the Mac'swork/and to look withpsbx-ios ls),psbx-ios launch BUNDLE_ID,psbx-ios terminate BUNDLE_ID, andpsbx-ios screenshot /work/phone.png(or> /work/phone.png) for the screen at full resolution (1206 × 2622 on an iPhone 17 Pro).BUNDLE_IDis the target'sPRODUCT_BUNDLE_IDENTIFIER:xcodebuild -project app/Hello.xcodeproj -scheme Hello -showBuildSettings | grep PRODUCT_BUNDLE_IDENTIFIER.simctl ARGSrunsxcrun simctl ARGSon the Mac with{udid}replaced by this simulator, for examplesimctl openurl {udid} myapp://orders/42.psbx-ios xcrun ARGSruns other Xcode tools, such aspsbx-ios xcrun --sdk iphonesimulator swiftc ....psbx-ios boot,psbx-ios shutdown,psbx-ios lsandpsbx-ios pwdwork too, and so do the input commands below andpsbx-ios help, which lists them all; any other word afterpsbx-iosis refused with exit code 126 andpsbx-lease: 不允許的指令: <word>.simctl privacy {udid} grant photos BUNDLE_ID(alsolocation,contacts,calendar,microphoneand the other servicessimctl privacylists) answers a permission prompt in advance; notifications are not among them, so tap Allow on that prompt (psbx-ios tapwaits for it to finish sliding in) or have a test build skip the request.simctl ui {udid} appearance darktakes effect for apps after arestartof the device.- The Xcode project of a Capacitor or React Native app links packages in
node_modulesby relative path: sync the folder that holds bothios/andnode_modules/(psbx-ios sync myapp), or the package graph will not resolve. - A Flutter app builds for iOS with the Mac's own Flutter, the same version as the box's
fluttertoolchain:psbx-ios sync app, thenpsbx-ios flutter app build ios --simulatorrunsflutter build ios --simulatorinapp/on the Mac, andpsbx-ios install app/build/ios/iphonesimulator/Runner.appputs it on the simulator. The firstpsbx-ios flutterof a lease takes a few seconds longer while the lease gets its own copy of the SDK. Plugins that need CocoaPods work too: the Mac has CocoaPods and Node on the PATH of everypsbx-iosword. - React Native, Expo and Capacitor 6 or older use CocoaPods: sync the folder that holds both
ios/andnode_modules/, runpsbx-ios pod myapp/ios install(CocoaPods inmyapp/ioson the Mac; Node is there for the Podfile and for the Xcode phase that bundles JavaScript), then build the.xcworkspacewithxcodebuild. Build React Native with-configuration Release: a debug build loads JavaScript from Metro, and Metro in the box is not reachable from the simulator. For Expo, runnpx expo prebuild --platform iosin the box first. Syncingnode_modulesto the Mac is slow (about 4.5 minutes for a fresh React Native app's 324 MB), so sync it once and later sync only what changed. With an iPhone attached,xcodebuildin the box forwards to the Mac, sonpx cap sync iosin the box also calls it and prints thatApp.xcodeprojdoes not exist; the web assets are still copied, andpsbx-ios podandxcodebuild -workspaceafterwards build normally. - Distribution signing is not available: the Mac has no signing identities, so archives for the App Store or TestFlight, and uploads to App Store Connect, are done on the person's own Mac. The Mac also runs only iOS simulators: a macOS desktop app cannot be tested there (the lease has no GUI session).
- An app's WebView cannot be opened in Safari's Web Inspector from the box. To read what it prints, start the app with
simctl launch --console --terminate-running-process {udid} BUNDLE_IDin asandbox_execwithbackground: true: the app's stdout and stderr, where Capacitor writes eachconsole.log, go to that command's log until the app stops. Or send messages from the page to a server in the box. - Mobile Safari is opened and closed once before
attachreturns, so its first real launch is not stuck on a black screen; a slow page load still shows only the page, so look withpsbx-ios screenshotwhen in doubt. A Vite dev server serves hundreds of separate modules, and Mobile Safari loads them through the box's URL far slower than Chrome on Android (50 seconds and more on the splash screen): to try a web app on the iPhone,vite buildand serve the built files (vite preview) instead.
The box display shows the simulator as a live stream of about 6 to 10 frames a second. The person taps, swipes and types on it during a review or takeover; typing accepts Unicode text, including Chinese and emoji. You can drive it the same way, with coordinates in points (the screen in pixels divided by the scale: 402 × 874 on an iPhone 17 Pro):
psbx-ios tap X Ytaps. It first waits up to 2 seconds for the screen to stop moving, so it does not land on a transition or on a system dialog still sliding in, then sends the touch the way the person's taps go, which a dialog does not block.psbx-ios swipe X1 Y1 X2 Y2 [SECONDS]swipes, 0.3 seconds unless you pass one.printf 'hello 中文測試🙂' | psbx-ios typetypes the text on stdin into the focused field, Chinese and emoji included: text with anything beyond ASCII goes through the simulator's Unicode helper by itself.psbx-ios unicodetypes through that helper only.- Add
--wait [MS]totap,swipe,type,unicode,keyorbuttonto return only once the screen has not changed forMSmilliseconds (default 500), so the nextsandbox_shotorpsbx-ios screenshotshows the result rather than the frame before it (typing through the Unicode helper returns before the text is on screen):psbx-ios tap 200 400 --wait,printf '你好' | psbx-ios type --wait 800. It prints how long it waited, and returns anyway after 5 more seconds of an animation or video.psbx-ios settle [MS]only waits. A blinking cursor does not count as a change. - Keys and typing retry while the screen is mid-transition, waiting for it to settle between tries; when they still fail, the message says a system dialog (a notification permission, a Safari prompt) may be in the way, so look with
sandbox_shot. psbx-ios key HIDCODEpresses a key: 40 Enter, 42 Backspace, 43 Tab, 41 Escape.psbx-ios button homepresses a hardware button:home,lock,side-button,siriorapple-pay.psbx-ios sizeprints the screen's width and height in pixels and the scale (1206 2622 3.000000).
A deep link (simctl openurl) or a UI test target (xcodebuild test) still reaches a screen faster; check it with sandbox_shot or psbx-ios screenshot. To catch something brief, such as the hand-off from the launch screen to the first screen, psbx-ios record SECONDS FILE records the simulator at its own frame rate for 1 to 120 seconds into an mp4 in the box (psbx-ios record 5 /work/launch.mp4, then ffmpeg -i /work/launch.mp4 -vf fps=20 /work/f_%03d.png for single frames); psbx-ios screenshot takes about 0.7 seconds a picture, too slow for that. sandbox_shot with record records the box screen, the live stream at about 10 frames a second.
To show a native app to the person, first tap and type through it yourself (artemis-adb tap/type on Android, psbx-ios tap/type on iOS, then sandbox_shot), leave it running on the device at its starting screen and call sandbox_review with open: "box". The card opens the box's screen, where they tap and type on the native app themselves. The review keeps nothing awake: the device freezes and is released with the box by the usual idle rules, and comes back with it when the person opens the review.
Every tool and topic is listed in the tool reference.