Metadata-Version: 2.5
Name: anybrowser
Version: 0.2.0
Summary: A pluggable browser-automation and agent runtime. Bring your own browser, transport, or model.
Project-URL: Homepage, https://github.com/christopher-inegbedion/anybrowser
Project-URL: Documentation, https://github.com/christopher-inegbedion/anybrowser/tree/main/docs
Project-URL: Source, https://github.com/christopher-inegbedion/anybrowser
Project-URL: Issues, https://github.com/christopher-inegbedion/anybrowser/issues
Project-URL: Changelog, https://github.com/christopher-inegbedion/anybrowser/blob/main/CHANGELOG.md
Author: AnyBrowser contributors
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: agent,automation,browser,cdp,chrome,llm,safari
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: agent
Requires-Dist: httpx>=0.27; extra == 'agent'
Requires-Dist: pillow>=10.0; extra == 'agent'
Provides-Extra: all
Requires-Dist: httpx>=0.27; extra == 'all'
Requires-Dist: pillow>=10.0; extra == 'all'
Requires-Dist: playwright>=1.44; extra == 'all'
Requires-Dist: rich>=13.0; extra == 'all'
Requires-Dist: typer>=0.12; extra == 'all'
Requires-Dist: websockets>=12.0; extra == 'all'
Provides-Extra: chrome
Requires-Dist: websockets>=12.0; extra == 'chrome'
Provides-Extra: cli
Requires-Dist: rich>=13.0; extra == 'cli'
Requires-Dist: typer>=0.12; extra == 'cli'
Provides-Extra: daemon
Requires-Dist: websockets>=12.0; extra == 'daemon'
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pillow>=10.0; extra == 'dev'
Requires-Dist: playwright>=1.44; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: rich>=13.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: typer>=0.12; extra == 'dev'
Requires-Dist: websockets>=12.0; extra == 'dev'
Provides-Extra: playwright
Requires-Dist: playwright>=1.44; extra == 'playwright'
Provides-Extra: safari
Requires-Dist: websockets>=12.0; extra == 'safari'
Description-Content-Type: text/markdown

# AnyBrowser

**A browser-automation and agent runtime you can take apart.** Three interfaces —
the browser, the transport, the model — each with a registry and a conformance
suite, so "implement your own" is something you can verify rather than hope for.

```python
from anybrowser import open_engine

async with await open_engine("playwright") as engine:
    await engine.navigate("https://example.com")
    page = await engine.snapshot()
    await engine.click(page.elements[0])
```

Same code against Chrome attached to your own logged-in window, against Safari,
or against a browser nobody has written a backend for yet.

---

## Why this exists

Most browser automation assumes it owns the browser: a fresh profile, a clean
window, a driver launched with the right flags. That assumption breaks the
moment the job is to work in the browser a person actually uses — their tabs,
their sessions, their logins, already open. You cannot ask them to log in again
inside a throwaway profile, and you cannot attach WebDriver to a window that was
not started for automation.

AnyBrowser assumes it does not own the browser. Every abstraction here comes from
driving real Chrome and Safari windows in production, and every one of the three
ideas below is a failure mode that cost someone a week.

**Backends differ in kind, not quality.** Safari has no CDP; Apple's Web
Inspector protocol needs private entitlements. WebDriver cannot adopt your open
window. So an engine *declares* what it can do and callers route around the
gaps, instead of discovering them by catching an exception halfway through a
task. See [capabilities](docs/architecture/capabilities.md).

**A no-op is not a success.** The most common way an agent dies is a click that
hit nothing, reported as "success", read back by the model as progress, and
repeated forever. Every action returns `changed` alongside `ok`, and the
conformance suite fails an engine that clicks dead space and calls it a change.
See [truthful outcomes](docs/architecture/truthful-outcomes.md).

**The contract is executable.** `pytest --pyargs anybrowser_conformance --engine
yours` is the definition of a working backend.

---

## Install

```bash
pip install anybrowser[playwright]      # the reference engine, easiest start
pip install anybrowser[chrome]          # attach to your own Chrome
pip install anybrowser[all]             # everything
```

Python 3.10+. `import anybrowser` pulls in no browser driver, no HTTP server and
no LLM SDK — every backend is an extra.

The Playwright engine needs its browser binaries once, which pip cannot fetch
for you:

```bash
playwright install chromium
```

Running a contract against something whose extra is missing skips rather than
fails, and says which extra to install — so `pip install anybrowser[all]` is the
shortcut if you would rather not think about it.

---

## The three interfaces

| You want to | Implement | Registry group | Ships with |
|---|---|---|---|
| Drive a different browser | [`BrowserEngine`](src/anybrowser/core/engine.py) | `anybrowser.engines` | `playwright`, `chrome`, `safari` |
| Change how clients reach the daemon | [`DaemonTransport`](src/anybrowser/daemon/transport.py) | `anybrowser.transports` | `websocket`, `unix`, `memory` |
| Use a different model | [`ModelProvider`](src/anybrowser/models/provider.py) | `anybrowser.models` | `openai`, `anthropic` |

Register with an entry point and yours is selectable by name everywhere:

```toml
[project.entry-points."anybrowser.engines"]
firefox = "my_package.engine:FirefoxEngine"
```

```bash
pytest --pyargs anybrowser_conformance --engine firefox     # the engine contract
pytest --pyargs anybrowser_conformance --transport grpc     # the transport one
```

Each flag selects a contract and skips the other. The suite is capability-gated. A backend that honestly declares it cannot drag
is skipped, not failed. A backend that *claims* it can drag and then doesn't is
failed — the suite tests truthfulness as hard as it tests function.

Full walkthrough: [**Writing an engine**](docs/guides/writing-an-engine.md).

---

## Engine status

| Engine | Attaches to your session | Trusted input | Engine contract | Gated by CI |
|---|---|---|---|---|
| `chrome` (DevTools) | no — needs the launch flag | yes | **28 passed, 4 skipped** | yes |
| `chrome` (extension) | **yes** — your own windows | yes | **28 passed, 4 skipped** (2026-10-02, Chrome 154) | no — needs a browser with the extension loaded |
| `playwright` | no — own profile | yes | **28 passed, 4 skipped** | yes, the reference engine |
| `safari` | yes — accessibility + extension | yes | **25 passed, 7 skipped** (2026-10-02, Safari 26.5, macOS 26.5; 6 of 7 runs, see [#23](https://github.com/christopher-inegbedion/anybrowser/issues/23)) | only that it refuses cleanly off-setup |

The engine contract is 32 tests. The 4 skips are the capability gate working as
intended: those tests assert the *error* an engine raises for something it does
not declare, so they stand down for an engine that declares it. Run the whole
suite directory and you will see more skips still — the transport and model
contracts, idle because you passed neither `--transport` nor `--model`.

Pointing the engine contract at the `remote` engine grades the whole daemon
stack — transport, protocol, codec, dispatch — and it scores the same **28
passed, 4 skipped** over both the memory and websocket transports.

The Chrome engine has two pipes behind one interface. `devtools` talks to a
browser started with `--remote-debugging-port` — standard, and the only mode
that runs in CI. `extension` relays CDP through a browser extension and is the
one that matters, because it attaches to the browser you already have open,
with your tabs and your logins. Same engine either way; the connection reports
which it is, and the declared `attach_to_user_session` capability is derived
from that rather than asserted next to it.

Both Chrome pipes pass the contract. To drive the browser you already have
open, see [driving your own Chrome](docs/guides/chrome-extension.md) — note that
`--load-extension` is silently ignored by Chrome stable, which is a memorable
afternoon if nobody tells you.

Safari is implemented in both halves — a Swift helper for trusted background
input and occlusion-proof capture, and a Web Extension for the DOM and pointer
gestures. It has passed the contract three runs in a row, with clicks carrying
real user activation (`isTrusted=true`), which is the whole reason the native
half exists. Seven tests stand down: `file_upload`, `full_page_screenshot` and
`cross_origin_frames` are honestly undeclared, and four are the capability gate.

**Reproducing it depends on one thing that is easy to get wrong.** Safari grants
an extension access per origin, and the suite serves its fixtures from
`127.0.0.1` — so permitting the site you happen to be looking at is not enough.
Choose *Always Allow on Every Website*, or the fixtures stay unpermitted, no
content script runs on them, and every test that touches the page fails while
the bridge itself connects and reports no problem.

The bridge now says so itself: its greeting carries `content_script`, which is
`"ok"` only when a content script in the active tab actually answered. Trust
that over `permissions.getAll()`, which reports what the extension *asked for*
and says `<all_urls>` even when Safari is injecting nothing anywhere.

Uploads are the notable absence. A file input's `files` cannot be set from
JavaScript, by design, and without a debugger protocol there is no counterpart
to CDP's `DOM.setFileInputFiles`. The accessibility path can only open the
system picker and drive it, which is a picker — so this engine says it cannot,
rather than appearing to and failing on a real form.

Two things that setup cannot be done without, and neither is optional:

- **The containing app must be signed with a real identity.** An ad-hoc
  signature (`CODE_SIGN_IDENTITY=-`) makes the extension invisible to Safari —
  it never appears in Settings ▸ Extensions, nothing is logged, and "Allow
  unsigned extensions" does not cover it. Sign with an Apple Development
  identity and install the app under `/Applications`.
- **Safari loads extension background content lazily.** The bridge dials the
  engine when its background page loads, and Safari does not load it just
  because the extension is enabled, so the engine waits and then refuses. Until
  that is fixed, wake it from Develop ▸ Web Extension Background Content before
  a run; it shows as "(not loaded)" when asleep.

---

## Command line

```bash
anybrowser plugins                        # what is installed
anybrowser info --engine chrome           # what that engine can do
anybrowser look https://example.com       # open a page, print what is on it
anybrowser serve --transport unix         # a daemon owning one browser
anybrowser run "find the pricing page"    # give an agent a goal
```

`run` drives a **reference loop**: deliberately small, honest about what it did,
and not where this library's value lives ([ADR-0005](docs/adr/0005-agent-is-a-reference-loop.md)).
It has seven tools. If you need more, call the engine directly, register your own
`Tool`, or bring your own loop and keep the browser layer — which is the part
with the contracts.

It takes a provider and its constructor options, so any OpenAI-compatible
endpoint works — a gateway, a local vLLM, Ollama:

```bash
anybrowser run "read the top pricing tier" \
  --engine playwright \
  --model openai --model-name google/gemini-3.5-flash \
  --model-option base_url=https://openrouter.ai/api/v1 \
  --model-option api_key=$OPENROUTER_API_KEY
```

`--confirm` asks before every action that changes the page, and `--max-steps`
caps the run. Engine options use `-o KEY=VALUE`, for example
`-o headless=true`.

---

## Layout

```
src/anybrowser/
  core/          interfaces and value types — imports no backend
  engines/       chrome, safari, playwright
  perception/    turning a page into a snapshot, engine-agnostic
  daemon/        protocol, transports, the server that owns an engine
  agent/         a reference loop: planner, tools, runner (see ADR-0005)
  models/        LLM providers
conformance/     the executable contract
docs/            architecture, guides, ADRs
```

---

## Docs

- [Architecture overview](docs/architecture/README.md)
- [Capabilities](docs/architecture/capabilities.md) — why backends declare instead of raise
- [Truthful outcomes](docs/architecture/truthful-outcomes.md) — why `changed` exists
- [Writing an engine](docs/guides/writing-an-engine.md)
- [Writing a transport](docs/guides/writing-a-transport.md)
- [Writing a model provider](docs/guides/writing-a-model-provider.md)
- [Driving your own Chrome](docs/guides/chrome-extension.md)
- [Driving Safari](docs/guides/safari-extension.md)
- [The agent is a reference loop](docs/adr/0005-agent-is-a-reference-loop.md) — what `anybrowser.agent` is for, and what it is not
- [ADRs](docs/adr/) — the decisions and what they cost

---

## Contributing

Start with [CONTRIBUTING.md](CONTRIBUTING.md). Good first issues are labelled
[`good first issue`](https://github.com/christopher-inegbedion/anybrowser/labels/good%20first%20issue);
a new backend is the highest-value contribution there is, and the conformance
suite means you can tell when it's done.

By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
Security reports go to [SECURITY.md](SECURITY.md), not the issue tracker.

## License

Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
