---
title: "OpenShell Quickstart"
description: "Run an agent in an NVIDIA OpenShell sandbox so its model traffic can leave only through Viper, signed with the agent's own key."
source: "Guardian Platform"
---
[OpenShell](https://github.com/NVIDIA/OpenShell) runs an agent in a sandbox whose network access is set by a policy. With Viper in the sandbox and a policy that lets only Viper reach the Agent Gateway, the agent's model requests are signed and audited, and the agent has no other way out.

This page takes you from an empty machine to a signed request from inside a sandbox that you can see in Guardian.

## How it works

```text
agent ──loopback──► viper proxy ──► gateway ──► provider
          (inside the sandbox)    (only viper may connect)
```

- **Viper runs beside the agent in the sandbox.** A small entrypoint starts `viper proxy`, then the agent with the proxy as its base URL.
- **The policy lets only the Viper binary reach the gateway.** The agent gets no network rule, so OpenShell refuses its connections to the gateway, to the provider, and everywhere else.
- **Viper is never above the agent in its process tree.** OpenShell lets a process use the network access of any program above it, so an agent started by Viper would share its route to the gateway. Inside a sandbox, `viper claude`, `viper codex`, `viper opencode` and `viper openclaw` refuse to start for this reason.
- **The gateway rejects anything Viper did not sign.** Even a process that does reach the gateway gets no further without Viper's signature.
- **Viper's connection to the gateway is not decrypted by OpenShell.** The policy sets `tls: skip` on the gateway rule, so OpenShell relays Viper's TLS untouched and cannot read or re-send signed requests.
- **The agent is enrolled before the sandbox starts.** You enroll on your own machine and upload the result; no Guardian Platform API key enters the sandbox.

## Before you start

- **OpenShell 0.1.4 or a later 0.1 release**, with the Docker driver, on Linux (x86_64 or aarch64) or macOS on Apple silicon. Install it with the installer script from the [OpenShell repository](https://github.com/NVIDIA/OpenShell). On macOS, turn on host networking in Docker Desktop (**Settings → Resources → Network**) and leave Enhanced Container Isolation off, as OpenShell cannot use it; without host networking, sandboxes fail to start with `ControlSupervisorStartFailed`.
- **Viper** on your own machine, pointed at your gateway. See [Install Viper](./install-viper.mdx). The sandbox image uses the Viper container image, so log in to `docker.cloudsmith.io` as that page describes.
- **An API key with the Register Agents scope**, or the **Register Agents** permission to get a registration token from Guardian, to enroll the agent. See [Register Agents](./register-agents.mdx#1-generate-an-enrollment-key).
- **Your own OpenAI API key.** The gateway does not hold provider keys; your key passes through it to OpenAI unchanged.
- **Your gateway URL**, which your administrator gives you, for example `https://gateway.example.com`. Guardian's **Register Agent** dialog also shows it in the commands it gives you, when your deployment is configured to.

## 1. Build the sandbox image

Make a directory with four files.

`Dockerfile`:

```dockerfile
FROM docker.cloudsmith.io/eqtylab/prod/viper:latest AS viper

FROM debian:bookworm-slim

# OpenShell needs iproute2 in the image.
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl iproute2 libpcsclite1 python3 \
    && rm -rf /var/lib/apt/lists/*

# Copy over `viper` and its libraries, as the binary itself rather than the
# image's link into /nix/store, which the sandbox policy would not match.
COPY --from=viper /nix/store/ /nix/store/
COPY --from=viper /usr/local/bin/ /tmp/viper-bin/
RUN cp -L /tmp/viper-bin/viper /usr/local/bin/viper && rm -rf /tmp/viper-bin

COPY viper-sidecar /usr/local/bin/viper-sidecar
COPY agent.py /sandbox/agent.py

# OpenShell runs the sandbox as a non-root user with a writable /sandbox.
RUN useradd --create-home --uid 1500 app && chown -R app:app /sandbox
USER app
WORKDIR /sandbox
```

`viper-sidecar`, the entrypoint that starts Viper and then the agent beside it. Make it executable with `chmod +x viper-sidecar`:

```bash
#!/usr/bin/env bash
# Sandbox entrypoint: starts `viper proxy`, then the agent beside it rather
# than below it, so the agent never shares viper's network access.
#
#   viper-sidecar <agent command...>
#
# Needs VIPER_GATEWAY_URL and an enrolled Viper config at VIPER_CONFIG
# (default /sandbox/viper/config.toml). The agent gets VIPER_PROXY_URL and
# OPENAI_BASE_URL pointing at the proxy. Set VIPER_PROXY_LOG to keep the
# proxy's log; otherwise it is removed when the agent exits.

set -uo pipefail

export VIPER_CONFIG="${VIPER_CONFIG:-/sandbox/viper/config.toml}"
log="${VIPER_PROXY_LOG:-}"

fail() { echo "viper-sidecar: $*" >&2; exit 2; }

[ $# -gt 0 ] || fail "usage: viper-sidecar <agent command...>"
[ -n "${VIPER_GATEWAY_URL:-}" ] || fail "set VIPER_GATEWAY_URL to the gateway URL Guardian shows"
[ -f "$VIPER_CONFIG" ] || fail "no Viper config at $VIPER_CONFIG; upload the enrolled viper directory with --upload viper:/sandbox"

# Viper's own rule: https unless a scheme is given, and a /v1 path or none
gateway="${VIPER_GATEWAY_URL%/}"
[[ "$gateway" == *://* ]] || gateway="https://$gateway"
gateway="${gateway%/v1}"
[[ "${gateway#*://}" != */* ]] || fail "VIPER_GATEWAY_URL must be the gateway's origin, optionally followed by /v1"

temp_log=""
[ -n "$log" ] || { log="$(mktemp -t viper-proxy.XXXXXX)"; temp_log="$log"; }

viper proxy --app proxy --require-agent-header --port 0 \
  --gateway-endpoint "$gateway/v1/openai_api" >"$log" 2>&1 &
proxy_pid=$!
trap 'kill "$proxy_pid" 2>/dev/null; [ -z "$temp_log" ] || rm -f "$temp_log"' EXIT

url=""
for _ in $(seq 1 100); do
  url="$(grep -m1 -oE '^http://localhost:[0-9]+$' "$log" 2>/dev/null)"
  [ -n "$url" ] && break
  kill -0 "$proxy_pid" 2>/dev/null || break
  sleep 0.1
done
if [ -z "$url" ]; then
  temp_log=""
  echo "viper-sidecar: viper proxy did not start (log: $log):" >&2
  tail -5 "$log" >&2
  exit 1
fi

# The proxy binds IPv4 loopback only, and localhost can resolve to ::1 first
export VIPER_PROXY_URL="http://127.0.0.1:${url##*:}"
export OPENAI_BASE_URL="$VIPER_PROXY_URL/v1"

"$@"
```

`agent.py`, a named agent using only the Python standard library. It registers its definition with the proxy, then names itself and that definition on its model request, as the Guardian Agent SDK's [Proxy API](https://guardian-agent-sdk.docs.eqtylab.io/reference/proxy-api/) reference describes:

```python
import json
import os
import sys
import urllib.error
import urllib.request

PROXY = os.environ["VIPER_PROXY_URL"]
BASE_URL = os.environ["OPENAI_BASE_URL"]
AGENT = os.environ["VIPER_AGENT"]
MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini")


def post(url: str, body: dict, headers: dict) -> dict:
    request = urllib.request.Request(
        url,
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json", **headers},
    )
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        sys.exit(
            f"{url} answered {error.code}: {error.read().decode(errors='replace')}"
        )


definition = {
    "schema": "viper/agent-definition/v1",
    "name": AGENT,
    "framework": {"name": "custom"},
    "models": [{"provider": "openai", "model": MODEL}],
}
definition_cid = post(
    f"{PROXY}/__viper__/v1/agents/{AGENT}/definitions", definition, {}
)["definitionCid"]

completion = post(
    f"{BASE_URL}/chat/completions",
    {
        "model": MODEL,
        "messages": [{"role": "user", "content": "Say hello in five words."}],
    },
    {
        "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
        "X-Viper-Agent": AGENT,
        "X-Viper-Definition": definition_cid,
    },
)
print(completion["choices"][0]["message"]["content"])
```

`policy.yaml`. Replace `gateway.example.com` with your gateway's host, and change the port if your gateway URL names one:

```yaml
# OpenShell sandbox policy for an agent behind Viper. Only the viper binary
# may reach the gateway; every other binary, the agent included, is denied.
# Replace the host with your gateway's, as Guardian shows it.
version: 1

# OpenShell's default paths, plus /nix/store, where viper's loader and libraries live
filesystem_policy:
  include_workdir: true
  read_only:
    - /usr
    - /lib
    - /etc
    - /var/log
    - /proc
    - /dev/urandom
    - /nix/store
  read_write:
    - /tmp
    - /dev/null

network_policies:
  guardian_gateway:
    name: Agent Gateway
    endpoints:
      - host: gateway.example.com
        port: 443
        # OpenShell relays viper's TLS to the gateway without terminating it
        tls: skip
    binaries:
      # The exact binary; a glob or a launcher would also allow what it starts
      - path: /usr/local/bin/viper
```

Keep the `binaries` entry to that exact path. A glob or a launcher such as a shell would also allow every process it starts, the agent included. Keep `/nix/store` in `filesystem_policy` too: Viper's loader and libraries live there, and without it `viper` fails to start with `Permission denied`.

Build the image:

```bash
docker build -t viper-openshell .
```

## 2. Enroll the agent

Enroll into a new `viper` directory beside the files. It holds the keys this sandbox signs with. Step 3 names the sandbox after the agent, and OpenShell takes sandbox names of up to 19 lowercase letters, digits and single hyphens, starting and ending with a letter or digit, so choose an agent name that fits.

```bash
export VIPER_GATEWAY_URL=https://gateway.example.com
export VIPER_CONFIG="$PWD/viper/config.toml"
viper did add --assign proxy
viper agent add support-bot
GUARDIAN_API_KEY=ak_… viper agent register support-bot
```

Viper prints the agent ID the Guardian Platform minted:

```console
registered agent `support-bot` (did:key:z6Mk…) as urn:uuid:3f2a…
```

If you have no API key, register with a token from Guardian instead: `VIPER_PROVISIONING_TOKEN='ggr1.…' viper agent register support-bot`. Choosing **OpenShell** in the **Register Agent** dialog shows this step's commands with the token and your gateway address filled in. See [Named agents](./register-agents.mdx#named-agents).

The proxy's own key from `viper did add` signs nothing here, because `--require-agent-header` makes the proxy refuse any request without an `X-Viper-Agent` header naming an agent, instead of signing it as the proxy. The proxy still needs the key to start.

Check the enrollment with `viper doctor`, with `VIPER_CONFIG` still set. It confirms the gateway answers and accepts `support-bot`'s key, and prints a fix for anything that fails. Its warning that the proxy's key is not registered is expected here.

To run the same agent in another sandbox, enroll a fresh `viper` directory and join the existing agent with an API key, so each sandbox has its own key under one Guardian agent. A registration token cannot join an agent. The uploaded directory must be named `viper`, so enroll in a new directory beside the first, with a copy of `policy.yaml`:

```bash
mkdir sandbox-2 && cp policy.yaml sandbox-2/ && cd sandbox-2
export VIPER_GATEWAY_URL=https://gateway.example.com
export VIPER_CONFIG="$PWD/viper/config.toml"
viper did add --assign proxy
viper agent add support-bot
GUARDIAN_API_KEY=ak_… viper agent register support-bot --agent-id urn:uuid:…
```

Use the agent ID from the first registration. Then run step 3 from `sandbox-2` with a different `--name`, such as `support-bot-2`, in both the `create` and `exec` commands, keeping `VIPER_AGENT=support-bot`.

## 3. Start the sandbox and run the agent

OpenShell takes uploads or a start command when it creates a sandbox, not both. Create the sandbox with the `viper` directory, then run the agent in it. If the directory is inside a Git work tree, OpenShell uploads only the files Git does not ignore; add `--no-git-ignore` if your `.gitignore` covers `viper/` or its keys.

```bash
openshell sandbox create --name support-bot --from viper-openshell --policy policy.yaml \
  --upload viper:/sandbox \
  --env VIPER_GATEWAY_URL="$VIPER_GATEWAY_URL" --env VIPER_AGENT=support-bot \
  --env OPENAI_API_KEY="$OPENAI_API_KEY" --no-credential-warnings --detach
openshell sandbox exec -n support-bot -- viper-sidecar python3 /sandbox/agent.py
```

The agent prints the model's answer. To run your own agent, replace `python3 /sandbox/agent.py` with its command; it reads the proxy address from `OPENAI_BASE_URL` (or `VIPER_PROXY_URL`) and must name itself on each request, as `agent.py` does.

OpenAI's key goes in as a plain environment variable rather than an OpenShell provider. OpenShell swaps a provider placeholder for the real key only on connections it decrypts, and the agent's requests reach OpenAI only through Viper's connection, which OpenShell does not decrypt. Hence `--no-credential-warnings`.

## 4. Confirm it in Guardian

1. Open the [Agent Catalog](./agent-inventory.mdx) and paste the agent ID from step 2 (`urn:uuid:…`) into the search box. `support-bot` is listed as a **Custom** agent.
2. Open the agent. The request appears in its **Request Logs**, signed with the agent's key.
3. To have project policy apply to the agent and see its traffic in a project, [associate it with the project](./associated-agents.mdx).

The sandbox keeps its Viper identity across `openshell sandbox stop` and `openshell sandbox start`. A deleted sandbox can be recreated with the same identity from the same `viper` directory; delete that directory once you no longer need it.

## What is and is not recorded

Recorded:

- Every model request the agent sends through the proxy, signed with the agent's own key and recorded with its definition.
- `OPENSHELL_SANDBOX` from the sandbox's environment, in the signed state of each request.

The agent cannot reach the gateway, the provider or any other host directly: OpenShell refuses the connection.

Not recorded:

- **Other egress your policy allows.** Any rule you add for another program, such as a package index or a tool's API, lets that traffic leave without going through Viper. It is neither signed nor audited.
- **Proof of the sandbox.** OpenShell sets `OPENSHELL_SANDBOX` but no sandbox ID, and any process can set the variable. Treat it as a hint, not as evidence that the request came from a sandbox.
- **An agent that impersonates Viper.** The agent runs as the same user as Viper, so it can read Viper's keys in `/sandbox/viper` and sign as Viper or as the enrolled agent. It can also unset `OPENSHELL_SANDBOX` to get past the wrapper refusal, which guards against mistakes, not against a hostile agent. This is true of every Viper deployment; the sandbox stops the agent from bypassing Viper, not from impersonating it.
- **Per-step agent state.** Requests name the agent and its definition, not a run, thread or step, and conversations aren't part of the definition.

## Troubleshooting

| Symptom                                                                    | What to do                                                                                                                                                                                                                                                              |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The sandbox fails to start with `ControlSupervisorStartFailed`             | On macOS, turn on host networking in Docker Desktop.                                                                                                                                                                                                                    |
| `viper-sidecar: no Viper config at /sandbox/viper/config.toml`             | Upload the enrolled directory with `--upload viper:/sandbox`. The directory must be named `viper`; OpenShell places it inside the destination.                                                                                                                          |
| `Git filtering selected no files` or `Git filtering failed`                | The `viper` directory is inside a Git work tree, and Git ignores it or could not list it. Add `--no-git-ignore` to `openshell sandbox create`. If the upload succeeds but the proxy fails to load a key, your `.gitignore` covers the key files; add the flag then too. |
| `viper proxy did not start` with `no DID configured for proxy`             | Create the proxy's key in the `viper` directory before uploading it: `viper did add --assign proxy`.                                                                                                                                                                    |
| `viper proxy did not start` with `/usr/local/bin/viper: Permission denied` | The sandbox cannot read Viper's loader and libraries. Add `/nix/store` to `filesystem_policy.read_only` in `policy.yaml`, then delete and create the sandbox again; OpenShell applies filesystem rules when the sandbox starts.                                         |
| `viper-sidecar: set VIPER_GATEWAY_URL`                                     | Pass `--env VIPER_GATEWAY_URL=…` when you create the sandbox.                                                                                                                                                                                                           |
| `VIPER_GATEWAY_URL must be the gateway's origin`                           | Use the gateway URL your administrator gave you: the origin, optionally followed by `/v1`.                                                                                                                                                                              |
| `cannot be fully mediated by viper inside an OpenShell sandbox`            | Wrapper commands refuse to run in a sandbox. Start the agent with `viper-sidecar` instead.                                                                                                                                                                              |
| The agent's requests fail with `502`                                       | Viper could not reach the gateway. Check that `policy.yaml` names the gateway's host and port exactly, with `tls: skip`, and that `/usr/local/bin/viper` is the binary, not a link.                                                                                     |
| Registering the definition returns `404` with `agent_not_enrolled`         | `VIPER_AGENT` does not match the name you enrolled in step 2.                                                                                                                                                                                                           |
| Model requests fail with `403` and `agent_header_required`                 | The proxy runs with `--require-agent-header`, so each request must name the agent in an `X-Viper-Agent` header, as `agent.py` does.                                                                                                                                     |
| Model requests fail with `409` and `definition_unknown`                    | `X-Viper-Definition` is missing or names a definition this proxy has not registered. `viper-sidecar` starts a new proxy for each run, so register the definition each time the agent starts and send the `definitionCid` it returns.                                    |
| Model requests fail with `401` from OpenAI                                 | `OPENAI_API_KEY` is missing or wrong. The gateway does not add a key.                                                                                                                                                                                                   |

## Related

- [LangGraph Quickstart](./langgraph-quickstart.mdx) — a named agent built with the Guardian Agent SDK, outside a sandbox
- [Deep Agents Quickstart](./deepagents-quickstart.mdx) — the same named-agent setup for an agent with sub-agents
- [Run Agents](./run-agents.mdx) — sidecar mode, and why wrapper commands refuse to run in a sandbox
- [Associated Agents](./associated-agents.mdx) — link the agent to a project so project policy applies
- [Agent Configuration](./agent-configuration.mdx) — what Viper captures about the agent at the start of each session
- [Agent Coverage](./agent-coverage.mdx) — how the sandbox route compares with the other integrations