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.
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
- API crate:
seleniumbase-rs-plugin - From seleniumbase-rs:
seleniumbase_rs::plugin_api, featureplugins - 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
revthat 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:
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 to | Use |
|---|---|
| Add browser switches, preferences, extensions or a proxy, or change headless mode | before_launch |
| Send a browser-level DevTools command | on_browser |
| Add a script or a stylesheet to every page | packages, or add_init_script in on_page_created |
| Send a DevTools command to every tab, such as blocking URLs or setting the time zone | on_page_created |
| Clean up, such as deleting temporary folders | on_close |
| Refuse a browser it cannot work with | requirements |
| Say what it is and what it needs | metadata |
| Run before or after other plugins | dependencies, or the ordering edges of metadata |
A DevTools commandChrome DevTools Protocol: a method name, such as Network.setBlockedURLs, and JSON parameters, sent to the browser or to one tab.
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
| Hook | Receives | Runs | When it fails |
|---|---|---|---|
before_launch | &mut LaunchSettings | Before the browser starts. Not when the program attaches to a running browser | The launch stops |
on_browser | &dyn BrowserHandle | Once the browser runs, started or attached | The launch stops |
packages, then on_page_created | &dyn PageHandle | For every tab or window the library attaches to, before your program uses it | The tab stops |
on_close | nothing | Once per launch, when the browser closes or is dropped | The 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
srcthe plugin
testsoptional
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.
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#
#[async_trait]comes from the API crate, so the crate needs no dependency of its own for it.add_init_scriptruns the script in every document the tab loads from now on, before the page’s own scripts on Chromium browsers. It returns aResult: an error goes up to the set, which stops the tab and names the plugin.metadatasays what the plugin is (aTestingplugin) and what it needs (a script on every page). Without it the plugin is a “legacy” plugin, which a strict setA PluginSet that refuses plugins that declare no metadata. The command line, the MCP server and every set built from names are strict. 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:
Then let name and metadata read from it, so each is written once:
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.
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.
To run the real launch steps of seleniumbase-rs with no Chrome, add it as a
dev-dependency with the features plugins and test-util.
Browser::launch_mocked checks the set, runs the hooks and prepares the first
tab against a scripted browser, and MockCtrl reads back what that browser was
sent.
This works only when this seleniumbase-rs and your crate take
seleniumbase-rs-plugin from the same rev. If Cargo builds two versions, see
Troubleshooting.
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:
Register it and turn it on#
A program adds the plugin crate next to seleniumbase-rs:
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.
SB_PLUGINS=hello turns it on for one run with the same code; it replaces the
enable list of the file. build_with returns a strict set and grants each
plugin what it declares, except Critical capabilities. An unknown name, an
option the plugin refuses, or a name given twice is an error before any browser
starts.
sbrs and sbrs-mcp know only the plugins compiled into them, the built-in
ones, so sbrs --plugin hello reports an unknown name. In a command-line
program of your own, PluginChoice applies the same --plugin and
--plugin-opt rules as sbrs:
The precedence is the command line, then SB_PLUGINS, then the settings file.
VALUE is a TOML value, so quote a string. Details are in
On the command line.
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.
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.
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.
What the plugin is and what it needs. See Declaring metadata and capabilities.
Default: "Metadata::legacy()"
Conditions the set checks before anything starts. Combine flags with |. A
refusal is an error, not a warning.
Default: "Requirements::empty()"
The names of plugins that must be in the same set. They run before this one, and a missing one stops the launch.
Default: "&[]"
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"
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"
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.
The browser runs, started or attached to. Use it for browser-level commands.
A tab or window was attached, before any command of your program runs in it. The plugin’s packages are already added.
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.
Metadata is a builder:
| Method | What 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):
| Category | For | Default phase |
|---|---|---|
Environment | Presets, switches, proxies, headless | Prepare, first |
Extension | Extensions and their workers | Normal |
Core | Interception, network logging, resource blocking | Normal |
Privacy | Hardening, telemetry, WebRTC, cookie isolation | Enforce, after the others |
Testing | Assertions, lifecycle, reporters, recording fakes | Observe |
Requirements::RUN_LAST puts a plugin in the phase Last.
Declare the capability that matches what the plugin does:
| Your plugin | Declares |
|---|---|
Adds to args, disable_features or enable_features | Capability::LaunchArgs (launch.args) |
Sets prefs | Capability::LaunchPrefs (launch.prefs) |
Sets host_resolver_rules, webrtc_policy or disable_quic | Capability::LaunchNetwork (launch.network) |
Sets proxy | Capability::LaunchProxy (launch.proxy) |
Adds extensions | Capability::LaunchExtensions (launch.extensions) |
Changes headless | Capability::LaunchHeadless (launch.headless) |
| Adds scripts or packages to pages | Capability::PageScript(HostScope::Hosts(...)), or HostScope::Everywhere (page.script:*) |
Calls evaluate | Capability::PageEvaluate (page.evaluate) |
| Sends the commands of one DevTools domain | Capability::Cdp("Network".into()) (cdp:Network) |
| Reads or writes files | Capability::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 whatpdeclares, except Critical capabilities.with_trusted(p, &[Capability])also grants the Critical ones you name.with_grants(p, set)grants exactlyset.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).
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.
The name the plugin is enabled by. Return the same string from Plugin::name.
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.
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:
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:
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.
What to copy from it:
- It sends raw commands with
page.send, so it saysRequirements::CHROMIUM_ONLY, and it declares the one domain it uses,cdp:Network, notraw:cdp. - It sends
Network.enablefirst, thenNetwork.setBlockedURLs. The host merges the list with those of the other plugins and thead_blocklist. configurebuilds the default preset from no options, and refuses a preset it does not know and apatternsvalue that is not an array of strings.
Its test checks the exact commands with RecordingPage:
The constructors and the other tests are in its source.
What to copy from it:
describe()holds what every crx plugin declares, andmetadata()adds what this one reads: its own files.LAUNCH_ONLY, because extensions load only into a browser it starts.per_launchandon_closekeep state per browser and clean it up.- Its file access is
host.fs.*, which a native plugin only declares:sbrs plugin info crxmarks itdeclared only.
shield shows the rest: packages, a before_launch that
sets preferences and network settings, and options read with serde.
Packaging and sharing#
| Way | For | How people get it | How it is turned on |
|---|---|---|---|
| A crate of your own | Most plugins | cargo add --git with your repository, at the same pinned rev of the API | PluginSet::with, or by name through your Registry and plugin_host::build_with |
| A built-in plugin | The official plugins | A 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 module | Any language, in a sandbox | A .wasm file, loaded with the crate seleniumbase-rs-plugin-wasm | PluginSet::with(WasmPlugin::from_file(...)?) |
A crate of your own#
- Name it
seleniumbase-rs-plugin-<name>, and publish it on GitHub with the topicseleniumbase-rs-pluginso people find it. - Say in its README which
revof 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.
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#
SandboxedNot 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.
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.
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:
Plugin framework and community plugins for seleniumbase-rs
templatecopy this folder and rename the crate
src
Cargo.tomlnames the packageseleniumbase-rs-plugin-hello. It takesseleniumbase-rs-plugin, andseleniumbase-rs-plugin-testingfor the tests, from the git URL of the plugins repository with norev: add therevthat your seleniumbase-rs pins (see Create the crate).src/lib.rshas aHelloplugin that declares its metadata (categoryTesting, the crate’s version, a summary andpage.evaluate) and adds a script to every page withadd_init_script. A plugin that adds scripts to pages declarespage.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#
The cargo features, the [plugins] table, SB_PLUGINS, --plugin and the builder calls.
Attach a set to a browser, and see when each hook runs on each engine.
Categories, capabilities, ordering and what the host enforces.
Plugins written in any language that compiles to WebAssembly.
The worked example above, as a plugin you can turn on.
A privacy plugin with packages, launch settings and serde options.