n4nAI

Setting up a sandboxed environment for computer-use agents

Step-by-step guide to building a secure Docker sandbox for computer-use agents: isolate browser automation, limit resources, and filter network egress.

n4n Team3 min read613 words

Audio narration

Coming soon — every post will get a voice note here.

A sandbox computer use agent needs hard boundaries: the model can click, type, and run shell commands, but a mistake or prompt injection shouldn’t reach your host. This guide builds a reproducible Docker-based sandbox that runs a headless browser and a minimal command API, giving you a safe place to test agent loops. We’ll cover image definition, kernel-level restrictions, network isolation, and wiring an LLM control plane.

Step 1: Define a minimal container image

Start from a stripped Ubuntu base and install only what the agent requires. A full desktop image is unnecessary; Xvfb provides a virtual display for browser automation without a window manager.

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
    python3.11 \
    python3-pip \
    xvfb \
    libnss3 \
    libnspr4 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libxkbcommon0 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    && rm -rf /var/lib/apt/lists/*

RUN pip3 install playwright openai fastapi uvicorn
RUN playwright install chromium

WORKDIR /agent
COPY agent_server.py /agent/agent_server.py

The sandbox computer use agent will execute inside this image. Keep the layer count low and avoid copying host secrets into the build context.

Step 2: Restrict syscalls with seccomp

Docker’s default seccomp profile blocks many dangerous calls, but a computer-use agent that drives a browser still gets more than it needs. Drop mount, reboot, and bpf explicitly.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": [
        "read", "write", "close", "fork", "wait4", "execve",
        "exit", "exit_group", "mmap", "munmap", "brk", "rt_sigaction",
        "rt_sigprocmask", "sigreturn", "ioctl", "poll", "select",
        "clock_gettime", "clone", "dup", "dup2", "pipe", "socket",
        "connect", "accept", "sendto", "recvfrom", "bind", "listen"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Save this as seccomp-agent.json. The whitelist covers browser and Python runtime needs while rejecting privilege escalation vectors.

Step 3: Run with resource limits and user namespaces

Launch the container with hard caps. Memory and CPU limits prevent a runaway agent loop from starving the host. User namespaces map the container root to an unprivileged host UID.

docker run -d \
  --name agent-sandbox \
  --memory=512m \
  --cpus=1.0 \
  --security-opt no-new-privileges \
  --security-opt seccomp=seccomp-agent.json \
  --cap-drop ALL \
  --user 1000:1000 \
  --network agent-internal \
  agent-image:latest

Running the sandbox computer use agent with --cap-drop ALL removes even basic capabilities like CHOWN. If the agent needs to bind port 8000, it can because that’s above privileged range; no NET_BIND_SERVICE required.

Step 4: Isolate network egress

Create a dedicated bridge network with no external routing. The agent can talk to itself but cannot reach your LAN or the internet unless you explicitly proxy.

docker network create --internal agent-internal

If the agent must fetch public web pages for the task, run a separate squid proxy container on a different network and only allow the sandbox to connect to that proxy IP. Never attach the sandbox to the default bridge with internet access.

Step 5: Install agent control dependencies

The container runs a tiny FastAPI server that accepts JSON actions ({"action": "click", "selector": "#btn"}) and returns observations. This is the only interface the outer loop uses.

# agent_server.py
from fastapi import FastAPI
from playwright.sync_api import sync_playwright
import uvicorn

app = FastAPI()
browser = None

@app.on_event("startup")
def startup():
    global browser
    with sync_playwright() as p:
        browser = p.chromium.launch()

@app.post("/act")
def act(cmd: dict):
    page = browser.new_page()
    if cmd["action"] == "goto":
        page.goto(cmd["url"])
    elif cmd["action"] == "title":
        return {"result": page.title()}
    page.close()
    return {"result": "ok"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Start it under Xvfb:

Xvfb :99 & DISPLAY=:99 python3 agent_server.py

Step 6: Wire the LLM control plane

The outer loop calls a model to decide the next action. Point the OpenAI client at an OpenAI-compatible gateway so you can swap models without code changes. n4n.ai exposes one endpoint fronting 240+ models and automatically falls back when a provider is rate-limited, which keeps the sandbox computer use agent resilient during long tasks.

import openai, requests

client = openai.OpenAI(
    base_url="https://api.n4n.ai/v1",
    api_key="YOUR_KEY"
)

def decide(action_history):
    resp = client.chat.completions.create(
        model="anthropic/claude-3.5-sonnet",
        messages=[
            {"role": "system", "content": "You control a browser. Return JSON actions."},
            {"role": "user", "content": str(action_history)}
        ]
    )
    return resp.choices[0].message.content

The gateway honors client routing directives and forwards provider cache-control hints, so repeated context across agent steps can hit provider prompt caches.

Step 7: Verify the sandbox with a test task

Run a scripted task: navigate to a local test page, extract the title, and write a marker file inside the container. Then confirm the host is untouched.

# test_run.py
import requests, json
r = requests.post("http://localhost:8000/act", json={"action":"goto","url":"http://example.com"})
print(r.json())
r = requests.post("http://localhost:8000/act", json={"action":"title"})
print("TITLE:", r.json()["result"])

Execute from the host against the forwarded port (map -p 8000:8000 only for testing, remove for production). Verify success by checking that /agent/marker.txt exists inside the container but no new files appear in your host home directory:

docker exec agent-sandbox ls /agent
ls ~ | grep marker.txt && echo "LEAK" || echo "HOST CLEAN"

If the container crashes on memory limit, dmesg on host shows OOM kill—expected under constraint. The sandbox computer use agent is now isolated: it cannot spawn privileged processes, cannot reach the network directly, and cannot write outside its mount.

Hardening notes

  • Mount /agent as tmpfs to avoid disk persistence across runs.
  • Use --read-only root filesystem and a writable scratch volume.
  • Rotate the LLM API key per sandbox instance; per-token metering on the gateway lets you attribute cost to each agent session.
  • For browser agents, disable WebRTC and geolocation via Playwright context to reduce data leakage surface.

Following these steps gives you a repeatable, auditable environment to run untrusted model-driven workflows without betting your infrastructure on their correctness.

Tagscomputer-usesandboxingsecurityhow-to

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All computer-use & browser agents posts →