Skip to content
Closed
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.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).

Expand Down
14 changes: 11 additions & 3 deletions docs/irx-soak-runbook.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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 <device id from device_open>)"
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 <udid> <bundle> data` + `Documents/irx-journal.jsonl`.
`simlock simctl get_app_container <udid> <bundle> data` + `Documents/irx-journal.jsonl`.

## Soak

Expand All @@ -39,5 +44,8 @@ python3 scripts/irx-soak.py --tag irx --udid <udid> --bundle-id <ios-bundle> \
--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.
16 changes: 12 additions & 4 deletions ios/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <subcommand> <udid> ...`, which refuses create, erase and delete. Run `simlock instructions` for Simlock's own rules.

`scripts/simlock-udid.sh <device id>` 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
Expand All @@ -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 <tag>`; with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <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-<slug>`, 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 <tag> --simulator-id <simlock-udid>`; with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <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 <tag> [--device-id <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.

Expand All @@ -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

Expand All @@ -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.
62 changes: 45 additions & 17 deletions ios/scripts/reload.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ set -euo pipefail

usage() {
cat <<'EOF'
Usage: ios/scripts/reload.sh --tag <tag> [--simulator <name>] [--simulator-id <id>] [--no-launch]
ios/scripts/reload.sh --tag <tag> --device [--device-id <id>] [--device-name <name>] [--team <team-id>] [--no-launch]
Usage: ios/scripts/reload.sh --tag <tag> --simulator-id <simlock-udid> [--no-launch]
ios/scripts/reload.sh --tag <tag> --simulator-id <simlock-udid> --device [--device-id <id>] [--device-name <name>] [--team <team-id>] [--no-launch]
ios/scripts/reload.sh --tag <tag> --device-only [--device-id <id>] [--device-name <name>] [--team <team-id>] [--no-launch]
ios/scripts/reload.sh --tag <tag> --simulator-only
ios/scripts/reload.sh --tag <tag> --simulator-id <simlock-udid> --simulator-only

Build, install, and launch the cmux iOS app with an isolated tag.

Expand All @@ -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-<slug>"), 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 <UDID> (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 <name> and the per-tag "cmux-dev-<slug>"
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -389,20 +399,36 @@ 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-<slug>" 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-<slug>".
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")"
[[ -n "$SIMULATOR_ID" ]] || { echo "error: could not resolve the isolated simulator for tag $TAG" >&2; exit 1; }
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"
Expand Down Expand Up @@ -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" \
Expand Down Expand Up @@ -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
Expand Down
24 changes: 17 additions & 7 deletions ios/scripts/toast-debug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <device id from T3 device_open>).
#
# Usage:
# ios/scripts/toast-debug.sh --udid <udid> --bundle-id <id> \
# ios/scripts/toast-debug.sh --udid <simlock-udid> --bundle-id <id> \
# [--style info|success|warning|failure] [--title <t>] --message <m> \
# [--icon <sf-symbol>] [--bottom] [--persistent] [--action <label>] \
# [--key <coalescing-key>]
Expand Down Expand Up @@ -34,19 +35,28 @@ while [[ $# -gt 0 ]]; do
esac
done

if [[ -z "$UDID" || -z "$BUNDLE_ID" ]]; then
if [[ -z "$BUNDLE_ID" ]]; then
echo "error: --udid and --bundle-id are required" >&2
exit 1
fi

# shellcheck source=../../scripts/lib/ios-simlock.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../scripts/lib/ios-simlock.sh"
if ! cmux_simlock_ci_default_set; then
cmux_simlock_require_udid "$UDID" --udid || exit $?
elif [[ -z "$UDID" ]]; then
echo "error: --udid and --bundle-id are required" >&2
exit 1
fi

if [[ "$DISMISS_ALL" -eq 1 ]]; then
xcrun simctl spawn "$UDID" notifyutil -p dev.cmux.toast.debug.dismiss
cmux_simctl "$UDID" spawn "$UDID" notifyutil -p dev.cmux.toast.debug.dismiss
echo "dismissed all toasts on $BUNDLE_ID"
exit 0
fi

if [[ "$DEMO" -eq 1 ]]; then
xcrun simctl spawn "$UDID" notifyutil -p dev.cmux.toast.debug.demo
cmux_simctl "$UDID" spawn "$UDID" notifyutil -p dev.cmux.toast.debug.demo
echo "started toast demo on $BUNDLE_ID"
exit 0
fi
Expand All @@ -70,6 +80,6 @@ EOF
)"

# -string: without it, defaults parses the JSON braces as a plist dictionary.
xcrun simctl spawn "$UDID" defaults write "$BUNDLE_ID" cmux.debug.toast -string "$SPEC"
xcrun simctl spawn "$UDID" notifyutil -p dev.cmux.toast.debug.present
cmux_simctl "$UDID" spawn "$UDID" defaults write "$BUNDLE_ID" cmux.debug.toast -string "$SPEC"
cmux_simctl "$UDID" spawn "$UDID" notifyutil -p dev.cmux.toast.debug.present
echo "presented: $SPEC"
Loading
Loading