-
-
Drive the Fortress stealth Chromium engine with one line — no build, no Chromium source.
- - - ---- - -**Stop getting blocked — without `puppeteer-stealth`.** JavaScript stealth patches self-reveal: a detector checks whether a getter is native code and catches the override. Fortress compiles the fingerprint correction into Chromium's **C++**, so a page inspecting itself sees stock Chrome. It clears **CreepJS**, **Sannysoft**, **BrowserScan**, and live **Cloudflare** as a normal Chrome install. - -## Install - -```bash -pip install tilion-fortress -``` - -On first use it downloads the prebuilt Fortress binary for your platform from the official GitHub Release (SHA-256 verified) and caches it. No Chromium source, no compilation. - -## Quick start - -```python -from tilion_fortress import Fortress -from playwright.sync_api import sync_playwright - -with Fortress() as f: # launches the stealth engine on a CDP endpoint - with sync_playwright() as p: - browser = p.chromium.connect_over_cdp(f.cdp_url) - page = browser.new_page() - page.goto("https://bot.sannysoft.com") - page.screenshot(path="all-green.png") -``` - -Keep your existing Playwright / Puppeteer / CDP code — just point it at `f.cdp_url`. Works the same under **browser-use**, **Crawl4AI**, **Stagehand**, and **LangChain**. - -## Verified against live detectors - -| Suite | Result | -|---|---| -| **CreepJS** | 0% headless · 0% stealth | -| **bot.sannysoft.com** | 0 failed · all green · WebGL = NVIDIA RTX 3060 / ANGLE D3D11 | -| **browserscan.net** | "No bots detected, could be a human" | -| **rebrowser bot-detector** | no `Runtime.enable` leak · `webdriver=false` | -| **Cloudflare Turnstile** | cleared a live challenge | - -## Custom persona - -The default persona is a coherent Windows identity. Override any surface: - -```python -Fortress( - persona={"timezone": "America/Chicago", "languages": "en-GB,en", - "hw_concurrency": 16, "webgl_renderer": "ANGLE (NVIDIA, RTX 3060 ...)"}, - extra_args=["--window-size=1280,800"], -) -``` - -## Platform support - -Linux x64 has a native prebuilt binary. On macOS / Windows the package transparently runs Fortress via the official Docker image (`arham766/fortress`) — Docker is the cross-OS vehicle until native Win/Mac builds ship. - -> **Still blocked?** ~90% of the time it's your **IP**, not your fingerprint — datacenter ranges are flagged before any page script runs. Route egress through a residential or mobile proxy and retry. - -## Links - -- **Source & docs:** https://github.com/tiliondev/fortress -- **Agent guide:** https://github.com/tiliondev/fortress/blob/main/AGENTS.md -- **Docker image:** https://hub.docker.com/r/arham766/fortress - -BSD-3-Clause · reproducible from source · monthly Chromium rebase · **Blink · V8 · BoringSSL** patched in-tree. diff --git a/sdk/python/tilion_fortress.egg-info/SOURCES.txt b/sdk/python/tilion_fortress.egg-info/SOURCES.txt deleted file mode 100644 index 1bfbbfc..0000000 --- a/sdk/python/tilion_fortress.egg-info/SOURCES.txt +++ /dev/null @@ -1,9 +0,0 @@ -README.md -pyproject.toml -tilion_fortress/__init__.py -tilion_fortress/__main__.py -tilion_fortress.egg-info/PKG-INFO -tilion_fortress.egg-info/SOURCES.txt -tilion_fortress.egg-info/dependency_links.txt -tilion_fortress.egg-info/entry_points.txt -tilion_fortress.egg-info/top_level.txt \ No newline at end of file diff --git a/sdk/python/tilion_fortress.egg-info/dependency_links.txt b/sdk/python/tilion_fortress.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/sdk/python/tilion_fortress.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sdk/python/tilion_fortress.egg-info/entry_points.txt b/sdk/python/tilion_fortress.egg-info/entry_points.txt deleted file mode 100644 index fac1db7..0000000 --- a/sdk/python/tilion_fortress.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -tilion-fortress = tilion_fortress.__main__:main diff --git a/sdk/python/tilion_fortress.egg-info/top_level.txt b/sdk/python/tilion_fortress.egg-info/top_level.txt deleted file mode 100644 index f6400c4..0000000 --- a/sdk/python/tilion_fortress.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -tilion_fortress diff --git a/tools/gauntlet.py b/tools/gauntlet.py index 805b72d..2751954 100755 --- a/tools/gauntlet.py +++ b/tools/gauntlet.py @@ -12,7 +12,7 @@ No third-party deps — raw CDP over a hand-rolled WebSocket so it runs anywhere Python 3 does. """ -import argparse, base64, json, os, socket, struct, subprocess, sys, time, http.client +import argparse, base64, json, os, shutil, socket, struct, subprocess, sys, tempfile, time, http.client def ws_eval(port, expr, timeout=20): @@ -68,16 +68,21 @@ def main(): ap.add_argument("--keep", action="store_true", help="leave the browser running") args = ap.parse_args() - launcher = os.path.join(args.bundle, "tilion") + launcher = os.path.join(args.bundle, "tilion.cmd" if os.name == "nt" else "tilion") if not os.path.exists(launcher): sys.exit(f"no launcher at {launcher}") + # Isolated, OS-appropriate temp dirs (not a hardcoded /tmp, which doesn't exist on Windows). + profile = tempfile.mkdtemp(prefix="fortress-gauntlet-") + home = tempfile.mkdtemp(prefix="fortress-gauntlet-home-") + cmd = [launcher, "--headless=new", "--no-sandbox", "--disable-gpu", + f"--remote-debugging-port={args.port}", + f"--user-data-dir={profile}", "about:blank"] + if os.name == "nt": + cmd = ["cmd", "/c", *cmd] # a .cmd launcher must run through the shell interpreter proc = subprocess.Popen( - [launcher, "--headless=new", "--no-sandbox", "--disable-gpu", - f"--remote-debugging-port={args.port}", - "--user-data-dir=/tmp/fortress-gauntlet", "about:blank"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - env={**os.environ, "HOME": "/tmp/fortress-gauntlet-home"}) + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + env={**os.environ, "HOME": home}) try: for _ in range(60): try: @@ -119,6 +124,12 @@ def main(): finally: if not args.keep: proc.terminate() + try: + proc.wait(timeout=10) + except Exception: + proc.kill() + shutil.rmtree(profile, ignore_errors=True) + shutil.rmtree(home, ignore_errors=True) if __name__ == "__main__":