Skip to main content
Early Feature: plugins

A plugin is a Rust type that changes how a browser starts and what every tab gets. This page builds one, from an empty folder to a tested crate that a program turns on by name. For turning plugins on and attaching a set to a browser, read the Browser Plugins guide. This page is about the plugin itself.

Info

What you will learn

  • What a plugin can do, and when each of its hooks runs
  • How to build a plugin crate step by step, with an option
  • How to test it without a browser, and against the mock browser
  • How to turn it on from Rust, from the settings file and from the command line
  • How to declare what it needs, so a strict set accepts it
  • How to package and share it, or write it in WebAssembly instead
Quick reference
  • API crate: seleniumbase-rs-plugin
  • From seleniumbase-rs: seleniumbase_rs::plugin_api, feature plugins
  • Required: fn name(&self) -> &str
  • Hooks: before_launch, on_browser, on_page_created, on_close
  • By name: Configurable, Descriptor, Registry
  • Tests: seleniumbase-rs-plugin-testing
  • Pin: the rev that seleniumbase-rs pins

A plugin#

A plugin is a type that implements the Plugin trait. name is the only method you must write; every hook has a default that does nothing. This one logs a line in the console of every page:

src/lib.rs

A plugin crate imports the API from seleniumbase_rs_plugin. In a program that already depends on seleniumbase-rs, the same items are seleniumbase_rs::plugin_api (feature plugins). A plugin never sees sb_cdp::Browser, Page or BaseCase. The browser and the tab reach it as two small traits, BrowserHandle and PageHandle, which both engines implement, so one plugin works with the pure CDP engine and with the WebDriver engine.

What a plugin can do, and where:

You want toUse
Add browser switches, preferences, extensions or a proxy, or change headless modebefore_launch
Send a browser-level DevTools commandon_browser
Add a script or a stylesheet to every pagepackages, or add_init_script in on_page_created
Send a DevTools command to every tab, such as blocking URLs or setting the time zoneon_page_created
Clean up, such as deleting temporary folderson_close
Refuse a browser it cannot work withrequirements
Say what it is and what it needsmetadata
Run before or after other pluginsdependencies, or the ordering edges of metadata

A DevTools command reaches only Chromium browsers. On Firefox and Safari, send returns an error.

How a plugin runs#

The set of plugins checks every plugin before anything starts. Then it calls the hooks in order, and each hook gets a handle to what exists at that point:

The hooks of one browser, from start to close

HookReceivesRunsWhen it fails
before_launch&mut LaunchSettingsBefore the browser starts. Not when the program attaches to a running browserThe launch stops
on_browser&dyn BrowserHandleOnce the browser runs, started or attachedThe launch stops
packages, then on_page_created&dyn PageHandleFor every tab or window the library attaches to, before your program uses itThe tab stops
on_closenothingOnce per launch, when the browser closes or is droppedThe error is logged, and the other plugins still run

Errors fail closed: a plugin that did not run must not leave a browser without what it adds. They come back as SeleniumBaseError::Plugin, which names the plugin and the hook. At each point, plugins run in the order of their phase, then of their requires, after and before edges, then in the order they were added. The exact points for each engine are in When the hooks run, The WebDriver engine and When a plugin fails.

Write your own#

You will build hello, a plugin that logs a message of your choice in every page. It reads one option, message, has tests that need no browser, and can be turned on by name.

Create the crate#

Copy the template/ folder of the plugins repository (see The template), or run cargo new --lib. Name the crate seleniumbase-rs-plugin-<name>.

seleniumbase-rs-plugin-hellothe plugin crate
Cargo.tomlthe plugin API, serde_json and the test fakes
srcthe plugin
lib.rsHello, its options and its unit tests
testsoptional
mock_browser.rsthe real launch steps against the mock browser
Cargo.toml

The crate depends on the plugin API, and on serde_json because options and DevTools parameters are JSON values. It never depends on seleniumbase-rs.

Warning

Name the rev that your seleniumbase-rs pins: the rev of seleniumbase-rs-plugin in its Cargo.toml, currently 628991af0e6c5711298e3bd3692dc2974887401a. Cargo counts a git repository under two references as two crates, and then your Plugin is not the trait seleniumbase-rs calls. See Cargo features.

Implement the Plugin trait#

src/lib.rs
  • #[async_trait] comes from the API crate, so the crate needs no dependency of its own for it.
  • add_init_script runs the script in every document the tab loads from now on, before the page’s own scripts on Chromium browsers. It returns a Result: an error goes up to the set, which stops the tab and names the plugin.
  • metadata says what the plugin is (a Testing plugin) and what it needs (a script on every page). Without it the plugin is a “legacy” plugin, which a strict set refuses. More in Declaring metadata and capabilities.

Read its options#

To be turned on by name, with options from a settings file or the command line, a plugin implements Configurable. It names the plugin, describes it without building one, and builds it from its options:

src/lib.rs

Then let name and metadata read from it, so each is written once:

src/lib.rs

options is Value::Null when no option was given, and otherwise the plugin’s table as JSON. Refuse a key you do not know: a typo must not turn a setting off without a word. The Options reference has the rules.

Test it without a browser#

seleniumbase-rs-plugin-testing has fakes of the two handles. RecordingPage and RecordingBrowser write down every command, script and expression a hook sends. block_on runs a hook without an async runtime, because the fakes never wait.

src/lib.rs
Terminal

RecordingPage::new().replying(method, value) answers a command with a value, and .failing(method) makes it fail. RecordingPage::on(Engine::WebDriver) and RecordingBrowser::of(engine, family) stand in for the other engine and for other browsers.

Check

cargo test passes: the plugin adds its script, names itself when it fails, satisfies a strict set and reads its options.

The whole file, tests included:

src/lib.rs

Register it and turn it on#

A program adds the plugin crate next to seleniumbase-rs:

Cargo.toml

The names that the settings file, SB_PLUGINS, plugins_named and plugin_selection accept come from the built-in registry only. A plugin of your own joins by name through a registry of your own: plugin_host::builtin_registry().with(Descriptor::of::<Hello>()), and then plugin_host::build_with.

Build a set and give it to a launch. PluginSet::with grants what the plugin declares, except Critical capabilities.

Check what it declares#

sbrs plugin info NAME prints what a plugin declares, for the plugins of the sbrs build. For your own registry the same text comes from plugin_cmd::info_with, and the table of sbrs plugin list from list_with:

Read it as a reviewer would: the category and phase, each capability with its risk, what the plugin needs from the browser, and the options in effect. PluginSet::audit() returns the same capabilities as rows (granted or not, risk, enforcement) for a test to check.

Ask an AI coding agent to start a plugin from this page

Create a seleniumbase-rs plugin crate named seleniumbase-rs-plugin-NAME from the template folder of github.com/MustCodeAl/seleniumbase-rs-plugins. Depend on seleniumbase-rs-plugin at the same git rev that my seleniumbase-rs pins. Implement the Plugin trait with only the hooks the plugin needs, declare its metadata (category, version, summary, capabilities), implement Configurable so that it refuses unknown options, and test it with RecordingPage from seleniumbase-rs-plugin-testing. The plugin should: DESCRIBE WHAT IT DOES.

Hook reference#

Every method of Plugin but name has a default. The hooks are async (through #[async_trait]) and return Result<()>. An error from any hook but on_close stops the launch or the tab.

namefn(&self) -> &strrequired#

The plugin’s unique, lower-case name, such as block-resources. A set refuses a second plugin with the same name. With Configurable, return Self::NAME.

metadatafn(&self) -> Metadata#

What the plugin is and what it needs. See Declaring metadata and capabilities.

Default: "Metadata::legacy()"

requirementsfn(&self) -> Requirements#

Conditions the set checks before anything starts. Combine flags with |. A refusal is an error, not a warning.

Default: "Requirements::empty()"

dependenciesfn(&self) -> &[&str]#

The names of plugins that must be in the same set. They run before this one, and a missing one stops the launch.

Default: "&[]"

packagesfn(&self) -> Vec<Package>#

JavaScript and CSS for every new tab, added before on_page_created runs. Each package must say where it loads, or the set refuses the plugin.

Default: "none"

per_launchfn(&self) -> Option<Arc<dyn Plugin>>#

A fresh instance for each browser, so pooled browsers launched from one set do not share state. It must have the same name. crx uses it so that closing one browser does not delete the folders of another.

Default: "None"

before_launchasync fn(&self, launch: &mut LaunchSettings) -> Result<()>#

Changes the start-up settings before the browser starts. It does not run when the program attaches to a running browser. After each plugin, the set checks the change: it needs the matching capability, lists only grow, and a field another plugin sealed keeps its value.

On Firefox and Safari a plugin that sets one of the Chromium-only fields is refused; what each browser accepts is in The WebDriver engine.

on_browserasync fn(&self, browser: &dyn BrowserHandle) -> Result<()>#

The browser runs, started or attached to. Use it for browser-level commands.

on_page_createdasync fn(&self, page: &dyn PageHandle) -> Result<()>#

A tab or window was attached, before any command of your program runs in it. The plugin’s packages are already added.

on_closeasync fn(&self) -> Result<()>#

The browser is about to close. Every plugin runs even when one fails; the errors are logged and do not stop the close.

Declaring metadata and capabilities#

A plugin can say what it is and what it needs by overriding metadata(). The default is Metadata::legacy(), which keeps older plugins working: no category, and the legacy capabilities, raw:cdp among them. A strict set refuses such a plugin.

src/lib.rs

Metadata is a builder:

MethodWhat it says
Metadata::new(category)A declared plugin of this category; the category sets the default phase
.version(v), .summary(s)Shown by sbrs plugin list and sbrs plugin info
.capability(c), .capabilities([...])What it needs. The check fails when the engine cannot give it or the set did not grant it
.optional(c)What it uses when granted. It never fails the check
.requires(name)A plugin that must be in the set and run before it
.conflicts_with(name)A plugin that may not be in the same set
.after(name), .before(name)Its order against a plugin, when that plugin is present
.phase(p)A phase other than the category’s
.seal(seals)Scalar launch fields (Seals::PROXY, Seals::WEBRTC, Seals::HEADLESS, …) that later plugins may not change to another value
.budget(budgets)Time limits for before_launch and on_page_created; stored, not enforced yet

Without phase, the category sets it (Category::default_phase):

CategoryForDefault phase
EnvironmentPresets, switches, proxies, headlessPrepare, first
ExtensionExtensions and their workersNormal
CoreInterception, network logging, resource blockingNormal
PrivacyHardening, telemetry, WebRTC, cookie isolationEnforce, after the others
TestingAssertions, lifecycle, reporters, recording fakesObserve

Requirements::RUN_LAST puts a plugin in the phase Last.

Declare the capability that matches what the plugin does:

Your pluginDeclares
Adds to args, disable_features or enable_featuresCapability::LaunchArgs (launch.args)
Sets prefsCapability::LaunchPrefs (launch.prefs)
Sets host_resolver_rules, webrtc_policy or disable_quicCapability::LaunchNetwork (launch.network)
Sets proxyCapability::LaunchProxy (launch.proxy)
Adds extensionsCapability::LaunchExtensions (launch.extensions)
Changes headlessCapability::LaunchHeadless (launch.headless)
Adds scripts or packages to pagesCapability::PageScript(HostScope::Hosts(...)), or HostScope::Everywhere (page.script:*)
Calls evaluateCapability::PageEvaluate (page.evaluate)
Sends the commands of one DevTools domainCapability::Cdp("Network".into()) (cdp:Network)
Reads or writes filesCapability::HostFsRead(...), Capability::HostFsWrite(PathScope::Temp)

The full list, with the risk of each, is on the architecture page.

A set grants capabilities when you add a plugin:

  • PluginSet::new().with(p) grants what p declares, except Critical capabilities.
  • with_trusted(p, &[Capability]) also grants the Critical ones you name.
  • with_grants(p, set) grants exactly set.
  • PluginSet::strict() refuses plugins that declare no metadata. The command line and the MCP server use strict sets.

The Critical capabilities are raw:*, host.process, cdp:Target and cdp:Browser. A set built from names never grants them, so a plugin that requires one is added in code, with with_trusted.

The set checks the required capabilities before the launch, against its grants and against what the engine can give: WebDriver has no network observation, and Firefox and Safari have no DevTools channel. It checks each change of before_launch right after the plugin runs. For DevTools commands, the host today refuses Fetch.* and merges Network.setBlockedURLs; other commands go to the browser as sent (see Limits).

Warning

For a native Rust plugin, capabilities are a declared contract, not a sandbox. Only WebAssembly plugins are sandboxed.

Options reference#

Options let one plugin serve many runs. The host reads them from the settings file, from --plugin-opt or from code, and hands them to Configurable::configure as one JSON value. SB_PLUGINS carries names only.

Configurable::NAMEconst &'static strrequired#

The name the plugin is enabled by. Return the same string from Plugin::name.

Configurable::describe()fn() -> Metadatarequired#

What the plugin declares, without building one. sbrs plugin list and sbrs plugin info read it, and so does the check that a project’s settings file cannot turn on a plugin with launch.proxy, artifacts.write or host.* alone.

Configurable::configure(options)fn(&Value) -> Result<Self>required#

Builds the plugin. Return PluginError::Config for an option that is wrong; the host adds the plugin’s name and stops before any browser starts.

Document the options on the plugin’s page, as the built-in plugins do (shield): sbrs plugin info cannot list them, because the plugin API does not describe options yet. For hello:

messagestring#

The text every page logs in its console.

Default: "hello from a plugin"

A plugin with several options can read them with serde, as shield does: #[serde(default, deny_unknown_fields)] turns an unknown key into an error. It needs serde with the derive feature:

src/lib.rs

To make a plugin known by name, put Descriptor::of::<P>() into a Registry. Registry::build(&PluginSpec) builds one plugin from a name and options, and Registry::plugin_set(&[PluginSpec]) builds a strict set. plugin_host::build_with does the same from a PluginSelection and reports a mistake as SeleniumBaseError::Plugin.

Worked example: block-resources#

The built-in block-resources plugin is a whole plugin in about a hundred lines. The tabs show its parts and those of crx, which works before the browser starts.

plugins/block-resources/src/lib.rs (abridged)

What to copy from it:

  • It sends raw commands with page.send, so it says Requirements::CHROMIUM_ONLY, and it declares the one domain it uses, cdp:Network, not raw:cdp.
  • It sends Network.enable first, then Network.setBlockedURLs. The host merges the list with those of the other plugins and the ad_block list.
  • configure builds the default preset from no options, and refuses a preset it does not know and a patterns value that is not an array of strings.

Its test checks the exact commands with RecordingPage:

plugins/block-resources/src/lib.rs (test)

The constructors and the other tests are in its source.

shield shows the rest: packages, a before_launch that sets preferences and network settings, and options read with serde.

Packaging and sharing#

WayForHow people get itHow it is turned on
A crate of your ownMost pluginscargo add --git with your repository, at the same pinned rev of the APIPluginSet::with, or by name through your Registry and plugin_host::build_with
A built-in pluginThe official pluginsA cargo feature of seleniumbase-rs (plugin-shield, …), compiled into the prebuilt sbrs and sbrs-mcp--plugin, the [plugins] table, SB_PLUGINS, plugins_named
A WebAssembly moduleAny language, in a sandboxA .wasm file, loaded with the crate seleniumbase-rs-plugin-wasmPluginSet::with(WasmPlugin::from_file(...)?)

A crate of your own#

  • Name it seleniumbase-rs-plugin-<name>, and publish it on GitHub with the topic seleniumbase-rs-plugin so people find it.
  • Say in its README which rev of the plugins repository it uses, the options it takes and the capabilities it declares.
  • Native code is never loaded at run time: a program compiles your crate in.
Info

A plugin depends only on seleniumbase-rs-plugin, never on seleniumbase-rs, so it keeps working as seleniumbase-rs changes. A dev-dependency for the mock browser test is fine.

Built-in plugins#

A built-in plugin is a crate in the plugins repository, behind a feature of seleniumbase-rs-plugins-builtin and in its registry(), and behind a plugin-<name> feature of seleniumbase-rs. Adding one changes both repositories. That is how the official plugins ship; see Choosing plugins.

WebAssembly plugins#

Sandboxed

Not a Rust programmer? Write the plugin in any language that compiles to WebAssembly and load it with the wasm host: the module only describes the plugin in JSON (a name, browser switches, page scripts, URLs to block) and runs in a sandbox. The ABI is on the wasm page.

src/main.rs

The loaded WasmPlugin is an ordinary Plugin with fixed hooks: its switches in before_launch, its scripts as packages, its blocked URLs in on_page_created. It declares only what it uses (launch.args, cdp:Network, page.script:*). It is not in the built-in registry, because it needs the path of a module, so it is added with PluginSet::with, not by name.

Danger

A plugin is code that runs with your program’s rights and sees every page, cookie and password the browser handles. Review a plugin before you add it, as you would any dependency.

The template#

The template is a whole plugin crate with tests, in the plugins repository:

MustCodeAl/seleniumbase-rs-plugins

Plugin framework and community plugins for seleniumbase-rs

00
templatecopy this folder and rename the crate
Cargo.tomlseleniumbase-rs-plugin-hello; the API and the test fakes from git, with no rev
src
lib.rsHello with its metadata, and two tests
  • Cargo.toml names the package seleniumbase-rs-plugin-hello. It takes seleniumbase-rs-plugin, and seleniumbase-rs-plugin-testing for the tests, from the git URL of the plugins repository with no rev: add the rev that your seleniumbase-rs pins (see Create the crate).
  • src/lib.rs has a Hello plugin that declares its metadata (category Testing, the crate’s version, a summary and page.evaluate) and adds a script to every page with add_init_script. A plugin that adds scripts to pages declares page.script, as the steps above do. Its two tests check that a strict set accepts it and that every page gets the script.

Rename the crate, the type and the name, then follow Write your own.

Troubleshooting#

The trait Plugin is not implemented (two versions of the API crate)The compiler mentions two versions of seleniumbase_rs_plugin

Your crate and seleniumbase-rs take seleniumbase-rs-plugin from different git references, so Cargo built two copies. Use the rev that seleniumbase-rs pins in every dependency on the plugins repository. Cargo features shows the cargo add line.

Declares no metadata, which a strict set requires

The plugin keeps the default metadata(). Return Metadata::new(category) with the capabilities it needs. The command line, the MCP server and every set built from names are strict.

No plugin named hello is available

plugins_named, plugin_selection, with_plugin_selection, sbrs --plugin and sbrs plugin info look names up in the built-in registry. Add your plugin to a registry of your own and build the set with plugin_host::build_with, as in Register it and turn it on. The prebuilt sbrs cannot load a plugin that is not compiled into it.

Not allowed to use launch.args: it changed a setting without the capability

before_launch changed a field the plugin did not declare. Declare the capability for that field (see the table under Declaring metadata and capabilities). The same check says “list settings only grow” when a plugin removed an entry, and refuses any change of the profile.

The switch is refused for every plugin

The switch is in switch_policy::REFUSED_SWITCHES, such as --no-sandbox or --proxy-server. Use the matching field instead (proxy, host_resolver_rules) where there is one. A plugin that truly needs the switch declares raw:switches, a Critical capability, and is added with with_trusted.

One of its packages says neither only_on nor everywhere

Every Package must say where it loads. Add .only_on(["example.com"]) or .everywhere().

Fetch.enable is not available to plugins

The host owns request interception: one Fetch.enable per tab, shared with the library’s proxy login. To stop requests, send Network.setBlockedURLs; the host merges it with the other blocklists of the tab.

Needs cdp:Network, which WebDriver with Firefox or Safari does not support

The engine cannot give a capability the plugin requires. Run it on a Chromium browser, and say Requirements::CHROMIUM_ONLY so the refusal names the reason. If the plugin can work without the capability, declare it with optional and handle the error that send returns there.

It is required but not granted

The plugin requires a Critical capability, which with and every set built from names leave out. Add it in code with PluginSet::new().with_trusted(plugin, &[capability]).

block_on or tokio::test?

block_on of seleniumbase-rs-plugin-testing is for the recording fakes, which never wait. A test against the mock browser runs a real launch, so it needs an async runtime: use #[tokio::test], as in Test it without a browser.

Next steps#