Site adapters are the #1 most-wanted contribution (see CONTRIBUTING.md). They inject short, hand-curated guidance into the agent's first message when it operates on a known site. The goal is NOT to encode every selector (those rot fast), but to capture the non-obvious quirks that cost an LLM several dead-end tool calls to discover on its own.
src/chrome/src/agent/adapters.js (and src/firefox/src/agent/adapters.js — both builds share the same file content, mirror changes to both).
getActiveAdapter(url) iterates the ADAPTERS array and returns the first adapter whose matches(url) returns true:
export function getActiveAdapter(url) {
if (!url) return null;
for (const a of ADAPTERS) {
try {
if (a.matches(url)) return a;
} catch (e) { /* skip malformed matchers */ }
}
return null;
}Only ONE adapter fires at a time, so prompt cost is fixed regardless of total adapter count.
For federated platforms such as Mastodon, keep generic URL shapes conservative.
Bare /@user and /users/user paths appear on many non-Mastodon sites, and the
current adapter matcher only sees the URL string. Future work may integrate
instances.social as a skill-backed lookup
or maintained known-instances list so candidate hosts can be verified before
injecting Mastodon guidance more broadly.
- First turn: the adapter's
notesare appended to the first user message in_enrichUserMessageWithCurrentPage(). - Mid-conversation navigation: if the user navigates to a URL matching a different adapter, the agent injects a
[Site context changed → now on <name>]message. Controlled by_maybeReinjectAdapter().
Every matched adapter also emits one content-free adapter_match trace note per
adapter revision in a run. It contains only adapter, revision, and
notesInjected; it never copies the notes, page URL, title, or page content.
This measures notes-only adapters as well as structured workflows. When a job
is selected, the existing adapter_context note separately records its bounded
workflow identity. Legacy notes-only adapters use implicit revision 1; adapters
with a structured contract use their declared workflow revision.
UNIVERSAL_PREAMBLE is injected alongside every system prompt when useSiteAdapters is enabled. It covers cookie/consent banners and paywalls — two patterns that appear across the public web and cause LLMs to make bad assumptions.
Adapters with repeated, evidence-backed tasks may also declare a versioned
webbrain-adapter-workflow/2 contract. This is runtime policy, not more page
prose. Before either the compact intent planner or the full planner runs,
WebBrain gives it only the active adapter name and a bounded list of app-owned
job IDs plus short descriptions. The planner selects site_job semantically,
so routing does not depend on the language of the user's request. Page content
cannot add or select a job, and an ID is accepted only if it still belongs to
the same adapter on the live page immediately before execution. Navigation
while planning or reviewing drops the binding unless the adapter, revision,
schema, and job still match. A trusted Continue turn may retain prior evidence
only after the same live revalidation.
The selected job can only tighten execution. It can require a consequential
tool result, job-bound terminal evidence after its submit/commit dispatch, or
item-level progress-ledger reconciliation. A generic success toast or another
site's submit cannot satisfy the selected job: transaction workflows require a
paid/ticket-issued state, protected messaging requires the recipient-bound
dispatch, an empty composer in that conversation, and a positive sent-status
confirmation, while form/publish/update jobs require their own confirmation
state. A workflow that requires a ledger
must reconcile exact app-owned inventory IDs from a complete accessibility-tree
read or app-seeded expected/classifier targets. Model-created rows—even one
terminal row—cannot prove complete coverage. Form inventory v1 is the last
exhaustive document-root snapshot (filter: all, not depth-truncated).
Skipped required rows cannot prove success; optional (required: false)
inventory rows may be skipped, and that flag is emitted only when optionality
is explicit (aria-required="false"). Missing required stays unknown.
Depth truncation is form-relevant (an omitted includable descendant). Empty
or erroring third-party frames are omitted only when another frame already
inventoried form controls; a lone failed cross-origin application frame stays
incomplete. Checkbox/radio/Next actions stale completeness until a
fresh root read. The executor receives the
app-owned stages plus success and partial evidence contract. Edited plan-review
text clears hidden workflow routing instead of retaining stale authorization.
Metadata-only traces retain only the adapter match/injection flag and, for a
selected job, its revision, schema, job, and template—never notes, page, form,
or message content.
{
name: 'example-forms',
category: 'general',
revision: 1,
regions: ['global'],
jobs: ['submit-form'],
workflow: {
schema: 'webbrain-adapter-workflow/2',
jobs: {
'submit-form': {
description: 'Fill, review, submit, and verify the form.',
template: 'form',
stateChange: true,
requiresSubmission: true,
requiresLedger: true,
stages: ['inventory', 'fill', 'review', 'reconcile', 'commit', 'verify'],
successEvidence: ['A post-submit confirmation is visible.'],
partialEvidence: ['Completed and unresolved questions plus the blocker are reported.'],
},
},
},
matches: (url) => /^https:\/\/forms\.example\.com\//.test(url),
notes: `...`,
}workflow.jobs must exactly match jobs. Job IDs and stages are stable
identifiers; evidence strings are bounded behavioral requirements, not CSS
selectors. requiresSubmission implies stateChange, every job includes
verify, and submission jobs also include commit. Bump revision whenever
the behavioral contract changes.
{
name: 'my-site', // unique short identifier
category: 'general', // 'general' | 'finance'
matches: (url) => /^https?:\/\/(www\.)?example\.com\//.test(url),
notes: `
- Bullet 1: the actionable tip.
- Bullet 2: another tip.
- Keep these SHORT (4–8 bullets max). Every adapter costs tokens on every first turn.
`,
}| Field | Type | Description |
|---|---|---|
name |
string | Unique identifier for the adapter. Used in system-prompt headings. |
category |
'general' or 'finance' |
'finance' adds a [FINANCE / HIGH-STAKES] banner to the heading and triggers extra safety guidance in the system prompt. |
matches |
(url) => boolean |
Returns true when the adapter should fire for this URL. Regex is preferred — keep it specific enough to avoid false matches. |
notes |
string | Bulleted guidance injected into the first user message. Keep 4–8 lines max. See style guidance below. |
revision |
positive integer | Optional workflow-contract revision. Required when any structured workflow field is present. |
regions |
string[] | Stable regions where the structured job contract applies, such as global, CN, or MENA. |
jobs |
string[] | Stable planner-routing IDs. Must exactly match workflow.jobs. |
workflow |
object | Optional validated webbrain-adapter-workflow/2 job contract used to tighten runtime completion. |
Adapters are ordered by category/site in the ADAPTERS array. Finance adapters must come BEFORE finance-generic, since finance-generic uses a broad regex that would shadow specific adapters. Currently: Stripe → Coinbase → Robinhood → TradingView → finance-generic.
- Describe the SHAPE of the page rather than literal selectors. Selectors rot; page layout patterns are stable longer.
// Good notes: `- The composer is a contenteditable div, not a textarea.` // Bad notes: `- Click div[contenteditable="true"] to compose.`
- Name the tool to prefer: guide toward AX tools (
click_ax,set_field) over legacy tools (click({text}),type_text). - Flag destructive subtleties: "The 'Cancel' button on the billing page immediately stops service — read the confirmation modal."
- Flag SPA navigation traps: "Settings changes autosave; navigating via browser back discards unsaved edits."
- Flag sticky overlays: "The cookie banner reappears every 24h. Don't describe its text as page content."
- Flag virtualized containers: "The timeline is virtualized — scroll to load more items."
- Keep each bullet to a single actionable tip. The model has limited context and will skim.
- Don't encode CSS selectors — they change with every site redesign.
- Don't write more than 8 bullets — the token cost compounds on every conversation.
- Don't include obvious advice the model would figure out by reading the page (e.g., "the submit button submits the form").
- Don't duplicate the universal preamble (cookie/paywall guidance).
- Don't add alphabetical or reference adapters — each adapter must provide real guidance that saves the model from at least 2–3 trial-and-error tool calls.
{
name: 'twitter',
category: 'general',
matches: (url) => /^https?:\/\/(www\.)?(twitter\.com|x\.com)\//.test(url),
notes: `
- The composer is a contenteditable, not a textarea. Character count is enforced client-side.
- The timeline is virtualized — tweets scroll out of the DOM. Use search, not scroll, to find a tweet.
- "Reply", "Retweet", "Like" icons are below each tweet.
- Quote tweets vs reposts: the retweet icon opens a menu with both options.
`,
}{
name: 'stripe',
category: 'finance',
matches: (url) => /^https?:\/\/(dashboard\.)?stripe\.com\//.test(url),
notes: `
- LIVE vs TEST mode toggle in the top-right. Always confirm which mode.
- Refunds are partial-by-default — check the amount carefully.
- Customer deletion is irreversible.
- SUBSCRIPTIONS: proration prompts ("Charge prorated amount immediately" vs "On next invoice").
`,
}- Add the adapter to both
src/chrome/src/agent/adapters.jsandsrc/firefox/src/agent/adapters.js. - Verify matching: navigate to the target URL in a browser with the extension loaded. Open the DevTools console on the service worker / background page and run:
import { getActiveAdapter, listAdapters } from './agent/adapters.js'; console.log(getActiveAdapter('https://example.com/some-page'));
- Verify the notes appear: in Ask, Act, or Dev mode, type a simple instruction (e.g., "what's on this page?"). Open the side panel's verbose mode and confirm the first user message contains
[Site guidance for <name>]with your notes. - Verify only ONE adapter fires: navigate to a URL that could match multiple matchers. Check that the first match wins and no others leak through.
- Test navigation re-injection: start a conversation on a non-adapted site, then navigate to your adapted site. Confirm a
[Site context changed]message appears. - If a workflow contract is present: validate
listAdapterWorkflowProfiles(), verify planner routing for positive and negative URLs, and test state-change, submission, ledger, trace-privacy, and Chrome/Firefox parity behavior.
Open each adapted site and verify:
- The adapter loads on page 1 (not on a SPA route change)
- The notes are useful (don't mislead the model)
- The model doesn't follow outdated instructions
- Add the adapter object to the
ADAPTERSarray insrc/chrome/src/agent/adapters.js - Mirror the exact same change to
src/firefox/src/agent/adapters.js - Ensure the
matches()regex is specific and doesn't shadow neighboring adapters - If
category: 'finance', place it BEFOREfinance-genericin the array - Verify the notes are 4–8 concise bullets
- Test matching with
getActiveAdapter(url) - Test end-to-end with the extension loaded
- For a structured profile, prove the job from repeated traces or current UI evidence; validate exact URL routing and completion requirements
- If the adapter targets a non-English market, add localized label hints (see the WordPress adapter for an example of how to annotate non-English UI labels)