Running autonomous AI agents inside NVIDIA's OpenShell runtime
OpenShell is an NVIDIA project — github.com/NVIDIA/OpenShell — described as a safe, private runtime for fleets of autonomous AI agents. In practice, a runtime is the layer that starts, isolates and supervises agent processes: your Python loop is the payload, the runtime is the box it lives in. A fleet just means more than one agent running at once.
One honest caveat before we start. The public announcement disclosed the repository URL and a single sentence of purpose — nothing about the license, supported platforms, entrypoint flags or concurrency limits. So this walkthrough never guesses those values. It shows you where to read them out of the repo in Step 1, then builds a hand-rolled sandbox around the agent payload so the isolation properties hold regardless of what the runtime ships. Budget 30–45 minutes, longer if Docker or GPU drivers need a fresh install.
What I checked before touching the repo
My working baseline for a small fleet: Linux x86_64 (Ubuntu 22.04 or 24.04 in the examples), git 2.x, Docker Engine 24+ with the Compose v2 plugin and access to the Docker socket, 4 CPU cores, 8 GB RAM and 20 GB of free disk. Local models need more, and GPU-backed agents need the NVIDIA driver plus nvidia-container-toolkit. Export your model provider key as an environment variable — never put it in the repository.
git --version docker --version docker compose version nvidia-smi # GPU hosts only df -h /var/lib/docker You should see git version 2.x, Docker version 2x.x, Docker Compose version v2.x, an nvidia-smi table if you have a GPU, and a df line with at least 20G free.
Quick pre-flight list: docker info returns cleanly, your user sits in the docker group (groups | grep docker), ports 8080 and 3000 are free (ss -ltnp | grep -E '8080|3000' prints nothing), and test -n "$MODEL_API_KEY" && echo OK prints OK.
Step 1: Clone, then read the actual contract
Don't assume the layout. A runtime project might ship a container image, a compiled binary, a Python package, or all three at once — and which one you get changes everything that follows.
mkdir -p ~/agents && cd ~/agents git clone https://github.com/NVIDIA/OpenShell.git cd OpenShell ls -la head -n 80 README.md On a fresh Ubuntu 22.04 host the clone ends with Resolving deltas: 100% ... done. and ls -la lists README.md, LICENSE and the project subdirectories. The top of the README usually carries an install command and a quick start block — treat that block as the source of truth for entrypoint flags.
Next, pin down the two facts the announcement left open:
sed -n '1,20p' LICENSE grep -rIl -i -E 'docker|compose|requirements|pyproject|package.json' . | head -n 20 LICENSE opens with the license name (Apache, MIT, whatever it happens to be) — write it down, because it decides whether you can ship agents commercially. The grep output tells you which packaging files exist. If a Dockerfile or docker-compose.yml shows up, the container path below works. If none of them exist, stop here and follow the README's native install path instead of forcing my recipe onto it.
Step 2: Build a boxed-in agent image
An autonomous agent loops, calls tools and fetches URLs on its own. Without a box, one bug has the whole machine as its blast radius. The image below strips away everything the agent doesn't strictly need: a non-root user, a minimal base, no writable filesystem baked in.
mkdir -p ~/agents/fleet && cd ~/agents/fleet cat > agent.py <<'PY' import os, time task = os.environ.get("AGENT_TASK", "noop") print(f"agent started, task={task}", flush=True) for step in range(1, 4): print(f"step {step}: working on {task}", flush=True) time.sleep(1) print("agent finished", flush=True) PY cat > Dockerfile <<'DOCKER' FROM python:3.12-slim WORKDIR /app COPY agent.py . RUN useradd -m agent USER agent CMD ["python", "agent.py"] DOCKER docker build -t openshell-agent:local . The build finishes with Successfully tagged openshell-agent:local. That image is an interchangeable payload: in production you swap agent.py for your real agent loop and leave every isolation flag untouched. If Step 1 revealed an official OpenShell image or entrypoint, use it in place of this one.
Step 3: Describe the fleet in Compose
Here is where the sandbox gets defined. Four lines do most of the work.
services: agent: build: . image: openshell-agent:local environment: - AGENT_TASK=demo - MODEL_API_KEY=${MODEL_API_KEY} read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true networks: - agent-net networks: agent-net: driver: bridge internal: true Validate before launching: docker compose config prints the fully resolved YAML with MODEL_API_KEY expanded and exits 0. If the variable is unset you get required variable MODEL_API_KEY is missing a value and the run halts — which is precisely the guard you want, since it means a missing secret can never silently fall through to an empty string.
Why each flag earns its place: read_only: true freezes the container filesystem, tmpfs: /tmp hands the agent exactly one scratch space that disappears with the container, no-new-privileges blocks setuid escalation, and internal: true removes any route to the internet.
Step 4: Launch three replicas and try to break out
cd ~/agents/fleet export MODEL_API_KEY=<your-provider-key> docker compose up -d --scale agent=3 docker compose ps docker compose logs --tail=20 agent docker compose ps shows fleet-agent-1, fleet-agent-2 and fleet-agent-3 all in state running, and the logs repeat the same block per replica: start line, three work steps, finish line.
Now the two tests that actually matter — the negative ones:
docker compose exec agent sh -c 'wget -q -T 5 -O- https://example.com || echo EGRESS-BLOCKED' docker compose exec agent sh -c 'touch /app/probe || echo FS-READ-ONLY' You want EGRESS-BLOCKED from the first and FS-READ-ONLY from the second. If wget returns an HTML page, the agent can reach the open internet and your network policy never applied. If touch /app/probe succeeds, read_only: true is missing from the running service.
The trade-off: no egress means no hosted model
internal: true cuts both ways. An agent pinned to that network cannot call a hosted model API, so a naive fleet will start, run, and then fail the moment it needs inference. The two escapes I'd consider are a second, allowlisted network attached to the same service, or an HTTP proxy that logs and filters outbound requests. Both are policy decisions you have to make deliberately. The runtime itself may offer its own egress controls — check the repository docs rather than assuming, because the announcement said nothing about them.
Takeaway
The isolation flags cost almost nothing to write and they are the difference between a demo and something you'd leave running overnight. Clone first, read the license and packaging files before you trust any install command, keep the agent in a read-only container with a throwaway /tmp and no egress, then prove both properties with negative tests instead of hoping. And when a project's public announcement stays quiet about licenses, flags and limits, treat the repository as the only documentation that counts.
This article was originally published on AI Release — https://ai-release.org — where I keep notes on self-hosted AI tooling; there's also a channel at https://t.me/ai_release1 if you'd rather follow along there.