pair-e2e: run the Mac app under the rig's own keychain
Root cause
The pair-e2e Mac pairing page stayed "preparing" with no code on a fleet GUI Mac (steps 59c5f9eb8bfb8976560ed476, c9c9b027f31d54119d869a81, e2b11b0cff9e8634d7530c87). The evidence from #27345 (step e2b11b0c) names the call that prompts.
IPhoneHostService.reconcile() IPhoneHostService.swift:419
MobileHostMacIdentityStore.loadOrCreate() MobileHostMacIdentity.swift:76
MobileHostMacIdentityStore.keychainAdd(_:) MobileHostMacIdentity.swift:106
SecItemAdd -> SecItemAdd_osx -> SecKeychainItemCreateFromContent
StorageManager::defaultKeychainUI -> StorageManager::makeLoginAuthUI
AuthorizationCopyRights (blocked for the whole sample)
- authd asks for
system.keychain.create.loginkc, and SecurityAgent runsloginKC:queryCreate. security default-keychain -d userprints "A default keychain could not be found": the worker's console account has no login keychain.- So the app's first write to the file keychain opens the "create a login keychain" dialog. The store's
interactionNotAllowedcontext doesn't cover that dialog, and nobody answers it. - hostd never starts.
DeviceHub.loadIdentity(DeviceKeychain.read) waits behind the dialog on the StorageManager lock.
Change (rig only)
scripts/ci/cmux_next_pair_e2e.py now runs the Mac app under the rig's own file keychain (RigKeychain):
- Before launch: it records the user domain's search list and default keychain in a 0600 file, after reading both successfully. It refuses to run, before changing anything, if either read fails. It then creates
rig.keychain-dbnext to the tagged app with a random password (passed in argv, never in a file or log), puts it first in the search list, and makes it the default. - In the rig's
finallyand in--forget-tag(the sim script's EXIT, for a run cut short):- It puts the list and the default back. An unset default is unset again, and a default someone else set during the run is left alone.
- It reads both back.
- Only after they read back does it delete this run's items from the rig's keychain, then the keychain, then the record. A failed restore keeps the record so the next call retries.
- It never reads, changes, or deletes an item in the account's own keychains.
forget_keychainnow deletes only inside the rig's keychain, and itssecuritycalls have timeouts.
No Swift changes, and the release path is untouched.
Later fixes on this branch (found by the fleet runs)
- The step's own HOME. The fleet worker runs a step with
HOME=<job>/home, which is empty. The Security framework keeps the user's keychain preferences under$HOME/Library/Preferences, and without that foldersecurity ... -sexits 0 and keeps nothing (step8703f0e6bd2dcb9d98a19319). That empty HOME is also why the app had no default keychain in the first place.start()now creates that folder and reads back what it set. So in a step, the list and default the rig changes belong to the step's HOME, not the console account's. - Restore.
security ... -swith no keychain argument is a no-op, so an empty list and an unset default are restored bydelete-keychaininstead. - The pairing code reaches the phone. The phone first dials at launch, before it has the code, so the rig restarts the app with the code.
simctl openurlshows an "Open in cmux?" alert that nothing answers (stepsd7afa7349ed59cb5603acf70,c8c0dec1495fb4e584ad2b9e). On a ref whose DEBUG phone app takesCMUX_MOBILE_PAIR_URL(#27445, held for the Swift hold), the code goes in the launch environment and never in argv or a log. Other refs keepopenurl. - The rig's Mac name. The rig's Mac is listed as
0 e2e rig Mac(DEBUG name override), so it sorts first in the phone's Settings > Devices list and no host name appears in the captures. - No token in error output. A failed
openurlor launch with the code raises without the link.
Proof: on a branch that combines this PR with #27445 (p0m/pair-e2e-proof), step 423a0baebf9379c6d48ee0ec on a fleet GUI Mac logged phone 70d27bfc paired from this Mac's pairing code, 2 s after launch, with no Allow prompt and no seed.
Review: Codex gpt-6-astra (high, fast) reviewed every head and said SHIP at 7dcf0e2d, 63fc30fe, 2b98b3f5 and 33829caa, after fixing each BLOCK. The last commit (4981c6cb, the Mac name) adds one DEBUG environment variable.
Risk: low. It's a CI rig on fleet CI Macs only (fleet-only guard). While a pair-e2e step runs, the console account's default keychain is the rig's.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the pair-e2e Mac pairing page staying "preparing" by running the Mac app under the rig's own file keychain. A fleet step runs with its own empty HOME, so the console account has no keychain preferences under $HOME/Library/Preferences; without a default keychain, the app's first keychain write prompts a "create login keychain" dialog that nobody answers, and hostd never starts.
Also fixes pairing when the page shows a code but the phone never presents it: the phone's dial happened at launch, before the app had the code, so its auth waited on the Mac's Allow and the later-opened link was never presented. The rig now ends the app before opening the link, so the Mac answers the earlier auth with the code; on a ref whose DEBUG phone app takes a pairing code at launch, the code is passed in the app's launch environment instead of via openurl (which makes the simulator ask "Open in cmux?" that nothing answers). The openurl call and the launch with the code are bounded and raise without exposing the link's token, the code, or argv.
Rig keychain
- Runs only under a step's own HOME; an account's HOME is refused, so two runs can never record or restore each other's keychains.
- Creates
rig.keychain-dbnext to the tagged app, plus~/Library/Preferencesfor the step HOME, puts the keychain first in the user search list and makes it the default, then restores both after the run. - Restores the account's list and default first in every cleanup path; only a recorded non-empty list or set default is restored, and empty list and unset default come back via
delete-keychain; the record is kept until the restore reads back correctly. - Names the rig's Mac "0 e2e rig Mac" via a DEBUG override so it sorts first in the phone's Devices list, keeping the host's name out of captures.