Skip to main content
Proposed

This page summarizes the design of the plugin platform, version 2. The full record, with the checks behind each decision, is ADR 0001 in the plugins repository (number 005 in this repository’s series).

Note

The ADR is proposed. Only phase 1 of its plan has begun: the plugin API crate, the built-in plugins crate with shield, and the core side that uses them (see What is built and What core does with it). Later phases are a plan, not a promise of dates.

The design in short#

  • One Plugin trait with more hooks, all with default bodies. The API crate stays on 0.1.x and changes only by addition, so existing plugins keep working as “legacy” plugins.
  • A plugin may describe itself with metadata(): a category, a phase, the capabilities it needs, and ordering edges.
  • Capabilities are declared by each plugin and checked when the set is built; for a native Rust plugin they are a contract, not a sandbox (see Capabilities).
  • Built-in plugins ship inside the prebuilt binaries and are turned on by name.
  • Third-party code that is not compiled in comes as WebAssembly or as a TOML manifest. Native code is never loaded at run time.

Five categories#

CategoryForRuns in phase
EnvironmentPresets, runtime, switches, proxies, headlessPrepare
TestingAssertions, lifecycle, reporters, recording fakesObserve
ExtensionCRX and unpacked extensions, background workersNormal
CoreInterception, network logging, resource blockingNormal
PrivacyHardening, telemetry, WebRTC, cookie isolationEnforce

Capabilities#

A capability is one thing a plugin may do, written as a stable string such as launch.args, net.observe or page.script:<hosts>. A scoped enum, not a bit set, because some capabilities carry a scope (hosts, paths).

RiskExamples
Lowlaunch.args, page.script:<hosts>, net.observe, test.hooks
Mediumlaunch.prefs, launch.headless, page.evaluate, net.intercept, contexts
Highpage.script:*, cookies.*, net.bodies, launch.proxy, launch.extensions, host.fs.*
Criticalraw:*, host.process, cdp:Target, cdp:Browser

How a set grants them:

  • PluginSet::with grants what the plugin declares, except Critical ones.
  • with_trusted must name each Critical capability.
  • with_grants gives exactly the grants it is passed, as the command line does from a lockfile.
  • A strict set refuses plugins that declare no metadata.
Warning

Capabilities are a contract at the API surface. For a native Rust plugin they are not a sandbox: it can still call std directly. Only WebAssembly modules are sandboxed.

Lifecycle and ordering#

Hooks run in this order of precedence: the plugin’s phase (Prepare, Normal, Enforce, Observe, Last), then requires, after and before edges, then insertion order. A set of plugins without metadata therefore keeps today’s order, with RUN_LAST plugins at the end. A launch field a plugin seals cannot later be changed to a different value by another plugin. An error in a launch or page hook stops that launch or tab.

Distribution#

WhoInstallEnable
Rust, built-inA feature such as plugin-shieldBuilder call, or a name in [plugins] / SB_PLUGINS
Rust, third-partycargo add the cratePluginSet::with, LaunchOptionsBuilder::plugins or BrowserConfig::with_plugins, or by name through a Registry
Prebuilt sbrs and sbrs-mcpNothing, built-ins are compiled in--plugin NAME, --plugin-opt NAME.KEY=<TOML>; sbrs plugin list and sbrs plugin info NAME show them
WASM or manifest (planned)sbrs plugin add into a content-addressed store, not built yet: sbrs plugin has only list and infoBy the name it declares

WASM and manifest plugins are pinned by SHA-256 in a lockfile and their capabilities are approved at install. A project’s own lock holds pins only, never grants. WASM ABI 2 puts a JSON manifest in a custom section and runs the module once at load; event mode (ABI 2.1) comes later.

The seven example plugins#

The ADR specifies seven built-ins that exercise the platform: shield and tor (privacy), fetcher and extractor (core), batcher (environment), crawler (testing) and browser (extension).

What is built#

Phase 1 starts in the API crate seleniumbase-rs-plugin, all additive:

PieceFile
Capability, CapabilitySet and string formscapability.rs
Category, Phase, Seals, Metadatametadata.rs
Ordering of a setorder.rs
The refused-switch list, applied to every pluginswitch_policy.rs
HostInfo, which capabilities each host supportshost.rs
Configurable, Descriptor, Registry, PluginSpecconfig.rs
Plugin::metadata(), Plugin::per_launch(), PluginSet::strict(), with_trusted, with_grants, audit()lib.rs

Next to the API crate there is the seleniumbase-rs-plugins-builtin crate (one cargo feature per plugin, and registry()), and the first plugin written for the new API, shield. How a program turns them on is in Choosing plugins.

Not part of this page’s claim: the store, manifests, WASM ABI 2 and the other six plugins belong to the later work. The plan, from the ADR:

PhaseDelivers
0Security hotfix: one refused-switch list for all plugins
1Metadata, registry, CLI and config surface, release features, shield
2CDP lifecycle hooks, the shared request router, fetcher, extractor, tor
3Tasks, test hooks, store and lockfile, manifest plugins, WASM ABI 2.0
4WebDriver parity, ABI 2.1 events, CRX3 signature checks

To declare metadata in your own plugin, see Writing your own plugin.

What core does with it#

The plugins repository listed seven things seleniumbase-rs had to do before the new API was useful. They are done, in both engines unless an item says otherwise:

1. The API enums are `#[non_exhaustive]`

A match on BrowserFamily, PluginError or a WebRTC policy has a wildcard arm. PluginError variants that name a plugin (Unsupported, Capability, Undeclared, Conflict, Sealed) now carry its name into SeleniumBaseError::Plugin, and the hint explains the new refusals.

2. Pooled browsers shared per-launch state

PluginSet::for_launch() runs before every check, so each browser, each attach and each WebDriver session gets a fresh instance of a plugin that offers per_launch; a shared set no longer shares state, and on_close reaches the instance that started.

3. The host was always the CDP engine

check_with(&LaunchContext, &HostInfo): HostInfo::cdp() for the pure CDP engine, HostInfo::webdriver(family) for chromedriver, geckodriver and Safari. A server that answers browserName: tauri is re-checked as HostInfo::embedded() once the session exists, and a plugin that needs more than a webview gives ends the session.

4. New `LaunchSettings` fields were dropped

host_resolver_rules is one --host-resolver-rules; disable_features and enable_features are one switch each, merged with the library’s own Translate (and with a switch the configuration already has); webrtc_policy is --force-webrtc-ip-handling-policy; disable_quic is --disable-quic. The profile is shown to plugins before they run, as persistent when user_data_dir is set. On Firefox and Safari a plugin that sets one of the Chromium-only fields is refused.

5a. `Network.setBlockedURLs` replaced the tab's list

The host keeps one blocklist per tab (per window with chromedriver): the library’s ad-block list, the tracker hosts of a fingerprint with block_trackers, and every pattern a plugin sent. Each raw send adds to it, and the union goes out. With chromedriver a window keeps its union when the session switches away and back.

5b. The `Fetch` domain had many owners

A plugin’s Fetch.* command, and the older Network.setRequestInterception that pauses requests the same way, are refused, on the browser and on a tab. The library’s own Fetch.enable for proxy passwords is not a plugin’s and stays.

6. Extension ids were dropped

Browser::extension_ids() keeps what Extensions.loadUnpacked answered, in load order.

7. Two lists of refused switches

A test reads FORBIDDEN_SWITCHES of src/stealth/evasions.rs and fails if one is missing from seleniumbase_rs_plugin::switch_policy::REFUSED_SWITCHES.

How these were checked#

On the scripted mock browser of the pure CDP engine (Browser::launch_mocked) and on a mock WebDriver server. They show which commands the host sends, in which order, and what a plugin is refused.

Warning

No real Chrome ran in this change, so nothing here shows that Chrome accepts the merged switches or enforces a merged blocklist in iframes, workers and popups; the plugins’ own docs say the same.

Limits#

  • The guard on raw commands is two rules: Fetch.* and Network.setRequestInterception are refused, and Network.setBlockedURLs is merged. Other commands go to the browser as sent. A plugin that reaches Fetch by wrapping it in Target.sendMessageToTarget is not stopped yet; the guarded handles of phase 2 check every command.
  • The blocklist is a union: patterns only get added. A plugin cannot take another plugin’s pattern back. Phase 2 keeps one list per plugin.
  • A plugin that declares no metadata is treated as holding the legacy capabilities, raw:cdp among them, which the host of Firefox, Safari and a Tauri webview cannot give. Checked strictly, every such plugin would be refused there, though they have always run (their send fails when called). So a WebDriver set that holds one is checked as for Chromium’s WebDriver, and skips the embedded re-check; a set of declared plugins only gets the browser’s own host. A mixed set therefore finds a declared plugin’s missing DevTools channel when it calls send, not at the check.
  • The Tauri re-check reads the name tauri from the session’s capabilities, as tauri-plugin-sbrs-automation answers it. Another embedded webview server is seen as the browser the configuration names.