AutoGen's `UserProxyAgent` with `code_execution_config` is a loaded gun pointed at your host. The default setup runs arbitrary Python in a subprocess with full filesystem and network access. That's fine for a local playground, but deploying it anywhere near real data or infrastructure is negligent.
The core problem is the `LocalCommandLineCodeExecutor`. It's a thin wrapper around `subprocess`. The good news: you can inject a hardened executor. The goal: a read-only filesystem (except a defined scratch directory) and a strict network egress whitelist.
Here's a concrete implementation using `seccomp`, `pivot_root`, and `iptables` via a wrapper script. This assumes a Linux host.
First, create a Python script that acts as our secured code executor (`secured_executor.py`). It uses `os.chroot` and resource limits.
```python
import sys, os, subprocess, resource, tempfile, json
def secure_setup(scratch_dir: str):
# 1. Limit CPU and memory
resource.setrlimit(resource.RLIMIT_AS, (100 * 1024 * 1024, 100 * 1024 * 1024)) # 100MB
resource.setrlimit(resource.RLIMIT_CPU, (5, 5)) # 5 seconds
# 2. Create a minimal, read-only filesystem view (simplified; full isolation needs namespaces)
# For true read-only, use a combination of chroot and bind mounts (setup done in shell wrapper).
os.chdir(scratch_dir)
if __name__ == "__main__":
data = json.loads(sys.stdin.read())
code = data["code"]
scratch = data.get("scratch", "/tmp/code_scratch")
if not os.path.exists(scratch):
os.makedirs(scratch)
secure_setup(scratch)
# Write code to a temporary file inside scratch
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', dir=scratch, delete=False) as f:
f.write(code)
script_path = f.name
try:
result = subprocess.run(
[sys.executable, script_path],
capture_output=True,
text=True,
timeout=4,
cwd=scratch
)
output = {"output": result.stdout, "error": result.stderr, "code": result.returncode}
except Exception as e:
output = {"output": "", "error": str(e), "code": 1}
finally:
os.unlink(script_path)
print(json.dumps(output))
```
That's the inner Python. The real hardening happens in a shell wrapper that sets up namespaces and firewall rules. This wrapper (`run_secured.sh`) uses `unshare` and `iptables`.
```bash
#!/bin/bash
# This requires root or appropriate capabilities
SCRATCH_DIR="/tmp/agent_scratch"
mkdir -p $SCRATCH_DIR
# Create network namespace (requires privileged)
ip netns add agent-ns
# Bring up lo, create veth pair, attach to host bridge, apply iptables drop by default
# ... (detailed iptables/network code omitted for brevity, but must whitelist specific IPs)
# Mount a tmpfs for /, bind-mount scratch dir as writable, make everything else read-only or inaccessible
unshare --mount --uts --ipc --net=/var/run/netns/agent-ns --chroot=/tmp/empty_root bash -c "
mount -t tmpfs tmpfs /tmp
mkdir -p $SCRATCH_DIR
mount --bind $SCRATCH_DIR $SCRATCH_DIR
# Now execute the Python secured_executor.py with the code passed via stdin
python3 /path/to/secured_executor.py
"
```
Finally, integrate it into AutoGen by subclassing `LocalCommandLineCodeExecutor`.
```python
from autogen.code_utils import LocalCommandLineCodeExecutor
class HardenedCodeExecutor(LocalCommandLineCodeExecutor):
def __init__(self, scratch_dir: str = "/tmp/agent_scratch", **kwargs):
self.scratch_dir = scratch_dir
# Use our shell wrapper as the command
command = ["sudo", "/path/to/run_secured.sh"]
super().__init__(command=command, **kwargs)
def format_code_for_subprocess(self, code: str) -> str:
import json
return json.dumps({"code": code, "scratch": self.scratch_dir})
# Use it in your agent
agent = UserProxyAgent(
name="hardened_executor",
code_execution_config={
"executor": HardenedCodeExecutor(scratch_dir="/tmp/agent_workspace"),
"work_dir": "/tmp/agent_workspace" # This is the host dir bound inside
}
)
```
Key points:
* This is a blueprint. The network namespace and iptables rules need careful configuration to allow only specific outbound connections (e.g., to your internal APIs).
* The filesystem isolation prevents writes outside the scratch directory. Use `tmpfs` for the root inside the namespace.
* You must audit the allowed Python modules; consider using `sys.settrace` or `importlib` hooks to block dangerous modules (`os`, `subprocess`, `socket` can be restricted or monitored).
* This adds overhead. It's not for every use case, but for any production-adjacent deployment, it's non-negotiable.
Without these measures, a single malicious LLM response can `rm -rf /`, exfiltrate data, or pivot to internal networks. The default config is a textbook example of a "default-unsafe" pattern.
—priya
trust, but verify — with sigtrap