Resolves CI slowdown issues caused by limited availability of Windows containers with 8 cores by switching to the standard Windows runner image.
## Changes Made
- Updated `rust-cross` job to use `windows-latest` instead of `windows-latest-8-cores`
- Updated `windows-integration` job to use `windows-latest` instead of `windows-latest-8-cores`
- Updated `global-install` job matrix to use `windows-latest` instead of `windows-latest-8-cores`
This change trades some performance for better availability and faster CI queue times, as the standard Windows runners have much better availability than the 8-core variant.
Fixes#741
* feat: add idle timeout to daemon to prevent orphaned Chrome processes
The daemon persists indefinitely after browser sessions are used,
leaving orphaned Chromium processes consuming memory and CPU.
Add a configurable idle timeout (default 15 minutes) that shuts down
the daemon when no commands arrive. Resets on every incoming command,
so active sessions are unaffected.
Set AGENT_BROWSER_IDLE_TIMEOUT_MS=0 to disable (preserves old behavior).
Fixes#721
* fix: save session state before shutdown to prevent silent data loss
The shutdown() function (used by idle timeout, SIGINT, SIGTERM, SIGHUP)
previously closed the browser without saving state, unlike the explicit
`close` command which calls saveStateToFile(). This meant idle timeouts
silently destroyed cookies, localStorage, and login sessions.
Now shutdown() mirrors the close command's auto-save behavior: it calls
saveStateToFile() before manager.close(), preserving session state to
disk. This makes idle timeout functionally equivalent to an explicit
close — users returning after an idle shutdown get their state restored.
Addresses review feedback on #722 by @ctate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Max Stoel <maxalerator@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The client (connection.rs) and native daemon (native/daemon.rs) used
different get_port_for_session() implementations on Windows:
- Client: i32, .chars(), djb2 — (hash << 5) - hash + c
- Daemon: i64, .bytes(), Java hashCode — hash * 31 + b
For session name "default", client computes port 50838 while the
daemon binds on 51174, causing a 5-second timeout and startup failure.
Fix: align native/daemon.rs to use the identical djb2 algorithm from
connection.rs (i32, chars, djb2), so both sides agree on the port.
Unix is unaffected (uses Unix domain sockets, no port hashing).
Tests: add port hash regression tests to all three implementations
(native/daemon.rs, connection.rs, daemon.ts) to prevent future drift.
Fixes#705
The handleGetText function now retrieves text using innerText, falling back to textContent if innerText is not available. This change enhances the accuracy of text extraction from elements.
Co-authored-by: Honglei Wu <honglei.wu@shopee.com>
When launched with --extension or --profile, launchPersistentContext()
is used which sets isPersistentContext=true but leaves this.browser as
null. The guard in newTab() checked !this.browser, causing a false
"Browser not launched" error even though the browser was running.
Replace !this.browser with !this.isLaunched(), which already accounts
for both launch paths (browser !== null || isPersistentContext).
Also improve the error message in newWindow() to clarify that it is
not supported in persistent context mode, since it requires a Browser
object to create a new context.
Fixes#411
* fix: isolate getEncryptionKey tests from local filesystem
Tests for getEncryptionKey() failed on machines where
~/.agent-browser/.encryption-key existed, because the file-based
fallback was not mocked out. Mock node:fs to isolate both env var
and key file paths, and add missing tests for the file fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: clean up fs mock naming in encryption tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove BROWSERBASE_PROJECT_ID requirement
The Browserbase API no longer requires a project ID to create sessions —
it is inferred from the API key. Remove the env var requirement from both
the TypeScript daemon and Rust CLI, and update docs accordingly.
* Remove unnecessary Content-Type header since no body is sent
Browserbase has no DELETE endpoint for sessions. The correct API is
POST /v1/sessions/:id with body { status: "REQUEST_RELEASE" }. The old
DELETE call returned an error that was silently swallowed, causing every
session to leak until the 30-min idle timeout.
Fixed in both Node.js (src/browser.ts) and native Rust
(cli/src/native/providers.rs) paths.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The connectViaCDP and connectToBrowserbase methods hardcoded
context.setDefaultTimeout(10000), ignoring the AGENT_BROWSER_DEFAULT_TIMEOUT
env var. This made page.goto time out after 10s on CDP connections even when
the env var was set to a higher value. Now both paths use getDefaultTimeout()
like all other connection modes.
Fixes#703
* fix: sanitize lone Unicode surrogates in snapshot and response serialization (#635)
Pages with emoji/special characters can contain lone surrogates (e.g. \uD800
without a matching \uDC00-\uDFFF), causing serde_json to fail with
"unexpected end of hex escape" when parsing the JSON response.
- Add sanitizeSurrogates() to replace lone surrogates with U+FFFD in
ariaSnapshot output
- Add sanitizeJsonSurrogates() safety net in serializeResponse for other
response fields (page.title, page.content, etc.)
- Add tests for both sanitization paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove trivial "no surrogates unchanged" test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: retry flaky Rust test
* refactor: remove unnecessary sanitizeSurrogates from snapshot.ts
Chromium's ariaSnapshot() converts lone surrogates to literal text
(e.g. the 6-char string "\ud800"), not actual surrogate code points.
The real fix is sanitizeJsonSurrogates() in protocol.ts which handles
eval and other response paths where actual surrogates appear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: use toWellFormed() instead of regex for lone surrogate sanitization
Upgrade tsconfig target/lib from ES2022 to ES2024 and replace the
manual regex-based surrogate sanitization with String.prototype.toWellFormed().
This is simpler, more readable, and relies on the standard API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove trivial no-surrogate test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use 127.0.0.1 instead of localhost when constructing CDP URL from port
number, since Chrome only binds to IPv4. This prevents connection
failures on systems like Ubuntu 24.04 where localhost resolves to ::1.
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Updates the skills documentation to include the missing `vercel-sandbox` skill that was missing from both the available skills list and installation commands.
## Changes
- Added `vercel-sandbox` skill to the Available Skills list with description
- Added installation command for `vercel-sandbox` skill
- Added dedicated section for `vercel-sandbox` with key features and usage details
- Removed duplicate paragraph in the electron section
The `vercel-sandbox` skill enables running agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs with features like snapshot startup, persistent workflows, and automatic OIDC authentication.
Fixes#712
The `set viewport` command is fully implemented but missing from the
agent-facing skill guide. Agents relying on SKILL.md would not know
they could resize the viewport, test responsive layouts, or use retina
scaling.
- Add viewport commands to Essential Commands section
- Add Viewport & Responsive Testing pattern with practical examples
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Make KERNEL_API_KEY optional for external credential injection
When running inside environments with external credential injection
(e.g. Vercel Sandbox credentials brokering), the KERNEL_API_KEY env
var can be omitted. The network layer injects the Authorization header
on outbound requests to api.onkernel.com, so the API key never needs
to exist inside the sandbox.
If KERNEL_API_KEY is set, it's used as before. If not, requests are
sent without an Authorization header, allowing external injection.
Without either, the Kernel API returns 401.
Made-with: Cursor
* Make KERNEL_API_KEY optional in native Rust daemon too
Applies the same change to the native Rust connect_kernel() function
so both the Node.js and native code paths support external credential
injection.
Made-with: Cursor
* Address review feedback: fix type errors, cargo fmt, always send cleanup DELETE
- Fix kernelApiKey assignment: use ?? null for undefined -> null
- Fix closeKernelSession signature: accept string | undefined
- Always send DELETE on cleanup even without local API key (external
injection covers it)
- Run cargo fmt on Rust code
Made-with: Cursor
* next.js guide
* better
* shadcn
* fixes
* fix: correct screenshot test assertion to check path instead of base64
The daemon returns { path: savePath } for screenshot commands, not base64.
* fix: cross-platform Chrome detection and gitignore hardening
- Replace hardcoded macOS Chrome path with findLocalChrome() that
searches common paths on macOS, Linux, and WSL, with a clear error
message when no Chrome is found.
- Add .env and .env*.local to .gitignore to prevent accidental
secret commits.
* fix: correct Vercel deploy button repo URL to vercel-labs/agent-browser
* clean up
* demo
* next page
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Add `cargo clippy -- -D warnings` step to the Rust CI job so that
clippy warnings fail the build. Also fix the one new lint
(`unnecessary_map_or`) introduced in the current stable clippy.
Fixes#672
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix clippy warnings across CLI codebase
Fixes#653
* Fix remaining items_after_test_module clippy warnings
Move functions defined after `mod tests` blocks to before the test
modules in recording.rs and webdriver/client.rs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Chrome extensions not loading by forcing headed mode when extensions present
Fixes#640
* Restore wait_or_kill() and add tests for headless+extensions logic
Restore the ChromeProcess::wait_or_kill() method that was accidentally
removed. It is still referenced by BrowserProcess in browser.rs and is
needed for graceful shutdown / cookie persistence (PR #650).
Add unit tests verifying --headless=new is omitted when extensions are
present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix window-size leak in headed+extensions mode and remove unused channel option
- Skip --window-size=1280,720 when extensions force headed mode (native)
- Remove unexplained channel: 'chromium' from extensions launch path (TS)
- Add window-size assertion to existing extension test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#566: Clarify snapshot vs snapshot -i usage
- Add guidance that snapshot -i is for clickable/fillable elements
- Add guidance that snapshot (no flag) is for reading page content
Fixes#565: Add reproducibility verification before collecting evidence
- Add guidance to verify issues are reproducible before recording video
- Prevent wasting turns on false positives
TypeScript CI has prettier --check but Rust CI only runs cargo test.
Add cargo fmt --check to catch formatting issues early, and fix the
8 pre-existing formatting violations on main.
reqwest_get_string() was hand-rolling HTTP/1.1 over raw TCP despite reqwest
being an existing dependency. The hand-rolled implementation had two bugs:
1. URL path parsing: url.find('/') matched the first '/' in 'http://',
producing path '//127.0.0.1:9222/json/version' instead of '/json/version'
2. read_to_end() hangs: Chrome's DevTools HTTP server ignores Connection: close
and keeps the socket open, so read_to_end() waits for EOF that never comes
This caused 'agent-browser --cdp <port>' to always timeout when AGENT_BROWSER_NATIVE=1.
Fix: replace 49 lines of broken TCP code with reqwest::get(), which was
already in Cargo.toml.
When the daemon process crashes during startup (e.g., missing
Playwright), stderr was discarded via Stdio::null(), so users only
saw "Daemon failed to start (port: ...)" with no diagnostic info.
Now captures daemon stderr via Stdio::piped() and detects early process
exit with try_wait() during the startup polling loop. If the daemon
crashes, the actual error from stderr is shown to the user.
Also forwards --debug flag to the daemon process as AGENT_BROWSER_DEBUG
so debug logging works end-to-end.
Closes#56
* fix: suppress spurious --native warning when set via env var
When AGENT_BROWSER_NATIVE=1 is set via environment variable, every
command after the first would warn:
⚠ --native ignored: daemon already running.
This is a false positive — the daemon was already spawned in native
mode and inherited the env var. The warning should only fire when
--native is explicitly passed on the CLI to an already-running daemon.
Add cli_native flag (consistent with existing cli_* pattern) to
distinguish CLI origin from env var origin.
* fix: add flag to test cfg
* fix: cli_native should track flag presence, not value
--native false on CLI should still warn when daemon is already
running, since the user is explicitly trying to change the mode.