deepcloak 0.1.0__py3-none-any.whl
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.
- deepcloak/__init__.py +3 -0
- deepcloak/bot_wall_detector.py +68 -0
- deepcloak/cli.py +87 -0
- deepcloak/config.py +168 -0
- deepcloak/evidence.py +67 -0
- deepcloak/fetch_router.py +109 -0
- deepcloak/ldr_shim.py +115 -0
- deepcloak/mcp_server.py +54 -0
- deepcloak/progress.py +36 -0
- deepcloak/research_core.py +141 -0
- deepcloak/retriever.py +118 -0
- deepcloak/setup.py +77 -0
- deepcloak/stealth_downloader.py +47 -0
- deepcloak-0.1.0.dist-info/METADATA +157 -0
- deepcloak-0.1.0.dist-info/RECORD +19 -0
- deepcloak-0.1.0.dist-info/WHEEL +4 -0
- deepcloak-0.1.0.dist-info/entry_points.txt +2 -0
- deepcloak-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepcloak-0.1.0.dist-info/licenses/NOTICE +21 -0
deepcloak/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Detect a Bot Wall in a plain HTTP response.
|
|
2
|
+
|
|
3
|
+
Pure module: ``classify()`` takes a status code, headers, and body and returns a
|
|
4
|
+
``BotWall`` (the kind + the signal that triggered it) or ``None``. A detected
|
|
5
|
+
Bot Wall is what triggers an Escalation in ``fetch_router``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = ["BotWall", "classify", "from_response"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BotWall:
|
|
19
|
+
kind: str # cloudflare | datadome | turnstile | recaptcha | rate_limit | blocked
|
|
20
|
+
signal: str # human-readable reason, recorded in the Evidence Record
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Vendor markers checked in the body (lower-cased), strongest signal first.
|
|
24
|
+
_TURNSTILE = ("challenges.cloudflare.com/turnstile", "cf-turnstile")
|
|
25
|
+
_DATADOME = ("captcha-delivery.com", "protected by datadome", "geo.captcha-delivery")
|
|
26
|
+
_RECAPTCHA = ("g-recaptcha", "recaptcha/api.js", "google.com/recaptcha")
|
|
27
|
+
_CLOUDFLARE = (
|
|
28
|
+
"just a moment",
|
|
29
|
+
"checking your browser",
|
|
30
|
+
"/cdn-cgi/challenge-platform",
|
|
31
|
+
"attention required! | cloudflare",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def classify(status_code: int, headers: Mapping[str, str], body: str) -> BotWall | None:
|
|
36
|
+
body_l = (body or "").lower()
|
|
37
|
+
hdr_l = {str(k).lower(): str(v).lower() for k, v in dict(headers or {}).items()}
|
|
38
|
+
|
|
39
|
+
if any(m in body_l for m in _TURNSTILE):
|
|
40
|
+
return BotWall("turnstile", "Cloudflare Turnstile widget present")
|
|
41
|
+
if "datadome" in hdr_l.get("set-cookie", "") or "x-datadome" in hdr_l or any(
|
|
42
|
+
m in body_l for m in _DATADOME
|
|
43
|
+
):
|
|
44
|
+
return BotWall("datadome", "DataDome challenge present")
|
|
45
|
+
if any(m in body_l for m in _RECAPTCHA):
|
|
46
|
+
return BotWall("recaptcha", "reCAPTCHA widget present")
|
|
47
|
+
if any(m in body_l for m in _CLOUDFLARE):
|
|
48
|
+
return BotWall("cloudflare", "Cloudflare interstitial challenge")
|
|
49
|
+
if hdr_l.get("cf-mitigated") == "challenge":
|
|
50
|
+
return BotWall("cloudflare", "cf-mitigated: challenge header")
|
|
51
|
+
|
|
52
|
+
if status_code == 429:
|
|
53
|
+
return BotWall("rate_limit", "HTTP 429 Too Many Requests")
|
|
54
|
+
if status_code == 503 and ("cf-ray" in hdr_l or "cloudflare" in body_l):
|
|
55
|
+
return BotWall("cloudflare", "HTTP 503 from Cloudflare")
|
|
56
|
+
if status_code == 403:
|
|
57
|
+
return BotWall("blocked", "HTTP 403 Forbidden")
|
|
58
|
+
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def from_response(resp: Any) -> BotWall | None:
|
|
63
|
+
"""Convenience wrapper for a requests-style response object."""
|
|
64
|
+
return classify(
|
|
65
|
+
status_code=getattr(resp, "status_code", 0),
|
|
66
|
+
headers=getattr(resp, "headers", {}) or {},
|
|
67
|
+
body=getattr(resp, "text", "") or "",
|
|
68
|
+
)
|
deepcloak/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""DeepCloak command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
__all__ = ["main", "build_parser"]
|
|
11
|
+
|
|
12
|
+
_SUBCOMMANDS = ("setup", "mcp")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
p = argparse.ArgumentParser(
|
|
17
|
+
prog="deepcloak",
|
|
18
|
+
description="Deep research that reads bot-walled pages.",
|
|
19
|
+
epilog="Subcommands: 'deepcloak setup' (install stealth browser), "
|
|
20
|
+
"'deepcloak mcp' (run MCP server).",
|
|
21
|
+
)
|
|
22
|
+
p.add_argument("--version", action="version", version=f"deepcloak {__version__}")
|
|
23
|
+
p.add_argument("query", nargs="?", help="The research question.")
|
|
24
|
+
p.add_argument("--depth", choices=["quick", "detailed", "report"], default="detailed")
|
|
25
|
+
p.add_argument("--engine", choices=["duckduckgo", "searxng", "auto"], default=None)
|
|
26
|
+
p.add_argument("--searxng-url", dest="searxng_url", default=None,
|
|
27
|
+
help="SearXNG instance URL (for --engine searxng)")
|
|
28
|
+
p.add_argument("--stealth", choices=["auto", "always", "off"], default=None)
|
|
29
|
+
p.add_argument("--respect-robots", action="store_true", dest="respect_robots")
|
|
30
|
+
p.add_argument("--proxy", default=None)
|
|
31
|
+
p.add_argument("--provider", default=None,
|
|
32
|
+
help="openai/anthropic/gemini/ollama/openai-endpoint")
|
|
33
|
+
p.add_argument("--model", default=None)
|
|
34
|
+
p.add_argument("--base-url", dest="base_url", default=None,
|
|
35
|
+
help="OpenAI-compatible URL (for openai-endpoint provider)")
|
|
36
|
+
p.add_argument("--out", default=None, help="Write the report to this file.")
|
|
37
|
+
return p
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cli_dict(args: argparse.Namespace) -> dict:
|
|
41
|
+
keys = ("depth", "engine", "stealth", "respect_robots", "proxy",
|
|
42
|
+
"provider", "model", "base_url", "searxng_url", "out")
|
|
43
|
+
return {k: getattr(args, k) for k in keys if getattr(args, k) not in (None, False)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> int:
|
|
47
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
48
|
+
|
|
49
|
+
# Subcommands are dispatched manually so the default `deepcloak "<query>"`
|
|
50
|
+
# form doesn't collide with an argparse subparser positional.
|
|
51
|
+
if argv and argv[0] == "setup":
|
|
52
|
+
from .setup import run_setup
|
|
53
|
+
|
|
54
|
+
return run_setup()
|
|
55
|
+
if argv and argv[0] == "mcp":
|
|
56
|
+
from .mcp_server import serve
|
|
57
|
+
|
|
58
|
+
serve()
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
args = build_parser().parse_args(argv)
|
|
62
|
+
if not args.query:
|
|
63
|
+
build_parser().print_help()
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
from .research_core import research
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = research(args.query, cli=_cli_dict(args), verbose=True)
|
|
70
|
+
except Exception as exc: # surface a clean message, not a traceback
|
|
71
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
if args.out:
|
|
75
|
+
with open(args.out, "w", encoding="utf-8") as fh:
|
|
76
|
+
fh.write(result.report)
|
|
77
|
+
sidecar = f"{args.out}.evidence.json"
|
|
78
|
+
with open(sidecar, "w", encoding="utf-8") as fh:
|
|
79
|
+
fh.write(result.evidence_json)
|
|
80
|
+
print(f"wrote {args.out} (+ {sidecar})")
|
|
81
|
+
else:
|
|
82
|
+
print(result.report)
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__": # pragma: no cover
|
|
87
|
+
raise SystemExit(main())
|
deepcloak/config.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Resolve runtime settings from CLI args + environment.
|
|
2
|
+
|
|
3
|
+
Pure module: ``resolve()`` takes plain mappings and returns a frozen ``Settings``.
|
|
4
|
+
No I/O, no imports of the heavy upstreams โ so it is trivially testable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
__all__ = ["ConfigError", "Settings", "resolve"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigError(Exception):
|
|
16
|
+
"""Raised when settings cannot be resolved (e.g. no LLM credentials)."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Provider auto-detection precedence: first env var present wins.
|
|
20
|
+
_PROVIDER_ENV: list[tuple[str, str]] = [
|
|
21
|
+
("openai", "OPENAI_API_KEY"),
|
|
22
|
+
("anthropic", "ANTHROPIC_API_KEY"),
|
|
23
|
+
("gemini", "GEMINI_API_KEY"),
|
|
24
|
+
("openrouter", "OPENROUTER_API_KEY"),
|
|
25
|
+
]
|
|
26
|
+
_PROVIDER_KEY = dict(_PROVIDER_ENV)
|
|
27
|
+
# Providers that run locally and need no API key.
|
|
28
|
+
_KEYLESS_PROVIDERS = {"ollama", "openai-endpoint"}
|
|
29
|
+
|
|
30
|
+
_VALID_STEALTH = {"auto", "always", "off"}
|
|
31
|
+
_VALID_DEPTH = {"quick", "detailed", "report"}
|
|
32
|
+
_VALID_ENGINE = {"duckduckgo", "searxng", "auto"}
|
|
33
|
+
|
|
34
|
+
_DEFAULT_MODEL = {
|
|
35
|
+
"openai": "gpt-4.1",
|
|
36
|
+
"anthropic": "claude-sonnet-4-6",
|
|
37
|
+
"gemini": "gemini-2.5-pro",
|
|
38
|
+
"openrouter": "openai/gpt-4.1",
|
|
39
|
+
"ollama": "llama3.1",
|
|
40
|
+
"openai-endpoint": None, # local OpenAI-compatible server โ model is server-defined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Our provider name -> the value local-deep-research expects in LDR_LLM_PROVIDER.
|
|
44
|
+
_LDR_PROVIDER = {
|
|
45
|
+
"openai": "openai",
|
|
46
|
+
"anthropic": "anthropic",
|
|
47
|
+
"gemini": "gemini",
|
|
48
|
+
"openrouter": "openai_endpoint",
|
|
49
|
+
"ollama": "ollama",
|
|
50
|
+
"openai-endpoint": "openai_endpoint",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Settings:
|
|
56
|
+
provider: str
|
|
57
|
+
model: str | None
|
|
58
|
+
api_key: str | None
|
|
59
|
+
search_engine: str
|
|
60
|
+
stealth_mode: str
|
|
61
|
+
depth: str
|
|
62
|
+
respect_robots: bool
|
|
63
|
+
out: str | None
|
|
64
|
+
proxy: str | None
|
|
65
|
+
searxng_url: str | None
|
|
66
|
+
base_url: str | None = None # for provider "openai-endpoint" (local OpenAI-compatible)
|
|
67
|
+
|
|
68
|
+
def to_ldr_env(self) -> dict[str, str]:
|
|
69
|
+
"""Map resolved settings to the LDR_* environment variables LDR reads."""
|
|
70
|
+
env: dict[str, str] = {
|
|
71
|
+
"LDR_LLM_PROVIDER": _LDR_PROVIDER[self.provider],
|
|
72
|
+
# DeepCloak's whole point is reading full pages (and Bypassing walls to
|
|
73
|
+
# do it), so we always fetch full content โ never snippet-only.
|
|
74
|
+
"LDR_SEARCH_SNIPPETS_ONLY": "false",
|
|
75
|
+
}
|
|
76
|
+
if self.model:
|
|
77
|
+
env["LDR_LLM_MODEL"] = self.model
|
|
78
|
+
if self.provider == "openai-endpoint":
|
|
79
|
+
if self.base_url:
|
|
80
|
+
env["LDR_LLM_OPENAI_ENDPOINT_URL"] = self.base_url
|
|
81
|
+
# llama.cpp / vLLM ignore the key, but LDR requires a non-empty one.
|
|
82
|
+
env["LDR_LLM_OPENAI_ENDPOINT_API_KEY"] = self.api_key or "local"
|
|
83
|
+
# Pass the credential through under its standard name (langchain reads these).
|
|
84
|
+
if self.api_key and self.provider in _PROVIDER_KEY:
|
|
85
|
+
env[_PROVIDER_KEY[self.provider]] = self.api_key
|
|
86
|
+
if self.search_engine == "searxng" and self.searxng_url:
|
|
87
|
+
env["LDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_INSTANCE_URL"] = self.searxng_url
|
|
88
|
+
return env
|
|
89
|
+
|
|
90
|
+
def to_ldr_overrides(self) -> dict:
|
|
91
|
+
"""Settings as an LDR settings-snapshot override dict (the supported API path).
|
|
92
|
+
|
|
93
|
+
Critically sets ``search.snippets_only=False`` so the research loop fetches
|
|
94
|
+
full pages โ which is what routes through the stealth shim and Bypasses walls.
|
|
95
|
+
"""
|
|
96
|
+
o: dict[str, object] = {
|
|
97
|
+
"llm.provider": _LDR_PROVIDER[self.provider],
|
|
98
|
+
"search.snippets_only": False,
|
|
99
|
+
"search.tool": self.search_engine,
|
|
100
|
+
}
|
|
101
|
+
if self.model:
|
|
102
|
+
o["llm.model"] = self.model
|
|
103
|
+
if self.provider == "openai-endpoint":
|
|
104
|
+
if self.base_url:
|
|
105
|
+
o["llm.openai_endpoint.url"] = self.base_url
|
|
106
|
+
o["llm.openai_endpoint.api_key"] = self.api_key or "local"
|
|
107
|
+
elif self.api_key and self.provider in _PROVIDER_KEY:
|
|
108
|
+
o[f"llm.{self.provider}.api_key"] = self.api_key
|
|
109
|
+
if self.search_engine == "searxng" and self.searxng_url:
|
|
110
|
+
o["search.engine.web.searxng.default_params.instance_url"] = self.searxng_url
|
|
111
|
+
return o
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_provider(cli: Mapping, env: Mapping) -> str:
|
|
115
|
+
provider = cli.get("provider")
|
|
116
|
+
if provider:
|
|
117
|
+
return provider
|
|
118
|
+
for name, key in _PROVIDER_ENV:
|
|
119
|
+
if env.get(key):
|
|
120
|
+
return name
|
|
121
|
+
wanted = ", ".join(key for _, key in _PROVIDER_ENV)
|
|
122
|
+
raise ConfigError(
|
|
123
|
+
"No LLM credentials found. Set one of: "
|
|
124
|
+
f"{wanted} (or pass --provider ollama for a local model)."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate(value: str, valid: set[str], label: str) -> str:
|
|
129
|
+
if value not in valid:
|
|
130
|
+
raise ConfigError(f"Invalid {label}: {value!r}. Choose one of {sorted(valid)}.")
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def resolve(cli: Mapping, env: Mapping) -> Settings:
|
|
135
|
+
"""Resolve effective settings. Precedence: CLI flags > environment."""
|
|
136
|
+
provider = _resolve_provider(cli, env)
|
|
137
|
+
api_key = None
|
|
138
|
+
if provider not in _KEYLESS_PROVIDERS:
|
|
139
|
+
api_key = env.get(_PROVIDER_KEY.get(provider, ""))
|
|
140
|
+
|
|
141
|
+
model = cli.get("model") or env.get("DEEPCLOAK_MODEL") or _DEFAULT_MODEL.get(provider)
|
|
142
|
+
search_engine = _validate(cli.get("engine") or "duckduckgo", _VALID_ENGINE, "search engine")
|
|
143
|
+
stealth_mode = _validate(cli.get("stealth") or "auto", _VALID_STEALTH, "stealth mode")
|
|
144
|
+
depth = _validate(cli.get("depth") or "detailed", _VALID_DEPTH, "depth")
|
|
145
|
+
searxng_url = cli.get("searxng_url") or env.get(
|
|
146
|
+
"LDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_INSTANCE_URL"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
base_url = cli.get("base_url") or env.get("LDR_LLM_OPENAI_ENDPOINT_URL")
|
|
150
|
+
if provider == "openai-endpoint" and not base_url:
|
|
151
|
+
raise ConfigError(
|
|
152
|
+
"provider 'openai-endpoint' needs --base-url (e.g. http://localhost:8080/v1) "
|
|
153
|
+
"or LDR_LLM_OPENAI_ENDPOINT_URL."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return Settings(
|
|
157
|
+
provider=provider,
|
|
158
|
+
model=model,
|
|
159
|
+
api_key=api_key,
|
|
160
|
+
search_engine=search_engine,
|
|
161
|
+
stealth_mode=stealth_mode,
|
|
162
|
+
depth=depth,
|
|
163
|
+
respect_robots=bool(cli.get("respect_robots", False)),
|
|
164
|
+
out=cli.get("out"),
|
|
165
|
+
proxy=cli.get("proxy"),
|
|
166
|
+
searxng_url=searxng_url,
|
|
167
|
+
base_url=base_url,
|
|
168
|
+
)
|
deepcloak/evidence.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Evidence Records โ the per-URL facts captured during a research run.
|
|
2
|
+
|
|
3
|
+
Slice 3 defines the record and a collector. Slice 4 adds the report badge and
|
|
4
|
+
the JSON sidecar rendering on top of the same data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
|
|
12
|
+
__all__ = ["EvidenceRecord", "EvidenceLog"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class EvidenceRecord:
|
|
17
|
+
url: str
|
|
18
|
+
bot_wall: str | None # detected Bot Wall kind, or None
|
|
19
|
+
escalated: bool # did we switch plain -> Stealth Fetch?
|
|
20
|
+
bypassed: bool # did the Stealth Fetch succeed after Escalation?
|
|
21
|
+
plain_status: int | None
|
|
22
|
+
elapsed_ms: int
|
|
23
|
+
signal: str | None = None # human-readable reason
|
|
24
|
+
|
|
25
|
+
def as_dict(self) -> dict:
|
|
26
|
+
return asdict(self)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class EvidenceLog:
|
|
31
|
+
records: list[EvidenceRecord] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
def add(self, record: EvidenceRecord) -> None:
|
|
34
|
+
self.records.append(record)
|
|
35
|
+
|
|
36
|
+
def summary(self) -> dict:
|
|
37
|
+
walls: dict[str, int] = {}
|
|
38
|
+
escalated = bypassed = 0
|
|
39
|
+
for r in self.records:
|
|
40
|
+
escalated += int(r.escalated)
|
|
41
|
+
bypassed += int(r.bypassed)
|
|
42
|
+
if r.bot_wall:
|
|
43
|
+
walls[r.bot_wall] = walls.get(r.bot_wall, 0) + 1
|
|
44
|
+
return {
|
|
45
|
+
"total": len(self.records),
|
|
46
|
+
"escalated": escalated,
|
|
47
|
+
"bypassed": bypassed,
|
|
48
|
+
"walls": walls,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def badge(self) -> str:
|
|
52
|
+
"""Markdown section appended to the report. Empty when nothing was bypassed."""
|
|
53
|
+
s = self.summary()
|
|
54
|
+
if not s["bypassed"]:
|
|
55
|
+
return ""
|
|
56
|
+
plural = "s" if s["bypassed"] != 1 else ""
|
|
57
|
+
lines = [f"## ๐ก๏ธ Bypassed {s['bypassed']} bot-walled source{plural}"]
|
|
58
|
+
if s["walls"]:
|
|
59
|
+
parts = ", ".join(f"{k} ร{v}" for k, v in sorted(s["walls"].items()))
|
|
60
|
+
lines += ["", f"Bot Walls encountered โ {parts}."]
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
def to_json(self, indent: int = 2) -> str:
|
|
64
|
+
return json.dumps(
|
|
65
|
+
{"summary": self.summary(), "records": [r.as_dict() for r in self.records]},
|
|
66
|
+
indent=indent,
|
|
67
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""The Escalation policy: plain fetch first, Stealth Fetch on a detected Bot Wall.
|
|
2
|
+
|
|
3
|
+
Deep module. ``fetch()`` takes injectable ``plain_fetch`` / ``stealth_fetch``
|
|
4
|
+
callables so the policy is fully testable with fakes โ no network, no browser.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .bot_wall_detector import BotWall, from_response
|
|
15
|
+
from .evidence import EvidenceRecord
|
|
16
|
+
|
|
17
|
+
__all__ = ["FetchResult", "fetch"]
|
|
18
|
+
|
|
19
|
+
PlainFetch = Callable[[str], Any]
|
|
20
|
+
StealthFetch = Callable[[str], str]
|
|
21
|
+
Detector = Callable[[Any], "BotWall | None"]
|
|
22
|
+
RobotsOk = Callable[[str], bool]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FetchResult:
|
|
27
|
+
content: str | None
|
|
28
|
+
evidence: EvidenceRecord
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch(
|
|
32
|
+
url: str,
|
|
33
|
+
*,
|
|
34
|
+
mode: str = "auto",
|
|
35
|
+
plain_fetch: PlainFetch,
|
|
36
|
+
stealth_fetch: StealthFetch,
|
|
37
|
+
detector: Detector = from_response,
|
|
38
|
+
respect_robots: bool = False,
|
|
39
|
+
robots_ok: RobotsOk | None = None,
|
|
40
|
+
) -> FetchResult:
|
|
41
|
+
start = time.monotonic()
|
|
42
|
+
|
|
43
|
+
def ms() -> int:
|
|
44
|
+
return int((time.monotonic() - start) * 1000)
|
|
45
|
+
|
|
46
|
+
def record(**kw) -> EvidenceRecord:
|
|
47
|
+
kw.setdefault("plain_status", None)
|
|
48
|
+
return EvidenceRecord(url=url, elapsed_ms=ms(), **kw)
|
|
49
|
+
|
|
50
|
+
if respect_robots and robots_ok is not None and not robots_ok(url):
|
|
51
|
+
return FetchResult(
|
|
52
|
+
None,
|
|
53
|
+
record(bot_wall=None, escalated=False, bypassed=False,
|
|
54
|
+
signal="skipped: robots.txt disallow"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if mode == "always":
|
|
58
|
+
try:
|
|
59
|
+
return FetchResult(
|
|
60
|
+
stealth_fetch(url),
|
|
61
|
+
record(bot_wall="forced", escalated=True, bypassed=True,
|
|
62
|
+
signal="stealth fetch (mode=always)"),
|
|
63
|
+
)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
return FetchResult(
|
|
66
|
+
None,
|
|
67
|
+
record(bot_wall="forced", escalated=True, bypassed=False,
|
|
68
|
+
signal=f"stealth failed: {exc}"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# mode "auto" or "off": try plain first.
|
|
72
|
+
plain_status: int | None = None
|
|
73
|
+
plain_content: str | None = None
|
|
74
|
+
wall: BotWall | None = None
|
|
75
|
+
try:
|
|
76
|
+
resp = plain_fetch(url)
|
|
77
|
+
plain_status = getattr(resp, "status_code", None)
|
|
78
|
+
plain_content = getattr(resp, "text", None)
|
|
79
|
+
wall = detector(resp)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
wall = BotWall("blocked", f"plain fetch failed: {exc}")
|
|
82
|
+
|
|
83
|
+
if mode == "off":
|
|
84
|
+
return FetchResult(
|
|
85
|
+
plain_content,
|
|
86
|
+
record(bot_wall=wall.kind if wall else None, escalated=False, bypassed=False,
|
|
87
|
+
plain_status=plain_status, signal=wall.signal if wall else "plain ok"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# mode "auto"
|
|
91
|
+
if wall is None:
|
|
92
|
+
return FetchResult(
|
|
93
|
+
plain_content,
|
|
94
|
+
record(bot_wall=None, escalated=False, bypassed=False,
|
|
95
|
+
plain_status=plain_status, signal="plain ok, no wall"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
return FetchResult(
|
|
100
|
+
stealth_fetch(url),
|
|
101
|
+
record(bot_wall=wall.kind, escalated=True, bypassed=True, plain_status=plain_status,
|
|
102
|
+
signal=f"bypassed {wall.kind}: {wall.signal}"),
|
|
103
|
+
)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
return FetchResult(
|
|
106
|
+
plain_content,
|
|
107
|
+
record(bot_wall=wall.kind, escalated=True, bypassed=False, plain_status=plain_status,
|
|
108
|
+
signal=f"escalation failed: {exc}"),
|
|
109
|
+
)
|
deepcloak/ldr_shim.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Install the Stealth Fetch into local-deep-research (ADR-0001).
|
|
2
|
+
|
|
3
|
+
We override the narrowest seam โ ``AutoHTMLDownloader._fetch_html``, the method
|
|
4
|
+
that returns a page's raw HTML โ so every HTML fetch in LDR's pipeline runs
|
|
5
|
+
through ``fetch_router`` (plain โ Escalate โ Stealth Fetch) and records an
|
|
6
|
+
Evidence Record. LDR's own text extraction (justext) still runs on the HTML we
|
|
7
|
+
return, so we reuse it rather than reimplementing it.
|
|
8
|
+
|
|
9
|
+
The seam is an internal LDR symbol, so the LDR version is pinned (==) and the
|
|
10
|
+
seam must be re-verified on upgrade.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from functools import partial
|
|
16
|
+
from typing import Any
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
from urllib.robotparser import RobotFileParser
|
|
19
|
+
|
|
20
|
+
from .evidence import EvidenceLog
|
|
21
|
+
from .fetch_router import fetch
|
|
22
|
+
from .stealth_downloader import plain_get, stealth_get
|
|
23
|
+
|
|
24
|
+
__all__ = ["install", "uninstall", "default_robots_ok", "make_fetch_html"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_robots_ok(url: str) -> bool:
|
|
28
|
+
"""Best-effort robots.txt check; on any error, allow (fail-open)."""
|
|
29
|
+
try:
|
|
30
|
+
parts = urlparse(url)
|
|
31
|
+
rp = RobotFileParser()
|
|
32
|
+
rp.set_url(f"{parts.scheme}://{parts.netloc}/robots.txt")
|
|
33
|
+
rp.read()
|
|
34
|
+
return rp.can_fetch("*", url)
|
|
35
|
+
except Exception:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_ldr_downloader() -> type:
|
|
40
|
+
from local_deep_research.research_library.downloaders.playwright_html import ( # type: ignore
|
|
41
|
+
AutoHTMLDownloader,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return AutoHTMLDownloader
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def make_fetch_html(
|
|
48
|
+
*, mode, respect_robots, evidence_log, plain_fetch, stealth_fetch, robots_ok, on_event=None
|
|
49
|
+
):
|
|
50
|
+
# LDR may call _fetch_html more than once per URL (download / download_with_result).
|
|
51
|
+
# Cache per run so we Stealth Fetch once and record exactly one Evidence Record.
|
|
52
|
+
cache: dict[str, str | None] = {}
|
|
53
|
+
|
|
54
|
+
def _fetch_html(self, url: str) -> str | None:
|
|
55
|
+
if url in cache:
|
|
56
|
+
return cache[url]
|
|
57
|
+
result = fetch(
|
|
58
|
+
url,
|
|
59
|
+
mode=mode,
|
|
60
|
+
plain_fetch=plain_fetch,
|
|
61
|
+
stealth_fetch=stealth_fetch,
|
|
62
|
+
respect_robots=respect_robots,
|
|
63
|
+
robots_ok=robots_ok,
|
|
64
|
+
)
|
|
65
|
+
evidence_log.add(result.evidence)
|
|
66
|
+
if on_event is not None:
|
|
67
|
+
on_event(result.evidence)
|
|
68
|
+
cache[url] = result.content
|
|
69
|
+
return result.content
|
|
70
|
+
|
|
71
|
+
return _fetch_html
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def install(
|
|
75
|
+
*,
|
|
76
|
+
evidence_log: EvidenceLog,
|
|
77
|
+
mode: str = "auto",
|
|
78
|
+
respect_robots: bool = False,
|
|
79
|
+
proxy: str | None = None,
|
|
80
|
+
target_cls: type | None = None,
|
|
81
|
+
plain_fetch: Any = plain_get,
|
|
82
|
+
stealth_fetch: Any = None,
|
|
83
|
+
robots_ok: Any = None,
|
|
84
|
+
on_event: Any = None,
|
|
85
|
+
) -> type:
|
|
86
|
+
"""Patch LDR's HTML downloader to route through DeepCloak. Returns the class."""
|
|
87
|
+
cls = target_cls or _load_ldr_downloader()
|
|
88
|
+
if stealth_fetch is None:
|
|
89
|
+
stealth_fetch = partial(stealth_get, proxy=proxy)
|
|
90
|
+
if respect_robots and robots_ok is None:
|
|
91
|
+
robots_ok = default_robots_ok
|
|
92
|
+
|
|
93
|
+
if not hasattr(cls, "_deepcloak_orig_fetch_html"):
|
|
94
|
+
cls._deepcloak_orig_fetch_html = getattr(cls, "_fetch_html", None)
|
|
95
|
+
cls._fetch_html = make_fetch_html(
|
|
96
|
+
mode=mode,
|
|
97
|
+
respect_robots=respect_robots,
|
|
98
|
+
evidence_log=evidence_log,
|
|
99
|
+
plain_fetch=plain_fetch,
|
|
100
|
+
stealth_fetch=stealth_fetch,
|
|
101
|
+
robots_ok=robots_ok,
|
|
102
|
+
on_event=on_event,
|
|
103
|
+
)
|
|
104
|
+
return cls
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def uninstall(cls: type) -> None:
|
|
108
|
+
"""Restore the original ``_fetch_html`` (used in tests / teardown)."""
|
|
109
|
+
orig = getattr(cls, "_deepcloak_orig_fetch_html", None)
|
|
110
|
+
if orig is not None:
|
|
111
|
+
cls._fetch_html = orig
|
|
112
|
+
elif hasattr(cls, "_fetch_html"):
|
|
113
|
+
delattr(cls, "_fetch_html")
|
|
114
|
+
if hasattr(cls, "_deepcloak_orig_fetch_html"):
|
|
115
|
+
delattr(cls, "_deepcloak_orig_fetch_html")
|
deepcloak/mcp_server.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""DeepCloak MCP server.
|
|
2
|
+
|
|
3
|
+
Its own entry point (not LDR's ldr-mcp) so the in-process stealth shim applies
|
|
4
|
+
and every research call produces a Bypass + Evidence. Tool bodies are plain
|
|
5
|
+
functions so they're testable without the MCP runtime installed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import itertools
|
|
11
|
+
|
|
12
|
+
__all__ = ["tool_deep_research", "tool_quick_summary", "tool_get_evidence", "build_server", "serve"]
|
|
13
|
+
|
|
14
|
+
_RUNS: dict[str, str] = {}
|
|
15
|
+
_COUNTER = itertools.count(1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def tool_deep_research(query: str, depth: str = "detailed") -> str:
|
|
19
|
+
"""Run a Deep Research with stealth fetch and return the cited report."""
|
|
20
|
+
from .research_core import research
|
|
21
|
+
|
|
22
|
+
result = research(query, cli={"depth": depth})
|
|
23
|
+
run_id = str(next(_COUNTER))
|
|
24
|
+
_RUNS[run_id] = result.evidence_json
|
|
25
|
+
_RUNS["last"] = result.evidence_json
|
|
26
|
+
return result.report
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def tool_quick_summary(query: str) -> str:
|
|
30
|
+
"""Fast, shallow answer for a query."""
|
|
31
|
+
return tool_deep_research(query, depth="quick")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def tool_get_evidence(run_id: str = "last") -> str:
|
|
35
|
+
"""Return the Evidence Records (JSON) of a prior run; 'last' for the latest."""
|
|
36
|
+
return _RUNS.get(run_id, "{}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_server():
|
|
40
|
+
"""Build the FastMCP server. Requires the optional `mcp` dependency."""
|
|
41
|
+
try:
|
|
42
|
+
from mcp.server.fastmcp import FastMCP
|
|
43
|
+
except ImportError as exc: # pragma: no cover
|
|
44
|
+
raise RuntimeError("The MCP server needs `pip install deepcloak[mcp]`.") from exc
|
|
45
|
+
|
|
46
|
+
server = FastMCP("deepcloak")
|
|
47
|
+
server.tool(name="deep_research")(tool_deep_research)
|
|
48
|
+
server.tool(name="quick_summary")(tool_quick_summary)
|
|
49
|
+
server.tool(name="get_evidence")(tool_get_evidence)
|
|
50
|
+
return server
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def serve() -> None: # pragma: no cover - exercised live, not in unit tests
|
|
54
|
+
build_server().run()
|
deepcloak/progress.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Human-readable live progress for a research run.
|
|
2
|
+
|
|
3
|
+
``format_event`` turns one Evidence Record into a single status line; the CLI
|
|
4
|
+
streams these to stderr as they happen so a real run is legible.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .evidence import EvidenceRecord
|
|
12
|
+
|
|
13
|
+
__all__ = ["format_event", "stderr_printer"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _host(url: str) -> str:
|
|
17
|
+
try:
|
|
18
|
+
return url.split("/")[2] if "://" in url else url
|
|
19
|
+
except Exception:
|
|
20
|
+
return url
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_event(rec: EvidenceRecord) -> str:
|
|
24
|
+
host = _host(rec.url)
|
|
25
|
+
ms = f"{rec.elapsed_ms} ms"
|
|
26
|
+
if rec.bypassed:
|
|
27
|
+
return f" ๐ก๏ธ {host} ๐งฑ {rec.bot_wall} โ โ
bypassed {ms}"
|
|
28
|
+
if rec.escalated:
|
|
29
|
+
return f" โ ๏ธ {host} ๐งฑ {rec.bot_wall} โ escalation failed {ms}"
|
|
30
|
+
if rec.bot_wall:
|
|
31
|
+
return f" โข {host} ๐งฑ {rec.bot_wall} (kept plain) {ms}"
|
|
32
|
+
return f" โ {host} open page {ms}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stderr_printer(rec: EvidenceRecord) -> None:
|
|
36
|
+
print(format_event(rec), file=sys.stderr, flush=True)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""The single entry point every surface (CLI, MCP, skill) goes through.
|
|
2
|
+
|
|
3
|
+
It resolves settings, applies the LDR environment, runs the research loop, and
|
|
4
|
+
attaches Evidence Records. In slice 1 this is the plain path; the stealth shim
|
|
5
|
+
is installed here from slice 3 onward.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from . import ldr_shim
|
|
16
|
+
from .config import Settings, resolve
|
|
17
|
+
from .evidence import EvidenceLog
|
|
18
|
+
|
|
19
|
+
__all__ = ["Result", "research"]
|
|
20
|
+
|
|
21
|
+
# Maps our depth to the LDR api.research_functions callable name.
|
|
22
|
+
_LDR_FUNCTION = {
|
|
23
|
+
"quick": "quick_summary",
|
|
24
|
+
"detailed": "detailed_research",
|
|
25
|
+
"report": "generate_report",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Result:
|
|
31
|
+
report: str
|
|
32
|
+
settings: Settings
|
|
33
|
+
evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
34
|
+
evidence_json: str = "{}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run_ldr(
|
|
38
|
+
query: str,
|
|
39
|
+
settings: Settings,
|
|
40
|
+
evidence_log: Any = None,
|
|
41
|
+
on_event: Any = None,
|
|
42
|
+
rf: Any | None = None,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Call local-deep-research's programmatic API. Imported lazily so the
|
|
45
|
+
package (and its pure modules) load without the heavy upstream installed.
|
|
46
|
+
``rf`` may be injected in tests."""
|
|
47
|
+
fn_kwargs: dict[str, Any] = {}
|
|
48
|
+
if rf is None:
|
|
49
|
+
from local_deep_research.api import research_functions as rf # type: ignore
|
|
50
|
+
|
|
51
|
+
overrides = settings.to_ldr_overrides()
|
|
52
|
+
|
|
53
|
+
# Real integration: hand LDR a StealthRetriever so it synthesises over
|
|
54
|
+
# full pages we fetched through the stealth path (Bypassing walls),
|
|
55
|
+
# instead of search snippets. This is what makes a research run actually
|
|
56
|
+
# read bot-walled sources.
|
|
57
|
+
if settings.searxng_url:
|
|
58
|
+
try:
|
|
59
|
+
from .retriever import build_stealth_retriever
|
|
60
|
+
|
|
61
|
+
fn_kwargs["retrievers"] = {
|
|
62
|
+
"stealth": build_stealth_retriever(
|
|
63
|
+
searxng_url=settings.searxng_url,
|
|
64
|
+
mode=settings.stealth_mode,
|
|
65
|
+
evidence_log=evidence_log,
|
|
66
|
+
on_event=on_event,
|
|
67
|
+
respect_robots=settings.respect_robots,
|
|
68
|
+
proxy=settings.proxy,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
overrides["search.tool"] = "stealth"
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
from local_deep_research.api.settings_utils import ( # type: ignore
|
|
77
|
+
create_settings_snapshot,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
fn_kwargs["settings_snapshot"] = create_settings_snapshot(overrides=overrides)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
fn = getattr(rf, _LDR_FUNCTION[settings.depth])
|
|
85
|
+
result = fn(query, **fn_kwargs)
|
|
86
|
+
# LDR functions return either a string or a dict with a summary/report field.
|
|
87
|
+
if isinstance(result, Mapping):
|
|
88
|
+
return str(result.get("summary") or result.get("report") or result)
|
|
89
|
+
return str(result)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def research(
|
|
93
|
+
query: str,
|
|
94
|
+
cli: Mapping | None = None,
|
|
95
|
+
env: Mapping | None = None,
|
|
96
|
+
verbose: bool = False,
|
|
97
|
+
) -> Result:
|
|
98
|
+
"""Run a Deep Research and return the report plus Evidence Records.
|
|
99
|
+
|
|
100
|
+
``verbose=True`` streams live progress to stderr (used by the CLI)."""
|
|
101
|
+
import sys
|
|
102
|
+
|
|
103
|
+
settings = resolve(cli or {}, env if env is not None else os.environ)
|
|
104
|
+
os.environ.update(settings.to_ldr_env())
|
|
105
|
+
|
|
106
|
+
on_event = None
|
|
107
|
+
if verbose:
|
|
108
|
+
from .progress import stderr_printer as on_event
|
|
109
|
+
|
|
110
|
+
print(f"๐ researching: {query}", file=sys.stderr, flush=True)
|
|
111
|
+
|
|
112
|
+
evidence_log = EvidenceLog()
|
|
113
|
+
try:
|
|
114
|
+
ldr_shim.install(
|
|
115
|
+
evidence_log=evidence_log,
|
|
116
|
+
mode=settings.stealth_mode,
|
|
117
|
+
respect_robots=settings.respect_robots,
|
|
118
|
+
proxy=settings.proxy,
|
|
119
|
+
on_event=on_event,
|
|
120
|
+
)
|
|
121
|
+
except Exception:
|
|
122
|
+
# LDR not importable yet / seam moved โ proceed without stealth (degraded).
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
report = _run_ldr(query, settings, evidence_log=evidence_log, on_event=on_event)
|
|
126
|
+
badge = evidence_log.badge()
|
|
127
|
+
if badge:
|
|
128
|
+
report = f"{report}\n\n{badge}"
|
|
129
|
+
if verbose:
|
|
130
|
+
s = evidence_log.summary()
|
|
131
|
+
print(
|
|
132
|
+
f"โ
done โ {s['total']} sources, {s['bypassed']} bot wall(s) bypassed",
|
|
133
|
+
file=sys.stderr,
|
|
134
|
+
flush=True,
|
|
135
|
+
)
|
|
136
|
+
return Result(
|
|
137
|
+
report=report,
|
|
138
|
+
settings=settings,
|
|
139
|
+
evidence=[r.as_dict() for r in evidence_log.records],
|
|
140
|
+
evidence_json=evidence_log.to_json(),
|
|
141
|
+
)
|
deepcloak/retriever.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""StealthRetriever โ the real integration seam.
|
|
2
|
+
|
|
3
|
+
A LangChain retriever that DeepCloak hands to local-deep-research's
|
|
4
|
+
``retrievers=`` API. For each search hit it fetches the *full page* through the
|
|
5
|
+
stealth ``fetch_router`` (plain โ Escalate โ Stealth Fetch), so the research
|
|
6
|
+
loop synthesises over Bypassed content โ not snippets. Every fetch is recorded
|
|
7
|
+
as an Evidence Record.
|
|
8
|
+
|
|
9
|
+
LangChain is only imported here (it ships with local-deep-research), so the
|
|
10
|
+
pure modules and CI stay free of it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .fetch_router import fetch
|
|
18
|
+
from .stealth_downloader import plain_get, stealth_get
|
|
19
|
+
|
|
20
|
+
__all__ = ["build_stealth_retriever", "searxng_search"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def searxng_search(base_url: str, query: str, max_results: int = 8) -> list[dict]:
|
|
24
|
+
"""Query a SearXNG instance and return [{url, title}] hits."""
|
|
25
|
+
import requests
|
|
26
|
+
|
|
27
|
+
r = requests.get(
|
|
28
|
+
f"{base_url.rstrip('/')}/search",
|
|
29
|
+
params={"q": query, "format": "json"},
|
|
30
|
+
timeout=20,
|
|
31
|
+
headers={"User-Agent": "deepcloak"},
|
|
32
|
+
)
|
|
33
|
+
r.raise_for_status()
|
|
34
|
+
out: list[dict] = []
|
|
35
|
+
for item in (r.json().get("results") or [])[:max_results]:
|
|
36
|
+
url = item.get("url")
|
|
37
|
+
if url:
|
|
38
|
+
out.append({"url": url, "title": item.get("title", "")})
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cap_content(text: str, max_chars: int | None) -> str:
|
|
43
|
+
"""Cap a document's text so many Bypassed pages still fit a model's context.
|
|
44
|
+
|
|
45
|
+
A small local model (e.g. a 16k-context llama-server) overflows when the
|
|
46
|
+
retriever feeds full pages into the synthesis prompt, so the report step
|
|
47
|
+
never runs. ``max_chars`` of ``None``/``0`` disables the cap.
|
|
48
|
+
"""
|
|
49
|
+
if max_chars and len(text) > max_chars:
|
|
50
|
+
return text[:max_chars]
|
|
51
|
+
return text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_text(html: str) -> str:
|
|
55
|
+
"""Best-effort readable-text extraction (reuses LDR's extractor if present)."""
|
|
56
|
+
try:
|
|
57
|
+
from local_deep_research.research_library.downloaders.extraction import ( # type: ignore
|
|
58
|
+
extract_content,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
text = extract_content(html, language="English", min_length=100)
|
|
62
|
+
if text:
|
|
63
|
+
return text
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return html
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_stealth_retriever(
|
|
70
|
+
*,
|
|
71
|
+
searxng_url: str,
|
|
72
|
+
mode: str = "auto",
|
|
73
|
+
max_results: int = 8,
|
|
74
|
+
max_chars: int = 2000,
|
|
75
|
+
evidence_log: Any = None,
|
|
76
|
+
on_event: Any = None,
|
|
77
|
+
respect_robots: bool = False,
|
|
78
|
+
robots_ok: Any = None,
|
|
79
|
+
proxy: str | None = None,
|
|
80
|
+
):
|
|
81
|
+
"""Construct a LangChain BaseRetriever backed by the stealth fetch path."""
|
|
82
|
+
from functools import partial
|
|
83
|
+
|
|
84
|
+
from langchain_core.retrievers import BaseRetriever, Document # type: ignore
|
|
85
|
+
|
|
86
|
+
stealth_fetch = partial(stealth_get, proxy=proxy)
|
|
87
|
+
|
|
88
|
+
class StealthRetriever(BaseRetriever):
|
|
89
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
90
|
+
|
|
91
|
+
def _get_relevant_documents(self, query: str, *, run_manager=None): # noqa: D401
|
|
92
|
+
docs = []
|
|
93
|
+
for hit in searxng_search(searxng_url, query, max_results):
|
|
94
|
+
url = hit["url"]
|
|
95
|
+
result = fetch(
|
|
96
|
+
url,
|
|
97
|
+
mode=mode,
|
|
98
|
+
plain_fetch=plain_get,
|
|
99
|
+
stealth_fetch=stealth_fetch,
|
|
100
|
+
respect_robots=respect_robots,
|
|
101
|
+
robots_ok=robots_ok,
|
|
102
|
+
)
|
|
103
|
+
if evidence_log is not None:
|
|
104
|
+
evidence_log.add(result.evidence)
|
|
105
|
+
if on_event is not None:
|
|
106
|
+
on_event(result.evidence)
|
|
107
|
+
if result.content:
|
|
108
|
+
docs.append(
|
|
109
|
+
Document(
|
|
110
|
+
page_content=_cap_content(
|
|
111
|
+
_extract_text(result.content), max_chars
|
|
112
|
+
),
|
|
113
|
+
metadata={"url": url, "title": hit.get("title", "")},
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return docs
|
|
117
|
+
|
|
118
|
+
return StealthRetriever()
|
deepcloak/setup.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""`deepcloak setup` โ download the stealth browser and check the environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
|
|
10
|
+
__all__ = ["run_setup", "check_env", "install_browser"]
|
|
11
|
+
|
|
12
|
+
_KEY_VARS = ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_env(env: Mapping[str, str] | None = None) -> list[str]:
|
|
16
|
+
"""Return a list of warnings about the environment (empty == all good)."""
|
|
17
|
+
env = os.environ if env is None else env
|
|
18
|
+
warnings: list[str] = []
|
|
19
|
+
if not any(env.get(k) for k in _KEY_VARS):
|
|
20
|
+
warnings.append(
|
|
21
|
+
"No LLM API key found โ set one of "
|
|
22
|
+
+ ", ".join(_KEY_VARS)
|
|
23
|
+
+ " (or use --provider ollama for a local model)."
|
|
24
|
+
)
|
|
25
|
+
return warnings
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _playwright_install() -> bool:
|
|
29
|
+
try:
|
|
30
|
+
subprocess.run(
|
|
31
|
+
[sys.executable, "-m", "playwright", "install", "chromium"],
|
|
32
|
+
check=True,
|
|
33
|
+
capture_output=True,
|
|
34
|
+
)
|
|
35
|
+
return True
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_UNSET = object()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def install_browser(cb: object = _UNSET) -> tuple[bool, str]:
|
|
44
|
+
"""Best-effort: ensure the stealth browser binary is available.
|
|
45
|
+
|
|
46
|
+
``cb`` is the cloakbrowser module; pass ``None`` to simulate it missing.
|
|
47
|
+
"""
|
|
48
|
+
if cb is _UNSET:
|
|
49
|
+
try:
|
|
50
|
+
import cloakbrowser as cb # type: ignore
|
|
51
|
+
except ImportError:
|
|
52
|
+
cb = None
|
|
53
|
+
if cb is None:
|
|
54
|
+
return False, "cloakbrowser is not installed โ run `pip install deepcloak`."
|
|
55
|
+
|
|
56
|
+
# cloakbrowser manages its own stealth Chromium via ensure_binary()/download().
|
|
57
|
+
for fname in ("ensure_binary", "download", "install"):
|
|
58
|
+
fn = getattr(cb, fname, None)
|
|
59
|
+
if callable(fn):
|
|
60
|
+
try:
|
|
61
|
+
path = fn()
|
|
62
|
+
where = f" ({path})" if isinstance(path, str) else ""
|
|
63
|
+
return True, f"Stealth browser ready via cloakbrowser.{fname}(){where}."
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
if _playwright_install():
|
|
67
|
+
return True, "Stealth browser (Chromium) installed via Playwright."
|
|
68
|
+
return True, "cloakbrowser present; could not confirm the browser download โ try manually."
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_setup(cb: object = _UNSET) -> int:
|
|
72
|
+
ok, msg = install_browser(cb)
|
|
73
|
+
print(msg)
|
|
74
|
+
for warning in check_env():
|
|
75
|
+
print(f"warning: {warning}")
|
|
76
|
+
print("DeepCloak is ready." if ok else "Setup incomplete โ see the message above.")
|
|
77
|
+
return 0 if ok else 1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Concrete plain and stealth fetchers used by the shim.
|
|
2
|
+
|
|
3
|
+
- ``plain_get`` is an ordinary HTTP GET (cheap, tried first).
|
|
4
|
+
- ``stealth_get`` drives CloakBrowser's stealth Chromium to Bypass a Bot Wall.
|
|
5
|
+
|
|
6
|
+
Both lazy-import their heavy dependency so the package imports without it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
__all__ = ["plain_get", "stealth_get"]
|
|
14
|
+
|
|
15
|
+
_UA = (
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
17
|
+
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def plain_get(url: str, timeout: int = 20) -> Any:
|
|
22
|
+
"""Plain HTTP GET, returning a requests.Response."""
|
|
23
|
+
try:
|
|
24
|
+
import requests
|
|
25
|
+
except ImportError as exc: # pragma: no cover - requests is a hard dep
|
|
26
|
+
raise RuntimeError("requests not installed") from exc
|
|
27
|
+
return requests.get(url, timeout=timeout, headers={"User-Agent": _UA})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def stealth_get(url: str, proxy: str | None = None, wait_ms: int = 3000) -> str:
|
|
31
|
+
"""Fetch a page through CloakBrowser's stealth Chromium and return its HTML."""
|
|
32
|
+
try:
|
|
33
|
+
import cloakbrowser
|
|
34
|
+
except ImportError as exc:
|
|
35
|
+
raise RuntimeError(
|
|
36
|
+
"cloakbrowser is not installed or the stealth browser is missing โ "
|
|
37
|
+
"run `deepcloak setup`."
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
kwargs: dict[str, Any] = {}
|
|
41
|
+
if proxy:
|
|
42
|
+
kwargs["proxy"] = proxy
|
|
43
|
+
with cloakbrowser.launch_context(**kwargs) as ctx:
|
|
44
|
+
page = ctx.new_page()
|
|
45
|
+
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
|
46
|
+
page.wait_for_timeout(wait_ms)
|
|
47
|
+
return page.content()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepcloak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first deep research agent that reads the whole web โ even pages behind Cloudflare, Datadome, Turnstile & reCAPTCHA.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mrbaeksang/deepcloak
|
|
6
|
+
Project-URL: Repository, https://github.com/Mrbaeksang/deepcloak
|
|
7
|
+
Project-URL: Author, https://baeksang.dev
|
|
8
|
+
Author-email: Mrbaeksang <contact@baeksang.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Keywords: ai-agent,anti-bot,cloudflare,deep-research,llm,local-first,mcp,rag,stealth,web-scraping
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: cloakbrowser>=0.3.27
|
|
15
|
+
Requires-Dist: local-deep-research==1.6.11
|
|
16
|
+
Requires-Dist: requests>=2.31
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
20
|
+
Provides-Extra: mcp
|
|
21
|
+
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
<h1>๐ก๏ธ DeepCloak</h1>
|
|
27
|
+
|
|
28
|
+
### The deep-research agent that reads the pages others can't.
|
|
29
|
+
|
|
30
|
+
**Cloudflare ยท Datadome ยท Turnstile ยท reCAPTCHA โ it walks straight through them.**
|
|
31
|
+
|
|
32
|
+
[](https://github.com/Mrbaeksang/deepcloak/actions/workflows/ci.yml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](pyproject.toml)
|
|
35
|
+
[](#-use-it-from-an-ai-agent)
|
|
36
|
+
[](CONTRIBUTING.md)
|
|
37
|
+
[](https://github.com/Mrbaeksang/deepcloak/stargazers)
|
|
38
|
+
[](https://youtu.be/p5ompjDLzaI)
|
|
39
|
+
|
|
40
|
+
**English** ยท [ํ๊ตญ์ด](README.ko.md) ยท [็ฎไฝไธญๆ](README.zh-CN.md)
|
|
41
|
+
|
|
42
|
+
### [โถ Live demo โ deepcloak.vercel.app](https://deepcloak.vercel.app) ยท [โถ Watch on YouTube](https://youtu.be/p5ompjDLzaI)
|
|
43
|
+
|
|
44
|
+
[](https://youtu.be/p5ompjDLzaI)
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## The problem
|
|
51
|
+
|
|
52
|
+
You ask a research tool a question. Half the best sources sit behind a **Bot Wall** โ Cloudflare, Datadome, a Turnstile, a reCAPTCHA. Every other tool gets a `403`, silently drops those pages, and hands you a thinner report. **You never even learn what it missed.**
|
|
53
|
+
|
|
54
|
+
## What DeepCloak does
|
|
55
|
+
|
|
56
|
+
When a plain fetch hits a Bot Wall, DeepCloak **Escalates** that one URL to a **Stealth Fetch** and **Bypasses** the wall โ recovering the content other agents abandon. Then it tells you, at the bottom of every report, exactly how many walls it broke through.
|
|
57
|
+
|
|
58
|
+
It's a thin, local-first orchestrator over two great projects: [`local-deep-research`](https://github.com/LearningCircuit/local-deep-research) (the research loop) and [`CloakBrowser`](https://github.com/CloakHQ/CloakBrowser) (the stealth browser). Use it as a **CLI**, an **MCP server**, or a **Claude skill**. MIT.
|
|
59
|
+
|
|
60
|
+
## ๐ Why we built this
|
|
61
|
+
|
|
62
|
+
The open web is quietly closing. More of the best writing now sits behind a bot check, and AI research agents โ the tools we increasingly trust to read the web *for us* โ go blind at exactly those doors, without ever saying so. A report that silently skips every walled source isn't neutral; it's wrong in a way you can't see.
|
|
63
|
+
|
|
64
|
+
DeepCloak's stance is simple: **your agent should be able to read what a person with a browser can read** โ and it should be honest about how it got there. So it Bypasses the wall when it has to, keeps everything local (no query or page leaves your machine), and prints an Evidence Record of every wall it crossed. Capability *and* transparency, MIT-licensed, no lock-in.
|
|
65
|
+
|
|
66
|
+
## โจ Why it's different
|
|
67
|
+
|
|
68
|
+
| | Plain deep research | **DeepCloak** |
|
|
69
|
+
| --- | :---: | :---: |
|
|
70
|
+
| Reads the open web | โ
| โ
|
|
|
71
|
+
| Reads Cloudflare / Datadome / Turnstile / reCAPTCHA pages | โ *dropped silently* | โ
**Bypassed** |
|
|
72
|
+
| Tells you which sources were walled | โ | โ
Evidence Record |
|
|
73
|
+
| Local-first (no API key required) | โ
| โ
|
|
|
74
|
+
| Fast on open pages | โ | โ
*plain-first, stealth only when needed* |
|
|
75
|
+
|
|
76
|
+
> **Verified live โ not mocked.** The clip above is an **unedited screen recording** (captured with `ffmpeg`, no compositing) of a real `deepcloak` run against a **local LLM (Qwen) + SearXNG โ no API key**. It Escalates on each Bot Wall and **Bypasses 8 Cloudflare/Turnstile walls** in one pass, then writes a cited report. Full clip: [`docs/media/demo-real.mp4`](docs/media/demo-real.mp4); a raw asciinema session is also kept at [`docs/media/demo.cast`](docs/media/demo.cast). Wall counts vary per run (8โ20) because the open web does.
|
|
77
|
+
|
|
78
|
+
## ๐ Quickstart
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install deepcloak
|
|
82
|
+
deepcloak setup # one-time: downloads the stealth browser
|
|
83
|
+
export OPENAI_API_KEY=... # or ANTHROPIC_API_KEY / GEMINI_API_KEY โ or --provider ollama
|
|
84
|
+
deepcloak "How does Cloudflare Turnstile detect bots?" --depth detailed --out report.md
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You get a cited `report.md` ending with a `๐ก๏ธ Bypassed N bot-walled sources` section, plus a `report.md.evidence.json` sidecar.
|
|
88
|
+
|
|
89
|
+
## ๐ง How it works
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
search (DuckDuckGo, no setup) โโถ candidate URLs
|
|
93
|
+
โ
|
|
94
|
+
โผ for each page:
|
|
95
|
+
plain fetch โโถ Bot Wall detected? โโnoโโโถ use it (fast)
|
|
96
|
+
โ yes
|
|
97
|
+
โผ
|
|
98
|
+
Escalate โโถ Stealth Fetch (CloakBrowser) โโถ Bypass
|
|
99
|
+
โ
|
|
100
|
+
โผ
|
|
101
|
+
research loop (local-deep-research) โโถ cited report + Evidence Records
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Stealth is heavy, so DeepCloak tries a cheap plain fetch first and only launches the stealth browser when it actually detects a Bot Wall (`--stealth auto`, the default). Use `--depth detailed`/`report` to fetch full pages where Bypasses happen.
|
|
105
|
+
|
|
106
|
+
## ๐ค Connect it to your agent (MCP)
|
|
107
|
+
|
|
108
|
+
DeepCloak runs as a stdio **MCP server** exposing `deep_research(query, depth)`, `quick_summary(query)`, and `get_evidence(run_id)`.
|
|
109
|
+
|
|
110
|
+
**Claude Code** โ add to your project's `.mcp.json` (an example ships in this repo):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "mcpServers": { "deepcloak": { "command": "deepcloak", "args": ["mcp"] } } }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Codex** โ add to `~/.codex/config.toml`:
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
[mcp_servers.deepcloak]
|
|
120
|
+
command = "deepcloak"
|
|
121
|
+
args = ["mcp"]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then your agent can call `deep_research` and read bot-walled sources directly. Prefer a slash-style skill? Drop [`skill/SKILL.md`](skill/SKILL.md) into `~/.claude/skills/deepcloak/`.
|
|
125
|
+
|
|
126
|
+
## โ๏ธ Configuration
|
|
127
|
+
|
|
128
|
+
| Flag | Default | Notes |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `--depth` | `detailed` | `quick` / `detailed` / `report` |
|
|
131
|
+
| `--engine` | `duckduckgo` | `searxng` / `auto` |
|
|
132
|
+
| `--stealth` | `auto` | `always` / `off` |
|
|
133
|
+
| `--provider` / `--model` | auto-detected | `OPENAI` โ `ANTHROPIC` โ `GEMINI`, or `ollama` |
|
|
134
|
+
| `--respect-robots` | off | honor robots.txt |
|
|
135
|
+
| `--proxy` | โ | SOCKS5 for the Stealth Fetch |
|
|
136
|
+
|
|
137
|
+
## โ ๏ธ Responsible use
|
|
138
|
+
|
|
139
|
+
DeepCloak Bypasses bot-detection. **You are responsible for having the right to access whatever you fetch.** robots.txt is **ignored by default**; pass `--respect-robots` to honor it ([ADR-0002](docs/adr/0002-ignore-robots-by-default.md)). Don't use it to violate sites' terms or the law.
|
|
140
|
+
|
|
141
|
+
## ๐ ๏ธ Built on
|
|
142
|
+
|
|
143
|
+
[`local-deep-research`](https://github.com/LearningCircuit/local-deep-research) (MIT) + [`CloakBrowser`](https://github.com/CloakHQ/CloakBrowser) (MIT), via pip โ no vendored code. Domain glossary in [CONTEXT.md](CONTEXT.md); design decisions in [docs/adr/](docs/adr/); contributing guide in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
144
|
+
|
|
145
|
+
## ๐ License
|
|
146
|
+
|
|
147
|
+
MIT โ see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
148
|
+
|
|
149
|
+
<div align="center">
|
|
150
|
+
|
|
151
|
+
**If DeepCloak read a page your last tool gave up on, drop a โญ โ it helps others find it.**
|
|
152
|
+
|
|
153
|
+
Built by **[Mrbaeksang](https://baeksang.dev)** ยท [baeksang.dev](https://baeksang.dev) ยท [contact@baeksang.dev](mailto:contact@baeksang.dev)
|
|
154
|
+
|
|
155
|
+
[](https://star-history.com/#Mrbaeksang/deepcloak&Date)
|
|
156
|
+
|
|
157
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
deepcloak/__init__.py,sha256=98KQ1ijIK4a4lXI_-ATlcOBosgpYgFKrHMbd0ounEd8,98
|
|
2
|
+
deepcloak/bot_wall_detector.py,sha256=TPR-ME_UZkixTzJ6eM_yB5_8RBWD_WTkV4mEkqqGDiI,2610
|
|
3
|
+
deepcloak/cli.py,sha256=dP35Fjc1rG7OjMxCrYh25NqTzJs40TsDnwhZiOfCvXw,3141
|
|
4
|
+
deepcloak/config.py,sha256=t4tfmej-g85oPZ3DQ2Tux88-G_7BoT6j1SRkBtYClfk,6288
|
|
5
|
+
deepcloak/evidence.py,sha256=9AkA--Jd5yKr8p_S2e8qRebZevgvaz2w39tdfoWEWDI,2156
|
|
6
|
+
deepcloak/fetch_router.py,sha256=eIohEJI_rIWbMfKYJ9geGWzvIDt8AsJHj_fphEDAY80,3431
|
|
7
|
+
deepcloak/ldr_shim.py,sha256=hY10rM3iWUA1hfG2lPED-rMYy61WIE__5EuMdQF_wMo,3774
|
|
8
|
+
deepcloak/mcp_server.py,sha256=9H-A1xFQrtoZ-flvu2gLvn6fVweBsyH35k9xHuEpfXY,1782
|
|
9
|
+
deepcloak/progress.py,sha256=VJhz6xND3H7cJpSG0fxDaJZbX_-tP8ofaiKYSNiaNqg,1034
|
|
10
|
+
deepcloak/research_core.py,sha256=Q3uRAyiAOuTcCuzbNVnQ_RRIhmLo3jG0HtsiQWDeVC8,4521
|
|
11
|
+
deepcloak/retriever.py,sha256=s-dTGREI5kwYZlCKJaENulUHwPTAHbX4xRb3ALspXNE,3984
|
|
12
|
+
deepcloak/setup.py,sha256=y68jryW4BOhh8ktpOw6xhYUOFyhu6arzvPQGj6B70AM,2522
|
|
13
|
+
deepcloak/stealth_downloader.py,sha256=7r36ZBPsD-YqgzIe3-sInx-PFbHpsW0D_BQNE3cRt1s,1571
|
|
14
|
+
deepcloak-0.1.0.dist-info/METADATA,sha256=vGuhfPXWD_Pwy1eX7locdvSgH9UGLuuJIpeiS2I5KHg,8234
|
|
15
|
+
deepcloak-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
deepcloak-0.1.0.dist-info/entry_points.txt,sha256=x1i7EWsSQXO463HmNKmzJbWhB6Q5t9hNtBtazdl7y5Q,49
|
|
17
|
+
deepcloak-0.1.0.dist-info/licenses/LICENSE,sha256=8m17jZTiuCB6Xv_5GVsK6SM86zpyFX31eoXXghTI03c,1067
|
|
18
|
+
deepcloak-0.1.0.dist-info/licenses/NOTICE,sha256=WzWf4iJunAFVZ15H5nNxhqcYuy85ng0qH_Jsaayw24M,897
|
|
19
|
+
deepcloak-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mrbaeksang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
DeepCloak
|
|
2
|
+
Copyright (c) 2026 Mrbaeksang
|
|
3
|
+
|
|
4
|
+
This product depends on, and is built around, the following open-source projects.
|
|
5
|
+
DeepCloak does not vendor their code; it depends on them via pip.
|
|
6
|
+
|
|
7
|
+
-------------------------------------------------------------------------------
|
|
8
|
+
local-deep-research
|
|
9
|
+
Homepage: https://github.com/LearningCircuit/local-deep-research
|
|
10
|
+
License: MIT
|
|
11
|
+
Role: the iterative multi-source Deep Research loop.
|
|
12
|
+
|
|
13
|
+
-------------------------------------------------------------------------------
|
|
14
|
+
CloakBrowser
|
|
15
|
+
Homepage: https://github.com/CloakHQ/CloakBrowser
|
|
16
|
+
License: MIT
|
|
17
|
+
Role: the stealth Chromium that performs the Stealth Fetch / Bypass.
|
|
18
|
+
|
|
19
|
+
-------------------------------------------------------------------------------
|
|
20
|
+
Both are MIT-licensed; their copyright and permission notices apply to their
|
|
21
|
+
respective code. See each project's repository for full license text.
|