feat(browser): add browser protocol with full Playwright MCP parity#59
feat(browser): add browser protocol with full Playwright MCP parity#59
Conversation
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
a305920 to
a552d29
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…th all ~55 step types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… advisory locking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements launcher.rs with chrome_binary_candidates (env var override, platform paths, which fallbacks), find_chrome, launch_chrome, connect_chrome, and configure_page (deny downloads by default). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…apshot, screenshot
Implements steps.rs with validate_url_scheme (http/https only), execute_steps
loop with per-step timeout, optional-step skip, and on_failure_screenshot
diagnostic capture. Navigation steps use chromiumoxide Page::goto/reload and
CDP Page.getNavigationHistory + navigateToHistoryEntry for back/forward.
Snapshot uses CDP Accessibility.getFullAXTree and converts to our AXNode tree
via render_ax_tree. Screenshot uses Page::save_screenshot with base64 encoding.
All unimplemented step variants stub with tracing::warn and Ok({"ok":true}).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds implementations for all previously-stubbed BrowserStep variants in execute_step, replacing the catch-all warn arm with exhaustive match arms. Also adds 28 new unit tests covering deserialization and helper APIs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `build_browser_request` in builder.rs that renders all Jinja template strings (session_id and every string field in each BrowserStep) using the earl_core TemplateRenderer trait, and returns a PreparedBrowserCommand. Add `BrowserExecutor` in executor.rs implementing ProtocolExecutor with ephemeral (no session_id) and session modes; ephemeral launches/closes Chrome around the step run while session mode acquires an advisory lock, reconnects or launches fresh Chrome as needed, and persists the session file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cleanup, and lock file - render_strings_in_value now skips the "action" key so the serde tag discriminant for BrowserStep is never mutated by the renderer - is_pid_alive returns true for pid=0 (unknown PID placeholder) so the CDP probe becomes the authoritative liveness check instead of always launching a new Chrome - run_ephemeral closes the browser before propagating errors from new_page or configure_page to avoid Chrome process leaks - run_with_session saves the session file after execute_steps completes, setting interrupted=true on failure so stale sessions are detectable - acquire_session_lock opens the lock file with truncate(true) to avoid stale content from previous lock holders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xecutor Add Browser variant to OperationTemplate/OperationProtocol, PreparedProtocolData, and dispatch arms in the builder and executor — all gated behind #[cfg(feature = "browser")]. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… check, optional steps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract duplicated test helpers from integration.rs into a shared common module, add a minimal async HTTP/1.1 TestServer backed by tokio::net::TcpListener, and add net/macros tokio dev-features. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Group 2 (screenshot capture) and Group 8 (PDF save) browser integration tests covering PNG magic-byte validation, full-page vs viewport size comparison, explicit path writes, and temp-file creation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement Group 1 (web scraping) and Group 10 (JavaScript execution) integration tests using a controlled local HTTP test server. All 8 tests pass with Chrome present; they skip gracefully when Chrome is absent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Group 7 (dialogs.rs) covering alert/prompt/confirm handling via HandleDialog, and Group 9 (assertions.rs) defining acceptance criteria for the VerifyTextVisible and VerifyElementVisible stub steps. All dialog tests are marked #[ignore] due to headless-Chrome timing sensitivity; all assertion tests are marked #[ignore] pending stub implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Group 3 (form automation) and Group 11 (keyboard/mouse)
integration tests with 6 form tests and 2 keyboard/mouse tests.
Also fixes a bug in step_fill where press_key("Return") was used
instead of the correct chromiumoxide key name press_key("Enter").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Group 5 (cookie management) and Group 6 (localStorage) integration tests. Also fixes step_cookie_delete to supply the current page URL to the CDP DeleteCookies command, which requires at least one of url or domain per the Chrome DevTools Protocol spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- step_handle_dialog: subscribe to Page.javascriptDialogOpening before trying to dismiss, so the step works whether the dialog is already open or fires shortly after (handles async onload dialogs) - tests/dialogs.rs: trigger dialogs via setTimeout in window.onload so Navigate returns before the dialog fires; add WaitFor after HandleDialog where needed to let JS callbacks update the DOM - tests/assertions.rs: remove #[ignore] — VerifyTextVisible and VerifyElementVisible are already implemented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix 7 clippy errors in steps.rs and launcher.rs: - Collapse nested ifs with let-chains (collapsible_if) - Convert single-arm matches to if let / is_ok() (single_match) - Remove useless format!() wrapper (useless_format) - Replace map_or(true, ...) with is_none_or (unnecessary_map_or) - Allow too_many_arguments on step_cookie_set - Fix await_holding_lock: change CHROME_SERIAL from std::sync::Mutex to tokio::sync::Mutex::const_new(()), update all 12 test files to use .lock().await instead of .lock().unwrap() - Run cargo fmt to fix all rustfmt failures - Add earl-protocol-browser to release.yml publish order (before earl) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Format src/protocol/executor.rs (cargo fmt --all missed the workspace root in the previous commit) - Change CI publish dry-run to run per crate in dependency order, matching release.yml; skip crates that already exist on crates.io or have unpublished workspace dependencies (new crates before first release) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical:
- Replace CHROME_SERIAL static mutex with chrome_lock() that uses both
a process-level OwnedMutexGuard and an fs4 file-based advisory lock,
serializing Chrome tests within and across test binaries
- Add #[ignore = "requires external network"] to navigate_and_snapshot
and optional_step_continues_on_failure in integration.rs
Important:
- Split session_persists_navigation_across_calls into two focused tests;
remove internal SessionFile assertions (test behaviour, not structure)
- Remove dead intermediate Evaluate step in checked_box_can_be_unchecked
- Remove duplicate disallowed_scheme_fails_at_scheme_validation test
- Add missing unhappy paths: VerifyElementVisible absent, HandleDialog
timeout, JS exceptions from Evaluate/RunCode
- Add missing step coverage: FillForm, CookieGet, VerifyListVisible,
VerifyValue, SessionStorageGet/Set/Delete/Clear
- Add navigate.expected_status match + mismatch tests
- Note: base64 stays in [dependencies] — used by production steps.rs
Suggestions:
- Replace timestamp_ms() with atomic counter + PID unique_id() in
session_mode, cookies, pdf, screenshot
- Replace WaitFor{time} hard sleep with WaitFor{text} condition polling
in forms (fill_with_submit) and remove sleep before Evaluate in
keyboard_mouse (MouseWheel is synchronous)
- Add WaitFor text_gone test in scraping.rs
Also: apply cargo fmt --all to workspace files that needed formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8066d8a to
3ebc567
Compare
…rence - templates.mdx: rename "five protocols" → "six protocols", add Browser section with an example login flow, session persistence explanation, and a link to the step reference - template-schema.mdx: add full Browser section including the browser block field table and a complete step reference organized by category (navigation, observation, interaction, mouse, wait/assertions, JavaScript, cookies, storage, tabs/viewport, network, recording, utility) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
{#browser} on the heading was parsed as a JSX expression by acorn,
breaking the build. Remove it and update the link in templates.mdx to
use the auto-generated anchor #step-reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude Review this |
|
Claude finished @jrandolf's task in 3m 6s —— View job Code Review —
|
| Step | Operation | Location |
|---|---|---|
screenshot |
write | steps.rs:537 |
storage_state |
write | steps.rs:1436 |
set_storage_state |
read | steps.rs:1446 |
pdf_save |
write | steps.rs:1570 |
These paths flow from template fields that users control. On a shared system, a user could read ~/.ssh/id_rsa via set_storage_state or overwrite ~/.bashrc via storage_state. These are likely the bulk of the 20+ CodeQL findings. Fix this →
🔴 CodeQL flagged — Cleartext sensitive data logging [builder.rs:24]
session_id is a Jinja2 template string rendered against the full execution context (which includes secrets). A user could write session_id: "{{ secrets.db_password }}", causing the rendered value to contain a secret. This rendered value flows into filenames and error messages such as:
BrowserError::SessionLocked { session_id: session_id.to_string(), pid }which is surfaced in user-facing error output. Consider not allowing secrets to render into session_id, or at minimum masking it in error messages. Fix this →
Functional Correctness
🟡 configure_page not called on reused session pages [executor.rs:121-128]
In session mode, when an existing page is reused from browser.pages(), configure_page (which sets SetDownloadBehaviorBehavior::Deny) is not called on it:
let page = match browser.pages().await {
Ok(pages) if !pages.is_empty() => pages.into_iter().next().unwrap(), // ← no configure_page
_ => {
let p = browser.new_page("about:blank").await?;
configure_page(&p).await?; // ← only new pages get configured
p
}
};Downloads would not be blocked on reused session pages. Fix this →
🟡 expected_status in Navigate is silently ignored [steps.rs:344-356]
The schema advertises an expected_status field but the implementation discards it:
async fn step_navigate(ctx, url, _expected_status: Option<u16>) -> Result<Value> {Users who set expected_status: 200 expecting a hard failure on HTTP errors will get a silent no-op. Either implement it or reject the field with an error. Fix this →
🟡 ref field in VerifyListVisible and VerifyValue is silently dropped [steps.rs:228-232]
BrowserStep::VerifyListVisible { r#ref: _, items, .. } => // ref discarded
BrowserStep::VerifyValue { r#ref: _, value, .. } // ref discardedThe schema and docs imply ref-based targeting works here, but it doesn't. This should either be documented as "not yet implemented" in the schema or raise an error when a ref is supplied. Fix this →
🟡 Double-click is two sequential single-clicks [steps.rs:614-622]
el.click().await?; // first click
if double_click {
el.click().await?; // second click — not a real double-click
}A native double-click fires mousedown → mouseup → click → mousedown → mouseup → click → dblclick. Just calling .click() twice misses the dblclick DOM event, which many frameworks listen to. Consider using CDP DispatchMouseEventParams with click_count: 2 or dispatching a dblclick custom event. Fix this →
🟡 Silent success stubs are misleading [steps.rs:311-313, steps.rs:319-321, steps.rs:325-336]
13 of 62 actions return {"ok": true, "note": "... requires session mode"} silently:
FileUpload,Download— return success even though nothing happenedConsoleMessages,NetworkRequests,NetworkClear,Route,RouteList,Unroute,StartVideo,StopVideo,StartTracing,StopTracing
The note says "requires session mode" but the executor.rs session path runs the same execute_steps code — these features are simply unimplemented in both modes. This could silently swallow errors in workflows that depend on them. Consider returning Err(BrowserError::NotImplemented { action }) instead of silent success.
🟡 Possible stack overflow with circular/deep AX trees [steps.rs:465-508]
build_tree is a recursive function with no depth limit. Chrome's CDP accessibility tree can be very deep for some SPAs. A circular reference in the CDP response (a bug in the browser or a crafted page) would cause unbounded recursion. Add a depth limit parameter mirroring the max_nodes logic. Fix this →
Design / API Gaps
🔵 slowly, button, and modifiers fields accepted but never used
Fill.slowly: bool— accepted by schema, ignored instep_fill(slowly: _)Click.button: Option<String>— accepted by schema, not passed tostep_click(button: _)Click.modifiers: Vec<Value>— accepted by schema, not used
These create a false impression to users (and AI agents using these as tools) that modifier-key clicks and slow typing work. Remove them from the schema if unimplemented, or implement them.
🔵 WaitFor.timeout_ms is required while all other timeout fields are optional
All other steps have #[serde(default)] timeout_ms: Option<u64>, but WaitFor requires timeout_ms: u64. This inconsistency will break user templates that use WaitFor without specifying a timeout.
🔵 FileUpload stub note is misleading [steps.rs:311-313]
BrowserStep::FileUpload { .. } => Ok(json!({"ok": true, "note": "file_upload dispatches to active file chooser; trigger the chooser first"})),This implies the step has partial functionality but it does nothing. The note should be "not yet implemented" or the feature should be added.
Nits
session.rs:50—ensure_sessions_dirsets directory permissions after creation, butcreate_dir_allmay leave intermediate directories with default permissions on non-Unix systems. Worth a comment explaining the limitation.steps.rs:523—max_nodes = 5000is hardcoded. Exposing this as aSnapshotfield (with a default) would give users control for very large/complex pages.executor.rs:160—let _ = updated_sf.save_to(&sf_path);silently swallows session-file persistence errors. Atracing::warn!here would help debug "my session didn't persist" issues.schema.rs—is_optionalandtimeout_msmethods onBrowserStepare ~140-line match blocks that will need updating every time a new step is added. A#[derive]macro or a trait on each variant would be more maintainable at scale.
Summary
The architecture, security boundary (SSRF protection, 0700 sessions dir, atomic session writes, advisory locking), and test structure are all solid. The main blockers before merging are:
- Path traversal via
session_id— high severity, easily exploitable - Arbitrary file read/write via step
pathfields — high severity, causes the bulk of CodeQL alerts configure_pagenot applied to reused session pages — downloads not blocked on reused pages
The stub actions and silently-ignored schema fields are medium-priority polish issues that could cause silent failures in user workflows.
Security: - validate_session_id() rejects path traversal in session IDs - validate_file_path() rejects `..` components in screenshot/pdf/storage paths - configure_page() now applied to reused session pages (download blocking) Correctness: - Implement expected_status in Navigate using Performance Navigation Timing API - WaitFor.timeout_ms is now optional (defaults to command-level timeout_ms) - VerifyListVisible and VerifyValue return error when ref field is provided (ref-based targeting not yet implemented) - Double-click now dispatches dblclick DOM event after two sequential clicks - Add tracing::warn on session file persistence failure Tests: - Add validate_session_id and validate_file_path unit tests - Update WaitFor struct literals to use Option<u64> for timeout_ms - Remove ref from VerifyListVisible/VerifyValue test steps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude Review this |
|
Claude finished @jrandolf's task in 2m 54s —— View job Code Review —
|
| Finding | Status |
|---|---|
Path traversal via session_id |
✅ Fixed — validate_session_id added [session.rs:12-26] |
Arbitrary file write/read via .. components |
✅ Fixed — validate_file_path added [steps.rs:27-35] |
configure_page not called on reused session pages |
✅ Fixed — both branches now call it [executor.rs:123-134] |
expected_status silently ignored |
✅ Fixed — implemented via Performance Navigation Timing API [steps.rs:382-403] |
ref in VerifyListVisible / VerifyValue silently dropped |
✅ Fixed — returns error when ref is supplied [steps.rs:241-258] |
Double-click missing dblclick DOM event |
✅ Fixed — dispatches dblclick via call_js_fn [steps.rs:676-681] |
| Session file save silently swallowed | ✅ Fixed — tracing::warn! added [executor.rs:167-169] |
WaitFor.timeout_ms required while others optional |
✅ Fixed — now Option<u64> with #[serde(default)] [schema.rs:266-267] |
Remaining Issues
🔴 validate_file_path blocks .. but not absolute paths to sensitive locations [steps.rs:27-35]
The new validation only checks for .. components via std::path::Component::ParentDir. It does not block absolute paths to sensitive locations:
fn validate_file_path(path: &str) -> Result<()> {
let p = std::path::Path::new(path);
if p.components().any(|c| c == std::path::Component::ParentDir) {
return Err(...) // blocks ../secret but not /etc/passwd
}
Ok(())
}A user template with path: "/etc/cron.d/evil" or path: "/home/user/.bashrc" passes this check today. Since path fields in screenshot, storage_state, set_storage_state, and pdf_save are rendered from user-controlled templates, this is still exploitable on a shared system. The test at steps.rs:1703 itself demonstrates the gap: validate_file_path("/tmp/../../etc/passwd") fails (uses ..), but validate_file_path("/etc/passwd") would pass.
Consider either: (a) rejecting absolute paths entirely and only allowing relative paths, or (b) requiring paths to be under a configurable output directory (e.g. current working directory). Fix this →
🟡 build_tree recursion has no depth guard [steps.rs:515-558]
The recursive build_tree function has no depth limit. While Chrome's CDP AX tree shouldn't contain circular references in normal operation, a crafted page with a very deeply nested DOM (common in some SPA frameworks) could cause a stack overflow. The existing max_nodes = 5000 limit at line 573 applies to render_ax_tree output truncation, not to the build_tree recursion.
A depth guard of ~50-100 levels would prevent stack exhaustion without affecting practical use. Fix this →
🔵 Click.button, Click.modifiers, Fill.slowly accepted but silently ignored
These three schema fields are still accepted and silently discarded:
Click { button: _, modifiers: _, .. }→step_clickonly usesdouble_clickFill { slowly: _, .. }→step_fillignores slow typing
A user setting button: "right" for a context menu, or modifiers: ["Shift"] for multi-select, will get silent wrong behavior. Either implement these or remove them from the schema. The Click.button case is particularly confusing since MouseClick does support button. Fix this →
🔵 Stub actions note "requires session mode" but session mode doesn't implement them either
ConsoleMessages, NetworkRequests, Route, StartVideo, StartTracing, etc. return {"ok": true, "note": "... requires session mode"}. But execute_steps runs the same code path in both ephemeral and session modes — there is no session-mode implementation. The note implies these features exist in session mode, which will mislead users. Consider s/requires session mode/not yet implemented/ to be accurate. Fix this →
🔵 Screenshots and PDFs written to world-readable /tmp when no path is given
When no path is specified, step_screenshot and step_pdf_save write to std::env::temp_dir(), which on Linux has mode 1777 (world-readable). A PDF containing sensitive page content or a screenshot of a login page saved there is readable by any local user. At minimum, these files should be chmod 0600 after creation, or the temp path should use tempfile::NamedTempFile with proper permissions.
Nit
step_screenshot:606—tokio::fs::readis called immediately afterpage.save_screenshotwrites the same file.chromiumoxidemay expose ascreenshot()method returning bytes directly, avoiding the round-trip write-then-read.session.rs:68-77—ensure_sessions_dircallscreate_dir_allthenset_permissions. On Linux,create_dir_allcreates intermediate dirs with the process umask, and only the final dir gets0700. If~/.config/earl/doesn't exist yet, it will be created with default permissions. Worth a comment noting this limitation.
Summary
The PR is in significantly better shape after 26adf30. The prior critical blockers (session_id path traversal, .. traversal in file paths, configure_page gap) are all resolved. The one remaining security issue worth fixing before merge is the absolute path gap in validate_file_path — it's a straightforward change and closes the file write/read vulnerability the CodeQL scan identified.
Security: - validate_file_path now rejects absolute paths (not just .. traversal) - Only relative paths permitted for user-controlled file operations Correctness: - build_tree recursion has depth guard (max 80) to prevent stack overflow on pathologically deep accessibility trees - Stub actions now note "not yet implemented" instead of "requires session mode" (session mode runs the same code path, no hidden implementation exists) Schema cleanup: - Remove `button` and `modifiers` from Click (use `mouse_click` for button control) - Remove `slowly` from Fill (not yet implemented) - Update docs to reflect removed fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude Review this |
|
Claude finished @jrandolf's task in 2m 45s —— View job Code Review —
|
| Finding | Status |
|---|---|
validate_file_path not blocking absolute paths |
✅ Fixed — p.is_absolute() check added [steps.rs:33-37] with test at [steps.rs:1717-1721] |
build_tree recursion has no depth guard |
✅ Fixed — MAX_TREE_DEPTH = 80 const added [steps.rs:520]; depth passed through all recursive calls |
Click.button, Click.modifiers silently ignored |
✅ Fixed — both fields removed from the Click schema variant [schema.rs:113-124] |
Fill.slowly silently ignored |
✅ Fixed — slowly field removed from Fill [schema.rs:149-161] |
| Stub actions note "requires session mode" | ✅ Fixed — all stubs now say "not yet implemented" [steps.rs:279-293, steps.rs:342-367] |
Remaining Issues
🟡 Temp files for screenshots/PDFs are world-readable and not cleaned up [steps.rs:600-622, steps.rs:1639-1650]
When no path is specified, step_screenshot and step_pdf_save fall back to std::env::temp_dir() (typically /tmp on Linux, mode 1777). The file is written, read back for base64 encoding, and then left on disk:
// step_screenshot (no path case)
let out_path = std::env::temp_dir().join(format!("earl-screenshot-{}.png", ...));
ctx.page.save_screenshot(..., &out_path).await?;
let bytes = tokio::fs::read(&out_path).await?; // read back
// out_path is never deletedTwo separate problems:
- A PNG of a login page or PDF of sensitive content is left world-readable in
/tmpindefinitely. - The round-trip through disk is unnecessary when the caller only needs the base64 bytes —
chromiumoxidereturns screenshot bytes directly viapage.screenshot()(without a save path), avoiding the write entirely.
Consider using page.screenshot() directly to get bytes (no disk write), or at minimum deleting the temp file after reading it. Fix this →
🟡 step_screenshot always returns base64 data even when the caller provides a path [steps.rs:619-622]
Ok(json!({
"path": out_path.to_string_lossy(),
"data": data, // ← full PNG contents returned regardless of whether path was given
}))When a user specifies path: "output/screenshot.png", they typically want the file saved to disk — not the entire PNG serialized into the response body. Returning a multi-MB base64 blob in every response that specified a path will inflate response sizes and may inadvertently log sensitive screenshots. Consider returning data only when no path was given.
🔵 ConsoleClear and NetworkClear silently succeed as stubs without a note [steps.rs:282, steps.rs:286]
BrowserStep::ConsoleClear { .. } => Ok(json!({"ok": true})),
BrowserStep::NetworkClear { .. } => Ok(json!({"ok": true})),Unlike Route, StartVideo, StartTracing, etc., these two stubs return bare {"ok": true} with no "note" field. A user relying on network_clear before a test assertion will get silent incorrect behavior. Add a note consistent with the other stubs.
🔵 Diagnostic failure screenshot is also written to world-readable temp with no cleanup [steps.rs:123-137]
attempt_failure_screenshot writes to /tmp/earl-browser-failure-{ts}.png. This file is left on disk, world-readable. For a diagnostic artifact this is lower priority, but worth noting.
Nits
step_screenshot:616—tokio::fs::readimmediately afterpage.save_screenshotreads the same file that was just written. Usingpage.screenshot()instead eliminates this (see 🟡 above).session.rs:68-77(still unaddressed from first review) —ensure_sessions_dirusescreate_dir_allthenset_permissions. If~/.config/earl/doesn't exist, it will be created with the process umask before the0700is applied to the final dir. A comment explaining this limitation would be useful.schema.rs—is_optional()andtimeout_ms()are ~140-line match blocks. Already flagged in review 1 — mentioning again as the PR is otherwise review-ready.
Summary
The PR is ready to merge contingent on the two 🟡 items. The security blockers from reviews 1 and 2 are all resolved. The remaining work is:
- Temp file cleanup + permissions for screenshots/PDFs (no world-readable leftover files)
- Don't return base64
datawhen apathwas given (avoid logging sensitive screenshots)
The ConsoleClear/NetworkClear note inconsistency is cosmetic and can be addressed in a follow-up.
…b notes
- step_screenshot: use page.screenshot() (bytes directly) instead of
save_screenshot+read round-trip; return {"path":p} when path given,
{"data":base64} when not — no world-readable temp file left behind
- step_pdf_save: same pattern; return {"path":p,"size":N} or
{"data":base64,"size":N}
- attempt_failure_screenshot: use page.screenshot() instead of
save_screenshot so temp file is only written when bytes arrive
- ConsoleClear/NetworkClear: add "not yet implemented" note to response
- tests: use relative paths (temp_dir absolute paths rejected by
validate_file_path); fix screenshot/pdf assertions to match new
response shapes; rename pdf_save_no_path test to reflect new behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude Review this |
|
Claude finished @jrandolf's task in 3m 35s —— View job Code Review —
|
| Finding | Status |
|---|---|
step_screenshot temp file leak (world-readable /tmp, no cleanup) |
✅ Fixed — now uses page.screenshot() to get bytes directly; no disk write when no path given [steps.rs:602-624] |
step_screenshot returning base64 data even when path given |
✅ Fixed — returns {"path": p} only when path given; returns {"data": ...} only when no path [steps.rs:614-624] |
step_pdf_save temp file leak (world-readable /tmp, no cleanup) |
✅ Fixed — uses PrintToPdfParams result directly; returns base64 when no path, writes to user path when given [steps.rs:1625-1654] |
ConsoleClear / NetworkClear stubs had no "note" field |
✅ Fixed — both now return {"ok": true, "note": "...: not yet implemented"} [steps.rs:279-287] |
New tests cover all four cases: screenshot-to-data, screenshot-to-path (verifies path field and file on disk, no data field), pdf-to-data, pdf-to-path.
Remaining Item
Nit — attempt_failure_screenshot still writes to world-readable /tmp without cleanup [steps.rs:122-135]
The diagnostic failure screenshot was partially refactored in this commit — it now calls page.screenshot() to get bytes (good), but still writes to std::env::temp_dir() and never deletes the file:
let path = std::env::temp_dir().join(format!(
"earl-browser-failure-{}.png",
chrono::Utc::now().timestamp_millis()
));
if let Ok(()) = std::fs::write(&path, &bytes) {
eprintln!("diagnostic screenshot saved: {}", path.display());
// ← file never deleted
}On a multi-user Linux system (/tmp is mode 1777), this file is readable by all local users until the OS clears it. For a diagnostic tool this is lower-risk than the user-facing screenshot step (a user would need to be monitoring /tmp during a failure), but worth a follow-up: either fs::set_permissions(&path, Permissions::from_mode(0o600)) immediately after the write, or deleting the file after the eprintln!. Fix this →
Summary
All critical and functional blockers from reviews 1–3 are resolved. The architecture and test coverage are solid — 3 screenshot tests, 2 PDF tests, dedicated test files for cookies, forms, storage, dialogs, JavaScript, session mode, scraping, and keyboard/mouse. The sole open item above is a low-severity nit that can be addressed in a follow-up.
This PR is ready to merge.
Set unix file permissions to owner-read/write-only immediately after writing the diagnostic failure screenshot to /tmp, preventing other local users from reading it on multi-user systems. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eartext-logging CodeQL flags session_id as a sensitive identifier and traces it from PreparedBrowserCommand through validate_session_id calls to the tracing::warn! sink in run_with_session. The session file path already conveys the relevant context (it encodes the session ID in the filename), so the explicit session_id field is redundant. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eartext-logging
The session file path is derived from format!("{session_id}.json"), so
CodeQL's taint tracking correctly identifies that session_id flows into
the tracing::warn! log via the path field (CWE-532). Remove path from
the log — the IO error message provides sufficient context without
exposing the session identifier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… CodeQL The session_id flowed from BrowserError::SessionLocked's Display impl (generated by #[error(...)]) into main.rs eprintln!, triggering CodeQL's rust/cleartext-logging (CWE-532) for every test that uses PreparedBrowserCommand. Remove session_id from the format string — callers already know which session they requested. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- <details><summary>0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/v0.5.0...v0.5.1) (2026-02-26) ### Features * add earl runtime skill and reorganize skills by category ([#66](https://github.com/brwse/earl/issues/66)) ([9bec065](https://github.com/brwse/earl/commit/9bec06510536ad6d3bb11d5119f19cdd3fe939aa)) * **browser:** add browser protocol with full Playwright MCP parity ([#59](https://github.com/brwse/earl/issues/59)) ([65f01da](https://github.com/brwse/earl/commit/65f01daf7a3c2ad63a737cd075d06fa06f975bd7)) * **browser:** default to 'default' session when session_id is omitted ([#63](https://github.com/brwse/earl/issues/63)) ([99c3a66](https://github.com/brwse/earl/commit/99c3a66e2be92c951b48f3c948961b168435b46e)) ### Bug Fixes * add earl-protocol-browser to release-please config and manifest ([#67](https://github.com/brwse/earl/issues/67)) ([6f18bc2](https://github.com/brwse/earl/commit/6f18bc24d1563225559a9e82a79b6f30e8528335)) ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 * earl-protocol-grpc bumped from 0.5.0 to 0.5.1 * earl-protocol-http bumped from 0.5.0 to 0.5.1 * earl-protocol-bash bumped from 0.5.0 to 0.5.1 * earl-protocol-sql bumped from 0.5.0 to 0.5.1 * earl-protocol-browser bumped from 0.5.0 to 0.5.1 </details> <details><summary>earl-core: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-core-v0.5.0...earl-core-v0.5.1) (2026-02-26) ### Miscellaneous Chores * **earl-core:** Synchronize earl versions </details> <details><summary>earl-protocol-bash: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-protocol-bash-v0.5.0...earl-protocol-bash-v0.5.1) (2026-02-26) ### Miscellaneous Chores * **earl-protocol-bash:** Synchronize earl versions ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 </details> <details><summary>earl-protocol-browser: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-protocol-browser-v0.5.0...earl-protocol-browser-v0.5.1) (2026-02-26) ### Features * **browser:** add browser protocol with full Playwright MCP parity ([#59](https://github.com/brwse/earl/issues/59)) ([65f01da](https://github.com/brwse/earl/commit/65f01daf7a3c2ad63a737cd075d06fa06f975bd7)) * **browser:** default to 'default' session when session_id is omitted ([#63](https://github.com/brwse/earl/issues/63)) ([99c3a66](https://github.com/brwse/earl/commit/99c3a66e2be92c951b48f3c948961b168435b46e)) ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 </details> <details><summary>earl-protocol-grpc: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-protocol-grpc-v0.5.0...earl-protocol-grpc-v0.5.1) (2026-02-26) ### Miscellaneous Chores * **earl-protocol-grpc:** Synchronize earl versions ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 </details> <details><summary>earl-protocol-http: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-protocol-http-v0.5.0...earl-protocol-http-v0.5.1) (2026-02-26) ### Miscellaneous Chores * **earl-protocol-http:** Synchronize earl versions ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 </details> <details><summary>earl-protocol-sql: 0.5.1</summary> ## [0.5.1](https://github.com/brwse/earl/compare/earl-protocol-sql-v0.5.0...earl-protocol-sql-v0.5.1) (2026-02-26) ### Miscellaneous Chores * **earl-protocol-sql:** Synchronize earl versions ### Dependencies * The following workspace dependencies were updated * dependencies * earl-core bumped from 0.5.0 to 0.5.1 </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Adds a new
browserprotocol to Earl that controls Chrome via CDP (chromiumoxide), exposing the same ~55 actions as the Playwright MCP server. Supports both scripted workflows (declarativestepsarray) and AI-driven interactive navigation (persistent sessions viasession_id).earl-protocol-browsercrate behind thebrowserfeature flag (enabled by default)~/.config/earl/browser-sessions/<id>.jsonwith advisory flock, atomic writes, and 0700 directory permissionshttp://andhttps://URLs allowed in navigate stepse{id}refs for AI-driven interactionNew files
crates/earl-protocol-browser/— new crate (error, schema, session, launcher, accessibility, steps, builder, executor)crates/earl-protocol-browser/tests/integration.rs— integration tests (Chrome-free + Chrome-dependent)examples/browser/screenshot.hcl— take a full-page screenshotexamples/browser/scrape.hcl— extract JS-rendered page textexamples/browser/login.hcl— scripted form login with session persistenceexamples/browser/ai_navigate.hcl— single-step commands for AI-driven browsing (snapshot, click, navigate)Test plan
cargo test --workspace— all tests pass (0 failures)cargo build --features browser— clean buildcargo clippy --features browser— zero errors or warnings🤖 Generated with Claude Code