From 313d05177c88f05e7a3ab841ad99418af8cb572e Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 16 May 2026 04:40:40 +0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(cdp)!:=20bump=20chromiumoxide=200.6?= =?UTF-8?q?=E2=86=920.9=20+=20add=20mouse=5F{down,up,move,hold}=20+=20bloc?= =?UTF-8?q?k=5Furls=20+=20clear=5Fcookies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome 142 emits CDP message variants chromiumoxide 0.6's protocol crate can't deserialize, breaking every WS frame. Bump to 0.9 restores wire compatibility. New BrowserPort primitives: - mouse_down(x,y), mouse_up(x,y), mouse_move(x,y,buttons_mask) - mouse_hold(x,y,ms): humanized approach + ±1px jitter drag during hold, designed to clear PerimeterX/HUMAN press-and-hold challenges - block_urls(patterns): Network.setBlockedURLs glob (uses legacy `urls` array since chromiumoxide_cdp 0.9's `urlPatterns` shape is rejected by Chrome stable) - clear_cookies(origin): Storage.clearDataForOrigin for cookies + storage, resets bot-detection clearance state between runs Adapter refactored: extracted mouse_input.rs + cdp_ext.rs to keep each file under 200 LOC; new with_page() helper and op! macro deduplicate the page_for + within boilerplate. BREAKING: workspace version bumped 2.8.0 → 3.0.0. BrowserPort trait gains 5 required methods (mouse_down/up/move/hold + block_urls + clear_cookies). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 +- Cargo.lock | 479 +++++++----------- Cargo.toml | 84 +-- ras-agent/tests/run_agent_executor_test.rs | 18 + ras-cdp/src/domain/repository.rs | 28 + ras-cdp/src/infrastructure/cdp_ext.rs | 122 +++++ .../infrastructure/chromiumoxide_adapter.rs | 152 +++--- ras-cdp/src/infrastructure/mod.rs | 2 + ras-cdp/src/infrastructure/mouse_input.rs | 145 ++++++ .../tests/click_resolves_clickable_index.rs | 18 + 10 files changed, 627 insertions(+), 424 deletions(-) create mode 100644 ras-cdp/src/infrastructure/cdp_ext.rs create mode 100644 ras-cdp/src/infrastructure/mouse_input.rs diff --git a/.gitignore b/.gitignore index f82159e..73aa5f5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ target/ .direnv/ result .cargo-cache/ -.don't-push/ \ No newline at end of file +.don't-push/ +graphify-out/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 86e13a0..e2ac78b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,11 +117,14 @@ dependencies = [ [[package]] name = "async-tungstenite" -version = "0.25.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +checksum = "8acc405d38be14342132609f06f02acaf825ddccfe76c4824a69281e0458ebd4" dependencies = [ + "atomic-waker", + "futures-core", "futures-io", + "futures-task", "futures-util", "log", "pin-project-lite", @@ -184,12 +187,6 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "byteorder-lite" version = "0.1.0" @@ -229,14 +226,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chromiumoxide" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601bde44842e2875fff5cbf7229a2e9d690b0788cb7b3caa21533b93e6e1bd56" +checksum = "26ed067eb6c1f660bdb87c05efb964421d2ca262bae0296cdfe38cf0cd949a3e" dependencies = [ "async-tungstenite", "base64", "bytes", - "cfg-if", "chromiumoxide_cdp", "chromiumoxide_types", "dunce", @@ -244,22 +240,22 @@ dependencies = [ "futures", "futures-timer", "pin-project-lite", - "reqwest", + "reqwest 0.13.3", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "tokio", "tracing", "url", "which", - "winreg", + "windows-registry", ] [[package]] name = "chromiumoxide_cdp" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978f47e0ca49c6d113ea55fffaabb21bfed4f7494c10bfbaae772043416e066" +checksum = "68a6a03a7ebac4ea85308f285d6959a3e6b2ce32a0c9465dc7a7b1db0144eec7" dependencies = [ "chromiumoxide_pdl", "chromiumoxide_types", @@ -269,26 +265,25 @@ dependencies = [ [[package]] name = "chromiumoxide_pdl" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e9aa35ba6bb637e9c169afe9d7774d71871f2d5f4253cfddef6c64aa2f28e6" +checksum = "c602dea92337bc4d824668d78c5b79c3b4ddb29b40dd7218282bbe8fd3fc2091" dependencies = [ "chromiumoxide_types", "either", - "heck 0.4.1", + "heck", "once_cell", "proc-macro2", "quote", "regex", - "serde", "serde_json", ] [[package]] name = "chromiumoxide_types" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c6ef4b8f990b1c2258c5f89bbdf785b4382fa2742db7952da2e2047154a827" +checksum = "678d5146e74f16fc4a41978b275af572cd913de1f10270d2b93b6c276bc57d80" dependencies = [ "serde", "serde_json", @@ -336,7 +331,7 @@ version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -784,12 +779,6 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -802,15 +791,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "http" version = "1.4.0" @@ -1140,12 +1120,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1398,7 +1372,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -1413,13 +1387,13 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.4", + "rand", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1460,35 +1434,14 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -1498,16 +1451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -1521,7 +1465,7 @@ dependencies = [ [[package]] name = "ras-agent" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "chrono", @@ -1543,7 +1487,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1553,7 +1497,7 @@ dependencies = [ [[package]] name = "ras-browser" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-cdp", @@ -1563,7 +1507,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1571,7 +1515,7 @@ dependencies = [ [[package]] name = "ras-cdp" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1581,7 +1525,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1590,7 +1534,7 @@ dependencies = [ [[package]] name = "ras-cli" -version = "2.8.0" +version = "3.0.0" dependencies = [ "anyhow", "clap", @@ -1610,7 +1554,7 @@ dependencies = [ "ras-validation", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "tracing-subscriber", @@ -1620,15 +1564,15 @@ dependencies = [ [[package]] name = "ras-cloud" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1637,30 +1581,30 @@ dependencies = [ [[package]] name = "ras-config" -version = "2.8.0" +version = "3.0.0" dependencies = [ "dotenvy", "once_cell", "ras-errors", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tracing", "tracing-subscriber", ] [[package]] name = "ras-cosmium" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-cdp", "ras-errors", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1668,7 +1612,7 @@ dependencies = [ [[package]] name = "ras-daemon" -version = "2.8.0" +version = "3.0.0" dependencies = [ "anyhow", "dotenvy", @@ -1679,7 +1623,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1688,7 +1632,7 @@ dependencies = [ [[package]] name = "ras-dom" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1700,7 +1644,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1708,15 +1652,15 @@ dependencies = [ [[package]] name = "ras-errors" -version = "2.8.0" +version = "3.0.0" dependencies = [ "serde", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "ras-events" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-broadcast", "async-trait", @@ -1725,7 +1669,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1734,7 +1678,7 @@ dependencies = [ [[package]] name = "ras-filesystem" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", @@ -1743,13 +1687,13 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", ] [[package]] name = "ras-judge" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "image", @@ -1758,13 +1702,13 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "url", ] [[package]] name = "ras-llm" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", @@ -1772,13 +1716,13 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "url", ] [[package]] name = "ras-llm-anthropic" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "chrono", @@ -1787,12 +1731,12 @@ dependencies = [ "ras-llm", "ras-types", "regex", - "reqwest", + "reqwest 0.12.28", "security-framework", "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1801,16 +1745,16 @@ dependencies = [ [[package]] name = "ras-llm-bedrock" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1818,17 +1762,17 @@ dependencies = [ [[package]] name = "ras-llm-cerebras" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1836,16 +1780,16 @@ dependencies = [ [[package]] name = "ras-llm-cloud" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1853,17 +1797,17 @@ dependencies = [ [[package]] name = "ras-llm-deepseek" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1871,16 +1815,16 @@ dependencies = [ [[package]] name = "ras-llm-google" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1888,17 +1832,17 @@ dependencies = [ [[package]] name = "ras-llm-groq" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1906,16 +1850,16 @@ dependencies = [ [[package]] name = "ras-llm-langchain" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1923,17 +1867,17 @@ dependencies = [ [[package]] name = "ras-llm-mistral" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1941,16 +1885,16 @@ dependencies = [ [[package]] name = "ras-llm-oci" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1958,16 +1902,16 @@ dependencies = [ [[package]] name = "ras-llm-ollama" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1975,16 +1919,16 @@ dependencies = [ [[package]] name = "ras-llm-openai" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1992,17 +1936,17 @@ dependencies = [ [[package]] name = "ras-llm-openrouter" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -2010,17 +1954,17 @@ dependencies = [ [[package]] name = "ras-llm-vercel" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -2028,7 +1972,7 @@ dependencies = [ [[package]] name = "ras-mcp" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", @@ -2037,7 +1981,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -2045,7 +1989,7 @@ dependencies = [ [[package]] name = "ras-recording" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "image", @@ -2054,57 +1998,57 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", ] [[package]] name = "ras-sandbox" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", ] [[package]] name = "ras-skills" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] [[package]] name = "ras-telemetry" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] [[package]] name = "ras-tokens" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "chrono", @@ -2112,17 +2056,17 @@ dependencies = [ "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] [[package]] name = "ras-tools" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "indexmap", @@ -2135,7 +2079,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -2143,7 +2087,7 @@ dependencies = [ [[package]] name = "ras-types" -version = "2.8.0" +version = "3.0.0" dependencies = [ "chrono", "indexmap", @@ -2152,14 +2096,14 @@ dependencies = [ "serde", "serde_json", "smol_str", - "thiserror 2.0.18", + "thiserror", "url", "uuid", ] [[package]] name = "ras-validation" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-errors", @@ -2167,13 +2111,13 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "validator", ] [[package]] name = "ras-watchdogs" -version = "2.8.0" +version = "3.0.0" dependencies = [ "async-trait", "ras-browser", @@ -2183,7 +2127,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -2269,6 +2213,35 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2289,19 +2262,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -2311,7 +2271,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -2639,37 +2599,17 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2883,20 +2823,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.8.6", + "rand", "sha1", - "thiserror 1.0.69", - "url", + "thiserror", "utf-8", ] @@ -3185,14 +3123,11 @@ dependencies = [ [[package]] name = "which" -version = "4.4.2" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "libc", ] [[package]] @@ -3245,6 +3180,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -3263,15 +3209,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3281,15 +3218,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -3308,21 +3236,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -3356,12 +3269,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3374,12 +3281,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3392,12 +3293,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3422,12 +3317,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3440,12 +3329,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3458,12 +3341,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3476,12 +3353,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3494,16 +3365,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wiremock" version = "0.6.2" @@ -3550,7 +3411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -3561,7 +3422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", "prettyplease", "syn", diff --git a/Cargo.toml b/Cargo.toml index ff07415..05a2ea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ exclude = ["examples", "tests", "docs", "scripts"] default-members = ["ras-cli", "ras-daemon"] [workspace.package] -version = "2.8.0" +version = "3.0.0" edition = "2024" rust-version = "1.95.0" license = "MIT" @@ -63,52 +63,52 @@ keywords = ["browser", "automation", "llm", "agent", "cdp"] categories = ["web-programming", "command-line-utilities"] [workspace.dependencies] -ras-errors = { path = "ras-errors", version = "2.1.0" } -ras-types = { path = "ras-types", version = "2.1.0" } -ras-validation = { path = "ras-validation", version = "2.1.0" } -ras-config = { path = "ras-config", version = "2.1.0" } - -ras-events = { path = "ras-events", version = "2.1.0" } -ras-cdp = { path = "ras-cdp", version = "2.1.0" } -ras-cosmium = { path = "ras-cosmium", version = "2.1.0" } - -ras-llm = { path = "ras-llm", version = "2.1.0" } -ras-llm-anthropic = { path = "ras-llm-anthropic", version = "2.1.0" } -ras-llm-openai = { path = "ras-llm-openai", version = "2.1.0" } -ras-llm-google = { path = "ras-llm-google", version = "2.1.0" } -ras-llm-groq = { path = "ras-llm-groq", version = "2.1.0" } -ras-llm-bedrock = { path = "ras-llm-bedrock", version = "2.1.0" } -ras-llm-openrouter = { path = "ras-llm-openrouter", version = "2.1.0" } -ras-llm-vercel = { path = "ras-llm-vercel", version = "2.1.0" } -ras-llm-deepseek = { path = "ras-llm-deepseek", version = "2.1.0" } -ras-llm-cerebras = { path = "ras-llm-cerebras", version = "2.1.0" } -ras-llm-mistral = { path = "ras-llm-mistral", version = "2.1.0" } -ras-llm-ollama = { path = "ras-llm-ollama", version = "2.1.0" } -ras-llm-oci = { path = "ras-llm-oci", version = "2.1.0" } -ras-llm-cloud = { path = "ras-llm-cloud", version = "2.1.0" } -ras-llm-langchain = { path = "ras-llm-langchain", version = "2.1.0" } - -ras-browser = { path = "ras-browser", version = "2.1.0" } -ras-dom = { path = "ras-dom", version = "2.1.0" } -ras-tools = { path = "ras-tools", version = "2.1.0" } -ras-watchdogs = { path = "ras-watchdogs", version = "2.1.0" } -ras-filesystem = { path = "ras-filesystem", version = "2.1.0" } -ras-tokens = { path = "ras-tokens", version = "2.1.0" } -ras-judge = { path = "ras-judge", version = "2.1.0" } -ras-telemetry = { path = "ras-telemetry", version = "2.1.0" } -ras-recording = { path = "ras-recording", version = "2.1.0" } -ras-sandbox = { path = "ras-sandbox", version = "2.1.0" } -ras-skills = { path = "ras-skills", version = "2.1.0" } -ras-mcp = { path = "ras-mcp", version = "2.1.0" } -ras-cloud = { path = "ras-cloud", version = "2.1.0" } - -ras-agent = { path = "ras-agent", version = "2.1.0" } +ras-errors = { path = "ras-errors", version = "3.0.0" } +ras-types = { path = "ras-types", version = "3.0.0" } +ras-validation = { path = "ras-validation", version = "3.0.0" } +ras-config = { path = "ras-config", version = "3.0.0" } + +ras-events = { path = "ras-events", version = "3.0.0" } +ras-cdp = { path = "ras-cdp", version = "3.0.0" } +ras-cosmium = { path = "ras-cosmium", version = "3.0.0" } + +ras-llm = { path = "ras-llm", version = "3.0.0" } +ras-llm-anthropic = { path = "ras-llm-anthropic", version = "3.0.0" } +ras-llm-openai = { path = "ras-llm-openai", version = "3.0.0" } +ras-llm-google = { path = "ras-llm-google", version = "3.0.0" } +ras-llm-groq = { path = "ras-llm-groq", version = "3.0.0" } +ras-llm-bedrock = { path = "ras-llm-bedrock", version = "3.0.0" } +ras-llm-openrouter = { path = "ras-llm-openrouter", version = "3.0.0" } +ras-llm-vercel = { path = "ras-llm-vercel", version = "3.0.0" } +ras-llm-deepseek = { path = "ras-llm-deepseek", version = "3.0.0" } +ras-llm-cerebras = { path = "ras-llm-cerebras", version = "3.0.0" } +ras-llm-mistral = { path = "ras-llm-mistral", version = "3.0.0" } +ras-llm-ollama = { path = "ras-llm-ollama", version = "3.0.0" } +ras-llm-oci = { path = "ras-llm-oci", version = "3.0.0" } +ras-llm-cloud = { path = "ras-llm-cloud", version = "3.0.0" } +ras-llm-langchain = { path = "ras-llm-langchain", version = "3.0.0" } + +ras-browser = { path = "ras-browser", version = "3.0.0" } +ras-dom = { path = "ras-dom", version = "3.0.0" } +ras-tools = { path = "ras-tools", version = "3.0.0" } +ras-watchdogs = { path = "ras-watchdogs", version = "3.0.0" } +ras-filesystem = { path = "ras-filesystem", version = "3.0.0" } +ras-tokens = { path = "ras-tokens", version = "3.0.0" } +ras-judge = { path = "ras-judge", version = "3.0.0" } +ras-telemetry = { path = "ras-telemetry", version = "3.0.0" } +ras-recording = { path = "ras-recording", version = "3.0.0" } +ras-sandbox = { path = "ras-sandbox", version = "3.0.0" } +ras-skills = { path = "ras-skills", version = "3.0.0" } +ras-mcp = { path = "ras-mcp", version = "3.0.0" } +ras-cloud = { path = "ras-cloud", version = "3.0.0" } + +ras-agent = { path = "ras-agent", version = "3.0.0" } async-trait = "0.1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } futures = "0.3" -chromiumoxide = { version = "0.6", default-features = false, features = ["tokio-runtime", "bytes"] } +chromiumoxide = { version = "0.9", default-features = false, features = ["bytes"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/ras-agent/tests/run_agent_executor_test.rs b/ras-agent/tests/run_agent_executor_test.rs index a173e26..55d8e14 100644 --- a/ras-agent/tests/run_agent_executor_test.rs +++ b/ras-agent/tests/run_agent_executor_test.rs @@ -63,6 +63,24 @@ impl BrowserPort for MockBrowser { async fn click_node(&self, _t: &TargetId, _n: BackendNodeId) -> Result<(), AppError> { Ok(()) } + async fn mouse_down(&self, _t: &TargetId, _x: i32, _y: i32) -> Result<(), AppError> { + Ok(()) + } + async fn mouse_up(&self, _t: &TargetId, _x: i32, _y: i32) -> Result<(), AppError> { + Ok(()) + } + async fn mouse_hold(&self, _t: &TargetId, _x: i32, _y: i32, _ms: u64) -> Result<(), AppError> { + Ok(()) + } + async fn mouse_move(&self, _t: &TargetId, _x: i32, _y: i32, _b: i64) -> Result<(), AppError> { + Ok(()) + } + async fn block_urls(&self, _t: &TargetId, _patterns: Vec) -> Result<(), AppError> { + Ok(()) + } + async fn clear_cookies(&self, _t: &TargetId, _origin: &str) -> Result<(), AppError> { + Ok(()) + } async fn type_text(&self, _t: &TargetId, _text: &str) -> Result<(), AppError> { Ok(()) } diff --git a/ras-cdp/src/domain/repository.rs b/ras-cdp/src/domain/repository.rs index cb59c52..c8e75f6 100644 --- a/ras-cdp/src/domain/repository.rs +++ b/ras-cdp/src/domain/repository.rs @@ -27,6 +27,21 @@ pub trait BrowserPort: Send + Sync + 'static { ) -> Result; async fn click_at(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError>; async fn click_node(&self, target: &TargetId, node: BackendNodeId) -> Result<(), AppError>; + async fn mouse_down(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError>; + async fn mouse_up(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError>; + /// Dispatch a single MouseMoved event. `buttons_mask` follows the W3C + /// buttons bitfield: 0 = none, 1 = left held (drag), 2 = right, 4 = middle. + async fn mouse_move( + &self, + target: &TargetId, + x: i32, + y: i32, + buttons_mask: i64, + ) -> Result<(), AppError>; + /// Humanized press-and-hold at (x, y): pre-approach moves, press, jittered + /// drag-style MouseMoved events with buttons=1 during the hold, release. + /// `ms` is the hold duration (excluding approach overhead). + async fn mouse_hold(&self, target: &TargetId, x: i32, y: i32, ms: u64) -> Result<(), AppError>; async fn type_text(&self, target: &TargetId, text: &str) -> Result<(), AppError>; async fn screenshot( &self, @@ -34,6 +49,19 @@ pub trait BrowserPort: Send + Sync + 'static { format: ScreenshotFormat, ) -> Result, AppError>; async fn set_viewport(&self, target: &TargetId, viewport: Viewport) -> Result<(), AppError>; + /// Enable Network domain and block requests whose URL matches any of the + /// given glob patterns (Chrome's `Network.setBlockedURLs.urls` accepts + /// `*` as the only wildcard — e.g. `*googlesyndication.com*`). Used to + /// suppress ads and trackers so the page layout settles deterministically. + async fn block_urls( + &self, + target: &TargetId, + url_patterns: Vec, + ) -> Result<(), AppError>; + /// Clear cookies + local storage + session storage for the given origin. + /// `origin` is a scheme+host (e.g. `https://www.pedidosya.com.ar`). Used + /// to reset bot-detection clearance cookies between test runs. + async fn clear_cookies(&self, target: &TargetId, origin: &str) -> Result<(), AppError>; async fn close_target(&self, target: &TargetId) -> Result<(), AppError>; async fn create_target(&self, url: &Url) -> Result; } diff --git a/ras-cdp/src/infrastructure/cdp_ext.rs b/ras-cdp/src/infrastructure/cdp_ext.rs new file mode 100644 index 0000000..4fbcbb7 --- /dev/null +++ b/ras-cdp/src/infrastructure/cdp_ext.rs @@ -0,0 +1,122 @@ +use std::future::Future; +use std::sync::Arc; +use std::time::Duration; + +use chromiumoxide::cdp::browser_protocol::network::EnableParams as NetworkEnableParams; +use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat; +use chromiumoxide::cdp::browser_protocol::storage::ClearDataForOriginParams; +use chromiumoxide::types::MethodId; +use chromiumoxide::{Browser, Command, Method, Page}; +use ras_errors::AppError; +use ras_types::TargetId; +use serde::Serialize; +use tokio::sync::Mutex; + +use crate::domain::repository::ScreenshotFormat; +use crate::infrastructure::chromiumoxide_helpers::page_for; +use crate::infrastructure::timeout::within; + +pub(crate) async fn with_page( + browser: &Arc>, + target: &TargetId, + timeout: Duration, + label: &'static str, + f: F, +) -> Result +where + F: FnOnce(Page) -> Fut, + Fut: Future>, +{ + let page = page_for(browser, target).await?; + within(label, timeout, f(page)).await +} + +#[derive(Serialize)] +pub(crate) struct LegacyBlockUrls { + pub urls: Vec, +} + +impl Method for LegacyBlockUrls { + fn identifier(&self) -> MethodId { + "Network.setBlockedURLs".into() + } +} + +impl Command for LegacyBlockUrls { + type Response = serde_json::Value; +} + +pub(crate) async fn cdp_block_urls(page: &Page, urls: Vec) -> Result<(), AppError> { + page.execute(NetworkEnableParams::default()) + .await + .map_err(|e| AppError::ActionFailed(format!("Network.enable: {e}")))?; + page.execute(LegacyBlockUrls { urls }) + .await + .map_err(|e| AppError::ActionFailed(format!("Network.setBlockedURLs: {e}")))?; + Ok(()) +} + +pub(crate) async fn cdp_clear_cookies(page: &Page, origin: &str) -> Result<(), AppError> { + let params = ClearDataForOriginParams::new( + origin.to_string(), + "cookies,local_storage,session_storage,indexeddb,service_workers,cache_storage", + ); + page.execute(params) + .await + .map_err(|e| AppError::ActionFailed(format!("Storage.clearDataForOrigin: {e}")))?; + Ok(()) +} + +pub(crate) async fn cdp_navigate(page: &Page, url_str: String) -> Result<(), AppError> { + page.goto(url_str.clone()) + .await + .map_err(|e| AppError::ActionFailed(format!("goto {url_str}: {e}")))?; + Ok(()) +} + +pub(crate) async fn cdp_evaluate( + page: &Page, + expression: String, +) -> Result { + let v = page + .evaluate(expression.as_str()) + .await + .map_err(|e| AppError::ActionFailed(format!("evaluate: {e}")))?; + Ok(v.into_value::() + .unwrap_or(serde_json::Value::Null)) +} + +pub(crate) async fn cdp_click_at(page: &Page, x: i32, y: i32) -> Result<(), AppError> { + page.click(chromiumoxide::layout::Point { + x: f64::from(x), + y: f64::from(y), + }) + .await + .map_err(|e| AppError::ActionFailed(format!("click_at: {e}")))?; + Ok(()) +} + +pub(crate) async fn cdp_screenshot( + page: &Page, + format: ScreenshotFormat, +) -> Result, AppError> { + let cdp_format = match format { + ScreenshotFormat::Png => CaptureScreenshotFormat::Png, + ScreenshotFormat::Jpeg => CaptureScreenshotFormat::Jpeg, + }; + page.screenshot( + chromiumoxide::page::ScreenshotParams::builder() + .format(cdp_format) + .full_page(false) + .build(), + ) + .await + .map_err(|e| AppError::ActionFailed(format!("screenshot: {e}"))) +} + +pub(crate) async fn cdp_close(page: Page) -> Result<(), AppError> { + page.close() + .await + .map_err(|e| AppError::ActionFailed(format!("close_target: {e}")))?; + Ok(()) +} diff --git a/ras-cdp/src/infrastructure/chromiumoxide_adapter.rs b/ras-cdp/src/infrastructure/chromiumoxide_adapter.rs index 63f4d8d..0120941 100644 --- a/ras-cdp/src/infrastructure/chromiumoxide_adapter.rs +++ b/ras-cdp/src/infrastructure/chromiumoxide_adapter.rs @@ -3,7 +3,6 @@ use std::time::Duration; use async_trait::async_trait; use chromiumoxide::Browser; -use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat; use chromiumoxide::handler::HandlerConfig; use futures::StreamExt; use ras_errors::AppError; @@ -14,8 +13,15 @@ use url::Url; use crate::domain::repository::{BrowserPort, ScreenshotFormat}; use crate::domain::viewport::Viewport; +use crate::infrastructure::cdp_ext::{ + cdp_block_urls, cdp_clear_cookies, cdp_click_at, cdp_close, cdp_evaluate, cdp_navigate, + cdp_screenshot, with_page, +}; use crate::infrastructure::chromiumoxide_helpers::{list_target_ids, new_target, page_for}; use crate::infrastructure::chromiumoxide_input::{click_backend_node, type_chars}; +use crate::infrastructure::mouse_input::{ + dispatch_mouse_hold, dispatch_mouse_move, dispatch_mouse_press, dispatch_mouse_release, +}; use crate::infrastructure::timeout::within; pub struct ChromiumoxideAdapter { @@ -58,119 +64,121 @@ impl ChromiumoxideAdapter { } } +macro_rules! op { + ($self:expr, $target:expr, $label:literal, |$p:ident| $body:expr) => { + with_page( + &$self.browser, + $target, + $self.request_timeout, + $label, + |$p| async move { $body }, + ) + .await + }; +} + #[async_trait] impl BrowserPort for ChromiumoxideAdapter { async fn cdp_url(&self) -> Result { Ok(self.cdp_url.clone()) } - async fn list_targets(&self) -> Result, AppError> { list_target_ids(&self.browser).await } - async fn focused_target(&self) -> Result { let mut targets = self.list_targets().await?; targets .pop() .ok_or_else(|| AppError::NotFound("no focused target".into())) } - async fn navigate(&self, target: &TargetId, url: &Url) -> Result<(), AppError> { - let page = page_for(&self.browser, target).await?; - let url_str = url.as_str().to_string(); - within("navigate", self.request_timeout, async move { - page.goto(url_str.clone()) - .await - .map_err(|e| AppError::ActionFailed(format!("goto {url_str}: {e}")))?; - Ok(()) - }) - .await + let u = url.as_str().to_string(); + op!(self, target, "navigate", |p| cdp_navigate(&p, u).await) } - async fn evaluate( &self, target: &TargetId, expression: &str, ) -> Result { - let page = page_for(&self.browser, target).await?; - let expr = expression.to_string(); - within("evaluate", self.request_timeout, async move { - let v = page - .evaluate(expr.as_str()) - .await - .map_err(|e| AppError::ActionFailed(format!("evaluate: {e}")))?; - Ok(v.into_value::() - .unwrap_or(serde_json::Value::Null)) - }) - .await + let e = expression.to_string(); + op!(self, target, "evaluate", |p| cdp_evaluate(&p, e).await) } - async fn click_at(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError> { - let page = page_for(&self.browser, target).await?; - within("click_at", self.request_timeout, async move { - page.click(chromiumoxide::layout::Point { - x: f64::from(x), - y: f64::from(y), - }) - .await - .map_err(|e| AppError::ActionFailed(format!("click_at: {e}")))?; - Ok(()) - }) - .await + op!(self, target, "click_at", |p| cdp_click_at(&p, x, y).await) } - async fn click_node(&self, target: &TargetId, node: BackendNodeId) -> Result<(), AppError> { + op!(self, target, "click_node", |p| click_backend_node(&p, node) + .await) + } + async fn mouse_down(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError> { + op!(self, target, "mouse_down", |p| dispatch_mouse_press( + &p, x, y + ) + .await) + } + async fn mouse_up(&self, target: &TargetId, x: i32, y: i32) -> Result<(), AppError> { + op!(self, target, "mouse_up", |p| dispatch_mouse_release( + &p, x, y + ) + .await) + } + async fn mouse_move( + &self, + target: &TargetId, + x: i32, + y: i32, + buttons_mask: i64, + ) -> Result<(), AppError> { + op!(self, target, "mouse_move", |p| dispatch_mouse_move( + &p, + x, + y, + buttons_mask + ) + .await) + } + async fn mouse_hold(&self, target: &TargetId, x: i32, y: i32, ms: u64) -> Result<(), AppError> { let page = page_for(&self.browser, target).await?; - within("click_node", self.request_timeout, async move { - click_backend_node(&page, node).await + let budget = self.request_timeout + Duration::from_millis(ms + 5_000); + within("mouse_hold", budget, async move { + dispatch_mouse_hold(&page, x, y, ms).await }) .await } - async fn type_text(&self, target: &TargetId, text: &str) -> Result<(), AppError> { - let page = page_for(&self.browser, target).await?; - let text = text.to_string(); - within("type_text", self.request_timeout, async move { - type_chars(&page, &text).await - }) - .await + let t = text.to_string(); + op!(self, target, "type_text", |p| type_chars(&p, &t).await) } - async fn screenshot( &self, target: &TargetId, format: ScreenshotFormat, ) -> Result, AppError> { - let page = page_for(&self.browser, target).await?; - let cdp_format = match format { - ScreenshotFormat::Png => CaptureScreenshotFormat::Png, - ScreenshotFormat::Jpeg => CaptureScreenshotFormat::Jpeg, - }; - within("screenshot", self.request_timeout, async move { - page.screenshot( - chromiumoxide::page::ScreenshotParams::builder() - .format(cdp_format) - .full_page(false) - .build(), - ) - .await - .map_err(|e| AppError::ActionFailed(format!("screenshot: {e}"))) - }) - .await + op!(self, target, "screenshot", |p| cdp_screenshot(&p, format) + .await) } - async fn set_viewport(&self, _target: &TargetId, _viewport: Viewport) -> Result<(), AppError> { Ok(()) } - + async fn block_urls( + &self, + target: &TargetId, + url_patterns: Vec, + ) -> Result<(), AppError> { + op!(self, target, "block_urls", |p| cdp_block_urls( + &p, + url_patterns + ) + .await) + } + async fn clear_cookies(&self, target: &TargetId, origin: &str) -> Result<(), AppError> { + let o = origin.to_string(); + op!(self, target, "clear_cookies", |p| cdp_clear_cookies(&p, &o) + .await) + } async fn close_target(&self, target: &TargetId) -> Result<(), AppError> { - let page = page_for(&self.browser, target).await?; - page.close() - .await - .map_err(|e| AppError::ActionFailed(format!("close_target: {e}")))?; - Ok(()) + op!(self, target, "close_target", |p| cdp_close(p).await) } - async fn create_target(&self, url: &Url) -> Result { new_target(&self.browser, url).await } diff --git a/ras-cdp/src/infrastructure/mod.rs b/ras-cdp/src/infrastructure/mod.rs index a9abb64..ffd5c7f 100644 --- a/ras-cdp/src/infrastructure/mod.rs +++ b/ras-cdp/src/infrastructure/mod.rs @@ -1,4 +1,6 @@ +pub mod cdp_ext; pub mod chromiumoxide_adapter; pub mod chromiumoxide_helpers; pub mod chromiumoxide_input; +pub mod mouse_input; pub mod timeout; diff --git a/ras-cdp/src/infrastructure/mouse_input.rs b/ras-cdp/src/infrastructure/mouse_input.rs new file mode 100644 index 0000000..028b21b --- /dev/null +++ b/ras-cdp/src/infrastructure/mouse_input.rs @@ -0,0 +1,145 @@ +use std::time::Instant; + +use chromiumoxide::Page; +use chromiumoxide::cdp::browser_protocol::input::{ + DispatchMouseEventParams, DispatchMouseEventType, MouseButton, +}; +use ras_errors::AppError; +use tokio::time::{Duration as TDur, sleep}; + +pub(crate) async fn dispatch_mouse_move( + page: &Page, + x: i32, + y: i32, + buttons_mask: i64, +) -> Result<(), AppError> { + let mut b = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseMoved) + .x(f64::from(x)) + .y(f64::from(y)) + .buttons(buttons_mask); + if buttons_mask & 1 != 0 { + b = b.button(MouseButton::Left); + } + let mv = b + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_move params: {e}")))?; + page.execute(mv) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_move dispatch: {e}")))?; + Ok(()) +} + +pub(crate) async fn dispatch_mouse_press(page: &Page, x: i32, y: i32) -> Result<(), AppError> { + let move_params = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseMoved) + .x(f64::from(x)) + .y(f64::from(y)) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_move params: {e}")))?; + page.execute(move_params) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_move dispatch: {e}")))?; + let press = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MousePressed) + .x(f64::from(x)) + .y(f64::from(y)) + .button(MouseButton::Left) + .click_count(1) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_press params: {e}")))?; + page.execute(press) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_press dispatch: {e}")))?; + Ok(()) +} + +pub(crate) async fn dispatch_mouse_release(page: &Page, x: i32, y: i32) -> Result<(), AppError> { + let release = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseReleased) + .x(f64::from(x)) + .y(f64::from(y)) + .button(MouseButton::Left) + .click_count(1) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_release params: {e}")))?; + page.execute(release) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_release dispatch: {e}")))?; + Ok(()) +} + +/// Press at (x, y), hold for total_ms while emitting small jittered MouseMoved +/// events with `buttons=1`, then release. Approaches the target with intermediate +/// moves first so the input trace looks human, not teleported. Used to defeat +/// PerimeterX / HUMAN "Press & Hold" challenges that score on movement entropy. +pub(crate) async fn dispatch_mouse_hold( + page: &Page, + x: i32, + y: i32, + total_ms: u64, +) -> Result<(), AppError> { + let steps = 6_i32; + let start_x = x - 40; + let start_y = y - 30; + for i in 1..=steps { + let t = f64::from(i) / f64::from(steps); + let cx = (f64::from(start_x) + (f64::from(x - start_x)) * t).round() as i32; + let cy = (f64::from(start_y) + (f64::from(y - start_y)) * t).round() as i32; + let mv = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseMoved) + .x(f64::from(cx)) + .y(f64::from(cy)) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_move params: {e}")))?; + page.execute(mv) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_move dispatch: {e}")))?; + sleep(TDur::from_millis(25)).await; + } + let press = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MousePressed) + .x(f64::from(x)) + .y(f64::from(y)) + .button(MouseButton::Left) + .buttons(1_i64) + .click_count(1) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_press params: {e}")))?; + page.execute(press) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_press dispatch: {e}")))?; + let started = Instant::now(); + let tick = TDur::from_millis(80); + let total = TDur::from_millis(total_ms); + let mut n: i64 = 0; + while started.elapsed() < total { + sleep(tick).await; + n = n.wrapping_add(1); + let dx = (((n.wrapping_mul(73)) % 3) - 1) as i32; + let dy = (((n.wrapping_mul(31)) % 3) - 1) as i32; + let mv = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseMoved) + .x(f64::from(x + dx)) + .y(f64::from(y + dy)) + .button(MouseButton::Left) + .buttons(1_i64) + .build() + .map_err(|e| AppError::ActionFailed(format!("hold_jitter params: {e}")))?; + if let Err(e) = page.execute(mv).await { + tracing::warn!(error = %e, "hold jitter dispatch failed; continuing"); + } + } + let release = DispatchMouseEventParams::builder() + .r#type(DispatchMouseEventType::MouseReleased) + .x(f64::from(x)) + .y(f64::from(y)) + .button(MouseButton::Left) + .click_count(1) + .build() + .map_err(|e| AppError::ActionFailed(format!("mouse_release params: {e}")))?; + page.execute(release) + .await + .map_err(|e| AppError::ActionFailed(format!("mouse_release dispatch: {e}")))?; + Ok(()) +} diff --git a/ras-tools/tests/click_resolves_clickable_index.rs b/ras-tools/tests/click_resolves_clickable_index.rs index 6c363db..9059cc8 100644 --- a/ras-tools/tests/click_resolves_clickable_index.rs +++ b/ras-tools/tests/click_resolves_clickable_index.rs @@ -44,6 +44,24 @@ impl BrowserPort for CaptureBrowser { self.clicked.lock().expect("lock").push(n); Ok(()) } + async fn mouse_down(&self, _t: &TargetId, _x: i32, _y: i32) -> Result<(), AppError> { + unimplemented!() + } + async fn mouse_up(&self, _t: &TargetId, _x: i32, _y: i32) -> Result<(), AppError> { + unimplemented!() + } + async fn mouse_hold(&self, _t: &TargetId, _x: i32, _y: i32, _ms: u64) -> Result<(), AppError> { + unimplemented!() + } + async fn mouse_move(&self, _t: &TargetId, _x: i32, _y: i32, _b: i64) -> Result<(), AppError> { + unimplemented!() + } + async fn block_urls(&self, _t: &TargetId, _patterns: Vec) -> Result<(), AppError> { + unimplemented!() + } + async fn clear_cookies(&self, _t: &TargetId, _origin: &str) -> Result<(), AppError> { + unimplemented!() + } async fn type_text(&self, _t: &TargetId, _s: &str) -> Result<(), AppError> { unimplemented!() } From c060b4e4b489266f5339b7e6c4ae6c59618125db Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 16 May 2026 04:41:00 +0700 Subject: [PATCH 2/4] feat(tools): press_and_hold_{element,coordinate} builtins Two new agent actions backed by BrowserPort::mouse_hold: - press_and_hold_element { index, ms? }: resolves bbox center of a clickable in the current DOM snapshot - press_and_hold_coordinate { x, y, ms? }: raw pixel coords for buttons that aren't in the snapshot (e.g. inside a cross-origin iframe) Default hold 12000ms, clamped 100..60000. Both dispatch the humanized CDP press (pre-approach + jitter during hold) so PerimeterX-style press-and-hold widgets accept the gesture as human. Registry default now exposes 10 actions; registry_test updated. Co-Authored-By: Claude Opus 4.7 (1M context) --- ras-tools/src/infrastructure/builtin/mod.rs | 1 + .../infrastructure/builtin/press_and_hold.rs | 142 ++++++++++++++++++ .../src/infrastructure/builtin/register.rs | 5 + ras-tools/tests/registry_test.rs | 8 +- 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 ras-tools/src/infrastructure/builtin/press_and_hold.rs diff --git a/ras-tools/src/infrastructure/builtin/mod.rs b/ras-tools/src/infrastructure/builtin/mod.rs index e191583..ed2024e 100644 --- a/ras-tools/src/infrastructure/builtin/mod.rs +++ b/ras-tools/src/infrastructure/builtin/mod.rs @@ -1,6 +1,7 @@ pub mod click; pub mod done; pub mod navigate; +pub mod press_and_hold; pub mod register; pub mod screenshot; pub mod scroll; diff --git a/ras-tools/src/infrastructure/builtin/press_and_hold.rs b/ras-tools/src/infrastructure/builtin/press_and_hold.rs new file mode 100644 index 0000000..77ef187 --- /dev/null +++ b/ras-tools/src/infrastructure/builtin/press_and_hold.rs @@ -0,0 +1,142 @@ +use async_trait::async_trait; +use ras_errors::AppError; +use ras_types::{ActionName, ActionResult, ActionTimeout}; +use serde::Deserialize; +use serde_json::json; + +use crate::domain::action::{ActionMetadata, ToolHandler}; +use crate::domain::registry::ToolContext; + +const DEFAULT_HOLD_MS: u64 = 12_000; +const MAX_HOLD_MS: u64 = 60_000; + +fn clamp_hold(ms: u64) -> u64 { + ms.clamp(100, MAX_HOLD_MS) +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct PressAndHoldCoordinateAction; + +#[derive(Deserialize)] +struct CoordParams { + x: i32, + y: i32, + #[serde(default)] + ms: Option, +} + +#[async_trait] +impl ToolHandler for PressAndHoldCoordinateAction { + fn metadata(&self) -> ActionMetadata { + ActionMetadata { + name: ActionName("press_and_hold_coordinate".into()), + description: "Press the left mouse button at (x, y) in CSS pixels, hold for `ms` \ + milliseconds (default 12000, max 60000), then release. Designed for \ + PerimeterX / HUMAN \"Press & Hold\" challenges. Uses CDP \ + Input.dispatchMouseEvent so events are isTrusted=true." + .into(), + parameters_schema: json!({ + "type": "object", + "properties": { + "x": {"type": "integer"}, + "y": {"type": "integer"}, + "ms": {"type": "integer", "minimum": 100, "maximum": MAX_HOLD_MS} + }, + "required": ["x", "y"] + }), + domain_filter: Vec::new(), + terminates_sequence: false, + timeout: ActionTimeout::default(), + } + } + + async fn execute( + &self, + params: serde_json::Value, + ctx: ToolContext, + ) -> Result { + let p: CoordParams = serde_json::from_value(params) + .map_err(|e| AppError::ValidationError(format!("press_and_hold_coordinate: {e}")))?; + let hold = clamp_hold(p.ms.unwrap_or(DEFAULT_HOLD_MS)); + let target = ctx.browser.focused_target().await?; + ctx.browser.mouse_hold(&target, p.x, p.y, hold).await?; + Ok(ActionResult::ok(format!( + "held ({}, {}) for {} ms (humanized: approach + jitter)", + p.x, p.y, hold + ))) + } +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct PressAndHoldElementAction; + +#[derive(Deserialize)] +struct IndexParams { + index: i64, + #[serde(default)] + ms: Option, +} + +#[async_trait] +impl ToolHandler for PressAndHoldElementAction { + fn metadata(&self) -> ActionMetadata { + ActionMetadata { + name: ActionName("press_and_hold_element".into()), + description: "Press-and-hold the interactive element with the given index. Resolves \ + the element's bbox center, dispatches a real CDP mouse press, sleeps \ + for `ms` (default 12000, max 60000), then releases. Use for PerimeterX \ + / HUMAN \"Press & Hold\" buttons." + .into(), + parameters_schema: json!({ + "type": "object", + "properties": { + "index": {"type": "integer"}, + "ms": {"type": "integer", "minimum": 100, "maximum": MAX_HOLD_MS} + }, + "required": ["index"] + }), + domain_filter: Vec::new(), + terminates_sequence: false, + timeout: ActionTimeout::default(), + } + } + + async fn execute( + &self, + params: serde_json::Value, + ctx: ToolContext, + ) -> Result { + let p: IndexParams = serde_json::from_value(params) + .map_err(|e| AppError::ValidationError(format!("press_and_hold_element: {e}")))?; + let element = ctx + .clickables + .iter() + .find(|c| i64::from(c.index) == p.index) + .ok_or_else(|| { + AppError::ValidationError(format!( + "no clickable with index {} in current snapshot ({} available)", + p.index, + ctx.clickables.len() + )) + })?; + let cx = (element.bbox.x + element.bbox.width / 2.0).round() as i32; + let cy = (element.bbox.y + element.bbox.height / 2.0).round() as i32; + let hold = clamp_hold(p.ms.unwrap_or(DEFAULT_HOLD_MS)); + tracing::debug!( + target: "ras_tools::press_and_hold", + list_index = p.index, + backend_node_id = ?element.backend_node_id, + xpath = %element.xpath, + x = cx, + y = cy, + ms = hold, + "press_and_hold_element resolved" + ); + let target = ctx.browser.focused_target().await?; + ctx.browser.mouse_hold(&target, cx, cy, hold).await?; + Ok(ActionResult::ok(format!( + "held element {} at ({}, {}) for {} ms (humanized: approach + jitter)", + p.index, cx, cy, hold + ))) + } +} diff --git a/ras-tools/src/infrastructure/builtin/register.rs b/ras-tools/src/infrastructure/builtin/register.rs index ab70a88..02eab1a 100644 --- a/ras-tools/src/infrastructure/builtin/register.rs +++ b/ras-tools/src/infrastructure/builtin/register.rs @@ -6,6 +6,9 @@ use crate::domain::registry::ActionRegistry; use crate::infrastructure::builtin::click::{ClickCoordinateAction, ClickElementAction}; use crate::infrastructure::builtin::done::DoneAction; use crate::infrastructure::builtin::navigate::NavigateAction; +use crate::infrastructure::builtin::press_and_hold::{ + PressAndHoldCoordinateAction, PressAndHoldElementAction, +}; use crate::infrastructure::builtin::screenshot::ScreenshotAction; use crate::infrastructure::builtin::scroll::ScrollAction; use crate::infrastructure::builtin::type_text::TypeTextAction; @@ -15,6 +18,8 @@ pub fn register_default_actions(registry: &mut ActionRegistry) -> Result<(), App registry.register(Arc::new(NavigateAction))?; registry.register(Arc::new(ClickElementAction))?; registry.register(Arc::new(ClickCoordinateAction))?; + registry.register(Arc::new(PressAndHoldElementAction))?; + registry.register(Arc::new(PressAndHoldCoordinateAction))?; registry.register(Arc::new(TypeTextAction))?; registry.register(Arc::new(ScrollAction))?; registry.register(Arc::new(ScreenshotAction))?; diff --git a/ras-tools/tests/registry_test.rs b/ras-tools/tests/registry_test.rs index e36f20e..18297cd 100644 --- a/ras-tools/tests/registry_test.rs +++ b/ras-tools/tests/registry_test.rs @@ -2,14 +2,16 @@ use ras_tools::{ActionRegistry, register_default_actions}; use ras_types::ActionName; #[test] -fn default_set_registers_eight_actions() { +fn default_set_registers_ten_actions() { let mut r = ActionRegistry::new(); register_default_actions(&mut r).expect("register"); - assert_eq!(r.len(), 8); + assert_eq!(r.len(), 10); for name in [ "navigate", "click_element", "click_coordinate", + "press_and_hold_element", + "press_and_hold_coordinate", "type_text", "scroll", "screenshot", @@ -34,7 +36,7 @@ fn exclude_removes_action() { register_default_actions(&mut r).expect("register"); let n = ActionName("done".into()); r.exclude(&n); - assert_eq!(r.len(), 7); + assert_eq!(r.len(), 9); assert!(r.get(&n).is_none()); } From 7e23f86287bfb1b9661cdfacb300e0b65cd52f62 Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 16 May 2026 04:43:02 +0700 Subject: [PATCH 3/4] feat(agent,llm): log brain decisions + raw LLM I/O ras_agent::brain target now emits per-step structured logs: - agent decision: eval / memory / next_goal / actions[] - agent action params (debug): raw parameters JSON per action - action result: ok, done, extracted_content, error ras_llm_openai gains TRACE-level dumps of the request body and the raw response body, so the full prompt + completion can be inspected when debugging a misbehaving model. Tracing call sites factored out to run_step_log.rs. Common log presets: RUST_LOG=info,ras_agent::brain=info # brain + action stream RUST_LOG=info,ras_agent::brain=debug # + raw action params RUST_LOG=info,ras_llm_openai=trace # full LLM I/O (very loud) Co-Authored-By: Claude Opus 4.7 (1M context) --- ras-agent/src/application/mod.rs | 1 + ras-agent/src/application/run_step.rs | 4 ++ ras-agent/src/application/run_step_log.rs | 47 +++++++++++++++++++ .../http/chat_openai_compatible.rs | 11 ++++- 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 ras-agent/src/application/run_step_log.rs diff --git a/ras-agent/src/application/mod.rs b/ras-agent/src/application/mod.rs index ef59cd8..f6a4483 100644 --- a/ras-agent/src/application/mod.rs +++ b/ras-agent/src/application/mod.rs @@ -7,4 +7,5 @@ pub mod render_plan; pub mod render_step_message; pub mod run_agent; pub mod run_step; +pub(crate) mod run_step_log; pub mod url_shortening; diff --git a/ras-agent/src/application/run_step.rs b/ras-agent/src/application/run_step.rs index 5f7b998..1b54a1d 100644 --- a/ras-agent/src/application/run_step.rs +++ b/ras-agent/src/application/run_step.rs @@ -15,6 +15,7 @@ use crate::application::compute_action_hash::compute_action_hash; use crate::application::detect_loop::{build_budget_warning, build_loop_nudge}; use crate::application::fallback_llm::should_switch_to_fallback; use crate::application::parse_output::parse_agent_output; +use crate::application::run_step_log::{log_action_err, log_action_ok, log_decision}; use crate::domain::agent_history::StepRecord; use crate::domain::loop_detector::ActionLoopDetector; use crate::domain::step_metadata::StepMetadata; @@ -65,6 +66,7 @@ impl RunStep { } let response = self.invoke_with_fallback(messages).await?; let output = parse_agent_output(&response)?; + log_decision(step.0, &output); let target = self.browser.focused_target().await.ok(); let page_url = match &target { @@ -110,12 +112,14 @@ impl RunStep { let terminates = reg.metadata.terminates_sequence; let is_done = r.is_done; let is_err = r.is_error(); + log_action_ok(step.0, action, &r); results.push(r); if terminates || is_done || is_err { break; } } Err(e) => { + log_action_err(step.0, action, &e.to_string()); results.push(ActionResult::err(e.to_string())); break; } diff --git a/ras-agent/src/application/run_step_log.rs b/ras-agent/src/application/run_step_log.rs new file mode 100644 index 0000000..cc5b90b --- /dev/null +++ b/ras-agent/src/application/run_step_log.rs @@ -0,0 +1,47 @@ +use ras_types::ActionResult; + +use crate::domain::agent_output::{ActionInvocation, AgentOutput}; + +pub(crate) fn log_decision(step: u32, output: &AgentOutput) { + tracing::info!( + target: "ras_agent::brain", + step = step, + eval = %output.current_state.evaluation_previous_goal, + memory = %output.current_state.memory, + next_goal = %output.current_state.next_goal, + actions = ?output.action.iter().map(|a| a.name.0.as_str()).collect::>(), + "agent decision" + ); + for a in &output.action { + tracing::debug!( + target: "ras_agent::brain", + step = step, + action = %a.name.0, + params = %a.parameters, + "agent action params" + ); + } +} + +pub(crate) fn log_action_ok(step: u32, action: &ActionInvocation, r: &ActionResult) { + tracing::info!( + target: "ras_agent::brain", + step = step, + action = %action.name.0, + ok = r.error.is_none(), + done = r.is_done, + result = %r.extracted_content.as_deref().unwrap_or(""), + error = %r.error.as_deref().unwrap_or(""), + "action result" + ); +} + +pub(crate) fn log_action_err(step: u32, action: &ActionInvocation, err: &str) { + tracing::warn!( + target: "ras_agent::brain", + step = step, + action = %action.name.0, + error = err, + "action failed" + ); +} diff --git a/ras-llm-openai/src/infrastructure/http/chat_openai_compatible.rs b/ras-llm-openai/src/infrastructure/http/chat_openai_compatible.rs index 6154ad4..86a51e7 100644 --- a/ras-llm-openai/src/infrastructure/http/chat_openai_compatible.rs +++ b/ras-llm-openai/src/infrastructure/http/chat_openai_compatible.rs @@ -3,7 +3,7 @@ use ras_errors::AppError; use ras_llm::{ChatMessage, ChatResponse, InvokeOptions, LlmClient, ProviderName}; use reqwest::Client; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; -use tracing::debug; +use tracing::{debug, trace}; use crate::infrastructure::http::dto::{ ChatCompletionResponse, ChatRequest, response_to_chat, to_dto_messages, @@ -106,6 +106,12 @@ impl LlmClient for ChatOpenAICompatible { } } debug!(provider = %self.provider, model = %self.model, "chat post"); + if tracing::enabled!(tracing::Level::TRACE) { + match serde_json::to_string(&req) { + Ok(s) => trace!(provider = %self.provider, body = %s, "llm request"), + Err(e) => trace!(provider = %self.provider, err = %e, "llm request serialize"), + } + } let resp = self .client .post(&url) @@ -119,6 +125,9 @@ impl LlmClient for ChatOpenAICompatible { .text() .await .map_err(|e| AppError::LlmProviderError(format!("{} body: {e}", self.provider)))?; + if tracing::enabled!(tracing::Level::TRACE) { + trace!(provider = %self.provider, status = %status, body = %body, "llm response"); + } if !status.is_success() { return Err(map_status(status, &body)); } From 18014f009352dc560985ffe46d1e97dcbce64dfb Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 16 May 2026 04:45:04 +0700 Subject: [PATCH 4/4] chore(scripts): exclude examples/tests/xtask when staged without parent path The pre-commit guard scripts skip `*/tests/*`, `*/examples/*`, `*/xtask/*`, but the glob requires a parent directory component. When lefthook passes relative paths like `examples/foo.rs` (no leading `*/`), the exclusion fails and the guard incorrectly checks throwaway PoCs. Add the bare-path alternatives (`tests/*`, `examples/*`, `xtask/*`) to the case arms so both forms match. Applied across check-loc.sh, check-no-comments.sh, check-no-unwrap.sh. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/Cargo.lock | 711 +++++-------------------- examples/Cargo.toml | 18 + examples/mouse_drag_smoke.rs | 263 +++++++++ examples/pedidosya_perimeterx_smoke.rs | 185 +++++++ examples/pedidosya_px_solver.rs | 304 +++++++++++ examples/youtube_search_claude_code.rs | 96 ++++ scripts/check-loc.sh | 5 +- scripts/check-no-comments.sh | 6 +- scripts/check-no-unwrap.sh | 6 +- 9 files changed, 1013 insertions(+), 581 deletions(-) create mode 100644 examples/mouse_drag_smoke.rs create mode 100644 examples/pedidosya_perimeterx_smoke.rs create mode 100644 examples/pedidosya_px_solver.rs create mode 100644 examples/youtube_search_claude_code.rs diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 3f7c403..034f2b4 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -32,179 +32,18 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel 2.5.0", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.1", - "futures-lite", - "rustix 1.1.4", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.4", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -213,17 +52,19 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] name = "async-tungstenite" -version = "0.25.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +checksum = "8acc405d38be14342132609f06f02acaf825ddccfe76c4824a69281e0458ebd4" dependencies = [ - "async-std", + "atomic-waker", + "futures-core", "futures-io", + "futures-task", "futures-util", "log", "pin-project-lite", @@ -264,19 +105,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "borsh" version = "1.6.1" @@ -299,12 +127,6 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "byteorder-lite" version = "0.1.0" @@ -344,15 +166,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chromiumoxide" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601bde44842e2875fff5cbf7229a2e9d690b0788cb7b3caa21533b93e6e1bd56" +checksum = "26ed067eb6c1f660bdb87c05efb964421d2ca262bae0296cdfe38cf0cd949a3e" dependencies = [ - "async-std", "async-tungstenite", "base64", "bytes", - "cfg-if", "chromiumoxide_cdp", "chromiumoxide_types", "dunce", @@ -360,22 +180,22 @@ dependencies = [ "futures", "futures-timer", "pin-project-lite", - "reqwest", + "reqwest 0.13.3", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "tokio", "tracing", "url", "which", - "winreg", + "windows-registry", ] [[package]] name = "chromiumoxide_cdp" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978f47e0ca49c6d113ea55fffaabb21bfed4f7494c10bfbaae772043416e066" +checksum = "68a6a03a7ebac4ea85308f285d6959a3e6b2ce32a0c9465dc7a7b1db0144eec7" dependencies = [ "chromiumoxide_pdl", "chromiumoxide_types", @@ -385,26 +205,25 @@ dependencies = [ [[package]] name = "chromiumoxide_pdl" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e9aa35ba6bb637e9c169afe9d7774d71871f2d5f4253cfddef6c64aa2f28e6" +checksum = "c602dea92337bc4d824668d78c5b79c3b4ddb29b40dd7218282bbe8fd3fc2091" dependencies = [ "chromiumoxide_types", "either", - "heck 0.4.1", + "heck", "once_cell", "proc-macro2", "quote", "regex", - "serde", "serde_json", ] [[package]] name = "chromiumoxide_types" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c6ef4b8f990b1c2258c5f89bbdf785b4382fa2742db7952da2e2047154a827" +checksum = "678d5146e74f16fc4a41978b275af572cd913de1f10270d2b93b6c276bc57d80" dependencies = [ "serde", "serde_json", @@ -507,7 +326,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -550,12 +369,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -573,16 +386,10 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener", "pin-project-lite", ] -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "fdeflate" version = "0.3.7" @@ -677,19 +484,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.32" @@ -698,7 +492,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -786,18 +580,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -813,33 +595,12 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "http" version = "1.4.0" @@ -1131,15 +892,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1158,18 +910,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" @@ -1190,9 +930,6 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] [[package]] name = "lru-slab" @@ -1311,23 +1048,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "png" version = "0.18.1" @@ -1341,20 +1061,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -1380,7 +1086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn", ] [[package]] @@ -1412,7 +1118,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -1427,13 +1133,13 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.4", + "rand", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1474,35 +1180,14 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -1512,16 +1197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -1535,7 +1211,7 @@ dependencies = [ [[package]] name = "ras-agent" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "chrono", @@ -1557,7 +1233,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1567,7 +1243,7 @@ dependencies = [ [[package]] name = "ras-browser" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-cdp", @@ -1577,7 +1253,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1585,7 +1261,7 @@ dependencies = [ [[package]] name = "ras-cdp" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1595,7 +1271,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1604,16 +1280,16 @@ dependencies = [ [[package]] name = "ras-cosmium" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-cdp", "ras-errors", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1621,7 +1297,7 @@ dependencies = [ [[package]] name = "ras-dom" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1633,7 +1309,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1641,15 +1317,15 @@ dependencies = [ [[package]] name = "ras-errors" -version = "2.6.4" +version = "2.8.0" dependencies = [ "serde", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "ras-events" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-broadcast", "async-trait", @@ -1658,7 +1334,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1681,6 +1357,8 @@ dependencies = [ "ras-llm-anthropic", "ras-llm-openrouter", "ras-tools", + "ras-types", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -1689,7 +1367,7 @@ dependencies = [ [[package]] name = "ras-filesystem" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-errors", @@ -1697,13 +1375,13 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", ] [[package]] name = "ras-judge" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "image", @@ -1712,13 +1390,13 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "url", ] [[package]] name = "ras-llm" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-errors", @@ -1726,13 +1404,13 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "url", ] [[package]] name = "ras-llm-anthropic" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "chrono", @@ -1741,11 +1419,11 @@ dependencies = [ "ras-llm", "ras-types", "regex", - "reqwest", + "reqwest 0.12.28", "security-framework", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1753,16 +1431,16 @@ dependencies = [ [[package]] name = "ras-llm-openai" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1770,17 +1448,17 @@ dependencies = [ [[package]] name = "ras-llm-openrouter" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-errors", "ras-llm", "ras-llm-openai", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1788,21 +1466,21 @@ dependencies = [ [[package]] name = "ras-telemetry" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-errors", "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] [[package]] name = "ras-tokens" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "chrono", @@ -1810,17 +1488,17 @@ dependencies = [ "ras-errors", "ras-llm", "ras-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] [[package]] name = "ras-tools" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "indexmap", @@ -1833,7 +1511,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -1841,7 +1519,7 @@ dependencies = [ [[package]] name = "ras-types" -version = "2.6.4" +version = "2.8.0" dependencies = [ "chrono", "indexmap", @@ -1850,14 +1528,14 @@ dependencies = [ "serde", "serde_json", "smol_str", - "thiserror 2.0.18", + "thiserror", "url", "uuid", ] [[package]] name = "ras-watchdogs" -version = "2.6.4" +version = "2.8.0" dependencies = [ "async-trait", "ras-browser", @@ -1867,7 +1545,7 @@ dependencies = [ "ras-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1953,6 +1631,35 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -1973,32 +1680,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.23.40" @@ -2067,7 +1748,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn", ] [[package]] @@ -2132,7 +1813,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2143,7 +1824,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2268,17 +1949,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -2307,16 +1977,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", + "syn", ] [[package]] @@ -2325,18 +1986,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "thiserror-impl", ] [[package]] @@ -2347,7 +1997,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2409,7 +2059,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2500,7 +2150,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2550,20 +2200,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.8.6", + "rand", "sha1", - "thiserror 1.0.69", - "url", + "thiserror", "utf-8", ] @@ -2634,12 +2282,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "version_check" version = "0.9.5" @@ -2721,7 +2363,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -2812,14 +2454,11 @@ dependencies = [ [[package]] name = "which" -version = "4.4.2" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "libc", ] [[package]] @@ -2843,7 +2482,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2854,7 +2493,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2863,6 +2502,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2881,15 +2531,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2899,15 +2540,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -2926,21 +2558,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -2974,12 +2591,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2992,12 +2603,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3010,12 +2615,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3040,12 +2639,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3058,12 +2651,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3076,12 +2663,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3094,12 +2675,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3112,16 +2687,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.46.0" @@ -3144,7 +2709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -3155,10 +2720,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3174,7 +2739,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3241,7 +2806,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -3262,7 +2827,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3282,7 +2847,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -3322,7 +2887,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 51148b0..869230e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -14,8 +14,25 @@ path = "claude_code_oauth_cosmium.rs" name = "youtube_search_smoke" path = "youtube_search_smoke.rs" +[[example]] +name = "youtube_search_claude_code" +path = "youtube_search_claude_code.rs" + +[[example]] +name = "pedidosya_perimeterx_smoke" +path = "pedidosya_perimeterx_smoke.rs" + +[[example]] +name = "mouse_drag_smoke" +path = "mouse_drag_smoke.rs" + +[[example]] +name = "pedidosya_px_solver" +path = "pedidosya_px_solver.rs" + [dependencies] anyhow = "1" +serde_json = "1" tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } @@ -31,3 +48,4 @@ ras-llm = { path = "../ras-llm" } ras-llm-anthropic = { path = "../ras-llm-anthropic", features = ["claude-code"] } ras-llm-openrouter = { path = "../ras-llm-openrouter" } ras-tools = { path = "../ras-tools" } +ras-types = { path = "../ras-types" } diff --git a/examples/mouse_drag_smoke.rs b/examples/mouse_drag_smoke.rs new file mode 100644 index 0000000..68dcd00 --- /dev/null +++ b/examples/mouse_drag_smoke.rs @@ -0,0 +1,263 @@ +// Drives CDP directly (no LLM) to prove that mouse_down + mouse_move(buttons=1) +// + mouse_up actually produce a drag the page can observe. +// +// Target: https://www.autodraw.com/ — Google's ML drawing tool. After +// clicking "Start Drawing", the viewport becomes one big `` that +// records pointer trails. If our CDP synthetic drag works, the canvas pixel +// content will go from blank to drawn-on; a screenshot is written to +// /tmp/mouse_drag_smoke.png. + +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result, bail}; +use ras_cdp::BrowserPort; +use ras_cdp::domain::repository::ScreenshotFormat; +use ras_cdp::infrastructure::chromiumoxide_adapter::ChromiumoxideAdapter; +use url::Url; + +const TARGET_URL: &str = "https://www.autodraw.com/"; + +// JS: click the "Start Drawing" / splash button if it's still visible. +// AutoDraw shows a landing splash with a big "Start Drawing" call-to-action; +// after click, the canvas takes the viewport. Idempotent — does nothing if +// already past the splash. +const ENTER_DRAW_MODE_JS: &str = r#"(function () { + var btn = Array.from(document.querySelectorAll('button, a, div, span')) + .find(function (n) { + var t = (n.textContent || '').trim(); + return /^start\s+drawing\s*\.?$/i.test(t) && t.length < 40; + }); + if (btn) { + btn.click(); + return 'clicked'; + } + return 'no_splash'; +})()"#; + +// JS: return the largest visible bbox. AutoDraw uses a single fullscreen +// canvas once you enter draw mode. +const FIND_CANVAS_JS: &str = r#"(function () { + var best = null; + var canvases = document.querySelectorAll('canvas'); + for (var i = 0; i < canvases.length; i++) { + var c = canvases[i]; + var r = c.getBoundingClientRect(); + if (r.width < 100 || r.height < 100) continue; + if (!best || r.width * r.height > best.w * best.h) { + best = { x: r.left, y: r.top, w: r.width, h: r.height }; + } + } + return best; +})()"#; + +// JS: count how many non-background pixels exist in the largest canvas. The +// AutoDraw canvas paints strokes in dark gray (~rgb(60,60,60)) on white. We +// sample every 4th pixel and tally any that aren't near-white. Returns +// { sampled, marks, total_pixels }. +const COUNT_INK_JS: &str = r#"(function () { + var best = null; + var canvases = document.querySelectorAll('canvas'); + for (var i = 0; i < canvases.length; i++) { + var c = canvases[i]; + var r = c.getBoundingClientRect(); + if (r.width < 100 || r.height < 100) continue; + if (!best || r.width * r.height > best.w * best.h) best = c; + } + if (!best) return { error: 'no canvas' }; + try { + var ctx = best.getContext('2d'); + if (!ctx) return { error: 'no 2d context' }; + var w = best.width, h = best.height; + // sample only a 400x400 window in the center for speed + var sw = Math.min(400, w), sh = Math.min(400, h); + var sx = Math.max(0, Math.floor((w - sw) / 2)); + var sy = Math.max(0, Math.floor((h - sh) / 2)); + var img = ctx.getImageData(sx, sy, sw, sh).data; + var sampled = 0, marks = 0; + for (var i = 0; i < img.length; i += 16) { // every 4th pixel + var r2 = img[i], g2 = img[i+1], b2 = img[i+2], a2 = img[i+3]; + if (a2 < 8) { sampled++; continue; } + sampled++; + // anything noticeably darker than near-white counts as ink + if (r2 < 230 || g2 < 230 || b2 < 230) marks++; + } + return { sampled: sampled, marks: marks, canvas_w: w, canvas_h: h }; + } catch (e) { + return { error: String(e && e.message || e) }; + } +})()"#; + +#[tokio::main] +async fn main() -> Result<()> { + let mut p = std::env::current_dir()?; + loop { + let candidate = p.join(".env"); + if candidate.exists() { + let _ = dotenvy::from_path(&candidate); + break; + } + if !p.pop() { + break; + } + } + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let cdp_url: Url = std::env::var("CDP_URL") + .unwrap_or_else(|_| "http://127.0.0.1:9222".into()) + .parse()?; + let ws = ras_cosmium::infrastructure::attach::resolve_attach_url(&cdp_url).await?; + let adapter = ChromiumoxideAdapter::connect(ws, Duration::from_secs(60)).await?; + let browser: Arc = Arc::new(adapter); + + let target = match browser.focused_target().await { + Ok(t) => t, + Err(_) => { + eprintln!("[smoke] no existing tab — creating one"); + browser + .create_target(&Url::parse("about:blank")?) + .await + .context("create new tab")? + } + }; + + browser + .navigate(&target, &Url::parse(TARGET_URL)?) + .await + .context("navigate autodraw")?; + eprintln!("[smoke] navigated to {TARGET_URL}"); + tokio::time::sleep(Duration::from_secs(2)).await; + + // dismiss the splash if it's there + let splash = browser + .evaluate(&target, ENTER_DRAW_MODE_JS) + .await + .unwrap_or(serde_json::Value::Null); + eprintln!( + "[smoke] splash: {}", + splash.as_str().unwrap_or("unknown") + ); + tokio::time::sleep(Duration::from_millis(800)).await; + + // locate the drawing canvas + let mut canvas: Option<(f64, f64, f64, f64)> = None; + for attempt in 0..20_i32 { + tokio::time::sleep(Duration::from_millis(500)).await; + let v = browser.evaluate(&target, FIND_CANVAS_JS).await?; + if let Some(obj) = v.as_object() { + let x = obj.get("x").and_then(|n| n.as_f64()); + let y = obj.get("y").and_then(|n| n.as_f64()); + let w = obj.get("w").and_then(|n| n.as_f64()); + let h = obj.get("h").and_then(|n| n.as_f64()); + if let (Some(x), Some(y), Some(w), Some(h)) = (x, y, w, h) { + eprintln!( + "[smoke] canvas found @{}ms: {:.0}x{:.0} at ({:.0},{:.0})", + attempt * 500, + w, + h, + x, + y + ); + canvas = Some((x, y, w, h)); + break; + } + } + } + let (cx, cy, cw, ch) = canvas.context("AutoDraw canvas not found after 10s")?; + + // baseline ink count BEFORE drawing + let baseline = browser.evaluate(&target, COUNT_INK_JS).await?; + let baseline_marks = baseline + .as_object() + .and_then(|o| o.get("marks").and_then(|n| n.as_i64())) + .unwrap_or(-1); + eprintln!("[smoke] baseline ink marks (before drag): {baseline_marks}"); + + // draw a zig-zag inside the canvas centered around the middle, well away + // from the left toolbar (~80px wide) and top app-bar. + let pad_left = 120.0_f64; + let pad_top = 80.0_f64; + let pad_right = 40.0_f64; + let pad_bot = 40.0_f64; + let usable_x0 = cx + pad_left; + let usable_y0 = cy + pad_top; + let usable_w = (cw - pad_left - pad_right).max(200.0); + let usable_h = (ch - pad_top - pad_bot).max(200.0); + + let center_x = usable_x0 + usable_w / 2.0; + let center_y = usable_y0 + usable_h / 2.0; + let radius_x = (usable_w * 0.30).min(220.0); + let radius_y = (usable_h * 0.30).min(220.0); + let steps: i32 = 80; + let start_x = (center_x + radius_x).round() as i32; + let start_y = center_y.round() as i32; + + eprintln!( + "[smoke] drawing circle center=({:.0},{:.0}) r=({:.0},{:.0}) steps={steps}", + center_x, center_y, radius_x, radius_y + ); + + // approach without button so canvas sees natural pointer movement first + for i in 1..=6_i32 { + let t = f64::from(i) / 6.0; + let x = (center_x + radius_x * 1.4 - radius_x * 0.4 * t).round() as i32; + let y = (center_y - 30.0 + 30.0 * t).round() as i32; + browser.mouse_move(&target, x, y, 0).await?; + tokio::time::sleep(Duration::from_millis(15)).await; + } + browser.mouse_down(&target, start_x, start_y).await?; + // trace a full ellipse + for i in 1..=steps { + let theta = (f64::from(i) / f64::from(steps)) * std::f64::consts::TAU; + let x = (center_x + radius_x * theta.cos()).round() as i32; + let y = (center_y + radius_y * theta.sin()).round() as i32; + browser.mouse_move(&target, x, y, 1).await?; + tokio::time::sleep(Duration::from_millis(18)).await; + } + browser.mouse_up(&target, start_x, start_y).await?; + + // settle + read pixel delta + tokio::time::sleep(Duration::from_millis(700)).await; + let after = browser.evaluate(&target, COUNT_INK_JS).await?; + let after_marks = after + .as_object() + .and_then(|o| o.get("marks").and_then(|n| n.as_i64())) + .unwrap_or(-1); + let sampled = after + .as_object() + .and_then(|o| o.get("sampled").and_then(|n| n.as_i64())) + .unwrap_or(-1); + let canvas_pixels = after + .as_object() + .and_then(|o| { + let w = o.get("canvas_w").and_then(|n| n.as_i64())?; + let h = o.get("canvas_h").and_then(|n| n.as_i64())?; + Some((w, h)) + }); + eprintln!( + "[smoke] ink AFTER drag: marks={after_marks} sampled={sampled} canvas_pixels={canvas_pixels:?}" + ); + + // screenshot for visual confirmation + let png = browser.screenshot(&target, ScreenshotFormat::Png).await?; + let out = "/tmp/mouse_drag_smoke.png"; + std::fs::write(out, &png).context("write screenshot")?; + eprintln!("[smoke] screenshot saved -> {out} ({} bytes)", png.len()); + + let delta = if baseline_marks >= 0 && after_marks >= 0 { + after_marks - baseline_marks + } else { + after_marks + }; + eprintln!("[smoke] ink delta: {delta} marks"); + + if delta < 50 { + bail!( + "drag did NOT mark the canvas (delta={delta}, baseline={baseline_marks}, after={after_marks}) — synthetic drag pipeline broken" + ); + } + eprintln!("[smoke] PASS (canvas inked +{delta} marks; check {out})"); + Ok(()) +} diff --git a/examples/pedidosya_perimeterx_smoke.rs b/examples/pedidosya_perimeterx_smoke.rs new file mode 100644 index 0000000..d3eb8e0 --- /dev/null +++ b/examples/pedidosya_perimeterx_smoke.rs @@ -0,0 +1,185 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result, bail}; +use ras_agent::application::run_agent::RunAgent; +use ras_cdp::BrowserPort; +use ras_cdp::infrastructure::chromiumoxide_adapter::ChromiumoxideAdapter; +use ras_dom::{ChromiumoxideDomExtractor, DomExtractor}; +use ras_events::{BroadcastBus, EventBus}; +use ras_llm::LlmClient; +use ras_llm_openrouter::{ChatOpenAICompatible, OpenAiAuth}; +use ras_tools::domain::registry::ActionRegistry; +use ras_tools::infrastructure::builtin::register::register_default_actions; +use url::Url; + +const TASK: &str = r#" +You are a browsing agent on pedidosya.com.ar (Argentine food delivery). +The site is protected by PerimeterX / HUMAN Security bot detection. + +Two distinct PX outcomes — you MUST distinguish them by the PRESENCE OR +ABSENCE OF A PRESS-AND-HOLD BUTTON, NOT by surrounding text: + (A) Solvable challenge — page shows a button labelled "Pulsar y + mantener pulsado" / "Press & Hold" / "Mantén presionado". Even + when the page ALSO says "Acceso ha sido denegado" or "Por favor + confirma que eres un humano", the presence of the button means + it IS solvable. Use the press_and_hold tools below. + (B) Hard deny — no interactive button anywhere on the page; just + static text and maybe a reference id. THIS is unsolvable. + Only conclude (B) after a screenshot confirms NO press-and-hold + button is present. + +Tools to know about: + - press_and_hold_element { index, ms? } — resolves the bbox center + of a clickable in the DOM snapshot and dispatches a humanized CDP + press (approach + press + jitter + release). ms default 12000, max + 60000. Use this when the hold button IS in the clickable list. + - press_and_hold_coordinate { x, y, ms? } — same gesture at raw pixels. + Use this when the hold button is in a cross-origin iframe and the + clickable list does NOT show it; first take a screenshot, identify + the button center in pixels, then call this tool. + - PerimeterX usually needs 15–25 seconds of sustained press. Try + ms=20000 first. If still blocked, ms=30000, then ms=45000. + +Your task: +1. Navigate to https://www.pedidosya.com.ar/ +2. CRITICAL: After navigate, your FIRST action MUST be wait with + seconds=5. The PerimeterX widget loads its JavaScript over ~3 seconds + and renders the button slightly later. A blank/grey area where the + button should be is NOT proof of hard deny — it means the widget + hasn't painted yet. NEVER call done="px_hard_deny" without first + doing wait(5) and then re-inspecting. +3. After the wait, take a screenshot and inspect the page: + - If the screenshot shows the normal PedidosYa homepage (city + selector, cuisines, search box) → call done with text="ok: cleared". + - If you see ANY button-shaped element labelled "Pulsar y mantener + pulsado" / "Press & Hold" / "Mantén presionado" anywhere on the + page (even when the page ALSO says "Acceso ha sido denegado" or + "Por favor confirma que eres un humano") → proceed to step 4. + The button is usually a rounded pill in the center of the page. + - If the screenshot STILL shows just text and a grey/empty area, + do ANOTHER wait(5) before deciding. Only conclude px_hard_deny + after at least 10 seconds total of waiting AND a clear screenshot + with no button-shaped element anywhere. +4. Solve the Press & Hold challenge: + a. First try press_and_hold_element: scan the clickable list for + labels containing "Press", "Hold", "Pulsar", "Mantener", + "Mantén". If found, call press_and_hold_element with that + index and ms=20000. + b. If the button is NOT in the clickable list (often happens + because PX renders inside an iframe), fall back: identify the + button center coordinates from the screenshot (it sits near the + visual center of the page), call press_and_hold_coordinate with + those (x, y) and ms=20000. + c. wait 4 seconds, then take another screenshot. + d. If still blocked, retry the same hold tool with ms=30000. + e. If still blocked, retry once more with ms=45000. + f. If still blocked after three attempts, call done with + text="px_blocked: hold rejected after 3 attempts". +5. Hard ceiling: do NOT exceed 18 steps. +"#; + +#[tokio::main] +async fn main() -> Result<()> { + let mut p = std::env::current_dir()?; + loop { + let candidate = p.join(".env"); + if candidate.exists() { + let _ = dotenvy::from_path(&candidate); + break; + } + if !p.pop() { + break; + } + } + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let api_key = std::env::var("OPENROUTER_API_KEY") + .context("OPENROUTER_API_KEY not set in env or .env")?; + let model = + std::env::var("RAS_MODEL").unwrap_or_else(|_| "google/gemini-2.5-flash".into()); + let cdp_url: Url = std::env::var("CDP_URL") + .unwrap_or_else(|_| "http://127.0.0.1:9222".into()) + .parse()?; + + let mut inner = ChatOpenAICompatible::new( + "openrouter", + model.clone(), + "https://openrouter.ai/api", + OpenAiAuth::Bearer(api_key), + )?; + inner.extra_headers = vec![ + ( + "HTTP-Referer".into(), + "https://github.com/KeyCode17/rust-ai-surfer".into(), + ), + ("X-Title".into(), "pedidosya_perimeterx_smoke".into()), + ]; + let llm: Arc = Arc::new(inner); + + let ws = ras_cosmium::infrastructure::attach::resolve_attach_url(&cdp_url).await?; + let adapter = ChromiumoxideAdapter::connect(ws, Duration::from_secs(60)).await?; + let browser_arc = adapter.browser_arc(); + let browser: Arc = Arc::new(adapter); + let extractor: Arc = Arc::new(ChromiumoxideDomExtractor::new( + browser_arc, + Duration::from_secs(30), + )); + + let mut registry = ActionRegistry::new(); + register_default_actions(&mut registry)?; + let registry = Arc::new(registry); + let events: Arc = Arc::new(BroadcastBus::default()); + + let history = RunAgent::new(TASK.to_string(), llm, registry, browser.clone(), events) + .with_max_steps(20) + .with_dom_extractor(extractor) + .execute() + .await?; + + let final_text = history.final_result().unwrap_or_default().to_string(); + let target = browser.focused_target().await?; + let url_value = browser.evaluate(&target, "location.href").await?; + let final_url = url_value.as_str().unwrap_or_default().to_string(); + let title_value = browser.evaluate(&target, "document.title").await?; + let final_title = title_value.as_str().unwrap_or_default().to_string(); + let px_probe = browser + .evaluate( + &target, + r#"(function(){ + var html = document.documentElement ? document.documentElement.innerHTML : ''; + var hit = /px-captcha|_pxCaptcha|Press\s*&\s*Hold|Mant[eé]n\s*presionado|Access\s*to\s*this\s*page\s*has\s*been\s*denied/i.test(html); + return hit ? 'px_present' : 'px_clear'; + })()"#, + ) + .await?; + let px_state = px_probe.as_str().unwrap_or("unknown").to_string(); + + eprintln!("\n[smoke] agent done text : {final_text}"); + eprintln!("[smoke] final url : {final_url}"); + eprintln!("[smoke] final title : {final_title}"); + eprintln!("[smoke] px probe : {px_state}"); + + let host_ok = final_url.contains("pedidosya.com.ar"); + let cleared = px_state == "px_clear" && !final_title.is_empty(); + + if !host_ok { + bail!( + "pedidosya run failed: never reached pedidosya.com.ar\n url={final_url}\n done={final_text}" + ); + } + if !cleared { + eprintln!( + "[smoke] WARN: PerimeterX challenge still present after agent run.\n\ + This is expected: the built-in click action cannot perform a sustained\n\ + press-and-hold mouse gesture, which is what most PX challenges require." + ); + bail!( + "px_blocked: challenge not cleared\n url={final_url}\n title={final_title}\n done={final_text}" + ); + } + eprintln!("[smoke] PASS"); + Ok(()) +} diff --git a/examples/pedidosya_px_solver.rs b/examples/pedidosya_px_solver.rs new file mode 100644 index 0000000..22fc99e --- /dev/null +++ b/examples/pedidosya_px_solver.rs @@ -0,0 +1,304 @@ +// Pure-CDP PerimeterX "Pulsar y mantener pulsado" solver. No LLM involved. +// +// 1. Navigate to pedidosya.com.ar +// 2. Wait for PX widget to paint, then locate the press-and-hold button by +// searching the DOM for its label text in every same-origin frame. +// 3. mouse_hold its bbox center for N ms (default 18000), with humanized +// approach + jittered MouseMoved events during the hold. +// 4. Wait + verify whether the deny page has cleared. +// +// Run: +// cargo run --example pedidosya_px_solver +// +// Optional env: +// PX_HOLD_MS=20000 hold duration (clamped 100..60000) +// PX_RETRIES=3 number of hold attempts before giving up +// CDP_URL=... CDP HTTP endpoint (default 127.0.0.1:9222) + +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result}; +use ras_cdp::BrowserPort; +use ras_cdp::domain::repository::ScreenshotFormat; +use ras_cdp::infrastructure::chromiumoxide_adapter::ChromiumoxideAdapter; +use url::Url; + +const TARGET_URL: &str = "https://www.pedidosya.com.ar/"; + +// The PerimeterX press-and-hold widget lives inside a CLOSED Shadow DOM +// hosted by `
`, with the actual button rendered in an +// iframe inside that shadow root. Closed shadow roots are NOT reachable +// from `querySelectorAll`, `elementFromPoint`, or iframe descent — but the +// shadow host element (`#px-captcha`) IS in the light DOM, and its +// bounding rect tells us exactly where the iframe paints in viewport +// pixels. That's the hold target. +// +// We still also scan for label text + iframe metadata as fallback diagnostics. +const FIND_BUTTON_JS: &str = r#"(function () { + try { + var w = window.innerWidth, h = window.innerHeight; + var host = document.querySelector('#px-captcha') + || document.querySelector('.px-captcha-container'); + if (!host) { + return { ok: false, why: 'no #px-captcha host yet', w: w, h: h }; + } + var r = host.getBoundingClientRect(); + if (!r || r.width < 50) { + return { + ok: false, + why: 'host present but not painted', + w: w, h: h, + host_w: r ? r.width : -1, + host_h: r ? r.height : -1 + }; + } + return { + ok: true, + w: w, h: h, + x: r.left, + y: r.top, + host_w: r.width, + host_h: r.height, + host_id: host.id || '', + host_cls: (host.className || '').toString().slice(0, 80) + }; + } catch (e) { + return { ok: false, why: String(e && e.message || e) }; + } +})()"#; + +// Lightweight probe: does the page currently still look like the PX deny / +// challenge screen? True = still blocked. Used as a "did we clear it" check. +const PX_PROBE_JS: &str = r#"(function(){ + var html = document.documentElement ? document.documentElement.innerHTML : ''; + var titleHit = /Acceso\s+ha\s+sido\s+denegado|Access\s+to\s+this\s+page\s+has\s+been\s+denied/i.test(document.title || ''); + var bodyHit = /Pulsar\s+y\s+mantener|Press\s*&\s*Hold|Mant[eé]n\s+presionado|px-captcha|_pxCaptcha/i.test(html); + return titleHit || bodyHit; +})()"#; + +#[tokio::main] +async fn main() -> Result<()> { + // load .env + let mut p = std::env::current_dir()?; + loop { + let candidate = p.join(".env"); + if candidate.exists() { + let _ = dotenvy::from_path(&candidate); + break; + } + if !p.pop() { + break; + } + } + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let hold_ms: u64 = std::env::var("PX_HOLD_MS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(18_000); + let retries: u32 = std::env::var("PX_RETRIES") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(3); + + let cdp_url: Url = std::env::var("CDP_URL") + .unwrap_or_else(|_| "http://127.0.0.1:9222".into()) + .parse()?; + let ws = ras_cosmium::infrastructure::attach::resolve_attach_url(&cdp_url).await?; + let adapter = ChromiumoxideAdapter::connect(ws, Duration::from_secs(60)).await?; + let browser: Arc = Arc::new(adapter); + + let target = match browser.focused_target().await { + Ok(t) => t, + Err(_) => { + eprintln!("[px] no existing tab — creating one"); + browser + .create_target(&Url::parse("about:blank")?) + .await + .context("create new tab")? + } + }; + + // Clear any prior PerimeterX clearance cookie (`_px3`/`_pxhd`) so every + // run forces a fresh challenge. PX caches successful holds for ~30 min + // and would otherwise let us straight through, hiding regressions. + if let Err(e) = browser + .clear_cookies(&target, "https://www.pedidosya.com.ar") + .await + { + eprintln!("[px] clear_cookies (pre-navigate) failed: {e} (continuing)"); + } else { + eprintln!("[px] cleared pedidosya cookies + storage"); + } + + browser + .navigate(&target, &Url::parse(TARGET_URL)?) + .await + .context("navigate pedidosya")?; + eprintln!("[px] navigated to {TARGET_URL}"); + + let mut attempt: u32 = 0; + loop { + attempt += 1; + if attempt > retries { + eprintln!("[px] all {retries} attempts exhausted"); + break; + } + eprintln!("[px] attempt {attempt}/{retries}: waiting for button to render..."); + + // Wait for PX widget to paint, then look up the `#px-captcha` host + // element. The press-and-hold iframe lives inside its closed shadow + // root; a coordinate-based mouse_down on the host's bbox hit-tests + // straight into the iframe via blink, so we don't need to pierce + // the shadow boundary to trigger the hold. + tokio::time::sleep(Duration::from_secs(4)).await; + let probe = browser.evaluate(&target, FIND_BUTTON_JS).await?; + let vw = probe.get("w").and_then(|n| n.as_f64()).unwrap_or(0.0); + let vh = probe.get("h").and_then(|n| n.as_f64()).unwrap_or(0.0); + let ok = probe.get("ok").and_then(|b| b.as_bool()).unwrap_or(false); + let why = probe.get("why").and_then(|s| s.as_str()).unwrap_or(""); + if !ok { + eprintln!("[px] probe: ok=false why={why} viewport={vw:.0}x{vh:.0}"); + } + let mut button: Option<(f64, f64)> = None; + if ok { + let x = probe.get("x").and_then(|n| n.as_f64()).unwrap_or(0.0); + let y = probe.get("y").and_then(|n| n.as_f64()).unwrap_or(0.0); + let hw = probe.get("host_w").and_then(|n| n.as_f64()).unwrap_or(0.0); + let hh = probe.get("host_h").and_then(|n| n.as_f64()).unwrap_or(0.0); + let hid = probe + .get("host_id") + .and_then(|s| s.as_str()) + .unwrap_or(""); + let hcls = probe + .get("host_cls") + .and_then(|s| s.as_str()) + .unwrap_or(""); + // PX's visible iframe is fixed-height 52px at the TOP of the + // host; the rest of the host's height is the error-alert

+ // BELOW it. Targeting host_h/2 lands at or below the button. + // Bias toward the iframe center: 26px down from host top. + let iframe_h = 52.0_f64.min(hh); + let cx = x + hw / 2.0; + let cy = y + iframe_h / 2.0; + eprintln!( + "[px] PX HOST: id=\"{hid}\" cls=\"{hcls}\" bbox={hw:.0}x{hh:.0} iframe_top_h={iframe_h:.0} hold target=({cx:.0},{cy:.0}) viewport={vw:.0}x{vh:.0}" + ); + button = Some((cx, cy)); + } + if button.is_none() && vw > 0.0 && vh > 0.0 { + let fx = vw / 2.0; + let fy = vh / 2.0 + 70.0; + eprintln!( + "[px] no #px-captcha host yet — falling back to (vp_center + 70): ({fx:.0},{fy:.0})" + ); + button = Some((fx, fy)); + } + + // probe state + let still_blocked = browser + .evaluate(&target, PX_PROBE_JS) + .await? + .as_bool() + .unwrap_or(true); + + if !still_blocked { + eprintln!("[px] page no longer shows PX challenge — CLEARED"); + break; + } + + let (cx, cy) = match button { + Some(b) => b, + None => { + eprintln!("[px] no target coords; still_blocked={still_blocked}"); + if attempt >= retries { + anyhow::bail!("could not derive hold target"); + } + continue; + } + }; + + // small pre-move to look human before hitting it + let _ = browser + .mouse_move(&target, cx as i32 - 80, cy as i32 - 60, 0) + .await; + tokio::time::sleep(Duration::from_millis(150)).await; + + eprintln!( + "[px] HOLDING ({:.0},{:.0}) for {} ms (attempt {}/{})", + cx, cy, hold_ms, attempt, retries + ); + browser + .mouse_hold(&target, cx as i32, cy as i32, hold_ms) + .await + .context("mouse_hold")?; + eprintln!("[px] release fired; settling 5s for redirect..."); + tokio::time::sleep(Duration::from_secs(5)).await; + + // After a successful PX clear the page navigates away, which + // destroys our JS execution context — evaluate then returns + // "Cannot find context with specified id". Treat that as + // "probably cleared" and re-probe a moment later. + let after_blocked = match browser.evaluate(&target, PX_PROBE_JS).await { + Ok(v) => v.as_bool().unwrap_or(true), + Err(e) => { + eprintln!("[px] post-hold evaluate failed ({e}); page likely navigated. Re-checking in 3s..."); + tokio::time::sleep(Duration::from_secs(3)).await; + browser + .evaluate(&target, PX_PROBE_JS) + .await + .map(|v| v.as_bool().unwrap_or(true)) + .unwrap_or(false) + } + }; + if !after_blocked { + eprintln!("[px] CLEARED on attempt {attempt}"); + break; + } + eprintln!("[px] still blocked after attempt {attempt}"); + if attempt >= retries { + break; + } + } + + // final state + screenshot (tolerant of stale context after navigate) + let title = browser + .evaluate(&target, "document.title") + .await + .ok() + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); + let url = browser + .evaluate(&target, "location.href") + .await + .ok() + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); + let still = browser + .evaluate(&target, PX_PROBE_JS) + .await + .map(|v| v.as_bool().unwrap_or(true)) + .unwrap_or(false); + let png = browser.screenshot(&target, ScreenshotFormat::Png).await?; + let out = "/tmp/pedidosya_px_solver.png"; + std::fs::write(out, &png).context("write screenshot")?; + + eprintln!("\n[px] final state:"); + eprintln!("[px] title : {title}"); + eprintln!("[px] url : {url}"); + eprintln!("[px] still_blocked: {still}"); + eprintln!("[px] screenshot : {out}"); + + if still { + anyhow::bail!( + "PX challenge NOT cleared after {} attempt(s) of {}ms hold each", + attempt, + hold_ms + ); + } + eprintln!("[px] PASS"); + Ok(()) +} diff --git a/examples/youtube_search_claude_code.rs b/examples/youtube_search_claude_code.rs new file mode 100644 index 0000000..ab0fa28 --- /dev/null +++ b/examples/youtube_search_claude_code.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result, bail}; +use ras_agent::application::run_agent::RunAgent; +use ras_cdp::BrowserPort; +use ras_cdp::infrastructure::chromiumoxide_adapter::ChromiumoxideAdapter; +use ras_dom::{ChromiumoxideDomExtractor, DomExtractor}; +use ras_events::{BroadcastBus, EventBus}; +use ras_llm::LlmClient; +use ras_llm_anthropic::infrastructure::http::chat_anthropic_claude_code::ChatAnthropicClaudeCode; +use ras_tools::domain::registry::ActionRegistry; +use ras_tools::infrastructure::builtin::register::register_default_actions; +use url::Url; + +const TASK: &str = r#" +You are a browsing agent. Your task: +1. Navigate to https://www.youtube.com/?gl=US&hl=en +2. If a cookie consent banner appears, accept or dismiss it. +3. Click the search input field. Type the exact text: how to backflip +4. Click the search submit button (NOT the URL bar - use the button). +5. Wait for the results page to load. +6. Call done with text="ok" if the final URL is the YouTube results + page with search_query containing 'backflip'. Otherwise call done + with text="failed: ". +"#; + +#[tokio::main] +async fn main() -> Result<()> { + let mut p = std::env::current_dir()?; + loop { + let candidate = p.join(".env"); + if candidate.exists() { + let _ = dotenvy::from_path(&candidate); + break; + } + if !p.pop() { + break; + } + } + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let model = std::env::var("RAS_MODEL").unwrap_or_else(|_| "claude-sonnet-4-5".into()); + let cdp_url: Url = std::env::var("CDP_URL") + .unwrap_or_else(|_| "http://127.0.0.1:9222".into()) + .parse()?; + + let llm: Arc = Arc::new( + ChatAnthropicClaudeCode::new(model.clone()) + .await + .context("Claude Code OAuth resolution failed - run `claude` to log in")?, + ); + eprintln!("[smoke] Claude Code OAuth resolved (model={model})"); + + let ws = ras_cosmium::infrastructure::attach::resolve_attach_url(&cdp_url).await?; + let adapter = ChromiumoxideAdapter::connect(ws, Duration::from_secs(60)).await?; + let browser_arc = adapter.browser_arc(); + let browser: Arc = Arc::new(adapter); + let extractor: Arc = Arc::new(ChromiumoxideDomExtractor::new( + browser_arc, + Duration::from_secs(30), + )); + + let mut registry = ActionRegistry::new(); + register_default_actions(&mut registry)?; + let registry = Arc::new(registry); + let events: Arc = Arc::new(BroadcastBus::default()); + + let history = RunAgent::new(TASK.to_string(), llm, registry, browser.clone(), events) + .with_max_steps(25) + .with_dom_extractor(extractor) + .execute() + .await?; + + let final_text = history.final_result().unwrap_or_default().to_string(); + let target = browser.focused_target().await?; + let url_value = browser.evaluate(&target, "location.href").await?; + let final_url = url_value.as_str().unwrap_or_default().to_string(); + + eprintln!("\n[smoke] agent done text: {final_text}"); + eprintln!("[smoke] final url : {final_url}"); + + let url_ok = final_url.contains("/results") + && final_url.contains("search_query=") + && (final_url.contains("backflip") || final_url.contains("how")); + + if !url_ok { + bail!( + "youtube search failed: url did not reach /results?search_query=*backflip*\n url={final_url}\n done={final_text}" + ); + } + eprintln!("[smoke] PASS"); + Ok(()) +} diff --git a/scripts/check-loc.sh b/scripts/check-loc.sh index d9284b2..3797e96 100755 --- a/scripts/check-loc.sh +++ b/scripts/check-loc.sh @@ -8,8 +8,9 @@ for f in "$@"; do *) continue ;; esac case "$f" in - */tests/*) continue ;; - */examples/*) continue ;; + */tests/*|tests/*) continue ;; + */examples/*|examples/*) continue ;; + */xtask/*|xtask/*) continue ;; esac [ -f "$f" ] || continue lines=$(wc -l < "$f") diff --git a/scripts/check-no-comments.sh b/scripts/check-no-comments.sh index 8891866..f0a64bc 100755 --- a/scripts/check-no-comments.sh +++ b/scripts/check-no-comments.sh @@ -7,9 +7,9 @@ for f in "$@"; do *) continue ;; esac case "$f" in - */tests/*) continue ;; - */examples/*) continue ;; - */xtask/*) continue ;; + */tests/*|tests/*) continue ;; + */examples/*|examples/*) continue ;; + */xtask/*|xtask/*) continue ;; esac [ -f "$f" ] || continue if grep -nE '^[[:space:]]*(//[^/!]|/\*)' "$f" >/dev/null 2>&1; then diff --git a/scripts/check-no-unwrap.sh b/scripts/check-no-unwrap.sh index 2c4b32f..dadf49a 100755 --- a/scripts/check-no-unwrap.sh +++ b/scripts/check-no-unwrap.sh @@ -7,9 +7,9 @@ for f in "$@"; do *) continue ;; esac case "$f" in - */tests/*) continue ;; - */examples/*) continue ;; - */xtask/*) continue ;; + */tests/*|tests/*) continue ;; + */examples/*|examples/*) continue ;; + */xtask/*|xtask/*) continue ;; esac [ -f "$f" ] || continue if grep -nE '\.unwrap\(\)' "$f" >/dev/null 2>&1; then