ghostfox 0.2.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.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostfox
3
+ Version: 0.2.0
4
+ Summary: Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime.
5
+ Author: autokeren
6
+ License: MIT OR Apache-2.0
7
+ Project-URL: Repository, https://github.com/autokeren/ghostfox
8
+ Keywords: browser,stealth,anti-detect,mcp,ai-agents,scraping
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # ghostfox (Python)
15
+
16
+ Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
17
+ agent-native stealth browser.
18
+
19
+ ```python
20
+ import ghostfox
21
+
22
+ # one-time: download the prebuilt engine + runtime (Linux x86_64)
23
+ ghostfox.install_engine()
24
+ ghostfox.install_runtime()
25
+
26
+ fox = ghostfox.GhostfoxMCP()
27
+ sid = fox.session_create(platform="android") # coherent mobile persona
28
+ page = fox.page_open(sid, "https://example.com")
29
+ print(fox.page_snapshot(sid, page)["content"])
30
+ fox.page_screenshot(sid, page) # evidence + live view
31
+ fox.close()
32
+ ```
33
+
34
+ Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
35
+ same server AI agents use.
@@ -0,0 +1,22 @@
1
+ # ghostfox (Python)
2
+
3
+ Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
4
+ agent-native stealth browser.
5
+
6
+ ```python
7
+ import ghostfox
8
+
9
+ # one-time: download the prebuilt engine + runtime (Linux x86_64)
10
+ ghostfox.install_engine()
11
+ ghostfox.install_runtime()
12
+
13
+ fox = ghostfox.GhostfoxMCP()
14
+ sid = fox.session_create(platform="android") # coherent mobile persona
15
+ page = fox.page_open(sid, "https://example.com")
16
+ print(fox.page_snapshot(sid, page)["content"])
17
+ fox.page_screenshot(sid, page) # evidence + live view
18
+ fox.close()
19
+ ```
20
+
21
+ Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
22
+ same server AI agents use.
@@ -0,0 +1,16 @@
1
+ """Ghostfox — the agent-native stealth browser you can own.
2
+
3
+ Python surface for the Ghostfox stack:
4
+ - ``ghostfox.install_engine()`` — download + unpack the prebuilt engine
5
+ - ``ghostfox.GhostfoxMCP`` — drive the runtime (the same MCP server agents
6
+ use) from plain Python
7
+
8
+ The runtime is a Rust binary (``ghostcloak-mcp``); this package wraps it so
9
+ the Python world gets the same one-command story as ``pip install browser-use``.
10
+ """
11
+
12
+ from .mcp import GhostfoxMCP, McpError
13
+ from .engine import install_engine, engine_home
14
+
15
+ __version__ = "0.2.0"
16
+ __all__ = ["GhostfoxMCP", "McpError", "install_engine", "engine_home"]
@@ -0,0 +1,110 @@
1
+ """Engine installer: fetch the prebuilt Ghostfox engine from GitHub Releases.
2
+
3
+ Layout after install: ``~/.ghostfox/engine`` (override the root with
4
+ ``GHOSTFOX_HOME_ROOT``). Point ``GHOSTFOX_HOME`` at the returned path when
5
+ launching the runtime.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import json
12
+ import os
13
+ import shutil
14
+ import sys
15
+ import urllib.request
16
+ import zipfile
17
+ from pathlib import Path
18
+
19
+ REPO = "autokeren/ghostfox"
20
+
21
+
22
+ def _latest_release() -> dict:
23
+ url = f"https://api.github.com/repos/{REPO}//releases/latest"
24
+ req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
25
+ with urllib.request.urlopen(req, timeout=30) as r:
26
+ return json.load(r)
27
+
28
+
29
+ def engine_home() -> Path:
30
+ """Where the engine is (or will be) installed."""
31
+ root = Path(os.environ.get("GHOSTFOX_HOME_ROOT", Path.home() / ".ghostfox"))
32
+ return root / "engine"
33
+
34
+
35
+ def install_engine(dest: Path | None = None, quiet: bool = False) -> Path:
36
+ """Download and unpack the latest prebuilt engine. Returns its path."""
37
+ dest = dest or engine_home()
38
+ if (dest / "ghostfox-bin").exists():
39
+ if not quiet:
40
+ print(f"engine already installed at {dest}", file=sys.stderr)
41
+ return dest
42
+
43
+ rel = _latest_release()
44
+ assets = {
45
+ a["name"]: a["browser_download_url"] for a in rel.get("assets", [])
46
+ }
47
+ url = next(
48
+ (u for n, u in assets.items() if "lin.x86_64" in n and n.endswith(".zip")),
49
+ None,
50
+ )
51
+ if not url:
52
+ raise RuntimeError(f"no Linux x86_64 engine asset in release {rel.get('tag_name')}")
53
+
54
+ if not quiet:
55
+ print(f"downloading {url.split('/')[-1]} ...", file=sys.stderr)
56
+ req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
57
+ data = urllib.request.urlopen(req, timeout=600).read()
58
+
59
+ tmp = dest.parent / ".engine-download"
60
+ shutil.rmtree(tmp, ignore_errors=True)
61
+ tmp.mkdir(parents=True, exist_ok=True)
62
+ with zipfile.ZipFile(io.BytesIO(data)) as zf:
63
+ zf.extractall(tmp)
64
+
65
+ src = tmp / "ghostfox" if (tmp / "ghostfox").is_dir() else tmp
66
+ shutil.rmtree(dest, ignore_errors=True)
67
+ dest.parent.mkdir(parents=True, exist_ok=True)
68
+ shutil.move(str(src), dest)
69
+ shutil.rmtree(tmp, ignore_errors=True)
70
+ if not quiet:
71
+ print(f"engine installed at {dest}", file=sys.stderr)
72
+ return dest
73
+
74
+
75
+ def install_runtime(dest: Path | None = None, quiet: bool = False) -> Path:
76
+ """Download the prebuilt ``ghostcloak-mcp`` runtime binary (Linux x86_64)."""
77
+ dest = dest or engine_home().parent / "mcp" / "ghostcloak-mcp"
78
+ if dest.exists():
79
+ return dest
80
+ rel = _latest_release()
81
+ url = next(
82
+ (a["browser_download_url"] for a in rel.get("assets", []) if a["name"] == "ghostcloak-mcp"),
83
+ None,
84
+ )
85
+ if not url:
86
+ raise RuntimeError("no prebuilt runtime asset — build it with cargo (see README)")
87
+ if not quiet:
88
+ print("downloading ghostcloak-mcp ...", file=sys.stderr)
89
+ req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
90
+ dest.parent.mkdir(parents=True, exist_ok=True)
91
+ dest.write_bytes(urllib.request.urlopen(req, timeout=120).read())
92
+ dest.chmod(0o755)
93
+ return dest
94
+
95
+
96
+ def default_runtime() -> Path:
97
+ """Find a runtime: $GHOSTFOX_MCP, the installed copy, or one on PATH."""
98
+ if v := os.environ.get("GHOSTFOX_MCP"):
99
+ p = Path(v)
100
+ if p.exists():
101
+ return p
102
+ installed = engine_home().parent / "mcp" / "ghostcloak-mcp"
103
+ if installed.exists():
104
+ return installed
105
+ if shutil.which("ghostcloak-mcp"):
106
+ return Path(shutil.which("ghostcloak-mcp"))
107
+ raise RuntimeError(
108
+ "runtime not found: set GHOSTFOX_MCP, or call ghostfox.install_runtime()"
109
+ )
110
+
@@ -0,0 +1,158 @@
1
+ """Synchronous Python client for the Ghostfox MCP runtime (ghostcloak-mcp).
2
+
3
+ The runtime is the same MCP server AI agents use; this client gives plain
4
+ Python the same surface:
5
+
6
+ from ghostfox import GhostfoxMCP
7
+
8
+ fox = GhostfoxMCP() # finds/downloads nothing; requires runtime+engine
9
+ sid = fox.session_create(platform="android")
10
+ page = fox.page_open(sid, "https://example.com")
11
+ print(fox.page_snapshot(sid, page)["content"])
12
+ fox.close()
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from .engine import default_runtime
25
+
26
+
27
+ class McpError(RuntimeError):
28
+ pass
29
+
30
+
31
+ class GhostfoxMCP:
32
+ """Drive the Ghostfox runtime over MCP stdio from Python."""
33
+
34
+ def __init__(
35
+ self,
36
+ runtime: Path | str | None = None,
37
+ engine_home: Path | str | None = None,
38
+ env: dict[str, str] | None = None,
39
+ ) -> None:
40
+ self.runtime = Path(runtime) if runtime else default_runtime()
41
+ self.env = dict(os.environ)
42
+ self.env["GHOSTFOX_HOME"] = str(engine_home or os.environ.get("GHOSTFOX_HOME") or Path.home() / ".ghostfox" / "engine")
43
+ if env:
44
+ self.env.update(env)
45
+ self.env.setdefault("RUST_LOG", "warn")
46
+
47
+ self.proc = subprocess.Popen(
48
+ [str(self.runtime)],
49
+ stdin=subprocess.PIPE,
50
+ stdout=subprocess.PIPE,
51
+ stderr=subprocess.DEVNULL,
52
+ text=True,
53
+ bufsize=1,
54
+ env=self.env,
55
+ )
56
+ self._id = 0
57
+ self._call("initialize", {
58
+ "protocolVersion": "2024-11-05",
59
+ "capabilities": {},
60
+ "clientInfo": {"name": "ghostfox-py", "version": "0.1.0"},
61
+ })
62
+ self._notify("notifications/initialized")
63
+
64
+ # -- protocol plumbing ---------------------------------------------------
65
+
66
+ def _send(self, method: str, params: dict | None = None, notify: bool = False) -> None:
67
+ msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
68
+ if params is not None:
69
+ msg["params"] = params
70
+ if not notify:
71
+ self._id += 1
72
+ msg["id"] = self._id
73
+ self.proc.stdin.write(json.dumps(msg) + "\n")
74
+ self.proc.stdin.flush()
75
+
76
+ def _recv(self, want: int, timeout: float = 120.0) -> dict:
77
+ deadline = time.time() + timeout
78
+ while time.time() < deadline:
79
+ line = self.proc.stdout.readline()
80
+ if not line:
81
+ raise McpError("runtime closed the connection")
82
+ line = line.strip()
83
+ if not line:
84
+ continue
85
+ try:
86
+ resp = json.loads(line)
87
+ except json.JSONDecodeError:
88
+ continue
89
+ if resp.get("id") == want:
90
+ return resp
91
+ raise McpError(f"timeout waiting for response {want}")
92
+
93
+ def _call(self, method: str, params: dict | None = None, timeout: float = 120.0) -> Any:
94
+ self._send(method, params)
95
+ resp = self._recv(self._id, timeout)
96
+ if "error" in resp:
97
+ raise McpError(f"{method}: {resp['error'].get('message')}")
98
+ return resp.get("result")
99
+
100
+ def _notify(self, method: str) -> None:
101
+ self._send(method, notify=True)
102
+
103
+ def _tool(self, name: str, args: dict, timeout: float = 180.0) -> str:
104
+ result = self._call("tools/call", {"name": name, "arguments": args}, timeout)
105
+ if result.get("isError"):
106
+ texts = [c.get("text", "") for c in result.get("content", [])]
107
+ raise McpError(f"{name}: {' '.join(texts)}")
108
+ return result["content"][0]["text"]
109
+
110
+ # -- the browser surface ---------------------------------------------------
111
+
112
+ def session_create(self, platform: str | None = None, profile_dir: str | None = None, proxy: str | None = None) -> str:
113
+ args: dict[str, Any] = {}
114
+ if platform:
115
+ args["platform"] = platform
116
+ if profile_dir:
117
+ args["profile_dir"] = profile_dir
118
+ if proxy:
119
+ args["proxy"] = proxy
120
+ return self._tool("session_create", args)
121
+
122
+ def page_open(self, session_id: str, url: str) -> str:
123
+ return self._tool("page_open", {"session_id": session_id, "url": url})
124
+
125
+ def page_snapshot(self, session_id: str, page_id: str) -> dict:
126
+ return json.loads(self._tool("page_snapshot", {"session_id": session_id, "page_id": page_id}))
127
+
128
+ def page_screenshot(self, session_id: str, page_id: str, full_page: bool = False) -> dict:
129
+ return json.loads(self._tool("page_screenshot", {"session_id": session_id, "page_id": page_id, "full_page": full_page}))
130
+
131
+ def page_click(self, session_id: str, page_id: str, selector: str) -> None:
132
+ self._tool("page_click", {"session_id": session_id, "page_id": page_id, "selector": selector})
133
+
134
+ def page_type(self, session_id: str, page_id: str, selector: str, text: str) -> None:
135
+ self._tool("page_type", {"session_id": session_id, "page_id": page_id, "selector": selector, "text": text})
136
+
137
+ def page_fill(self, session_id: str, page_id: str, selector: str, text: str) -> None:
138
+ self._tool("page_fill", {"session_id": session_id, "page_id": page_id, "selector": selector, "text": text})
139
+
140
+ def page_press(self, session_id: str, page_id: str, key: str) -> None:
141
+ self._tool("page_press", {"session_id": session_id, "page_id": page_id, "key": key})
142
+
143
+ def identity_generate(self) -> str:
144
+ return self._tool("identity_generate", {})
145
+
146
+ def identity_audit(self, identity_toml: str) -> str:
147
+ return self._tool("identity_audit", {"identity_toml": identity_toml})
148
+
149
+ def session_evidence(self, session_id: str) -> dict:
150
+ return json.loads(self._tool("session_evidence", {"session_id": session_id}))
151
+
152
+ def close(self) -> None:
153
+ if self.proc.poll() is None:
154
+ self.proc.terminate()
155
+ try:
156
+ self.proc.wait(timeout=10)
157
+ except subprocess.TimeoutExpired:
158
+ self.proc.kill()
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostfox
3
+ Version: 0.2.0
4
+ Summary: Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime.
5
+ Author: autokeren
6
+ License: MIT OR Apache-2.0
7
+ Project-URL: Repository, https://github.com/autokeren/ghostfox
8
+ Keywords: browser,stealth,anti-detect,mcp,ai-agents,scraping
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # ghostfox (Python)
15
+
16
+ Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
17
+ agent-native stealth browser.
18
+
19
+ ```python
20
+ import ghostfox
21
+
22
+ # one-time: download the prebuilt engine + runtime (Linux x86_64)
23
+ ghostfox.install_engine()
24
+ ghostfox.install_runtime()
25
+
26
+ fox = ghostfox.GhostfoxMCP()
27
+ sid = fox.session_create(platform="android") # coherent mobile persona
28
+ page = fox.page_open(sid, "https://example.com")
29
+ print(fox.page_snapshot(sid, page)["content"])
30
+ fox.page_screenshot(sid, page) # evidence + live view
31
+ fox.close()
32
+ ```
33
+
34
+ Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
35
+ same server AI agents use.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ ghostfox/__init__.py
4
+ ghostfox/engine.py
5
+ ghostfox/mcp.py
6
+ ghostfox.egg-info/PKG-INFO
7
+ ghostfox.egg-info/SOURCES.txt
8
+ ghostfox.egg-info/dependency_links.txt
9
+ ghostfox.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ ghostfox
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ghostfox"
7
+ version = "0.2.0"
8
+ description = "Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT OR Apache-2.0" }
12
+ authors = [{ name = "autokeren" }]
13
+ keywords = ["browser", "stealth", "anti-detect", "mcp", "ai-agents", "scraping"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
17
+ ]
18
+
19
+ [project.urls]
20
+ Repository = "https://github.com/autokeren/ghostfox"
21
+
22
+ [tool.setuptools.packages.find]
23
+ include = ["ghostfox*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+