- Introduce a new Chrome image for the sandbox, enabling VNC access with real Chrome and CDP support (amd64 only). - Update README to reflect the addition of the Chrome image and its build instructions. - Modify build script to include a dedicated build process for the Chrome image. - Enhance .gitignore to exclude the new Chrome plan file. Co-authored-by: Cursor <cursoragent@cursor.com>
275 lines
10 KiB
Python
275 lines
10 KiB
Python
"""
|
|
DuckDuckGo Search Demo — PyAutoGUI OS-level Mouse + Smart Keyboard Fallback
|
|
|
|
Demonstrates anti-detection browser automation inside sandbox-claude-chrome:
|
|
1. Open DuckDuckGo homepage
|
|
2. Click search box with PyAutoGUI (real OS mouse event)
|
|
3. Type query with PyAutoGUI keyboard (auto-fallback to Playwright if needed)
|
|
4. Submit search
|
|
5. Click first search result (PyAutoGUI mouse)
|
|
6. Go back
|
|
7. Click second search result (PyAutoGUI mouse)
|
|
|
|
DuckDuckGo has no IP-based rate limiting or reCAPTCHA, making it ideal for
|
|
demonstrating pure anti-fingerprint capabilities without IP interference.
|
|
|
|
Prerequisites:
|
|
- Running inside sandbox-claude-chrome container
|
|
- DISPLAY=:99 (Xvfb virtual display)
|
|
- VNC optional for live observation (http://localhost:6080)
|
|
|
|
Usage:
|
|
DISPLAY=:99 python3 demo-duckduckgo.py
|
|
"""
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
from playwright_stealth import Stealth
|
|
import pyautogui
|
|
import time
|
|
import random
|
|
import os
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PyAutoGUI config
|
|
# ---------------------------------------------------------------------------
|
|
pyautogui.FAILSAFE = False
|
|
pyautogui.PAUSE = 0.1
|
|
|
|
# Screenshot output directory
|
|
SCREENSHOT_DIR = os.environ.get("SCREENSHOT_DIR", "/workspace")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Human-like helpers
|
|
# ---------------------------------------------------------------------------
|
|
def human_move(x, y):
|
|
"""Move mouse with randomized speed to simulate human behavior."""
|
|
duration = random.uniform(0.4, 0.8)
|
|
pyautogui.moveTo(x, y, duration=duration)
|
|
time.sleep(random.uniform(0.1, 0.3))
|
|
|
|
|
|
def human_click(x, y):
|
|
"""Move to (x, y) then click — mimics a real user click."""
|
|
human_move(x, y)
|
|
time.sleep(random.uniform(0.05, 0.15))
|
|
pyautogui.click()
|
|
time.sleep(random.uniform(0.2, 0.5))
|
|
|
|
|
|
def smart_type(element, text):
|
|
"""Type text with PyAutoGUI first; fallback to Playwright if it didn't land.
|
|
|
|
On native amd64 Linux, PyAutoGUI keyboard works perfectly.
|
|
On ARM Mac (Rosetta 2), X11 keyboard events may not reach Chrome,
|
|
so we detect and automatically fallback to Playwright's type().
|
|
"""
|
|
# Attempt PyAutoGUI keyboard (OS-level X11 events)
|
|
for ch in text:
|
|
pyautogui.press(ch)
|
|
time.sleep(random.uniform(0.05, 0.12))
|
|
time.sleep(0.5)
|
|
|
|
# Verify input landed
|
|
actual = element.input_value()
|
|
if actual and len(actual) >= len(text) * 0.8:
|
|
print(" Keyboard: PyAutoGUI (OS-level) ✓", flush=True)
|
|
return
|
|
|
|
# Fallback: Playwright type()
|
|
print(" PyAutoGUI keyboard didn't land — fallback to Playwright", flush=True)
|
|
element.fill("")
|
|
element.type(text, delay=80)
|
|
actual = element.input_value()
|
|
print(" Keyboard: Playwright fallback — '{}'".format(actual), flush=True)
|
|
|
|
|
|
def find_element(page, selectors, min_width=50, timeout=2000):
|
|
"""Try multiple CSS selectors, return (element, bounding_box) or (None, None)."""
|
|
for selector in selectors:
|
|
try:
|
|
el = page.locator(selector).first
|
|
box = el.bounding_box(timeout=timeout)
|
|
if box and box["width"] >= min_width:
|
|
return el, box
|
|
except Exception:
|
|
continue
|
|
return None, None
|
|
|
|
|
|
def find_results(page, selectors, min_count=2, min_width=80):
|
|
"""Find search result elements with bounding boxes."""
|
|
results = []
|
|
for selector in selectors:
|
|
elements = page.locator(selector).all()
|
|
for el in elements[:10]:
|
|
try:
|
|
box = el.bounding_box(timeout=1000)
|
|
title = el.text_content().strip()
|
|
if (box and box["width"] >= min_width and box["y"] > 0
|
|
and title and len(title) > 5):
|
|
if not any(r["title"] == title for r in results):
|
|
results.append({"box": box, "title": title})
|
|
except Exception:
|
|
pass
|
|
if len(results) >= min_count:
|
|
break
|
|
return results
|
|
|
|
|
|
def screenshot(page, name):
|
|
"""Save screenshot to SCREENSHOT_DIR."""
|
|
path = os.path.join(SCREENSHOT_DIR, name)
|
|
page.screenshot(path=path)
|
|
print(" Screenshot: " + path, flush=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
def main():
|
|
print("=" * 60, flush=True)
|
|
print(" DuckDuckGo Search — PyAutoGUI OS-Level Demo", flush=True)
|
|
print("=" * 60, flush=True)
|
|
|
|
with Stealth().use_sync(sync_playwright()) as p:
|
|
browser = p.chromium.launch(
|
|
channel="chrome",
|
|
headless=False,
|
|
args=[
|
|
"--no-sandbox",
|
|
"--disable-blink-features=AutomationControlled",
|
|
"--disable-dev-shm-usage",
|
|
"--window-size=1920,1080",
|
|
"--window-position=0,0",
|
|
],
|
|
)
|
|
ctx = browser.new_context(
|
|
viewport={"width": 1920, "height": 1080},
|
|
locale="en-US",
|
|
timezone_id="America/New_York",
|
|
)
|
|
page = ctx.new_page()
|
|
|
|
# Inject stealth script
|
|
stealth_path = "/usr/local/share/yao/stealth-init.js"
|
|
if os.path.exists(stealth_path):
|
|
page.add_init_script(open(stealth_path).read())
|
|
|
|
# ---- Step 1: Open DuckDuckGo ----
|
|
print("\n[1/7] Opening DuckDuckGo...", flush=True)
|
|
page.goto("https://duckduckgo.com", timeout=30000)
|
|
page.wait_for_timeout(3000)
|
|
print(" Title: " + page.title(), flush=True)
|
|
screenshot(page, "ddg-01-homepage.png")
|
|
|
|
# ---- Step 2: Find & click search box ----
|
|
print("\n[2/7] Finding search box...", flush=True)
|
|
search_selectors = [
|
|
"input[name=q]", "#searchbox_input",
|
|
"input[type=text]", "input[placeholder*='Search']",
|
|
]
|
|
search_el, box = find_element(page, search_selectors, min_width=100)
|
|
|
|
if not box:
|
|
print(" ⚠ Could not find search box!", flush=True)
|
|
screenshot(page, "ddg-02-no-searchbox.png")
|
|
browser.close()
|
|
return
|
|
|
|
cx = int(box["x"] + box["width"] / 2)
|
|
cy = int(box["y"] + box["height"] / 2)
|
|
print(" [PyAutoGUI] Clicking search box at ({}, {})".format(cx, cy), flush=True)
|
|
human_move(200, 200)
|
|
time.sleep(0.3)
|
|
human_click(cx, cy)
|
|
|
|
# ---- Step 3: Type search query ----
|
|
print("\n[3/7] Typing search query...", flush=True)
|
|
query = "yao app engine github"
|
|
smart_type(search_el, query)
|
|
time.sleep(0.5)
|
|
screenshot(page, "ddg-02-typed.png")
|
|
|
|
# ---- Step 4: Submit search ----
|
|
print("\n[4/7] Submitting search...", flush=True)
|
|
# Use Playwright Enter on the element (reliable cross-platform)
|
|
search_el.press("Enter")
|
|
page.wait_for_timeout(6000)
|
|
print(" URL: " + page.url[:120], flush=True)
|
|
print(" Title: " + page.title()[:80], flush=True)
|
|
screenshot(page, "ddg-03-results.png")
|
|
|
|
# Check if we actually reached results page
|
|
on_results = (
|
|
"q=" in page.url
|
|
or "/search" in page.url
|
|
or page.title() != "DuckDuckGo - Protection. Privacy. Peace of mind."
|
|
)
|
|
if not on_results:
|
|
print(" ⚠ Still on homepage — search may not have submitted", flush=True)
|
|
screenshot(page, "ddg-03-still-homepage.png")
|
|
browser.close()
|
|
return
|
|
|
|
# ---- Step 5: Find results ----
|
|
print("\n[5/7] Finding search results...", flush=True)
|
|
result_selectors = [
|
|
"article h2 a", "a[data-testid='result-title-a']",
|
|
"h2 a[href]", "ol li h2 a", "h2 a",
|
|
]
|
|
results = find_results(page, result_selectors, min_count=3)
|
|
print(" Found {} results".format(len(results)), flush=True)
|
|
for i, r in enumerate(results[:5]):
|
|
print(" [{}] {}".format(i + 1, r["title"][:70]), flush=True)
|
|
|
|
if len(results) < 2:
|
|
print("\n ⚠ Not enough results to click.", flush=True)
|
|
screenshot(page, "ddg-04-no-results.png")
|
|
else:
|
|
# ---- Step 6: Click first result ----
|
|
r1 = results[0]
|
|
rx = int(r1["box"]["x"] + r1["box"]["width"] / 2)
|
|
ry = int(r1["box"]["y"] + r1["box"]["height"] / 2)
|
|
print("\n[6/7] [PyAutoGUI] Clicking result #1 at ({},{})".format(rx, ry), flush=True)
|
|
print(" " + r1["title"][:70], flush=True)
|
|
pyautogui.scroll(-1)
|
|
time.sleep(0.3)
|
|
human_click(rx, ry)
|
|
page.wait_for_timeout(6000)
|
|
print(" Landed: {} | {}".format(page.title()[:50], page.url[:80]), flush=True)
|
|
screenshot(page, "ddg-04-page1.png")
|
|
|
|
# Go back
|
|
print(" [PyAutoGUI] Going back (Alt+Left)...", flush=True)
|
|
pyautogui.hotkey("alt", "Left")
|
|
page.wait_for_timeout(4000)
|
|
|
|
# ---- Step 7: Click second result ----
|
|
results2 = find_results(page, result_selectors, min_count=3)
|
|
if len(results2) >= 2:
|
|
r2 = results2[1]
|
|
rx2 = int(r2["box"]["x"] + r2["box"]["width"] / 2)
|
|
ry2 = int(r2["box"]["y"] + r2["box"]["height"] / 2)
|
|
print("\n[7/7] [PyAutoGUI] Clicking result #2 at ({},{})".format(rx2, ry2), flush=True)
|
|
print(" " + r2["title"][:70], flush=True)
|
|
human_click(rx2, ry2)
|
|
page.wait_for_timeout(6000)
|
|
print(" Landed: {} | {}".format(page.title()[:50], page.url[:80]), flush=True)
|
|
screenshot(page, "ddg-05-page2.png")
|
|
else:
|
|
print("\n[7/7] ⚠ Could not re-find results for second click", flush=True)
|
|
|
|
# Keep browser open for VNC observation
|
|
print("\n" + "=" * 60, flush=True)
|
|
print(" Demo complete! Browser stays open 30s for observation.", flush=True)
|
|
print(" Connect via VNC: http://localhost:6080", flush=True)
|
|
print("=" * 60, flush=True)
|
|
page.wait_for_timeout(30000)
|
|
browser.close()
|
|
|
|
print("Done.", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|