# Browser Driver

Bundled, first-party verify-proof driver targeting Chromium / Firefox /
WebKit via two transports: the Puppeteer MCP server OR the
`agent-browser` skill (Claude Code / OpenCode hosts).

## When Aperant picks this driver

`/apt:verify-proof` auto-selects `browser` when:

- The project's `package.json` declares a web framework (Next.js, React,
  Vue, Svelte, Angular, Solid) AND the project is NOT an Electron /
  Tauri / React Native container.
- The required IDL capabilities are a subset of: `navigate`, `click`,
  `type`, `screenshot`, `assert_visible`, `assert_text`, `wait_for_idle`,
  `execute_js`.

## Capabilities

The driver maps each IDL verb to BOTH transports. The workflow runner
picks the transport at dispatch time via `chooseTransport({has_skill_tool})`;
the chosen transport is recorded on each VerbResult as `transport: "skill" | "mcp"`.

| IDL verb | Puppeteer MCP tool | agent-browser CLI |
|----------|--------------------|--------------------|
| `navigate` | `mcp__puppeteer__puppeteer_navigate` | `agent-browser open <url>` |
| `click` | `mcp__puppeteer__puppeteer_click` | `agent-browser click @e<N>` |
| `type` | `mcp__puppeteer__puppeteer_fill` | `agent-browser fill @e<N> <text>` |
| `screenshot` | `mcp__puppeteer__puppeteer_screenshot` | `agent-browser screenshot` |
| `assert_visible` | `mcp__puppeteer__puppeteer_evaluate` (selector check) | `agent-browser snapshot` + `agent-browser get role <sel>` |
| `assert_text` | `mcp__puppeteer__puppeteer_evaluate` (textContent match) | `agent-browser snapshot` + `agent-browser get text <sel>` |
| `wait_for_idle` | `mcp__puppeteer__puppeteer_evaluate` (networkidle check) | `agent-browser wait --load networkidle` |
| `execute_js` | `mcp__puppeteer__puppeteer_evaluate` | `agent-browser eval <script>` |

## Transport selection

The driver carries TWO transport declarations on its manifest:

```json
"transport": { "mcp_server_id": "puppeteer", "skill_id": "agent-browser" }
```

The workflow runner picks between them via the driver's exported
`chooseTransport({has_skill_tool})` helper. The selection rule:

- **`Skill` tool exposed by host CLI** (Claude Code, OpenCode) → `'skill'`.
  Dispatches via `Skill({skill: "agent-browser", args: ...})`. One fewer
  external dep (no MCP server install).
- **No Skill tool** (legacy host, MCP-only environment) → `'mcp'`.
  Dispatches via `mcp__puppeteer__*` (the existing path).

The resolver's "one driver per runtime tag" invariant is preserved —
`browser` stays one driver with two transport options. No new
driverId; no resolver disambiguation needed.

## Manifest

- **Priority:** 100 (highest among bundled drivers — browser is the most
  common target).
- **Stability:** `ga`.
- **Trust:** `first_party_signed` (default-allow).
- **Transport:** `{ mcp_server_id: "puppeteer", skill_id: "agent-browser" }`.
  The `McpRegistry` handles consent gating for the MCP path per AC21;
  the existing skill-permissions registry handles consent for the
  Skill path.

## V1 scope — adapter is thin

This driver's `driver.mjs` is a manifest-conformant **adapter shim** in
v1. Each verb validates its args against `IDL_VERB_ARGS`, then returns
a VerbResult stub. **The actual transport dispatch — invoking
`mcp__puppeteer__*` tools OR `Skill({skill: "agent-browser", ...})` —
happens at the workflow-runner layer**, not inside this file: the model
loads the workflow prose, the workflow names IDL verbs, and the model
calls the tool whose name the driver manifest binds via
`transport.mcp_server_id` (MCP path) or `transport.skill_id` (Skill
path). The split exists because both tool surfaces are the model's, not
the driver's runtime — drivers in v1 are the validator + manifest +
resolver-anchor, not the executor.

The driver's adapter shim DOES record the chosen transport on the
returned VerbResult (`transport: "skill" | "mcp"`) so the workflow
runner can populate the corresponding field on each
`verification.json:tests[]` entry for audit purposes.

Follow-up `verify-proof-driver-transport-wiring-v2` will lift transport
dispatch into the driver process itself for drivers that want
out-of-band execution (e.g. node-side fetch for the api driver, or
direct Puppeteer.connect for headless CI).

## Overriding

To force the resolver to pick a different driver:

```json
// .aperant/config.json
{
  "verification": {
    "runtimes": ["electron", "cli"]
  }
}
```

The resolver places listed driver IDs first; the highest-coverage / most-
compatible / highest-priority driver still wins within the override list.
