Skip to content
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3537c7b
[docs] Propose ADR: the driver installs extensions directly
titusfortner Jul 24, 2026
551bc46
[docs] Number ADR 17817 and link its PR
titusfortner Jul 24, 2026
d047e73
[docs] ADR 17817: sharpen the CDP-access framing
titusfortner Jul 24, 2026
cfdf561
[docs] ADR 17817: address review copyedits
titusfortner Jul 24, 2026
b1b9606
[docs] ADR 17817: restructure decisions and trim considered options
titusfortner Aug 4, 2026
bfec758
[docs] ADR 17817: tighten decisions to read as decisions
titusfortner Aug 4, 2026
4b825a3
[docs] ADR 17817: note the classic fallback is reduced-capability
titusfortner Aug 5, 2026
d28dd8d
[docs] ADR 17817: fix inverted temporary/permanent wording
titusfortner Aug 5, 2026
e894d6a
[docs] ADR 17817: adopt installWebExtension naming and the web-extens…
titusfortner Aug 5, 2026
8ac2df1
[docs] ADR 17817: scope the CDP-pipe flags to Chromium sessions
titusfortner Aug 5, 2026
183e0ad
[docs] ADR 17817: correct the classic private-browsing behavior
titusfortner Aug 5, 2026
192c35b
[docs] ADR 17817: require bindings to reject allowPrivateBrowsing:fal…
titusfortner Aug 5, 2026
8119540
[docs] ADR 17817: drop the CDP tradeoff, obsoleted by Chrome 149 remo…
titusfortner Aug 6, 2026
1969b32
[docs] ADR 17817: require BiDi for installWebExtension on Chrome
titusfortner Aug 6, 2026
e23bc4e
[docs] ADR 17817: split backwards-compat from the raise-when-unsuppor…
titusfortner Aug 6, 2026
2c7d7e8
[docs] ADR 17817: scope vendor options to the vendor driver, drop the…
titusfortner Aug 6, 2026
f224e6e
[docs] ADR 17817: drop the vendor-option consequence, Decision 1 alre…
titusfortner Aug 6, 2026
fdffcad
[docs] ADR 17817: make the Grid consequence transport-neutral
titusfortner Aug 13, 2026
2c90f41
[docs] ADR 17817: clarify remote-end delivery and Chromium scoping
titusfortner Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/decisions/17817-driver-extension-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 17817. The driver installs web extensions directly

- Status: Proposed
- Discussion: https://github.com/SeleniumHQ/selenium/pull/17817

## Context

Firefox can install a web extension mid-session through a WebDriver-classic endpoint, but every binding
hangs it off a browser-specific type rather than the driver.

Chromium takes web extensions through capabilities applied at session creation. Branded Chrome stopped
honoring that path in Chrome 137 (Chrome for Testing and unbranded Chromium still do), so installing
after the session starts is now a requirement.

WebDriver BiDi specifies web extension install and uninstall, which both Firefox and Chromium
implement. Most bindings already expose the BiDi module for it, and pointing users at that module
is what we advertise today.

| Binding | Firefox-only method (classic) | Currently advertised BiDi approach |
|------------|-------------------------------|------------------------------------|
| Java | `installExtension` (on `FirefoxDriver`) | `new WebExtension(driver).install(...)` |
| Python | `install_addon` | `driver.webextension.install(...)` |
| Ruby | `install_addon` (`HasAddons`) | `BiDi::Protocol::WebExtension` (protocol module) |
| .NET | `InstallAddOn`, `InstallAddOnFromFile`, `InstallAddOnFromDirectory` | `(await driver.AsBiDiAsync()).WebExtension.InstallAsync(...)` |
| JavaScript | `installAddon` | none |
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

## Decision

1. **Add two methods to the driver instance.** Every binding exposes `installWebExtension` and
`uninstallWebExtension` on the driver instance itself, not on a browser-specific type or the BiDi
module.
* **Install behavior:** accepts an archive, a directory, or base64, as well as vendor-specific options
(on Firefox, `permanent` and `allowPrivateBrowsing`). The implementation must work with the Grid.
The method returns a `WebExtension` object which wraps the id.
* **Uninstall behavior:** accepts the `WebExtension` object rather than a raw id.

2. **Backwards compatible**. On Firefox these methods fall back to the WebDriver-Classic endpoint when
BiDi is not enabled, so no existing capability is lost; the existing classic install methods and
parameters are deprecated in favor of them.

3. **Raise when the target cannot honor the request.** When a browser or transport cannot fulfill a
request, it raises rather than silently doing less β€” such as `installWebExtension` on Chromium without
BiDi.

## Considered options

These are the alternatives considered and not taken; the accepted choice is the decision above.

**Where the method lives**
- **Re-implement the existing methods instead of adding a new one** β€” the direction the project is
generally moving is to give existing methods new behavior rather than grow the API surface. It
cannot deliver this capability on its own: the existing methods are Firefox-only and inconsistently
named, and Chromium has none to re-implement, so there is no uniform method to carry it. A new,
uniformly-named method is what makes it cross-browser.
- **A dedicated `webExtensions` namespace** β€” consistent with `network` / `script`, with room to grow,
but two methods make that seem excessive, especially when the precedent from Firefox is to have the
method on the driver directly

**Conditional availability**
- **Expose the method only where it works** β€” hide it on a Chromium session without BiDi rather than
raising. Not taken: Java cannot conditionally implement the interface, and doing it only in a binding
that can (Ruby) would make it the odd one out; a uniform surface that raises a clear error is simpler.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

**Naming**
- **`installExtension` returning `Extension`** β€” shorter and reuses the name Java already ships, but
"extension" is overloaded, and reusing Java's classic name collides by return type with its
`installExtension(Path)`, forcing a distinct-parameter workaround. `installWebExtension` models the
standard "web extension" noun and keeps the deprecation uniform across all five bindings; the public
`WebExtension` type sits in its own package, separate from the internal BiDi `WebExtension` module.

**Return type**
- **A raw id string** β€” untyped; the signature would accept any string.
- **Self acting object with `webExtension.uninstall()`** β€” out of scope for now

**The legacy `installAddon` methods**
- **Redirect `installAddon` to `installWebExtension` when BiDi is enabled** β€” keeps the legacy name
working as an alias instead of steering users to `installWebExtension`, so the two names persist
rather than converge, but if the point is to move to a new common name we shouldn't extend the old method
- **Keep it completely separate**. `installAddon` is always classic implementation and
`installWebExtension` is always BiDi implementation. This isn't how we plan to manage other
transitions, and we want to converge on a single common method.

**Unsigned web extensions**
- **Require an explicit opt-in** β€” the browser restriction guards a profile someone actually browses with,
not an automation-launched session, so it adds a step without buying protection.

## Consequences

- Because the remote end may run on a different host than the client (for example a Grid node), the
implementation cannot pass a client-local filesystem path; it must deliver the extension to the remote
end and reference it in a form the remote end can resolve β€” inline content, or a location obtained by
uploading to the remote end first.