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).
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
Plugintrait 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#
| Category | For | Runs in phase |
|---|---|---|
| Environment | Presets, runtime, switches, proxies, headless | Prepare |
| Testing | Assertions, lifecycle, reporters, recording fakes | Observe |
| Extension | CRX and unpacked extensions, background workers | Normal |
| Core | Interception, network logging, resource blocking | Normal |
| Privacy | Hardening, telemetry, WebRTC, cookie isolation | Enforce |
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).
| Risk | Examples |
|---|---|
| Low | launch.args, page.script:<hosts>, net.observe, test.hooks |
| Medium | launch.prefs, launch.headless, page.evaluate, net.intercept, contexts |
| High | page.script:*, cookies.*, net.bodies, launch.proxy, launch.extensions, host.fs.* |
| Critical | raw:*, host.process, cdp:Target, cdp:Browser |
How a set grants them:
PluginSet::withgrants what the plugin declares, except Critical ones.with_trustedmust name each Critical capability.with_grantsgives exactly the grants it is passed, as the command line does from a lockfile.- A strict set refuses plugins that declare no metadata.
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#
| Who | Install | Enable |
|---|---|---|
| Rust, built-in | A feature such as plugin-shield | Builder call, or a name in [plugins] / SB_PLUGINS |
| Rust, third-party | cargo add the crate | PluginSet::with, LaunchOptionsBuilder::plugins or BrowserConfig::with_plugins, or by name through a Registry |
Prebuilt sbrs and sbrs-mcp | Nothing, 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 info | By 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:
| Piece | File |
|---|---|
Capability, CapabilitySet and string forms | capability.rs |
Category, Phase, Seals, Metadata | metadata.rs |
| Ordering of a set | order.rs |
| The refused-switch list, applied to every plugin | switch_policy.rs |
HostInfo, which capabilities each host supports | host.rs |
Configurable, Descriptor, Registry, PluginSpec | config.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:
| Phase | Delivers |
|---|---|
| 0 | Security hotfix: one refused-switch list for all plugins |
| 1 | Metadata, registry, CLI and config surface, release features, shield |
| 2 | CDP lifecycle hooks, the shared request router, fetcher, extractor, tor |
| 3 | Tasks, test hooks, store and lockfile, manifest plugins, WASM ABI 2.0 |
| 4 | WebDriver 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.
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.
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.*andNetwork.setRequestInterceptionare refused, andNetwork.setBlockedURLsis merged. Other commands go to the browser as sent. A plugin that reachesFetchby wrapping it inTarget.sendMessageToTargetis 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:cdpamong 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 (theirsendfails 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 callssend, not at the check. - The Tauri re-check reads the name
taurifrom the session’s capabilities, as tauri-plugin-sbrs-automation answers it. Another embedded webview server is seen as the browser the configuration names.