PM-26250 Explore options to enable direct importer for mac app store build#17479
PM-26250 Explore options to enable direct importer for mac app store build#17479
Conversation
|
Great job! No new security vulnerabilities introduced in this pull request |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #17479 +/- ##
==========================================
- Coverage 47.10% 46.93% -0.17%
==========================================
Files 3951 3952 +1
Lines 119755 119872 +117
Branches 18349 18328 -21
==========================================
- Hits 56413 56267 -146
- Misses 59107 59363 +256
- Partials 4235 4242 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…porter-for-mac-app-store-build
…mac-app-store-build' of github.com:bitwarden/clients into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build merge conflict resolution
…porter-for-mac-app-store-build
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE This PR enables the Chromium direct importer for Mac App Store (sandboxed) builds by introducing an Objective-C Code Review DetailsNo new findings. |
There was a problem hiding this comment.
It seems odd put swift into a crate called a objc, but also, do we have to add another language? The build file for objc was so simple, and the interface contained an easy to use async/future-compatible API with error handling and allowed complex data structures by using JSON serialization. Aren't all Apple APIs compatible with objc? Can't we just use that?
There was a problem hiding this comment.
Aren't all Apple APIs compatible with objc? Can't we just use that?
I can check, Apple has introduced APIs available in Swift that are not backwards compatible with objc. Swift has something like FFI (even cleaner) for working with any objc code, but the reverse is not guaranteed to be true.
This Swift code is used to display an NSOpenPanel(), I have a feeling that existed in the objc days, but I am not sure what the equivalent of DispatchQueue.main.sync is, and a few of the other Apple APIs used here, I will need to check on.
There was a problem hiding this comment.
@coroiu I ran the Swift file by Claude and all of the APIs have equivalents in objc, I'll get started on making the change.
There was a problem hiding this comment.
@coltonhurst You're currently the only users of the objc crate and its runCommand framework. In hindsight I admit that the JSON approach might not have been the best, but there's still some callback-to-async and string interop for automatically freeing memory (without depending on libc) that I feel is worth using/saving. Does Autofill have any current plans for improving the objc crate? Would you also prefer swift?
There was a problem hiding this comment.
@coroiu thanks for the mention. I am not intimately familiar with this area of the code base, but I agree with your points.
I think keeping only Objective-C code in the objc directory is a good move. It looks like that might have been updated in this PR since your message and my time in looking at this? (Thanks for your patience!)
Regarding the JSON pattern... I don't think we should deviate from an existing pattern, creating two different patterns, unless there is a plan to transition the old stuff, or if there is a specific reason we need to do it a different way. This is just my preference; I would rather code patterns be consistent (even if not the "perfect" solution), rather than "right" stuff that is not consistent and less easily understood to new readers. So... the tldr for this point is it might be worth looking into a way to do this in a way that follows the existing pattern, rather than creating new patterns.
There was a problem hiding this comment.
@coroiu please let me know if you believe anything currently in this PR deviates from the preferences mentioned above; I believe I have addressed all concerns and have a working MAS build running locally.
There was a problem hiding this comment.
@harr1424 Have you looked at how the existing objc code looks, how it is executed, how parameters are sent, how data is returned and how memory is cleared? I see significant differences:
- Functions referenced directly using
extern "C" - Parameters are constructed manually in every consuming function using unsafe rust
- Return data is converted manually in every consuming function using unsafe rust
- Memory is manually cleared
- Libc is added as a dependency to clear memory
The reason I asked @coltonhurst is because we might very well want to move over to referencing functions directly, I think that would make writing cross-platform passkey autofill code easier, but in that case I think we should be more restrictive with where we put unsafe rust, isolate it more, and make a plan for how to support async functions.
Seeing as we do not want to deviate from existing patterns then this code, @harr1424, needs to be convert to using the JSON command structure and be called using run_command instead of referenced directly using extern "c" blocks. We should also remove libc as a dependency since run_command will manage the memory for you. run_command will also add some exception handling for you which I think might be missing in the current version of your code (not sure if it's actually able to throw anything, but doesn't hurt to be safe).
There was a problem hiding this comment.
@harr1424 do you mind letting me know when the files relevant to this convo are ready for review? (That may be now, I'm not sure.)
I also think it would be nice to also add some architecture documentation to our contributing docs around the pattern so it's at least documented more. Any ideas @coroiu?
There was a problem hiding this comment.
@coltonhurst my apologies I've moved this PR back to draft status pending Oscar's review. I'll reach out once it is ready for your re-review.
There was a problem hiding this comment.
@harr1424 no problem! I don't think I'm a required code owner anyways, just was getting involved!
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
…porter-for-mac-app-store-build
|
Hello @harr1424 et. al, I'm field testing a new performing-multi-agent-code-review skill and, after seeing your review hit my Github Notifications, I gave it a local run against the PR. The findings are below for your analysis. Please incorporate the findings that are true signals and let me know if there are egregious errors that need to be corrected with planned tuning to a Sonnet Code Review: PM-26250 Explore options to enable direct importer for mac app store build (#17479)Date: 2026-05-05 | Reviewed by: Claude Code | Model: Sonnet Summary
This PR enables the Chromium-based direct importer for Mac App Store sandboxed builds by adding a security-scoped bookmark flow: an Findings
|
| Severity | Count |
|---|---|
| 🛑 Blocker | 0 |
| 0 | |
| ♻️ Refactor | 9 |
The PR enables the Chromium direct-importer flow for Mac App Store (sandboxed) desktop builds by introducing a security-scoped-bookmark request flow on the ObjC side and threading a new mas_build boolean through the napi → Rust → IPC → Angular call chain. No blocker- or important-tier issues survived validation: the most colorful claims (panic-in-Drop, mismatched-NSURL security-scope leak) were either dismissed by the validation pass or downgraded to refactor once the runtime guarantees and Apple's per-counter (not per-instance) contract were considered. What remains is structural debt: parallel browser arrays in three files, an inconsistent source of truth for the isMas flag spread across renderer, preload, and main, an unused bookmark field papered over with #[allow(dead_code)], and shared library code that reaches for desktop-only i18n keys. None of these block merge, but each is worth resolving before the next change in this area.
Findings
♻️ Refactor
Parallel browser arrays will drift when Chromium browsers are added
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:7-15
Caught by: Architecture agent
Details
BROWSER_BUNDLE_IDS (sandbox.rs lines 7-15) duplicates the browser-name keys already enumerated in SUPPORTED_BROWSERS (apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs lines 14-43) and KEYCHAIN_CONFIG (macos.rs lines 68-104). Adding or removing a Chromium-based browser now requires synchronized edits across three parallel arrays in two files, with the only coupling being a string compare on the name field. The new sandbox path looks the bundle ID up by name; the natural home for that data is on BrowserConfig itself (e.g. an optional bundle_id: Option<&'static str> field next to data_dir) so the macOS browser table remains the single source of truth.
Inconsistent source of truth for isMas across IPC handlers
apps/desktop/src/main/tools/import/chromium-importer.service.ts:9-25
Caught by: Architecture agent
Details
Within a single class, two different patterns are used for the same mas_build parameter that is threaded all the way through napi → Rust:
chromium_importer.getMetadataacceptsisMasfrom the renderer (apps/desktop/src/app/tools/import/desktop-import-metadata.service.tscallsthis.system.environment.isMacAppStore()and ships it across IPC; preload.ts forwards it).chromium_importer.requestBrowserAccess,getAvailableProfiles, andimportLoginsignore any renderer hint and derive the value in the main process by callingisMacAppStore()fromapps/desktop/src/utils.ts.
Pick one. The main-process isMacAppStore() utility is already used by every other consumer in apps/desktop/src/main/ and is the established pattern for this signal. Threading the boolean through preload and the renderer for getMetadata only adds a parameter to the IPC contract that the main process does not trust for the other three handlers — and the preload-side change in apps/desktop/src/app/tools/preload.ts only updated getMetadata and requestBrowserAccess to accept new args, leaving the other two preload signatures unchanged, which makes the asymmetry visible in three layers.
Shared lib component depends on desktop-only i18n keys
libs/importer/src/components/chrome/import-chrome.component.ts:181-205
Caught by: Code quality agent
Details
translateValidationError (in libs/importer) calls i18nService.t("chromiumImporterBrowserNotInstalled", ...) and i18nService.t("browserAccessDenied"). Both keys are added only to apps/desktop/src/locales/en/messages.json in this PR — they don't exist in libs/common or apps/web/apps/browser locales.
This violates the apps/libs boundary documented in the repo's CLAUDE.md ("Strict boundaries must be maintained between apps and libraries"). Today the shared component is wired only by the desktop callback so the codepath isn't reached in web/browser, but a sibling app that wires onLoadProfilesFromBrowser will silently render the raw key. Move the strings into a shared locale, or move the translation step into the desktop caller and have the shared component pass through already-translated messages.
Inconsistent MAS-flag origin between IPC handlers (duplicate framing)
apps/desktop/src/main/tools/import/chromium-importer.service.ts:8-25
Caught by: Code quality agent
Details
Three of four IPC handlers (requestBrowserAccess, getAvailableProfiles, importLogins) call isMacAppStore() themselves in the main process and ignore any renderer-supplied flag. getMetadata, however, accepts isMas from the renderer. Pick one. The main-process isMacAppStore() utility is the established pattern for this signal; threading the boolean through preload and the renderer for getMetadata only adds a parameter to the IPC contract that the main process does not trust for the other three handlers. (Same root issue as the architecture agent's finding above; surfaced here from the renderer-side perspective.)
ScopedBrowserAccess::Drop spawns fire-and-forget tokio task
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:147-164
Caught by: Code quality agent
Details
Drop calls tokio::task::spawn to send a stop_access cleanup. Two concerns:
tokio::task::spawnpanics if no current runtime is available. In theDroppath that becomes a double-unwind and aborts the process. The construction sites today are allasync fn resume()calls, so the runtime is reliably present when the value is constructed and dropped — but a future refactor that constructs/dropsScopedBrowserAccesssynchronously (e.g., for testing) would silently introduce abort-in-Drop.- The returned
JoinHandleis discarded and the result ofdesktop_objc::run_commandis ignored (let _ = ...). Cleanup is fire-and-forget; if the runtime tears down before the task runs, the security-scoped resource is not released until process exit.
Recommend an explicit async fn close(self) consumed by import_logins before returning, with Drop retained only as a best-effort fallback gated by tokio::runtime::Handle::try_current() so it cannot panic.
Dead bookmark field in RequestAccessResponse
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:32-36
Caught by: Code quality agent
Details
#[derive(Debug, Deserialize)]
struct RequestAccessResponse {
#[allow(dead_code)]
bookmark: String,
}The field is parsed from the ObjC response but never read — the only consumer matches CommandResult::Success { .. } with ... The #[allow(dead_code)] annotation papers over a true unused field added by this PR rather than removing it. Either delete the field (the ObjC side already saves the bookmark to NSUserDefaults, so there's no need to round-trip it across FFI) or have a consumer actually read it.
ScopedBrowserAccess::Drop calls tokio::task::spawn (duplicate framing)
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:144
Caught by: Bug analysis agent
Details
Same Drop + tokio::task::spawn concern as flagged by the code quality and security agents. The bug-analysis perspective: the JoinHandle is discarded and let _ = desktop_objc::run_command(input_json).await swallows any error, so any failure to release the security-scoped resource is invisible. In practice the runtime is always present when the value is dropped (because the construction site is async), so this lands at refactor severity rather than important. See the quality-3 finding above for the suggested fix.
preload.requestBrowserAccess signature does not forward isMas
apps/desktop/src/app/tools/preload.ts:12
Caught by: Bug analysis agent
Details
The napi .d.ts declares requestBrowserAccess(browser: string, masBuild: boolean): Promise<void> with masBuild required. The preload bridge sends only browser:
requestBrowserAccess: (browser: string): Promise<void> =>
ipcRenderer.invoke("chromium_importer.requestBrowserAccess", browser),The ipcMain handler in chromium-importer.service.ts calls chromium_importer.requestBrowserAccess(browser, isMacAppStore()), so functionally this works today. However, the inconsistent shape — getMetadata forwards isMas from the renderer, requestBrowserAccess/getAvailableProfiles/importLogins do not — is the same root issue surfaced by the architecture agent. A future change that wires requestBrowserAccess to honor a renderer-supplied flag will silently break.
Drop cleanup is fire-and-forget — implicates P05 (controlled access)
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:149
Caught by: Security & logic agent
Details
The only mechanism intended to release the security scope at the end of an import is Drop → tokio::task::spawn → stop_access. The result is discarded (let _ = ...), so any failure to release the scope is silent. The doc comment in import_logins ("releases the scope on Drop after the reads complete") advertises a contract that is not reliably honored.
The runtime presence guarantee at the construction site means this does not cross the Important bar in practice — but the cleanup pattern is the kind of P05 (Controlled access) concern worth tightening: an explicit awaited close() with the panic-prone Drop reduced to a guarded fallback would make the contract observable.
Reviewed and Dismissed
🔍 5 initial findings dismissed after validation
Strict path-equality check returns generic error on mis-selection
apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access_manager.m:65-71
Caught by: Code quality agent
Original severity:
Original confidence: 82/100
Dismissed at: Step 4 validation
Dismissed because: UX nit, not a bug — the Local-State JSON validation provides defense-in-depth and the dialog pre-selects the expected directoryURL, so the common path passes the equality check; users selecting a different path getting a generic error is a UX polish item below Refactor severity.
Trailing whitespace and out-of-order entry in messages.json
apps/desktop/src/locales/en/messages.json:4806
Caught by: Code quality agent
Original severity: ♻️ Refactor
Original confidence: 80/100
Dismissed at: Step 4 validation
Dismissed because: Trailing whitespace in JSON is harmless and a linter would catch it; the out-of-order placement of the new key is sub-Refactor noise that a senior reviewer would not block on.
is_browser_installed returns true for unknown browser names
apps/desktop/desktop_native/chromium_importer/src/chromium/platform/sandbox.rs:197
Caught by: Bug analysis agent
Original severity:
Original confidence: 90/100
Dismissed at: Step 4 validation
Dismissed because: The only caller request_only validates browser_name against SUPPORTED_BROWSERS before invoking is_browser_installed, and BROWSER_BUNDLE_IDS contains the same seven entries as macOS SUPPORTED_BROWSERS, so the unknown-name branch is unreachable today; the structural drift risk is already covered by arch-1.
stopAccessingBrowser resolves bookmark to a fresh NSURL — start/stop unbalanced
apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access_manager.m:154
Caught by: Bug analysis agent
Original severity:
Original confidence: 85/100
Dismissed at: Step 4 validation
Dismissed because: Apple's documented contract for security-scoped resources balances start/stop calls on the URL but does not unambiguously require the same NSURL instance — the underlying counter is on the resolved security scope, and per the validation directive without conclusive proof of an instance-identity requirement this is dismissed.
stopAccessingBrowser calls stop on different NSURL instance — scoped resource not released
apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access_manager.m:153
Caught by: Security & logic agent
Original severity:
Original confidence: 85/100
Dismissed at: Step 4 validation
Dismissed because: Same reasoning as the bug-analysis duplicate above — cannot conclusively confirm Apple's contract requires the identical NSURL instance for stopAccessingSecurityScopedResource; the security-scope counter is associated with the resolved bookmark scope, not strictly the Obj-C object identity.
|
Hi John 👋🏼 Have you consider or ran into the following? apps/desktop/resources/entitlements.mas.plist
Details and fixApple's documentation article "Enabling Security-Scoped Bookmark and URL Access" states:
It defines two entitlement keys:
The new What currently exists in both MAS plist files: <key>com.apple.security.files.user-selected.read-write</key>
<true/>
Fix — add to both <key>com.apple.security.files.bookmarks.app-scope</key>
<true/>How to verify on a real MAS build:
If the picker reappears every launch, the entitlement is missing or not taking effect. I tracked down some Apple specific documentation here: Enabling Security-Scoped Bookmark and URL Access |
…porter-for-mac-app-store-build
|
coroiu
left a comment
There was a problem hiding this comment.
No changes to platform code




🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-26250?atlOrigin=eyJpIjoiN2Y1Y2NhNmQ4OTBhNDU4YzkwZWNmYmI1MTdiNzc1NDgiLCJwIjoiaiJ9
📔 Objective
For the direct importer, browser profiles aren't populating when the desktop app was installed via the mac app store. This is because macOS apps can be sandboxed and won't have access to system directories to look for browser profiles.
The goal of this effort is to enable the direct importer for the mac app store build.
🎥 Screencast
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes