Open-Web Acquisition — web_search / web_fetch
Two tools front the sandbox open-web engine (decepticon.sandbox_web):
web_search(query, provider="duckduckgo") — keyword OSINT over an
allowlisted search provider. Target-EXEMPT (it hits the provider, not the
target), so no engagement scope is required. Use it to DISCOVER: the org's
public footprint, exposed tech / version-specific advisories, leaked
references, doc/changelog/status pages, third-party subdomains and assets
named in the wild.
web_fetch(url, selector="", device="auto") — read ONE page's content,
escalating past WAF / anti-bot blocks. RoE target-gated (the url must be
in plan/roe.json scope) and SSRF-safe. Prefer it over curl-in-bash
whenever a public page is blocked, returns a challenge, or is JS-rendered.
Typical flow: web_search to find a URL → web_fetch to read it.
How web_fetch escalates (you do NOT drive this — the engine does)
web_fetch runs an escalation ladder inside the sandbox automatically:
- Phase 0 — official public APIs. Reddit / X(Twitter) / YouTube URLs are
auto-routed to their no-auth endpoints (Reddit
.rss, X tweet-result/oEmbed/
syndication, YouTube yt-dlp) BEFORE any WAF grid. Just pass the normal page
URL; a Phase-0 win shows verdict with profile=phase0:<platform>.
- curl_cffi TLS-impersonation grid. Real Safari/Chrome/Firefox JA3/JA4
fingerprints × URL transforms (mobile subdomain, …) × referer strategies,
tried exhaustively. This clears most WAFs (Cloudflare TLS, F5, AWS WAF, …).
- Headless browser tier. For JS challenges the curl grid can't clear
(Cloudflare Turnstile, Akamai Bot Manager, DataDome), a local headless
Chromium with stealth runs. There is no Playwright-MCP in the sandbox —
this local browser IS the final rung. If it still fails, that is a real wall.
You never pick a TLS profile, transform, or browser — the engine detects the WAF
and chooses. Your only knobs are selector and device.
R2 — HTTP 200 is NOT success
The engine validates every response (4-layer: challenge markers / body size /
cookie sensor / your selector). A 200 that is actually a challenge or empty
SPA shell is reported as a FAILURE, not content. Trust the tool's verdict /
[web_fetch OK|FAILED] line, never a raw status. Always pass a selector
(e.g. article, #content, [class*='product']) when you know the content
marker — it upgrades a "looks clean" guess into proven success.
R6 — a give-up is NOT exhaustion (the discipline that matters)
When web_fetch returns [web_fetch FAILED] it includes stop_reason= and
grid_exhausted=. Do not declare a page unreadable on the first FAILED.
- TERMINAL walls —
stop_reason is auth_required (login/paywall) or
not_found (404). These are real: record it and move on; retrying cannot
help.
- Everything else (
challenge, blocked, rate_limited, or a
non-exhausted grid) still has escalation left:
- rate_limited (429) is NOT terminal — back off, then retry.
- Retry once with
device="mobile" (mobile TLS + m. subdomain often
walk past a desktop block) and/or a precise selector.
- Only after a terminal
stop_reason may you honestly conclude the page is
unreachable.
This is the whole point of the engine: the cheap path failing is the start of
escalation, not permission to stop.
RoE & OSINT boundary
web_search is OSINT → not scope-gated (allowlisted provider egress only).
web_fetch is target-gated → the url must be in plan/roe.json scope; an
out-of-scope fetch returns ROE_REFUSED. SSRF-blocked (internal/metadata/
private) hosts are refused regardless.
- Reading public pages is OSINT, not exploitation. Use
bash/http_request
for ACTIVE probing of in-scope target infrastructure; use web_fetch to READ
pages (the target's own blocked/JS pages, or in-scope public assets).
Platform fast-paths (handled by Phase 0 — just pass the URL)
| Need |
Pass to web_fetch |
| Reddit thread/sub |
the reddit.com/... URL (auto .rss) |
| A specific tweet / X profile |
the x.com/.../status/... or profile URL |
| YouTube metadata / captions |
the youtube.com/watch?v=... URL (yt-dlp) |
| GitHub repo (metadata, stars, language) |
the github.com/<owner>/<repo> URL → repos API JSON |
| npm package (deps, versions, maintainers) |
the npmjs.com/package/<pkg> URL → registry JSON |
| PyPI package (versions, deps, urls) |
the pypi.org/project/<pkg> URL → JSON API |
| Any WAF-protected article/page |
the page URL — the grid + browser tier handle it |
GitHub/npm/PyPI return structured JSON (great for supply-chain / source recon)
instead of HTML — just pass the normal page URL and the engine routes it.
For keyword discovery on a platform (e.g. "what's said about X on Reddit"),
web_search("site:reddit.com <topic>") first, then web_fetch the result URLs.
1---2name: open-web3description: Resilient public-page reading and OSINT keyword search — web_search (allowlisted-provider OSINT) and web_fetch (curl_cffi TLS-impersonation grid + headless-browser fallback that gets past WAF/403/anti-bot). Use when a fetch is blocked, a page is JS-rendered, or you need open-web OSINT about a target/org.4---56# Open-Web Acquisition — web_search / web_fetch78Two tools front the sandbox open-web engine (`decepticon.sandbox_web`):910- **`web_search(query, provider="duckduckgo")`** — keyword OSINT over an11 allowlisted search provider. Target-EXEMPT (it hits the provider, not the12 target), so no engagement scope is required. Use it to DISCOVER: the org's13 public footprint, exposed tech / version-specific advisories, leaked14 references, doc/changelog/status pages, third-party subdomains and assets15 named in the wild.16- **`web_fetch(url, selector="", device="auto")`** — read ONE page's content,17 escalating past WAF / anti-bot blocks. **RoE target-gated** (the `url` must be18 in `plan/roe.json` scope) and SSRF-safe. Prefer it over `curl`-in-bash19 whenever a public page is blocked, returns a challenge, or is JS-rendered.2021Typical flow: **`web_search` to find a URL → `web_fetch` to read it.**2223## How web_fetch escalates (you do NOT drive this — the engine does)2425`web_fetch` runs an escalation ladder inside the sandbox automatically:26271. **Phase 0 — official public APIs.** Reddit / X(Twitter) / YouTube URLs are28 auto-routed to their no-auth endpoints (Reddit `.rss`, X tweet-result/oEmbed/29 syndication, YouTube `yt-dlp`) BEFORE any WAF grid. Just pass the normal page30 URL; a Phase-0 win shows `verdict` with `profile=phase0:<platform>`.312. **curl_cffi TLS-impersonation grid.** Real Safari/Chrome/Firefox JA3/JA432 fingerprints × URL transforms (mobile subdomain, …) × referer strategies,33 tried exhaustively. This clears most WAFs (Cloudflare TLS, F5, AWS WAF, …).343. **Headless browser tier.** For JS challenges the curl grid can't clear35 (Cloudflare Turnstile, Akamai Bot Manager, DataDome), a local headless36 Chromium with stealth runs. There is **no Playwright-MCP in the sandbox** —37 this local browser IS the final rung. If it still fails, that is a real wall.3839You never pick a TLS profile, transform, or browser — the engine detects the WAF40and chooses. Your only knobs are `selector` and `device`.4142## R2 — HTTP 200 is NOT success4344The engine validates every response (4-layer: challenge markers / body size /45cookie sensor / your `selector`). A 200 that is actually a challenge or empty46SPA shell is reported as a FAILURE, not content. Trust the tool's `verdict` /47`[web_fetch OK|FAILED]` line, never a raw status. **Always pass a `selector`**48(e.g. `article`, `#content`, `[class*='product']`) when you know the content49marker — it upgrades a "looks clean" guess into proven success.5051## R6 — a give-up is NOT exhaustion (the discipline that matters)5253When `web_fetch` returns `[web_fetch FAILED]` it includes `stop_reason=` and54`grid_exhausted=`. **Do not declare a page unreadable on the first FAILED.**5556- **TERMINAL walls** — `stop_reason` is `auth_required` (login/paywall) or57 `not_found` (404). These are real: record it and move on; retrying cannot58 help.59- **Everything else** (`challenge`, `blocked`, `rate_limited`, or a60 non-exhausted grid) still has escalation left:61 - **rate_limited (429) is NOT terminal** — back off, then retry.62 - Retry once with **`device="mobile"`** (mobile TLS + `m.` subdomain often63 walk past a desktop block) and/or a precise **`selector`**.64 - Only after a terminal `stop_reason` may you honestly conclude the page is65 unreachable.6667This is the whole point of the engine: the cheap path failing is the *start* of68escalation, not permission to stop.6970## RoE & OSINT boundary7172- `web_search` is OSINT → not scope-gated (allowlisted provider egress only).73- `web_fetch` is target-gated → the `url` must be in `plan/roe.json` scope; an74 out-of-scope fetch returns `ROE_REFUSED`. SSRF-blocked (internal/metadata/75 private) hosts are refused regardless.76- **Reading public pages is OSINT, not exploitation.** Use `bash`/`http_request`77 for ACTIVE probing of in-scope target infrastructure; use `web_fetch` to READ78 pages (the target's own blocked/JS pages, or in-scope public assets).7980## Platform fast-paths (handled by Phase 0 — just pass the URL)8182| Need | Pass to web_fetch |83|---|---|84| Reddit thread/sub | the `reddit.com/...` URL (auto `.rss`) |85| A specific tweet / X profile | the `x.com/.../status/...` or profile URL |86| YouTube metadata / captions | the `youtube.com/watch?v=...` URL (yt-dlp) |87| GitHub repo (metadata, stars, language) | the `github.com/<owner>/<repo>` URL → repos API JSON |88| npm package (deps, versions, maintainers) | the `npmjs.com/package/<pkg>` URL → registry JSON |89| PyPI package (versions, deps, urls) | the `pypi.org/project/<pkg>` URL → JSON API |90| Any WAF-protected article/page | the page URL — the grid + browser tier handle it |9192GitHub/npm/PyPI return structured JSON (great for supply-chain / source recon)93instead of HTML — just pass the normal page URL and the engine routes it.9495For keyword discovery on a platform (e.g. "what's said about X on Reddit"),96`web_search("site:reddit.com <topic>")` first, then `web_fetch` the result URLs.