Repository navigation
fix(ios): decrypt pushes on release builds by sharing state via the keychain - #14039
Conversation
Encrypted pushes (#12384) are opened by the notification service extension, which needs the active account and the pinned Mac key. The host app wrote both to the group.dev.cmux.ios App Group suite, but the release host App IDs (INTERNAL, BETA, App Store) never had App Groups and API-key-signed dev builds drop it (#13492). Their writes landed in a private per-app plist, the extension found no account, and iOS showed the generic "An agent needs your attention" alert for every push. Both targets already share the host keychain access group in every signing lane, so iOS now stores this state as keychain items there. The Mac keeps its process-local defaults and reads its existing pins. The extension logs which check failed instead of failing silently. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Warning Review limit reachedNext included review available in 4 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe change adds a shared storage abstraction for push account and peer-key state, with platform-specific backends. It also adds reason-specific logging for suppressed notifications in the notification service. ChangesShared push state
Notification suppression logging
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant HostApp
participant SharedStateStorage
participant NotificationService
HostApp->>SharedStateStorage: Store active account and pinned peer key
NotificationService->>SharedStateStorage: Read active account and pinned peer key
NotificationService->>NotificationService: Decrypt notification payload
Merge Risk: ⚪ Minimal · up to The supplied evidence does not establish a reason to block merging. Release-build push decryption still needs on-device validation. Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (23 passed)
Full details: Cmux Swift Actor IsolationExplanation The production diff adds the file-scoped ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The namespace lint rejects an all-static enum; the default storage is derived from the bundle's Info.plist, so it reads as a Bundle property. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1ba90a1 fix(ios): decrypt pushes on release builds by sharing state via the keychain (manaflow-ai#14039) 73f12e5 Regenerate config schema and shortcut docs for toggleFileEditorWordWrap (manaflow-ai#14052) 4dafd99 Keep startup retry in reconnecting state (manaflow-ai#13856) 58bbcfa coderouter: report Server-Timing on every route (manaflow-ai#13976) e5a1d11 Drop the retired staging legacy Subrouter default (manaflow-ai#13946) 727d3f0 ci: fetch previous nightly DMGs by asset id and treat misses as no delta (manaflow-ai#13970) 72490a9 ci: make cross-run product reuse actually adopt products (manaflow-ai#14007) # Conflicts: # .github/workflows/ci-macos.yml # .github/workflows/persistent-macos-compile.yml # .github/workflows/test-e2e.yml # .github/workflows/test-ios.yml
#14039 moved the account marker the notification extension reads into the shared keychain, but the host only wrote it on sign-in or after a protected-data unlock. A launch that restores a cached session never wrote it, so an updated release build kept showing "An agent needs your attention" until the user signed in again. The auth composition now mirrors authenticatedSessionIdentities() into the store for the app's lifetime. The stream yields the current identity first, so a restored session writes the marker immediately, and later sign-in, account switches, and sign-out follow through the same path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Correction: the description says the host rewrites the account marker at launch. It does not. The host only wrote it on sign-in or after a protected-data unlock, so after updating, pushes stayed on the placeholder until the next sign-in. Follow-up: #14110 |
…#14110) * fix(ios): mirror the signed-in account for push decryption at launch #14039 moved the account marker the notification extension reads into the shared keychain, but the host only wrote it on sign-in or after a protected-data unlock. A launch that restores a cached session never wrote it, so an updated release build kept showing "An agent needs your attention" until the user signed in again. The auth composition now mirrors authenticatedSessionIdentities() into the store for the app's lifetime. The stream yields the current identity first, so a restored session writes the marker immediately, and later sign-in, account switches, and sign-out follow through the same path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: import workspace liveness in Codex restore policy * fix: declare the restore observation before the Codex intent check 36c3050 used matchingObservation in the Codex restore-intent check one line before declaring it, so main stopped compiling ("use of local variable 'matchingObservation' before its declaration"). Declare the observation first; the check order and inputs are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Since encrypted pushes landed in #12384 (2026-09-17), every push on INTERNAL, BETA, and App Store builds shows the placeholder "cmux / An agent needs your attention" instead of the agent's title and body. API-key-signed dev phone builds have shown the same since #13492 (2026-09-21).
The notification service extension decrypts each push. To do that it needs the signed-in account ID and the Mac key pinned during pairing. The host app wrote both to the
group.dev.cmux.iosApp Group suite. The host App IDs for those lanes do not have App Groups: in App Store Connect,dev.cmux.app.internal,com.cmux.app, anddev.cmux.app.betalist noAPP_GROUPScapability, and only their.NotificationServiceApp IDs do.cmux-release.entitlementsalso omits the group. An unentitledUserDefaults(suiteName:)writes to a private plist in the app's own container. The extension therefore found no account, returned empty content, and iOS fell back to the payload's generic alert. On the reporter's iPhone, INTERNAL's container holdsLibrary/Preferences/group.dev.cmux.ios.plistwithcmux.activeAccountID.dev.cmux.app.internal. The App Store app has the same private file.The host app and its extension share the host keychain access group (
TEAMID.<host bundle id>) in every signing lane, and the extension already reads the push private key from that group. iOS now stores the account marker and Mac key pins there as keychain items, behind a smallPhonePushSharedStateStorageprotocol. The Mac keeps process-localUserDefaultsand still reads pins written in the old registry format. The extension also logs which check failed (account_mismatch,sender_not_pinned,decrypt_failed, and so on). Until now every failure was silent.After upgrading, the host rewrites the account marker at launch and re-pins the Mac key on its next attach. No manual migration is needed. #13741 (2026-09-22) is unrelated to the storage bug. It fans each push out to every paired app, which increased the number of placeholder banners.
Testing
swift testinPackages/macOS/CmuxPhonePush: 9 tests pass. The newPhonePushSharedStateTestscover these cases:CMUXHostBundleIdentifier) read them and decrypt the title, body, and expiry.swift build --triple arm64-apple-ios17.0).ios/NotificationService/NotificationService.swifttype-checks against it.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Fixes encrypted push notifications on INTERNAL, BETA, App Store, and API-key-signed dev builds showing the generic "An agent needs your attention" placeholder instead of the real title and body. The notification service extension needed the account ID and Mac key pins that the host app wrote to an App Group suite, but those signing lanes lack App Groups capabilities, so the extension found no account and iOS fell back to the payload's generic alert.
PhonePushSharedStateStorageprotocol exposed as aBundleproperty.UserDefaultsand still reads pins written in the old registry format.account_mismatch,sender_not_pinned,decrypt_failed,expired) instead of failing silently.Written for commit e0a97fa. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes