From aa1ee9465084cdfdbd80d8bd086bc0d830e0d310 Mon Sep 17 00:00:00 2001 From: Tomer Danan Date: Sat, 14 Nov 2026 12:20:40 +0200 Subject: [PATCH] chore(ios): lease simulators from Simlock instead of the default set The iOS simulator scripts now take a Simlock UDID (--simulator-id, IOS_SIMULATOR_ID, --iphone-sim/--ipad-sim, --udid), route every simctl call for it through `simlock simctl`, and exit with the lease steps when none is given, instead of creating, booting or picking a default-set simulator. ios/scripts/reload.sh builds with -DVTSimulatorSetLocation on Xcode 27+, or for a generic simulator on older Xcode, then installs with simlock simctl. GitHub Actions has no Simlock, so GITHUB_ACTIONS=true keeps the old named and per-tag simulators. scripts/simlock-udid.sh maps a T3 device_open id (the lease id) to its UDID. --- CLAUDE.md | 2 +- docs/irx-soak-runbook.md | 14 +- ios/AGENTS.md | 16 +- ios/scripts/reload.sh | 62 +++++-- ios/scripts/toast-debug.sh | 24 ++- scripts/dev-setup.sh | 16 +- scripts/install-iphone-queue-agent.sh | 2 + scripts/ios-sim-install.sh | 44 +++-- scripts/irx-soak.py | 10 +- scripts/lib/ios-simlock.sh | 158 ++++++++++++++++++ scripts/lib/mobile-attach.sh | 16 +- scripts/lib/mobile-attach.test.mjs | 125 ++++++++++++-- scripts/listauth-gates.py | 15 +- scripts/mobile-dev-launch.sh | 27 ++- scripts/mobile-stability-soak.sh | 30 +++- scripts/mobile-stability-soak/mobile-soak.py | 16 +- .../mobile-stability-soak/resource-monitor.py | 4 +- scripts/simlock-udid.sh | 20 +++ 18 files changed, 510 insertions(+), 91 deletions(-) create mode 100644 scripts/lib/ios-simlock.sh create mode 100755 scripts/simlock-udid.sh diff --git a/CLAUDE.md b/CLAUDE.md index 4afe20f0820a..e8242bdf0f12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,7 @@ The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-.soc Rules that only matter in one part of the tree live next to that code. Read the file before working there; not every agent loads a nested file on its own when launched from the repository root. -- `ios/`, `Packages/iOS/`: `ios/AGENTS.md` (Apple HIG rule, iPhone install and auth gates, iOS and verification capacity on the controller, cross-tag Mac access, dev auth profiles). +- `ios/`, `Packages/iOS/`: `ios/AGENTS.md` (simulators leased from Simlock, never from Xcode's default set; Apple HIG rule, iPhone install and auth gates, iOS and verification capacity on the controller, cross-tag Mac access, dev auth profiles). - `web/` and any cmux Cloud database work: `web/AGENTS.md` (database provider). - `cmux-tui/`: `cmux-tui/AGENTS.md` (hosted verification, Blacksmith Testbox). diff --git a/docs/irx-soak-runbook.md b/docs/irx-soak-runbook.md index 57300e361f5a..3742eab417f7 100644 --- a/docs/irx-soak-runbook.md +++ b/docs/irx-soak-runbook.md @@ -1,6 +1,6 @@ # irx soak runbook (tag irx) -One tagged Mac + one isolated simulator, relay-only, 15 minutes engaged. +One tagged Mac + one Simlock simulator, relay-only, 15 minutes engaged. ## Mac (host) @@ -21,16 +21,21 @@ tail -f /tmp/cmux-irx-journal-mac-irx.jsonl ## Simulator (client) +Lease an iOS simulator with T3 Code's `device_open` on the "Simlock" host, then +install the tagged iOS app on it (`ios/scripts/reload.sh --simulator-id` or +`scripts/ios-sim-install.sh --simulator-id`). + ```bash +UDID="$(scripts/simlock-udid.sh )" SIMCTL_CHILD_CMUX_IRX_ENABLED=1 \ SIMCTL_CHILD_CMUX_IRX_FORCE_RELAY=1 \ scripts/mobile-dev-launch.sh --tag irx --agent --ensure-mac \ - --simulator "cmux-dev-irx" + --simulator-id "$UDID" ``` The sim app registers its own irx binding, mints a pair grant against the Mac's binding on first dial, and connects relay-only. Journal: -`simctl get_app_container data` + `Documents/irx-journal.jsonl`. +`simlock simctl get_app_container data` + `Documents/irx-journal.jsonl`. ## Soak @@ -39,5 +44,8 @@ python3 scripts/irx-soak.py --tag irx --udid --bundle-id \ --minutes 15 ``` +The harness reaches the Simlock simulator through `simlock simctl`; its `idb` +typing addresses Xcode's default device set only, so pass `--no-input` on a +Simlock simulator. Open a workspace with a terminal on the phone first; the harness types `date` every ~30s and screenshots every 60s. PASS criteria are in the script header. diff --git a/ios/AGENTS.md b/ios/AGENTS.md index 63f8e0dc881d..4285c2fbc98c 100644 --- a/ios/AGENTS.md +++ b/ios/AGENTS.md @@ -11,6 +11,14 @@ or explain the remaining verification gap in the PR. The sections below moved here from the repository root `CLAUDE.md`, unchanged. Team dev builds and fleet allocation stay in the root file ("Dev builds on the Mac mini fleet"); nothing here overrides it. Script paths are relative to the repository root. +## Simulators come from Simlock + +Every iOS simulator an agent uses is leased from the Simlock pool, through T3 Code's `device_open` on the host named "Simlock" (`device_list` shows it), and handed back with `device_close`. Never `xcrun simctl create`, never boot a simulator, and never pick or reuse one from Xcode's default device set. Simlock keeps its simulators in its own device set (`~/.simlock/devices/ios`, or `SIMLOCK_IOS_DEVICE_SET`), which plain `xcrun simctl` cannot see: reach the leased simulator with `simlock simctl ...`, which refuses create, erase and delete. Run `simlock instructions` for Simlock's own rules. + +`scripts/simlock-udid.sh ` turns the device id from `device_open` (the Simlock lease id) into the simulator UDID. Pass that UDID to the scripts: `ios/scripts/reload.sh --simulator-id`, `scripts/mobile-dev-launch.sh --simulator-id`, `scripts/dev-setup.sh --simulator-id`, `scripts/ios-sim-install.sh --simulator-id` (all four also read `IOS_SIMULATOR_ID`), `scripts/mobile-stability-soak.sh --iphone-sim/--ipad-sim`, and `ios/scripts/toast-debug.sh --udid`. Without a Simlock UDID these shell scripts print the lease steps and exit nonzero instead of choosing a simulator. `scripts/irx-soak.py`, `scripts/listauth-gates.py` and the soak's `mobile-soak.py` do not check; give them a Simlock `--udid` yourself, and pass `--no-input`, since their `idb` typing cannot reach a Simlock simulator. All of them route simctl calls for a Simlock UDID through `simlock simctl`, and `ios/scripts/reload.sh` builds with `-DVTSimulatorSetLocation` on Xcode 27 or later (`generic/platform=iOS Simulator` plus `simlock simctl install` on older Xcode). Only GitHub Actions (`GITHUB_ACTIONS=true`, no Simlock) keeps the old named and per-tag default-set simulators. + +Not yet converted, so do not run them on a shared machine: `scripts/run-iroh-release-gate.sh`, `scripts/run-iroh-direct-transport-gate.sh`, `ios/scripts/appstore-shots.sh` and `ios/fastlane` create or erase their own simulators; `ios/scripts/appstore-shots-real.sh`, `ios/scripts/capture-streamed.py`, `idb`, `axe` and `xcodebuildmcp` address the default device set only, so the stability soak's UI checks fail where `xcodebuildmcp` is installed. + ## iOS UI follows the Apple HIG `Packages/iOS/AGENTS.md` requires consulting the Apple Human Interface @@ -19,7 +27,7 @@ Guidelines for any iOS UI change and citing the page in the PR. It applies to ## iOS builds open on the iPhone by default -Any work verified by opening the iOS app installs BOTH an isolated-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use a provisioned, verified controller recipe for the team simulator dev-build leg, following the current HQ runbook. A macOS CMUX build proves neither iOS compilation nor installation. Do not use `ios/scripts/reload-cloud.sh` while it allocates through maclease; report a missing controller recipe. Standalone contributors may use `ios/scripts/reload.sh --tag `; with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id ` still overrides (`xcrun devicectl list devices`). Physical iPhone builds always select the `personal` auth profile. Agent-driven Simulator verification always selects `agent`. Both named profiles live in `~/.secrets/cmuxterm-dev.env`; neither may fall back to the other. The simulator leg uses the tag's own isolated device `cmux-dev-`, created on demand; do not target a shared or user-visible simulator. +Any work verified by opening the iOS app installs BOTH a Simlock-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use a provisioned, verified controller recipe for the team simulator dev-build leg, following the current HQ runbook. A macOS CMUX build proves neither iOS compilation nor installation. Do not use `ios/scripts/reload-cloud.sh` while it allocates through maclease; report a missing controller recipe. Standalone contributors may use `ios/scripts/reload.sh --tag --simulator-id `; with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id ` still overrides (`xcrun devicectl list devices`). Physical iPhone builds always select the `personal` auth profile. Agent-driven Simulator verification always selects `agent`. Both named profiles live in `~/.secrets/cmuxterm-dev.env`; neither may fall back to the other. The simulator leg runs only on a simulator leased from Simlock (see "Simulators come from Simlock"); without `--simulator-id` the reload exits before building, and `--device-only` skips the simulator leg. Do not target a shared or user-visible simulator. **Every phone install MUST be authenticated before handoff. Installed-but-signed-out is a failed install.** A tagged bundle id can retain an older account, so every authenticated launch clears that tagged session, signs both surfaces into the selected profile, verifies the exact tagged Mac account through `auth status`, then mints the pairing ticket. The iPhone auth gate passes only after the same-account host accepts the phone RPC and emits `mobile.rpc.ready`. `scripts/verify-iphone-auth.sh --tag [--device-id ]` repeats the Mac-account check, relaunches the phone without credentials, and passes only when persisted phone state reconnects. Never install with raw `devicectl device install app`, and never pass `--no-sign-in`/`--no-attach`/`--no-setup` for a dogfood build. The scripts refuse those device paths unless a human sets `CMUX_ALLOW_UNAUTHENTICATED_INSTALL=1`. If setup fails, report the gate reason and exact retry command. @@ -39,8 +47,8 @@ runners, including Blacksmith. Do not migrate those workflows to the mini fleet. A missing controller verification recipe is a migration gap, not authorization to revive maclease or run an unscheduled SSH build. For an explicitly authorized -local simulator verification, use an isolated device, never an existing visible -or user-owned simulator, and keep at most three local simulators booted. +local simulator verification, lease a simulator from Simlock, never an existing +visible or user-owned simulator, and give it back with `device_close` when done. ## Cross-tag Mac access for DEV iPhone builds @@ -65,4 +73,4 @@ iPhone build; do not mint a shared tag or rebuild the phone for that. ## iOS dev auth -`~/.secrets/cmuxterm-dev.env` is the only mobile dev credential file. `CMUX_DOGFOOD_STACK_*` is the `personal` profile for physical iPhone dogfood. `CMUX_UITEST_STACK_*` is the `agent` profile for isolated Simulators. Run `scripts/setup-team-dev.sh` once to verify and merge the personal pair without deleting the agent pair. Use `scripts/mobile-dev-launch.sh --check-auth-contract --auth-profile personal` or `--auth-profile agent` for a mutation-free preflight. Never substitute one profile when the requested profile is incomplete. +`~/.secrets/cmuxterm-dev.env` is the only mobile dev credential file. `CMUX_DOGFOOD_STACK_*` is the `personal` profile for physical iPhone dogfood. `CMUX_UITEST_STACK_*` is the `agent` profile for Simlock Simulators. Run `scripts/setup-team-dev.sh` once to verify and merge the personal pair without deleting the agent pair. Use `scripts/mobile-dev-launch.sh --check-auth-contract --auth-profile personal` or `--auth-profile agent` for a mutation-free preflight. Never substitute one profile when the requested profile is incomplete. diff --git a/ios/scripts/reload.sh b/ios/scripts/reload.sh index de2d1d7841da..9c503feaf9c7 100755 --- a/ios/scripts/reload.sh +++ b/ios/scripts/reload.sh @@ -3,10 +3,10 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: ios/scripts/reload.sh --tag [--simulator ] [--simulator-id ] [--no-launch] - ios/scripts/reload.sh --tag --device [--device-id ] [--device-name ] [--team ] [--no-launch] +Usage: ios/scripts/reload.sh --tag --simulator-id [--no-launch] + ios/scripts/reload.sh --tag --simulator-id --device [--device-id ] [--device-name ] [--team ] [--no-launch] ios/scripts/reload.sh --tag --device-only [--device-id ] [--device-name ] [--team ] [--no-launch] - ios/scripts/reload.sh --tag --simulator-only + ios/scripts/reload.sh --tag --simulator-id --simulator-only Build, install, and launch the cmux iOS app with an isolated tag. @@ -16,8 +16,16 @@ enabled automatically; --simulator-only opts out explicitly. Without a configured default, this reloads only the simulator unless --device is given. If the iPhone is unreachable, the signed build is parked in the offline install queue (scripts/iphone-install-queue.sh) and auto-installs when the phone -reconnects. Unless a simulator is named explicitly, the simulator leg uses the -tag's own isolated device ("cmux-dev-"), created on demand. +reconnects. + +The simulator leg runs on a simulator leased from Simlock, never on one from +Xcode's default device set. Lease one with T3 Code's device_open on the +"Simlock" host, turn its device id into a UDID with scripts/simlock-udid.sh, +and pass --simulator-id (or IOS_SIMULATOR_ID). Without one, the reload +exits before building; --device-only skips the simulator leg. Every simctl call +for the leased simulator goes through `simlock simctl`. On GitHub Actions, +which has no Simlock, --simulator and the per-tag "cmux-dev-" +simulator still work. When a trusted phone leg is enabled, the simulator app is installed but not launched. The simulator's agent auto-pair would replace the phone's personal @@ -344,6 +352,8 @@ fi # sanitize_tag call below. # shellcheck source=../../scripts/lib/mobile-attach.sh source "$IOS_DIR/../scripts/lib/mobile-attach.sh" +# shellcheck source=../../scripts/lib/ios-simlock.sh +source "$IOS_DIR/../scripts/lib/ios-simlock.sh" # Fail before building if the tag would collide with a fallback/reserved identity # or exceed the cloud presence limit. if ! cmux_attach_validate_dev_tag "$TAG"; then @@ -389,10 +399,22 @@ if [[ "$RELOAD_DEVICE" -eq 1 && "$PROD_AUTH" -eq 0 ]] \ exit 2 fi -# Isolated per-tag simulator by default: unless the caller explicitly picked a -# simulator (flag or IOS_SIMULATOR_NAME/IOS_SIMULATOR_ID), resolve or create -# "cmux-dev-" so concurrent agent sessions never share a simulator. -if [[ "$RELOAD_SIMULATOR" -eq 1 && "$SIMULATOR_EXPLICIT" -eq 0 ]]; then +# The simulator leg needs a booted Simlock simulator. Check it before any build +# work, so a missing lease costs nothing. +if [[ "$RELOAD_SIMULATOR" -eq 1 ]] && ! cmux_simlock_ci_default_set; then + simlock_status=0 + cmux_simlock_require_udid "$SIMULATOR_ID" --simulator-id || simlock_status=$? + if [[ "$simlock_status" -ne 0 ]]; then + echo "To build only for the iPhone, pass --device-only instead." >&2 + exit "$simlock_status" + fi + SIMULATOR_NAME="Simlock simulator" +fi + +# GitHub Actions only (no Simlock there): isolated per-tag simulator by default. +# Unless the caller explicitly picked a simulator (flag or +# IOS_SIMULATOR_NAME/IOS_SIMULATOR_ID), resolve or create "cmux-dev-". +if [[ "$RELOAD_SIMULATOR" -eq 1 && "$SIMULATOR_EXPLICIT" -eq 0 ]] && cmux_simlock_ci_default_set; then # shellcheck source=../../scripts/lib/ios-sim-isolate.sh source "$IOS_DIR/../scripts/lib/ios-sim-isolate.sh" SIMULATOR_ID="$(cmux_ios_isolated_sim_udid "$TAG_SLUG")" @@ -400,9 +422,13 @@ if [[ "$RELOAD_SIMULATOR" -eq 1 && "$SIMULATOR_EXPLICIT" -eq 0 ]]; then SIMULATOR_NAME="$(cmux_ios_isolated_sim_name "$TAG_SLUG")" fi -DESTINATION="platform=iOS Simulator,name=$SIMULATOR_NAME" -if [[ -n "$SIMULATOR_ID" ]]; then - DESTINATION="platform=iOS Simulator,id=$SIMULATOR_ID" +if cmux_simlock_is_udid "$SIMULATOR_ID"; then + cmux_simlock_xcodebuild_destination "$SIMULATOR_ID" + DESTINATION_ARGS=("${CMUX_SIMLOCK_DESTINATION_ARGS[@]}") +elif [[ -n "$SIMULATOR_ID" ]]; then + DESTINATION_ARGS=(-destination "platform=iOS Simulator,id=$SIMULATOR_ID") +else + DESTINATION_ARGS=(-destination "platform=iOS Simulator,name=$SIMULATOR_NAME") fi MOBILE_DEV_LAUNCH="$IOS_DIR/../scripts/mobile-dev-launch.sh" DEVICE_PROCESS_HELPER="$IOS_DIR/../scripts/ios-device-process.sh" @@ -756,7 +782,7 @@ reload_simulator() { -workspace "$WORKSPACE" \ -scheme "$SCHEME" \ -configuration Debug \ - -destination "$DESTINATION" \ + "${DESTINATION_ARGS[@]}" \ -derivedDataPath "$DERIVED_DATA" \ ${IROH_RELAY_POLICY_BUILD_ARGS[@]+"${IROH_RELAY_POLICY_BUILD_ARGS[@]}"} \ CMUX_APP_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ @@ -804,13 +830,15 @@ PY )" fi - xcrun simctl boot "$SIM_ID" >/dev/null 2>&1 || true - xcrun simctl install "$SIM_ID" "$APP_PATH" + if ! cmux_simlock_is_udid "$SIM_ID"; then + xcrun simctl boot "$SIM_ID" >/dev/null 2>&1 || true + fi + cmux_simctl "$SIM_ID" install "$SIM_ID" "$APP_PATH" if [[ "$LAUNCH" -eq 1 && "$SIMULATOR_LAUNCH" -eq 1 ]]; then - xcrun simctl terminate "$SIM_ID" "$BUNDLE_ID" >/dev/null 2>&1 || true + cmux_simctl "$SIM_ID" terminate "$SIM_ID" "$BUNDLE_ID" >/dev/null 2>&1 || true if [[ "$NO_SETUP" -eq 1 || "$NO_SIGN_IN" -eq 1 ]]; then - xcrun simctl launch "$SIM_ID" "$BUNDLE_ID" >/dev/null + cmux_simctl "$SIM_ID" launch "$SIM_ID" "$BUNDLE_ID" >/dev/null elif ! auto_setup_launch simulator "$SIM_ID"; then echo "error: installed $BUNDLE_ID, but signed setup failed; refusing an unpaired fallback launch" >&2 echo "error: repair the tagged Mac/Iroh route, or pass --no-attach, --no-sign-in, or --no-setup explicitly" >&2 diff --git a/ios/scripts/toast-debug.sh b/ios/scripts/toast-debug.sh index b516c772573e..f5cc1d78f540 100755 --- a/ios/scripts/toast-debug.sh +++ b/ios/scripts/toast-debug.sh @@ -2,10 +2,11 @@ # Present a toast inside a RUNNING cmux iOS DEBUG build on a simulator, with # no touch input (drives ToastDebugTrigger via defaults + a darwin # notification). DEBUG builds only; physical devices are not supported -# (simctl spawn is simulator-only). +# (simctl spawn is simulator-only). The simulator is a Simlock lease: pass its +# UDID (scripts/simlock-udid.sh ). # # Usage: -# ios/scripts/toast-debug.sh --udid --bundle-id \ +# ios/scripts/toast-debug.sh --udid --bundle-id \ # [--style info|success|warning|failure] [--title ] --message \ # [--icon ] [--bottom] [--persistent] [--action