Isolation¶
Policy and risk decide whether a call runs. Isolation limits what it can do if it runs and turns out to be malicious: a test suite from an untrusted repository, a build script, or a command an agent was tricked into. AgentGuard provides three layers.
Containers¶
ContainerExecutor runs allowlisted commands in Docker or Podman, optionally under
gVisor for a user-space kernel.
from agentguard.execution import ContainerExecutor
tests = ContainerExecutor(
"python:3.12-slim",
{"run_tests": ["python", "-m", "pytest", "-q"]},
workspace="./workspace", # mounted at /workspace, read-only by default
runtime="runsc", # gVisor; omit for the engine's default runtime
timeout=300,
check=False, # return failing tests as results
)
@guard.tool(capability="shell.execute", executor=tests)
def run(cmd: str) -> dict:
"""Run an allowlisted command: run_tests."""
raise AssertionError("The container runs instead")
| Default | Meaning |
|---|---|
network="none" |
No network at all |
--read-only root, --tmpfs /tmp (noexec, 64 MB) |
Nothing outside the workspace and /tmp is writable |
--cap-drop ALL, no-new-privileges |
No Linux capabilities; setuid binaries cannot escalate |
user = nobody (65534:65534) |
Not root. With workspace_mode="rw", the host's own uid:gid so files stay writable (never root) |
memory="512m", cpus="1", pids=128 |
Resource limits, no swap |
workspace_mode="ro" |
The workspace is read-only unless you pass "rw" |
pull="never" |
Only images already present locally run |
--init, --rm, unique name |
Child processes are reaped; the container is removed; a timeout or output overflow kills it by name |
Only an exact allowlisted command name selects an argv; the model never supplies the
command line. Environment variables reach the container only through environment=.
What containers do not solve
A container shares the host kernel unless you use gVisor or a VM-based runtime. The workspace you mount is reachable. Images must be trusted and pinned (for example by digest). Follow your engine's hardening guide (rootless mode, user namespaces).
DNS pinning¶
NetworkExecutor resolves the host once, requires every address to be public, and
connects to exactly those addresses, while TLS still verifies the certificate for the
hostname. A DNS server that answers with a public address for the check and a private one
for the connection (DNS rebinding) cannot redirect the request. Redirects and environment
proxies are disabled.
Egress proxy¶
For tools that need some network access (a package install, a documentation fetch), run the allowlisting proxy and make it the only way out:
agentguard egress-proxy --allow pypi.org --allow files.pythonhosted.org --port 3128 \
--audit egress.jsonl
Point the tool at it with HTTPS_PROXY=http://127.0.0.1:3128. The proxy:
- accepts only
CONNECT host:port, for allowlisted domains and ports (443 by default); - resolves each host once, refuses non-public addresses, and tunnels to the checked address;
- answers everything else with
403,405or502, and records each decision in the audit log when--auditis set.
It cannot inspect TLS, so a client could present a different SNI or Host to a server it
is allowed to reach (domain fronting on shared CDNs). Use it together with network
isolation, for example a container network whose only route is the proxy, so tools cannot
bypass it.
from agentguard.execution.egress import EgressProxy
proxy = EgressProxy(["pypi.org", "files.pythonhosted.org"], on_decision=guard.audit.append)
host, port = proxy.start() # background thread; proxy.stop() when done