AGENTS.md · git:20260923.d8dbc36 · 2026-09-23 · sha256 b06fbfef78a7562e

AGENTS.md git:20260923.d8dbc36A

Immutable. This exact content never changes and is served at /api/v1/blob/b06fbfef78a7562e.

# Repository Guidelines

Repo: https://github.com/platonai/Browser4

## Architecture

```
browser4-cli (Rust)  ──MCP over HTTP──▶  browser4-rest (Kotlin/Spring)  ──▶  PulsarWebDriver (Kotlin/CDP)
     ▲                                           │
     │                                           ▼
     └──── e2e tests ────▶  Fixture HTTP server (Rust test harness)
```

- **CLI:** `cli/browser4-cli/` — Rust binary, MCP tool calls over HTTP
- **Backend:** `browser4-rest/` — Spring Boot, `MCPToolController` dispatches tools
- **Browser driver:** `browser4-core/browser4-browser/` — `PulsarWebDriver` wraps CDP
- **Agent tools:** `browser4-agentic/` — `AgentToolManager` maps MCP tool names → driver methods

### Dispatch chain (CLI → browser)

1. CLI builds MCP tool call: `{tool: "browser_type", arguments: {ref: "#el", text: "hi"}}`
2. `MCPToolController.callTool()` → `dispatchToToolExecutor()`
3. `normalizeFrontendToolCall()` applies `FRONTEND_TOOL_NAME_ALIASES` (e.g., `browser_type` → `fill`)
4. `DefaultArgumentNormalizer` maps `ref` → `selector`, strips `sessionId`, converts snake_case
5. `resolveMcpToolCall()` → `ToolCall("tab", "fill", args)`
6. `AgentToolManager.execute()` → `executor.callFunctionOn(toolCall, driver)` → `PulsarWebDriver.fill()`

### Batch commands

`handleCommandBatch()` → `handleBatchTool()` in `MCPToolController`. CLI's `compile_batch_request()` builds step arrays with `op: "tool"`. `preFocusSelector` only added for `keydown`/`keyup` (not fill/type/press).

### Known CDP pitfalls

- **crbug.com/444929150:** `Input.dispatchMouseEvent` type `mouseWheel` race condition in headless Chrome. Fix: dispatch to `{passive: false}` wheel listener.
- **Cursor positioning:** `DOM.focus()` + `Input.dispatchMouseEvent` (click) may leave cursor at 0. Fix: `setSelectionRange(99999, 99999)` after focus+click.
- **`Input.insertText` racing:** 0ms delay between chars drops `input` events. Fix: use same inter-char delay as `type()` via `randomDelayMillis("type")` (90-240ms).

### Direct CDP methods: mandatory review gates

Any method that implements behavior by calling CDP directly (or by evaluating
JavaScript that synthesizes browser events) — `drag`, `press`, `click`, dialog
handling, screenshots, scroll, storage restore, etc. — must satisfy ALL of the
following before it is considered done. "It works on my machine" is not
sufficient; each gate below has burned this project before.

1. **Anti-bot detection** — the page can observe the automation, and many sites
   actively fingerprint it. Consider:
   - `event.isTrusted` is `false` for JS-synthesized events; if a trusted path
     exists (CDP `Input.*`, `userGesture=true`), prefer it or document why not.
   - Fixed inter-event delays and exact element-center coordinates are
     fingerprints; randomize delays (via `randomDelayMillis` or an equivalent
     bucket) and jitter coordinates like real input would.
   - The full input story: real drags/clicks have trusted `mousedown` →
     `mousemove` → `mouseup` context; a synthetic event sequence with no
     trusted accompaniment is itself detectable.
2. **Negative impact** — check the failure modes, not just the happy path:
   - No silent failures: a method must fail loudly rather than dispatch onto
     an unrelated element (occlusion, `pointer-events:none`, layout shift,
     frames). Validate before dispatching so retries stay idempotent.
   - Error messages must surface the real cause to the CLI/user instead of
     being swallowed or wrapped by retry machinery.
   - Retry safety: blocks inside `invokeOnPage`/`invokeOnElement` must be
     idempotent or the deterministic failures must bypass the retry.
   - Don't regress error semantics of the upstream method you override.
3. **Cross-platform compatibility** — headless vs GUI Chrome, Docker/CI,
   Windows/Linux/macOS all behave differently:
   - Scroll commits and layout updates are asynchronous (`scrollIntoViewIfNeeded`
     may prefer smooth animation); poll for the observable state instead of
     assuming a single read is current.
   - Event dispatch timing, renderer scheduling, and viewport geometry differ
     under load and in containers; never rely on a single hardcoded delay.
   - CDP API availability varies by Chrome version; provide fallbacks or
     explicit errors, never a silent no-op.
4. **Real-webpage testing** — unit tests alone are insufficient:
   - Add a fixture page (under
     `browser4-tests/pulsar-tests-common/src/main/resources/static/b4/`) that
     exercises the real behavior: event order, payloads, occluded/disabled/
     off-viewport/frame targets, async layout changes.
   - Prove it against a real browser through the e2e harness
     (`cargo test --test e2e -- --scenario=...`, `requires_browser4: true`),
     not only against mocks.

## Project Structure

| Module | Description |
|---|---|
| `browser4-core` | Core engine: sessions, scheduling, DOM, browser control |
| `browser4-dependencies` | BOM and dependency alignment |
| `browser4-tools` | Operational tools and launch helpers |
| `browser4-agentic` | AI agents, MCP, skill registration |
| `browser4-agent-tools` | High-level agent tools: scraping, crawling, stateful page interaction |
| `browser4-rest` | Spring Boot REST layer & command endpoints |
| `cli/browser4-cli` | Rust CLI binary |
| `skills/browser4-cli` | AI agent skill definitions |
| `browser4-apps/browser4-standalone` | Product packaging, unified launcher (`target/Browser4.jar`) |
| `examples/browser4-examples` | Runnable examples |
| `browser4-tests` | E2E, integration, scenario tests |
| `browser4-tests/pulsar-tests-common` | Shared test base classes and utilities |
| `cdp-protocol` | Chrome DevTools Protocol JSON definitions |
| `coworker/` | File-queue automation for task-driven AI workflows |

## Development Environment (Running from Source)

When developing or running the CLI from the source tree (not an installed
binary), the following applies.

### Invocation wrappers

| Shell | Command | Notes |
|-------|---------|-------|
| PowerShell (Windows) | `./b4w.ps1 <command>` | Primary dev wrapper; builds from source if needed |
| Git Bash (Windows) | `./b4w.sh <command>` | Quotes args automatically for pwsh safety |
| Git Bash (alt) | `pwsh ./b4w.ps1 <command>` | Direct PowerShell invocation |
| Linux / macOS | `./b4w.sh <command>` | Same script works cross-platform |
| Any (installed) | `browser4-cli <command>` | After `browser4-cli install` |

> **Important:** The `$(./b4w.ps1) <command>` syntax shown in some task
> instructions does **not** work in bash — `$(…)` is command substitution, not
> invocation. Use `pwsh ./b4w.ps1 <command>` or `./b4w.sh <command>` instead.

### First-run latency from a source tree

The first launch builds the runtime bundle via Maven (~1–3 min, before the
spinner appears) and then starts the Browser4 backend (Spring Boot + JVM,
~10s). Subsequent commands are instant — the server stays alive between
invocations. The spinner shows stage-level progress (JVM → Spring Boot → MCP
tools).

### Prerequisites for development

- **[Rust](https://rustup.rs/)** — via `rustup`; needed to compile the CLI binary.
- **Java 25+** — required by the Browser4 backend server.
- **[Git](https://git-scm.com/)** — for cloning the repository.

Verify your setup with:

```bash
cargo --version && java -version
```

### Running the CLI from source

When running from source (not a globally installed binary), use `cargo run`
from the CLI directory:

```bash
cd cli/browser4-cli
cargo build                     # build the binary
cargo run -- <command>          # run a command (the -- separates cargo args from CLI args)
cargo run -- goto "https://example.com"
cargo run -- snapshot -v 0
```

**Note:** All docs use `browser4-cli` as the generic command name. If running
from source, substitute `cargo run --` (with the leading
`cd cli/browser4-cli &&` if not already in that directory).

**From repo root (no `cd` required):**

```bash
cargo run --manifest-path cli/browser4-cli/Cargo.toml -- <command>
```

This pattern works from any directory — no need to `cd` first.

### Output Redirection in Dev Mode

The working directory during `cargo run` is `cli/browser4-cli/`, so relative
file paths must account for this. Use `--quiet` to suppress cargo build output:

```bash
# From repo root: redirect query results to a file
cd cli/browser4-cli && cargo run --quiet -- htmlsnapshot query --sql @../../query.sql --result-only > ../../results.json

# From cli/browser4-cli/: same pattern with shorter relative paths
cargo run --quiet -- htmlsnapshot query --sql @query.sql --result-only > results.json
```

> **Tip:** `--quiet` passes through to cargo and suppresses the "Finished" /
> "Running" build-status lines that would otherwise pollute the output file.
> Without `--quiet`, those lines appear on stderr but `2>&1` captures them
> along with the data — use `--quiet` instead of `2>&1` for clean output.

## Build & Test

### Quick commands

```bash
# Build (skip tests)
./mvnw -DskipTests                           # Linux/macOS
.\mvnw.cmd -q -D"skipTests"                  # Windows PowerShell

# Rust unit tests (fast, no backend needed)
cd cli/browser4-cli && cargo test --bin browser4-cli

# Kotlin tests
mvn test -pl browser4-rest -am
mvn test -pl browser4-rest -am -Dtest=MCPToolControllerTest

# E2E tests (needs running backend or mock server)
cargo test --test e2e -- --nocapture
cargo test --test e2e -- --nocapture --scenario=test_e2e_batch_*

# Scoped test runs (Windows)
bin/test.ps1 fast|it|e2e|rest|skills|mcp|cli|browser4|mock-site
```

Maven profile switches in root `pom.xml`: `-DrunITs=true`, `-DrunE2ETests=true`, `-DrunCoreTests=true`, `-DrunRestTests=true`.

### E2E test filtering

```
cargo test --test e2e -- --help           # All options
--scenario <pattern>                      # Glob filter
--group <name>                            # Group filter (repeatable)
--level SMOKE|BASIC|EXTENDED|ALL          # Test depth
--fail-fast / --failed                    # Stop early / rerun failures
--max-failures <count>                    # Tolerated failing scenarios (default 5, 0 = none)
--list / --list-groups                    # Discover without running
--enable-all                              # Opt into excluded-by-default tests
--force-rebuild-bundle                    # Force local Maven + runtime rebuild
--force-remote-bundle                     # Download pre-built bundle instead
```

### Test locations

| Scope | Path |
|---|---|
| Unit tests | `src/test/kotlin/...` |
| REST integration/E2E | `browser4-tests/browser4-rest-tests/` |
| Shared utilities | `browser4-tests/pulsar-tests-common/` |
| Rust E2E | `cli/browser4-cli/tests/e2e/` |

> **Note:** The former integration/E2E suites in `pulsar-it-tests` and `pulsar-e2e-tests`
> have been migrated into the base library (browser4-core modules and
> `pulsar-tests-common`); the placeholder modules have been removed from this repo.

## Code Style

### Kotlin
- Immutable `data class`; explicit return types; null-safety (`require`/`check`/`?:`)
- Public APIs require KDoc
- Store AI-generated task docs in `docs-dev/copilot/`

### Logging
```kotlin
logger.info("Task {} finished in {} ms", taskId, cost)  // placeholders, never concatenation
```

### Naming
- Test methods: camelCase + `@DisplayName("...")` — **NOT** backtick naming
- Test classes: `<Name>Test.kt` (unit), `<Name>IT.kt` (integration), `<Name>E2ETest.kt` (e2e)

## Testing Guidelines

**Default policy:** Don't run full suites. Compile with tests skipped, run smallest relevant scope. Upgrade scope when risk increases (cross-module, public API/DTO/serialization, Spring wiring, dependency bumps, concurrency/I/O, browser lifecycle).

**Tag-driven scheduling** — use tags from `docs/TESTING.md`:
- Scope: `Unit`, `Integration`, `E2E`, `SDK`
- Speed: `Fast` (<5s), `Slow` (5–30s), `Heavy` (>30s)
- Gates: `Requires*`, `ManualOnly`

**Coverage targets:** Global ≥70%, Core ≥80%, Utilities ≥90%, Controllers ≥85%

**CI has two gates with different Maven test scopes** — pick the tags for the gate you are writing tests for:

| Gate | Workflow | Maven `excluded_groups` | Scope |
|---|---|---|---|
| PR Quality Gate | `.github/workflows/pr.yml` | `ManualOnly,RequiresAI,E2E,E2ETest,Slow,Heavy,HeavyTest,Integration,IntegrationTest,RequiresServer,RequiresBrowser,RequiresDocker,TestInfraCheck` | fast/unit only (`run_pulsar_tests: 'false'`) |
| CI/CD Pipeline (main + release tags) | `.github/workflows/ci.yml` | `ManualOnly,RequiresAI,E2E,E2ETest,Slow,HeavyTest,TestInfraCheck` | adds integration/infra tests that need Chrome, Docker and the started app |

Both gates pass `-Dsurefire.excludes=**integration**` (class-file pattern, not tags) and both derive success from **the Maven exit code plus the surefire XML totals** — the exit code is authoritative (`reconcile-status` in `.github/actions/run-tests/action.yml` refuses to turn a non-zero exit into a pass, and refuses a pass when no XML was produced at all), so a breached JaCoCo floor, a compile error or a dead test fork fails the gate even with zero parsed test failures. A test class is skipped by **tag**, never by name. `SDK` is excluded by neither gate (no test carries that tag today); `Heavy` is excluded only by the PR gate, `HeavyTest` by both. `.github/workflows/ci.yml` also builds all-main-modules, starts a Dockerized app on port 8182 and runs `cargo test` in `cli/browser4-cli`.

Neither `ci.yml` nor `nightly.yml` fails early any more: `Check Test Status` only records `MAVEN_TESTS_FAILED` in `$GITHUB_ENV`, the Docker build / app startup / CLI e2e stages still run, and a final `Enforce CI Gate` / `Enforce Nightly Gate` step (after `Pipeline Summary`) decides the job outcome from the JVM stage flag plus the CLI e2e outcome. So one round reports both sides instead of hiding the CLI suite behind a single broken JVM test.

The nightly gate (`.github/workflows/nightly.yml`, 00:00 UTC) is a superset: it adds `Slow`/`HeavyTest`/`TestInfraCheck`, measures JaCoCo in observe-only mode
(`-P...,quality-gate -Djacoco.check.skip=true`, no floor), runs `cargo test --bin browser4-cli --lib` (the only place the Rust unit tests run), runs the CLI e2e suite with `--level=EXTENDED --enable-all --max-failures=0`, and defers the job outcome to a final `Enforce Nightly Gate` step so a failing JVM test never skips the CLI e2e stage. See [TESTING.md § CI 门禁实际覆盖](docs/TESTING.md) for the current coverage inventory.
See [CI stabilization notes](docs-dev/copilot/ci-stabilization-4.13.x.md) before changing either list.

## Configuration

- Default port: **8182**
- Config files: `application.properties` → `application-*.properties` → `application-private.properties` (git-ignored, secrets here or env vars)
- Key properties: `openrouter.api.key`, `browser.profile.mode` (DEFAULT|SYSTEM_DEFAULT|SEQUENTIAL|TEMPORARY), `browser.display.mode` (GUI|HEADLESS|SUPERVISED)
- Display mode precedence: session capabilities (`headed` / `displayMode`, e.g. from `open --headed`/`--headless`) override the server-wide `browser.display.mode` default at browser launch (`AbstractPulsarSession.createBoundDriver`); server default applies only when the session has no display preference
- LLM providers configured via env vars: `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `VOLCENGINE_API_KEY`, `OPENAI_API_KEY`

## Development Patterns

### CLI command naming: spaced form preferred

New CLI commands must use the **spaced name** style (`verb noun`) — e.g.
`swarm submit`, `htmlsnapshot get`, `profiles list`, `plugin <domain>` — to
stay consistent with every other prefixed command. Internally the CLI is
kebab-case (`swarm-submit`); users type the spaced form, which
`rewrite_prefixed_command()` (main.rs) rewrites to the internal kebab name.

When adding a prefixed command:

1. `CommandDef.name` stays kebab-case (the internal dispatch name).
2. Register the prefix in `rewrite_prefixed_command()` so `prefix sub` →
   `prefix-sub`. If the prefix also works standalone (`crawl`, `webdb`,
   `doctor`, `webminer`, …), gate it with a `known_subs` allowlist so bare
   usage and positional args pass through untouched.
3. Register the kebab form in `preferred_spaced_command_form()` (and the bare
   prefix in `preferred_prefixed_group_form()` when the bare prefix is
   invalid) so users get a "Use 'browser4-cli prefix sub' instead" hint.
4. Single-word commands without subcommands (`goto`, `close`, `eval`, …) stay
   bare kebab — no prefix, no spaced form.
5. Plugin tool domains are already invoked spaced as `plugin <domain> <method>`
   (dynamic discovery via `/mcp/tools`, no registration needed).
6. Plugins can declare a **named CLI command** without any CLI change: set
   `ToolSpec.cliName` (spaced form, e.g. `"profile import"`) on the tool spec.
   The CLI discovers these from `GET /mcp/tools/specs` at startup and renders
   them as first-class commands with argument parsing (`browser4-cli profile
   import --source chrome`). No `CommandDef` needed — the spec's `arguments`
   define the `--key value` options.

### Adding a `browser4-cli` command

1. **`commands.rs`** — Add `CommandDef`: CLI name kebab-case, MCP tool `browser_`-prefixed snake_case, map args in `tool_params_fn`
2. **`MCPToolController.kt`** — Add frontend alias so `browser_my_tool` resolves to internal name `my_tool`
3. **Backend tool** — Reuse existing when possible; if new capability needed add `@MCP` method in `WebDriver.kt`, implement in concrete driver. Add explicit `BrowserTabToolExecutor` case only for non-trivial parameter mapping
4. **`main.rs`** — Update only for: custom dispatch, dynamic tool-name selection, stale-session recovery, `no_snapshot_commands()`, or custom batch handling in `compile_batch_request()`
5. **Docs** — Update `skills/browser4-cli/SKILL.md`; extensive docs → `skills/browser4-cli/references/<topic>.md`
6. **Tests** — `commands.rs` unit tests → controller mapping tests → `e2e.rs` → `MCPToolControllerE2ETest.kt`
7. **Common failures:** missing backend alias, omitted `sessionId`, forgetting `no_snapshot_commands()`/`batch_supported`, element-ref parameter name mismatches, snake_case/camelCase normalization

### REST-based commands (swarm, crawl)

- `tool_name_fn` returns `""`; dispatch entirely in `main.rs` via custom `handle_*`
- HTTP in `http.rs`: `submit_*` → POST `/api/<resource>`, `get_*_result` → GET `/api/<resource>/{id}/result`
- Backend: `@RestController` + `@Service` + `ConcurrentHashMap` task store + `CoroutineScope`
- Return task UUID from POST; CLI polls for completion. No MCP alias needed.

### Snapshot-related commands

Category `Category::Snapshot`:
- `htmlsnapshot get` / `query` — HTML retrieval with pagination (`-limit`, `-offset`)
- `htmlsnapshot grep` — regex search (`-i`, `-v`, `-F`, `-w`, `-A/B/C`, `--selector`)
- `htmlsnapshot inspect` — CSS selector discovery for recurring patterns
- `htmlsnapshot summary` — compressed Web Page Summary Index
- `snapshot` — live a11y snapshot with `--boxes`, `--stdout`, `--limit`

### Modifying install/uninstall/upgrade code

After changing `cli/browser4-cli/src/daemon.rs`, run install-scenario e2e tests:
```bash
cargo test --test e2e -- --nocapture --level EXTENDED --enable-all --scenario '*install*'
```

## PowerShell Cross-Platform Compatibility

- Use `$IsWindows`, `$IsLinux`, `$IsMacOS` for platform branching
- Avoid Windows-only cmdlets; use `Join-Path` / `[System.IO.Path]::Combine()`
- Prefer `$env:HOME` over `$env:USERPROFILE`
- Shebang: `#!/usr/bin/env pwsh`
- **When fixing one `.ps1`, check siblings for the same issue**

## Test Script Portability

Scripts under `browser4-tests/tests-production/` and `bin/test-production.ps1` test globally-installed `browser4-cli`. They must **never** depend on: git, repo root, source code, Maven/Cargo build outputs. Use `$PSScriptRoot` for sibling references. Repo-awareness must be opt-in with clear error messages when absent.

## Coworker Automation

File-queue system for task-driven AI workflows (`coworker/`). Task files (Markdown with optional `Title:`/`Prompt:` headers) route through state directories: `0draft/` → `1ready/` → `2working/` → `3complete/` (or `3aborted/`, `4review/`, `5approved/`). See [Coworker SKILL.md](coworker/SKILL.md).

## Definition of Done

- [ ] Build and related tests pass
- [ ] No new high-noise logs or warnings
- [ ] New/changed logic has tests (main path + edge case)
- [ ] No secrets or private endpoints committed
- [ ] No arbitrary version changes (follow parent BOM)
- [ ] Documentation updated for public behavior changes
- [ ] Performance impact assessed if >5%

## Common Issues

| Issue | Solution |
|---|---|
| `.ps1` scripts don't run on Linux | `sudo apt-get install -y powershell`, then `pwsh script.ps1` |
| `mvnw` no execute permission | `chmod +x mvnw` |
| JDK version mismatch | JDK 25+ in `JAVA_HOME` |
| Windows parameter escaping | `-D"key.with.dots=value"` |
| Port 8182 in use | Override `server.port` in root `application.properties` |
| JaCoCo reports empty / coverage floor never trips | The surefire `argLine` in the root `pom.xml` must use late binding `@{jacocoArgLine}`, never `${jacocoArgLine}`: the property is declared empty, so `${...}` is substituted to `""` while the effective model is built — before `prepare-agent` sets it — and the agent never attaches (`Skipping JaCoCo execution due to missing execution data file`). Verify with `mvn -X -Pquality-gate -pl :browser4-common test` and look for `-javaagent:` on the surefire fork command line |
| BrowserProtocol retry log storms | Use existing retry utilities, lower log level |

## Documentation Update Rule

When a user says **"update documents"** (or "update docs", "refresh documentation"), update ALL of the following that reference the changed feature:

1. **`README.md`** and **`README.zh.md`** — root-level project readmes
2. **`skills/browser4-cli/SKILL.md`** — primary CLI skill document
3. **`skills/browser4-cli/references/*.md`** — reference docs (htmlsnapshot.md, x-sql.md, etc.)
4. **`cli/browser4-cli/README.md`** — CLI-specific README
5. **`cli/browser4-cli/src/help.rs`** — CLI help text generation
6. **`cli/browser4-cli/src/tips.rs`** — CLI tips/hints shown to users
7. **Any other `.md` files** in `docs/`, `skills/`, or project root that reference the changed command or feature

When adding a new CLI option or changing command behavior, always check these locations.

## Documentation References

- [Testing Taxonomy](docs/TESTING.md)
- [Build from Source](docs/build-from-source.md)
- [Configuration Guide](docs/config.md)
- [CLI Skill Guide](skills/browser4-cli/SKILL.md)
- [ARIA Snapshots](docs/aria-snapshots.md)
- [HTML Snapshot](docs/htmlsnapshot-inspect-summary.md)
- [Eval Command Output](docs/eval-command-output.md)
- [Load Options Guide](docs/load-options-guide.md)
- [Mock Site](docs/mocksite.md)
- [QL Functions Guide](docs/ql-functions-guide.md)
- [Coworker Automation](coworker/SKILL.md)

---

*Last updated: 2026-08-25*