Extension Migration Progress Update – Part 2

This post walks through the process of bringing back an extension-focused //extensions/shell in Chromium and enabling the “Add to Chrome” button on the Chrome Web Store.

If you haven’t read it yet, I recommend starting with Extension Migration Progress Update – Part 1, where I described the initial stage of migrating extension installation formats and APIs out of the //chrome layer.

After migrating the extension installation formats — CRX, ZIP, and Unpacked — into the //extensions layer, the next natural step was to enable embedders to install extensions directly from the Chrome Web Store.

To achieve this, the Web Store Private APIs were an essential piece of the puzzle.

Migrating Web Store Private APIs

The Web Store Private APIs had a relatively large dependency on //chrome. We started by migrating the relevant files and gradually replacing Chrome-specific dependencies with alternative implementations where necessary.

In some cases, this meant introducing new interfaces, while in others it meant moving code to a more appropriate layer.

After working through these dependencies, the final changes were eventually landed upstream: Chromium CL – Web Store Private API migration.

The changes are included in M152.

However, after this migration was completed, we encountered an unexpected obstacle.

Where did extensions/shell go?

Chromium originally had a lightweight embedder called //extensions/shell, previously built as the app_shell target.

The idea was simple: provide an environment where extensions could be installed and run without bringing up the entire Chrome browser.

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.

Platform apps were eventually deprecated, and the shell was later repurposed primarily as a lightweight environment for extension browser tests.

Over time, the extension browser tests were moved into the //chrome layer. The shell was no longer used by any real product, and maintaining it became technical debt. As a result, app_shell was eventually removed upstream.

Why were the browser tests moved from //extensions to //chrome?

I asked an Extension owner about the reasoning behind this change.

The key point was that the old shell-based browser tests did not provide a sufficiently realistic environment for testing extensions.

There were two major problems.

First, many extension APIs delegate part of their implementation to the embedder through interfaces such as ExtensionsApiClient. Testing only the //extensions implementation with the shell meant that the actual embedding logic was not exercised. Bugs in the delegated implementation could therefore go unnoticed.

Second, extensions depend on much more than the //extensions layer itself. UI, networking, rendering, process management, and other browser functionality all affect how extensions behave. The //chrome layer provides an important part of that environment.

Therefore, the upstream direction was to move extension browser tests into the real Chrome browser environment rather than maintaining a separate shell.

That reasoning makes sense for testing.

In fact, we agreed that bringing the old shell back simply as a test environment would not be the right approach.

But I had a different question:

What if we use a lightweight shell not as a test environment, but as a reference implementation and a demo environment for embedders?

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.

The suggested direction was instead to use extension-based Web Platform Tests (WPT) to provide consistency, while considering actual browsers as the reference implementations.

That approach makes sense from the upstream perspective.

However, for our migration work, we still needed something slightly different.

We needed a small, working environment where we could demonstrate that extensions can actually be supported outside of the //chrome layer.

So we decided to bring back an extension-focused shell — not as an upstream testing framework, but as a practical environment for integration and demonstration.

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’s browser tests or an upstream reference implementation.

Rebuilding the Extension Shell

The first step was to establish the basic structure of the shell.

1. Building the skeleton

We created a custom ShellBrowserContext directly derived from content::BrowserContext, bootstrapped the Views environment, and added a simple placeholder window.

On top of that, we connected the extension system through:

  • ShellExtensionsBrowserClient — the entry point for implementing embedder-specific Extensions behavior for the shell.
  • ShellExtensionSystem — the service responsible for core extension management, including loading and unloading extensions and parsing manifests.

This gave us the basic environment needed to initialize Chromium’s extension infrastructure without depending on the full Chrome browser.

2. Building the installation pipeline

Next, the shell needed to actually install extensions.

We connected ExtensionExternalInstaller, which orchestrates the overall installation flow — including downloading, unpacking, and manifest validation — to support CRX and ZIP installation.

We also implemented ShellContentUtilityClient, which registers the sandboxed utility-process service needed to safely handle untrusted data, such as ZIP decompression, out of process.

We then completed the renderer-side wiring so that an installed Manifest V3 extension could actually execute its service worker and access chrome-extension:// resources.

Several pieces of the extension installation infrastructure also needed to be connected, including:

  • Blocklist — a KeyedService that tracks which extensions are blocklisted, backed by Safe Browsing.
  • InstallStageTracker — tracks installation stages and failure events specifically for policy force-installed extensions.
  • InstallTracker — broadcasts installation start, completion, and failure events to observers.
  • InstallVerifier — confirms that an installed extension is either verified through a Web Store signature or allowlisted by enterprise policy.
  • SharedModuleService — supports the import/export mechanism that extensions use to pull in shared modules from other extensions.

At this point, we had a shell that could actually install and initialize extensions.

3. Making it look like a browser

The next step was to make the shell usable as an actual browser rather than just an extension test environment.

We used the existing content/shell implementation and ShellPlatformDelegateViews as references, without directly depending on them.

The shell now has a basic browser UI with:

  • Back
  • Forward
  • Reload
  • Stop
  • Address bar
  • A views::WebView containing a real content::WebContents

This allowed us to browse normal web pages as well as run installed extensions.

4. Enforcing network-level policies

With the browser environment in place, we also started validating extension APIs that operate at the network level.

In particular, we tested declarativeNetRequest and webRequest, which uncovered a few issues that needed to be fixed in the shell integration.

After these steps, the basic foundation of an embedder that can install and run extensions was finally becoming stable.

And that naturally led to the next question:

Can we install an extension directly from the Chrome Web Store?

The answer was initially no.

Enabling the “Add to Chrome” Button

When opening an extension page on the Chrome Web Store, the “Add to Chrome” button was disabled.

The reason was another missing piece of Chrome-specific functionality.

The Web Store uses the chrome.webstorePrivate extension API to perform the installation flow.

Behind this API is WebstorePrivateAPIDelegate, 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.

Since our shell was not Chrome, these pieces were not implemented.

Implementing the missing pieces

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.

This included:

  • WebstorePrivateAPIDelegate — provides the embedder-specific implementation required by the chrome.webstorePrivate API to handle Web Store extension installation.
  • ShellExtensionsAPIClient — provides shell-specific implementations of browser-level services required by extension APIs.
  • ShellExtensionInstallPromptClient — provides the shell-specific UI handling for extension installation prompts.
  • WebstorePrivateAPI factory — creates and provides the WebstorePrivateAPI service for extensions running in the shell.
  • ManagementAPI factory — creates and provides the ManagementAPI service, which exposes extension management functionality to extensions.

The shell also needed to provide appropriate User-Agent metadata, which was delegated through:

embedder_support::GetUserAgentMetadata().

After wiring these pieces together, the Chrome Web Store was finally able to recognize our shell as an environment capable of handling extension installation.

Demo

And this is where things became much more interesting.

The “Add to Chrome” button is now enabled, and clicking it can trigger the extension installation flow in our extension shell.

This is an important milestone for the migration.

We now have a lightweight browser environment that can:

  1. Launch independently of Chrome.
  2. Browse normal web pages.
  3. Install extensions.
  4. Run Manifest V3 extensions.
  5. Exercise extension APIs.
  6. Install an extension directly from the Chrome Web Store.

What’s next?

Getting an extension installed is only part of the story.

The next step is making the installed extensions behave like they do in a real browser.

There are still several APIs that have traditionally depended on the //chrome layer. Permissions, cookies, and other browser integration points are among the areas that need to be migrated or provided by the embedder.

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:

Chrome Web Store → Extension installation → Extension startup → Extension API usage

This is the next stage of the migration work.

I’ll continue documenting the progress as more of these APIs move into the //extensions layer and the extension shell becomes a more complete demonstration environment for embedders.

Leave a Reply

Your email address will not be published. Required fields are marked *