Skip to content

a7garden/oxibrowser

Repository files navigation

OxiBrowser logo

🌐 OxiBrowser

The headless browser built in pure Rust for AI agents.

Not a Chromium fork. Not a C++ wrapper. A browser engine written from scratch in Rust, designed from day one for automation, web scraping, and AI-driven workflows.

CI Crates.io docs.rs GitHub release License: MIT GitHub stars Rust

Report Bug · Request Feature · Read the Docs · Discord


24 MB
Single static binary
~50 ms
Cold start time
~8 MB
Base memory
554 tests
Full coverage
Rust-first
C toolchain for TLS only
OxiBrowser Headless Chrome
24 MB binary ~400 MB install
~8 MB RAM base ~200 MB RAM base
~50 ms startup ~800 ms startup
Pure Rust (boa) C++ (V8)
MIT BSD / ToS

✨ Why OxiBrowser?

You're building AI agents that need to browse the web. You don't need a full browser with GPU rendering, audio output, and extension support. You need something fast, small, and programmable.

OxiBrowser is built for exactly that use case:

  • 🤖 AI-Agent First — CLI designed for agents: --json output, describe for schema, skill for prompts, session for multi-step
  • Blazing Fast — Cold starts in ~50ms, no Chromium overhead, no Node.js required
  • 🦀 Rust-Firstboa_engine (JS, no V8), html5ever (HTML) are pure Rust. TLS uses btls (BoringSSL C binding) for stealth fingerprint emulation. Single static binary.
  • 🔌 CDP Compatible — Puppeteer, Playwright, and any Chrome DevTools Protocol client works out of the box
  • 🛡️ Secure by Default — SSRF protection with CIDR blocking, robots.txt respect, no sandbox escape surface
  • 📦 Tiny Footprint — 24 MB binary, ~8 MB base memory. Run 100 instances without breaking a sweat

🆕 What's New in 0.17.0

This release closes most of the gap between "HTML fetcher" and "real headless browser" — events now behave like a browser, innerHTML works for SPA-style DOM injection, and fetch returns a spec-compliant Response with arrayBuffer().

Events & DOM

  • Event constructors honor the init dict — new MouseEvent('click', { clientX, clientY, ctrlKey, ... }) now actually carries those fields. Covers MouseEvent, KeyboardEvent, FocusEvent, Event, and a new DragEvent.
  • dispatchEvent sets event.target / event.currentTarget and returns !defaultPrevented. preventDefault / stopPropagation / stopImmediatePropagation work on every event.
  • Event bubbling walks the parent chain. Listeners are stored in a thread-local registry keyed by nodeId, so they survive across element-object re-queries (the bug that made parent.addEventListener invisible to child.dispatchEvent).
  • requestAnimationFrame / cancelAnimationFrame now schedule properly with a 16 ms deadline and pass a DOMHighResTimeStamp to the callback.
  • innerText getter and standalone performance global (window.performance === performance).

HTML & innerHTML

  • innerHTML setter parses the fragment via html5ever and inserts child nodes into the snapshot. outerHTML getter serializes the node back. A new dom_serializer module handles the round-trip with proper void-element / attribute-escape handling (12 unit tests).

Network

  • Response.text() / json() / arrayBuffer() all return spec-shaped Promises that resolve to the actual response body. fetch options headers (content-type, accept, authorization, user-agent, cookie) are now forwarded.
  • SSRF filter is now scheme-aware: only http/https go through DNS/host checks. about:blank is supported (Puppeteer/Playwright's default target URL now works).

CDP

  • Input.dispatchMouseEvent emits a real sequence: mousePressedmousedown; mouseReleasedmouseup + click; mouseMovedmousemove. Input.dispatchDragEvent is wired to a DragEvent on the element at the point.

🚀 Quick Start

Install

cargo install oxibrowser

Fetch a page (human-readable)

$ oxibrowser fetch https://example.com

Example Domain

# Example Domain

This domain is for use in documentation examples...
[Learn more](https://iana.org/domains/example)

Fetch a page (agent mode)

$ oxibrowser fetch https://example.com --json
{"ok":true,"data":{"url":"https://example.com/","title":"Example Domain","status":200,"markdown":"..."},"meta":{"elapsed_ms":152}}

Extract structured data

$ oxibrowser extract https://example.com --links --json
{"ok":true,"data":{"links":["https://iana.org/domains/example"],"title":"Example Domain"}}

Multi-step session (stdin/stdout JSON REPL)

$ oxibrowser session
new
{"ok":true,"data":{"tab_id":"t1"}}
goto t1 https://example.com
{"ok":true,"data":{"status":200,"title":"Example Domain"}}
eval t1 document.title
{"ok":true,"data":{"value":"Example Domain"}}
close t1
{"ok":true,"data":{"closed":"t1"}}
exit
{"ok":true,"data":{"exit":true}}

Start CDP server (Puppeteer/Playwright)

oxibrowser serve --port 9222
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
    browserWSEndpoint: 'ws://127.0.0.1:9222',
});

const page = await browser.newPage();
await page.goto('https://news.ycombinator.com');
console.log(await page.title());
await browser.close();

📋 CLI Reference

oxibrowser <COMMAND>

COMMANDS:
  fetch      Fetch a URL and return content (markdown default)
  extract    Extract structured data (links, text, elements)
  run        Run a YAML automation script
  session    Interactive stdin/stdout JSON REPL (22 commands)
  serve      Start CDP WebSocket server
  search     Web / GitHub / GitHub-issues search (no browser needed)
  describe   Print CLI schema as JSON (for agents)
  skill      Print agent skill guide
  version    Print version information

fetch — One-shot page fetch

# Human-readable (markdown, default)
oxibrowser fetch https://example.com

# Agent mode
oxibrowser fetch https://example.com --json

# Click then read
oxibrowser fetch https://example.com --click button --wait .result --json

# Quick page summary
oxibrowser fetch https://example.com --summary --json

# Run JS
oxibrowser fetch https://example.com --eval "document.title" --json

# Limit response size
oxibrowser fetch https://example.com --max-bytes 8000 --json

# Select specific fields
oxibrowser fetch https://example.com --fields url,title,status --json

extract — Structured data extraction

# Get all links
oxibrowser extract https://example.com --links --json

# Extract elements by CSS selector
oxibrowser extract https://example.com --selector "a" --all --attrs text,href --json

# Title + full text
oxibrowser extract https://example.com --title --text --json

session — Multi-step automation

oxibrowser session  # Start REPL

# 22 commands:
new, goto, back, forward, reload, click, fill, press, type,
select, check, uncheck, scroll, eval, extract, content,
screenshot, wait, close, close --all, list, help, exit

describe — Agent introspection

# Compact (~200 tokens)
oxibrowser describe --compact

# Full command details
oxibrowser describe fetch
oxibrowser describe session

search — Web / GitHub search (no browser needed)

# Web search (DuckDuckGo)
oxibrowser search "rust async" --engine ddg --max-results 5 --json

# GitHub search
oxibrowser search "memory pool" --source github --json

# GitHub issues for a specific repo
oxibrowser search "panic on shutdown" --source github-issues --repo a7garden/oxibrowser --json

run — YAML automation

name: example
steps:
  - step_type: goto
    data:
      goto: https://example.com
  - step_type: content
    data:
      format: markdown
oxibrowser run script.yaml

JSON Output Format

All --json responses follow the same schema:

{
  "ok": true,
  "data": { ... },
  "meta": { "elapsed_ms": 152 }
}

On error:

{
  "ok": false,
  "error": "URL scheme must be http or https",
  "error_code": "INVALID_URL"
}

Exit codes: 0=success, 1=runtime, 2=input validation, 3=timeout, 4=network


🏗 Architecture

┌──────────────────────────────────────────────────────┐
│            Puppeteer / Playwright / Rust CDP          │
└────────────────────────┬─────────────────────────────┘
                         │ CDP WebSocket
                         ▼
┌──────────────────────────────────────────────────────┐
│                 CDP Server (10 domains)               │
│  Browser · DOM · Fetch · Input · Network             │
│  OXI · Page · Runtime · Target                       │
├──────────────────────────────────────────────────────┤
│          Browser → Session → Page → Frame            │
├──────────┬──────────┬──────────────┬─────────────────┤
│  WebAPI  │  Network │  JS Runtime  │  CSS Rendering  │
│  DOM     │  HTTP    │  boa_engine  │  PNG screenshot │
│  Tree    │  Cookies │  ES2024+     │  ASCII/Unicode  │
│  Storage │  SSRF    │  persistent  │  text→image     │
├──────────┴──────────┴──────────────┴─────────────────┤
│   html5ever · encoding_rs · reqwest · image · boa    │
└──────────────────────────────────────────────────────┘

Crate Structure

Crate Lines Purpose
oxibrowser 4,242 Binary + CLI (8 subcommands, session REPL, agent features)
oxibrowser-core 19,794 Browser engine: Session, Page, Frame, JS Runtime
oxibrowser-cdp 4,583 CDP WebSocket server with 10 domain handlers
oxibrowser-webapi 1,587 DOM tree, CSS selectors, Markdown conversion
Total 30,206

🌟 Features

Agent-First CLI

Designed for AI agent workflows — no daemon, no socket, single binary:

Feature Description
--json Machine-readable output (opt-in, human by default)
--max-bytes N Truncate response to N bytes
--fields a,b,c Select specific output fields
--summary Quick page metadata (title, links, headings)
describe CLI schema as JSON for agent introspection
skill Agent skill guide for prompt injection
session Stdin/stdout JSON REPL with 22 commands
Exit codes 0=success, 1=runtime, 2=input, 3=timeout, 4=network

JavaScript Runtime (ES2024+)

Powered by boa_engine — pure Rust, no V8 dependency:

Web API Status
document.querySelector / querySelectorAll ✅ Full
document.createElement / createTextNode ✅ Full
element.appendChild / removeChild / insertBefore ✅ Full
element.getAttribute / setAttribute / removeAttribute ✅ Full
element.cloneNode / remove() ✅ Full
element.style (CSSStyleDeclaration) ✅ Property accessor
element.classList (DOMTokenList) ✅ Property accessor
element.textContent / innerHTML ✅ Read/Write
element.addEventListener / dispatchEvent ✅ Full
element.click() ✅ With event handlers
fetch() ✅ Full (channel bridge)
XMLHttpRequest ✅ Full with callbacks
localStorage ✅ Persistent
MutationObserver ✅ observe/disconnect/takeRecords
setTimeout / setInterval ✅ TokioJobQueue
console.log/warn/error ✅ With formatting
URL / URLSearchParams ✅ Full
crypto.getRandomValues ✅ Pseudo-random
TextEncoder / TextDecoder ✅ UTF-8
atob / btoa ✅ Base64
requestAnimationFrame ✅ Polyfill

CDP Protocol (Chrome DevTools Protocol)

10 domain handlers — Puppeteer and Playwright compatible:

Domain Key Methods
Browser getVersion, close
DOM getDocument, describeNode, querySelector, querySelectorAll
Fetch enable/disable, continueRequest, failRequest, fulfillRequest, getResponseBody
Input dispatchKeyEvent, dispatchMouseEvent, insertText
Network enable/disable, setExtraHTTPHeaders, getResponseBody
OXI 🤖 getMarkdown, getPageInfo — AI-native extensions
Page navigate, captureScreenshot, getFrameTree, getTitle
Runtime evaluate, callFunctionOn, enable, consoleAPICalled
Target getTargets, attachToTarget, detachFromTarget

OXI Domain — Built for AI Agents

import websockets, json, asyncio

async def ai_scrape():
    ws = await websockets.connect('ws://localhost:9222/ws')
    
    await ws.send(json.dumps({
        "id": 1, "method": "Page.navigate",
        "params": {"url": "https://news.ycombinator.com"}
    }))
    await asyncio.sleep(2)
    
    # Clean markdown — perfect for LLM ingestion
    await ws.send(json.dumps({"id": 2, "method": "OXI.getMarkdown"}))
    resp = json.loads(await ws.recv())
    print(resp['result']['markdown'])

Network Layer

Feature Description
HTTP Client reqwest with cookie persistence, redirect following
Cookie Jar Domain-scoped cookie storage with Set-Cookie parsing
SSRF Protection CIDR blocking for private network ranges
robots.txt RFC 9309 compliant parser, --obey-robots flag
Network Interception Pause, modify, or block any request via Fetch domain
Custom Headers Per-session and per-request header injection
Charset Detection encoding_rs for automatic charset detection and conversion

CSS Text Rendering

  • ASCII/Unicode text output — Render DOM to readable text with proper indentation
  • Markdown conversion — Full HTML→Markdown with heading, link, and list support
  • PNG screenshots — Built-in 8×16 bitmap font, renders text content as images
  • No external dependencies — Font data embedded in binary

🧪 Testing

# Run all tests
cargo test --workspace

# CLI integration tests (fast, no network)
cargo test -p oxibrowser --test cli

# E2E CDP tests
cargo test -p oxibrowser-cdp

# Integration tests (real websites, requires internet)
cargo test --workspace -- --ignored

🔧 Advanced Usage

Rust API

use oxibrowser_core::Browser;
use oxibrowser_core::config::BrowserConfig;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let browser = Browser::new(BrowserConfig::default()).await?;
    let session = browser.new_session().await?;
    
    session.navigate("https://example.com").await?;
    
    let title = session.evaluate("document.title").await?;
    println!("Title: {:?}", title);
    
    Ok(())
)
}

Use as a library

[dependencies]
oxibrowser-core = "0.11"
# Or the CDP server:
oxibrowser-cdp = "0.11"

Request Interception

const client = await page.target().createCDPSession();

await client.send('Fetch.enable', {
    patterns: [{ urlPattern: '*ads*' }]
});

client.on('Fetch.requestPaused', async ({ requestId }) => {
    await client.send('Fetch.failRequest', {
        requestId,
        reason: 'BlockedByClient'
    });
});

🤝 Contributing

See CONTRIBUTING.md for full guidelines.

git clone https://github.com/a7garden/oxibrowser.git
cd oxibrowser
cargo build
cargo test --workspace
cargo clippy --workspace -- -D warnings

📄 License

OxiBrowser is licensed under the MIT License.

🙏 Acknowledgments

  • boa_engine — Pure Rust JavaScript engine (ES2024+)
  • html5ever — HTML parser from the Servo project
  • reqwest — Ergonomic HTTP client for Rust
  • tokio — Async runtime powering the entire networking stack

⬆ Back to Top

Made with 🦀 in Rust

About

Headless browser in pure Rust — Servo-powered, CDP-compatible, designed for AI agents

Resources

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
LICENSE.md

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages