From 3537c7bca67ba25574fc6e1ac0441562e942da62 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 24 Jul 2026 11:02:35 -0500 Subject: [PATCH 01/19] [docs] Propose ADR: the driver installs extensions directly --- docs/decisions/driver-extension-install.md | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/decisions/driver-extension-install.md diff --git a/docs/decisions/driver-extension-install.md b/docs/decisions/driver-extension-install.md new file mode 100644 index 0000000000000..a9a49fc673760 --- /dev/null +++ b/docs/decisions/driver-extension-install.md @@ -0,0 +1,112 @@ +# NNNN. The driver installs extensions directly + + + +- Status: Proposed +- Discussion: + +## 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. + +A Chromium driver reaches the browser over one of two transports: a local TCP debugging port, or a +pipe inherited from the process that launched the browser. The port is what makes CDP reachable; +the pipe is private to the driver and carries WebDriver BiDi only. Chrome accepts extension install +over BiDi only on the pipe. + +WebDriver BiDi specifies extension install and uninstall, which both Firefox and Chromium +implement. 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` | `driver.AsBiDiAsync()` → `BiDi.WebExtension.InstallAsync(...)` | +| JavaScript | `installAddon` | none | + +## 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. **A session provides BiDi or CDP, not both.** The pipe carries BiDi but not CDP, so a BiDi + session has no CDP available. No configuration provides both — a single transport carries one or + the other — so a caller who needs CDP does not enable BiDi. + +## 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 makes users know which protocol services the command in order to use it. +- **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 over the pipe with no additional browser flags. Unsigned extensions — the +common case for locally built or test extensions — additionally require Chrome's unsigned-extension +flag, which must be set when the browser launches and cannot be added once the session is running. + +- **Allow unsigned extensions by default** (Accepted) — automation routinely loads locally built, + unsigned extensions, and the flag has no effect unless the user's own code installs one. Because + it must 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. +- **CDP is unavailable in a BiDi session.** Users who need `execute_cdp_cmd`, port-based DevTools, + or Grid `se:cdp` do not enable BiDi for that session. This needs a release-note call-out. +- **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. From 551bc463b7fd17815ce553e93faafb15009bd095 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 24 Jul 2026 11:16:40 -0500 Subject: [PATCH 02/19] [docs] Number ADR 17817 and link its PR --- ...tension-install.md => 17817-driver-extension-install.md} | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename docs/decisions/{driver-extension-install.md => 17817-driver-extension-install.md} (96%) diff --git a/docs/decisions/driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md similarity index 96% rename from docs/decisions/driver-extension-install.md rename to docs/decisions/17817-driver-extension-install.md index a9a49fc673760..73b8d5d8c98a1 100644 --- a/docs/decisions/driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -1,9 +1,7 @@ -# NNNN. The driver installs extensions directly - - +# 17817. The driver installs extensions directly - Status: Proposed -- Discussion: +- Discussion: https://github.com/SeleniumHQ/selenium/pull/17817 ## Context From d047e737b4dc75bdbad980b8ae07bfbe66cca842 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 24 Jul 2026 13:14:15 -0500 Subject: [PATCH 03/19] [docs] ADR 17817: sharpen the CDP-access framing --- .../17817-driver-extension-install.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 73b8d5d8c98a1..194d59cb2fab5 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -14,10 +14,12 @@ created. Branded production Chrome stopped honoring that path in Chrome 137; Chr unbranded Chromium builds still honor it. Installing an extension after the session is created is therefore now a requirement, not a convenience. -A Chromium driver reaches the browser over one of two transports: a local TCP debugging port, or a -pipe inherited from the process that launched the browser. The port is what makes CDP reachable; -the pipe is private to the driver and carries WebDriver BiDi only. Chrome accepts extension install -over BiDi only on the pipe. +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. Bindings already expose the BiDi module for it, and pointing users at that module is @@ -44,9 +46,9 @@ what we advertise today. extension requires no additional flags from the user, signed or not. Firefox already exposes BiDi natively. -3. **A session provides BiDi or CDP, not both.** The pipe carries BiDi but not CDP, so a BiDi - session has no CDP available. No configuration provides both — a single transport carries one or - the other — so a caller who needs CDP does not enable BiDi. +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 @@ -99,8 +101,9 @@ flag, which must be set when the browser launches and cannot be added once the s - **Extensions can be added and removed mid-session**, on both Firefox and Chromium, without preparing capabilities before the session starts. -- **CDP is unavailable in a BiDi session.** Users who need `execute_cdp_cmd`, port-based DevTools, - or Grid `se:cdp` do not enable BiDi for that session. This needs a release-note call-out. +- **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 From cfdf561510e40e4902cd53639699915230919ea6 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 24 Jul 2026 14:04:44 -0500 Subject: [PATCH 04/19] [docs] ADR 17817: address review copyedits --- .../17817-driver-extension-install.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 194d59cb2fab5..10927b875b41e 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -22,15 +22,15 @@ port exposes that live connection, so over the pipe the vendored endpoint still API does not. WebDriver BiDi specifies extension install and uninstall, which both Firefox and Chromium -implement. Bindings already expose the BiDi module for it, and pointing users at that module is -what we advertise today. +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` | `driver.AsBiDiAsync()` → `BiDi.WebExtension.InstallAsync(...)` | +| .NET | `InstallAddOn`, `InstallAddOnFromFile`, `InstallAddOnFromDirectory` | `(await driver.AsBiDiAsync()).WebExtension.InstallAsync(...)` | | JavaScript | `installAddon` | none | ## Decision @@ -60,7 +60,7 @@ what we advertise today. 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 makes users know which protocol services the command in order to use it. + JavaScript. It also forces users to know which protocol services the command in order to use it. - **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, @@ -78,14 +78,14 @@ what we advertise today. **Signed and unsigned extensions** -Signed extensions install over the pipe with no additional browser flags. Unsigned extensions — the -common case for locally built or test extensions — additionally require Chrome's unsigned-extension -flag, which must be set when the browser launches and cannot be added once the session is running. +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 the flag has no effect unless the user's own code installs one. Because - it must 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. + 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 From b1b960602f3fa532d7d0d9103acfecea168e5eaf Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 13:32:11 -0500 Subject: [PATCH 05/19] [docs] ADR 17817: restructure decisions and trim considered options --- .../17817-driver-extension-install.md | 154 ++++++++---------- 1 file changed, 70 insertions(+), 84 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 10927b875b41e..9e9599db34744 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -5,21 +5,25 @@ ## 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. +Firefox can install an extension mid-session through a WebDriver-classic endpoint, but every binding +hangs it off a browser-specific type rather than the driver. + +Chromium takes 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. + +Chromium can connect to BiDi over either an inherited pipe or a debugging port. Selenium's CDP API +only works through the debugging port, but for security reasons Chromium does not allow installing an +extension over BiDi through the debugging port, only through the inherited pipe. That leaves two +options: + +1. Require the user to pass arguments in capabilities to switch to the pipe before extensions can be + installed. +2. Switch Selenium's default connection to the inherited pipe, which removes support for the CDP API + (Decision 5). + +Over the pipe it is still possible to send CDP commands through the vendored `goog/cdp/execute` +endpoint, but no asynchronous behavior (events) is supported. 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 @@ -35,79 +39,61 @@ is what we advertise today. ## 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. +1. **Methods available on the driver.** Every binding exposes `installExtension` and + `uninstallExtension` on the driver instance itself — not on a browser-specific type or the BiDi + module. + +2. **Install behavior.** `installExtension` accepts a packed archive, an unpacked directory, or + base64 bytes, supports browser-specific install options (on Firefox, `permanent` and + `allowPrivateBrowsing`), and returns an `Extension`. + +3. **Uninstall behavior.** `uninstallExtension` takes the `Extension` that install returned — only + that object, not a raw id, so the call is type-checked; its id is readable as `.id`. A convenience + `extension.uninstall()` is deferred as a non-breaking future upgrade, pending the network handler + ADR ([#17685](https://github.com/SeleniumHQ/selenium/pull/17685)). + +4. **The classic Firefox install methods are not deprecated yet.** `installExtension` requires BiDi, + which is not the default transport, so the classic methods remain the supported path for non-BiDi + sessions and keep working even when BiDi is enabled. They are superseded by `installExtension` and + become deprecation, then removal, candidates once BiDi is the default and the replacement is + available without opting in. In Java, whose classic method is already named `installExtension(Path)`, + the cross-browser method coexists as an overload — a typed source plus a Firefox options object, + distinct from the classic `Path`/`Boolean` signature — so nothing is renamed or broken. Chromium + has nothing here; it never had a session-time install method. + +5. **`installExtension` requires BiDi, and enabling BiDi disables the CDP API.** Once BiDi is + enabled, no further flags or configuration are needed, signed or unsigned. Enabling BiDi turns + Selenium's CDP API off (the vendored CDP endpoint stays available); on Firefox, which has no CDP, + this costs nothing. ## Considered options +These are the alternatives considered and not taken; the accepted choice is the decision above. + **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. -- **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). +- **The BiDi module** — internal per [ADR 17670](17670-bidi-implementation-boundaries.md): protocol-shaped, reached differently in each binding, and absent in JavaScript. +- **A dedicated `extensions` namespace** — consistent with `network` / `script`, with room to grow, but two verbs don't earn the indirection; revisit if extension operations grow. + +**Handle type** +- **A raw id string** — untyped; the signature would accept any string. + +**Base64 encoding** +- **Emit the BiDi `path` / `archivePath` variant** — the browser resolves the path, which breaks when a Grid sits between client and browser. +- **Accept only file and directory** — drops the classic base64 entry point, so `installExtension` would no longer be a strict superset for Java and .NET. + +**The classic Firefox methods** +- **Deprecate them now** — the replacement requires BiDi, which is not the default transport, so a + warning would push non-BiDi users toward a method they cannot use without opting in first. +- **Remove them outright** — breaks existing users and violates the deprecation policy. + +**Unsigned 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 -- **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. +- **Extensions can be added and removed mid-session**, on Firefox and Chromium. +- **`installExtension` requires BiDi on every browser** — without it the call errors telling the user + to enable it. Firefox users not ready for BiDi keep using the classic method until then. +- **CDP users switch to the BiDi equivalent, the raw CDP endpoint, or leave BiDi off** — enabling + BiDi disables Selenium's CDP API. +- **Local input is transmitted base64-encoded**, so install works through a Grid. From bfec7589d823bdb2b17111c7e5f3522c6cc4e32a Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 18:51:33 -0500 Subject: [PATCH 06/19] [docs] ADR 17817: tighten decisions to read as decisions --- .../17817-driver-extension-install.md | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 9e9599db34744..949def7602ada 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -20,7 +20,7 @@ options: 1. Require the user to pass arguments in capabilities to switch to the pipe before extensions can be installed. 2. Switch Selenium's default connection to the inherited pipe, which removes support for the CDP API - (Decision 5). + (Decision 3). Over the pipe it is still possible to send CDP commands through the vendored `goog/cdp/execute` endpoint, but no asynchronous behavior (events) is supported. @@ -39,61 +39,62 @@ is what we advertise today. ## Decision -1. **Methods available on the driver.** Every binding exposes `installExtension` and - `uninstallExtension` on the driver instance itself — not on a browser-specific type or the BiDi +1. **Add two methods to the driver instance.** Every binding exposes `installExtension` and + `uninstallExtension` 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 an `Extension` object which wraps the id. + * **Uninstall behavior:** accepts the `Extension` object rather than a raw id. -2. **Install behavior.** `installExtension` accepts a packed archive, an unpacked directory, or - base64 bytes, supports browser-specific install options (on Firefox, `permanent` and - `allowPrivateBrowsing`), and returns an `Extension`. - -3. **Uninstall behavior.** `uninstallExtension` takes the `Extension` that install returned — only - that object, not a raw id, so the call is type-checked; its id is readable as `.id`. A convenience - `extension.uninstall()` is deferred as a non-breaking future upgrade, pending the network handler - ADR ([#17685](https://github.com/SeleniumHQ/selenium/pull/17685)). - -4. **The classic Firefox install methods are not deprecated yet.** `installExtension` requires BiDi, - which is not the default transport, so the classic methods remain the supported path for non-BiDi - sessions and keep working even when BiDi is enabled. They are superseded by `installExtension` and - become deprecation, then removal, candidates once BiDi is the default and the replacement is - available without opting in. In Java, whose classic method is already named `installExtension(Path)`, - the cross-browser method coexists as an overload — a typed source plus a Firefox options object, - distinct from the classic `Path`/`Boolean` signature — so nothing is renamed or broken. Chromium - has nothing here; it never had a session-time install method. - -5. **`installExtension` requires BiDi, and enabling BiDi disables the CDP API.** Once BiDi is - enabled, no further flags or configuration are needed, signed or unsigned. Enabling BiDi turns - Selenium's CDP API off (the vendored CDP endpoint stays available); on Firefox, which has no CDP, - this costs nothing. +2. **Backwards compatible**. These methods must also support WebDriver-Classic functionality for Firefox + when BiDi is not enabled. Any existing methods or parameters for installing addons in Firefox will be + deprecated in favor of the new methods. + +3. **Enabling BiDi disables the CDP API.** Bindings will pass the arguments for `remote-debugging-pipe` + and `enable-unsafe-extension-debugging` when BiDi is enabled to allow users to install extensions + without setting the arguments themselves. This prevents users from accessing the CDP API, + so bindings must throw an exception when attempting to use it. ## Considered options These are the alternatives considered and not taken; the accepted choice is the decision above. **Where the method lives** -- **The BiDi module** — internal per [ADR 17670](17670-bidi-implementation-boundaries.md): protocol-shaped, reached differently in each binding, and absent in JavaScript. -- **A dedicated `extensions` namespace** — consistent with `network` / `script`, with room to grow, but two verbs don't earn the indirection; revisit if extension operations grow. - -**Handle type** +- **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 `extensions` 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 + +**Return type** - **A raw id string** — untyped; the signature would accept any string. +- **Self acting object with `extension.uninstall()`** — out of scope for now -**Base64 encoding** -- **Emit the BiDi `path` / `archivePath` variant** — the browser resolves the path, which breaks when a Grid sits between client and browser. -- **Accept only file and directory** — drops the classic base64 entry point, so `installExtension` would no longer be a strict superset for Java and .NET. - -**The classic Firefox methods** -- **Deprecate them now** — the replacement requires BiDi, which is not the default transport, so a - warning would push non-BiDi users toward a method they cannot use without opting in first. -- **Remove them outright** — breaks existing users and violates the deprecation policy. +**The legacy `installAddon` methods** +- **Redirect `installAddon` to `installExtension` when BiDi is enabled** — keeps the legacy name + working as an alias instead of steering users to `installExtension`, 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 `installExtension` + 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 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. +- **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 -- **Extensions can be added and removed mid-session**, on Firefox and Chromium. -- **`installExtension` requires BiDi on every browser** — without it the call errors telling the user - to enable it. Firefox users not ready for BiDi keep using the classic method until then. -- **CDP users switch to the BiDi equivalent, the raw CDP endpoint, or leave BiDi off** — enabling - BiDi disables Selenium's CDP API. -- **Local input is transmitted base64-encoded**, so install works through a Grid. +- Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 + before sending to target +- Users with CDP implementations wanting to install extensions must switch to BiDi equivalents or make use of + the raw CDP endpoint +- **The deprecation applies differently in Java.** Java's existing method is already named + `installExtension`, so the deprecation lands on its old overloads — `installExtension(Path)` and + `installExtension(Path, Boolean temporary)`, which return an id string — rather than on a + differently-named method. Because return type alone cannot distinguish overloads, the new + `Extension`-returning signature takes a distinct parameter type to coexist with the deprecated + `Path` overload. From 4b825a39289236de434b760979d23e6eaf5ae57e Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 14:18:07 -0500 Subject: [PATCH 07/19] [docs] ADR 17817: note the classic fallback is reduced-capability --- docs/decisions/17817-driver-extension-install.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 949def7602ada..0b60a7618ab03 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -90,6 +90,10 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target +- **The classic fallback is reduced-capability.** Firefox's classic endpoint supports only `temporary` + (i.e. `permanent`), not `allowPrivateBrowsing`. With BiDi off, `installExtension` still installs, but a + BiDi-only option raises rather than being silently ignored, so the base install stays backwards-compatible + while the BiDi-only extras are gated on BiDi. - Users with CDP implementations wanting to install extensions must switch to BiDi equivalents or make use of the raw CDP endpoint - **The deprecation applies differently in Java.** Java's existing method is already named From d28dd8d3d2eca3610769f93bffc692b033d29975 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 15:03:08 -0500 Subject: [PATCH 08/19] [docs] ADR 17817: fix inverted temporary/permanent wording --- docs/decisions/17817-driver-extension-install.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 0b60a7618ab03..8418429281db4 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -90,8 +90,9 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **The classic fallback is reduced-capability.** Firefox's classic endpoint supports only `temporary` - (i.e. `permanent`), not `allowPrivateBrowsing`. With BiDi off, `installExtension` still installs, but a +- **The classic fallback is reduced-capability.** Firefox's classic endpoint accepts only `temporary` + (the inverse of the new `permanent` option), not `allowPrivateBrowsing`. With BiDi off, `installExtension` + still installs, but a BiDi-only option raises rather than being silently ignored, so the base install stays backwards-compatible while the BiDi-only extras are gated on BiDi. - Users with CDP implementations wanting to install extensions must switch to BiDi equivalents or make use of From e894d6ad356c8bb0f93107708270beb89ed6713c Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 16:58:37 -0500 Subject: [PATCH 09/19] [docs] ADR 17817: adopt installWebExtension naming and the web-extension noun --- .../17817-driver-extension-install.md | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 8418429281db4..1632a90d0fc0b 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -1,23 +1,23 @@ -# 17817. The driver installs extensions directly +# 17817. The driver installs web extensions directly - Status: Proposed - Discussion: https://github.com/SeleniumHQ/selenium/pull/17817 ## Context -Firefox can install an extension mid-session through a WebDriver-classic endpoint, but every binding +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 extensions through capabilities applied at session creation. Branded Chrome stopped +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. Chromium can connect to BiDi over either an inherited pipe or a debugging port. Selenium's CDP API -only works through the debugging port, but for security reasons Chromium does not allow installing an -extension over BiDi through the debugging port, only through the inherited pipe. That leaves two +only works through the debugging port, but for security reasons Chromium does not allow installing a +web extension over BiDi through the debugging port, only through the inherited pipe. That leaves two options: -1. Require the user to pass arguments in capabilities to switch to the pipe before extensions can be +1. Require the user to pass arguments in capabilities to switch to the pipe before web extensions can be installed. 2. Switch Selenium's default connection to the inherited pipe, which removes support for the CDP API (Decision 3). @@ -25,7 +25,7 @@ options: Over the pipe it is still possible to send CDP commands through the vendored `goog/cdp/execute` endpoint, but no asynchronous behavior (events) is supported. -WebDriver BiDi specifies extension install and uninstall, which both Firefox and Chromium +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. @@ -39,21 +39,21 @@ is what we advertise today. ## Decision -1. **Add two methods to the driver instance.** Every binding exposes `installExtension` and - `uninstallExtension` on the driver instance itself, not on a browser-specific type or the BiDi +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 an `Extension` object which wraps the id. - * **Uninstall behavior:** accepts the `Extension` object rather than a raw id. - -2. **Backwards compatible**. These methods must also support WebDriver-Classic functionality for Firefox - when BiDi is not enabled. Any existing methods or parameters for installing addons in Firefox will be - deprecated in favor of the new methods. - -3. **Enabling BiDi disables the CDP API.** Bindings will pass the arguments for `remote-debugging-pipe` - and `enable-unsafe-extension-debugging` when BiDi is enabled to allow users to install extensions - without setting the arguments themselves. This prevents users from accessing the CDP API, + * **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**. These methods must also support WebDriver-Classic functionality for Firefox + when BiDi is not enabled. Any existing methods or parameters for installing web extensions in Firefox + will be deprecated in favor of the new methods. + +3. **Enabling BiDi disables the CDP API.** Bindings will pass the arguments for `remote-debugging-pipe` + and `enable-unsafe-extension-debugging` when BiDi is enabled to allow users to install web extensions + without setting the arguments themselves. This prevents users from accessing the CDP API, so bindings must throw an exception when attempting to use it. ## Considered options @@ -66,40 +66,40 @@ These are the alternatives considered and not taken; the accepted choice is the 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 `extensions` 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 +- **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 +**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 `extension.uninstall()`** — out of scope for now +- **Self acting object with `webExtension.uninstall()`** — out of scope for now **The legacy `installAddon` methods** -- **Redirect `installAddon` to `installExtension` when BiDi is enabled** — keeps the legacy name - working as an alias instead of steering users to `installExtension`, so the two names persist +- **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 `installExtension` - is always BiDi implementation. This isn't how we plan to manage other transitions, and we want to - converge on a single common 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 extensions** -- **Require an explicit opt-in** — the browser restriction guards a profile someone actually browses with, +**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 -- Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 +- Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **The classic fallback is reduced-capability.** Firefox's classic endpoint accepts only `temporary` - (the inverse of the new `permanent` option), not `allowPrivateBrowsing`. With BiDi off, `installExtension` - still installs, but a - BiDi-only option raises rather than being silently ignored, so the base install stays backwards-compatible - while the BiDi-only extras are gated on BiDi. -- Users with CDP implementations wanting to install extensions must switch to BiDi equivalents or make use of - the raw CDP endpoint -- **The deprecation applies differently in Java.** Java's existing method is already named - `installExtension`, so the deprecation lands on its old overloads — `installExtension(Path)` and - `installExtension(Path, Boolean temporary)`, which return an id string — rather than on a - differently-named method. Because return type alone cannot distinguish overloads, the new - `Extension`-returning signature takes a distinct parameter type to coexist with the deprecated - `Path` overload. +- **The classic fallback is reduced-capability.** Firefox's classic endpoint accepts only `temporary` + (the inverse of the new `permanent` option), not `allowPrivateBrowsing`. With BiDi off, + `installWebExtension` still installs, but a BiDi-only option raises rather than being silently ignored, + so the base install stays backwards-compatible while the BiDi-only extras are gated on BiDi. +- Users with CDP implementations wanting to install web extensions must switch to BiDi equivalents or + make use of the raw CDP endpoint From 8ac2df19df34d8fb371fa56ef6a8c3561a77e595 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 17:22:59 -0500 Subject: [PATCH 10/19] [docs] ADR 17817: scope the CDP-pipe flags to Chromium sessions --- docs/decisions/17817-driver-extension-install.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 1632a90d0fc0b..a724065fe22f3 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -51,10 +51,10 @@ is what we advertise today. when BiDi is not enabled. Any existing methods or parameters for installing web extensions in Firefox will be deprecated in favor of the new methods. -3. **Enabling BiDi disables the CDP API.** Bindings will pass the arguments for `remote-debugging-pipe` - and `enable-unsafe-extension-debugging` when BiDi is enabled to allow users to install web extensions - without setting the arguments themselves. This prevents users from accessing the CDP API, - so bindings must throw an exception when attempting to use it. +3. **Enabling BiDi disables the CDP API.** For Chromium sessions, bindings pass the arguments for + `remote-debugging-pipe` and `enable-unsafe-extension-debugging` when BiDi is enabled so users can + install web extensions without setting the arguments themselves. This prevents users from accessing + the CDP API, so bindings must throw an exception when attempting to use it. ## Considered options From 183e0ad581feb44e5924daff2f722ca31197d700 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 17:44:34 -0500 Subject: [PATCH 11/19] [docs] ADR 17817: correct the classic private-browsing behavior --- docs/decisions/17817-driver-extension-install.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index a724065fe22f3..dbb68d56fe5d0 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -97,9 +97,10 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **The classic fallback is reduced-capability.** Firefox's classic endpoint accepts only `temporary` - (the inverse of the new `permanent` option), not `allowPrivateBrowsing`. With BiDi off, - `installWebExtension` still installs, but a BiDi-only option raises rather than being silently ignored, - so the base install stays backwards-compatible while the BiDi-only extras are gated on BiDi. +- **The classic fallback cannot disable private browsing.** Firefox's classic endpoint always installs + a web extension with private-browsing access and exposes no toggle, so with BiDi off + `allowPrivateBrowsing` is effectively always `true`. `allowPrivateBrowsing: true` (or unspecified) is + satisfied; explicitly passing `allowPrivateBrowsing: false` raises rather than silently enabling it + anyway. - Users with CDP implementations wanting to install web extensions must switch to BiDi equivalents or make use of the raw CDP endpoint From 192c35b984feade480c80af7ebc6ae14eff705aa Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 18:18:45 -0500 Subject: [PATCH 12/19] [docs] ADR 17817: require bindings to reject allowPrivateBrowsing:false on classic --- docs/decisions/17817-driver-extension-install.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index dbb68d56fe5d0..d2062a6dfb666 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -98,9 +98,9 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target - **The classic fallback cannot disable private browsing.** Firefox's classic endpoint always installs - a web extension with private-browsing access and exposes no toggle, so with BiDi off - `allowPrivateBrowsing` is effectively always `true`. `allowPrivateBrowsing: true` (or unspecified) is - satisfied; explicitly passing `allowPrivateBrowsing: false` raises rather than silently enabling it - anyway. + a web extension with private-browsing access and exposes no toggle, so `allowPrivateBrowsing: true` + (or unspecified) is satisfied on a non-BiDi session. Because the classic `/moz/addon/install` payload + cannot represent the option, a binding must validate `allowPrivateBrowsing: false` and throw before + delegating to classic, rather than silently installing with private-browsing access anyway. - Users with CDP implementations wanting to install web extensions must switch to BiDi equivalents or make use of the raw CDP endpoint From 8119540b45a6f6e2f0897577f3ba3dad46678a7f Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 07:57:32 -0500 Subject: [PATCH 13/19] [docs] ADR 17817: drop the CDP tradeoff, obsoleted by Chrome 149 removing the pipe requirement --- .../17817-driver-extension-install.md | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index d2062a6dfb666..f35f22e90a2c5 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -12,19 +12,6 @@ Chromium takes web extensions through capabilities applied at session creation. honoring that path in Chrome 137 (Chrome for Testing and unbranded Chromium still do), so installing after the session starts is now a requirement. -Chromium can connect to BiDi over either an inherited pipe or a debugging port. Selenium's CDP API -only works through the debugging port, but for security reasons Chromium does not allow installing a -web extension over BiDi through the debugging port, only through the inherited pipe. That leaves two -options: - -1. Require the user to pass arguments in capabilities to switch to the pipe before web extensions can be - installed. -2. Switch Selenium's default connection to the inherited pipe, which removes support for the CDP API - (Decision 3). - -Over the pipe it is still possible to send CDP commands through the vendored `goog/cdp/execute` -endpoint, but no asynchronous behavior (events) is supported. - 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. @@ -51,11 +38,6 @@ is what we advertise today. when BiDi is not enabled. Any existing methods or parameters for installing web extensions in Firefox will be deprecated in favor of the new methods. -3. **Enabling BiDi disables the CDP API.** For Chromium sessions, bindings pass the arguments for - `remote-debugging-pipe` and `enable-unsafe-extension-debugging` when BiDi is enabled so users can - install web extensions without setting the arguments themselves. This prevents users from accessing - the CDP API, so bindings must throw an exception when attempting to use it. - ## Considered options These are the alternatives considered and not taken; the accepted choice is the decision above. @@ -102,5 +84,3 @@ These are the alternatives considered and not taken; the accepted choice is the (or unspecified) is satisfied on a non-BiDi session. Because the classic `/moz/addon/install` payload cannot represent the option, a binding must validate `allowPrivateBrowsing: false` and throw before delegating to classic, rather than silently installing with private-browsing access anyway. -- Users with CDP implementations wanting to install web extensions must switch to BiDi equivalents or - make use of the raw CDP endpoint From 1969b324aa7c42b6add38dc4d39bd7d6ed04874d Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 08:04:17 -0500 Subject: [PATCH 14/19] [docs] ADR 17817: require BiDi for installWebExtension on Chrome --- docs/decisions/17817-driver-extension-install.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index f35f22e90a2c5..e13270fd8aff3 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -38,6 +38,10 @@ is what we advertise today. when BiDi is not enabled. Any existing methods or parameters for installing web extensions in Firefox will be deprecated in favor of the new methods. +3. **The methods are always present; on Chrome they require BiDi.** They are never conditionally hidden + per session. Chrome has no classic install path, so `installWebExtension` raises there when BiDi is + not enabled. + ## Considered options These are the alternatives considered and not taken; the accepted choice is the decision above. @@ -52,6 +56,11 @@ These are the alternatives considered and not taken; the accepted choice is the 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 Chrome 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. + **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 From e23bc4ec8e0beda79bcab4cb7cace7b51ccb1444 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 11:44:15 -0500 Subject: [PATCH 15/19] [docs] ADR 17817: split backwards-compat from the raise-when-unsupported rule --- .../17817-driver-extension-install.md | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index e13270fd8aff3..77235524327c1 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -34,13 +34,10 @@ is what we advertise today. The method returns a `WebExtension` object which wraps the id. * **Uninstall behavior:** accepts the `WebExtension` object rather than a raw id. -2. **Backwards compatible**. These methods must also support WebDriver-Classic functionality for Firefox - when BiDi is not enabled. Any existing methods or parameters for installing web extensions in Firefox - will be deprecated in favor of the new methods. - -3. **The methods are always present; on Chrome they require BiDi.** They are never conditionally hidden - per session. Chrome has no classic install path, so `installWebExtension` raises there when BiDi is - not enabled. +2. **Backwards compatible**. On Firefox these methods fall back to the WebDriver-Classic endpoint when + BiDi is not enabled, and the existing classic install methods and parameters are deprecated in favor + of them. Chrome has no classic install path to preserve, so there `installWebExtension` requires BiDi + and raises when it is not enabled rather than being conditionally hidden. ## Considered options @@ -88,8 +85,9 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **The classic fallback cannot disable private browsing.** Firefox's classic endpoint always installs - a web extension with private-browsing access and exposes no toggle, so `allowPrivateBrowsing: true` - (or unspecified) is satisfied on a non-BiDi session. Because the classic `/moz/addon/install` payload - cannot represent the option, a binding must validate `allowPrivateBrowsing: false` and throw before - delegating to classic, rather than silently installing with private-browsing access anyway. +- **A vendor option the target can't honor raises; it is never silently dropped.** `permanent` and + `allowPrivateBrowsing` are Firefox-only, so a binding raises if either is passed on Chrome. And on a + non-BiDi Firefox session, the classic `/moz/addon/install` endpoint always installs with + private-browsing access and cannot represent `allowPrivateBrowsing: false`, so a binding validates and + raises before delegating to classic rather than installing with access anyway (`true` or unspecified is + satisfied by the classic default). From 2c7d7e80c8209d91325b45f5491293be9a339dbd Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 12:33:26 -0500 Subject: [PATCH 16/19] [docs] ADR 17817: scope vendor options to the vendor driver, drop the vendor-option raise --- .../17817-driver-extension-install.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 77235524327c1..669d164ebe244 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -35,9 +35,12 @@ is what we advertise today. * **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, and the existing classic install methods and parameters are deprecated in favor - of them. Chrome has no classic install path to preserve, so there `installWebExtension` requires BiDi - and raises when it is not enabled rather than being conditionally hidden. + 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 Chrome without + BiDi. ## Considered options @@ -85,9 +88,6 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **A vendor option the target can't honor raises; it is never silently dropped.** `permanent` and - `allowPrivateBrowsing` are Firefox-only, so a binding raises if either is passed on Chrome. And on a - non-BiDi Firefox session, the classic `/moz/addon/install` endpoint always installs with - private-browsing access and cannot represent `allowPrivateBrowsing: false`, so a binding validates and - raises before delegating to classic rather than installing with access anyway (`true` or unspecified is - satisfied by the classic default). +- **Vendor-specific options are only made available on that vendor's driver.** Options like Firefox's + `permanent` and `allowPrivateBrowsing` are exposed only on that vendor's driver surface, so a session + for another browser has no way to pass them — there is nothing to silently drop or raise. From f224e6ed4786eb401e812db2b35489a51d435f8b Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 13:06:27 -0500 Subject: [PATCH 17/19] [docs] ADR 17817: drop the vendor-option consequence, Decision 1 already scopes it --- docs/decisions/17817-driver-extension-install.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 669d164ebe244..1bbe9cd212197 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -88,6 +88,3 @@ These are the alternatives considered and not taken; the accepted choice is the - Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 before sending to target -- **Vendor-specific options are only made available on that vendor's driver.** Options like Firefox's - `permanent` and `allowPrivateBrowsing` are exposed only on that vendor's driver surface, so a session - for another browser has no way to pass them — there is nothing to silently drop or raise. From fdffcad451edfc08a5d6640e41fe9ea73d0d8f59 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 13 Aug 2026 12:18:58 -0500 Subject: [PATCH 18/19] [docs] ADR 17817: make the Grid consequence transport-neutral --- docs/decisions/17817-driver-extension-install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index 1bbe9cd212197..e36d70dbcbb31 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -86,5 +86,5 @@ These are the alternatives considered and not taken; the accepted choice is the ## Consequences -- Since the implementation must work with the Grid, bindings will have to convert path or archive to Base64 - before sending to target +- Because the browser may run on a different host than the client (Grid), the implementation cannot pass a + client-local path; the extension has to be delivered to the target host itself. From 2c90f41fd96f46bbeb2373b4f36f421deee68978 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 21 Aug 2026 08:24:05 -0500 Subject: [PATCH 19/19] [docs] ADR 17817: clarify remote-end delivery and Chromium scoping --- docs/decisions/17817-driver-extension-install.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/decisions/17817-driver-extension-install.md b/docs/decisions/17817-driver-extension-install.md index e36d70dbcbb31..b6ae1beb86377 100644 --- a/docs/decisions/17817-driver-extension-install.md +++ b/docs/decisions/17817-driver-extension-install.md @@ -39,7 +39,7 @@ is what we advertise today. 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 Chrome without + request, it raises rather than silently doing less — such as `installWebExtension` on Chromium without BiDi. ## Considered options @@ -57,7 +57,7 @@ These are the alternatives considered and not taken; the accepted choice is the method on the driver directly **Conditional availability** -- **Expose the method only where it works** — hide it on a Chrome session without BiDi rather than +- **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. @@ -86,5 +86,7 @@ These are the alternatives considered and not taken; the accepted choice is the ## Consequences -- Because the browser may run on a different host than the client (Grid), the implementation cannot pass a - client-local path; the extension has to be delivered to the target host itself. +- 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.