Skip to content
Merged
Changes from 4 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
113 changes: 113 additions & 0 deletions docs/decisions/17817-driver-extension-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# 17817. The driver installs extensions directly
Comment thread
titusfortner marked this conversation as resolved.
Outdated

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

## Context

Installing a browser extension is a routine automation need. Firefox can already install one
mid-session, through a WebDriver-classic endpoint β€” but exposed under a different name in every
binding and hanging off a browser-specific type rather than the driver.

Chromium has traditionally taken extensions through capabilities, applied when the session is
created. Branded production Chrome stopped honoring that path in Chrome 137; Chrome for Testing and
unbranded Chromium builds still honor it. Installing an extension after the session is created is
therefore now a requirement, not a convenience.

Installing an extension in Chromium with BiDi requires the driver to connect to the browser over an
inherited pipe rather than a local debugging port, and that has a cost. Chromium exposes CDP two
ways: a vendored endpoint (`goog/cdp/execute`) that sends and receives CDP commands but not events,
and a live DevTools connection that Selenium's higher-level CDP API is built on. Only the debugging
port exposes that live connection, so over the pipe the vendored endpoint still works but the CDP
API does not.

WebDriver BiDi specifies 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. **Installing an extension is supported directly on the driver.** Every binding exposes, on the
driver instance itself, a cross-browser implementation of `installExtension` β€” taking an unpacked
directory, a packed archive, or base64 bytes β€” and `uninstallExtension`, taking the id returned by
install. It must also support any browser-specific install options the browser exposes. The
existing Firefox methods route through it when BiDi is enabled.

2. **Extension installation works with default settings when BiDi is enabled.** Enabling BiDi
switches Chromium to the pipe transport and permits unsigned extensions, so installing an
extension requires no additional flags from the user, signed or not. Firefox already exposes BiDi
natively.

3. **When BiDi is enabled, Selenium disables its CDP API.** The vendored CDP endpoint remains
available; the CDP API is turned off rather than left to fail when its DevTools connection is
unavailable.

## Considered options

**Where the method lives**
- **On the driver instance** (Accepted) β€” an installed extension is session state, and the driver is
the object every binding already hands the user; it is the one shape that works identically in all
five bindings.
- **Keep pointing users at the BiDi module** (Rejected) β€” the status quo, and what we advertise
today. Per [ADR 17670](17670-bidi-implementation-boundaries.md) that module is the internal implementation: protocol-shaped, outside the
deprecation policy, and reached differently in every binding β€” direct construction in Java, an
accessor in Python, a protocol class in Ruby, through a BiDi object in .NET, and absent in
JavaScript. It also forces users to know which protocol services the command in order to use it.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
- **A dedicated `extensions` namespace on the driver** (Rejected) β€” the strongest alternative:
consistent with the high-level `network` / `script` surfaces, available in every binding, and with
room for later operations such as listing or enabling. Rejected because the surface is two verbs,
which does not earn the extra indirection, and a flat method matches the Firefox install methods
it replaces. Worth revisiting if extension operations grow.

**Naming of the method**
- **Extend `install_addon` cross-browser** (Rejected) β€” "add-on" is Mozilla terminology that
misleads for Chromium extensions, and the classic name already differs across bindings, so there
is no single name to preserve.
- **`installWebExtension`** (Rejected) β€” "web extension" is the BiDi module's noun; per ADR 17670 the
supported surface should not mirror the protocol.
- **`installExtension` / "extension" concept** (Accepted) β€” neutral, the word users say, and matches
Java's existing `installExtension`.

**Signed and unsigned extensions**

Signed extensions install with no extra browser configuration. Unsigned extensions β€” the common
case for locally built or test extensions β€” additionally require the browser to be launched with
unsigned-extension loading enabled, which cannot be turned on once the session is running.

- **Allow unsigned extensions by default** (Accepted) β€” automation routinely loads locally built,
unsigned extensions, and enabling it has no effect unless the user's own code installs one.
Because it can only be set at launch, an opt-in would force the choice before the user knows
whether they will need it, turning a late discovery into a session restart.
- **Leave unsigned extensions to an explicit opt-in** (Rejected) β€” a more conservative browser
posture, but the restriction exists to stop malicious software installing extensions into a
browser someone actually browses with. That does not describe a session the automation itself
launched, so the opt-in adds a step without buying protection.

**Default transport**
- **Keep the port as the default** (Rejected) β€” the method would then require the user to pass
specific Chrome flags before it works at all, which is the friction this decision exists to
remove.
- **Switch to the pipe when BiDi is enabled** (Accepted).

## Consequences

- **Extensions can be added and removed mid-session**, on both Firefox and Chromium, without
preparing capabilities before the session starts.
- **Users relying on Selenium's CDP API can switch to the BiDi equivalent**, call the CDP endpoint
directly through their language's wrapper method (`execute_cdp_cmd` and equivalents), or not
enable BiDi.
- **Chromium BiDi sessions are more secure** β€” no localhost CDP control port for other local
processes to attach to. The gain is largest on shared, containerized, or CI Grid nodes.
- **Grid needs no code changes.** BiDi over Grid is proxied through chromedriver's own
`webSocketUrl` and never used the CDP port; with the pipe, a session that would have exposed
`se:cdp` simply comes back BiDi-only.
- **Firefox is unaffected by the transport change** β€” it exposes BiDi natively and gates nothing on
transport.