IT-RAT the stack/scopyx Live demo Guides Console
egress plane

Scopyx. Your agents already reach the web. This is the part that decides whether they may.

Every other plane in the stack governs what your agents do to your own services: what they spend, what they may call, who they are. None of them watches the one direction that leads outside. An agent reads a page, and the page tells it what to do next. An agent posts a customer list to an address it found in a prompt. Scopyx sits between an agent and whatever fetching tool you already run, and adds the four things that were missing: a decision before the request, a decision on every redirect, a bound on what comes back, and a record of the fetch and of the refusal.

shippedv0.1.2, two images and manifests
coreGo · one direct dependency
one way outhttp and https, and nothing else
failsclosed, and says which refusal
keepsthe origin and a hash, never the URL
licenceApache-2.0
one request, in flight

Five gates, in this order, and the order is the claim.

The scheme, then the host, then every address that name resolves to at the moment of the fetch, then your policy plane, and last the site's own robots.txt. The first three are local and free, so a cheap refusal never becomes a network call. Your policy is asked fourth, which means a destination you refuse is never contacted at all, not even for its robots.txt. Pick a request and watch where it stops.

the plane, one request at a time pick a run, or let it cycle
302, and the second destination is one nobody named your agent a URL, and no headers SCOPYX, THE PLANE YOU RUN scheme http or https, nothing else host not localhost, not .internal address every one it resolves to, now your policy robots.txt the site's own, asked last wardryx decides the open web nobody's promise 302 to another host hop 1, decided again
https://example.com/ writes web_fetch fetched

Nothing here is a network call until the fourth gate has answered. A scheme, a name and a set of addresses cost nothing to refuse, so the cheap refusals happen while the request is still inside your own process. The site's own preference is asked last, which is the ordering that matters: a destination your policy refuses is never contacted at all, not even to read its robots.txt.

one run, measured

The diagram argues. This is the receipt.

Four real requests, one container, the published image against a fixture policy and the live internet. Watch where each stops: a name pointing inside your own deployment is refused before any policy runs, and the request that never became a request is the last row.

scopyx v0.1.2 four requests, one container, one fixture policy, re-run 2026-08-10
policy said yes https://example.com/ fetched
policy said no https://example.org/secret-report deny_policy
address, before policy http://169.254.169.254/latest/meta-data/ deny_address
at the tool surface fetch_url(url, headers: {Authorization}) refused by name
The third row is the one worth reading twice. The fixture policy said allow, and it was refused anyway. Policy languages talk about domains an agent may reach, and none of them was written with the cloud metadata endpoint in mind: an agent that can be talked into fetching that address reads the credentials of whatever it runs on. The fourth is not a fetch at all. A free-form header parameter is a credential-laundering channel straight past a broker's inspection, so the tool has no field for one, and an unknown argument is refused by name rather than ignored.
the hop nobody asked for

A redirect is a destination the caller never named.

An allowed host answering 302 to a denied one is the oldest allowlist bypass there is, and it is invisible to any check that reads only the URL the caller passed. The same argument applies one layer down, to the name itself.

It refuses to follow

The fetcher it ships with returns the redirect instead of following it. Following inside the fetcher would be a second request that no decision preceded, which is exactly the bypass. Every HTTP client in every language does the opposite by default, which is why this is written down rather than assumed.

Every hop is resolved again

The target's name is looked up at the moment of the hop, never from anything remembered. An address resolved a minute ago would satisfy the check while the fetch reached something else.

And the socket goes where the decision looked

Every HTTP client resolves the name again, inside the dialer, with no memory of what was checked, and a hostile zone answers differently between the two lookups for nothing. So the checked addresses are pinned and the transport refuses to dial a host no decision covered, which makes it the floor under everything above it: a code path that reached out without asking cannot open a socket.

And bounded twice

Your configured depth, and a ceiling the plane keeps regardless. Two servers pointing at each other is a fetch that never returns, and a fetch that never returns is worse than one that is refused, because nothing reports it.

what it decides over

It is not a browser, and that is the whole design.

Scopyx supplies no capability you do not already have. Point it at the Firecrawl account you already pay for, the Browserbase you already run, or the fetcher it ships with, and the tool you own gains a decision, a bound and a record. Anyone proposing a feature here should ask whether it makes the fetch better or the fetch more governed. The first belongs in somebody else's product.

The one that needs nothing

The default fetcher wants no account, no API token and no browser on the host, so a box is governed on the day it is installed rather than the day somebody arranges a fetching service. It runs no JavaScript, and the result says so, because a page assembled in a browser arrives as the shell that assembles it.

The one your invoice already covers

Wrapping a service you already run is the commercially important case and the least impressive code in the repository. A destination your policy refuses never reaches that service, so it never appears in your bill for it either.

And a browser you installed

It runs the page's own JavaScript, so a document assembled in the browser arrives assembled. Nothing is bundled and nothing is downloaded: the image stays small, and a missing browser is refused at startup with a message about the browser rather than at the first fetch with a message about the network.

try breaking it

A page is forty requests nobody named. Here is what stops them.

A rendering backend is the first one that fetches things the caller never asked for: fonts, images, scripts, each potentially a different host. Two separate things stop a refused one, and they are not redundancy for its own sake. Switch either off and see. Switch both off and see that too, which is exactly the experiment the test suite runs.

one page, one subresource your policy refuses switch a mechanism off
A BROWSER YOU INSTALLED chromium fresh profile, deleted after the page asks for a font on allowed.example a pixel on tracker.example the proxy scopyx owns it the only route out of the browser THE FLOOR a socket that is not opened CDP interception the full URL of every request, including inside TLS THE ACCOUNTANT allowed.example dialled at the checked address tracker.example: refused and counted as blocked in the record the tracker's own server saw nothing the open web reached only through the floor
both on refused twice over never reached

Two mechanisms, and they are not the same kind of thing. The proxy is the floor: the browser is launched with no route to the network except it, so a refused destination is a socket that is never opened, and no flag or version of somebody else's browser can talk it out of that. CDP interception is the accountant: it sees the full URL of every request, including inside TLS, which the proxy cannot, so it produces the counts and the per-URL decisions. Where the two disagree the connection wins, because the connection is what carries bytes.

what the answer admits

Governed at the navigation is not governed per request.

A fetch through a service you already run is decided before it is made, and then that service loads the page's images, fonts and scripts with nothing in between. The navigation was governed. The requests it caused were not. Every result says which of the two guarantees was in force, so a partial answer is visible as one rather than read as a complete one.

backend  passthrough-http
enforcement  per_request  · every request this fetch made was decided
content_bytes  559
subresources_requested  0  · it asked for nothing, and knows it
subresources_blocked_by_policy  0
redirect_hops  0
truncated_by  null

A count nobody can supply is null

Zero says the page asked for nothing. Null says nobody knows. Reporting the second as the first claims perfect fidelity for exactly the backend that can see the least, and an agent that does not know it read half a page will report confidently on the half it got.

Nothing extracted with a failure is not an empty page

It is an error. For a person, degrading to a blank frame is right. For an agent it is the worst failure available, because the model cannot tell the difference and you cannot either.

what is kept

A URL is personal data, and the record is built around that.

https://crm.example/customers/12345?email=jane@example.com is an address and also a name, an identifier and a contact detail. The path and the query string, which is exactly where an identifier or a session token lives, are never assembled into the event at all. What is kept is the origin and a SHA-384, so two records can be compared without either holding the address.

one URL, and what survives into the record retention is yours to set
ONE URL AN AGENT WAS ASKED TO FETCH https://crm.example /customers/12345?email=jane@example.com&session=8f2c... an address a name, an identifier, a contact detail, and a credential THE EVENT THIS PLANE WRITES origin https://crm.example url_sha384 sha384:0ae4006e9bd9fa46... url_unprotected https://crm.example/customers/12345?email=jane@example.com&session=8f2c... two records compare without either holding the address

The path and the query string are not dropped later or redacted later. The field is never built, so there is no window in which the event holds them and no code path that could log one by accident.

Its own journal, its own volume

A component that can write the shared log can, once compromised, corrupt the trail it was adding to. This is the one part of the box that reaches the public internet on purpose, which makes it the last one that should be able to rewrite anybody else's record.

Chained, so a deletion shows

Every line names its predecessor, in the same envelope the rest of the stack speaks. A journal that did not chain would look identical until somebody tried to verify it, which is the moment it matters and the moment it is too late.

No identity, no record, and the skip is counted

A fabricated subject makes every downstream count wrong and puts a name on an alert that did not do the thing. A fetch with no authenticated agent behind it is not written, and the number of times that happened is reported rather than hidden.

the boundaries

What it will not do, stated rather than discovered.

No stealth, no CAPTCHA solving, no TLS-fingerprint matching, no bulk crawl, no image harvesting, and robots.txt is honoured. It identifies itself as scopyx and never as a browser.

Because of what this is for

Defensive tooling for an operator governing their own agents. A component that defeated a third party's controls would be the first thing here useful to somebody attacking a stranger, and that is a different product.

And because of Article 5

The EU AI Act prohibits untargeted scraping of facial images to build recognition databases. A bulk-crawl mode is the feature that turns a governance tool into that, so there is not one.

The one that needed code

Every other line above holds because no code path exists to do it, and an absence enforces itself. Honouring robots.txt is the only one that had to be written: group boundaries, longest match, Allow beating Disallow at equal length, and both wildcards. It reads as one item in a list and it is not one.

docker run ghcr.io/taipanbox/scopyx:v0.1.2 it refuses to start bound wide with no credentials, deliberately
docker run ghcr.io/taipanbox/scopyx:v0.1.2-chromium the same service with a browser: 267 MB to pull against 3.5 MB

Scopyx is not an AI system under Article 3(1): it applies rules you wrote and infers nothing. Used beside one, it supplies evidence for Article 12 record-keeping and Article 14 human oversight. The wording here is "covers the requirements of Article 12", never "AI Act compliant", because there is no certification and no auditor behind those words, and a claim nobody can hold is worse than no claim.