AGENTS.md · git:20260924.106c80b · 2026-09-24 · sha256 a563a5e69d1416a7
AGENTS.md git:20260924.106c80bA
Immutable. This exact content never changes and is served at /api/v1/blob/a563a5e69d1416a7.
# 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 |
## 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 BASIC|EXTENDED|ALL # Test depth
--fail-fast / --failed # Stop early / rerun failures
--list / --list-groups # Discover without running
--enable-install-scenario # Opt into install tests
--enable-batch-scenario # Opt into batch 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/...` |
| Integration | `browser4-tests/pulsar-it-tests/` |
| E2E | `browser4-tests/pulsar-e2e-tests/` |
| REST integration/E2E | `browser4-tests/browser4-rest-tests/` |
| Shared utilities | `browser4-tests/pulsar-tests-common/` |
| Rust E2E | `cli/browser4-cli/tests/e2e/` |
## 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 surefire XML totals — 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`.
See [CI stabilization notes](docs-dev/copilot/ci-stabilization-4.13.x.md) before changing either list.
## Configuration
- Default port: **18182**
- 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
### 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 ALL --enable-install-scenario --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 17+ in `JAVA_HOME` |
| Windows parameter escaping | `-D"key.with.dots=value"` |
| Port 8182 in use | Override `server.port` in root `application.properties` |
| 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)
- [Crawl Checkpoint & Resume](docs/crawl-checkpoint-resume.md)
- [Mock Site](docs/mocksite.md)
- [QL Functions Guide](docs/ql-functions-guide.md)
- [Coworker Automation](coworker/SKILL.md)
---
*Last updated: 2026-08-25*