A deterministic gate at the fetch boundary: a 200 is transport success, not a read.
Project description
FetchGate
A 200 is not a read.
When an AI agent fetches a page and it quietly comes back empty (a JavaScript app, a 403, a Cloudflare wall), the agent often does not stop. It answers from memory and never tells you it did not actually read the page. FetchGate is a small gate at the fetch boundary that refuses to let that happen: the agent gets the content only when the page was really read, and an honest refusal otherwise.
It is deterministic (no model, no API keys), stdlib-only at its core, and free.
Quickstart
git clone https://github.com/MoAz06/FetchGate
cd FetchGate
python demo.py https://example.com https://httpbin.org/status/403
https://example.com
verdict : RETRIEVED (clean)
answerable : ALLOW
bytes : raw 559 / extracted 127
tiers : static
gate-confirmed content (127 chars): Example Domain This domain is for use ...
https://httpbin.org/status/403
verdict : FAILED (transport.http_403)
answerable : STOP
refused: the model gets no content and cannot answer as if it read this page.
Run the tests (no network, no model):
pip install -e .
python -m unittest discover -s tests
Use it in Claude Desktop or Cursor
pip install ".[mcp]" # the fetch tool
pip install ".[browser]" # optional: render JavaScript pages
python -m playwright install chromium
Add to your MCP config (details in docs/mcp.md):
{ "mcpServers": { "fetchgate": { "command": "fetchgate-mcp" } } }
Your agent now has a fetch_url tool that returns content only for a confirmed
read, and a refusal otherwise.
Use it in your own agent
from fetchgate import default_gate
gate = default_gate() # renders JS pages if the [browser] extra is installed
def read_url(url: str) -> str:
out = gate.guarded_fetch(url)
content = gate.content_for(out.handle)
if content is None:
return f"NOT READ: {url} ({out.result.reason}). Do not answer as if you read it."
return content
How it decides
Every fetch gets one verdict:
- RETRIEVED: a real read. The agent may answer and cite it.
- FAILED: a definite non-read (a 4xx or 5xx, a timeout, an https to http downgrade, an empty body). Stop.
- UNKNOWN: fetched, but not enough readable content to confirm a read (often a JavaScript page). Stop, or render and try again.
Three layers feed the verdict, and the result is the strictest of them:
- transport: status and redirect chain.
- extraction: readable text kept separate from raw bytes. A 17 byte price JSON is a read, a 400 KB article at 2 percent text is a read, a JS shell with no text is not.
- content-validity: a best-effort fingerprint for Cloudflare, consent, paywall, and soft-404 walls. It can only downgrade to UNKNOWN, never fake a read.
When a page looks thin or empty and the render tier is on, FetchGate escalates to a real headless browser and re-checks. It renders, it does not evade: a page still walled after an honest render stays a non-read.
Stated limits
content_sha256is a receipt anchor, not tamper-evidence. A cloaked page hashes cleanly.- Provenance is not authenticity. The gate closes fail-silent-on-empty, not fetch-succeeds-on-wrong-content.
- A read is not an answer. RETRIEVED means enough content arrived, not that it held the fact you need. Claim support is a downstream job.
- Fail-closed has an availability cost. Anyone who can make your fetch fail can force a refusal. That is the trade for correctness.
What is built
The deterministic core, the offline evaluation with a signed manifest, the
build-breaking cheat-test, the MCP server, and the Playwright render tier. Optional
extras: [mcp], [browser], [extract] (trafilatura), [sign] (Ed25519).
MIT licensed.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fetchgate-0.1.0.tar.gz.
File metadata
- Download URL: fetchgate-0.1.0.tar.gz
- Upload date:
- Size: 28.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
438123a439eb4a9f235457d9cbf4a14d634653c19c50db0a10020b3b4393d03a
|
|
| MD5 |
64194d52a771facaad1047c1e6c3bea4
|
|
| BLAKE2b-256 |
8625bbe10fef5ff3dd40b9f6195a039698b5462b608e86f41b3f4ad3e1fd61f6
|
File details
Details for the file fetchgate-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fetchgate-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36cef34149b8ad235417025b3b6584e3cd59bbb4e50b6a9fca6329db75b3b664
|
|
| MD5 |
8cec943d8008bfd4fcc60bfb8ca8da24
|
|
| BLAKE2b-256 |
7ee09d1c7d4d0d855c94d55c37c9f0ea61aec5e8aad4565e5a727b1207225b29
|