thunder-agent 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thunder_agent-0.3.0/PKG-INFO +195 -0
- thunder_agent-0.3.0/README.md +167 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/pyproject.toml +1 -1
- thunder_agent-0.3.0/tests/test_stealth.py +60 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/__init__.py +1 -1
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/body.py +17 -1
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/captcha.py +73 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/fingerprint.py +81 -18
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/human.py +20 -1
- thunder_agent-0.3.0/thunder/proxy.py +120 -0
- thunder_agent-0.3.0/thunder/warmup.py +43 -0
- thunder_agent-0.3.0/thunder_agent.egg-info/PKG-INFO +195 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder_agent.egg-info/SOURCES.txt +3 -0
- thunder_agent-0.2.2/PKG-INFO +0 -143
- thunder_agent-0.2.2/README.md +0 -115
- thunder_agent-0.2.2/thunder_agent.egg-info/PKG-INFO +0 -143
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/LICENSE +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/setup.cfg +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/tests/test_body.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/tests/test_core.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/cli.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/demo.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/ghost.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/mcp_server.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/mind.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder/nodriver_body.py +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder_agent.egg-info/dependency_links.txt +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder_agent.egg-info/entry_points.txt +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder_agent.egg-info/requires.txt +0 -0
- {thunder_agent-0.2.2 → thunder_agent-0.3.0}/thunder_agent.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thunder-agent
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Thunder — the human engine for AI agents: stealth browser body, human hands, TLS ghost, CAPTCHA bridge, and a stewmind thought layer.
|
|
5
|
+
Author-email: Emmanuel Ene Rejoice <emmanuelerogian723@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/emmanuelerogian723-alt/thunder
|
|
8
|
+
Project-URL: Powered by, https://huggingface.co/Erog0291/stewmind-1.7b
|
|
9
|
+
Keywords: ai-agents,browser-automation,stealth,mcp,anti-detection,human-like,scraping,stewmind
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: curl_cffi>=0.7
|
|
19
|
+
Requires-Dist: playwright>=1.40
|
|
20
|
+
Provides-Extra: mind
|
|
21
|
+
Requires-Dist: transformers>=4.44; extra == "mind"
|
|
22
|
+
Requires-Dist: torch; extra == "mind"
|
|
23
|
+
Provides-Extra: solver
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
<img src="site/assets/thunder-logo.png" width="220" alt="Thunder logo"/>
|
|
32
|
+
|
|
33
|
+
# ⚡ Thunder
|
|
34
|
+
|
|
35
|
+
**The human engine for AI agents.**
|
|
36
|
+
|
|
37
|
+
Give any software a human body: a real browser with a real fingerprint,
|
|
38
|
+
a mouse that trembles like a hand, keystrokes at human cadence, and a
|
|
39
|
+
TLS handshake indistinguishable from Chrome's.
|
|
40
|
+
|
|
41
|
+
*A bot that walks like a person.*
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/thunder-agent/)
|
|
44
|
+
[](https://pypi.org/project/thunder-agent/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
[](tests/)
|
|
47
|
+
|
|
48
|
+
**[Install](#-install-in-30-seconds)** · **[Watch the proof](https://emmanuelerogian723-alt.github.io/thunder/)** · **[MCP for any software](#-mcp-give-any-software-a-human-body)**
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why Thunder exists
|
|
55
|
+
|
|
56
|
+
Every anti-bot system on Earth looks for the same three tells:
|
|
57
|
+
|
|
58
|
+
1. *Protocol tells* — a Python scraper's TLS/HTTP-2 handshake screams "bot" before a byte of JS runs.
|
|
59
|
+
2. *Fingerprint tells* — `navigator.webdriver`, headless quirks, software-rendered WebGL, empty plugin arrays.
|
|
60
|
+
3. *Behavioral tells* — perfectly straight mouse paths, instant typing, zero reading pauses.
|
|
61
|
+
|
|
62
|
+
Thunder is a body for your AI that has **none of them**. Not patched over — *absent*, the way a real human's browser is.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
pip install thunder-agent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import thunder
|
|
70
|
+
|
|
71
|
+
body = thunder.Body(profile="ada") # a persistent human named Ada
|
|
72
|
+
await body.warmup() # age the session like a returning user
|
|
73
|
+
await body.go("https://protected-site.com")
|
|
74
|
+
await body.type("#email", "ada@example.com") # typed like a person. Typos included.
|
|
75
|
+
await body.look("proof.png") # screenshot evidence
|
|
76
|
+
await body.rest() # close, keep the identity
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Ada comes back tomorrow with the same cookies, same screen, same GPU string, same timezone. The site sees a returning regular.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 📦 Install in 30 seconds
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install thunder-agent
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Works on Linux, macOS, Windows. Real-Chrome engine auto-detected; falls back to bundled Chromium.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
thunder demo # watch it browse like a human, right now
|
|
93
|
+
thunder ghost https://nowsecure.nl # TLS-level fetch that passes the gate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## ⚡ The five layers
|
|
99
|
+
|
|
100
|
+
| Layer | What it does | The tell it kills |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| **Body** | Stealth browser: real Google Chrome, persistent profiles | headless shells, disposable identities |
|
|
103
|
+
| **Skin** | One consistent identity: UA, platform, GPU, screen, timezone, locale | fingerprint mismatches, `SwiftShader` WebGL |
|
|
104
|
+
| **Hand** | Bézier mouse with *hand tremor*, human keystroke cadence, typos it fixes | straight-line paths, machine-perfect timing |
|
|
105
|
+
| **Ghost** | Pure HTTP with Chrome's exact TLS/JA3 fingerprint | Python-scrape handshake = instant 403 |
|
|
106
|
+
| **Warmup** | Organic light browsing before your target | cold first-hit sessions |
|
|
107
|
+
|
|
108
|
+
Verified live: `bot.sannysoft.com` under Thunder — **WebDriver missing (passed) · WebDriver Advanced passed · Chrome present (passed) · PluginArray passed** — *4 passed, 0 failed* with headed real Chrome. Ghost pulls a **200** from a Cloudflare gate that 403s plain Python, same minute.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🔌 MCP: give ANY software a human body
|
|
113
|
+
|
|
114
|
+
Thunder ships an MCP server (10 tools). Paste one config and Claude, Cursor, Cline, Windsurf — anything that speaks MCP — browses as a human:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{ "mcpServers": { "thunder": {
|
|
118
|
+
"command": "pipx", "args": ["run", "thunder-agent", "thunder-mcp"] } } }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{ "mcpServers": { "thunder": {
|
|
123
|
+
"command": "uvx", "args": ["--from", "thunder-agent", "thunder-mcp"] } } }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Tools: `thunder_open` · `thunder_read` · `thunder_search` · `thunder_click` · `thunder_type` · `thunder_scroll` · `thunder_ghost` · `thunder_solve` · `thunder_feel` · `thunder_close` — each one moves like a person, never like a script.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🚫 The blocker table — what used to stop bots, and Thunder's answer
|
|
131
|
+
|
|
132
|
+
| Blocker | Thunder's counter |
|
|
133
|
+
|---|---|
|
|
134
|
+
| Datacenter IP suspicion | `ProxyManager`: sticky per-profile residential proxies, health-checked, auto-rotation — `THUNDER_PROXY` env or `ProxyManager.from_env()` |
|
|
135
|
+
| No GPU / software WebGL | Full WebGL1 **and** WebGL2 spoof: vendor + renderer read as a real consumer GPU, on every API path detectors use |
|
|
136
|
+
| Headless leaks | Headless-evasion pack: `outerWidth/Height`, `colorDepth`, broken-image dimensions, full `window.chrome` shape, battery API |
|
|
137
|
+
| Advanced CAPTCHAs | Keyless `soft_solve` (human-hand click inside challenges) + `ocr_solve` (local OCR for image/math) + CapSolver/2Captcha bridge for the hardest |
|
|
138
|
+
| Behavioral ML | Hand tremor + micro-corrections + warmup sessions + thinking pauses — entropy that reads as a person, because it moves like one |
|
|
139
|
+
|
|
140
|
+
## 🔒 CAPTCHAs without a subscription
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from thunder.captcha import soft_solve, ocr_solve
|
|
144
|
+
result = await soft_solve(body.page) # human-hand click, no keys needed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Non-interactive challenges often pass on a warmed-up session. For Turnstile/reCAPTCHA Enterprise, wire your own CapSolver or 2Captcha key — Thunder senses the challenge type and routes it.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🧠 Pair with any mind
|
|
152
|
+
|
|
153
|
+
Thunder is the body; bring any brain. Works with stewmind, Ollama, LM Studio, vLLM, OpenAI-compatible APIs, or fully offline transformers.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from thunder.mind import Mind
|
|
157
|
+
mind = Mind(provider="ollama", model="llama3")
|
|
158
|
+
plan = await mind.think("find the pricing page and extract the tiers", body)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🧪 Tested, not promised
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
git clone https://github.com/emmanuelerogian723-alt/thunder
|
|
167
|
+
cd thunder && pip install -e ".[dev]" && pytest
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Stealth regression suite: webdriver erased + scan-invisible · genuine PluginArray · headless pack · WebGL skin-consistency · proxy rotation. Each one fails loudly if a future commit breaks the human illusion.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## ⚖️ The honest limits (why people trust Thunder)
|
|
175
|
+
|
|
176
|
+
- No tool on Earth is "undetectable." Anyone selling that is lying to you.
|
|
177
|
+
- Cloud IPs still look like clouds — pair Thunder with a residential proxy for the hardest targets.
|
|
178
|
+
- Google-scale behavioral ML scores the whole session. Thunder's warmup + hand give you a real chance; it's not a magic key.
|
|
179
|
+
- **Use Thunder only where you're authorized.** Your accounts, your tests, your sites. That's the license, and it's the line.
|
|
180
|
+
|
|
181
|
+
MIT licensed. Built in the open — every claim on this page has a script in `tests/` or a screenshot in `proofs/`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
<div align="center">
|
|
186
|
+
|
|
187
|
+
**Watch Thunder beat the bot-detection gauntlet, live:**
|
|
188
|
+
|
|
189
|
+
**https://emmanuelerogian723-alt.github.io/thunder/**
|
|
190
|
+
|
|
191
|
+
⭐ If this saved you a month of anti-detect work, star the repo — it helps others find it.
|
|
192
|
+
|
|
193
|
+
*Thunder — built so software can finally act human.* 🌩️
|
|
194
|
+
|
|
195
|
+
</div>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="site/assets/thunder-logo.png" width="220" alt="Thunder logo"/>
|
|
4
|
+
|
|
5
|
+
# ⚡ Thunder
|
|
6
|
+
|
|
7
|
+
**The human engine for AI agents.**
|
|
8
|
+
|
|
9
|
+
Give any software a human body: a real browser with a real fingerprint,
|
|
10
|
+
a mouse that trembles like a hand, keystrokes at human cadence, and a
|
|
11
|
+
TLS handshake indistinguishable from Chrome's.
|
|
12
|
+
|
|
13
|
+
*A bot that walks like a person.*
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/thunder-agent/)
|
|
16
|
+
[](https://pypi.org/project/thunder-agent/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](tests/)
|
|
19
|
+
|
|
20
|
+
**[Install](#-install-in-30-seconds)** · **[Watch the proof](https://emmanuelerogian723-alt.github.io/thunder/)** · **[MCP for any software](#-mcp-give-any-software-a-human-body)**
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why Thunder exists
|
|
27
|
+
|
|
28
|
+
Every anti-bot system on Earth looks for the same three tells:
|
|
29
|
+
|
|
30
|
+
1. *Protocol tells* — a Python scraper's TLS/HTTP-2 handshake screams "bot" before a byte of JS runs.
|
|
31
|
+
2. *Fingerprint tells* — `navigator.webdriver`, headless quirks, software-rendered WebGL, empty plugin arrays.
|
|
32
|
+
3. *Behavioral tells* — perfectly straight mouse paths, instant typing, zero reading pauses.
|
|
33
|
+
|
|
34
|
+
Thunder is a body for your AI that has **none of them**. Not patched over — *absent*, the way a real human's browser is.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
pip install thunder-agent
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import thunder
|
|
42
|
+
|
|
43
|
+
body = thunder.Body(profile="ada") # a persistent human named Ada
|
|
44
|
+
await body.warmup() # age the session like a returning user
|
|
45
|
+
await body.go("https://protected-site.com")
|
|
46
|
+
await body.type("#email", "ada@example.com") # typed like a person. Typos included.
|
|
47
|
+
await body.look("proof.png") # screenshot evidence
|
|
48
|
+
await body.rest() # close, keep the identity
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Ada comes back tomorrow with the same cookies, same screen, same GPU string, same timezone. The site sees a returning regular.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 📦 Install in 30 seconds
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install thunder-agent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Works on Linux, macOS, Windows. Real-Chrome engine auto-detected; falls back to bundled Chromium.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
thunder demo # watch it browse like a human, right now
|
|
65
|
+
thunder ghost https://nowsecure.nl # TLS-level fetch that passes the gate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## ⚡ The five layers
|
|
71
|
+
|
|
72
|
+
| Layer | What it does | The tell it kills |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| **Body** | Stealth browser: real Google Chrome, persistent profiles | headless shells, disposable identities |
|
|
75
|
+
| **Skin** | One consistent identity: UA, platform, GPU, screen, timezone, locale | fingerprint mismatches, `SwiftShader` WebGL |
|
|
76
|
+
| **Hand** | Bézier mouse with *hand tremor*, human keystroke cadence, typos it fixes | straight-line paths, machine-perfect timing |
|
|
77
|
+
| **Ghost** | Pure HTTP with Chrome's exact TLS/JA3 fingerprint | Python-scrape handshake = instant 403 |
|
|
78
|
+
| **Warmup** | Organic light browsing before your target | cold first-hit sessions |
|
|
79
|
+
|
|
80
|
+
Verified live: `bot.sannysoft.com` under Thunder — **WebDriver missing (passed) · WebDriver Advanced passed · Chrome present (passed) · PluginArray passed** — *4 passed, 0 failed* with headed real Chrome. Ghost pulls a **200** from a Cloudflare gate that 403s plain Python, same minute.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🔌 MCP: give ANY software a human body
|
|
85
|
+
|
|
86
|
+
Thunder ships an MCP server (10 tools). Paste one config and Claude, Cursor, Cline, Windsurf — anything that speaks MCP — browses as a human:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "mcpServers": { "thunder": {
|
|
90
|
+
"command": "pipx", "args": ["run", "thunder-agent", "thunder-mcp"] } } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{ "mcpServers": { "thunder": {
|
|
95
|
+
"command": "uvx", "args": ["--from", "thunder-agent", "thunder-mcp"] } } }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Tools: `thunder_open` · `thunder_read` · `thunder_search` · `thunder_click` · `thunder_type` · `thunder_scroll` · `thunder_ghost` · `thunder_solve` · `thunder_feel` · `thunder_close` — each one moves like a person, never like a script.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🚫 The blocker table — what used to stop bots, and Thunder's answer
|
|
103
|
+
|
|
104
|
+
| Blocker | Thunder's counter |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Datacenter IP suspicion | `ProxyManager`: sticky per-profile residential proxies, health-checked, auto-rotation — `THUNDER_PROXY` env or `ProxyManager.from_env()` |
|
|
107
|
+
| No GPU / software WebGL | Full WebGL1 **and** WebGL2 spoof: vendor + renderer read as a real consumer GPU, on every API path detectors use |
|
|
108
|
+
| Headless leaks | Headless-evasion pack: `outerWidth/Height`, `colorDepth`, broken-image dimensions, full `window.chrome` shape, battery API |
|
|
109
|
+
| Advanced CAPTCHAs | Keyless `soft_solve` (human-hand click inside challenges) + `ocr_solve` (local OCR for image/math) + CapSolver/2Captcha bridge for the hardest |
|
|
110
|
+
| Behavioral ML | Hand tremor + micro-corrections + warmup sessions + thinking pauses — entropy that reads as a person, because it moves like one |
|
|
111
|
+
|
|
112
|
+
## 🔒 CAPTCHAs without a subscription
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from thunder.captcha import soft_solve, ocr_solve
|
|
116
|
+
result = await soft_solve(body.page) # human-hand click, no keys needed
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Non-interactive challenges often pass on a warmed-up session. For Turnstile/reCAPTCHA Enterprise, wire your own CapSolver or 2Captcha key — Thunder senses the challenge type and routes it.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧠 Pair with any mind
|
|
124
|
+
|
|
125
|
+
Thunder is the body; bring any brain. Works with stewmind, Ollama, LM Studio, vLLM, OpenAI-compatible APIs, or fully offline transformers.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from thunder.mind import Mind
|
|
129
|
+
mind = Mind(provider="ollama", model="llama3")
|
|
130
|
+
plan = await mind.think("find the pricing page and extract the tiers", body)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🧪 Tested, not promised
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/emmanuelerogian723-alt/thunder
|
|
139
|
+
cd thunder && pip install -e ".[dev]" && pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Stealth regression suite: webdriver erased + scan-invisible · genuine PluginArray · headless pack · WebGL skin-consistency · proxy rotation. Each one fails loudly if a future commit breaks the human illusion.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## ⚖️ The honest limits (why people trust Thunder)
|
|
147
|
+
|
|
148
|
+
- No tool on Earth is "undetectable." Anyone selling that is lying to you.
|
|
149
|
+
- Cloud IPs still look like clouds — pair Thunder with a residential proxy for the hardest targets.
|
|
150
|
+
- Google-scale behavioral ML scores the whole session. Thunder's warmup + hand give you a real chance; it's not a magic key.
|
|
151
|
+
- **Use Thunder only where you're authorized.** Your accounts, your tests, your sites. That's the license, and it's the line.
|
|
152
|
+
|
|
153
|
+
MIT licensed. Built in the open — every claim on this page has a script in `tests/` or a screenshot in `proofs/`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
<div align="center">
|
|
158
|
+
|
|
159
|
+
**Watch Thunder beat the bot-detection gauntlet, live:**
|
|
160
|
+
|
|
161
|
+
**https://emmanuelerogian723-alt.github.io/thunder/**
|
|
162
|
+
|
|
163
|
+
⭐ If this saved you a month of anti-detect work, star the repo — it helps others find it.
|
|
164
|
+
|
|
165
|
+
*Thunder — built so software can finally act human.* 🌩️
|
|
166
|
+
|
|
167
|
+
</div>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thunder-agent"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Thunder — the human engine for AI agents: stealth browser body, human hands, TLS ghost, CAPTCHA bridge, and a stewmind thought layer."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Stealth regression: every known tell must stay covered. (sync, no pytest-asyncio)"""
|
|
2
|
+
import asyncio
|
|
3
|
+
import pytest
|
|
4
|
+
from thunder.body import Body
|
|
5
|
+
from thunder.proxy import _normalize, ProxyManager
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(scope="module")
|
|
8
|
+
def body():
|
|
9
|
+
b = Body(profile="stealth-test", headless=True)
|
|
10
|
+
asyncio.run(b.wake("https://example.com"))
|
|
11
|
+
yield b
|
|
12
|
+
asyncio.run(b.rest())
|
|
13
|
+
|
|
14
|
+
def test_webdriver_erased(body):
|
|
15
|
+
r = asyncio.run(body.page.evaluate(
|
|
16
|
+
"(() => ({w: navigator.webdriver, scan: (() => { let f=null;"
|
|
17
|
+
" for (const k in navigator) if (/web/i.test(k)&&/driver/i.test(k)) f=k;"
|
|
18
|
+
" return f; })()}))()"))
|
|
19
|
+
assert r["w"] in (None, False) and r["scan"] is None
|
|
20
|
+
|
|
21
|
+
def test_plugins(body):
|
|
22
|
+
r = asyncio.run(body.page.evaluate(
|
|
23
|
+
"(() => ({pa: navigator.plugins instanceof PluginArray,"
|
|
24
|
+
" l: navigator.plugins.length}))()"))
|
|
25
|
+
assert r["pa"] is True and r["l"] == 5
|
|
26
|
+
|
|
27
|
+
def test_headless_pack(body):
|
|
28
|
+
r = asyncio.run(body.page.evaluate(
|
|
29
|
+
"(() => ({ow: window.outerWidth, oh: window.outerHeight,"
|
|
30
|
+
" cd: screen.colorDepth, ch: !!window.chrome,"
|
|
31
|
+
" bt: typeof navigator.getBattery}))()"))
|
|
32
|
+
assert r["ow"] > 0 and r["oh"] > 0 and r["cd"] == 24 and r["ch"] is True
|
|
33
|
+
assert r["bt"] == "function"
|
|
34
|
+
|
|
35
|
+
def test_webgl(body):
|
|
36
|
+
r = asyncio.run(body.page.evaluate(
|
|
37
|
+
"""(() => { const c=document.createElement('canvas');
|
|
38
|
+
const gl=c.getContext('webgl2')||c.getContext('webgl');
|
|
39
|
+
const ext=gl.getExtension('WEBGL_debug_renderer_info');
|
|
40
|
+
return {v: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
|
|
41
|
+
r: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
|
|
42
|
+
d: gl.getParameter(37446)}; })()"""))
|
|
43
|
+
assert r["v"] == body.skin.webgl_vendor
|
|
44
|
+
assert r["d"] == r["r"]
|
|
45
|
+
assert "SwiftShader" not in r["r"]
|
|
46
|
+
|
|
47
|
+
def test_proxy_normalize():
|
|
48
|
+
assert _normalize("host:1234")["server"] == "http://host:1234"
|
|
49
|
+
p = _normalize("user:pass@1.2.3.4:8080")
|
|
50
|
+
assert p["server"] == "http://1.2.3.4:8080" and p["username"] == "user"
|
|
51
|
+
assert _normalize("socks5://h:1")["server"] == "socks5://h:1"
|
|
52
|
+
|
|
53
|
+
def test_proxy_sticky_rotation():
|
|
54
|
+
pm = ProxyManager([{"server": "http://a:1"}, {"server": "http://b:2"}])
|
|
55
|
+
a = pm.pick("ada"); b = pm.pick("ada")
|
|
56
|
+
assert a == b
|
|
57
|
+
c = pm.pick("bob")
|
|
58
|
+
assert c["server"] != a["server"]
|
|
59
|
+
pm.mark_bad(a, cooldown=9999)
|
|
60
|
+
assert pm.pick("ada")["server"] == c["server"]
|
|
@@ -24,5 +24,5 @@ from thunder.ghost import Ghost
|
|
|
24
24
|
from thunder.mind import StewMind
|
|
25
25
|
from thunder.fingerprint import Skin, random_skin
|
|
26
26
|
|
|
27
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.3.0"
|
|
28
28
|
__all__ = ["Body", "Ghost", "StewMind", "Skin", "random_skin", "NodriverBody", "__version__"]
|
|
@@ -42,10 +42,12 @@ class Body:
|
|
|
42
42
|
def __init__(self, profile: str = "default", skin: Optional[Skin] = None,
|
|
43
43
|
engine: str = "playwright-stealth", headless: bool = True,
|
|
44
44
|
record_video: bool = False, viewport: Optional[dict] = None,
|
|
45
|
-
locale: str = "en-US", timezone: str = "Africa/Lagos"
|
|
45
|
+
locale: str = "en-US", timezone: str = "Africa/Lagos",
|
|
46
|
+
proxy=None):
|
|
46
47
|
self.profile = profile
|
|
47
48
|
self.engine = engine
|
|
48
49
|
self.headless = headless
|
|
50
|
+
self.proxy = proxy
|
|
49
51
|
self.viewport = viewport
|
|
50
52
|
self.record = record_video
|
|
51
53
|
self.locale = locale
|
|
@@ -93,6 +95,13 @@ class Body:
|
|
|
93
95
|
# nodriver is a stronger, separate engine; supported via
|
|
94
96
|
# thunder.nodriver_engine when installed (AGPL-3).
|
|
95
97
|
raise ValueError("use thunder.nodriver_engine.NodriverBody for the nodriver engine")
|
|
98
|
+
if self.proxy:
|
|
99
|
+
p = self.proxy
|
|
100
|
+
if isinstance(p, str):
|
|
101
|
+
from .proxy import _normalize
|
|
102
|
+
p = _normalize(p)
|
|
103
|
+
if p:
|
|
104
|
+
launch_kw["proxy"] = p
|
|
96
105
|
launch_kw.update({
|
|
97
106
|
"user_agent": self.skin.user_agent, # header + JS always match the Skin
|
|
98
107
|
"viewport": {"width": self.skin.window_w, "height": self.skin.window_h},
|
|
@@ -132,6 +141,13 @@ class Body:
|
|
|
132
141
|
except Exception:
|
|
133
142
|
pass
|
|
134
143
|
|
|
144
|
+
async def warmup(self, visits: int = 2) -> dict:
|
|
145
|
+
"""Age the session with organic light browsing so arriving at
|
|
146
|
+
your target reads like a returning human, not a cold first hit.
|
|
147
|
+
Big targets score the WHOLE session — this is the counter."""
|
|
148
|
+
from .warmup import warmup
|
|
149
|
+
return await warmup(self, visits=visits)
|
|
150
|
+
|
|
135
151
|
async def rest(self):
|
|
136
152
|
"""Close the body gracefully (context persists to disk)."""
|
|
137
153
|
try:
|
|
@@ -151,3 +151,76 @@ async def describe_challenge(page, mind=None) -> str:
|
|
|
151
151
|
if mind is None:
|
|
152
152
|
return prompt # caller routes screenshot + prompt to a vision model
|
|
153
153
|
return prompt
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── KEYLESS FALLBACKS ─────────────────────────────────────────────
|
|
157
|
+
# Thunder avoids most challenges with skin + hand. These two give you
|
|
158
|
+
# a real shot at the rest WITHOUT any solver subscription.
|
|
159
|
+
|
|
160
|
+
async def soft_solve(page, wait_after: float = 8.0) -> Dict[str, Any]:
|
|
161
|
+
"""Human-hand click inside the challenge — no solver key needed.
|
|
162
|
+
|
|
163
|
+
Non-interactive Turnstile and 'just click the box' reCAPTCHA often
|
|
164
|
+
pass when the click comes from a human-shaped hand on a warmed-up
|
|
165
|
+
session instead of an instant synthetic one.
|
|
166
|
+
"""
|
|
167
|
+
frames = page.frames
|
|
168
|
+
target = None
|
|
169
|
+
for f in frames:
|
|
170
|
+
if any(k in (f.url or "") for k in ("turnstile", "recaptcha", "hcaptcha")):
|
|
171
|
+
target = f
|
|
172
|
+
break
|
|
173
|
+
if target is None:
|
|
174
|
+
return {"solved": False, "error": "no challenge frame found"}
|
|
175
|
+
try:
|
|
176
|
+
box = await target.locator("body").bounding_box()
|
|
177
|
+
if not box:
|
|
178
|
+
return {"solved": False, "error": "no clickable area"}
|
|
179
|
+
from .human import Hand
|
|
180
|
+
hand = Hand(page)
|
|
181
|
+
# human approach: drift in from nearby, pause, click
|
|
182
|
+
await hand.idle(1.5)
|
|
183
|
+
await hand.click((box["x"] + box["width"] * 0.5 + 15,
|
|
184
|
+
box["y"] + box["height"] * 0.5))
|
|
185
|
+
await page.wait_for_timeout(int(wait_after * 1000))
|
|
186
|
+
token = await page.evaluate("""(() => {
|
|
187
|
+
const el = document.querySelector(
|
|
188
|
+
'[name=cf-turnstile-response],textarea#g-recaptcha-response,[name=h-captcha-response]');
|
|
189
|
+
return el ? el.value : window.turnbridge_token || null; })()""")
|
|
190
|
+
return {"solved": bool(token), "token": (token or "")[:24] + "..."}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return {"solved": False, "error": str(e)[:120]}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def ocr_solve(page, input_selector: str = "input",
|
|
196
|
+
img_selector: str = "img") -> Dict[str, Any]:
|
|
197
|
+
"""Local OCR for simple image/math CAPTCHAs — still keyless.
|
|
198
|
+
|
|
199
|
+
Uses tesseract when installed (apt install tesseract-ocr).
|
|
200
|
+
Reads the challenge image, types the answer with the human hand.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
import pytesseract # optional; pip install pytesseract
|
|
204
|
+
except ImportError:
|
|
205
|
+
return {"solved": False,
|
|
206
|
+
"error": "install pytesseract + tesseract-ocr for local OCR"}
|
|
207
|
+
import base64
|
|
208
|
+
img = page.locator(img_selector).first
|
|
209
|
+
data = await img.screenshot()
|
|
210
|
+
import io
|
|
211
|
+
from PIL import Image
|
|
212
|
+
text_img = Image.open(io.BytesIO(data)).convert("L")
|
|
213
|
+
answer = pytesseract.image_to_string(
|
|
214
|
+
text_img, config="--psm 7 -c tessedit_char_whitelist=0123456789+-*=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
215
|
+
).strip()
|
|
216
|
+
if not answer:
|
|
217
|
+
return {"solved": False, "error": "OCR read nothing"}
|
|
218
|
+
# simple arithmetic like "7 + 4 ="
|
|
219
|
+
import re
|
|
220
|
+
m = re.search(r"(\d+)\s*([+\-x*])\s*(\d+)", answer)
|
|
221
|
+
if m:
|
|
222
|
+
a, op, c = int(m.group(1)), m.group(2), int(m.group(3))
|
|
223
|
+
answer = str(a + c if op == "+" else a - c if op == "-" else a * c)
|
|
224
|
+
from .human import Hand
|
|
225
|
+
await Hand(page).type(input_selector, answer)
|
|
226
|
+
return {"solved": True, "answer": answer}
|