Version: 1.0
Status: Draft
Date: 2026-03-25
Authors: Web Agent Bridge Contributors
License: MIT
Repository: github.com/abokenan444/web-agent-bridge
- Abstract
- Terminology
- Protocol Overview
- Discovery Protocol
- Command Protocol
- Lifecycle Protocol
- Transport Layers
- Security Model
- Fairness Protocol
- MCP Compatibility
- Conformance
- Appendix A: JSON Schema for agent-bridge.json
- Appendix B: Error Codes
- Appendix C: MIME Types and Headers
The Web Agent Bridge (WAB) Protocol is an open protocol that enables AI agents to interact with websites through a standardized command interface. Where robots.txt tells bots what they cannot do, WAB tells AI agents what they can do — and exactly how to do it.
WAB functions as OpenAPI for human-facing pages. Website owners publish a machine-readable discovery document describing the actions, permissions, and entry points their site exposes. AI agents consume this document, authenticate, and execute commands through a uniform request/response protocol — eliminating the need for DOM scraping, fragile selectors, or reverse-engineered APIs.
The protocol is transport-agnostic. A single command schema works identically across in-browser JavaScript globals, WebSocket connections, and HTTP REST endpoints. This allows the same agent logic to operate inside a browser tab, from a remote orchestrator, or within a server-to-server pipeline.
- Declarative: Sites declare capabilities; agents discover them at runtime.
- Secure: Every interaction is scoped by permissions, rate limits, and session tokens.
- Fair: A neutrality layer ensures small and large sites receive equal agent visibility.
- Interoperable: WAB maps cleanly onto MCP (Model Context Protocol) for LLM tool use.
- Simple: A minimal conforming implementation requires only a JSON file and a
<script>tag.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
| Term | Definition |
|---|---|
| Agent | An autonomous or semi-autonomous software entity (typically AI-powered) that discovers, plans, and executes actions on websites via the WAB protocol. |
| Bridge | The runtime layer on a website that exposes the WAB interface. In the reference implementation this is the ai-agent-bridge.js script that creates window.AICommands. |
| Site Owner | The person or organization that deploys the Bridge on their website and configures its discovery document, permissions, and actions. |
| Discovery Document | A JSON file (agent-bridge.json or /.well-known/wab.json) that describes a site's WAB capabilities, permissions, transport options, and metadata. |
| Command | A structured JSON message sent by an Agent to a Bridge requesting a specific operation. |
| Action | A named capability exposed by a Bridge (e.g., search, addToCart, signup). Actions are registered by the Site Owner and discovered by Agents at runtime. |
| Transport | The communication channel over which Commands flow between Agent and Bridge. WAB defines three transports: JavaScript Global, WebSocket, and HTTP REST. |
| Session | A time-bounded, authenticated context linking an Agent to a Bridge. Sessions are identified by tokens and scoped by permissions. |
| Tier | The subscription level governing which features and rate limits apply to a site's Bridge instance. Standard tiers are free, starter, pro, and enterprise. |
| Selector | A CSS selector string identifying a DOM element. Bridges use selectors to map Actions to page elements. |
| BiDi Interface | The WebDriver BiDi-compatible interface exposed at window.__wab_bidi for structured command exchange within a browser context. |
WAB is organized into three layers:
┌─────────────────────────────────────────┐
│ Protocol Layer (Spec) │ ← This document
│ Discovery · Commands · Lifecycle │
├─────────────────────────────────────────┤
│ Runtime Layer (JS SDK) │ ← ai-agent-bridge.js / WABAgent SDK
│ Bridge · Actions · Permissions · Logs │
├─────────────────────────────────────────┤
│ Transport Layer (Wire) │ ← JS Global / WebSocket / HTTP
│ window.AICommands · ws:// · /api/wab │
└─────────────────────────────────────────┘
Protocol Layer defines the abstract data formats, lifecycle phases, and conformance requirements. It is implementation-agnostic.
Runtime Layer is a concrete JavaScript SDK that implements the Protocol Layer. The reference implementation provides ai-agent-bridge.js (site-side Bridge) and WABAgent (agent-side SDK).
Transport Layer carries serialized Commands and Responses between Agent and Bridge. All three transports MUST implement the identical Command Protocol defined in Section 5.
Every WAB interaction follows a five-phase lifecycle:
Discover → Authenticate → Plan → Execute → Confirm
- Discover — Agent locates and parses the site's Discovery Document.
- Authenticate — Agent establishes a Session with the Bridge.
- Plan — Agent reads available Actions and determines an execution strategy.
- Execute — Agent sends Commands to perform Actions.
- Confirm — Agent verifies the results of executed Commands.
Each phase is detailed in Section 6.
- Opt-in only. No site is WAB-enabled unless the Site Owner explicitly deploys a Bridge.
- Least privilege. Agents receive only the permissions the Site Owner grants.
- Transport symmetry. A Command sent over HTTP MUST produce the same result as the same Command sent over WebSocket or the JS global.
- Graceful degradation. If a transport is unavailable, the Agent SHOULD fall back to an alternative transport listed in the Discovery Document.
- Neutrality. The protocol includes a Fairness Protocol (Section 9) to prevent concentration of agent traffic.
A WAB-enabled site MUST serve its Discovery Document at one or both of the following locations:
| Priority | URL | Content-Type |
|---|---|---|
| 1 (primary) | https://{host}/agent-bridge.json |
application/json |
| 2 (fallback) | https://{host}/.well-known/wab.json |
application/json |
An Agent MUST attempt the primary URL first. If it returns a non-2xx status, the Agent SHOULD attempt the fallback URL. If both fail, the site MUST be treated as non-WAB-enabled.
The Discovery Document MAY also be referenced via an HTML <meta> tag:
<meta name="wab-discovery" content="/agent-bridge.json">The Discovery Document is a JSON object with the following top-level fields:
{
"wab_version": "1.0",
"provider": {
"name": "Acme Restaurant",
"category": "restaurant",
"url": "https://acme-restaurant.com",
"location": {
"city": "Amman",
"country": "JO",
"support_local": true
}
},
"capabilities": {
"commands": [
{
"name": "viewMenu",
"description": "View the restaurant menu",
"trigger": "navigate",
"params": [],
"requiresAuth": false
},
{
"name": "placeOrder",
"description": "Place a food order",
"trigger": "fill_and_submit",
"params": [
{ "name": "items", "type": "array", "required": true, "description": "List of menu item IDs" },
{ "name": "address", "type": "string", "required": true, "description": "Delivery address" }
],
"requiresAuth": true
},
{
"name": "searchMenu",
"description": "Search menu items by keyword",
"trigger": "api",
"params": [
{ "name": "query", "type": "string", "required": true, "description": "Search term" }
],
"requiresAuth": false
}
],
"permissions": {
"readContent": true,
"click": true,
"fillForms": true,
"scroll": true,
"navigate": false,
"apiAccess": true,
"automatedLogin": false,
"extractData": false
},
"tier": "starter"
},
"agent_access": {
"preferred_entry_point": "/menu",
"api_fallback": "https://api.acme-restaurant.com/v1",
"selectors": {
"menu": "#main-menu",
"cart": "#shopping-cart",
"searchInput": "input[name='search']",
"orderForm": "#order-form"
}
},
"fairness_metrics": {
"commission_rate": "0%",
"direct_benefit": "Orders go directly to the restaurant",
"is_independent": true
},
"trust_signatures": [
"sha256:abc123...",
"wab-registry:verified"
],
"transport": {
"js_global": {
"enabled": true,
"interface": "window.AICommands"
},
"websocket": {
"enabled": true,
"url": "wss://acme-restaurant.com/ws/wab"
},
"http": {
"enabled": true,
"base_url": "/api/wab"
}
},
"security": {
"require_origin_match": true,
"session_ttl": 3600,
"max_rate": 60
}
}A string indicating the WAB protocol version. For this specification, the value MUST be "1.0".
Metadata about the site and its owner.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | REQUIRED | Human-readable name of the site or business. |
category |
string | REQUIRED | Business category (e.g., "restaurant", "ecommerce", "saas", "news"). |
url |
string | REQUIRED | Canonical URL of the site. |
location |
object | OPTIONAL | Physical location. Contains city (string), country (ISO 3166-1 alpha-2), and support_local (boolean). |
Describes what the Bridge can do.
| Field | Type | Required | Description |
|---|---|---|---|
commands |
array | REQUIRED | List of Action definitions. See Section 4.4. |
permissions |
object | REQUIRED | Map of permission names to booleans. See Section 4.5. |
tier |
string | OPTIONAL | Subscription tier: "free", "starter", "pro", or "enterprise". Defaults to "free". |
Hints for agents on how to interact with the site.
| Field | Type | Required | Description |
|---|---|---|---|
preferred_entry_point |
string | OPTIONAL | URL path the agent SHOULD navigate to first. |
api_fallback |
string | OPTIONAL | Base URL for a REST API the agent MAY use when DOM interaction is unavailable. |
selectors |
object | OPTIONAL | Map of logical names to CSS selectors for key page elements. |
Transparency data for the Fairness Protocol (Section 9).
| Field | Type | Required | Description |
|---|---|---|---|
commission_rate |
string | OPTIONAL | Commission charged to the site (e.g., "0%", "15%"). |
direct_benefit |
string | OPTIONAL | Human-readable description of how the site benefits. |
is_independent |
OPTIONAL | boolean | Whether the site is independently owned. |
An array of strings representing cryptographic or registry-based trust attestations. Agents MAY use these to verify the authenticity of the Discovery Document.
Declares available Transport Layers. At least one transport MUST be enabled.
| Field | Type | Description |
|---|---|---|
js_global.enabled |
boolean | Whether the JS global interface is available. |
js_global.interface |
string | Global variable name (default: "window.AICommands"). |
websocket.enabled |
boolean | Whether a WebSocket endpoint is available. |
websocket.url |
string | Full WebSocket URL. |
http.enabled |
boolean | Whether an HTTP REST endpoint is available. |
http.base_url |
string | Base URL path for the HTTP transport. |
| Field | Type | Default | Description |
|---|---|---|---|
require_origin_match |
boolean | true |
Whether the Bridge validates the requesting origin. |
session_ttl |
integer | 3600 |
Session lifetime in seconds. |
max_rate |
integer | 60 |
Maximum commands per minute per session. |
Each entry in capabilities.commands MUST conform to:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | REQUIRED | Unique action identifier (alphanumeric, hyphens, underscores). |
description |
string | REQUIRED | Human-readable description of the action. |
trigger |
string | REQUIRED | Execution method. One of: "click", "fill_and_submit", "scroll", "api", "navigate". |
params |
array | REQUIRED | Array of parameter definitions (MAY be empty). |
requiresAuth |
boolean | OPTIONAL | Whether the action requires an authenticated session. Defaults to false. |
Each parameter definition:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | REQUIRED | Parameter name. |
type |
string | REQUIRED | JSON Schema type: "string", "number", "boolean", "array", "object". |
required |
boolean | REQUIRED | Whether the parameter is mandatory. |
description |
string | RECOMMENDED | Human-readable description. |
default |
any | OPTIONAL | Default value if not provided. |
enum |
array | OPTIONAL | Allowed values. |
The permissions object uses the following standard keys:
| Permission | Type | Description |
|---|---|---|
readContent |
boolean | Agent MAY read visible text content from the page. |
click |
boolean | Agent MAY trigger click events on permitted elements. |
fillForms |
boolean | Agent MAY fill and submit form fields. |
scroll |
boolean | Agent MAY scroll the page. |
navigate |
boolean | Agent MAY navigate to different URLs within the site. |
apiAccess |
boolean | Agent MAY call the site's API endpoints. |
automatedLogin |
boolean | Agent MAY perform automated authentication flows. |
extractData |
boolean | Agent MAY extract and store structured data from the page. |
A Bridge MUST enforce these permissions at runtime. If an Agent attempts an action that requires a permission set to false, the Bridge MUST reject the command with error code PERMISSION_DENIED.
The DNS Discovery Protocol (DDP) is an OPTIONAL infrastructure-layer mechanism that lets agents discover a site's WAB endpoint before issuing any HTTP request. A site advertises its capabilities via DNS TXT records resolved over DNS over HTTPS (DoH), eliminating HTTP probing, cookie-banner consent flows, and ISP-level lookup leaks.
Three sibling labels under the apex domain. All three are TXT records.
| Label | Required | Purpose |
|---|---|---|
_wab.{apex} |
REQUIRED | Discovery — points at the wab.json contract. |
_wab-trust.{apex} |
OPTIONAL | Trust contract — declares data scope, security contact, complaint channel. |
_wab-policy.{apex} |
OPTIONAL | Policy contract — declares rate limits, capability TTL, fairness metrics. |
A WAB-aware Agent MUST query _wab.{apex} first. If NXDOMAIN, the site is treated as non-WAB-enabled. If a record is found, the Agent MAY query _wab-trust and _wab-policy in parallel as enrichment — neither is required to proceed.
wab-record = version-tag *( ws? ";" ws? field ) [ ws? ";" ]
version-tag = "v=" version-id
version-id = "wab" 1*DIGIT
field = field-name ws? "=" ws? field-value
field-name = ALPHA *( ALPHA / DIGIT / "-" / "_" )
field-value = 1*( ALPHA / DIGIT / "-" / "." / "_" / "/" / ":" / "+" / "%" / "?" / "&" / "=" )
ws = 1*( SP / HTAB )
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39
SP = %x20
HTAB = %x09Reserved field names for _wab:
| Field | Type | Required | Description |
|---|---|---|---|
v |
version-id | REQUIRED | Protocol version. Current: wab1. |
endpoint |
URL | REQUIRED | HTTPS URL of the discovery JSON document. |
path |
URL path | OPTIONAL | Alternative to endpoint for relative discovery (path=/agent.json). |
fingerprint |
hash | OPTIONAL | sha256:<hex64> of the discovery JSON for tamper detection. |
capability_ttl |
seconds | OPTIONAL | How long agents MAY cache the discovery JSON. Default 3600. |
status |
enum | OPTIONAL | active / paused / deprecated. Default active. |
Reserved field names for _wab-policy:
| Field | Type | Description |
|---|---|---|
rate |
integer | Maximum requests per minute per agent. |
concurrency |
integer | Maximum concurrent connections per agent. |
commission |
percent | Commission rate the platform takes (e.g. 0%). Fairness signal. |
trust-record = field *( ws? ";" ws? field ) [ ws? ";" ]
field = field-name ws? "=" ws? field-value
field-name = "trust" / "security" / "complaint" / "iodef"
field-value = "https:" 1*( ALPHA / DIGIT / "-" / "." / "_" / "/" / ":" / "+" / "%" / "?" / "&" / "=" )
/ "mailto:" 1*VCHAR
ws = 1*( SP / HTAB )
VCHAR = %x21-7EForward-compatibility: Verifiers MUST accept and surface — but not fail on — additional
field-name=field-valuepairs they do not recognise (e.g. a futurev=wab2version tag). Unknown fields SHOULD be reported as warnings so the deployment remains visible to operators.
For sites with capabilities too large for a single TXT record (DNS allows ~255 octets per string), a _wab record MAY use the JSON-Pointer form:
v=wab1; endpoint=https://example.com/.well-known/wab.json
The agent then fetches the URL and parses a JSON document conforming to the schema in Appendix A. The MIME type MUST be application/json and the document SHOULD be cacheable per capability_ttl.
Recommended JSON envelope for _wab-pointed documents (extends §4.2):
{
"version": "wab1",
"endpoint": "https://example.com",
"capability_ttl": 3600,
"fingerprint": "sha256:abc…",
"capabilities": ["search", "purchase", "auth"],
"security": { "dnssec_required": true, "doh_only": true }
}| Requirement | Level |
|---|---|
| Use DoH (RFC 8484), not plain UDP/53 | MUST |
Set the DO bit / request AD flag |
MUST |
| Validate DNSSEC chain when AD=1 trusted | SHOULD |
Honour the record's capability_ttl |
SHOULD |
| Cap effective TTL at 86400 seconds | MUST |
| Condition | Agent Action | Rationale |
|---|---|---|
NXDOMAIN on _wab |
Stop. Treat site as non-WAB. | Site has not opted in. |
NXDOMAIN on _wab-trust or _wab-policy |
Continue with defaults. | Optional records. |
DNS SERVFAIL |
Retry with exponential backoff (max 3 attempts). | Resolver-side fault, not authoritative. |
| DoH HTTP 5xx | Retry with backoff or fall back to alternate DoH. | Resolver outage. |
| DoH HTTP 4xx (other than 404) | Stop with DOH_REQUEST_REJECTED. |
Bad query, no point retrying. |
| DoH timeout (>5s) | Retry once, then fall back to alternate DoH. | Network issue. |
AD=false on the answer |
Continue with dnssec_unverified warning. |
DNSSEC not deployed yet. |
AD=false AND record's dnssec_required=true |
Stop with DNSSEC_REQUIRED. |
Site demands authenticated answers. |
DNSSEC Bogus |
Stop with DNSSEC_BOGUS security alert. |
Possible MITM. |
| Record exists but fails ABNF | Stop with INVALID_FORMAT. |
Cannot trust malformed contract. |
Multiple _wab records returned |
Pick the one whose v=wab1 matches the highest |
Allows zero-downtime version migration. |
| version the agent supports; ignore others. | ||
endpoint URL is non-HTTPS |
Stop with INSECURE_ENDPOINT. |
DoH gain is undone if endpoint is plaintext. |
fingerprint mismatch with fetched JSON |
Stop with FINGERPRINT_MISMATCH. |
Tampered or stale record. |
DoH moves the trust point from the user's ISP to the chosen DoH resolver. The query is not invisible — the resolver sees it. Agents SHOULD let users select their resolver, MUST NOT hardcode a single provider in production builds, and SHOULD support resolver rotation. See /dns#privacy for the threat-model table.
A reference implementation of the resolver and validator ships as the npm package
@wab/dns-verify (packages/dns-verify). It implements §4.6.1
through §4.6.6 and is suitable for use in CI pipelines.
Every command sent from an Agent to a Bridge MUST conform to the following JSON structure:
{
"id": "cmd_a1b2c3d4",
"method": "wab.executeAction",
"params": {
"name": "searchMenu",
"data": {
"query": "vegetarian"
}
},
"context": {
"url": "https://acme-restaurant.com/menu",
"sessionToken": "sess_x9y8z7w6",
"timestamp": "2026-03-25T12:00:00Z"
}
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | REQUIRED | Unique identifier for this command. The Bridge MUST echo it in the response. |
method |
string | REQUIRED | The WAB method to invoke. See Section 5.3. |
params |
object | REQUIRED | Method-specific parameters. MAY be empty {}. |
context |
object | OPTIONAL | Execution context. Includes url, sessionToken, and timestamp. |
Every response from a Bridge to an Agent MUST conform to:
Success response:
{
"id": "cmd_a1b2c3d4",
"type": "success",
"result": {
"items": [
{ "name": "Falafel Wrap", "price": 5.99 },
{ "name": "Veggie Burger", "price": 8.49 }
],
"total": 2
}
}Error response:
{
"id": "cmd_a1b2c3d4",
"type": "error",
"error": {
"code": "PERMISSION_DENIED",
"message": "Action 'placeOrder' requires authentication"
}
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | REQUIRED | Matches the id from the originating command. |
type |
string | REQUIRED | Either "success" or "error". |
result |
object | CONDITIONAL | Present when type is "success". Method-specific result data. |
error |
object | CONDITIONAL | Present when type is "error". Contains code and message. |
A conforming WAB Bridge MUST implement the following methods:
Returns the site's Discovery Document.
- Params: None.
- Result: The full Discovery Document object.
{ "id": "1", "method": "wab.discover", "params": {} }Returns the current Bridge context including version, permissions, and session state.
- Params: None.
- Result:
{ version, permissions, session, tier, url }.
{ "id": "2", "method": "wab.getContext", "params": {} }Response:
{
"id": "2",
"type": "success",
"result": {
"version": "1.0.0",
"permissions": { "readContent": true, "click": true, "fillForms": false },
"session": { "authenticated": false, "ttl": 3600 },
"tier": "free",
"url": "https://acme-restaurant.com/menu"
}
}Lists all available Actions, optionally filtered by category.
- Params:
{ "category": "string" }(OPTIONAL). - Result: Array of Action definitions.
{ "id": "3", "method": "wab.getActions", "params": { "category": "ordering" } }Executes a named Action with the provided parameters.
- Params:
{ "name": "string", "data": {} }(REQUIRED). - Result: Action-specific result object.
{
"id": "4",
"method": "wab.executeAction",
"params": {
"name": "placeOrder",
"data": { "items": ["item_01", "item_02"], "address": "123 Main St" }
},
"context": { "sessionToken": "sess_x9y8z7w6" }
}Reads the text content of a page element identified by a CSS selector.
- Params:
{ "selector": "string" }(REQUIRED). - Result:
{ "text": "string", "html": "string", "selector": "string" }. - Requires permission:
readContent.
{ "id": "5", "method": "wab.readContent", "params": { "selector": "#main-menu" } }Returns metadata about the current page and Bridge state.
- Params: None.
- Result:
{ title, url, description, bridgeVersion, actionsCount, permissions }.
{ "id": "6", "method": "wab.getPageInfo", "params": {} }Response:
{
"id": "6",
"type": "success",
"result": {
"title": "Acme Restaurant - Menu",
"url": "https://acme-restaurant.com/menu",
"description": "Fresh Mediterranean cuisine",
"bridgeVersion": "1.0.0",
"actionsCount": 5,
"permissions": { "readContent": true, "click": true }
}
}Authenticates an Agent with the Bridge using an API key or token.
- Params:
{ "apiKey": "string", "meta": {} }(REQUIRED). - Result:
{ "sessionToken": "string", "expiresAt": "ISO 8601", "permissions": {} }.
{
"id": "7",
"method": "wab.authenticate",
"params": {
"apiKey": "wab_key_abc123",
"meta": { "agentName": "ShoppingAssistant", "version": "2.1" }
}
}Response:
{
"id": "7",
"type": "success",
"result": {
"sessionToken": "sess_x9y8z7w6",
"expiresAt": "2026-03-25T13:00:00Z",
"permissions": { "readContent": true, "click": true, "fillForms": true }
}
}Subscribes to real-time events from the Bridge. Only available on transports that support push messaging (WebSocket, JS global with event listeners).
- Params:
{ "events": ["string"] }(REQUIRED). Valid events:"actionExecuted","permissionChanged","sessionExpired","error". - Result:
{ "subscriptionId": "string", "events": ["string"] }.
{
"id": "8",
"method": "wab.subscribe",
"params": { "events": ["actionExecuted", "error"] }
}Health check. Returns immediately to confirm the Bridge is operational.
- Params: None.
- Result:
{ "status": "ok", "timestamp": "ISO 8601", "version": "string" }.
{ "id": "9", "method": "wab.ping", "params": {} }Response:
{
"id": "9",
"type": "success",
"result": { "status": "ok", "timestamp": "2026-03-25T12:00:00Z", "version": "1.0.0" }
}Command IDs MUST be unique within a session. Implementations SHOULD use one of:
- UUIDv4 (e.g.,
"550e8400-e29b-41d4-a716-446655440000") - Monotonically increasing integers (e.g.,
"1","2","3") - Prefixed counters (e.g.,
"cmd_001","cmd_002")
The Bridge MUST NOT reuse command IDs and MUST reject duplicate IDs within the same session with error code DUPLICATE_COMMAND_ID.
Every agent-site interaction follows five ordered phases. Agents MUST complete each phase before advancing to the next.
sequenceDiagram
participant A as Agent
participant B as Bridge
participant S as Site
Note over A,S: Phase 1 — Discovery
A->>S: GET /agent-bridge.json
S-->>A: 200 OK (Discovery Document)
Note over A,B: Phase 2 — Authentication
A->>B: wab.authenticate({ apiKey, meta })
B-->>A: { sessionToken, expiresAt, permissions }
Note over A,B: Phase 3 — Planning
A->>B: wab.getActions()
B-->>A: [{ name, description, params, ... }]
A->>B: wab.getPageInfo()
B-->>A: { title, url, actionsCount, ... }
Note over A: Agent plans execution strategy
Note over A,B: Phase 4 — Execution
A->>B: wab.executeAction({ name, data })
B-->>A: { result }
A->>B: wab.executeAction({ name, data })
B-->>A: { result }
Note over A,B: Phase 5 — Confirmation
A->>B: wab.readContent({ selector })
B-->>A: { text, html }
A->>B: wab.getPageInfo()
B-->>A: { updated state }
Note over A: Agent verifies outcomes
The Agent locates the site's Discovery Document by:
- Fetching
https://{host}/agent-bridge.json. - If unavailable, fetching
https://{host}/.well-known/wab.json. - Optionally, parsing the HTML
<meta name="wab-discovery">tag.
The Agent MUST validate the document against the WAB JSON Schema (Appendix A). If the wab_version field indicates a version the Agent does not support, it SHOULD terminate gracefully with an informative error.
The Agent SHOULD cache the Discovery Document for a reasonable duration (RECOMMENDED: 5 minutes). The response MAY include standard HTTP caching headers (Cache-Control, ETag).
If the Discovery Document includes actions where requiresAuth is true, the Agent MUST authenticate before executing those actions.
- The Agent sends a
wab.authenticatecommand with its API key and optional metadata. - The Bridge validates the key and returns a
sessionTokenwith an expiration time. - The Agent MUST include the
sessionTokenin thecontextfield of all subsequent commands.
If no actions require authentication, this phase MAY be skipped. The Bridge MUST still accept unauthenticated commands for actions where requiresAuth is false.
Session renewal: When a session approaches expiration (RECOMMENDED: within 10% of TTL remaining), the Agent SHOULD re-authenticate to obtain a fresh token. The Bridge MUST NOT invalidate the old token until it naturally expires.
The Agent reads the available actions and Bridge context to formulate an execution plan:
- Call
wab.getActions()to retrieve the full list of available actions. - Call
wab.getContext()to understand current permissions and state. - Optionally call
wab.getPageInfo()for page metadata.
The Agent SHOULD filter actions by the permissions granted in the session. The Agent MUST NOT attempt to execute actions for which it lacks the required permissions.
Planning is internal to the Agent. The protocol does not prescribe a planning algorithm — this is where LLM-powered agents apply their reasoning capabilities.
The Agent sends wab.executeAction commands to perform the planned actions:
- Each command targets a single action by
name. - The
datafield contains the action's required and optional parameters. - The Bridge validates the command against the action's parameter schema.
- The Bridge executes the action (clicking elements, filling forms, calling APIs, etc.).
- The Bridge returns the result or an error.
Sequential vs. parallel: Agents MAY send multiple commands concurrently if the actions are independent. The Bridge MUST process each command atomically. The Bridge SHOULD document in the Discovery Document if certain actions have ordering dependencies.
Rate limiting: The Bridge MUST enforce the max_rate from the security configuration. If an Agent exceeds the rate limit, the Bridge MUST respond with error code RATE_LIMITED and a Retry-After value.
After execution, the Agent verifies that the intended outcomes occurred:
- Call
wab.readContent()to verify visible page changes. - Call
wab.getPageInfo()to check updated page state. - Compare actual results against expected results from the plan.
Confirmation is RECOMMENDED but not strictly required. Agents that skip confirmation accept the risk of undetected failures.
WAB defines three Transport Layers. Each transport MUST implement the full Command Protocol from Section 5. A conforming Bridge MUST support at least one transport. A conforming Agent SHOULD support all three.
Identifier: js_global
Scope: In-browser (same page as the Bridge)
Interface: window.AICommands and window.__wab_bidi
This is the primary transport for agents running inside a browser (Puppeteer, Playwright, browser extensions).
The AICommands object exposes convenience methods that map to WAB standard methods:
// Get all available actions
const actions = await window.AICommands.getActions();
// Get a specific action
const action = await window.AICommands.getAction('searchMenu');
// Execute an action
const result = await window.AICommands.execute('searchMenu', { query: 'vegetarian' });
// Read page content
const content = await window.AICommands.readContent('#main-menu');
// Get page info
const info = await window.AICommands.getPageInfo();
// Authenticate
const session = await window.AICommands.authenticate('wab_key_abc123', { agentName: 'MyAgent' });All methods MUST return Promises. Errors MUST be thrown as JavaScript Error objects with a code property matching the WAB error codes (Appendix B).
The BiDi interface provides a lower-level, WebDriver BiDi-compatible transport:
const response = await window.__wab_bidi.send({
id: 1,
method: 'wab.executeAction',
params: { name: 'searchMenu', data: { query: 'vegetarian' } }
});The BiDi interface MUST:
- Accept the standard Command format from Section 5.1.
- Return the standard Response format from Section 5.2.
- Expose a
getContext()method returning the current Bridge context. - Support event subscriptions via
subscribe(events, callback).
The Bridge MUST signal readiness by dispatching a custom DOM event:
window.dispatchEvent(new CustomEvent('wab:ready', {
detail: { version: '1.0.0', transport: 'js_global' }
}));Agents SHOULD listen for this event or poll for the existence of window.AICommands / window.__wab_bidi.
Identifier: websocket
Scope: Remote agents, real-time bidirectional communication
URL: Declared in transport.websocket.url
The Agent establishes a WebSocket connection to the URL specified in the Discovery Document:
const ws = new WebSocket('wss://acme-restaurant.com/ws/wab');Upon connection, the Agent SHOULD send a wab.authenticate command as the first message if authentication is required.
All messages are JSON-encoded text frames. Binary frames MUST NOT be used.
Agent → Bridge (Command):
{ "id": "cmd_001", "method": "wab.getActions", "params": {} }Bridge → Agent (Response):
{ "id": "cmd_001", "type": "success", "result": [...] }Bridge → Agent (Push Event):
{ "id": null, "type": "event", "event": "actionExecuted", "data": { "name": "searchMenu" } }Push events have a null id and type set to "event".
- The Bridge SHOULD send a
wab.pingresponse every 30 seconds as a heartbeat. - If the Agent receives no messages for 60 seconds, it SHOULD close and reconnect.
- The Bridge MUST close the connection when the session expires.
- The close frame SHOULD include a reason code:
4001(session expired),4002(rate limited),4003(authentication failed).
Identifier: http
Scope: Server-to-server, stateless interactions
Base URL: Declared in transport.http.base_url
Each WAB method maps to an HTTP endpoint:
| WAB Method | HTTP Method | Path |
|---|---|---|
wab.discover |
GET | {base}/discover |
wab.getContext |
GET | {base}/context |
wab.getActions |
GET | {base}/actions |
wab.executeAction |
POST | {base}/execute |
wab.readContent |
POST | {base}/read |
wab.getPageInfo |
GET | {base}/page-info |
wab.authenticate |
POST | {base}/authenticate |
wab.subscribe |
POST | {base}/subscribe |
wab.ping |
GET | {base}/ping |
GET requests pass parameters as query strings:
GET /api/wab/actions?category=ordering HTTP/1.1
Host: acme-restaurant.com
Authorization: Bearer sess_x9y8z7w6
X-WAB-Version: 1.0POST requests pass parameters as JSON bodies:
POST /api/wab/execute HTTP/1.1
Host: acme-restaurant.com
Content-Type: application/json
Authorization: Bearer sess_x9y8z7w6
X-WAB-Version: 1.0
{
"name": "placeOrder",
"data": { "items": ["item_01"], "address": "123 Main St" }
}HTTP responses use standard status codes and return the WAB Response format in the body:
| HTTP Status | WAB Type | Meaning |
|---|---|---|
| 200 | success |
Command executed successfully. |
| 400 | error |
Invalid command or parameters. |
| 401 | error |
Authentication required or failed. |
| 403 | error |
Permission denied. |
| 404 | error |
Action not found. |
| 429 | error |
Rate limited. Includes Retry-After header. |
| 500 | error |
Internal Bridge error. |
| Header | Direction | Required | Description |
|---|---|---|---|
X-WAB-Version |
Request | REQUIRED | WAB protocol version (1.0). |
Authorization |
Request | CONDITIONAL | Bearer {sessionToken} for authenticated methods. |
Content-Type |
Request | CONDITIONAL | application/json for POST requests. |
X-WAB-Request-Id |
Request | RECOMMENDED | Unique request ID for tracing. |
X-WAB-Request-Id |
Response | RECOMMENDED | Echoed from request. |
Retry-After |
Response | CONDITIONAL | Seconds to wait (on 429 responses). |
The Bridge enforces a layered permission model:
- Discovery-level permissions — Declared in
capabilities.permissions. These are the maximum permissions the site grants. - Session-level permissions — Returned in the
wab.authenticateresponse. These MAY be a subset of discovery-level permissions based on the Agent's tier or API key. - Action-level requirements — Each action's
requiresAuthfield determines whether a session is needed.
Permission enforcement is multiplicative: an action is permitted only if ALL applicable permission layers allow it.
Commands that trigger DOM interactions (click, fill, scroll) MUST be executed within a security sandbox:
- Selector validation: The Bridge MUST verify that the target selector is not in the
blockedSelectorslist and, ifallowedSelectorsis non-empty, is in theallowedSelectorslist. - Action isolation: Each action MUST be executed atomically. A failure in one action MUST NOT corrupt the state of other pending actions.
- Output sanitization: The Bridge MUST sanitize all content returned via
wab.readContentto prevent injection attacks (strip<script>tags, event handlers, etc.).
A conforming Bridge SHOULD maintain an audit log of all commands received and responses sent. Each log entry MUST include:
| Field | Description |
|---|---|
timestamp |
ISO 8601 timestamp. |
commandId |
The command's id field. |
method |
The WAB method invoked. |
agentId |
Identifier for the Agent (from session or API key). |
origin |
The requesting origin (for JS global and HTTP transports). |
status |
"success" or "error". |
errorCode |
Error code if applicable. |
latencyMs |
Time to process the command in milliseconds. |
Audit logs SHOULD be retained for at least 30 days. Enterprise-tier implementations MAY retain logs for up to 7 years for compliance purposes.
Sessions provide the primary authentication mechanism:
- An Agent authenticates via
wab.authenticatewith an API key. - The Bridge returns a
sessionTokenwith a bounded TTL (default: 3600 seconds). - The Agent includes the token in all subsequent commands.
- The Bridge MUST reject commands with expired or invalid tokens with error code
SESSION_EXPIREDorINVALID_TOKEN.
Session tokens MUST be:
- At least 128 bits of entropy.
- Opaque to the Agent (no embedded claims that the Agent can decode).
- Transmitted only over secure channels (HTTPS, WSS).
When security.require_origin_match is true:
- The Bridge MUST validate the
Originheader on HTTP requests. - The Bridge MUST validate the
document.referrerorwindow.locationfor JS global transport. - Commands from non-matching origins MUST be rejected with error code
ORIGIN_MISMATCH.
The Bridge MUST enforce rate limits as declared in security.max_rate:
- Rate limits are per-session (authenticated) or per-origin (unauthenticated).
- When the limit is exceeded, the Bridge MUST return error code
RATE_LIMITED. - The response MUST include a
retryAfterfield (seconds) orRetry-AfterHTTP header. - The Bridge SHOULD use a sliding window algorithm for rate calculation.
The Bridge MUST prevent privilege escalation:
- An Agent MUST NOT gain permissions beyond those granted at authentication.
- Re-authentication MUST NOT expand permissions without Site Owner configuration change.
- Session tokens MUST NOT be transferable between Agents.
- The Bridge MUST detect and reject replayed commands (duplicate
idwithin a session).
For high-security environments, the Bridge MAY require command signing:
{
"id": "cmd_001",
"method": "wab.executeAction",
"params": { "name": "transferFunds", "data": { "amount": 100 } },
"context": { "sessionToken": "sess_abc" },
"signature": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}The signature field is an HMAC-SHA256 of the canonical JSON representation of method + params, keyed with a shared secret established during authentication.
Status: REQUIRED for new clients. Tokens issued without an explicit
scopefield MUST be treated as legacy unscoped tokens (admin:*:*) and SHOULD trigger a deprecation warning in the issuer's audit log.
A session token granted for one environment (e.g. staging) or one access
level (e.g. read-only analytics) MUST NOT be usable to perform a destructive
operation in production. This is the primary defence against the
"compromised-or-confused agent" failure mode in which a single broad token
straddles environments and lets a runaway agent execute irreversible
commands (database drops, volume deletions, mass account purges).
A scope is a triplet (access, env, resources):
| Field | Values | Default |
|---|---|---|
access |
read < write < admin (strict hierarchy) |
read |
env |
sandbox | staging | production | * |
* |
resources |
array of glob patterns ("*", "orders.*", "a/b/*") |
["*"] |
Compact string form: access:env[,env]:resource[,resource].
Object form: { "access": "read", "env": ["staging"], "resources": ["*"] }.
Aliases accepted by the parser: readonly/ro → read; rw → write;
full → admin; prod/live → production; dev/development → sandbox.
The Bridge MUST treat the following verbs (case-insensitive, token-split on
. - _ / : whitespace) as destructive by default:
delete, destroy, drop, truncate, purge, wipe, erase,
remove, unlink, rm, rmdir,
reset, reinit, reformat, format,
shutdown, terminate, kill,
revoke, disable, deactivate,
volume-delete, db-drop, database-drop
Sites MAY extend or override this list via two wab.json fields:
A read-scope token MUST be denied with DESTRUCTIVE_REQUIRES_WRITE if the
target action matches the destructive list. admin access subsumes write
for destructive operations; read never does.
For every command (name, env, resource, optional action_kind) the
Bridge MUST evaluate, in order:
-
Environment match. If
scope.env≠*and the command'senvis not inscope.env, deny withENV_MISMATCH. -
Destructive gate. If the action is destructive (per §8.9.3) and
scope.access == read, deny withDESTRUCTIVE_REQUIRES_WRITE. -
Access level. Compute the required access level:
- explicit
action_kindif present, else readif name matches^(read|get|list|search|find|view|page-info|ping|discover|actions), elsewrite.
If
rank(scope.access) < rank(required), deny withREADONLY_VIOLATION(when the gap is read→write) orINSUFFICIENT_SCOPE. - explicit
-
Resource glob. If a
resourceis supplied and no pattern inscope.resourcescovers it, deny withRESOURCE_OUT_OF_SCOPE.
Tokens MAY delegate by issuing narrower sub-tokens. The Bridge MUST compute the intersection of parent and child scopes and MUST NOT permit the child to widen any axis:
| Axis | Rule |
|---|---|
access |
child rank ≤ parent rank |
env |
child env set ⊆ parent env set (* inherits parent) |
resources |
every child glob covered by ≥ 1 parent glob (* inherits parent) |
Any violation MUST be rejected at issuance with INSUFFICIENT_SCOPE or
ENV_MISMATCH — never silently downgraded.
| Code | HTTP | Meaning |
|---|---|---|
INVALID_SCOPE |
400 | Scope string/object did not parse. |
INSUFFICIENT_SCOPE |
403 | Token lacks the required access level. |
READONLY_VIOLATION |
403 | Read-scope token attempted a write. |
DESTRUCTIVE_REQUIRES_WRITE |
403 | Destructive verb used by a read-scope token. |
ENV_MISMATCH |
403 | Token environment does not include the requested env. |
RESOURCE_OUT_OF_SCOPE |
403 | Resource glob does not cover the target. |
POST /api/wab/authenticate and POST /api/license/token accept an
optional scope field (string or object form). The response MUST echo
the canonical scope back:
POST /api/wab/authenticate
{
"siteId": "site_abc",
"scope": "read:staging"
}
200 OK
{
"type": "success",
"result": {
"authenticated": true,
"token": "…",
"scope": "read:staging:*"
}
}server/security/token-scope.js (this repository, MIT) — pure functional
authoriser tested across >40 cases including environment isolation,
destructive-verb edge cases, and delegation intersection. Verifiers in
other languages MUST produce the same allow/deny verdict for every test
vector in tests/scoped-tokens.test.js.
The 2024 PocketOS incident — a single agent deletes a production database
and its backups in nine seconds — established the need for an
irreversibility brake below the scope-token layer. Even an admin-scope
token MUST NOT be enough to one-shot a destructive action; the agent
MUST first issue a dry_run: true request, receive a server-issued
plan that summarises what would happen, and only then retry with
dry_run: false carrying the plan id.
POST /api/wab/actions/{name} # phase 1
Authorization: Bearer <token>
Content-Type: application/json
{ "dry_run": true, "params": { ... } }200 response:
{ "type": "success", "result": {
"dry_run": true,
"plan_id": "wabp_<32 hex>",
"expires_at": "2025-01-01T12:05:00Z",
"simulated": {
"would_affect": ["site:s1", "action:deleteVolume"],
"side_effects": ["deleteVolume"],
"reversible": false,
"summary": "Would execute …"
}
}}POST /api/wab/actions/{name} # phase 2
{ "dry_run": false, "plan_id": "wabp_…", "params": { … same as phase 1 } }A plan is bound to four axes; any drift invalidates it:
- session token (fingerprinted via
sha256truncated to 16 hex) - site id
- action name
- canonical hash of
params(sorted keys, recursive)
Plans are single-use and consumed on success.
| knob | default | max |
|---|---|---|
| TTL | 5 min | 60 min |
| Plan store size | — | 5 000 entries (LRU eviction at 90 % cap) |
A multi-instance Bridge MUST back the plan store with a shared store (Redis recommended); the in-memory implementation in this repo is correct for single-process deployments and tests.
"dryRunPolicy": "auto" | "off" | "always", // default "auto"
"destructiveActions": ["finalize-invoice"], // extends default verbs
"nonDestructiveActions":["delete-draft"] // suppressesauto defers to the default destructive-verb list (see §8.9.4); always
gates every action; off disables the gate entirely (only for fully
isolated sandboxes).
| code | meaning |
|---|---|
DRY_RUN_REQUIRED |
destructive call without prior plan |
DRY_RUN_PLAN_NOT_FOUND |
unknown / already-consumed plan_id |
DRY_RUN_PLAN_MISMATCH |
session, site, action, or params drifted |
DRY_RUN_PLAN_EXPIRED |
plan past TTL |
server/security/dry-run.js (this repository, MIT) — pure functional
plan store + classifier. 30 jest cases in tests/dry-run.test.js.
Even with scoped tokens and a dry-run plan, a fully-prompt-injected
agent can self-confirm by sending a dry_run: false retry. The fix is
an approval channel the agent cannot see — Telegram, WhatsApp, email,
Slack, or any operator-supplied transport. The Bridge generates a
6-digit numeric code, sends it via the configured transport, and
withholds execution until a human approves OOB.
Phase 1 — agent attempts a gated action:
POST /api/wab/actions/{name} → 202 Accepted
{
"type": "error",
"error": {
"code": "HUMAN_GATE_REQUIRED",
"challenge_id": "wabh_<32 hex>",
"expires_at": "…",
"dispatched_to": "telegram:…|whatsapp:…|email:…"
}
}Phase 2 — human approves OOB:
POST /api/wab/human-gate/approve
{ "challenge_id": "wabh_…", "code": "473820" }Phase 3 — agent retries, supplying the (now approved) confirmation_id:
POST /api/wab/actions/{name}
{ "params": {…}, "confirmation_id": "wabh_…" }Approvals are bound to (sessionFingerprint, siteId, actionName, paramsHash) — same axes as dry-run. State machine:
pending ─approve→ approved ─consume→ consumed
│
└─reject→ rejected
5 wrong code attempts on a single challenge transitions it directly to
rejected with reason too_many_attempts. Approved challenges are
single-use; consumption is atomic.
The gate is enabled by siteConfig.humanGate.enabled === true AND one
of:
- site tier ∈ {
pro,premium,enterprise}, OR siteConfig.humanGate.force === true(free-tier opt-in for safety).
Pluggable: humanGate.setTransport(name, fn). Default null is a
no-op (operator must use admin peek). Implementations SHOULD support
at minimum: telegram, whatsapp-cloud-api, generic-webhook, smtp.
| code | HTTP | meaning |
|---|---|---|
HUMAN_GATE_REQUIRED |
202 | first attempt — challenge issued |
HUMAN_GATE_PENDING |
425 | retry while approval is still pending |
HUMAN_GATE_REJECTED |
403 | human rejected the action |
HUMAN_GATE_MISMATCH |
403 | bindings drifted since approval |
HUMAN_GATE_EXPIRED |
403 | challenge past TTL |
HUMAN_GATE_CONSUMED |
403 | approval already used |
HUMAN_GATE_BAD_CODE |
401 | wrong code |
HUMAN_GATE_LOCKED |
429 | 5+ wrong attempts |
HUMAN_GATE_NOT_FOUND |
404 | unknown challenge_id |
server/security/human-gate.js (this repository, MIT). 27 jest cases
in tests/human-gate.test.js.
A scope-allowed, dry-run-confirmed, human-approved request can still be
catastrophic if it carries panic-pattern signals (force=true,
all=*, large bulk arrays, burst velocity after a failure). The Intent
Engine adds a risk score 0..100 computed from the COMPOSITION of the
request and ESCALATES the gate stack when the score crosses thresholds.
| signal | weight |
|---|---|
| destructive verb | +50 |
| write verb | +10 |
| production environment (write/destructive only) | +30 |
| staging environment | +10 |
danger token in params (force, permanent, cascade, …) |
+15 each, cap +30 |
wildcard target (*, all) |
+15 |
| large array (≥20 elements) | +10 |
| burst (≥3 destructive ops in 60 s) | +15 |
| high velocity (<1 s since last) | +10 |
Score is capped 0..100. Levels:
| score | level | required gate |
|---|---|---|
| 0–29 | low | none |
| 30–69 | medium | dry_run |
| 70–89 | high | human_gate |
| 90–100 | critical | block |
Auto-on for tiers premium and enterprise; opt-in via
config.intentEngine.enabled = true. Sites may override thresholds and
add custom rewrites (e.g. delete-account → archive-account).
The engine runs between permission check and the dry-run gate. On
block the response is HTTP 403:
{ "type": "error",
"error": {
"code": "INTENT_BLOCKED",
"message": "Request blocked by intent analysis (score=95, level=critical). Reasons: …",
"intent": { "score": 95, "level": "critical", "reasons": [...], "verb_class": "destructive", "rewrites": [...] }
}}For non-block escalations the engine MUTATES the gate stack: a
dry_run verdict triggers the §8.10 path even on non-default-destructive
verbs; a human_gate verdict triggers §8.11.
server/security/intent-engine.js (this repository, MIT). 31 jest
cases in tests/intent-engine.test.js.
Defense-in-depth presumes failures. When all upstream gates allow a destructive action and that action turns out to be a mistake, an operator break-glass MUST exist to undo it. Module 5 records a before-image of every executed destructive action and exposes admin endpoints to restore it.
CREATE TABLE wab_snapshots (
id TEXT PRIMARY KEY, -- "wabs_<32 hex>"
site_id TEXT NOT NULL,
action_name TEXT NOT NULL,
actor_id TEXT,
actor_type TEXT NOT NULL DEFAULT 'agent',
session_fingerprint TEXT,
params_hash TEXT,
snapshot TEXT NOT NULL, -- opaque JSON
meta TEXT,
reversible INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'recorded'
CHECK(status IN ('recorded','restored','expired','failed')),
created_at TEXT NOT NULL,
restored_at TEXT,
expires_at TEXT
);recorded → restored (success) or recorded → failed (restorer
rejects/throws) or recorded → expired (TTL passed). Restoration is
single-use: a restored row cannot be replayed.
Each site registers a per-site restorer callable via
rollback.setRestorer(siteId, fn). The function receives:
{ snapshot_id: string,
site_id: string,
action_name: string,
params_hash: string,
snapshot: any,
meta: object }and returns { ok: true } or { ok: false, error: string }. Throws
mark the snapshot failed with code RESTORER_THREW.
Authenticated with the site's apiKey via headers X-WAB-Site-Id +
X-WAB-Api-Key:
GET /api/wab/admin/snapshots[?status=recorded&limit=50]
GET /api/wab/admin/snapshots/:id
POST /api/wab/admin/rollback/:id
Activated when:
- site tier =
enterprise, OR config.snapshots.enabled === true.
The Bridge records a snapshot just before executing any action whose
verb is destructive (per §8.9.4) or which the Intent Engine
classified as verb_class: 'destructive'. The returned action result
includes snapshot_id so operators can find it instantly in audit.
Default 30 days. Past-TTL rows are batch-marked expired by
rollback.expireOld() (run via cron or on demand).
server/security/rollback-store.js (this repository, MIT). 20 jest
cases in tests/rollback.test.js.
The WAB Fairness Protocol is a unique feature that addresses a critical problem in AI-driven commerce: the tendency for AI agents to preferentially route traffic to large, well-known brands at the expense of small and independent businesses.
The Fairness Protocol establishes a set of rules and mechanisms that ensure WAB-enabled sites are treated equitably by AI agents, regardless of the site's size, brand recognition, or advertising budget.
Agents that implement the WAB protocol MUST adhere to the following fairness rules:
- No preferential routing. An Agent MUST NOT preferentially route users to one WAB-enabled site over another based solely on brand size, popularity metrics, or commercial arrangements between the Agent operator and the site.
- Capability-based ranking. When an Agent selects between multiple WAB-enabled sites that can fulfill a user's request, selection MUST be based on relevance to the user's query, capability match (which site's actions best fulfill the request), and quality signals (user ratings, response time, error rate).
- Transparency of selection. An Agent SHOULD be able to explain why it selected one site over another. The explanation MUST reference objective criteria, not commercial relationships.
To ensure equal visibility, the WAB ecosystem defines a Discovery Registry — a public, decentralized index of WAB-enabled sites:
- Any site with a valid Discovery Document MAY register with the Discovery Registry.
- The Registry MUST accept all registrations that pass schema validation and trust verification.
- The Registry MUST NOT charge differential fees based on site size or traffic volume.
- Agents SHOULD use the Discovery Registry as their primary source for finding WAB-enabled sites.
Registry entries contain:
{
"url": "https://acme-restaurant.com",
"provider": { "name": "Acme Restaurant", "category": "restaurant" },
"location": { "city": "Amman", "country": "JO" },
"capabilities_summary": ["viewMenu", "placeOrder", "searchMenu"],
"fairness_metrics": { "commission_rate": "0%", "is_independent": true },
"trust_level": "verified",
"registered_at": "2026-01-15T00:00:00Z"
}When multiple sites can fulfill a request, Agents SHOULD use the following scoring model:
| Factor | Weight | Description |
|---|---|---|
| Relevance | 40% | How well the site's capabilities match the user's intent. |
| Proximity | 20% | Geographic proximity to the user (for location-based services). |
| Capability depth | 15% | Number and richness of exposed actions. |
| Quality | 15% | Historical success rate, response time, uptime. |
| Freshness | 10% | How recently the Discovery Document was updated. |
The following factors MUST NOT influence priority:
- Brand recognition or popularity metrics (Alexa rank, domain authority, etc.).
- Paid placement or advertising spend.
- Commercial agreements between the Agent operator and the site.
- Site traffic volume.
The fairness_metrics.commission_rate field provides transparency about intermediary costs:
- Sites MUST accurately report their commission rate.
- Agents SHOULD present commission information to users when relevant (e.g., comparing ordering platforms).
- Agents SHOULD prefer direct-to-business sites (commission_rate = "0%") when quality and relevance are equal.
- The
direct_benefitfield SHOULD explain in plain language how the interaction benefits the site owner.
To prevent gaming of the Fairness Protocol:
- Discovery Documents MAY be signed with a cryptographic key registered in the Discovery Registry.
- Third-party auditors MAY verify that Agent implementations comply with the fairness rules.
- The
trust_signaturesfield in the Discovery Document allows sites to present third-party attestations. - Agents SHOULD log their site selection decisions for auditability.
Agent operators SHOULD publish periodic fairness reports including:
- Distribution of traffic across site sizes (small / medium / large).
- Percentage of traffic routed to independent vs. chain businesses.
- Average commission rate of selected sites.
- Selection algorithm transparency (open-source or audited).
The Model Context Protocol (MCP) provides a standard interface for LLMs to access external tools, resources, and prompts. WAB is designed to be fully compatible with MCP, enabling WAB-enabled sites to be exposed as MCP tools to any LLM.
Each WAB action maps to an MCP tool:
| WAB Concept | MCP Concept |
|---|---|
| Action | Tool |
| Action name | Tool name |
| Action params | Tool input schema (JSON Schema) |
| Action result | Tool output |
| Discovery Document | Resource |
| Bridge context | Resource |
A WAB action:
{
"name": "searchMenu",
"description": "Search menu items by keyword",
"trigger": "api",
"params": [
{ "name": "query", "type": "string", "required": true, "description": "Search term" }
]
}Maps to an MCP tool:
{
"name": "acme_restaurant__searchMenu",
"description": "Search menu items by keyword on Acme Restaurant",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search term" }
},
"required": ["query"]
}
}The Discovery Document is exposed as an MCP resource:
{
"uri": "wab://acme-restaurant.com/discovery",
"name": "Acme Restaurant WAB Discovery",
"mimeType": "application/json",
"description": "WAB capabilities for Acme Restaurant"
}Bridge context is exposed as a second resource:
{
"uri": "wab://acme-restaurant.com/context",
"name": "Acme Restaurant Bridge Context",
"mimeType": "application/json"
}A WAB-MCP bridge implementation MUST support both directions:
WAB → MCP (Site as Tool Provider):
- Read the site's Discovery Document.
- For each action, generate an MCP tool definition.
- When the LLM calls a tool, translate it to a
wab.executeActioncommand. - Return the WAB response as the MCP tool output.
MCP → WAB (LLM as Agent):
- The LLM receives WAB tools via MCP.
- The LLM decides which tool to call based on the user's request.
- The MCP server translates the tool call to a WAB command.
- The WAB response is returned to the LLM.
A reference MCP server for WAB SHOULD implement:
interface WABMCPServer {
// List WAB sites as MCP tools
listTools(): Tool[];
// Execute a WAB action via MCP tool call
callTool(name: string, arguments: object): ToolResult;
// List WAB discovery documents as MCP resources
listResources(): Resource[];
// Read a WAB resource
readResource(uri: string): ResourceContent;
}The MCP server MUST:
- Namespace tool names to avoid collisions (e.g.,
{site}__{action}). - Map WAB error codes to MCP error responses.
- Respect WAB rate limits and propagate
Retry-Afterinformation. - Cache Discovery Documents according to Section 6.2.
This specification defines two conformance levels:
| Level | Role | Description |
|---|---|---|
| WAB Bridge | Site-side | A site that implements the WAB protocol for agent consumption. |
| WAB Agent | Agent-side | An agent that consumes WAB-enabled sites according to the protocol. |
A conforming WAB Bridge:
- MUST serve a valid Discovery Document at
/agent-bridge.jsonor/.well-known/wab.json. - MUST support at least one transport layer (JS global, WebSocket, or HTTP).
- MUST implement all standard methods defined in Section 5.3.
- MUST enforce the permission model defined in Section 8.1.
- MUST enforce rate limits as declared in the Discovery Document.
- MUST return responses conforming to the Response Format in Section 5.2.
- MUST reject commands with invalid or expired session tokens.
- MUST use error codes from Appendix B.
- SHOULD implement audit logging as described in Section 8.3.
- SHOULD implement sandbox execution as described in Section 8.2.
- MAY implement the WebSocket and HTTP transports in addition to the JS global.
- MAY implement command signing as described in Section 8.8.
A conforming WAB Agent:
- MUST discover sites via the Discovery Document before interacting.
- MUST respect all permissions declared in the Discovery Document.
- MUST NOT execute actions for which it lacks permission.
- MUST authenticate before executing actions that require authentication.
- MUST respect rate limits and honor
Retry-Afterdirectives. - MUST follow the lifecycle phases defined in Section 6.
- MUST use the Command Format from Section 5.1.
- SHOULD support all three transport layers.
- SHOULD implement the Fairness Protocol from Section 9.
- SHOULD cache Discovery Documents to reduce load on sites.
- SHOULD implement graceful degradation across transport layers.
- MAY implement command signing for high-security interactions.
| Keyword | Count | Meaning |
|---|---|---|
| MUST | Core requirements | The implementation is non-conforming if violated. |
| MUST NOT | Prohibitions | The implementation is non-conforming if this occurs. |
| SHOULD | Strong recommendations | May be ignored with good reason, but implications must be understood. |
| SHOULD NOT | Discouraged practices | May be done with good reason, but implications must be understood. |
| MAY | Optional features | Truly optional; implementations may include or omit. |
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://webagentbridge.com/schemas/agent-bridge.json",
"title": "WAB Discovery Document",
"description": "Schema for the Web Agent Bridge discovery document (agent-bridge.json)",
"type": "object",
"required": ["wab_version", "provider", "capabilities", "transport"],
"properties": {
"wab_version": {
"type": "string",
"const": "1.0",
"description": "WAB protocol version"
},
"provider": {
"type": "object",
"required": ["name", "category", "url"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"category": { "type": "string", "minLength": 1 },
"url": { "type": "string", "format": "uri" },
"location": {
"type": "object",
"properties": {
"city": { "type": "string" },
"country": { "type": "string", "pattern": "^[A-Z]{2}$" },
"support_local": { "type": "boolean" }
}
}
}
},
"capabilities": {
"type": "object",
"required": ["commands", "permissions"],
"properties": {
"commands": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "description", "trigger", "params"],
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$"
},
"description": { "type": "string", "minLength": 1 },
"trigger": {
"type": "string",
"enum": ["click", "fill_and_submit", "scroll", "api", "navigate"]
},
"params": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "type", "required"],
"properties": {
"name": { "type": "string" },
"type": { "type": "string", "enum": ["string", "number", "boolean", "array", "object"] },
"required": { "type": "boolean" },
"description": { "type": "string" },
"default": {},
"enum": { "type": "array" }
}
}
},
"requiresAuth": { "type": "boolean", "default": false }
}
}
},
"permissions": {
"type": "object",
"properties": {
"readContent": { "type": "boolean" },
"click": { "type": "boolean" },
"fillForms": { "type": "boolean" },
"scroll": { "type": "boolean" },
"navigate": { "type": "boolean" },
"apiAccess": { "type": "boolean" },
"automatedLogin": { "type": "boolean" },
"extractData": { "type": "boolean" }
}
},
"tier": {
"type": "string",
"enum": ["free", "starter", "pro", "enterprise"],
"default": "free"
}
}
},
"agent_access": {
"type": "object",
"properties": {
"preferred_entry_point": { "type": "string" },
"api_fallback": { "type": "string", "format": "uri" },
"selectors": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"fairness_metrics": {
"type": "object",
"properties": {
"commission_rate": { "type": "string" },
"direct_benefit": { "type": "string" },
"is_independent": { "type": "boolean" }
}
},
"trust_signatures": {
"type": "array",
"items": { "type": "string" }
},
"transport": {
"type": "object",
"required": [],
"properties": {
"js_global": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"interface": { "type": "string", "default": "window.AICommands" }
}
},
"websocket": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"url": { "type": "string", "format": "uri" }
}
},
"http": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"base_url": { "type": "string" }
}
}
},
"anyOf": [
{ "properties": { "js_global": { "properties": { "enabled": { "const": true } } } } },
{ "properties": { "websocket": { "properties": { "enabled": { "const": true } } } } },
{ "properties": { "http": { "properties": { "enabled": { "const": true } } } } }
]
},
"security": {
"type": "object",
"properties": {
"require_origin_match": { "type": "boolean", "default": true },
"session_ttl": { "type": "integer", "minimum": 60, "default": 3600 },
"max_rate": { "type": "integer", "minimum": 1, "default": 60 }
}
}
}
}All WAB error responses MUST use one of the following standard error codes:
| Code | HTTP Status | Description |
|---|---|---|
INVALID_COMMAND |
400 | The command is malformed or missing required fields. |
INVALID_PARAMS |
400 | One or more parameters are invalid or missing. |
INVALID_METHOD |
400 | The specified method is not recognized. |
AUTHENTICATION_REQUIRED |
401 | The command requires authentication but no session token was provided. |
INVALID_TOKEN |
401 | The provided session token is invalid. |
SESSION_EXPIRED |
401 | The session token has expired. |
PERMISSION_DENIED |
403 | The Agent lacks permission to perform this action. |
ORIGIN_MISMATCH |
403 | The request origin does not match the allowed origins. |
ACTION_NOT_FOUND |
404 | The specified action name does not exist. |
SELECTOR_NOT_FOUND |
404 | The target DOM element could not be found. |
RATE_LIMITED |
429 | The Agent has exceeded the rate limit. |
DUPLICATE_COMMAND_ID |
409 | A command with this ID was already processed in this session. |
SELECTOR_BLOCKED |
403 | The target selector is in the blocked list. |
EXECUTION_FAILED |
500 | The action was attempted but failed during execution. |
BRIDGE_ERROR |
500 | An internal Bridge error occurred. |
TRANSPORT_ERROR |
502 | The transport layer encountered an error. |
TIMEOUT |
504 | The command timed out before completing. |
UNSUPPORTED_VERSION |
400 | The requested WAB protocol version is not supported by this Bridge. |
SANDBOX_VIOLATION |
403 | The command attempted an operation outside the security sandbox. |
SIGNATURE_INVALID |
401 | The command signature failed verification. |
Error response structure:
{
"id": "cmd_001",
"type": "error",
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Maximum 60 requests per minute.",
"retryAfter": 12
}
}The error object:
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | REQUIRED | One of the standard error codes above. |
message |
string | REQUIRED | Human-readable error description. |
retryAfter |
integer | CONDITIONAL | Seconds to wait before retrying (on RATE_LIMITED). |
details |
object | OPTIONAL | Additional error context for debugging. |
| MIME Type | Usage |
|---|---|
application/json |
Discovery Document, Command/Response bodies. |
application/vnd.wab+json |
Formal WAB media type (OPTIONAL). Implementations MAY use this for stricter content negotiation. |
text/event-stream |
Server-Sent Events fallback for subscriptions over HTTP (OPTIONAL). |
| Header | Required | Description |
|---|---|---|
X-WAB-Version |
REQUIRED | Protocol version. Value: 1.0. |
X-WAB-Request-Id |
RECOMMENDED | Unique request identifier (UUIDv4). |
X-WAB-Agent-Name |
OPTIONAL | Human-readable name of the Agent. |
X-WAB-Agent-Version |
OPTIONAL | Version of the Agent software. |
Authorization |
CONDITIONAL | Bearer {sessionToken} for authenticated requests. |
Content-Type |
CONDITIONAL | application/json for POST/PUT requests. |
Accept |
RECOMMENDED | application/json or application/vnd.wab+json. |
| Header | Required | Description |
|---|---|---|
X-WAB-Version |
REQUIRED | Protocol version supported by the Bridge. |
X-WAB-Request-Id |
RECOMMENDED | Echoed from request for correlation. |
X-WAB-Rate-Remaining |
RECOMMENDED | Number of requests remaining in the current rate window. |
X-WAB-Rate-Reset |
RECOMMENDED | Unix timestamp when the rate window resets. |
Retry-After |
CONDITIONAL | Seconds to wait (on 429 responses). |
Content-Type |
REQUIRED | application/json. |
Bridges serving the HTTP transport MUST configure CORS headers to allow agent access:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-WAB-Version, X-WAB-Request-Id, X-WAB-Agent-Name, X-WAB-Agent-Version
Access-Control-Expose-Headers: X-WAB-Version, X-WAB-Request-Id, X-WAB-Rate-Remaining, X-WAB-Rate-Reset, Retry-After
Access-Control-Max-Age: 86400If security.require_origin_match is true, the Bridge SHOULD replace the wildcard * with explicit allowed origins.
When serving the Discovery Document, the server SHOULD include:
Content-Type: application/json
Cache-Control: public, max-age=300
ETag: "v1-abc123"
X-WAB-Version: 1.0End of WAB Protocol Specification v1.0
Copyright 2026 Web Agent Bridge Contributors. Licensed under MIT.
The Agent Transaction Primitive elevates WAB from a discover-and-execute protocol into a trust + transaction layer. It introduces four DB-backed first-class primitives — intents, transactions, steps, receipts — that together provide the guarantees agentic commerce has been missing:
- Intent contracts — the user's signed authorization (scope, spend cap, expiry, single-use nonce).
- Idempotent execution —
UNIQUE (intent_id, idempotency_key)means retries can never double-execute. - Signed receipts — Ed25519-signed canonical JSON; the public
/api/atp/receipts/verifyendpoint requires no auth. - Compensation — explicit rollback path that decrements the intent's
spend counter and surfaces a
compensatedterminal state.
| Table | Purpose | Key invariants |
|---|---|---|
atp_intents |
Human → agent authorization contracts | nonce UNIQUE, CHECK on status |
atp_transactions |
Executions under an intent | UNIQUE (intent_id, idempotency_key), CHECK on status |
atp_steps |
Per-step ledger inside a transaction | UNIQUE (transaction_id, seq) |
atp_receipts |
Signed proof of outcome | UNIQUE (transaction_id) |
atp_nonces |
Single-use nonces, replay protection | PK = nonce |
Intent: draft → authorized → consumed | revoked | expired
Transaction:
pending → executing → executed → settled
↘ compensated
↘ failed → compensated
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /intents | JWT | Create draft intent. Daily quota by tier. |
| GET | /intents | JWT | List my intents. |
| GET | /intents/:id | JWT | Fetch one (owner only). |
| POST | /intents/:id/authorize | JWT | Burn nonce, move to authorized. |
| POST | /intents/:id/revoke | JWT | Revoke with reason. |
| POST | /transactions | JWT | Begin tx. Idempotency-Key header honored. |
| GET | /transactions/:id | JWT | Fetch tx + ordered steps. |
| POST | /transactions/:id/steps | JWT | Append step with evidence + compensation. |
| POST | /transactions/:id/transition | JWT | Move state machine. |
| POST | /transactions/:id/compensate | JWT | Rollback. |
| POST | /transactions/:id/receipt | JWT | Issue signed receipt (idempotent per tx). |
| GET | /receipts/:id | — | Public. Returns the signed receipt. |
| POST | /receipts/verify | — | Public. Verify by id or by raw body. |
| GET | /health | — | Liveness. |
Canonical JSON; signed with Ed25519 via wab-crypto.signManifest. The
top-level signature field is excluded from the canonicalized body, so
verifiers can reproduce the bytes bit-for-bit and validate without trusting
the server that issued them.
{
"type": "atp.receipt.v1",
"receipt_id": "atp_rcpt_…",
"issued_at": "ISO-8601",
"transaction": { "id": "...", "status": "settled", "amount_cents": 1500, "currency": "EUR", ... },
"intent": { "id": "...", "purpose": "...", "scope": { "actions": ["..."] }, ... },
"steps": [ { "seq": 1, "action": "...", "state": "succeeded", ... } ],
"site_id": null,
"agent_id": null,
"signature": {
"algorithm": "ed25519",
"value": "<base64>",
"key_id": "<16-char fingerprint>",
"public_key":"<base64, optional>",
"signed_at": "ISO-8601"
}
}The protocol, the SDK and public receipt verification are MIT-licensed and unauthenticated — that is the trust primitive and it must spread. Throughput, persistent key binding, compensation tooling and enterprise features are paid.
| Tier | Daily intent quota |
|---|---|
| free | 10 |
| starter | 50 |
| pro | 500 |
| business | 5 000 |
| enterprise | 100 000 |
- Nonces are single-use across the user (PK on
atp_nonces). - State transitions are guarded by both code (
VALID_TX_TRANSITIONS) and DB (CHECK (status IN ('pending','executing','executed','settled','failed','compensated'))). - Idempotency is structural, not advisory:
UNIQUE (intent_id, idempotency_key). - Spend cap is checked at every
beginTransactionagainst the livespent_centscounter — not just at intent creation. - Canonical JSON prevents the classic "re-serialize and forge" attack.
- Public verification is rate-limited (120 req/min per IP).
require('web-agent-bridge/sdk').ATPClient is a zero-dependency client
that wraps the REST surface and handles the Idempotency-Key header.
See sdk/atp.js.
WAB applies ATP to its own subscription business. Every payment processed by webagentbridge.com through Stripe produces a complete ATP cycle:
- On
invoice.payment_succeeded,recordPlatformPayment()runs the full lifecycle for the paying user:createIntent(purpose =WAB platform subscription � <tier>, scope ={actions:['pay']}, spend_cap = invoice amount),authorizeIntent,beginTransaction(idempotency_key = Stripe invoice id),appendStepwith the payment evidence, thenpending ? executing ? executed ? settled, and finallyissueReceipt. - The intent is tagged
metadata.platform = trueso the receipt can be aggregated into a public transparency feed without leaking any non-platform receipts. - Idempotency: replaying the same
invoice.idreturns the original receipt instead of creating a duplicate cycle.
Two new public endpoints expose the result:
| Method | Path | Description |
|---|---|---|
| GET | /api/atp/platform/receipts |
Latest platform receipts (id, tier, amount). |
| GET | /api/atp/platform/stats |
Aggregated counts and totals by tier. |
The human-facing view is /transparency.html. Any visitor can:
- read the feed,
- fetch the full signed body via
GET /api/atp/receipts/<id>, - re-verify the Ed25519 signature in their browser via
POST /api/atp/receipts/verify { "receipt_id": "<id>" }.
This is the marketing claim made operational: "WAB doesn't just build the trust layer for agentic commerce � it runs its own business on it."
{ "destructiveActions": ["finalize-invoice", "refund-all"], "nonDestructiveActions": ["delete-draft"] // suppress a default verb }