egress-guard
Deterministic, dependency-free detection of two things a task run inside a sandbox has no legitimate reason to do:
- reaching for the host — the container runtime socket, host memory, the cloud metadata endpoint, the orchestrator's credentials; and
- contacting a host it never declared — any external host outside the run's own egress allowlist.
It is defensive. It recognises these patterns in order to flag them and stop a run. It performs none of them, and it executes nothing it inspects — every rule is a regular expression over the text of an agent's tool calls and transcript.
from egress_guard import run_detectors
hits = run_detectors(tool_calls, egress_allowlist=["*.example.com"], messages=transcript)
for hit in hits:
print(hit.category, hit.rule, hit.evidence)
The one rule that matters
Internal is fine; external is not.
A run whose task is to probe or attack its own declared target inside the sandbox is working correctly and must never be flagged — otherwise the guard gets switched off, which is worse than no guard. The alarm is only for reaching outside the declared boundary. So the caller's egress allowlist is not just the enforcement boundary; it is the definition of "external" that the library tests against. With an empty allowlist, every external host is flagged — the correct default for a run that declared no egress.
Offensive-shaped tooling is judged by where it points, not by its name: the same scanner aimed at a declared internal target and at a public address are different events, and only the second is a hit.
Why deterministic, not a model
These behaviours are not subtle. They are enumerable, and a named rule for each has no false-negative rate from model weakness and needs no calibration. That is the opposite regime from catching subtle deception, where rules are useless and you need a model. Use the right instrument for each: this is the cheap, exhaustive one for the blatant cases.
Evasion is handled before matching
Anything that changes a command's spelling without changing its effect is normalised first, so a literal rule sees through it:
| written as | normalised to |
|---|---|
curl http://2852039166/ |
169.254.169.254 (integer-encoded IP) |
curl http://0xa9fea9fe/ |
hex-encoded IP |
n''m''a''p -p- … |
nmap … (shell-quote splitting) |
echo <base64> | base64 -d | sh |
the decoded payload, appended |
API
run_detectors(tool_calls, egress_allowlist=(), messages=())→list[Hit]— the main entry point; scans both what the agent did and what came back, and labels anything found only in the transcript as a mention rather than an action.detect_host_escape(tool_calls)→list[Hit]detect_undeclared_egress(tool_calls, egress_allowlist=())→list[Hit]is_internal(host)→boolnormalise(text)→str
Each Hit carries a detector_id, a category (sandbox_escape or external_attack),
a human-readable rule, the matched evidence, and the index of the tool call it was
found in.
The names run_containment_detectors, detect_sandbox_escape, and
detect_external_attack are kept as aliases for callers migrating from an in-tree copy.
Scope, honestly
This is a tripwire, not a sandbox. It tells you an agent reached for the exit; it does not contain anything, and a determined adversary who knows the exact rules can craft around them. Its value is catching the blatant, enumerable cases cheaply and without a false-negative rate — pair it with a real sandbox and, for subtle cases, a model-based monitor. Do not rely on it as your only boundary.
Licence
Apache-2.0. Built for AnyEval; useful anywhere agent traces need a cheap, deterministic boundary check.
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 egress_guard-1.0.0.tar.gz.
File metadata
- Download URL: egress_guard-1.0.0.tar.gz
- Upload date:
- Size: 15.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f2fd8937e7972e6a1c8d9e9af342f50458a9ef7d821e2de1035244ad6826f92
|
|
| MD5 |
6b5925c22b873d5028d5610cd040272f
|
|
| BLAKE2b-256 |
0525d16fd1e38c1de608b710680f4e932222b5506598996a69641e021365bd6c
|
File details
Details for the file egress_guard-1.0.0-py3-none-any.whl.
File metadata
- Download URL: egress_guard-1.0.0-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc486210bd233ac255f374daf644a9236e8bce5a542caa73b9357d6478085799
|
|
| MD5 |
c8e732816d04819df8494ed32dfed311
|
|
| BLAKE2b-256 |
823317a55950dd3a0cb3b0412a3f8090366ae5e384ce01e454c3bebbfe734978
|