{"id":171,"date":"2026-09-07T05:52:51","date_gmt":"2026-09-07T05:52:51","guid":{"rendered":"https:\/\/blogs.igalia.com\/mshin\/?p=171"},"modified":"2026-09-07T05:52:51","modified_gmt":"2026-09-07T05:52:51","slug":"extension-migration-progress-update-part-2","status":"publish","type":"post","link":"https:\/\/blogs.igalia.com\/mshin\/2026\/09\/07\/extension-migration-progress-update-part-2\/","title":{"rendered":"Extension Migration Progress Update \u2013 Part 2"},"content":{"rendered":"\n<p>This post walks through the process of bringing back an extension-focused <code>\/\/extensions\/shell<\/code> in Chromium and enabling the <strong>\u201cAdd to Chrome\u201d<\/strong> button on the Chrome Web Store.<\/p>\n\n\n\n<p>If you haven\u2019t read it yet, I recommend starting with <strong><a href=\"https:\/\/blogs.igalia.com\/mshin\/2026\/04\/02\/extension-migration-progress-update-part-1\/\">Extension Migration Progress Update \u2013 Part 1<\/a><\/strong>, where I described the initial stage of migrating extension installation formats and APIs out of the <code>\/\/chrome<\/code> layer.<\/p>\n\n\n\n<p>After migrating the extension installation formats \u2014 CRX, ZIP, and Unpacked \u2014 into the <code>\/\/extensions<\/code> layer, the next natural step was to enable embedders to install extensions directly from the Chrome Web Store.<\/p>\n\n\n\n<p>To achieve this, the <strong>Web Store Private APIs<\/strong> were an essential piece of the puzzle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Migrating Web Store Private APIs<\/h2>\n\n\n\n<p>The Web Store Private APIs had a relatively large dependency on <code>\/\/chrome<\/code>. We started by migrating the relevant files and gradually replacing Chrome-specific dependencies with alternative implementations where necessary.<\/p>\n\n\n\n<p>In some cases, this meant introducing new interfaces, while in others it meant moving code to a more appropriate layer.<\/p>\n\n\n\n<p>After working through these dependencies, the final changes were eventually landed upstream: <a href=\"https:\/\/source.chromium.org\/chromium\/chromium\/src\/+\/599148a4fd877e26351c56b567a4a325375bf493?utm_source=chatgpt.com\">Chromium CL \u2013 Web Store Private API migration<\/a>.<\/p>\n\n\n\n<p>The changes are included in <strong>M152<\/strong>.<\/p>\n\n\n\n<p>However, after this migration was completed, we encountered an unexpected obstacle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where did <code>extensions\/shell<\/code> go?<\/h2>\n\n\n\n<p>Chromium originally had a lightweight embedder called <code>\/\/extensions\/shell<\/code>, previously built as the <code>app_shell<\/code> target.<\/p>\n\n\n\n<p>The idea was simple: provide an environment where extensions could be installed and run without bringing up the entire Chrome browser.<\/p>\n\n\n\n<p>However, the shell had originally been created about a decade ago for a different purpose. It was intended to provide an independent environment for ChromeOS platform applications without requiring the full Chrome browser.<\/p>\n\n\n\n<p>Platform apps were eventually deprecated, and the shell was later repurposed primarily as a lightweight environment for extension browser tests.<\/p>\n\n\n\n<p>Over time, the extension browser tests were moved into the <code>\/\/chrome<\/code> layer. The shell was no longer used by any real product, and maintaining it became technical debt. As a result, <code>app_shell<\/code> was eventually removed upstream.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why were the browser tests moved from <code>\/\/extensions<\/code> to <code>\/\/chrome<\/code>?<\/h3>\n\n\n\n<p>I asked an Extension owner about the reasoning behind this change.<\/p>\n\n\n\n<p>The key point was that the old shell-based browser tests did not provide a sufficiently realistic environment for testing extensions.<\/p>\n\n\n\n<p>There were two major problems.<\/p>\n\n\n\n<p>First, many extension APIs delegate part of their implementation to the embedder through interfaces such as <code>ExtensionsApiClient<\/code>. Testing only the <code>\/\/extensions<\/code> implementation with the shell meant that the actual embedding logic was not exercised. Bugs in the delegated implementation could therefore go unnoticed.<\/p>\n\n\n\n<p>Second, extensions depend on much more than the <code>\/\/extensions<\/code> layer itself. UI, networking, rendering, process management, and other browser functionality all affect how extensions behave. The <code>\/\/chrome<\/code> layer provides an important part of that environment.<\/p>\n\n\n\n<p>Therefore, the upstream direction was to move extension browser tests into the real Chrome browser environment rather than maintaining a separate shell.<\/p>\n\n\n\n<p>That reasoning makes sense for <strong>testing<\/strong>.<\/p>\n\n\n\n<p>In fact, we agreed that bringing the old shell back simply as a test environment would not be the right approach.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>But I had a different question:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>What if we use a lightweight shell not as a test environment, but as a reference implementation and a demo environment for embedders?<\/p>\n<\/blockquote>\n\n\n\n<p>The answer was that Chromium currently does not plan to maintain such an embedder. The concern is that a simplified shell could itself become an inaccurate reference implementation. If its behavior diverged from the real Chrome integration, it could create more confusion rather than provide useful guidance.<\/p>\n\n\n\n<p>The suggested direction was instead to use <strong>extension-based Web Platform Tests (WPT)<\/strong> to provide consistency, while considering actual browsers as the reference implementations.<\/p>\n\n\n\n<p>That approach makes sense from the upstream perspective.<\/p>\n\n\n\n<p>However, for our migration work, we still needed something slightly different.<\/p>\n\n\n\n<p>We needed a small, working environment where we could demonstrate that extensions can actually be supported <strong>outside of the <code>\/\/chrome<\/code> layer<\/strong>.<\/p>\n\n\n\n<p>So we decided to bring back an extension-focused shell \u2014 not as an upstream testing framework, but as a practical environment for integration and demonstration.<\/p>\n\n\n\n<p>Although this shell is not part of upstream Chromium, we are currently maintaining the implementation internally in the Igalia repository, with plans to open-source it in the future. It serves as a lightweight environment for demonstrating and validating Extension support outside of the \/\/chrome layer, rather than as a replacement for Chromium&#8217;s browser tests or an upstream reference implementation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rebuilding the Extension Shell<\/h2>\n\n\n\n<p>The first step was to establish the basic structure of the shell.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Building the skeleton<\/h3>\n\n\n\n<p>We created a custom <code>ShellBrowserContext<\/code> directly derived from <code>content::BrowserContext<\/code>, bootstrapped the Views environment, and added a simple placeholder window.<\/p>\n\n\n\n<p>On top of that, we connected the extension system through:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ShellExtensionsBrowserClient<\/code> \u2014 the entry point for implementing embedder-specific Extensions behavior for the shell.<\/li>\n\n\n\n<li><code>ShellExtensionSystem<\/code> \u2014 the service responsible for core extension management, including loading and unloading extensions and parsing manifests.<\/li>\n<\/ul>\n\n\n\n<p>This gave us the basic environment needed to initialize Chromium\u2019s extension infrastructure without depending on the full Chrome browser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Building the installation pipeline<\/h3>\n\n\n\n<p>Next, the shell needed to actually install extensions.<\/p>\n\n\n\n<p>We connected <code>ExtensionExternalInstaller<\/code>, which orchestrates the overall installation flow \u2014 including downloading, unpacking, and manifest validation \u2014 to support CRX and ZIP installation.<\/p>\n\n\n\n<p>We also implemented <code>ShellContentUtilityClient<\/code>, which registers the sandboxed utility-process service needed to safely handle untrusted data, such as ZIP decompression, out of process.<\/p>\n\n\n\n<p>We then completed the renderer-side wiring so that an installed Manifest V3 extension could actually execute its service worker and access <code>chrome-extension:\/\/<\/code> resources.<\/p>\n\n\n\n<p>Several pieces of the extension installation infrastructure also needed to be connected, including:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Blocklist<\/code> \u2014 a <code>KeyedService<\/code> that tracks which extensions are blocklisted, backed by Safe Browsing.<\/li>\n\n\n\n<li><code>InstallStageTracker<\/code> \u2014 tracks installation stages and failure events specifically for policy force-installed extensions.<\/li>\n\n\n\n<li><code>InstallTracker<\/code> \u2014 broadcasts installation start, completion, and failure events to observers.<\/li>\n\n\n\n<li><code>InstallVerifier<\/code> \u2014 confirms that an installed extension is either verified through a Web Store signature or allowlisted by enterprise policy.<\/li>\n\n\n\n<li><code>SharedModuleService<\/code> \u2014 supports the import\/export mechanism that extensions use to pull in shared modules from other extensions.<\/li>\n<\/ul>\n\n\n\n<p>At this point, we had a shell that could actually install and initialize extensions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Making it look like a browser<\/h3>\n\n\n\n<p>The next step was to make the shell usable as an actual browser rather than just an extension test environment.<\/p>\n\n\n\n<p>We used the existing <code>content\/shell<\/code> implementation and <code>ShellPlatformDelegateViews<\/code> as references, without directly depending on them.<\/p>\n\n\n\n<p>The shell now has a basic browser UI with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Back<\/li>\n\n\n\n<li>Forward<\/li>\n\n\n\n<li>Reload<\/li>\n\n\n\n<li>Stop<\/li>\n\n\n\n<li>Address bar<\/li>\n\n\n\n<li>A <code>views::WebView<\/code> containing a real <code>content::WebContents<\/code><\/li>\n<\/ul>\n\n\n\n<p>This allowed us to browse normal web pages as well as run installed extensions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Enforcing network-level policies<\/h3>\n\n\n\n<p>With the browser environment in place, we also started validating extension APIs that operate at the network level.<\/p>\n\n\n\n<p>In particular, we tested <code>declarativeNetRequest<\/code> and <code>webRequest<\/code>, which uncovered a few issues that needed to be fixed in the shell integration.<\/p>\n\n\n\n<p>After these steps, the basic foundation of an <strong>embedder that can install and run extensions<\/strong> was finally becoming stable.<\/p>\n\n\n\n<p>And that naturally led to the next question:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong><em>Can we install an extension directly from the Chrome Web Store?<\/em><\/strong><\/p>\n<\/blockquote>\n\n\n\n<p>The answer was initially <strong>no<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Enabling the \u201cAdd to Chrome\u201d Button<\/h2>\n\n\n\n<p>When opening an extension page on the Chrome Web Store, the <strong>\u201cAdd to Chrome\u201d<\/strong> button was disabled.<\/p>\n\n\n\n<p>The reason was another missing piece of Chrome-specific functionality.<\/p>\n\n\n\n<p>The Web Store uses the <code>chrome.webstorePrivate<\/code> extension API to perform the installation flow.<\/p>\n\n\n\n<p>Behind this API is <code>WebstorePrivateAPIDelegate<\/code>, which delegates browser-specific operations to the embedder. These operations include things such as installation approval, login state, Safe Browsing checks, and the installation confirmation UI.<\/p>\n\n\n\n<p>Since our shell was not Chrome, these pieces were not implemented.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Implementing the missing pieces<\/h3>\n\n\n\n<p>To make the Web Store installation flow work, we implemented the required delegate functionality with safe defaults and added the necessary extension infrastructure to the shell.<\/p>\n\n\n\n<p>This included:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>WebstorePrivateAPIDelegate<\/code> \u2014 provides the embedder-specific implementation required by the <code>chrome.webstorePrivate<\/code> API to handle Web Store extension installation.<\/li>\n\n\n\n<li><code>ShellExtensionsAPIClient<\/code> \u2014 provides shell-specific implementations of browser-level services required by extension APIs.<\/li>\n\n\n\n<li><code>ShellExtensionInstallPromptClient<\/code> \u2014 provides the shell-specific UI handling for extension installation prompts.<\/li>\n\n\n\n<li><code>WebstorePrivateAPI<\/code> factory \u2014 creates and provides the <code>WebstorePrivateAPI<\/code> service for extensions running in the shell.<\/li>\n\n\n\n<li><code>ManagementAPI<\/code> factory \u2014 creates and provides the <code>ManagementAPI<\/code> service, which exposes extension management functionality to extensions.<\/li>\n<\/ul>\n\n\n\n<p>The shell also needed to provide appropriate User-Agent metadata, which was delegated through:<\/p>\n\n\n\n<p><code>embedder_support::GetUserAgentMetadata()<\/code>.<\/p>\n\n\n\n<p>After wiring these pieces together, the Chrome Web Store was finally able to recognize our shell as an environment capable of handling extension installation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Demo<\/h2>\n\n\n\n<p>And this is where things became much more interesting.<\/p>\n\n\n\n<p>The <strong>\u201cAdd to Chrome\u201d<\/strong> button is now enabled, and clicking it can trigger the extension installation flow in our extension shell.<\/p>\n\n\n\n<figure class=\"wp-block-video\"><video controls src=\"https:\/\/blogs.igalia.com\/mshin\/files\/2026\/08\/demo_for_webstore.mp4\"><\/video><\/figure>\n\n\n\n<p>This is an important milestone for the migration.<\/p>\n\n\n\n<p>We now have a lightweight browser environment that can:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Launch independently of Chrome.<\/li>\n\n\n\n<li>Browse normal web pages.<\/li>\n\n\n\n<li>Install extensions.<\/li>\n\n\n\n<li>Run Manifest V3 extensions.<\/li>\n\n\n\n<li>Exercise extension APIs.<\/li>\n\n\n\n<li>Install an extension directly from the Chrome Web Store.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s next?<\/h2>\n\n\n\n<p>Getting an extension installed is only part of the story.<\/p>\n\n\n\n<p>The next step is making the installed extensions behave like they do in a real browser.<\/p>\n\n\n\n<p>There are still several APIs that have traditionally depended on the <code>\/\/chrome<\/code> layer. Permissions, cookies, and other browser integration points are among the areas that need to be migrated or provided by the embedder.<\/p>\n\n\n\n<p>Once these pieces are migrated, we should be able to go beyond simply installing an extension from the Web Store and demonstrate a more complete end-to-end experience:<\/p>\n\n\n\n<p><strong>Chrome Web Store \u2192 Extension installation \u2192 Extension startup \u2192 Extension API usage<\/strong><\/p>\n\n\n\n<p>This is the next stage of the migration work.<\/p>\n\n\n\n<p>I\u2019ll continue documenting the progress as more of these APIs move into the <code>\/\/extensions<\/code> layer and the extension shell becomes a more complete demonstration environment for embedders.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post walks through the process of bringing back an extension-focused \/\/extensions\/shell in Chromium and enabling the \u201cAdd to Chrome\u201d button on the Chrome Web Store. If you haven\u2019t read it yet, I recommend starting with Extension Migration Progress Update \u2013 Part 1, where I described the initial stage of migrating extension installation formats and &hellip; <a href=\"https:\/\/blogs.igalia.com\/mshin\/2026\/09\/07\/extension-migration-progress-update-part-2\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Extension Migration Progress Update \u2013 Part 2<\/span><\/a><\/p>\n","protected":false},"author":58,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,3],"tags":[4,10,6],"class_list":["post-171","post","type-post","status-publish","format-standard","hentry","category-chromium","category-igalia","tag-chromium","tag-extensions","tag-igalia"],"_links":{"self":[{"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/posts\/171","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/users\/58"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/comments?post=171"}],"version-history":[{"count":6,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/posts\/171\/revisions"}],"predecessor-version":[{"id":178,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/posts\/171\/revisions\/178"}],"wp:attachment":[{"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/media?parent=171"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/categories?post=171"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.igalia.com\/mshin\/wp-json\/wp\/v2\/tags?post=171"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}