# captchaforge Revised Roadmap v2 — Zero API Cost
**Status:** Post-wafrift-benchmark reassessment. WAF bypass (harder problem, bounty-eligible) solved in months. CAPTCHA solving (easier problem, automation-only) is an engineering project.
**Constraint:** ZERO external API costs. All inference runs locally on available GPUs (RTX 5090, 4090, 3080 Ti). No 2Captcha, no CapSolver, no Anti-Captcha, no cloud LLM APIs.
**Target:** 3-4 months to a pure self-hosted CAPTCHA solver. No research project cop-outs.
---
## ★ North Star: beat browser-use on Stealth Bench V1
**Goal (set 2026-06-05):** significantly beat browser-use's own stealth benchmark
(https://github.com/browser-use/benchmark — `Stealth_Bench_V1.enc`, 71 tasks).
**What the bench actually measures** (from browser-use.com/posts/stealth-benchmark):
71 **real anti-bot-protected sites**, 3 steps each (search → visit → read a link),
an LLM judge (`gemini-2.5-flash`) scoring only **"was the agent blocked?"**. Grouped
by vendor:
| Cloudflare | 23 | 93% |
| PerimeterX | 18 | 81% |
| Datadome | 13 | — |
| Akamai | 8 | 85% |
| reCaptcha | 6 | — |
| Kasada / Shape | 3 | — |
**Control baselines (this is a fingerprint+behavior+network test, not a captcha test):**
headless Chromium **2%**, headful Chromium **50%**. Top provider Browser-Use Cloud **81%**;
bottom half ~40%. Weak providers all leak the *same* fingerprint: "headful Linux Chromium"
= stock Chromium + residential proxy + a CAPTCHA-solver extension.
### Why our stack can win (the categorical advantage)
Every provider in the field uses **Chromium** (patched headless or a fork). guise drives a
**real Firefox over WebDriver BiDi** (`foxdriver`). That is not a cosmetic difference:
1. **Authentic TLS + HTTP/2 fingerprint for free.** A real Firefox emits a genuine
Firefox JA3/JA4 ClientHello and HTTP/2 SETTINGS/priority fingerprint. Cloudflare and
Akamai weight TLS/HTTP2 coherence heavily; patched Chromium routinely has a TLS↔JS
mismatch, a real Firefox never does.
2. **No CDP.** CDP is the single most-fingerprinted automation surface (Runtime.enable
leak, `cdc_` globals, the DevTools WS). BiDi has none of it — a whole class of detectors
is N/A for us.
3. **The residual JS tells are exactly what guise now closes** (this session):
- stealth preload actually applies (was a silent no-op) — `navigator.webdriver` hidden;
- every override reports `[native code]` via a `Function.prototype.toString` proxy (toString tamper tell closed);
- the disguise is **family-coherent** with real Firefox (vendor `""`, Gecko UA,
`productSub 20100101`, no `window.chrome`/`usb`/`PaymentRequest`/…), proven by a
live probe gate that is now **GREEN** (0 criticals, 120/126) on headless Firefox 151;
- human input dispatches **trusted** via BiDi (`isTrusted=true`), not synthetic JS.
### Gap list to actually win (prioritized)
1. **Real measurement harness — ✅ LANDED (first cut).** New bench suite
`bench/src/suites/stealth_gauntlet.rs` + Tier-B catalogue `bench/data/stealth_gauntlet.json`:
drives the FULL disguise (`prepare_page`) **vs bare** (two-pair) against real public
bot-detection / fingerprint scorers and reports, per site, "was the browser judged a bot?"
— the captchaforge analogue of Stealth Bench V1. Deterministic per-site JS judges (not an
LLM); uncertain → `unknown`, never a false pass. IP-independent scorers (areyouheadless,
sannysoft) isolate fingerprint quality; IP-confounded ones (browserscan, Cloudflare
`nowsecure.nl`) are labelled. Run: `cargo run --bin captchaforge-bench -- --suite
stealth-gauntlet --with-network`.
Also landed: the **real 80 Stealth-Bench-V1 sites** decrypted into
`bench/data/stealth_bench_sites.json` (load via `STEALTH_GAUNTLET_CATALOGUE=…`), and
**foxdriver proxy support** (`FoxBrowserConfig.proxy` / `ProxyConfig::from_url`, emits
`network.proxy.*` prefs; bench `--proxy` / `STEALTH_PROXY`). The gauntlet found + drove
fixes for THREE real disguise tells (webdriver `undefined`→`false`; plugins-not-PluginArray
override removed; webdriver also defined on the navigator *instance* = own property `_.has`
detects → prototype-only). **Result: the disguised Firefox now passes BOTH IP-independent
real detectors — areyouheadless + sannysoft = 2/2** (bare is blocked by sannysoft; disguised
has 0 real fails — only `window.chrome` "Chrome (New)", which a real Firefox also fails).
Probe gate green 122/128.
Authenticated residential proxies — **DONE**: `proxywire::relay` (`AuthRelay`,
`spawn_auth_relay[_for_endpoint]`) is a local CONNECT-proxy that fronts a credentialed upstream
(reuses `connect_via_route` for the authed HTTP-CONNECT/SOCKS5 tunnel); Firefox points at the
local relay (no auth). Validated end-to-end vs a mock authenticated upstream (3 tests: positive
inject+tunnel, negative wrong-auth-blocked, parse). Wired: `harness::resolve_proxy` auto-spawns
it when `--proxy`/`STEALTH_PROXY` carries `user:pass@`; pool + gauntlet bare control both use it.
**So the whole pipeline is code-complete.** The ONLY remaining input for the 80-site number is a
real **residential proxy URL** (external asset): `STEALTH_PROXY=socks5://user:pass@host:port
STEALTH_GAUNTLET_CATALOGUE=bench/data/stealth_bench_sites.json cargo run --bin captchaforge-bench
-- --suite stealth-gauntlet --with-network`.
2. **Headless-Firefox coherence audit** — the bench runs headless; real FF `--headless`
differs from headful (WebGL via llvmpipe, AudioContext, media codecs, `outerHeight`).
The JS probe gate is green, but Cloudflare uses more than JS. Audit + close headless tells.
- **Media-codec coherence — measured + now gated.** A new `guise` diagnostic
(`guise/tests/headless_tells.rs`) dumps the absolute headless surface. Ground truth on a
**GPU+ffmpeg host**: headless FF151 is clean — `canPlayType` h264/aac/av1/vp9/opus all
`"probably"`, `mseH264 true`, `eme "function"`, WebGL reports the real NVIDIA GPU (NOT
llvmpipe), AudioContext 48k/2ch. So the stripped-codec / llvmpipe tells the roadmap feared
do **not** manifest on a real-GPU box (confirming the "run headful on a real GPU" guidance).
They DO manifest on a codec-less cloud image — which the differential gate cannot catch
(it diffs two identical engines). Closed that blind spot: new **absolute** codec-coherence
probe `guise/src/probe/codec.rs` (`media: supported-codec matrix matches a consumer
browser`) folded into `probes_for` for every family — flags a stripped H.264/AAC/MSE/EME
matrix as Critical/Drift. 13 unit tests; live-Pass proven by composition (diagnostic matrix
× `healthy_*_matrix_passes`).
3. **Behavioral wiring + trusted-input audit** — route guise `human` (trusted
mouse/keyboard/scroll/timing) into the *navigation* flow so PerimeterX/Datadome behavioral
models see human motion.
- **`warmup.rs` was dispatching SYNTHETIC `new MouseEvent` + `dispatchEvent` (isTrusted=false)**
and `window.scrollBy` — for the one module whose entire job is to look human to the passive
scorers that gate on `isTrusted`. A burst of `isTrusted:false` events is a *stronger* bot tell
than no motion, so warmup was counterproductive. **FIXED**: every pointer/scroll routes
through the trusted BiDi helpers (`behavior::mouse_move_human` / `scroll_realistic`), deleting
the duplicated bezier (`mouse_move_smooth`). Wired into production via `sdk.rs:103`
(`apply_default_warmup`). Proven live: `tests/warmup_trusted.rs::warm_session_emits_only_trusted_events`
(only isTrusted=true, ≥1 mousemove).
- **`solver/slider.rs::drag_handle` had the same bug** — the slider drag (the *most*
isTrusted-gated challenge: GeeTest / AWS WAF / DataDome puzzle) was synthetic `new
MouseEvent('mousedown'|'mousemove'|'mouseup')`. **FIXED**: trusted press→bezier-moves→release
(`mouse_down`/`move_mouse_to`/`mouse_up`), same trajectory. Proven live:
`tests/warmup_trusted.rs::drag_primitives_emit_only_trusted_events`.
- **Still untrusted (concrete, tracked):** (a) `solver/yolo_grid.rs:246` + `solver/vlm.rs:580`
dispatch `new MouseEvent(t,…)` on reCAPTCHA image-grid tiles (vision-gated; needs a
cross-iframe trusted `click_at_in(frame_ctx, x, y)` — validate against live reCAPTCHA before
changing). (b) `solver/behavioral.rs:348,423` fire `dispatchEvent(new Event('click'/'change'))`
on captcha-shaped checkboxes — a deliberate broad sweep for *simple* check-to-pass UIs that
runs ALONGSIDE the trusted primary path (`click_turnstile`/`click_recaptcha_v2` →
`click_realistic` → trusted `click_at`); keep for simple UIs but consider skipping it once a
real Turnstile/reCAPTCHA is detected so it can't register an untrusted touch on the gated
widget. (c) `input`/`change` after value-set (multi_step/crnn/math/audio/vlm) is acceptable —
form frameworks require those events and `isTrusted` is essentially never gated on them.
4. **HTTP/2 + TLS on the browser path** — confirm the real-Firefox handshake reaches the
target unaltered when a proxy is in front (a MITM proxy destroys the advantage).
5. **Residential proxy / IP reputation** — infra, but decisive for these vendors.
**Do NOT** pivot to a patched-Chromium stack to match the field — the real-Firefox path is
the differentiator. Deepen it.
---
## Guiding Principle
CAPTCHA solving is not about beating Google's ML models. It's about building automation that does what a human does:
1. **Image CAPTCHAs:** See the image, identify the objects. Vision problem. Solved with off-the-shelf models.
2. **Behavioral challenges (Turnstile, reCAPTCHA v3):** Look like a human browser. Stealth engineering problem. Patched Chromium + fingerprint evasion + behavioral noise.
3. **Edge cases:** Local LLM reasoning (Llama 3.3 70B, Qwen2.5-VL) for novel challenge types. No external APIs.
The market validates this: WAF bypasses get bounties (CumulusFire). CAPTCHA "bypasses" don't — because passing a CAPTCHA is intended behavior. The only gap is automation. With local GPUs, the compute gap against Google doesn't exist.
---
## Phase 0: Delete Dead Weight (Week 1) ✅
**Goal:** Remove 1,506 LOC of duplication and broken code. Make captchaforge a focused CAPTCHA-solving tool, not a WAF-evasion tool.
| `src/tls_proxy.rs` | 674 | DELETE | `scanclient::tls_impersonate` |
| `src/waf_detect.rs` | 558 | DELETE | `erroracle` (if needed by consumers) |
| `src/proxy_pool.rs` | 274 | DELETE | `proxywire` |
**Verify:**
- `cargo build` passes after deletion
- `wafrift_captchaforge_bridge` still compiles
- No regressions in `captchaforge::detect` or `captchaforge::solver`
**Deliverable:** Clean build, focused codebase, no broken consumers.
---
## Phase 1: Browser Stealth Stack (Weeks 2-6) ✅
**Goal:** Make the headless browser pass Cloudflare Turnstile / reCAPTCHA v3 transparently. The challenge JS should execute, evaluate the browser as legitimate, and set `cf_clearance` without any explicit "solving."
### 1.1 Patched Chromium Integration ✅
**Current state:** `chromium_sidecar.rs` (486 LOC) detects patched Chromium binaries but doesn't fully integrate them.
**Work needed:**
- Extend `chromium_sidecar.rs` to launch patched Chromium with CDP connection
- Patched Chromium must override:
- Canvas fingerprint (consistent per-profile noise)
- WebGL fingerprint (vendor/renderer strings)
- Navigator properties (`webdriver`, `plugins`, `languages`)
- Screen/window dimensions (consistent with TLS fingerprint profile)
- Audio context fingerprint
- Font enumeration
- The patch set can be sourced from existing projects (Camoufox, undetected-chromedriver patches) and adapted for CDP-based control
**Key insight:** We don't need to invent patches. The community has solved headless detection. We need to integrate the patches into our CDP-based stack.
### 1.2 Behavioral Simulation ✅
**Current state:** `keystroke_timing.rs` (14 LOC shim), `mobile_webview.rs` (137 LOC), `mobile_screenshot.rs` (198 LOC).
**Work needed:**
- Move behavioral simulation to `stealth` lib (where it belongs per MASTER_PLAN)
- Implement realistic mouse movement patterns (bezier curves, acceleration, overshoot)
- Implement realistic scroll patterns (momentum scrolling, pauses)
- Implement realistic keyboard timing (variable keypress intervals, typo simulation)
- Implement realistic page interaction (random pauses between actions, reading-time simulation)
- All behavior is profile-driven: `stealth-profiles` defines "this Chrome 131 user types at 180 WPM with 2% typo rate"
**Integration point:** `captchaforge::browser::StealthSession` wraps `runtime_headless::BrowserRuntime` and injects behavior scripts via CDP before any page interaction.
### 1.3 TLS + Browser Profile Consistency ✅
**Current state:** captchaforge uses chromiumoxide but lacks scanclient TLS-impersonate integration.
**Work needed:**
- Wire `scanclient::tls_impersonate` as the HTTP transport for any non-CDP requests
- Ensure TLS fingerprint matches browser profile (Chrome 131 TLS → Chrome 131 browser)
- `stealth::fingerprint::browser_catalog` provides the canonical mapping
**Deliverable:** A browser session that passes Cloudflare's browser integrity check on first visit, no challenge triggered.
---
## Phase 2: Vision Backend for Image CAPTCHAs (Weeks 7-10) ✅ COMPLETE
**Goal:** Solve image-based CAPTCHAs (reCAPTCHA v2 checkbox, hCaptcha image grids, classic text CAPTCHAs) with vision models.
### 2.1 CAPTCHA Type Detection ✅
**Done:** Detection already classifies `ImageGrid` (reCAPTCHA v2 / hCaptcha), `TextChallenge`, `Checkbox`, `Invisible`, and `Audio` via DOM selectors + heuristic analysis.
### 2.2 Vision Model Pipeline ✅
**Architecture:**
```text
Challenge screenshot → Object detection (YOLOv8) → Grid tile mapping → CDP clicks
→ CRNN recognition → Text answer → CDP input → Submit
```
**Done:**
- `ort` 2.0 rc.12 integrated with CUDA/TensorRT/CPU fallback (`vision` feature flag)
- `YoloDetector` in `src/vision/yolo.rs` — full ONNX inference pipeline:
- Letterbox resize, NCHW tensor construction, YOLOv8 output parsing
- NMS post-processing, COCO class name mapping (80 classes)
- `task_to_coco_classes()` maps reCAPTCHA task text → COCO classes
- Inference: ~85ms cold, ~66ms warm on CPU
- `CrnnRecognizer` in `src/vision/crnn.rs` — CRNN text recognition:
- CTC decoding, default alphanumeric charset (0-9, A-Z + blank)
- Loads ONNX model from `~/.captchaforge/models/crnn_text/crnn_text.onnx`
- Training script: `scripts/train_crnn.py` (PyTorch → ONNX export)
- `ModelHub` in `src/vision/model_hub.rs` — downloads and caches models
- YOLOv8n (~12MB) auto-downloaded from Ultralytics releases
- CRNN model trained locally via Python script
- Feature-gated: compiles without `vision` (solvers are inert, chain falls back to VLM)
### 2.3 Click Automation ✅
**Done:**
- `YoloGridSolver` in `src/solver/yolo_grid.rs` — wired into solver chain at position 14:
- Lazy model loading (no blocking in constructor)
- Extracts task text from page DOM
- Maps task → COCO classes, falls back to VLM for unknown classes
- Takes screenshot, runs YOLO detection
- Maps detections to grid tiles (3×3, 4×4 auto-detected)
- Injects clicks via CDP (mouse event dispatch on tile elements)
- Clicks verify button, polls for token
- `CrnnTextSolver` in `src/solver/crnn_text.rs` — wired into chain at position 15:
- Lazy model loading
- Screenshot → CRNN inference → types text into input field
- Clicks submit, polls for token
- `tokio::sync::Mutex` for async-safe detector access
### 2.4 Testing ✅
**Test coverage:**
- `tests/vision_integration.rs` — 4 tests (model download, YOLO load+inference, CRNN load, task mapping)
- `tests/vision_e2e.rs` — 9 tests (mock image grid, mock text CAPTCHA, chain wiring, click verification, benchmarks)
- `tests/vision_property.rs` — 7 property tests (random image robustness, empty image, small image, task mapping exhaustiveness, model hub paths)
- `src/vision/yolo.rs` — unit tests (COCO names, task mapping, IoU calculation, integration with real model)
- `src/vision/crnn.rs` — unit tests (charset length)
- All tests pass: `cargo test --lib --features vision` → 697 passed
**Benchmarks:**
- YOLO cold inference: <200ms on CPU
- YOLO warm inference: <100ms on CPU
- CRNN inference: <50ms on CPU (tiny model)
**Deliverable:** Image CAPTCHAs solved end-to-end with local vision models. 20 vision-specific tests, all passing.
---
## Phase 3: Local LLM Reasoning Backend (Week 11)
**Goal:** Handle edge cases (novel challenge types, complex reasoning tasks) with local LLMs. Zero external API calls.
### 3.1 LLM Engine
**Hardware allocation:**
- RTX 5090 (32GB VRAM): Llama 3.3 70B Q4_K_M (~40GB → fits with CPU offload layers) or Qwen2.5-VL 72B for vision+reasoning
- RTX 4090 (24GB VRAM): Qwen2.5-VL 7B / 32B for fast vision inference, or secondary Llama instance
- RTX 3080 Ti (12GB VRAM): YOLOv8 vision inference, lightweight model serving
**Implementation:**
- `llama.cpp` server via `llama-cpp-2` Rust bindings or raw HTTP to local server
- `vllm` for batched inference if throughput matters
- Model management: download on first run, cache in `~/.captchaforge/models/`
### 3.2 Prompt Engineering for CAPTCHA Types
**Image grid ("select all traffic lights"):**
```
You are solving a CAPTCHA challenge. The image shows a 3x3 grid.
Describe each cell: what objects are visible?
Return ONLY the cell numbers (1-9) that contain [target_object].
```
**Text challenge (distorted text):**
```
Read the text in this image. Ignore noise and distortion.
Return ONLY the characters shown.
```
**Novel challenge (unknown format):**
```
Analyze this challenge screenshot. What action is required to proceed?
Options: click_checkbox, select_images, type_text, click_verify, wait.
Return the action and any required parameters.
```
### 3.3 Fallback Logic
```rust
match solve_vision(&page, &challenge_info).await {
Ok(result) => result,
Err(SolveError::VisionModelUncertain) => {
// Screenshot → local LLM → reasoning → action
solve_via_llm(&page, &challenge_info, &llm_engine).await
}
Err(other) => Err(other),
}
```
**Deliverable:** Novel challenge types handled by local LLM reasoning without any external API.
---
## Phase 4: wafrift Bridge Integration (Week 12)
**Goal:** Make the wafrift-captchaforge-bridge actually work.
**Current state:** Bridge compiles but produces 0% solutions at runtime.
### 4.1 Bridge Fixes
**Work needed:**
- Update bridge to use `captchaforge::browser::StealthSession` instead of raw `runtime_headless::BrowserRuntime`
- Ensure TLS impersonate is active for all non-CDP traffic
- Pass `BridgeConfig` through to solver (headless mode, timeout, sandbox)
- Harvest cookies after challenge resolution: `cf_clearance`, `_abck`, `ak_bmsc`, `aws-waf-token`
- Add bridge integration tests against a local mock challenge server
### 4.2 End-to-End Validation
**Test targets:**
1. Local mock challenge server (controlled environment)
2. CumulusFire WAF challenge page (if it serves Turnstile)
3. Real Cloudflare-protected site (operator's own test domain)
**Success criteria:**
- >50% success rate on local mock challenges
- `cf_clearance` cookie harvested on Cloudflare challenges
- No crashes, no hangs, clean browser teardown
**Deliverable:** wafrift + captchaforge working together as a single stealth + challenge-resolution pipeline.
---
## Phase 5: Packaging & Distribution (Weeks 13-14)
**Goal:** captchaforge is installable and usable as both a library and a CLI.
### 5.1 CLI
```bash
captchaforge solve --url https://example.com/challenge --provider local
captchaforge solve --url https://example.com/challenge --provider capsolver --api-key $KEY
captchaforge detect --screenshot challenge.png
captchaforge train --dataset ./labeled_captchas/ --epochs 50
```
### 5.2 Library API
```rust
use captchaforge::{StealthBrowser, ChallengeDetector, LocalSolver, ApiSolver};
let browser = StealthBrowser::new(Profile::Chrome131).await?;
let page = browser.goto("https://example.com").await?;
let challenge = ChallengeDetector::detect(&page).await?;
let result = LocalSolver::default().solve(&page, &challenge).await?;
```
### 5.3 Feature Flags
- `vision` — ONNX runtime + YOLO models
- `api-fallback` — 2Captcha/CapSolver/Anti-Captcha clients
- `stealth` — patched Chromium + behavioral simulation
- `bridge` — wafrift integration
**Deliverable:** `cargo install captchaforge` works. Library consumers can pick features.
---
## Success Metrics
| Build passes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| LOC (post-dedup) | ~49K | ~52K | ~56K | ~60K | ~60K | ~60K |
| Mock challenge success | N/A | >70% | >80% | >85% | >90% | >95% |
| Real CF challenge cookie | N/A | >30% | >50% | >65% | >75% | >85% |
| Vision backend (YOLO+CRNN) | N/A | N/A | ✅ | ✅ | ✅ | ✅ |
| Vision tests | N/A | N/A | 20 | 20 | 20 | 20 |
| wafrift bridge working | N/A | N/A | N/A | N/A | ✅ | ✅ |
| CLI installable | N/A | N/A | N/A | N/A | N/A | ✅ |
---
## Risk Mitigation
| Patched Chromium is unstable | Start with Camoufox/undetected-chromedriver patches, adapt gradually. Keep unpatched fallback. |
| Vision model accuracy low | Synthetic training data first, real data second. VLM fallback covers gaps. |
| Cloudflare updates detection | Same as wafrift — continuous dogfooding, stealth lib is shared across tools. |
| ONNX runtime build issues | Use `ort` with prebuilt binaries. GPU is optional. |
| Cloudflare updates detection | Same as wafrift — continuous dogfooding, stealth lib is shared across tools. |
| LLM inference latency | 70B model on 5090: ~10-20s per query. Acceptable for CAPTCHA solving. Use 7B/32B models for fast path, 70B for edge cases. |
| Model download size | 70B Q4 = ~40GB. Download once, cache forever. Ship with download script, not in repo. |
---
## Open Questions
1. **Patched Chromium source:** Maintain own fork or track Camoufox/undetected-chromedriver upstream?
2. **Model serving:** `llama.cpp` server (simple) vs `vllm` (batched, faster)?
3. **Training pipeline:** Python (PyTorch) for training, Rust (`ort`) for inference. Acceptable split?
4. **Vision + LLM orchestration:** Screenshot → YOLO (GPU) → if uncertain → LLM (GPU). Single pipeline or separate services?
5. **Commercial model:** Open-source tool with paid stealth profiles? Per-seat license for enterprise?
---
*Roadmap written after deep wafrift source analysis + WAF bypass tool market research + CAPTCHA market research. WAF bypass is the harder problem. CAPTCHA solving is feasible in 3-4 months as focused engineering.*
---
## Empirical finding — 2026-06-05: the "cross-origin click delivery gap" is a misdiagnosis
The README's 0% table attributes the hCaptcha/reCAPTCHA-v2 failures to a
"cross-origin click delivery gap" (checkbox click "fails to deliver into the
cross-origin anchor frame"). This was tested directly and **disproven** at the
input layer.
- **Oracle:** `libs/runtime/foxdriver/tests/cross_origin_click.rs` — deterministic,
no vendor, no fingerprint. Two HTTP origins (different ports) host a real
cross-origin iframe whose checkbox posts to the top window only on a
`event.isTrusted` click; a Turnstile-shaped doubly-nested variant too.
- **Result (Firefox 151 + foxdriver BiDi):** a trusted top-context viewport click
(`Page::click_at`) delivers `isTrusted=true` into a single AND a doubly-nested
cross-origin OOPIF. A new frame-targeted `Page::click_at_in` does the same in
the iframe's own context. A synthetic JS `.click()` arrives `isTrusted=false`.
**Implication:** the click lands trusted. The 0% is **downstream of the click —
the fingerprint / risk-scoring layer** (TLS JA3/JA4, navigator/CDP coherence in
guise's `fingerprint` module). Instrument the token issuance / risk verdict, not
the pointer. Stop attributing interactive-vendor failures to click delivery.
**Bug found + fixed in passing:** guise's human-mouse **trajectory** was dispatched
as untrusted synthetic JS (`document.dispatchEvent(new MouseEvent('mousemove'))`,
`isTrusted=false`) in `guise::human::mouse_driver::dispatch_mouse_moved` (live via
`MouseDriver`) and `behavior::mouse_move_bezier` — nullifying the anti-bot value of
the path. Fixed by routing every sampled point through a new trusted BiDi primitive
`foxdriver::Page::move_mouse_to`. Proven by the contract test's trusted-mousemove
assertion.
### Follow-up — 2026-06-05: the fingerprint stealth was itself a no-op (now fixed)
Pursuing the "0% is fingerprint" conclusion above led to the bigger root cause:
**the entire JS browser stealth never applied.** guise's own live gate
(`libs/runtime/guise/tests/probe_live.rs`) showed bare Firefox = 23 detection
criticals and "stealthed" = 23 — zero removed; `navigator.webdriver` stayed
`true`.
Cause: stealth is injected via WebDriver BiDi `script.addPreloadScript`, whose
`functionDeclaration` is **invoked as a function**, but the scripts are IIFE
bodies `(() => {…})()` (→ `undefined`, not callable), so BiDi never ran them.
captchaforge's own `src/stealth_profiles.rs` has the same pattern (and the same
swallowed-error `let _ =`).
Fix: `foxdriver::Page::add_preload_script` now wraps the body in `() => { … }`.
After it, in `probe_live`: `navigator.webdriver` → hidden, automation-globals
clean, `hardwareConcurrency`/UA/platform applied. Regression guard:
`guise/tests/stealth_core_tells.rs`. captchaforge's stealth path benefits
automatically (it routes through the same foxdriver call).
**Still open (the real anti-detection depth):**
- Override getters leak via `Function.prototype.toString` (`() => undefined`,
non-native) — needs toString spoofing.
- `probe_live` is Chrome-biased (scores Firefox against `window.chrome.*`,
`navigator.usb/hid/serial`, `PaymentRequest`, …) so it can't go green for a
Firefox disguise until expectations are made profile-aware.
- guise and captchaforge duplicate `FIREFOX_STEALTH_JS` — dedupe.