Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26afbaba6 | ||
|
|
ffb386e3af | ||
|
|
947d150561 |
Generated
+1
-1
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.27.0-fork.7"
|
version = "0.27.0-fork.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.27.0-fork.7"
|
version = "0.27.0-fork.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+74
-52
@@ -485,9 +485,8 @@ async fn verify_click_target(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
// Tight 500ms timeout — this is a defensive guard, not critical path.
|
// Resolve once. backendNodeId is stable across renders; only the
|
||||||
// If it can't run quickly, fall through and let the click proceed
|
// element under (x, y) is what changes when an overlay flickers.
|
||||||
// (we're no worse off than the unguarded code path).
|
|
||||||
let resolve_params = DomResolveNodeParams {
|
let resolve_params = DomResolveNodeParams {
|
||||||
backend_node_id: Some(backend_node_id),
|
backend_node_id: Some(backend_node_id),
|
||||||
node_id: None,
|
node_id: None,
|
||||||
@@ -512,6 +511,35 @@ async fn verify_click_target(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-retry on transient occlusion. Many real-world overlays
|
||||||
|
// (modal backdrops, focus rings, click-outside masks) blink in for
|
||||||
|
// a frame or two during state transitions and clear on their own.
|
||||||
|
// Without retries the user gets an "occluded" error and has to
|
||||||
|
// wrap every click in their own retry loop. With retries the
|
||||||
|
// common case is invisible — only persistent overlays surface.
|
||||||
|
//
|
||||||
|
// AGENT_BROWSER_OCCLUSION_RETRIES (default 3, 0 disables)
|
||||||
|
// AGENT_BROWSER_OCCLUSION_RETRY_DELAY_MS (default 200)
|
||||||
|
let max_retries: u32 = std::env::var("AGENT_BROWSER_OCCLUSION_RETRIES")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(3);
|
||||||
|
let retry_delay_ms: u64 = std::env::var("AGENT_BROWSER_OCCLUSION_RETRY_DELAY_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(200);
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Occluder {
|
||||||
|
tag: Option<String>,
|
||||||
|
testid: Option<String>,
|
||||||
|
role: Option<String>,
|
||||||
|
#[serde(rename = "ariaLabel")]
|
||||||
|
aria_label: Option<String>,
|
||||||
|
text: Option<String>,
|
||||||
|
reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// function(x, y) { ... } where `this` is the target element.
|
// function(x, y) { ... } where `this` is the target element.
|
||||||
// Return null → click is safe.
|
// Return null → click is safe.
|
||||||
// Return JSON → describes the occluding element.
|
// Return JSON → describes the occluding element.
|
||||||
@@ -527,51 +555,46 @@ async fn verify_click_target(
|
|||||||
text: ((at.textContent||'').trim().slice(0, 60)) \
|
text: ((at.textContent||'').trim().slice(0, 60)) \
|
||||||
}); \
|
}); \
|
||||||
}";
|
}";
|
||||||
let call_params = serde_json::json!({
|
|
||||||
"objectId": object_id,
|
|
||||||
"functionDeclaration": function_decl,
|
|
||||||
"arguments": [{"value": x}, {"value": y}],
|
|
||||||
"returnByValue": true,
|
|
||||||
});
|
|
||||||
let call_fut = client.send_command_typed::<_, serde_json::Value>(
|
|
||||||
"Runtime.callFunctionOn",
|
|
||||||
&call_params,
|
|
||||||
Some(session_id),
|
|
||||||
);
|
|
||||||
let Ok(call_resp) =
|
|
||||||
tokio::time::timeout(std::time::Duration::from_millis(500), call_fut).await
|
|
||||||
else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Ok(call_result) = call_resp else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = call_result
|
let mut last_occ: Option<Occluder> = None;
|
||||||
.get("result")
|
for attempt in 0..=max_retries {
|
||||||
.and_then(|r| r.get("value"));
|
if attempt > 0 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(retry_delay_ms)).await;
|
||||||
// null / undefined / missing → safe
|
}
|
||||||
let json_str = match value {
|
let call_params = serde_json::json!({
|
||||||
Some(serde_json::Value::String(s)) => s.clone(),
|
"objectId": object_id,
|
||||||
_ => return Ok(()),
|
"functionDeclaration": function_decl,
|
||||||
};
|
"arguments": [{"value": x}, {"value": y}],
|
||||||
|
"returnByValue": true,
|
||||||
#[derive(Deserialize)]
|
});
|
||||||
struct Occluder {
|
let call_fut = client.send_command_typed::<_, serde_json::Value>(
|
||||||
tag: Option<String>,
|
"Runtime.callFunctionOn",
|
||||||
testid: Option<String>,
|
&call_params,
|
||||||
role: Option<String>,
|
Some(session_id),
|
||||||
#[serde(rename = "ariaLabel")]
|
);
|
||||||
aria_label: Option<String>,
|
let Ok(call_resp) =
|
||||||
text: Option<String>,
|
tokio::time::timeout(std::time::Duration::from_millis(500), call_fut).await
|
||||||
reason: Option<String>,
|
else {
|
||||||
|
return Ok(()); // probe itself stalled — fall through to click
|
||||||
|
};
|
||||||
|
let Ok(call_result) = call_resp else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let value = call_result.get("result").and_then(|r| r.get("value"));
|
||||||
|
let json_str = match value {
|
||||||
|
Some(serde_json::Value::String(s)) => s.clone(),
|
||||||
|
// null / undefined → element at point IS our target. Safe.
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
let occ: Occluder = match serde_json::from_str(&json_str) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
last_occ = Some(occ);
|
||||||
}
|
}
|
||||||
let occ: Occluder = match serde_json::from_str(&json_str) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// All retries exhausted — overlay is sticky. Build the descriptive error.
|
||||||
|
let occ = last_occ.expect("loop ran at least once");
|
||||||
if let Some(reason) = occ.reason {
|
if let Some(reason) = occ.reason {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Ref {} cannot be clicked at its computed position: {}. \
|
"Ref {} cannot be clicked at its computed position: {}. \
|
||||||
@@ -579,7 +602,6 @@ async fn verify_click_target(
|
|||||||
ref_id, reason
|
ref_id, reason
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut desc = occ.tag.unwrap_or_else(|| "unknown".to_string());
|
let mut desc = occ.tag.unwrap_or_else(|| "unknown".to_string());
|
||||||
if let Some(t) = occ.testid {
|
if let Some(t) = occ.testid {
|
||||||
desc.push_str(&format!("[testid={}]", t));
|
desc.push_str(&format!("[testid={}]", t));
|
||||||
@@ -595,13 +617,13 @@ async fn verify_click_target(
|
|||||||
desc.push_str(&format!(" text=\"{}\"", t));
|
desc.push_str(&format!(" text=\"{}\"", t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let waited_ms = (max_retries as u64) * retry_delay_ms;
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Ref {} is occluded by {} at the click point. \
|
"Ref {} is occluded by {} at the click point (still occluded after \
|
||||||
A transient overlay (modal backdrop, mask, sticky banner, etc.) \
|
{} retries / {}ms). A persistent overlay is in the way — \
|
||||||
appeared between snapshot and click. \
|
re-run snapshot, dismiss the overlay, or set \
|
||||||
Wait for it to clear or re-snapshot, then retry. \
|
AGENT_BROWSER_VERIFY_CLICK_TARGET=0 to bypass.",
|
||||||
Set AGENT_BROWSER_VERIFY_CLICK_TARGET=0 to bypass this guard.",
|
ref_id, desc, max_retries, waited_ms,
|
||||||
ref_id, desc
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-13
@@ -20,10 +20,10 @@ services:
|
|||||||
|
|
||||||
# Build both targets in parallel
|
# Build both targets in parallel
|
||||||
(echo "→ Linux x64" && cargo zigbuild --release --target x86_64-unknown-linux-gnu && cp /build/target/x86_64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-x64 && chmod +x /output/agent-browser-linux-x64 && echo "✓ Linux x64 done") &
|
(echo "→ Linux x64" && cargo zigbuild --release --target x86_64-unknown-linux-gnu && cp /build/target/x86_64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-x64 && chmod +x /output/agent-browser-linux-x64 && echo "✓ Linux x64 done") &
|
||||||
PID1=$!
|
PID1=$$!
|
||||||
|
|
||||||
(echo "→ Linux ARM64" && cargo zigbuild --release --target aarch64-unknown-linux-gnu && cp /build/target/aarch64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-arm64 && chmod +x /output/agent-browser-linux-arm64 && echo "✓ Linux ARM64 done") &
|
(echo "→ Linux ARM64" && cargo zigbuild --release --target aarch64-unknown-linux-gnu && cp /build/target/aarch64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-arm64 && chmod +x /output/agent-browser-linux-arm64 && echo "✓ Linux ARM64 done") &
|
||||||
PID2=$!
|
PID2=$$!
|
||||||
|
|
||||||
# Wait for both and check exit codes individually — without this
|
# Wait for both and check exit codes individually — without this
|
||||||
# the outer script exits 0 even if one of the parallel builds
|
# the outer script exits 0 even if one of the parallel builds
|
||||||
@@ -31,8 +31,8 @@ services:
|
|||||||
# previous release. Caused 0.27.0-fork.5 to ship with a stale
|
# previous release. Caused 0.27.0-fork.5 to ship with a stale
|
||||||
# linux-x64 binary at the first publish attempt until caught
|
# linux-x64 binary at the first publish attempt until caught
|
||||||
# manually by checking the embedded version string.
|
# manually by checking the embedded version string.
|
||||||
wait $PID1 || { echo "✗ Linux x64 build failed"; exit 1; }
|
wait $$PID1 || { echo "✗ Linux x64 build failed"; exit 1; }
|
||||||
wait $PID2 || { echo "✗ Linux ARM64 build failed"; exit 1; }
|
wait $$PID2 || { echo "✗ Linux ARM64 build failed"; exit 1; }
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ Linux platforms built successfully!"
|
echo "✓ Linux platforms built successfully!"
|
||||||
@@ -71,16 +71,21 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TARGET=${TARGET:-x86_64-unknown-linux-gnu}
|
- TARGET=${TARGET:-x86_64-unknown-linux-gnu}
|
||||||
- OUTPUT_NAME=${OUTPUT_NAME:-agent-browser-linux-x64}
|
- OUTPUT_NAME=${OUTPUT_NAME:-agent-browser-linux-x64}
|
||||||
|
# NOTE: $$ escapes a literal $ for the in-container shell. A single $ is
|
||||||
|
# interpolated by docker compose at YAML parse time against the *host*
|
||||||
|
# environment, which silently drops script-local variables like SRC
|
||||||
|
# (caused 0.27.0-fork.7 to ship with a stale linux-arm64 binary because
|
||||||
|
# the cp command resolved to `cp "" "/output/"` after compose ate $SRC
|
||||||
|
# and $OUTPUT_NAME). $TARGET / $OUTPUT_NAME are set via `environment:`
|
||||||
|
# below — those are also passed into the container, so $$TARGET and
|
||||||
|
# $$OUTPUT_NAME read them at script time.
|
||||||
command: |
|
command: |
|
||||||
-c '
|
-c '
|
||||||
set -e
|
set -e
|
||||||
cargo zigbuild --release --target $TARGET
|
cargo zigbuild --release --target $$TARGET
|
||||||
# Copy the binary explicitly. The previous `agent-browser*` glob
|
SRC="/build/target/$$TARGET/release/agent-browser"
|
||||||
# matched the binary AND its `.d` dependency file, which made cp
|
if [ -f "$$SRC.exe" ]; then SRC="$$SRC.exe"; fi
|
||||||
# treat the destination as a directory and silently failed.
|
cp "$$SRC" "/output/$$OUTPUT_NAME"
|
||||||
SRC="/build/target/$TARGET/release/agent-browser"
|
chmod +x /output/$$OUTPUT_NAME 2>/dev/null || true
|
||||||
if [ -f "$SRC.exe" ]; then SRC="$SRC.exe"; fi
|
echo "✓ Built $$OUTPUT_NAME"
|
||||||
cp "$SRC" "/output/$OUTPUT_NAME"
|
|
||||||
chmod +x /output/$OUTPUT_NAME 2>/dev/null || true
|
|
||||||
echo "✓ Built $OUTPUT_NAME"
|
|
||||||
'
|
'
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-browser-stealth",
|
"name": "agent-browser-stealth",
|
||||||
"version": "0.27.0-fork.7",
|
"version": "0.27.0-fork.8",
|
||||||
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
|
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Reference in New Issue
Block a user