# NVIDIA OpenShell # NVIDIA OpenShell Developer Guide > OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure. NVIDIA OpenShell is the safe, private runtime for autonomous AI agents. It provides sandboxed execution environments that protect your data, credentials, and infrastructure. Agents run with exactly the permissions they need and nothing more, governed by declarative policies that prevent unauthorized file access, data exfiltration, and uncontrolled network activity. ## Get Started Install OpenShell and create your first sandbox in two commands. ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh openshell sandbox create -- claude ``` Refer to the [Quickstart](/get-started/quickstart) for more details. *** ## Explore Learn about OpenShell and its capabilities. Concept Install OpenShell and create your first sandbox in two commands. Tutorial Hands-on walkthroughs from first sandbox to custom policies. Concept Deploy gateways, create sandboxes, configure policies, providers, and community images for your AI agents. Concept Keep inference traffic private by routing API calls to local or self-hosted backends. Concept Understand sandbox logs, access them with the CLI and TUI, and export OCSF JSON records. How-To Policy schema, environment variables, and default policy details. Reference Every configurable security control, its default, and the risk of changing it. Concept *** This software automatically retrieves, accesses or interacts with external materials. Those retrieved materials are not distributed with this software and are governed solely by separate terms, conditions and licenses. You are solely responsible for finding, reviewing and complying with all applicable terms, conditions, and licenses, and for verifying the security, integrity and suitability of any retrieved materials for your specific use case. This software is provided "AS IS", without warranty of any kind. The author makes no representations or warranties regarding any retrieved materials, and assumes no liability for any losses, damages, liabilities or legal consequences from your use or inability to use this software or any retrieved materials. Use this software and the retrieved materials at your own risk. # Overview of NVIDIA OpenShell > OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure. NVIDIA OpenShell is an open-source runtime for executing autonomous AI agents in sandboxed environments with kernel-level isolation. It combines sandbox runtime controls and a declarative YAML policy so teams can run agents without giving them unrestricted access to local files, credentials, and external networks. ## Why OpenShell Exists AI agents are most useful when they can read files, install packages, call APIs, and use credentials. That same access can create material risk. OpenShell is designed for this tradeoff: preserve agent capability while enforcing explicit controls over what the agent can access. ## Common Risks and Controls The table below summarizes common failure modes and how OpenShell mitigates them. | Threat | Without controls | With OpenShell | | ---------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Data exfiltration | Agent uploads source code or internal files to unauthorized endpoints. | Network policies allow only approved destinations; other outbound traffic is denied. | | Credential theft | Agent reads local secrets such as SSH keys or cloud credentials. | Filesystem restrictions (Landlock) confine access to declared paths only. | | Unauthorized API usage | Agent sends prompts or data to unapproved model providers. | Privacy routing and network policies control where inference traffic can go. | | Privilege escalation | Agent attempts `sudo`, setuid paths, or dangerous syscall behavior. | Unprivileged process identity and seccomp restrictions block escalation paths. | ## Protection Layers at a Glance OpenShell applies defense in depth across the following policy domains. | Layer | What it protects | When it applies | | ---------- | --------------------------------------------------- | --------------------------- | | Filesystem | Prevents reads/writes outside allowed paths. | Locked at sandbox creation. | | Network | Blocks unauthorized outbound connections. | Hot-reloadable at runtime. | | Process | Blocks privilege escalation and dangerous syscalls. | Locked at sandbox creation. | | Inference | Reroutes model API calls to controlled backends. | Hot-reloadable at runtime. | For details, refer to [Customize Sandbox Policies](/sandboxes/policies) and [Default Policy](/reference/default-policy). ## Common Use Cases OpenShell supports a range of agent deployment patterns. | Use Case | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------ | | Secure coding agents | Run Claude Code, OpenCode, Codex, or GitHub Copilot CLI with constrained file and network access. | | Private enterprise development | Route inference to self-hosted or private backends while keeping sensitive context under your control. | | Compliance and audit | Treat policy YAML as version-controlled security controls that can be reviewed and audited. | | Reusable environments | Use community sandbox images or bring your own containerized runtime. | ## Next Steps Explore these topics to go deeper: * To understand the runtime architecture, refer to [How OpenShell Works](/about/how-it-works). * To install the CLI and create your first sandbox, refer to the [Quickstart](/get-started/quickstart). * To learn how OpenShell enforces policy controls across protection layers, refer to [Customize Sandbox Policies](/sandboxes/policies). # How OpenShell Works > Understand the OpenShell architecture, runtime boundaries, gateways, sandboxes, and ecosystem integration points. OpenShell is built around three stable runtime components: the **CLI**, the **Gateway**, and the **Supervisor**. The CLI, SDK, and TUI provide user-facing access. The gateway is the control plane: it owns API access, state, policy and settings delivery, provider and inference configuration, and relay coordination. The supervisor runs inside every sandbox workload and is the local security boundary. It launches the agent as a restricted child process and enforces policy where process identity, filesystem access, network egress, and runtime credentials are visible. Infrastructure-specific work sits behind integration boundaries. Compute, credentials, control-plane identity, and sandbox identity each have a driver or adapter boundary so OpenShell can integrate with native runtimes, secret stores, identity providers, and workload identity systems without moving those concerns into the core gateway or sandbox model. ```mermaid flowchart TB subgraph UI["User interfaces"] CLI["CLI"] SDK["SDK"] TUI["TUI"] end subgraph CP["Control plane"] GW["Gateway"] DB[("Entity persistence")] DRIVERS["Compute, credentials, and identity drivers"] end subgraph INFRA["Integrated infrastructure"] RUNTIME["Docker, Podman, Kubernetes, or VM"] SECRETSTORE["Secret stores"] IDP["Identity providers"] WORKLOADID["Workload identity"] end subgraph DP["Sandbox data plane"] SUP["Supervisor"] AGENT["Restricted agent process"] PROXY["Policy proxy"] POLICY["OPA policy engine"] ROUTER["Inference router"] end CLI -->|"gRPC / HTTP"| GW SDK -->|"gRPC / HTTP"| GW TUI -->|"gRPC / HTTP"| GW GW --> DB GW --> DRIVERS DRIVERS --> RUNTIME DRIVERS --> SECRETSTORE DRIVERS --> IDP DRIVERS --> WORKLOADID RUNTIME -->|"provisions workload"| SUP SUP -->|"control, config, logs, relay"| GW SUP -->|"spawn and restrict"| AGENT AGENT -->|"ordinary egress"| PROXY PROXY -->|"evaluate"| POLICY PROXY -->|"allowed traffic"| EXT["External services"] PROXY -->|"inference.local"| ROUTER ROUTER -->|"managed inference"| MODEL["Inference backends"] ``` ## Deployment Models OpenShell can run on a single local machine or in a remote Kubernetes cluster. The CLI workflow stays the same: users point the CLI, SDK, or TUI at a gateway, and the gateway provisions sandboxes through its configured compute driver. | Deployment | How it works | Best for | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Local machine | The gateway runs on the user's workstation or a nearby development host and creates sandboxes with Docker, Podman, or a VM runtime. The supervisor inside each sandbox connects back to that local gateway. | Individual development, local agent experiments, and private workstation workflows. | | Remote Kubernetes cluster | The gateway runs as a cluster service and creates sandbox pods in the configured namespace. Supervisors connect outbound to the gateway endpoint, so clients do not need direct pod access. | Shared teams, centrally managed policy, remote compute, GPUs, and production-like environments. | This deployment split keeps the runtime model consistent. Local deployments use the host's container or VM runtime as the integrated infrastructure. Kubernetes deployments use the cluster scheduler, networking, secrets, identity, and GPU device plugins without changing the gateway and sandbox contract. ## Core Components | Component | Boundary | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Sandboxes](/sandboxes/manage-sandboxes) | Data-plane workloads that run the supervisor, launch restricted agent processes, apply local isolation, push logs, and maintain the gateway session. | | [Gateways](/sandboxes/manage-gateways) | Authenticated control plane that owns API access, durable state, sandbox lifecycle, settings delivery, authorization, and relay coordination. | | [Providers](/sandboxes/manage-providers) | Credential and provider records that map logical agent needs to platform or user-managed secrets without exposing raw credentials to the agent process. | | [Policies](/sandboxes/policies) | Declarative controls for filesystem access, process identity, network egress, L7 rules, credential injection, and runtime policy updates. | | [Inference Routing](/sandboxes/inference-routing) | Managed `https://inference.local` path that routes model traffic to configured backends while keeping provider credentials outside the sandbox. | ## Gateways and Sandboxes The gateway and sandbox split control-plane authority from runtime enforcement. The gateway owns durable platform state: sandboxes, policy revisions, runtime settings, provider records, inference configuration, session records, and authorization decisions. A sandbox owns the local execution boundary: process identity, filesystem access, network egress, credential injection, local logs, and the agent child process. The relationship is supervisor initiated. Each sandbox supervisor connects outbound to a known gateway endpoint, authenticates as a sandbox workload, and keeps a live session open for control traffic and relays. This avoids requiring every compute driver to solve gateway-to-sandbox reachability through pod IPs, bridge networks, port mappings, NAT traversal, or custom tunnels. The gateway delivers desired state. The supervisor applies it locally, keeps last-known-good config when refresh fails, and leaves static isolation controls in place until the sandbox is recreated. Live operations such as config refresh, policy updates, credential delivery, log push, connect, exec, file sync, and relay setup use the same authenticated gateway-supervisor relationship. ## Supervisor Protection Layers The supervisor is the sandbox-local enforcement component. It starts before the agent process, prepares the sandbox runtime, fetches gateway configuration, and then launches the agent under the active policy. | Protection layer | Supervisor responsibility | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Process | Drops privileges, applies process identity rules, disables privilege escalation paths, and starts the agent as a restricted child process. | | Filesystem | Applies filesystem policy before the agent starts so undeclared paths are inaccessible and declared paths are read-only or read-write as configured. | | Network | Routes ordinary egress through the policy proxy so destination, port, binary identity, and L7 request rules can be evaluated before traffic leaves the sandbox. | | Credentials | Receives credential material from the gateway and injects it only through configured policy paths or request-time proxy rules. | | Inference | Intercepts `https://inference.local` and forwards model traffic through the configured inference route instead of exposing provider credentials to the agent. | | Observability | Emits local security and lifecycle logs, pushes sandbox logs to the gateway, and keeps relay endpoints available for connect, exec, and file transfer operations. | Static controls such as filesystem and process isolation are established at sandbox start and require sandbox recreation to change. Dynamic controls such as network policy, credential delivery, and inference routing can refresh over the live gateway-supervisor session. ## Ecosystem Integration OpenShell integrates with infrastructure ecosystems instead of replacing them. Runtimes, schedulers, secret stores, identity providers, workload identity systems, image pipelines, storage, and GPU or device exposure remain owned by the platforms that provide them. The gateway owns OpenShell control-plane semantics: sandbox state, lifecycle ordering, policy and settings resolution, credential mapping, authorization, inference configuration, and relay coordination. Drivers translate those semantics into platform-native operations. The supervisor owns OpenShell sandbox semantics. Filesystem policy, process privilege reduction, network proxying, inference interception, credential injection, security logging, and gateway relay behavior stay consistent across Docker, Podman, Kubernetes, VM-backed sandboxes, and future integrations. # Installation > Install OpenShell, choose a compute driver, and connect to a gateway. ## Install OpenShell Install OpenShell with a single command: ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` The script detects your operating system and installs the OpenShell CLI and gateway with your native package manager. It then starts the local gateway server so you can begin creating sandboxes. You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. Use `openshell status` to confirm the CLI can reach the gateway. ## Supported Compute Drivers OpenShell supports several local compute drivers. Package-managed gateways leave the driver unset by default so the gateway can auto-detect an available driver. Set `compute_drivers` in the gateway TOML when you need to pin a specific driver. | Compute Driver | How It Is Configured | System Requirements | | -------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Podman | The gateway is configured to create rootless Podman containers through the Podman API socket. | Linux with Podman 5.x, cgroups v2, rootless networking, and an active Podman user socket. | | Docker | The gateway is configured to create containers through Docker Desktop or Docker Engine. | Docker Desktop or Docker Engine 28.04 or later on the gateway host. | | MicroVM | The gateway is configured to create VM-backed sandboxes. | Host virtualization support. MicroVM uses Hypervisor.framework on macOS, KVM on Linux, and QEMU for GPU-backed sandboxes on Linux. | For detailed driver behavior, refer to [Sandbox Compute Drivers](/reference/sandbox-compute-drivers). For gateway and sandbox operations, refer to [Gateways](/sandboxes/manage-gateways) and [Sandboxes](/sandboxes/manage-sandboxes). ## macOS On macOS, the install script uses Homebrew. The Homebrew package installs the `openshell` CLI, the gateway binary, and a Homebrew-managed gateway service. The Homebrew service listens on `https://127.0.0.1:17670` and generates a local mTLS bundle on install. The gateway starts from built-in defaults and reads `~/.config/openshell/gateway.toml` when that file exists. If that file is absent, the Homebrew service also falls back to a Homebrew prefix config when present, such as `/opt/homebrew/var/openshell/gateway.toml`. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. The installer starts the service for you. Use Homebrew service commands when you need to inspect, restart, or stop the gateway service: ```shell brew services list brew services restart openshell ``` ## Linux On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. Linux packages require glibc 2.31 or newer. The installer checks libc before downloading packages and exits with an error on older glibc versions, Alpine, musl-based distributions, or unknown libc environments. The Linux user service listens on `https://127.0.0.1:17670`, starts from built-in defaults, and generates a local mTLS bundle before the gateway starts. Create `~/.config/openshell/gateway.toml` only when you need to override those defaults. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. The installer starts the service for you. Use systemd user commands when you need to inspect, restart, or stop the gateway service: ```shell systemctl --user status openshell-gateway systemctl --user restart openshell-gateway journalctl --user -u openshell-gateway -f ``` To keep the user service running after logout, enable linger: ```shell sudo loginctl enable-linger $USER ``` ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). ## Next Steps * To create your first sandbox, refer to the [Quickstart](/get-started/quickstart). * To run the gateway as a container without the installer, refer to [Running the Gateway as a Container](/about/container-gateway). * To register, select, and inspect gateways, refer to [Gateways](/sandboxes/manage-gateways). * To supply API keys or tokens, refer to [Manage Providers](/sandboxes/manage-providers). * To control what the agent can access, refer to [Policies](/sandboxes/policies). # Running the Gateway as a Container > Run the OpenShell gateway using docker run or docker-compose without the installer. Use this approach when you want to run the OpenShell gateway as a container instead of installing it with the system package manager. This is useful on immutable OS distributions (Fedora CoreOS, bootc-based images, Silverblue) where the standard installer is not appropriate, or anywhere you prefer a container-first workflow. The gateway image is published at `ghcr.io/nvidia/openshell/gateway`. ## Prerequisites for the Docker Driver When the gateway runs as a container and creates Docker-backed sandboxes, the gateway container communicates with the host Docker daemon via the mounted socket. This requires three things beyond a basic `docker run`: 1. **Docker socket access.** The gateway process must be able to read and write the Docker socket. Add the `docker` group (or the GID of `/var/run/docker.sock`) so the socket is accessible without running as root. 2. **gRPC endpoint.** Sandbox containers call back to the gateway over the `OPENSHELL_GRPC_ENDPOINT` address. The Docker driver substitutes `host.openshell.internal` as the host and the gateway's own bind port as the port — only the **scheme** (`http` or `https`) is preserved. Use `http://host.openshell.internal:8080` when TLS is disabled and `https://host.openshell.internal:8080` when mTLS is enabled. The docker driver automatically binds the gateway to the bridge network interface so sandbox containers can reach it — you do not need to expose the port on `0.0.0.0`. 3. **Supervisor binary on the host.** The gateway bind-mounts the `openshell-sandbox` supervisor binary into each sandbox container. Because bind-mount paths are resolved by the host Docker daemon (not inside the gateway container), the binary must exist at a path on the **host** filesystem and be mounted at the **same absolute path** inside the gateway container. That way the path the gateway records internally matches what Docker can find on the host when it creates sandbox containers. ## Quick Start Extract the supervisor binary to the host once, then start the gateway: ```shell mkdir -p ~/openshell/supervisor docker create --name tmp-supervisor ghcr.io/nvidia/openshell/supervisor:latest docker cp tmp-supervisor:/openshell-sandbox ~/openshell/supervisor/openshell-sandbox docker rm tmp-supervisor chmod +x ~/openshell/supervisor/openshell-sandbox ``` Start the gateway: ```shell docker run -d \ --name openshell-gateway \ --restart unless-stopped \ --group-add docker \ -p 127.0.0.1:8080:8080 \ -v openshell-state:/var/openshell \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ -e OPENSHELL_GRPC_ENDPOINT=http://host.openshell.internal:8080 \ -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/var/openshell/openshell.db \ -e OPENSHELL_DISABLE_TLS=true \ ghcr.io/nvidia/openshell/gateway:latest ``` The volume mount uses `~/openshell/supervisor/openshell-sandbox` for both the host and container paths. The shell expands `~` in both halves before passing the argument to Docker, so both sides resolve to the same absolute path (e.g., `/home/user/openshell/supervisor/openshell-sandbox`). This satisfies the same-path requirement so the host Docker daemon can find the binary when creating sandbox containers. Register the gateway with the CLI. If running on the same machine, use `--local`: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` If registering from a different machine on the same network, use the host IP and `--remote`: ```shell openshell gateway add http://HOST_IP:8080 --remote --name remote ``` Confirm the CLI can reach the gateway: ```shell openshell status ``` Disabling TLS removes authentication. This example binds to `127.0.0.1` so only local connections are accepted. To accept remote connections, enable mTLS or restrict access with a firewall rule. ## Full mTLS Setup To run the gateway with mutual TLS, generate the PKI bundle first, then start the gateway with the cert paths configured. Bootstrap the PKI into a local state directory: ```shell mkdir -p ~/.local/state/openshell/tls docker run --rm \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v "$HOME/.config/openshell:/home/openshell/.config/openshell" \ ghcr.io/nvidia/openshell/gateway:latest \ generate-certs \ --output-dir /home/openshell/.local/state/openshell/tls \ --server-san host.openshell.internal ``` This writes the server and client certificates under `~/.local/state/openshell/tls/`, writes sandbox JWT signing keys under `~/.local/state/openshell/tls/jwt/`, and copies the client bundle to `~/.config/openshell/gateways/openshell/mtls/` so the CLI picks it up automatically. Start the gateway with mTLS enabled: ```shell docker run -d \ --name openshell-gateway \ --restart unless-stopped \ --group-add docker \ -p 127.0.0.1:8080:8080 \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ -e OPENSHELL_GRPC_ENDPOINT=https://127.0.0.1:8080 \ -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/home/openshell/.local/state/openshell/openshell.db \ -e OPENSHELL_LOCAL_TLS_DIR=/home/openshell/.local/state/openshell/tls \ -e OPENSHELL_TLS_CERT=/home/openshell/.local/state/openshell/tls/server/tls.crt \ -e OPENSHELL_TLS_KEY=/home/openshell/.local/state/openshell/tls/server/tls.key \ -e OPENSHELL_TLS_CLIENT_CA=/home/openshell/.local/state/openshell/tls/ca.crt \ -e OPENSHELL_ENABLE_MTLS_AUTH=true \ -e OPENSHELL_DOCKER_TLS_CA=/home/openshell/.local/state/openshell/tls/ca.crt \ -e OPENSHELL_DOCKER_TLS_CERT=/home/openshell/.local/state/openshell/tls/client/tls.crt \ -e OPENSHELL_DOCKER_TLS_KEY=/home/openshell/.local/state/openshell/tls/client/tls.key \ ghcr.io/nvidia/openshell/gateway:latest ``` Register the gateway with mTLS: ```shell openshell gateway add https://127.0.0.1:8080 --local --name local ``` ## Docker Compose The [`deploy/docker/`](https://github.com/NVIDIA/OpenShell/tree/main/deploy/docker) directory in the repository contains a production-ready Compose setup with full inline documentation: | File | Purpose | | -------------------- | --------------------------------------------------- | | `docker-compose.yml` | Gateway service, volumes, and environment variables | | `gateway.toml` | TOML configuration mounted into the container | Clone or copy those files, then start the gateway: ```shell docker compose -f deploy/docker/docker-compose.yml up -d ``` Register the gateway with the CLI. If registering from the same machine: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` If registering from a different machine on the same network, replace `HOST_IP` with the machine's LAN address: ```shell openshell gateway add http://HOST_IP:8080 --remote --name remote ``` ## Using Podman Replace `docker` with `podman` in the commands above. Mount the Podman socket instead of the Docker socket and set the driver to `podman`: ```shell podman run -d \ --name openshell-gateway \ -p 127.0.0.1:8080:8080 \ -v openshell-state:/var/openshell \ -v "$XDG_RUNTIME_DIR/podman/podman.sock:/var/run/podman.sock" \ -e OPENSHELL_DRIVERS=podman \ -e OPENSHELL_PODMAN_SOCKET=/var/run/podman.sock \ -e OPENSHELL_DB_URL=sqlite:/var/openshell/openshell.db \ -e OPENSHELL_DISABLE_TLS=true \ ghcr.io/nvidia/openshell/gateway:latest ``` ## Next Steps * To create your first sandbox, refer to the [Quickstart](/get-started/quickstart). * To control what the agent can access, refer to [Policies](/sandboxes/policies). * For environment variable reference, refer to [Sandbox Compute Drivers](/reference/sandbox-compute-drivers). # Supported Agents > AI agent frameworks and runtimes compatible with OpenShell sandboxes. The following table summarizes the agents that run in OpenShell sandboxes. Most agent sandbox images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Agents in the base image are auto-configured when passed as the trailing command to `openshell sandbox create`. | Agent | Source | Default Policy | Notes | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Works out of the box. Requires `ANTHROPIC_API_KEY` for direct Anthropic access, or use `inference.local` with a configured provider (e.g. Vertex AI). | | [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Partial coverage | Pre-installed. Use `ANTHROPIC_BASE_URL="https://inference.local/v1"` with a configured provider. Add `opencode.ai` endpoint and OpenCode binary paths to the policy for full functionality. | | [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | No coverage | Pre-installed. Requires a custom policy with OpenAI endpoints and Codex binary paths. Requires `OPENAI_API_KEY`. | | [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Pre-installed. Works out of the box. Requires `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | | [OpenClaw](https://openclaw.ai/) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Blueprint-managed | Run OpenClaw more securely inside NVIDIA OpenShell with managed inference using NemoClaw. | | [Ollama](https://ollama.com/) | [`ollama`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/ollama) | Bundled | Run cloud and local models. Includes Claude Code, Codex, and OpenCode. Launch with `openshell sandbox create --from ollama`. | | [Pi](https://pi.dev/) | [`pi`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/pi) | Bundled | Comes with Pi pre-installed. Launch with `openshell sandbox create --from pi`. | For base image details and `--from` usage, refer to [Sandboxes](/sandboxes/manage-sandboxes#base-sandbox-container). For a complete support matrix, refer to the [Support Matrix](/reference/support-matrix) page. # NVIDIA OpenShell Release Notes > Track the latest changes and improvements to NVIDIA OpenShell. NVIDIA OpenShell follows a frequent release cadence. Use the following GitHub resources directly. | Resource | Description | | --------------------------------------------------------------------------------------- | ------------------------------------------------ | | [Releases](https://github.com/NVIDIA/OpenShell/releases) | Versioned release notes and downloadable assets. | | [Release comparison](https://github.com/NVIDIA/OpenShell/compare) | Diff between any two tags or branches. | | [Merged pull requests](https://github.com/NVIDIA/OpenShell/pulls?q=is%3Apr+is%3Amerged) | Individual changes with review discussion. | | [Commit history](https://github.com/NVIDIA/OpenShell/commits/main) | Full commit log on `main`. | # Quickstart > Install the OpenShell CLI, connect to a gateway, and create your first sandboxed AI agent. This page gets you from a reachable OpenShell gateway to a running, policy-enforced sandbox. ## Prerequisites Before you begin, make sure you have: * A reachable OpenShell gateway. * At least one compute driver configured for the gateway: Kubernetes, Docker, Podman, or MicroVM. * The OpenShell CLI installed on your workstation. For a complete list of requirements, refer to [Support Matrix](/reference/support-matrix). If you have not chosen a compute driver yet, refer to [Installation](/about/installation). ## Install the OpenShell CLI Run the install script: ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` The install script uses Homebrew, RPM, or a Debian package based on your machine. It starts the local gateway server after installation. If you prefer [uv](https://docs.astral.sh/uv/): ```shell uv tool install -U openshell ``` After installing the CLI, run `openshell --help` in your terminal to view the full CLI reference. You can also clone the [NVIDIA OpenShell GitHub repository](https://github.com/NVIDIA/OpenShell) and use the `/openshell-cli` skill to load the CLI reference into your agent. ## Create Your First OpenShell Sandbox Create a sandbox and launch an agent inside it. Choose the tab that matches your agent: Run the following command to create a sandbox with Claude Code: ```shell openshell sandbox create -- claude ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `ANTHROPIC_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. `ANTHROPIC_API_KEY` is an API key from [console.anthropic.com](https://console.anthropic.com), not a subscription token. Subscription users must generate a separate API key. Run the following command to create a sandbox with OpenCode: ```shell openshell sandbox create -- opencode ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `OPENAI_API_KEY` or `OPENROUTER_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. Run the following command to create a sandbox with Codex: ```shell openshell sandbox create -- codex ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `OPENAI_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. Use the `--from` flag to create a sandbox from the base container: ```shell openshell sandbox create --from base ``` # Tutorials > Step-by-step walkthroughs for OpenShell, from first sandbox to production-ready policies. Hands-on walkthroughs that teach OpenShell concepts by building real configurations. Each tutorial builds on the previous one, starting with core sandbox mechanics and progressing to production workflows. Create a sandbox, observe default-deny networking, apply a read-only L7 policy, and inspect audit logs. No AI agent required. Launch Claude Code in a sandbox, diagnose a policy denial, and iterate on a custom GitHub policy from outside the sandbox. Configure a Providers v2 Microsoft Graph provider with gateway-managed OAuth2 refresh-token rotation. Route inference through Ollama using cloud-hosted or local models, and verify it from a sandbox. Route inference to a local LM Studio server using the OpenAI-compatible or Anthropic-compatible APIs. Run the OpenShell gateway as a Docker Compose service and create agent sandboxes including OpenClaw. # Run the Gateway with Docker Compose > Run the OpenShell gateway as a Docker Compose service and create agent sandboxes including OpenClaw. This tutorial shows how to run the OpenShell gateway as a Docker Compose service on a Linux host or on a machine running Docker Desktop (Windows or macOS). After completing this tutorial you have: * An OpenShell gateway running as a Compose service. * The `openshell` CLI registered against that gateway. * An AI provider configured with your API key. * A running OpenClaw sandbox. ## Prerequisites * Docker Desktop (Windows or macOS) or Docker Engine with the Compose plugin (Linux). * The `openshell` CLI installed on your workstation. See [Install the CLI](#install-the-cli) below. * Port 8080 available on the host. ## Compose files The Compose configuration lives at [`deploy/docker/`](https://github.com/NVIDIA/OpenShell/tree/main/deploy/docker) in the repository. | File | Purpose | | -------------------- | ---------------------------------------------------------- | | `docker-compose.yml` | Gateway service, volumes, and environment variables | | `gateway.toml` | TOML reference for release builds with config-file support | ## Port note The Docker compute driver injects `host.openshell.internal:` into every sandbox container as its callback address. The gateway listens on port 8080 inside the container, so **port 8080 must be published at the same number on the Docker host**. Publishing it as a different host port (for example `18080:8080`) causes sandbox containers to call back to the wrong port and remain stuck in the `Provisioning` phase. If port 8080 is taken, change `OPENSHELL_SERVER_PORT` and update the port mapping to `:8080`, then set `OPENSHELL_PORT=` in an `.env` file. ## Data directory The gateway extracts the `openshell-sandbox` supervisor binary from `ghcr.io/nvidia/openshell/supervisor:latest` on first start and caches it at: ```text /var/lib/openshell/openshell/docker-supervisor//openshell-sandbox ``` This path is used as a bind-mount source when Docker creates sandbox containers. Docker resolves bind-mount sources against the **host filesystem**, not the container filesystem, so the data directory must be bind-mounted at the **same absolute path** in both the host and the container. The Compose file uses `/var/lib/openshell` for this purpose and sets `create_host_path: true` so Docker creates it on first run. ## Start the gateway ```shell cd deploy/docker docker compose up -d ``` Verify the gateway is healthy: ```shell curl -sf http://localhost:8080/healthz ``` ## Install the CLI **Binary (recommended — macOS / Linux / WSL):** ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` **From PyPI (any platform with [uv](https://docs.astral.sh/uv/)):** ```shell uv tool install -U openshell ``` On Windows without WSL, install the CLI inside a WSL 2 distribution (for example AlmaLinux or Ubuntu) and run all `openshell` commands from that distribution. ## Register the gateway Run this once after the gateway starts: ```shell openshell gateway add http://localhost:8080 --name openshell-docker ``` Verify the connection: ```shell openshell status ``` The output should show `Status: Connected`. ## Configure an AI provider Set your API key as an environment variable and create a provider: ```shell ANTHROPIC_API_KEY=sk-ant-... \ openshell provider create --name anthropic --type anthropic --from-existing ``` ```shell OPENAI_API_KEY=sk-... \ openshell provider create --name openai --type openai --from-existing ``` Confirm the provider was stored: ```shell openshell provider list ``` ## Pre-pull sandbox images (optional) Sandbox images are pulled automatically on first use, but the initial pull can take several minutes for large images. Pre-pull to avoid long waits at sandbox creation time: ```shell # Base image — includes Claude Code, OpenCode, Codex, and Copilot docker pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest # OpenClaw image docker pull ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest ``` ## Create a sandbox ```shell openshell sandbox create --from openclaw ``` OpenClaw launches directly. The first run pulls the image if it is not cached. ```shell openshell sandbox create -- claude ``` ```shell openshell sandbox create -- opencode ``` Wait for the phase to change from `Provisioning` to `Ready`: ```shell openshell sandbox list ``` Then connect: ```shell openshell sandbox connect ``` ## Manage the gateway | Command | Purpose | | ------------------------ | ----------------------------------------- | | `docker compose up -d` | Start or restart the gateway | | `docker compose down` | Stop the gateway and remove the container | | `docker compose logs -f` | Tail gateway logs | | `docker compose pull` | Pull a new gateway image version | ## Linux notes On Linux, `host.docker.internal` and `host.openshell.internal` are not automatically resolvable from containers. Add the following under the `gateway` service in `docker-compose.yml`: ```yaml extra_hosts: - "host.docker.internal:host-gateway" - "host.openshell.internal:host-gateway" ``` ## Next steps * [First Network Policy](/get-started/tutorials/first-network-policy) — apply L7 policies to your sandbox. * [GitHub Push Access](/get-started/tutorials/github-sandbox) — grant a sandbox scoped GitHub access. # Write Your First Sandbox Network Policy > Learn how OpenShell network policies work by creating a sandbox, observing default-deny in action, and applying a fine-grained L7 read-only rule. This tutorial shows how OpenShell's network policy system works in under five minutes. You create a sandbox, watch a request get blocked by the default-deny policy, apply a fine-grained L7 rule, and verify that reads are allowed while writes are blocked, all without restarting anything. After completing this tutorial, you understand: * How default-deny networking blocks all outbound traffic from a sandbox. * How to apply a network policy that grants read-only access to a specific API. * How L7 enforcement distinguishes between HTTP methods such as GET and POST on the same endpoint. * How to inspect deny logs for a complete audit trail. ## Prerequisites * A working OpenShell installation. Complete the [Quickstart](/get-started/quickstart) before proceeding. * Docker Desktop running on your machine. To run every step of this tutorial, you can also use the automated demo script at the [examples/sandbox-policy-quickstart](https://github.com/NVIDIA/OpenShell/blob/main/examples/sandbox-policy-quickstart) directory in the NVIDIA OpenShell repository. It runs the full walkthrough in under a minute but without any user interaction. ```shell bash examples/sandbox-policy-quickstart/demo.sh ``` ## Create a Sandbox Start by creating a sandbox with no network policies. This gives you a clean environment to observe default-deny behavior. ```shell openshell sandbox create --name demo --keep --no-auto-providers ``` `--keep` keeps the sandbox running after you exit so you can reconnect later. `--no-auto-providers` skips the provider setup prompt since this tutorial uses `curl` instead of an AI agent. You land in an interactive shell inside the sandbox: ```text sandbox@demo:~$ ``` ## Try to Reach the GitHub API With no network policy in place, every outbound connection is blocked. Test this by making a simple API call from inside the sandbox: ```shell curl -s https://api.github.com/zen ``` `https://api.github.com/zen` is a lightweight, unauthenticated GitHub REST endpoint that returns a random aphorism on each call. It requires no tokens or parameters, which makes it a convenient smoke-test target for verifying outbound HTTPS connectivity. The request fails. By default, all outbound network traffic is denied. The sandbox proxy intercepted the HTTPS CONNECT request to `api.github.com:443` and rejected it because no network policy authorizes `curl` to reach that host. ```text curl: (56) Received HTTP code 403 from proxy after CONNECT ``` Exit the sandbox. The `--keep` flag keeps it running: ```shell exit ``` ## Check the Deny Log Every denied connection produces a structured log entry. Query the sandbox logs from your host to confirm the denial and inspect the reason. ```shell openshell logs demo --since 5m ``` You see a line like: ```text action=deny dst_host=api.github.com dst_port=443 binary=/usr/bin/curl deny_reason="no matching network policy" ``` Every denied connection is logged with the destination, the binary that attempted it, and the reason. Nothing gets out silently. ## Apply a Read-Only GitHub API Policy To allow the sandbox to reach the GitHub API, define a network policy that grants read-only access. The policy specifies which host, port, binary, and HTTP methods are permitted. Create a file called `github_readonly.yaml` with the following content: ```yaml version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] read_write: [/sandbox, /tmp, /dev/null] landlock: compatibility: best_effort process: run_as_user: sandbox run_as_group: sandbox network_policies: github_api: name: github-api-readonly endpoints: - host: api.github.com port: 443 protocol: rest enforcement: enforce access: read-only binaries: - { path: /usr/bin/curl } ``` The `filesystem_policy`, `landlock`, and `process` sections preserve the default sandbox settings. This is required because `policy set` replaces the entire policy. The `network_policies` section is the key part: `curl` can make GET, HEAD, and OPTIONS requests to `api.github.com` over HTTPS. Everything else is denied. The proxy auto-detects TLS on HTTPS endpoints and terminates it to inspect each HTTP request and enforce the `read-only` access preset at the method level. Apply it: ```shell openshell policy set demo --policy github_readonly.yaml --wait ``` `--wait` blocks until the sandbox confirms the new policy is loaded. No restart required. Policies are hot-reloaded. This tutorial uses `curl` and `read-only` access to keep things simple. When building policies for real workloads: * To scope the policy to an agent, replace the `binaries` section with your agent's binary, such as `/usr/local/bin/claude`, instead of `curl`. * To grant write access, change `access: read-only` to `read-write` or add explicit `rules` for specific paths. Refer to the [Policy Schema](/reference/policy-schema). * To allow additional endpoints, stack multiple policies in the same file for PyPI, npm, or your internal APIs. Refer to [Policies](/sandboxes/policies) for examples. ## Verify If GET Requests Are Allowed The policy is now active. Reconnect to the sandbox and retry the same request to confirm that read access works. ```shell openshell sandbox connect demo ``` Retry the same request: ```shell curl -s https://api.github.com/zen ``` ```text Anything added dilutes everything else. ``` It works. The `read-only` preset allows GET requests through. ## Try a Write The read-only preset allows GET but blocks mutating methods like POST, PUT, and DELETE. Test this by sending a POST request to the GitHub API while still inside the sandbox: ```shell curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ -H "Content-Type: application/json" \ -d '{"title":"oops"}' ``` ```json {"error":"policy_denied","policy":"github-api-readonly","detail":"POST /repos/octocat/hello-world/issues not permitted by policy"} ``` The CONNECT request succeeded because `api.github.com` is allowed, but the L7 proxy inspected the HTTP method and returned `403`. `POST` is not in the `read-only` preset. An agent with this policy can read code from GitHub but cannot create issues, push commits, or modify anything. Exit the sandbox: ```shell exit ``` ## Check the L7 Deny Log L7 denials are logged separately from connection-level denials. The log entry includes the exact HTTP method and path that the proxy rejected. ```shell openshell logs demo --level warn --since 5m ``` ```text l7_decision=deny dst_host=api.github.com l7_action=POST l7_target=/repos/octocat/hello-world/issues l7_deny_reason="POST /repos/octocat/hello-world/issues not permitted by policy" ``` The log captures the exact HTTP method, path, and deny reason. In production, pipe these logs to your SIEM for a complete audit trail of every request your agent makes. To log violations without blocking requests, set `enforcement: audit` instead of `enforcement: enforce` in the policy. This is useful for building a policy iteratively: deploy in audit mode, review the logs, and switch to enforce when the rules are correct. ## Clean Up Delete the sandbox to free resources. This stops all processes and purges any injected credentials. ```shell openshell sandbox delete demo ``` To run this entire walkthrough non-interactively, use the automated demo script: ```shell bash examples/sandbox-policy-quickstart/demo.sh ``` ## Next Steps * To walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox, refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox). # Grant GitHub Push Access to a Sandboxed Agent > Learn the iterative policy workflow by launching a sandbox, diagnosing a GitHub access denial, and applying a custom policy to fix it. This tutorial walks through an iterative sandbox policy workflow. You launch a sandbox, ask Claude Code to push code to GitHub, and observe the default network policy denying the request. You then diagnose the denial from your machine and from inside the sandbox, apply a policy update, and verify that the policy update to the sandbox takes effect. After completing this tutorial, you have: * A running sandbox with Claude Code that can push to a GitHub repository. * A custom network policy that grants GitHub access for a specific repository. * Experience with the policy iteration workflow: fail, diagnose, update, verify. This tutorial shows example prompts and responses from Claude Code. The exact wording you see might vary between sessions. Use the examples as a guide for the type of interaction, not as expected output. ## Prerequisites This tutorial requires the following: * A working OpenShell installation. Complete the [Quickstart](/get-started/quickstart) before proceeding. * A GitHub personal access token (PAT) with `repo` scope. Generate one from the [GitHub personal access token settings page](https://github.com/settings/tokens) by selecting **Generate new token (classic)** and enabling the `repo` scope. * An [Anthropic account](https://console.anthropic.com/) with access to Claude Code. OpenShell provides the sandbox runtime, not the agent. You must authenticate with your own account. * A GitHub repository you own to use as the push target. A scratch repository is sufficient. You can [create one](https://github.com/new) with a README if needed. This tutorial uses two terminals to demonstrate the iterative policy workflow: * **Terminal 1**: The sandbox terminal. You create the sandbox in this terminal by running `openshell sandbox create` and interact with Claude Code inside it. * **Terminal 2**: A terminal outside the sandbox on your machine. You use this terminal for viewing the sandbox logs with `openshell term` and applying an updated policy with `openshell policy set`. Each section below indicates which terminal to use. ## Set Up a Sandbox with Your GitHub Token Depending on whether you start a new sandbox or use an existing sandbox, choose the appropriate tab and follow the instructions. In terminal 2, create a new sandbox with Claude Code. The [default policy](/reference/default-policy) is applied automatically, which allows read-only access to GitHub. Create a [credential provider](/sandboxes/manage-providers) that injects your GitHub token into the sandbox automatically. The provider reads `GITHUB_TOKEN` from your host environment and sets it as an environment variable inside the sandbox: ```shell GITHUB_TOKEN= openshell provider create --name my-github --type github --from-existing openshell sandbox create --provider my-github -- claude ``` `openshell sandbox create` keeps the sandbox running after Claude Code exits, so you can apply policy updates later without recreating the environment. Add `--no-keep` if you want the sandbox deleted automatically instead. Claude Code starts inside the sandbox. It prints an authentication link. Open it in your browser, sign in to your Anthropic account, and return to the terminal. When prompted, trust the `/sandbox` workspace to allow Claude Code to read and write files. In terminal 1, connect to a sandbox that is already running and set your GitHub token as an environment variable: ```shell openshell sandbox connect export GITHUB_TOKEN= ``` To find the name of running sandboxes, run `openshell sandbox list` in terminal 2. ## Push Code to GitHub In terminal 1, ask Claude Code to write a simple script and push it to your repository. Replace `` with your GitHub organization or username and `` with your repository name. ```md title="Prompt" wordWrap showLineNumbers={false} Write a `hello_world.py` script and push it to `https://github.com//`. ``` Claude recognizes that it needs GitHub credentials. It asks how you want to authenticate. Provide your GitHub personal access token by pasting it into the conversation. Claude configures authentication and attempts the push. The push fails. Claude reports an error, but the failure is not an authentication problem. The default sandbox policy permits read-only access to GitHub and blocks write operations, so the proxy denies the push before the request reaches the GitHub server. ## Diagnose the Denial In this section, you diagnose the denial from your machine and from inside the sandbox. ### View the Logs from Your Machine In terminal 2, launch the OpenShell terminal: ```shell openshell term ``` The dashboard shows sandbox status and a live stream of policy decisions. Look for entries with `l7_decision=deny`. Select a deny entry to see the full detail: ```text l7_action: PUT l7_target: /repos///contents/hello_world.py l7_decision: deny dst_host: api.github.com dst_port: 443 l7_protocol: rest policy: github_rest_api l7_deny_reason: PUT /repos///contents/hello_world.py not permitted by policy ``` The log shows that the sandbox proxy intercepted an outbound `PUT` request to `api.github.com` and denied it. The `github_rest_api` policy allows read operations (GET) but blocks write operations (PUT, POST, DELETE) to the GitHub API. A similar denial appears for `github.com` if Claude attempted a git push over HTTPS. ### Ask Claude Code to Check the Sandbox Logs In terminal 1, ask Claude Code to check the sandbox logs for denied requests: ```md title="Prompt" wordWrap showLineNumbers={false} Check the sandbox logs for any denied network requests. What is blocking the push? ``` Claude reads the deny entries and identifies the root cause. It explains that the failure is a sandbox network policy restriction, not a token permissions issue. For example, the following is a possible response: The sandbox runs a proxy that enforces policies on outbound traffic. The `github_rest_api` policy allows GET requests (used to read the file) but blocks PUT/write requests to GitHub. This is a sandbox-level restriction, not a token issue. No matter what token you provide, pushes through the API are blocked until you update the policy. Both perspectives confirm the same thing: the proxy is doing its job. The default policy is designed to be restrictive. To allow GitHub pushes, you need to update the network policy. Copy the deny reason from Claude's response. You paste it into an agent running on your machine in the next step. ## Update the Policy from Your Machine In terminal 2, paste the deny reason from the previous step into your coding agent on your machine, such as Claude Code or Cursor, and ask it to recommend a policy update. The deny reason gives the agent the context it needs to generate the correct policy rules. After pasting the following prompt sample, properly provide the GitHub organization and repository names of the repository you are pushing to. ```md title="Prompt" wordWrap showLineNumbers={false} Based on the following deny reasons, recommend a sandbox policy update that allows GitHub pushes to `https://github.com//`, and save to `/tmp/sandbox-policy-update.yaml`: The `filesystem_policy`, `landlock`, and `process` sections are static. They are read once at sandbox creation and cannot be changed by a hot-reload. They are included here for completeness so the file is self-contained, but only the `network_policies` section takes effect when you apply this to a running sandbox. ``` The following steps outline the expected process done by the agent: 1. Inspects the deny reasons. 2. Writes an updated policy that adds `github_git` and `github_api` blocks that grant write access to your repository. 3. Saves the policy to `/tmp/sandbox-policy-update.yaml`. ## Review the Generated Policy Refer to the following policy example to compare with the generated policy before applying it. Confirm that the policy grants only the access you expect. In this case, `git push` operations and GitHub REST API access scoped to a single repository. The following YAML shows a complete policy that extends the [default policy](/reference/default-policy) with GitHub access for a single repository. Replace `` with your GitHub organization or username and `` with your repository name. The `filesystem_policy`, `landlock`, and `process` sections are static. OpenShell reads them at sandbox creation, and a hot reload cannot change them. They are included here for completeness so the file is self-contained, but only the `network_policies` section takes effect when you apply this to a running sandbox. ```yaml version: 1 # ── Static (locked at sandbox creation) ────────────────────────── filesystem_policy: include_workdir: true read_only: - /usr - /lib - /proc - /dev/urandom - /app - /etc - /var/log read_write: - /sandbox - /tmp - /dev/null landlock: compatibility: best_effort process: run_as_user: sandbox run_as_group: sandbox # ── Dynamic (hot-reloadable) ───────────────────────────────────── network_policies: # Claude Code ↔ Anthropic API claude_code: name: claude-code endpoints: - { host: api.anthropic.com, port: 443, protocol: rest, enforcement: enforce, access: full } - { host: statsig.anthropic.com, port: 443 } - { host: sentry.io, port: 443 } - { host: raw.githubusercontent.com, port: 443 } - { host: platform.claude.com, port: 443 } binaries: - { path: /usr/local/bin/claude } - { path: /usr/bin/node } # NVIDIA inference endpoint nvidia_inference: name: nvidia-inference endpoints: - { host: integrate.api.nvidia.com, port: 443 } binaries: - { path: /usr/bin/curl } - { path: /bin/bash } - { path: /usr/local/bin/opencode } # ── GitHub: git operations (clone, fetch, push) ────────────── github_git: name: github-git endpoints: - host: github.com port: 443 protocol: rest enforcement: enforce rules: - allow: method: GET path: "//.git/info/refs*" - allow: method: POST path: "//.git/git-upload-pack" - allow: method: POST path: "//.git/git-receive-pack" binaries: - { path: /usr/bin/git } # ── GitHub: REST API ───────────────────────────────────────── github_api: name: github-api endpoints: - host: api.github.com port: 443 path: "/repos///**" protocol: rest enforcement: enforce rules: # Full read-write access to the repository - allow: method: "*" path: "/repos///**" - host: api.github.com port: 443 path: "/graphql" protocol: graphql enforcement: enforce rules: # GitHub GraphQL API (used by gh CLI) - allow: operation_type: query - allow: operation_type: mutation fields: [createIssue, updateIssue, addComment] deny_rules: - operation_type: mutation fields: [deleteRepository, deleteRef, updateBranchProtectionRule] binaries: - { path: /usr/local/bin/claude } - { path: /usr/local/bin/opencode } - { path: /usr/bin/gh } - { path: /usr/bin/curl } # ── Package managers ───────────────────────────────────────── pypi: name: pypi endpoints: - { host: pypi.org, port: 443 } - { host: files.pythonhosted.org, port: 443 } - { host: github.com, port: 443 } - { host: objects.githubusercontent.com, port: 443 } - { host: api.github.com, port: 443 } - { host: downloads.python.org, port: 443 } binaries: - { path: /sandbox/.venv/bin/python } - { path: /sandbox/.venv/bin/python3 } - { path: /sandbox/.venv/bin/pip } - { path: "/sandbox/.uv/python/**/python*" } - { path: /usr/local/bin/uv } - { path: "/sandbox/.uv/python/**" } # ── VS Code Remote ────────────────────────────────────────── vscode: name: vscode endpoints: - { host: update.code.visualstudio.com, port: 443 } - { host: "*.vo.msecnd.net", port: 443 } - { host: vscode.download.prss.microsoft.com, port: 443 } - { host: marketplace.visualstudio.com, port: 443 } - { host: "*.gallerycdn.vsassets.io", port: 443 } binaries: - { path: /usr/bin/curl } - { path: /usr/bin/wget } - { path: "/sandbox/.vscode-server/**" } - { path: "/sandbox/.vscode-remote-containers/**" } ``` The following table summarizes the two GitHub-specific blocks: | Block | Endpoint | Behavior | | ------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `github_git` | `github.com:443` | Git Smart HTTP protocol. The proxy auto-detects and terminates TLS to inspect requests. Permits `info/refs` (clone/fetch), `git-upload-pack` (fetch data), and `git-receive-pack` (push) for the specified repository. Denies all operations on unlisted repositories. | | `github_api` | `api.github.com:443` | REST API. The proxy auto-detects and terminates TLS to inspect requests. Permits all HTTP methods for the specified repository and GraphQL queries. Denies API access to unlisted repositories. | The remaining blocks (`claude_code`, `nvidia_inference`, `pypi`, `vscode`) are identical to the [default policy](/reference/default-policy). The default policy's `github_ssh_over_https` and `github_rest_api` blocks are replaced by the `github_git` and `github_api` blocks above, which grant write access to the specified repository. Sandbox behavior outside of GitHub operations is unchanged. For details on policy block structure, refer to [Policies](/sandboxes/policies). ## Apply the Policy After you have reviewed the generated policy, apply it to the running sandbox: ```shell openshell policy set --policy /tmp/sandbox-policy-update.yaml --wait ``` Network policies are hot-reloadable. The `--wait` flag blocks until the policy engine confirms the new revision loaded, and the update takes effect immediately without restarting the sandbox or reconnecting Claude Code. ## Retry the Push In terminal 1, ask Claude Code to retry the push: ```md title="Prompt" wordWrap showLineNumbers={false} The sandbox policy has been updated. Try pushing to the repository again. ``` The push completes successfully. The `openshell term` dashboard now shows `l7_decision=allow` entries for `api.github.com` and `github.com` where it previously showed denials. ## Clean Up When you are finished, delete the sandbox to free gateway compute resources: ```shell openshell sandbox delete ``` ## Next Steps The following resources cover related topics in greater depth: * To add per-repository access levels (read-write vs read-only) or restrict to specific API methods, refer to the [Policy Schema Reference](/reference/policy-schema). * To learn the full policy iteration workflow (pull, edit, push, verify), refer to [Policies](/sandboxes/policies). * To inject credentials automatically instead of pasting tokens, refer to [Manage Providers](/sandboxes/manage-providers) # Run Local Inference with Ollama > Run local and cloud models inside an OpenShell sandbox using the Ollama community sandbox, or route sandbox requests to a host-level Ollama server. This tutorial covers two ways of running Ollama with OpenShell: 1. Ollama sandbox. This is the recommended way to run Ollama. A self-contained sandbox with Ollama, Claude Code, and Codex pre-installed. One command starts it. 2. Host-level Ollama. This is an alternative way to run Ollama. Run Ollama on the gateway host and route sandbox inference to it. Use this option when you want a single Ollama instance shared across multiple sandboxes. After completing this tutorial, you know how to: * Launch the Ollama community sandbox for a batteries-included experience. * Use `ollama launch` to start coding agents inside a sandbox. * Expose a host-level Ollama server to sandboxes through `inference.local`. ## Prerequisites * A working OpenShell installation. Complete the [Quickstart](/get-started/quickstart) before proceeding. ## Option A: Ollama Community Sandbox (Recommended) The Ollama community sandbox bundles Ollama, Claude Code, OpenCode, and Codex into a single image. Ollama starts automatically when the sandbox launches. ### Create the Sandbox ```shell openshell sandbox create --from ollama ``` This pulls the community sandbox image, applies the bundled policy, and drops you into a shell with Ollama running. ### Chat with a Model Chat with a local model ```shell ollama run qwen3.5 ``` Or a cloud model ```shell ollama run kimi-k2.5:cloud ``` Or use `ollama launch` to start a coding agent with Ollama as the model backend: ```shell ollama launch claude ollama launch codex ollama launch opencode ``` For CI/CD and automated workflows, `ollama launch` supports a headless mode: ```shell ollama launch claude --yes --model qwen3.5 ``` ### Model Recommendations | Use case | Model | Notes | | -------------------- | ------------------ | ---------------------------------------------------------------- | | Smoke test | `qwen3.5:0.8b` | Fast, lightweight, good for verifying setup | | Coding and reasoning | `qwen3.5` | Strong tool calling support for agentic workflows | | Complex tasks | `nemotron-3-super` | 122B parameter model, needs 48GB+ VRAM | | No local GPU | `qwen3.5:cloud` | Runs on Ollama's cloud infrastructure, no `ollama pull` required | Cloud models use the `:cloud` tag suffix and do not require local hardware. ```shell openshell sandbox create --from ollama ``` ### Tool Calling Agentic workflows (Claude Code, Codex, OpenCode) rely on tool calling. The following models have reliable tool calling support: Qwen 3.5, Nemotron-3-Super, GLM-5, and Kimi-K2.5. Check the [Ollama model library](https://ollama.com/library) for the latest models. ### Updating Ollama To update Ollama inside a running sandbox: ```shell update-ollama ``` Or auto-update on every sandbox start: ```shell openshell sandbox create --from ollama -e OLLAMA_UPDATE=1 ``` ## Option B: Host-Level Ollama Use this approach when you want a single Ollama instance on the gateway host, shared across multiple sandboxes through `inference.local`. This approach uses Ollama because it is easy to install and run locally, but you can substitute other inference engines such as vLLM, SGLang, TRT-LLM, and NVIDIA NIM by changing the startup command, base URL, and model name. ### Install and Start Ollama Install [Ollama](https://ollama.com/) on the gateway host: ```shell curl -fsSL https://ollama.com/install.sh | sh ``` Start Ollama on all interfaces so it is reachable from sandboxes: ```shell OLLAMA_HOST=0.0.0.0:11434 ollama serve ``` If you see `Error: listen tcp 0.0.0.0:11434: bind: address already in use`, Ollama is already running as a system service. Stop it first: ```shell systemctl stop ollama OLLAMA_HOST=0.0.0.0:11434 ollama serve ``` ### Pull a Model In a second terminal, pull a model: ```shell ollama run qwen3.5:0.8b ``` Type `/bye` to exit the interactive session. The model stays loaded. ### Create a Provider Create an OpenAI-compatible provider pointing at the host Ollama: ```shell openshell provider create \ --name ollama \ --type openai \ --credential OPENAI_API_KEY=empty \ --config OPENAI_BASE_URL=http://host.openshell.internal:11434/v1 ``` OpenShell injects `host.openshell.internal` so sandboxes and the gateway can reach the host machine. You can also use the host's LAN IP. ### Set Inference Routing ```shell openshell inference set --provider ollama --model qwen3.5:0.8b ``` Confirm: ```shell openshell inference get ``` ### Verify from a Sandbox ```shell openshell sandbox create -- \ curl https://inference.local/v1/chat/completions \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' ``` The response should be JSON from the model. ## Troubleshooting Common issues and fixes: * **Ollama not reachable from sandbox:** Ollama must be bound to `0.0.0.0`, not `127.0.0.1`. This applies to host-level Ollama only; the community sandbox handles this automatically. * **`OPENAI_BASE_URL` wrong:** Use `http://host.openshell.internal:11434/v1`, not `localhost` or `127.0.0.1`. * **Model not found:** Run `ollama ps` to confirm the model is loaded. Run `ollama pull ` if needed. * **HTTPS instead of HTTP:** Code inside sandboxes must call `https://inference.local`, not `http://`. * **AMD GPU driver issues:** Ollama v0.18+ requires ROCm 7 drivers for AMD GPUs. Update your drivers if you see GPU detection failures. Useful commands: ```shell openshell status openshell inference get openshell provider get ollama ``` ## Next Steps * To learn more about managed inference, refer to [Inference Routing](/sandboxes/inference-routing). * To configure a different self-hosted backend, refer to [Inference Routing](/sandboxes/inference-routing#configure-inference-routing). * To learn how sandbox containers are selected, refer to [Sandboxes](/sandboxes/manage-sandboxes#custom-containers). # Route Local Inference Requests to LM Studio > Configure inference.local to route sandbox requests to a local LM Studio server running on the gateway host. This tutorial describes how to configure OpenShell to route inference requests to a local LM Studio server. The LM Studio server provides easy setup with both OpenAI and Anthropic compatible endpoints. This tutorial covers: * Expose a local inference server to OpenShell sandboxes. * Verify end-to-end inference from inside a sandbox. ## Prerequisites First, complete OpenShell installation and follow the [Quickstart](/get-started/quickstart). [Install the LM Studio app](https://lmstudio.ai/download). Make sure that your LM Studio is running in the same environment as your gateway. If you prefer to work without having to keep the LM Studio app open, download llmster (headless LM Studio) with the following command: ```shell curl -fsSL https://lmstudio.ai/install.sh | bash ``` ```shell irm https://lmstudio.ai/install.ps1 | iex ``` And start llmster: ```shell lms daemon up ``` ## Start LM Studio Local Server Start the LM Studio local server from the Developer tab, and verify the OpenAI-compatible endpoint is enabled. LM Studio listens to `127.0.0.1:1234` by default. For use with OpenShell, configure LM Studio to listen on all interfaces (`0.0.0.0`). If you use the GUI, go to the Developer Tab, select Server Settings, then enable Serve on Local Network. If you use llmster in headless mode, run `lms server start --bind 0.0.0.0`. ## Test with a small model In the LM Studio app, head to the Model Search tab to download a small model like Qwen3.5 2B. In the terminal, use the following command to download and load the model: ```shell lms get qwen/qwen3.5-2b lms load qwen/qwen3.5-2b ``` ## Add LM Studio as a provider Choose the provider type that matches the client protocol you want to route through `inference.local`. Add LM Studio as an OpenAI-compatible provider through `host.openshell.internal`: ```shell openshell provider create \ --name lmstudio \ --type openai \ --credential OPENAI_API_KEY=lmstudio \ --config OPENAI_BASE_URL=http://host.openshell.internal:1234/v1 ``` Use this provider for clients that send OpenAI-compatible requests such as `POST /v1/chat/completions` or `POST /v1/responses`. Add a provider that points to LM Studio's Anthropic-compatible `POST /v1/messages` endpoint: ```shell openshell provider create \ --name lmstudio-anthropic \ --type anthropic \ --credential ANTHROPIC_API_KEY=lmstudio \ --config ANTHROPIC_BASE_URL=http://host.openshell.internal:1234 ``` Use this provider for Anthropic-compatible `POST /v1/messages` requests. ## Configure LM Studio as the local inference provider Set the managed inference route for the active gateway: ```shell openshell inference set --provider lmstudio --model qwen/qwen3.5-2b ``` If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected OpenAI-compatible request shape. ```shell openshell inference set --provider lmstudio-anthropic --model qwen/qwen3.5-2b ``` If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected Anthropic-compatible request shape. The active `inference.local` route is gateway-scoped, so only one provider and model pair is active at a time. Re-run `openshell inference set` whenever you want to switch between OpenAI-compatible and Anthropic-compatible clients. Confirm the saved config: ```shell openshell inference get ``` You should see either `Provider: lmstudio` or `Provider: lmstudio-anthropic`, along with `Model: qwen/qwen3.5-2b`. ## Verify from Inside a Sandbox Run a simple request through `https://inference.local`: ```shell showLineNumbers={true} openshell sandbox create -- \ curl https://inference.local/v1/chat/completions \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' openshell sandbox create -- \ curl https://inference.local/v1/responses \ --json '{ "instructions": "You are a helpful assistant.", "input": "hello", "max_output_tokens": 10 }' ``` ```shell openshell sandbox create -- \ curl https://inference.local/v1/messages \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' ``` ## Troubleshooting If setup fails, check these first: * LM Studio local server is running and reachable from the gateway host * `OPENAI_BASE_URL` uses `http://host.openshell.internal:1234/v1` when you use an `openai` provider * `ANTHROPIC_BASE_URL` uses `http://host.openshell.internal:1234` when you use an `anthropic` provider * The gateway and LM Studio run on the same machine or a reachable network path * The configured model name matches the model exposed by LM Studio Useful commands: ```shell openshell status openshell inference get openshell provider get lmstudio openshell provider get lmstudio-anthropic ``` ## Next Steps * To learn more about using the LM Studio CLI, refer to [LM Studio docs](https://lmstudio.ai/docs/cli) * To learn more about managed inference, refer to [Inference Routing](/sandboxes/inference-routing). * To configure a different self-hosted backend, refer to [Inference Routing](/sandboxes/inference-routing#configure-inference-routing). # Refresh Microsoft Graph Credentials with Providers v2 > Configure a Providers v2 Microsoft Graph profile with gateway-managed OAuth2 refresh-token rotation. Use Providers v2 to keep Microsoft Graph access tokens short lived while sandboxes receive a stable `MS_GRAPH_ACCESS_TOKEN` placeholder. OpenShell stores the non-injectable refresh material at the gateway, refreshes the Microsoft Graph access token before it expires, updates the provider record, and injects the current credential into newly launched sandbox processes. After completing this tutorial, you have: * A custom Microsoft Graph mail provider profile. * A provider instance configured with `oauth2-refresh-token`. * A sandbox that can use `curl` to read Microsoft Graph mail through provider-owned policy. This tutorial starts after your OAuth client has already completed the initial Microsoft sign-in flow. It does not publish a token bootstrap script. Use the Microsoft identity platform documentation for the [device authorization grant flow](https://learn.microsoft.com/en-ie/entra/identity-platform/v2-oauth2-device-code) or [authorization code flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow), and use any standards-compliant client that returns an access token, refresh token, and expiry. ## Prerequisites * A working OpenShell installation with an active gateway. Complete the [Quickstart](/get-started/quickstart) before proceeding. * A Microsoft Entra app registration that can acquire delegated Microsoft Graph mail access. * Delegated Microsoft Graph mail permission for the signed-in user. `Mail.Read` allows reading the signed-in user's mailbox; see the [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference). OAuth material from your initial Microsoft sign-in flow: | Variable | Value | | ---------------------------------- | ----------------------------------------------- | | `MS_TENANT_ID` | Microsoft Entra tenant ID, domain, or `common`. | | `MS_CLIENT_ID` | Microsoft Entra application client ID. | | `MS_GRAPH_ACCESS_TOKEN` | Current delegated Microsoft Graph access token. | | `MS_GRAPH_REFRESH_TOKEN` | Delegated OAuth refresh token. | | `MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT` | Absolute expiry for the current access token. | `MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT` can be an RFC3339 timestamp such as `2026-01-01T00:00:00Z` or a Unix epoch millisecond timestamp. Do not commit access tokens, refresh tokens, or local `.env` files. The commands below pass token material to the gateway; they are not examples of values to store in source control. ## Enable Providers v2 Enable provider profile policy composition on the active gateway: ```shell openshell settings set --global --key providers_v2_enabled --value true --yes ``` ## Create a Microsoft Graph Provider Profile Create `microsoft-graph-mail.yaml` with this profile: ```yaml showLineNumbers={false} # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 id: microsoft-graph-mail display_name: Microsoft Graph Mail description: Delegated Microsoft Graph mail read access category: messaging credentials: - name: graph_access_token description: Microsoft Graph delegated access token env_vars: [MS_GRAPH_ACCESS_TOKEN] required: true auth_style: bearer header_name: authorization refresh: strategy: oauth2_refresh_token token_url: https://login.microsoftonline.com/common/oauth2/v2.0/token scopes: [https://graph.microsoft.com/.default] refresh_before_seconds: 600 max_lifetime_seconds: 3600 material: - name: tenant_id description: Microsoft Entra tenant ID required: true - name: client_id description: Microsoft Entra application client ID required: true - name: refresh_token description: Delegated OAuth refresh token required: true secret: true endpoints: - host: graph.microsoft.com port: 443 protocol: rest access: read-only enforcement: enforce binaries: - /usr/bin/curl - /usr/local/bin/curl ``` Lint and import the profile: ```shell openshell provider profile lint -f microsoft-graph-mail.yaml openshell provider profile import -f microsoft-graph-mail.yaml ``` The profile defines the refresh strategy and Graph network policy. The `tenant_id` refresh material selects the Microsoft token endpoint during gateway-managed refresh. ## Create the Provider Create the provider with the current Microsoft Graph access token: ```shell openshell provider create \ --name microsoft-mail \ --type microsoft-graph-mail \ --credential MS_GRAPH_ACCESS_TOKEN="$MS_GRAPH_ACCESS_TOKEN" ``` The current CLI requires an initial credential at provider creation time. Refresh material is configured separately and is not injected into the sandbox. ## Configure Refresh Configure gateway-managed OAuth2 refresh-token rotation: ```shell openshell provider refresh configure microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN \ --strategy oauth2-refresh-token \ --material tenant_id="$MS_TENANT_ID" \ --material client_id="$MS_CLIENT_ID" \ --material refresh_token="$MS_GRAPH_REFRESH_TOKEN" \ --secret-material-key refresh_token \ --credential-expires-at "$MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT" ``` `--secret-material-key refresh_token` names the material key to mark as sensitive. It is not the refresh-token value. If Microsoft returns a rotated refresh token during refresh, OpenShell stores the new `refresh_token` material and marks it secret automatically. Force the first refresh immediately: ```shell openshell provider refresh rotate microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` Check refresh status: ```shell openshell provider refresh status microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` The status output shows refresh state, expiry, next refresh, and last refresh timing. It does not print access-token values or refresh material. ## Launch a Sandbox Launch a sandbox with the Microsoft Graph provider attached: ```shell openshell sandbox create \ --name microsoft-graph-mail \ --keep \ --provider microsoft-mail \ --no-auto-providers \ -- /bin/sh ``` Provider policy allows `curl` to reach `graph.microsoft.com:443`. The sandbox process receives `MS_GRAPH_ACCESS_TOKEN` as an OpenShell placeholder, and the proxy resolves that placeholder to the current gateway-managed access token when `curl` sends it in the authorization header. ## Verify Microsoft Graph Access Inside the sandbox, list a small page of mailbox messages: ```shell curl -sS \ -H "Authorization: Bearer $MS_GRAPH_ACCESS_TOKEN" \ 'https://graph.microsoft.com/v1.0/me/messages?$select=sender,subject&$top=5' ``` The request uses the [Microsoft Graph list messages API](https://learn.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0). If the token has delegated mail read permission, Microsoft Graph returns message metadata for the signed-in user's mailbox. ## Update Running Sandboxes Provider refresh updates the provider record at the gateway. Running sandboxes poll for provider environment revisions, but already-running processes keep the environment they started with. If you attach this provider to an existing sandbox or update provider credentials after a process has already started, launch a new process inside the sandbox before expecting `MS_GRAPH_ACCESS_TOKEN` to appear in that process environment. # Manage Sandboxes > Create sandboxes, understand sandbox isolation, and manage the full sandbox lifecycle. A sandbox is the OpenShell data plane: a safe, private execution environment where an AI agent runs. Each sandbox combines runtime isolation with OpenShell policy controls that prevent unauthorized data access, credential exposure, and network exfiltration. You need an active gateway before creating a sandbox. ## Create a Sandbox Create a sandbox with a single command. For example, to create a sandbox with Claude, run: ```shell openshell sandbox create -- claude ``` Every sandbox requires a gateway. Register or select one before running sandbox commands: ```shell openshell gateway add http://127.0.0.1:18080 --local --name local openshell gateway select local ``` ### CPU and Memory Set per-sandbox CPU and memory amounts with `--cpu` and `--memory`: ```shell openshell sandbox create --cpu 2 --memory 4Gi -- claude ``` CPU values use Kubernetes-style quantities such as `500m`, `1`, or `2.5`. Memory values use byte quantities such as `512Mi`, `4Gi`, or `8G`. Docker and Podman apply these values as runtime limits. Kubernetes applies each value as both the request and the limit so the scheduler reserves the same amount the sandbox can use. The VM driver currently accepts these flags but does not change VM allocation. ### GPU Resources To request GPU resources, add `--gpu`: ```shell openshell sandbox create --gpu -- claude ``` For Docker-backed sandboxes, GPU injection uses Docker CDI. If you enable Docker CDI after the gateway starts, restart the gateway so OpenShell can detect the updated Docker daemon capability. ### Custom Containers Use `--from` to create a sandbox from the base image, another pre-built sandbox name, a local directory, or a container image: ```shell openshell sandbox create --from base openshell sandbox create --from ollama openshell sandbox create --from ./my-sandbox-dir openshell sandbox create --from my-registry.example.com/my-image:latest ``` Bare names such as `base` and `ollama` resolve to images under `ghcr.io/nvidia/openshell-community/sandboxes`. Set `OPENSHELL_COMMUNITY_REGISTRY` when you need to use an internal mirror. Local directories and Dockerfiles require a local gateway because the CLI builds through the local Docker daemon. Use a registry image reference for remote gateways. ## Base Sandbox Container The `base` sandbox container is the default runtime image for standard OpenShell sandboxes unless the gateway overrides its default sandbox image. It is published as `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` and maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) repository. The base container includes common development tooling, supported agent CLIs, and the default sandbox policy. Use it when you want a general-purpose agent environment without a workflow-specific image: ```shell openshell sandbox create --from base ``` For default policy coverage by agent, refer to [Default Policy](/reference/default-policy). For the supported agent list, refer to [Supported Agents](/about/supported-agents). ## Connect to a Sandbox Open an SSH session into a running sandbox: ```shell openshell sandbox connect my-sandbox ``` Launch VS Code or Cursor directly into the sandbox workspace: ```shell openshell sandbox create --editor vscode --name my-sandbox openshell sandbox connect my-sandbox --editor cursor ``` When `--editor` is used, OpenShell keeps the sandbox alive and installs an OpenShell-managed SSH include file instead of cluttering your main `~/.ssh/config` with generated host blocks. ## Execute a Command in a Sandbox Run a one-shot command inside a running sandbox without opening an interactive shell: ```shell openshell sandbox exec -n my-sandbox -- ls -la /workspace ``` Pipe stdin into the command: ```shell echo "hello" | openshell sandbox exec -n my-sandbox -- cat ``` The command's exit code is propagated to the CLI, so `exec` works in scripts that check return codes. Run an interactive shell with a TTY: ```shell openshell sandbox exec -n my-sandbox --tty -- /bin/bash ``` OpenShell allocates a TTY automatically when both stdin and stdout are terminals. Force the behavior with `--tty` or disable it with `--no-tty`. | Flag | Purpose | | -------------- | -------------------------------------------------------- | | `-n`, `--name` | Sandbox to target. | | `--workdir` | Working directory for the command inside the sandbox. | | `--timeout` | Command timeout in seconds. `0` disables the timeout. | | `--tty` | Force TTY allocation. | | `--no-tty` | Disable TTY allocation even when attached to a terminal. | ## Label a Sandbox Attach labels when you create a sandbox to track ownership, environment, or workflow grouping: ```shell openshell sandbox create --label env=dev --label team=platform -- claude ``` List only the sandboxes that match a label selector: ```shell openshell sandbox list --selector env=dev openshell sandbox list --selector env=dev,team=platform ``` ## Expose Long Running Services Service forwarding makes a long-running process inside a sandbox reachable through a gateway-managed URL. Use it for development servers, notebooks, dashboards, or other services that keep listening after the sandbox starts. Run the service on loopback inside the sandbox, expose its port, then open the URL printed by OpenShell. Expose a service that listens on loopback inside the sandbox: ```shell openshell service expose my-sandbox 8080 ``` Pass an optional service name to create a named service URL: ```shell openshell service expose my-sandbox 8080 web ``` List exposed endpoints: ```shell openshell service list ``` List endpoints for one sandbox: ```shell openshell service list my-sandbox ``` Show or delete one endpoint: ```shell openshell service get my-sandbox web openshell service delete my-sandbox web ``` Omit the service name to manage the unnamed endpoint: ```shell openshell service get my-sandbox openshell service delete my-sandbox ``` Loopback gateways return local `openshell.localhost` URLs. Remote gateways return HTTPS URLs that require normal gateway authentication. For gateway service-domain configuration, refer to [Manage Gateways](/sandboxes/manage-gateways#configure-service-forwarding). ## Monitor and Debug List all sandboxes: ```shell openshell sandbox list ``` Filter the list by labels when you want a narrower view: ```shell openshell sandbox list --selector team=platform ``` Use `-o json` or `-o yaml` for machine-readable output: ```shell openshell sandbox list -o json openshell sandbox list -o yaml ``` Get detailed information about a specific sandbox. The output lists **Policy source** (`sandbox` or `global`), **Revision** (the active policy’s row version for that source), and the formatted active policy YAML: ```shell openshell sandbox get my-sandbox ``` Print only that policy YAML for scripting (same effective policy, no metadata): ```shell openshell sandbox get my-sandbox --policy-only ``` Stream sandbox logs to monitor agent activity and diagnose policy decisions: ```shell openshell logs my-sandbox ``` | Flag | Purpose | Example | | ---------- | ---------------------------- | ---------------------------------- | | `--tail` | Stream logs in real time | `openshell logs my-sandbox --tail` | | `--source` | Filter by log source | `--source sandbox` | | `--level` | Filter by severity | `--level warn` | | `--since` | Show logs from a time window | `--since 5m` | OpenShell Terminal combines sandbox status and live logs in a single real-time dashboard: ```shell openshell term ``` Use the terminal to spot blocked connections marked `action=deny` and inference-related proxy activity. If a connection is blocked unexpectedly, add the host to your network policy. Refer to [Policies](/sandboxes/policies) for the workflow. ## Port Forwarding Forward a local port to a running sandbox to access services inside it, such as a web server or database: ```shell openshell forward start 8000 my-sandbox openshell forward start 8000 my-sandbox -d # run in background ``` List and stop active forwards: ```shell openshell forward list openshell forward stop 8000 my-sandbox ``` You can also forward a port at creation time with `--forward`: ```shell openshell sandbox create --forward 8000 -- claude ``` ## SSH Config Generate an SSH config entry for a sandbox so tools like VS Code Remote-SSH can connect directly: ```shell openshell sandbox ssh-config my-sandbox ``` Append the output to `~/.ssh/config` or use `--editor` on `sandbox create`/`sandbox connect` for automatic setup. ## Transfer Files Upload files from your host into the sandbox: ```shell openshell sandbox upload my-sandbox ./src /sandbox/src ``` When the local path is a named directory, OpenShell preserves that basename at the destination, matching `scp -r` and `cp -r`. The example above creates `/sandbox/src/src`. OpenShell preserves symlinks during upload. A symlink arrives in the sandbox as a symlink with the same target path instead of an expanded copy of the target file or directory. Dangling symlinks are also preserved. Download files from the sandbox to your host: ```shell openshell sandbox download my-sandbox /sandbox/output ./local ``` When the sandbox-side source is a single file, the destination follows `cp`-style placement: if the destination already exists as a directory or ends with `/`, the file lands inside it as `/`; otherwise the file is written at the exact destination path. The CLI only allows sandbox-side sources that resolve inside the writable workspace (`/sandbox`). Paths that escape lexically (`/etc/passwd`, `/sandbox/../etc/passwd`) and paths that escape through a symlink (`/sandbox/etc-link` pointing at `/etc`) are both refused before any data is transferred. You can also upload files at creation time with the `--upload` flag on `openshell sandbox create`. Pass `--upload` multiple times to upload several paths in a single command: ```shell openshell sandbox create --upload ./src:/workspace/src --upload ./config:/workspace/config -- claude ``` ## Delete Sandboxes Deleting a sandbox stops all processes, releases resources, and purges injected credentials. ```shell openshell sandbox delete my-sandbox ``` ## Sandbox Lifecycle Every sandbox moves through a defined set of phases: | Phase | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Provisioning | The runtime is setting up the sandbox environment, injecting credentials, and applying your policy. | | Ready | The sandbox is running. The agent process is active and all isolation layers are enforced. You can connect, sync files, and view logs. | | Error | Something went wrong during provisioning or execution. Check logs with `openshell logs` for details. | | Deleting | The sandbox is being torn down. The system releases resources and purges credentials. | ## Sandbox Compute Drivers The gateway's configured compute driver determines how OpenShell creates each sandbox. The CLI workflow stays the same across drivers: you create, connect to, inspect, and delete sandboxes through the gateway API. For Docker, Podman, MicroVM, and Kubernetes behavior, refer to [Sandbox Compute Drivers](/reference/sandbox-compute-drivers). ## Next Steps * To follow a complete end-to-end example, refer to the [GitHub Sandbox](/get-started/tutorials/github-sandbox) tutorial. * To supply API keys or tokens, refer to [Manage Providers](/sandboxes/manage-providers). * To control what the agent can access, refer to [Policies](/sandboxes/policies). * To use the default runtime image, refer to [Base Sandbox Container](#base-sandbox-container). # Manage Gateways > Register OpenShell gateways, switch between environments, inspect gateway status, and troubleshoot gateway access. The gateway is the control plane for OpenShell. All control-plane traffic between the CLI and running sandboxes flows through the gateway. The gateway is responsible for: * Provisioning and managing sandboxes, including creation, deletion, and status monitoring. * Storing provider credentials and delivering them to sandboxes at startup. * Delivering network and filesystem policies to sandboxes. Policy enforcement itself happens inside each sandbox through the proxy, OPA, Landlock, and seccomp. * Managing inference configuration and serving inference bundles so sandboxes can route requests to the correct backend. * Providing the SSH tunnel endpoint so you can connect to sandboxes without exposing them directly. OpenShell separates gateway access from the compute driver that runs sandboxes. Use [Installation](/about/installation) to install OpenShell, choose a compute driver, and start a gateway. This page covers working with gateway entries after a gateway exists. ## Gateway Compute Drivers A gateway provisions sandboxes through the compute driver configured for that gateway. | Compute Driver | Where sandboxes run | Best for | | -------------- | ---------------------------------------- | --------------------------------------------------------------- | | Docker | Containers on the gateway host. | Solo development, quick iteration, and single-machine gateways. | | Podman | Rootless containers on the gateway host. | Workstations that avoid a rootful Docker daemon. | | Kubernetes | Pods in an operator-managed cluster. | Shared clusters and cloud environments. | | MicroVM | VM-backed sandboxes. | Workflows that need VM-backed isolation. | All compute drivers expose the same gateway API surface. Sandboxes, policies, and providers work the same after the CLI registers the gateway endpoint. The difference is how the gateway creates sandbox workloads and how operators expose the gateway to users. For driver setup, including Docker, Podman, MicroVM, and Kubernetes paths, refer to [Installation](/about/installation). ## Configure Service Forwarding Sandbox service routing is enabled for gateways by default. Users expose long-running sandbox services with `openshell service expose`, and the gateway routes browser traffic from the printed service URL to the loopback port inside the sandbox. Loopback gateways use `openshell.localhost` service URLs. OpenShell prints a URL in the form `http://.openshell.localhost:/` or `http://--.openshell.localhost:/` when a service name is provided. Browser traffic enters the same gateway listener as mTLS-protected gRPC, but plaintext HTTP is accepted only from loopback clients and only for sandbox service hostnames. Gateway APIs, auth routes, health endpoints, and non-service hostnames remain unavailable over plaintext HTTP. Cross-origin and sibling-subdomain browser requests are rejected before reaching the sandbox service. Disable the local browser path with `--enable-loopback-service-http=false` or `OPENSHELL_ENABLE_LOOPBACK_SERVICE_HTTP=false`. Custom HTTPS service domains use the gateway server SAN configuration. Add a wildcard DNS SAN such as `*.apps.example.com` to the gateway certificate and pass the same SAN to the gateway with `--server-san` or `OPENSHELL_SERVER_SAN`. For remote or non-loopback gateways, browser service URLs remain HTTPS and require normal gateway authentication. ## Register an Existing Gateway Use `openshell gateway add` to register any reachable gateway endpoint so the CLI can target it. Register a plaintext local endpoint, such as a trusted port-forward: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` Register a gateway behind an authenticated reverse proxy: ```shell openshell gateway add https://gateway.example.com --name production ``` This opens your browser for the proxy's login flow when the gateway uses edge authentication. If the token expires later, re-authenticate with: ```shell openshell gateway login production ``` For direct mTLS endpoints, place the CLI client certificate bundle in the gateway credential directory described in [Gateway Authentication](/reference/gateway-auth), then register or select that gateway name. ## Manage Multiple Gateways One gateway is always the active gateway. All CLI commands target it by default. `gateway add` sets the new gateway as active. The active gateway is the persisted default. The `-g` flag and the `OPENSHELL_GATEWAY` environment variable override it when commands resolve a gateway. If `OPENSHELL_GATEWAY` is set to a different gateway, `openshell gateway select ` still saves the new default and warns that the current shell continues to use the environment value until you unset or update it. List all registered gateways: ```shell openshell gateway list ``` Switch the active gateway: ```shell openshell gateway select production ``` Override the active gateway for a single command with `-g`: ```shell openshell status -g staging ``` ## Inspect Gateway Status Use `openshell status` for a quick health check: ```shell openshell status ``` Use `openshell gateway info` when you need the registered endpoint, gateway metadata, or compute driver details: ```shell openshell gateway info openshell gateway info --name production ``` Remove a local CLI registration without stopping the gateway service: ```shell openshell gateway remove production ``` ## Troubleshoot Check gateway health: ```shell openshell status openshell gateway info ``` For Docker-backed local gateways, inspect Docker and the gateway process or container started by your local workflow: ```shell openshell doctor check openshell gateway info ``` For Kubernetes gateways, inspect the gateway workload and cluster events: ```shell kubectl -n openshell get pods kubectl -n openshell logs statefulset/openshell kubectl -n openshell get events --sort-by=.lastTimestamp ``` For Podman or MicroVM gateways managed by systemd, inspect the user service and logs: ```shell systemctl --user status openshell-gateway journalctl --user -u openshell-gateway --no-pager -n 50 ``` For sandbox startup failures, inspect the selected compute driver: | Compute Driver | What to check | | -------------- | ------------------------------------------------------------------------------------------------- | | Docker | Docker daemon health, image availability, gateway logs, and sandbox container state. | | Podman | Podman socket availability, rootless networking, image availability, and sandbox container state. | | Kubernetes | Events and sandbox pods in the namespace configured by `server.sandboxNamespace`. | | MicroVM | VM driver logs, rootfs availability, and gateway logs. | ## Next Steps * To install OpenShell and choose a compute driver, refer to [Installation](/about/installation). * To create a sandbox using the gateway, refer to [Manage Sandboxes](/sandboxes/manage-sandboxes). # Providers > Create and manage credential providers that inject API keys and tokens into OpenShell sandboxes. AI agents typically need credentials to access external services: an API key for the AI model provider, a token for GitHub or GitLab, and so on. OpenShell manages these credentials as first-class entities called *providers*. Create and manage providers that supply credentials to sandboxes. Providers v2 is available for profile-backed provider policy, provider-owned network rules, and gateway-managed credential refresh. This page remains the credential-focused provider command reference. For the new workflow, see [Providers v2](/sandboxes/providers-v2). Provider profiles include metadata for known endpoints and binaries. View the available profiles before creating a provider: ```shell openshell provider list-profiles ``` ## Create a Provider Providers can be created from local environment variables or with explicit credential values. For refresh-backed providers such as `google-vertex-ai --from-gcloud-adc`, `openshell provider create` now waits for the gateway to configure refresh metadata and mint the initial access token before it reports success. ### From Local Credentials The fastest way to create a provider is to let the CLI discover credentials from your shell environment: ```shell openshell provider create --name my-claude --type claude --from-existing ``` This reads `ANTHROPIC_API_KEY` or `CLAUDE_API_KEY` from your current environment and stores them in the provider. ### With Explicit Credentials Supply a credential value directly: ```shell openshell provider create --name my-api --type generic --credential API_KEY=sk-abc123 ``` ### Bare Key Form Pass a key name without a value to read the value from the environment variable of that name: ```shell openshell provider create --name my-api --type generic --credential API_KEY ``` This looks up the current value of `$API_KEY` in your shell and stores it. Provider profile metadata is available for known provider types. Provider profile network policy is gateway opt-in: ```shell openshell settings set --global --key providers_v2_enabled --value true ``` Without `providers_v2_enabled=true`, provider behavior remains credential-only. When `providers_v2_enabled=true`, `--from-existing` uses profile-backed discovery instead of the legacy provider registry. The requested `--type` must have a built-in or imported provider profile with a `discovery` section. If no matching profile exists, the CLI returns an error instead of falling back to legacy discovery. ## Manage Providers List, inspect, update, and delete providers from the active gateway. List all providers: ```shell openshell provider list ``` Inspect a provider: ```shell openshell provider get my-claude ``` Update a provider's credentials: ```shell openshell provider update my-claude --from-existing ``` Set or clear a credential expiry timestamp: ```shell openshell provider update my-graph \ --credential MS_GRAPH_ACCESS_TOKEN="$MS_GRAPH_ACCESS_TOKEN" \ --credential-expires-at MS_GRAPH_ACCESS_TOKEN=1767225600000 ``` Use `0` as the timestamp to clear expiry for a credential key. ## Credential Refresh Provider refresh stores non-injectable refresh material separately from the provider's current credential values. The gateway can mint OAuth2 refresh-token tokens, OAuth2 client credentials tokens, and Google service account JWT tokens, then write the current access token back to the provider record for sandbox injection. Configure refresh metadata for one injectable credential key: ```shell openshell provider refresh configure my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN \ --strategy oauth2-client-credentials \ --material tenant_id="$TENANT_ID" \ --material client_id="$CLIENT_ID" \ --material client_secret="$CLIENT_SECRET" \ --secret-material-key client_secret \ --credential-expires-at 1767225600000 ``` Check refresh status: ```shell openshell provider refresh status my-graph ``` Delete refresh metadata for a credential: ```shell openshell provider refresh delete my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` Force a gateway-managed refresh for one credential: ```shell openshell provider refresh rotate my-graph --credential-key MS_GRAPH_ACCESS_TOKEN ``` External refresh systems should continue to push new current credentials through `openshell provider update`. The `--credential-expires-at` option works for static credentials, externally refreshed credentials, and gateway-managed refresh strategies. Delete a provider: ```shell openshell provider delete my-claude ``` ## Attach Providers to Sandboxes Pass one or more `--provider` flags when creating a sandbox: ```shell openshell sandbox create --provider my-claude --provider my-github -- claude ``` Each `--provider` flag attaches one provider. The sandbox receives all credentials from every attached provider at runtime. Profile-managed providers also contribute provider-generated network policy entries when `providers_v2_enabled` is enabled at the gateway. When the setting is disabled, providers keep the previous behavior and only provide credentials. Legacy provider attachment is fixed at sandbox creation time. Providers v2 adds `openshell sandbox provider attach` and `openshell sandbox provider detach` for running sandboxes. See [Providers v2](/sandboxes/providers-v2#attach-and-detach-providers) for runtime attach and detach behavior. ### Auto-Discovery Shortcut When `providers_v2_enabled=false` and the trailing command in `openshell sandbox create` is a recognized tool name (`claude`, `codex`, or `opencode`), the CLI auto-creates the required provider from your local credentials if one does not already exist. You do not need to create the provider separately: ```shell openshell sandbox create -- claude ``` This detects `claude` as a known tool, finds your `ANTHROPIC_API_KEY`, creates a provider, attaches it to the sandbox, and launches Claude Code. Providers v2 disables command-derived provider inference. When `providers_v2_enabled=true`, create or import the provider profile, create the provider instance, and pass `--provider ` explicitly. ## How Credential Injection Works The agent process inside the sandbox never sees real credential values. At startup, the proxy replaces each credential with an opaque placeholder token in the agent's environment. When the agent sends an HTTP request containing a placeholder, the proxy resolves it to the real credential before forwarding upstream. This resolution requires the proxy to see plaintext HTTP. Endpoints must use `protocol: rest` in the policy (which auto-terminates TLS) or explicit `tls: terminate`. Endpoints without TLS termination pass traffic through as an opaque stream, and credential placeholders are forwarded unresolved. ### Supported injection locations The proxy resolves credential placeholders in the following parts of an HTTP request: | Location | How the agent uses it | Example | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | Header value | Agent reads `$API_KEY` from env and places it in a header. | `Authorization: Bearer ` | | Header value (Basic auth) | Agent base64-encodes `user:` in an `Authorization: Basic` header. The proxy decodes, resolves, and re-encodes. | `Authorization: Basic ` | | Query parameter value | Agent places the placeholder in a URL query parameter. | `GET /api?key=` | | URL path segment | Agent builds a URL with the placeholder in the path. Supports concatenated patterns. | `POST /bot/sendMessage` | The proxy does not modify request bodies, cookies, or response content. ### Fail-closed behavior If the proxy detects a credential placeholder in a request but cannot resolve it, it rejects the request with HTTP 500 instead of forwarding the raw placeholder to the upstream server. This prevents accidental credential leakage in server logs or error responses. ### Example: Telegram Bot API (path-based credential) Create a provider with the Telegram bot token: ```shell openshell provider create --name telegram --type generic --credential TELEGRAM_BOT_TOKEN=123456:ABC-DEF ``` The agent reads `TELEGRAM_BOT_TOKEN` from its environment and builds a request like `POST /bot/sendMessage`. The proxy resolves the placeholder in the URL path and forwards `POST /bot123456:ABC-DEF/sendMessage` to the upstream. ### Example: Google API (query parameter credential) ```shell openshell provider create --name google --type generic --credential YOUTUBE_API_KEY=AIzaSy-secret ``` The agent sends `GET /youtube/v3/search?part=snippet&key=`. The proxy resolves the placeholder in the query parameter value and percent-encodes the result before forwarding. ## Supported Provider Types The following provider types are supported. | Type | Environment Variables Injected | Typical Use | | ----------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `anthropic` | `ANTHROPIC_API_KEY` | Anthropic API | | `claude` | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | Claude Code, Anthropic API | | `codex` | `OPENAI_API_KEY` | OpenAI Codex | | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` | GitHub Copilot CLI | | `generic` | User-defined | Any service with custom credentials | | `github` | `GITHUB_TOKEN`, `GH_TOKEN` | GitHub API and `gh` CLI. Refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox). | | `gitlab` | `GITLAB_TOKEN`, `GLAB_TOKEN`, `CI_JOB_TOKEN` | GitLab API, `glab` CLI | | `nvidia` | `NVIDIA_API_KEY` | NVIDIA API Catalog | | `openai` | `OPENAI_API_KEY` | Any OpenAI-compatible endpoint. Set `--config OPENAI_BASE_URL` to point to the provider. Refer to [Inference Routing](/sandboxes/inference-routing). | | `opencode` | `OPENCODE_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY` | OpenCode | `ANTHROPIC_API_KEY` is an API key from [console.anthropic.com](https://console.anthropic.com), not a subscription token. Subscription users must generate a separate API key from the Anthropic Console. Use the `generic` type for any service not listed above. You define the environment variable names and values yourself with `--credential`. ## Supported Inference Providers The following providers have been tested with `inference.local`. Any provider that exposes an OpenAI-compatible API works with the `openai` type. Set `--config OPENAI_BASE_URL` to the provider's base URL and `--credential OPENAI_API_KEY` to your API key. | Provider | Name | Type | Base URL | API Key Variable | | ------------------ | ---------------- | ------------------ | ------------------------------------------------- | -------------------------------------------------------------------- | | NVIDIA API Catalog | `nvidia-prod` | `nvidia` | `https://integrate.api.nvidia.com/v1` | `NVIDIA_API_KEY` | | Anthropic | `anthropic-prod` | `anthropic` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` | | Google Vertex AI | `vertex-prod` | `google-vertex-ai` | Regional, global, or multi-region Vertex endpoint | `GOOGLE_VERTEX_AI_TOKEN` or `GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN` | | Baseten | `baseten` | `openai` | `https://inference.baseten.co/v1` | `OPENAI_API_KEY` | | Bitdeer AI | `bitdeer` | `openai` | `https://api-inference.bitdeer.ai/v1` | `OPENAI_API_KEY` | | Deepinfra | `deepinfra` | `openai` | `https://api.deepinfra.com/v1/openai` | `OPENAI_API_KEY` | | Groq | `groq` | `openai` | `https://api.groq.com/openai/v1` | `OPENAI_API_KEY` | | Ollama (local) | `ollama` | `openai` | `http://host.openshell.internal:11434/v1` | `OPENAI_API_KEY` | | LM Studio (local) | `lmstudio` | `openai` | `http://host.openshell.internal:1234/v1` | `OPENAI_API_KEY` | Refer to your provider's documentation for the correct base URL, available models, and API key setup. For the Vertex-specific auth flows and config keys, refer to [Google Vertex AI](/providers/google-vertex-ai). To configure inference routing, refer to [Inference Routing](/sandboxes/inference-routing). ## Next Steps Explore related topics: * To control what the agent can access, refer to [Policies](/sandboxes/policies). * To use the base sandbox container, refer to [Sandboxes](/sandboxes/manage-sandboxes#base-sandbox-container). * To view the complete field reference for the policy YAML, refer to the [Policy Schema Reference](/reference/policy-schema). # Providers v2 > Use provider profiles to attach credentials, network policy, and refresh metadata to OpenShell sandboxes. Providers v2 turns providers from credential records into profile-backed access bundles. A provider profile describes the credentials, endpoints, binaries, policy rules, and refresh behavior for a provider type. A provider instance stores the concrete credential and config values for one gateway. Use Providers v2 when you want provider-owned policy rules to travel with provider credentials. For example, a GitHub provider can describe both `GITHUB_TOKEN` and the GitHub API endpoints that a sandbox needs, so users do not have to copy the same network policy into every sandbox. ## Why Providers v2 Exists Provider credentials and network policy were previously configured through separate workflows. A user could create a GitHub provider that stored `GITHUB_TOKEN`, but the sandbox still needed a separate policy that allowed `api.github.com`, selected the right binaries, and configured REST enforcement. Providers v2 keeps those pieces together: | Need | Providers v2 behavior | | --------------------------- | --------------------------------------------------------------------------------------------------------- | | Repeatable provider setup | Built-in and custom provider profiles define reusable provider types. | | Provider-aware policy | Attached providers contribute `_provider_*` network policy entries to the effective sandbox policy. | | Custom provider definitions | You can export, edit, lint, import, list, and delete custom profiles. | | Runtime provider lifecycle | You can list, attach, and detach providers on existing sandboxes. | | Credential rotation | Provider refresh metadata lets the gateway refresh short-lived access tokens and update provider records. | | Backward compatibility | Credential delivery still uses environment placeholders and proxy rewrite. | ## Enable Providers v2 Provider profile policy composition is controlled by the gateway-level `providers_v2_enabled` setting. Enable it on the active gateway: ```shell openshell settings set --global --key providers_v2_enabled --value true ``` When the setting is disabled or unset, providers keep the existing credential-only behavior. Sandboxes still receive provider credential placeholders, but attached provider profiles do not add network policy entries to the effective policy. To disable provider profile policy composition, delete the setting: ```shell openshell settings delete --global --key providers_v2_enabled ``` The feature flag controls provider-derived policy layers. It does not change the current credential injection model. OpenShell still injects placeholder environment variables into sandbox processes and resolves those placeholders in outbound HTTP traffic. ## Available Features Providers v2 currently includes these user-facing features: * Built-in provider profiles stored in the `providers/` directory of the GitHub repository. * `openshell provider list-profiles` with table, YAML, and JSON output. * `openshell provider profile export`, `import`, `lint`, and `delete` for custom profiles. * Provider instances created from built-in or imported profile IDs with `openshell provider create --type `. * Profile-backed credential discovery for explicit `openshell provider create --from-existing` and `openshell provider update --from-existing` flows. The built-in `google-vertex-ai` profile also supplements discovery with Vertex config env vars such as `VERTEX_AI_PROJECT_ID` and `VERTEX_AI_REGION`. * Just-in-time effective policy composition from sandbox policy plus attached provider profiles. * Runtime sandbox provider lifecycle commands under `openshell sandbox provider list|attach|detach`. * Credential refresh configuration with `openshell provider refresh status|configure|rotate|delete`. * Credential expiry metadata with `openshell provider update --credential-expires-at`; values accept Unix epoch milliseconds or ISO/RFC3339 timestamps. ## Roadmap The following Providers v2 design items are not part of the current behavior: | Roadmap item | Current behavior | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Profile-driven explicit credential injection | Profile `auth_style`, `header_name`, and `query_param` fields are stored and validated, but runtime injection still depends on environment placeholders generated from provider credentials. | | Endpoint and binary scoped credential injection | Provider profile endpoints and binaries affect policy composition. They do not yet restrict which outbound requests can receive credential injection. | | Credential verification on create | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`. | | Automatic credential scope extraction | OpenShell does not yet inspect upstream provider responses to discover credential scopes. | | Inference mounting from attached providers | `inference_capable` is profile metadata. Attaching an inference-capable provider does not yet create `inference.local` routes. | | Multi-provider inference routing | Path-based routing such as `inference.local/openai/...` and `inference.local/anthropic/...` is not yet wired to provider profiles. | | Policy prover integration | OpenShell does not yet run the policy prover automatically on sandbox startup or block startup based on prover findings. | | Refresh telemetry as OCSF events | Credential refresh logs are secret-safe gateway logs. OCSF refresh events and metrics are future work. | Use [Inference Routing](/sandboxes/inference-routing) for the current `inference.local` model. ## Provider Profiles A provider profile defines a provider type. It contains metadata, credential declarations, endpoint policy, binary policy, inference metadata, and optional credential refresh metadata. List available profiles: ```shell openshell provider list-profiles ``` Built-in Providers v2 profiles currently include: | Profile ID | Category | Credential environment variables | | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `claude-code` | `agent` | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | | `codex` | `agent` | `CODEX_AUTH_ACCESS_TOKEN`, `CODEX_AUTH_REFRESH_TOKEN`, `CODEX_AUTH_ACCOUNT_ID`, `CODEX_AUTH_ID_TOKEN` | | `copilot` | `agent` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` | | `cursor` | `agent` | None | | `github` | `source_control` | `GITHUB_TOKEN`, `GH_TOKEN` | | `google-vertex-ai` | `inference` | `GOOGLE_SERVICE_ACCOUNT_KEY`, `GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN`, `VERTEX_AI_SERVICE_ACCOUNT_TOKEN`, `GOOGLE_VERTEX_AI_TOKEN`, `VERTEX_AI_TOKEN` | | `nvidia` | `inference` | `NVIDIA_API_KEY` | | `pypi` | `data` | None | Export a built-in profile as YAML: ```shell openshell provider profile export github -o yaml > github-profile.yaml ``` Lint a profile before importing it: ```shell openshell provider profile lint -f github-profile.yaml ``` Import one profile file: ```shell openshell provider profile import -f github-profile.yaml ``` Import all non-recursive `*.yaml`, `*.yml`, and `*.json` files from a directory: ```shell openshell provider profile import --from ./provider-profiles ``` Custom profile IDs must use lowercase kebab-case with `a-z`, `0-9`, and `-`. Built-in profile IDs and legacy provider aliases are reserved. Built-in profiles are read-only, and OpenShell rejects deleting a custom profile while a sandbox-attached provider uses it. ### Category Enum The `category` field controls how `openshell provider list-profiles` groups profiles. Use one of these canonical YAML values: | Value | Use for | | ---------------- | ------------------------------------------------------------------------------------ | | `other` | Profiles that do not fit a more specific category. This is the default when omitted. | | `inference` | Model and inference API providers. | | `agent` | Agent CLIs and coding tools. | | `source_control` | Git hosting, repository, and source control providers. | | `messaging` | Chat, email, notification, and messaging APIs. | | `data` | Data storage, file, database, and document APIs. | | `knowledge` | Search, retrieval, and knowledge-base providers. | ### Profile Schema Provider profile YAML and JSON use this shape. Treat this as a field map, not a profile to import verbatim. The endpoint and rule fields mirror the network policy schema used under `network_policies`. Refer to [Policy Schema Reference](/reference/policy-schema) for field semantics. ```yaml wordWrap showLineNumbers={false} id: custom-api display_name: Custom API description: Custom API access for sandbox agents category: data inference_capable: false credentials: - name: api_token description: API access token env_vars: [CUSTOM_API_TOKEN] required: true # Accepted values: basic, bearer, header, query. # These fields describe the intended credential placement. # Runtime injection still uses env placeholder resolution today. auth_style: bearer header_name: authorization query_param: api_key refresh: # Accepted values: # static, external, oauth2_refresh_token, # oauth2_client_credentials, google_service_account_jwt. strategy: oauth2_client_credentials token_url: https://login.example.com/oauth2/token scopes: [api.read, api.write] refresh_before_seconds: 300 max_lifetime_seconds: 3600 material: - name: client_id description: OAuth client ID required: true secret: false - name: client_secret description: OAuth client secret required: true secret: true discovery: credentials: [api_token] endpoints: - host: api.example.com port: 443 path: /v1/** protocol: rest tls: "" access: read-write enforcement: enforce allowed_ips: [] ports: [] allow_encoded_slash: false websocket_credential_rewrite: false request_body_credential_rewrite: false persisted_queries: deny graphql_max_body_bytes: 65536 rules: - allow: method: GET path: /v1/projects/** command: "" query: tag: any: ["prod-*", "staging-*"] operation_type: "" operation_name: "" fields: [] deny_rules: - method: DELETE path: /v1/projects/** command: "" query: {} operation_type: "" operation_name: "" fields: [] graphql_persisted_queries: # Key must match the request's persisted query hash or saved-query ID. 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08: operation_type: query operation_name: GetProject fields: [project] binaries: - /usr/bin/curl - /usr/local/bin/custom-cli ``` ### Profile Sections `id`, `display_name`, and `description` identify the profile. `id` is the value passed to `openshell provider create --type`. `category` groups profiles in `openshell provider list-profiles`. Use one of the values in the category enum. `credentials` declares the credential names, environment variables, auth metadata, and optional refresh metadata for the provider type. The current runtime still exposes configured credential keys as placeholder environment variables and resolves placeholders in outbound HTTP requests. `discovery` controls what `--from-existing` scans when `providers_v2_enabled=true`. Each entry in `discovery.credentials` must name a credential declared under `credentials`. OpenShell scans the referenced credential's `env_vars` in order and stores the first non-empty local environment value under the actual environment variable key. `endpoints` contains the same endpoint object shape as sandbox network policy. A profile can use access presets, protocol-specific allow rules, deny rules, WebSocket credential rewriting, request body credential rewriting, GraphQL fields, and SSRF IP allowlists. `binaries` contains the executable paths allowed to reach the profile endpoints when the profile contributes policy to a sandbox. `inference_capable` marks profiles that are intended to participate in inference workflows. It does not currently mount or configure `inference.local`. ### Refresh Metadata Credential refresh metadata belongs to one credential declaration. The profile defines allowed defaults, such as token URL, scopes, refresh lead time, maximum lifetime, and required material keys. The provider instance stores the actual refresh material. Profile YAML can declare these refresh strategies: | Strategy | Behavior | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `static` | Current credentials are updated through `openshell provider update`. The gateway does not mint a token. | | `external` | An external process updates current credentials through `openshell provider update`. The gateway does not mint a token. | | `oauth2_refresh_token` | The gateway exchanges a refresh token for a short-lived access token. | | `oauth2_client_credentials` | The gateway mints a short-lived access token with OAuth2 client credentials. | | `google_service_account_jwt` | The gateway signs a Google service account JWT and exchanges it for an access token. | `openshell provider refresh configure` accepts only gateway-mintable strategies: `oauth2-refresh-token`, `oauth2-client-credentials`, and `google-service-account-jwt`. Use `openshell provider update` for `static` and `external` refresh patterns. Gateway-managed refresh strategies use these material keys: | Strategy | Material keys | | ---------------------------- | ---------------------------------------------------------------------------------- | | `oauth2_refresh_token` | `client_id`, `refresh_token`, optional `client_secret`. | | `oauth2_client_credentials` | `client_id`, `client_secret`, optional `tenant_id` for Microsoft Entra token URLs. | | `google_service_account_jwt` | `client_email`, `private_key`, optional `subject` or `sub`. | OpenShell keeps token endpoints profile-owned. Refresh material cannot override `token_url` or `token_uri` during refresh configuration. ## Provider Instances A provider instance stores concrete credentials and config for a profile type. Built-in profile IDs and imported custom profile IDs are accepted by `--type`. Create a GitHub provider from the built-in `github` profile: ```shell openshell provider create \ --name work-github \ --type github \ --credential GITHUB_TOKEN ``` Create a provider from local credentials discovered through the provider profile: ```shell openshell provider create \ --name work-claude \ --type claude-code \ --from-existing ``` When `providers_v2_enabled=true`, `--from-existing` uses the provider profile's `discovery` section. If no profile exists for the requested type, the command fails instead of falling back to the legacy provider registry. When `providers_v2_enabled=false`, `--from-existing` uses the legacy provider registry and ignores profile `discovery` metadata. For example, with Providers v2 enabled, `--type openai --from-existing` requires an imported `openai` profile with a `discovery` section. Setting `OPENAI_API_KEY` alone is not enough for v2 profile discovery. Create a provider from an imported custom profile: ```shell openshell provider create \ --name custom-api \ --type custom-api \ --credential CUSTOM_API_TOKEN ``` Inspect the provider: ```shell openshell provider get custom-api ``` Update provider credentials: ```shell openshell provider update custom-api --credential CUSTOM_API_TOKEN ``` Set or clear credential expiry metadata: ```shell openshell provider update custom-api \ --credential CUSTOM_API_TOKEN="$CUSTOM_API_TOKEN" \ --credential-expires-at CUSTOM_API_TOKEN=2026-01-01T00:00:00Z ``` Use an ISO/RFC3339 timestamp or Unix epoch milliseconds. Use `0` as the timestamp to clear expiry for a credential key. OpenShell skips expired provider credentials when it builds a sandbox provider environment. Running sandboxes also reject expired retained credential generations during placeholder resolution, so stale placeholders fail closed instead of forwarding unresolved or expired credential material. ## Configure Credential Refresh Refresh configuration is stored separately from the current injectable credential value. The gateway refresh worker reads refresh state, mints a new short-lived token for supported strategies, writes the token back to the provider record, and updates credential expiry metadata. For a complete Microsoft Graph OAuth2 refresh-token walkthrough, see [Refresh Microsoft Graph Credentials with Providers v2](/get-started/tutorials/microsoft-graph-provider-refresh). The profile YAML strategy values use underscores, while the CLI `--strategy` values use kebab-case: | Profile YAML | CLI | | ---------------------------- | ---------------------------- | | `oauth2_refresh_token` | `oauth2-refresh-token` | | `oauth2_client_credentials` | `oauth2-client-credentials` | | `google_service_account_jwt` | `google-service-account-jwt` | Create the provider instance first: ```shell openshell provider create \ --name my-graph \ --type microsoft-graph-mail \ --credential MS_GRAPH_ACCESS_TOKEN ``` This example assumes you imported a custom profile with `id: microsoft-graph-mail`. Provider refresh can be configured only for provider types whose profile declares compatible credential refresh metadata. Configure OAuth2 client credentials refresh: ```shell openshell provider refresh configure my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN \ --strategy oauth2-client-credentials \ --material tenant_id="$MS_TENANT_ID" \ --material client_id="$MS_CLIENT_ID" \ --material client_secret="$MS_CLIENT_SECRET" \ --secret-material-key client_secret \ --credential-expires-at 2026-01-01T00:00:00Z ``` Configure OAuth2 refresh-token refresh: ```shell openshell provider refresh configure my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN \ --strategy oauth2-refresh-token \ --material client_id="$MS_CLIENT_ID" \ --material refresh_token="$MS_REFRESH_TOKEN" \ --material client_secret="$MS_CLIENT_SECRET" \ --secret-material-key refresh_token \ --secret-material-key client_secret ``` Configure Google service account JWT refresh: ```shell openshell provider create \ --name drive-work \ --type google-drive \ --credential GOOGLE_DRIVE_ACCESS_TOKEN openshell provider refresh configure drive-work \ --credential-key GOOGLE_DRIVE_ACCESS_TOKEN \ --strategy google-service-account-jwt \ --material client_email="$GOOGLE_CLIENT_EMAIL" \ --material private_key="$GOOGLE_PRIVATE_KEY" \ --secret-material-key private_key ``` This example assumes you imported a custom profile with `id: google-drive`. `--secret-material-key` takes the name of a `--material` key, not the secret value. For example, use `--material client_secret="$MS_CLIENT_SECRET"` with `--secret-material-key client_secret`. The key should match a material entry so OpenShell can record that material field as sensitive when it stores refresh state. The gateway uses the material values to mint future access tokens, but only the `--credential-key` value, such as `MS_GRAPH_ACCESS_TOKEN`, becomes the injectable provider credential. If a refresh response rotates an OAuth refresh token, OpenShell stores the new `refresh_token` material and marks `refresh_token` as secret automatically. Use `--credential-expires-at` when the current provider credential already has a known expiry timestamp. For refresh-managed keys, the value can be Unix epoch milliseconds or an ISO/RFC3339 timestamp such as `2026-01-01T00:00:00Z` or `2026-01-01T01:00:00+01:00`. OpenShell stores that value as epoch milliseconds in both refresh state and provider credential metadata. Later gateway-managed refreshes replace it with the minted token expiry. Force a refresh immediately: ```shell openshell provider refresh rotate my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` Check refresh status: ```shell openshell provider refresh status my-graph ``` The status table reports operational state without printing token values or refresh material: ```text PROVIDER CREDENTIAL_KEY STRATEGY STATUS EXPIRES_AT NEXT_REFRESH LAST_REFRESH LAST_ERROR my-graph MS_GRAPH_ACCESS_TOKEN oauth2_refresh_token refreshed 2026-06-01 00:00:00 2026-05-31 23:50:00 2026-05-31 23:00:00 - ``` When no refresh configuration exists, the CLI distinguishes whole-provider checks from credential-specific checks: ```text No refresh configurations found for provider 'my-graph'. No refresh configuration found for provider 'my-graph' credential 'MS_GRAPH_ACCESS_TOKEN'. ``` Delete refresh state for one credential: ```shell openshell provider refresh delete my-graph \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` Deleting refresh state clears the provider credential expiry only when that expiry came from the deleted refresh state. If you later set a different expiry manually with `openshell provider update --credential-expires-at`, OpenShell preserves the manual value. ### Refresh Logs The gateway emits secret-safe refresh logs during each worker sweep. Use these logs to check which credentials the gateway is watching, when the next refresh is due, and whether a credential is already refreshed. ```text 2026-05-16T19:42:34.705768Z INFO openshell_server::provider_refresh: provider credential refresh worker sweep watched_count=1 due_count=0 rotation_requested_count=0 2026-05-16T19:42:34.705905Z INFO openshell_server::provider_refresh: provider credential refresh watch provider=outlook-email credential_key=MS_GRAPH_ACCESS_TOKEN strategy=oauth2_refresh_token status=refreshed expires_at_ms=1778961995456 seconds_until_expiry=1440 next_refresh_at_ms=1778961395456 last_refresh_at_ms=1778958395456 seconds_until_refresh=840 due=false rotation_requested=false ``` The sweep line summarizes how many credential refresh records the worker inspected. The watch line shows the provider, credential key, strategy, refresh status, expiry time, next refresh time, and whether refresh is due or manually requested. It does not include access-token values or refresh material. Refresh updates the provider record. Sandboxes receive the updated credential through the same placeholder environment and proxy rewrite path as other provider credentials. ## Launch Sandboxes with Providers Attach providers when creating a sandbox with repeated `--provider` flags: ```shell openshell sandbox create \ --name provider-demo \ --provider work-claude \ --provider work-github \ -- claude ``` When `providers_v2_enabled=true`, each attached provider with a matching profile contributes a provider policy layer to the sandbox effective policy. When the setting is disabled, the sandbox receives provider credentials but not provider-derived policy entries. Providers v2 does not infer or auto-attach providers from the sandbox command. Attach providers explicitly with `--provider` during sandbox creation, or use `openshell sandbox provider attach` after creation. List providers attached to a sandbox: ```shell openshell sandbox provider list provider-demo ``` The list output includes provider name, provider type, credential key count, and config key count. ## Policy Composition OpenShell stores sandbox-authored policy and provider attachments separately. When a sandbox asks for its effective policy, the gateway composes the current sandbox policy with provider policy layers just in time. For example, the built-in GitHub profile contains these endpoints and binaries: ```yaml wordWrap showLineNumbers={false} id: github display_name: GitHub category: source_control credentials: - name: api_token env_vars: [GITHUB_TOKEN, GH_TOKEN] required: true auth_style: bearer header_name: authorization endpoints: - host: api.github.com port: 443 protocol: rest access: read-only enforcement: enforce - host: api.github.com port: 443 path: /graphql protocol: graphql access: read-only enforcement: enforce - host: github.com port: 443 protocol: rest access: read-only enforcement: enforce binaries: [/usr/bin/gh, /usr/local/bin/gh, /usr/bin/git, /usr/local/bin/git] ``` If a sandbox attaches a provider named `work-github`, the effective policy includes a generated provider rule: ```yaml wordWrap showLineNumbers={false} network_policies: custom_pypi: name: custom_pypi endpoints: - host: pypi.org port: 443 protocol: rest access: read-only enforcement: enforce binaries: - path: /usr/bin/python _provider_work_github: name: _provider_work_github endpoints: - host: api.github.com port: 443 protocol: rest access: read-only enforcement: enforce - host: api.github.com port: 443 path: /graphql protocol: graphql access: read-only enforcement: enforce - host: github.com port: 443 protocol: rest access: read-only enforcement: enforce binaries: - path: /usr/bin/gh - path: /usr/local/bin/gh - path: /usr/bin/git - path: /usr/local/bin/git ``` Inspect the effective policy: ```shell openshell policy get provider-demo ``` Composition follows these rules: * Provider policy entries use reserved `_provider_*` keys derived from provider instance names. * Provider policy entries are derived data. OpenShell does not persist them back into the sandbox-authored policy. * If a user-authored rule already uses the same key, OpenShell keeps the user rule and adds a numeric suffix to the provider rule. * Provider and user rules are concatenated. Overlapping endpoints remain separate rules. * A gateway global policy override suppresses provider-derived policy layers. ## Attach and Detach Providers Attach an existing provider to a running sandbox: ```shell openshell sandbox provider attach provider-demo work-github ``` Detach a provider: ```shell openshell sandbox provider detach provider-demo work-github ``` Attach and detach are idempotent. Attach validates that the provider exists before mutating the sandbox, and provider deletion fails while the provider is attached to any sandbox. ### Runtime Limitations Provider attach and detach update the persisted sandbox provider list. Running sandboxes poll for provider environment revisions and effective policy changes. The policy effect applies to future effective policy reads after the sandbox observes the update. The credential environment effect applies only to new process launches after the update is observed, such as later SSH, exec, or SFTP sessions. Already-running processes keep the environment they started with. OpenShell does not mutate a live process environment after provider attach, detach, or credential update. If a long-running process needs a newly attached provider credential placeholder, restart that process or launch a new process after the sandbox has observed the provider update. Detaching a provider removes its provider policy layer from future effective policy reads and removes its credential placeholders from future process environments. It does not remove environment variables from already-running processes. OpenShell rejects provider updates and refresh configuration when they would make two providers attached to the same sandbox expose the same active credential environment key. Use provider-specific credential names when one sandbox needs multiple providers with overlapping upstream concepts. ## Next Steps * Use [Providers](/sandboxes/manage-providers) for the current provider command reference. * Use [Customize Sandbox Policies](/sandboxes/policies) to apply user-authored policy rules. * Use [Policy Schema Reference](/reference/policy-schema) for endpoint and L7 rule field details. # Customize Sandbox Policies > Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes. Use this page to apply and iterate policy changes on running sandboxes. For a full field-by-field YAML definition, use the [Policy Schema Reference](/reference/policy-schema). ## Policy Structure A policy has static sections `filesystem_policy`, `landlock`, and `process` that are locked at sandbox creation, and a dynamic section `network_policies` that is hot-reloadable on a running sandbox. ```yaml wordWrap showLineNumbers={false} version: 1 # Static: locked at sandbox creation. Paths the agent can read vs read/write. filesystem_policy: read_only: [/usr, /lib, /etc] read_write: [/sandbox, /tmp] # Static: Landlock LSM kernel enforcement. best_effort uses highest ABI the host supports. landlock: compatibility: best_effort # Static: Unprivileged user/group the agent process runs as. process: run_as_user: sandbox run_as_group: sandbox # Dynamic: hot-reloadable. Named blocks of endpoints + binaries allowed to reach them. network_policies: my_api: name: my-api endpoints: - host: api.example.com port: 443 protocol: rest enforcement: enforce access: full binaries: - path: /usr/bin/curl ``` Static sections are locked at sandbox creation. Changing them requires destroying and recreating the sandbox. Dynamic sections can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting. When a hot reload changes rules on an active HTTP L7 endpoint, existing keep-alive tunnels are closed before forwarding another parsed request. Credential-injection-only HTTP passthrough tunnels use the same reload boundary. Most HTTP clients reconnect automatically, and the next request is evaluated against the current policy. Raw streams are connection-scoped and outside L7 live-reload guarantees. This includes `tls: skip`, non-HTTP TCP payloads, HTTP upgrades such as WebSocket, and long-lived response streams such as SSE. A reload applies to the next connection or next parsed HTTP request; it does not interrupt an already-forwarded raw stream. Use `protocol: websocket` when policy should stay attached to the RFC 6455 upgrade and client text messages after the allowed upgrade. Add `websocket_credential_rewrite: true` only when the relay should rewrite credential placeholders in client-to-server WebSocket text messages. Add `request_body_credential_rewrite: true` only on inspected REST endpoints that need OpenShell to rewrite placeholders in supported text request bodies. | Section | Type | Description | | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filesystem_policy` | Static | Controls which directories the agent can access on disk. Paths are split into `read_only` and `read_write` lists. Any path not listed in either list is inaccessible. Set `include_workdir: true` to automatically add the agent's working directory to `read_write`. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level. | | `landlock` | Static | Configures Landlock LSM enforcement behavior. Set `compatibility` to `best_effort` (skip individual inaccessible paths while applying remaining rules) or `hard_requirement` (fail if any path is inaccessible or the required kernel ABI is unavailable). Refer to the [Policy Schema Reference](/reference/policy-schema#landlock) for the full behavior table. | | `process` | Static | Sets the OS-level identity for the agent process. `run_as_user` and `run_as_group` default to `sandbox`. Root (`root` or `0`) is rejected. The agent also runs with seccomp filters that block dangerous system calls. | | `network_policies` | Dynamic | Controls network access for ordinary outbound traffic from the sandbox. Each block has a name, a list of endpoints (host, port, protocol, and optional rules), and a list of binaries allowed to use those endpoints.
Every outbound connection except `https://inference.local` goes through the proxy, which queries the [policy engine](/about/how-it-works#core-components) with the destination and calling binary. A connection is allowed only when both match an entry in the same policy block.
For endpoints with `protocol: rest`, the proxy auto-detects TLS and terminates it so each HTTP request can be checked against that endpoint's `rules` (method and path). For endpoints with `protocol: websocket`, the proxy validates the RFC 6455 upgrade and evaluates `GET` rules for the handshake plus either `WEBSOCKET_TEXT` rules for raw client text messages or GraphQL operation rules for GraphQL-over-WebSocket messages. Set `websocket_credential_rewrite: true` only when a WebSocket or REST compatibility endpoint must keep placeholder credentials in sandbox-owned text frames and resolve them at the OpenShell relay boundary.
Endpoints without `protocol` allow the TCP stream through without inspecting payloads.
If no endpoint matches, the connection is denied. Configure managed inference separately through [Inference Routing](/sandboxes/inference-routing). | ## Baseline Filesystem Paths When a sandbox runs in proxy mode (the default), OpenShell automatically adds baseline filesystem paths required for the sandbox child process to function: `/usr`, `/lib`, `/etc`, `/var/log` (read-only) and `/sandbox`, `/tmp` (read-write). Paths like `/app` are included in the baseline set but are only added if they exist in the container image. For GPU sandboxes, OpenShell also adds existing GPU device nodes as read-write paths. CUDA workloads require write access to procfs for thread metadata, so GPU baseline enrichment moves `/proc` from read-only to read-write when GPU devices are present. This filtering prevents a missing baseline path from degrading Landlock enforcement. Without it, a single missing path could cause the entire Landlock ruleset to fail, leaving the sandbox with no filesystem restrictions at all. User-specified paths in your policy YAML are not pre-filtered. If you list a path that does not exist: * In `best_effort` mode, the path is skipped with a warning and remaining rules are still applied. * In `hard_requirement` mode, sandbox startup fails immediately. This distinction means baseline system paths degrade gracefully while user-specified paths surface configuration errors. ## Apply a Custom Policy Pass a policy YAML file when creating the sandbox: ```shell openshell sandbox create --policy ./my-policy.yaml -- claude ``` `openshell sandbox create` keeps the sandbox running after the initial command exits, which is useful when you plan to iterate on the policy. Add `--no-keep` if you want the sandbox deleted automatically instead. To avoid passing `--policy` every time, set a default policy with an environment variable: ```shell export OPENSHELL_SANDBOX_POLICY=./my-policy.yaml openshell sandbox create -- claude ``` The CLI uses the policy from `OPENSHELL_SANDBOX_POLICY` whenever `--policy` is not explicitly provided. ## Iterate on a Running Sandbox To change what the sandbox can access, pull the current policy, edit the YAML, and push the update. The workflow is iterative: create the sandbox, monitor logs for denied actions, pull the policy, modify it, push, and verify. ```mermaid flowchart TD A["1. Create sandbox with initial policy"] --> B["2. Monitor logs for denied actions"] B --> C["3. Pull current policy"] C --> D["4. Modify the policy YAML"] D --> E["5. Push updated policy"] E --> F["6. Verify the new revision loaded"] F --> B style A fill:#76b900,stroke:#000000,color:#000000 style B fill:#76b900,stroke:#000000,color:#000000 style C fill:#76b900,stroke:#000000,color:#000000 style D fill:#ffffff,stroke:#000000,color:#000000 style E fill:#76b900,stroke:#000000,color:#000000 style F fill:#76b900,stroke:#000000,color:#000000 linkStyle default stroke:#76b900,stroke-width:2px ``` The following steps outline the hot-reload policy update workflow. 1. Create the sandbox with your initial policy by following [Apply a Custom Policy](#apply-a-custom-policy) above (or set `OPENSHELL_SANDBOX_POLICY`). 2. Monitor denials. Each log entry shows host, port, binary, and reason. Alternatively, use `openshell term` for a live dashboard. ```shell openshell logs --tail --source sandbox ``` 3. For additive network changes, use `openshell policy update`. This is the fastest path for adding endpoints, binaries, or REST and WebSocket allow/deny rules without replacing the full policy. The full option and format reference is in [Incremental Policy Updates](#incremental-policy-updates). ```shell openshell policy update \ --add-endpoint api.github.com:443:read-only:rest:enforce \ --binary /usr/bin/gh \ --wait openshell policy update \ --add-allow 'api.github.com:443:POST:/repos/*/issues' \ --wait ``` `--add-allow` and `--add-deny` target existing `protocol: rest` or `protocol: websocket` endpoints. If you pass multiple update flags in one command, OpenShell applies them as one atomic merge batch and persists at most one new revision. 4. For larger edits, pull the current effective policy and edit the YAML directly. Before reusing the file, strip the metadata header above the `---` line. If the sandbox has attached Providers v2 providers, remove generated `_provider_*` entries before reapplying the policy; those entries are derived from provider profiles. ```shell openshell policy get --full > current-policy.yaml ``` To inspect a stored sandbox-authored revision instead of the current effective policy, pass `--rev `. 5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, or `rules`. 6. Push the updated policy when you need a full replacement. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout. ```shell openshell policy set --policy current-policy.yaml --wait ``` 7. Verify the new revision. If status is `loaded`, repeat from step 2 as needed; if `failed`, fix the policy and repeat from step 4. ```shell openshell policy list ``` ## Incremental Policy Updates Use `openshell policy update` when you want to merge network policy changes into the current live policy instead of replacing the whole YAML document. This command only updates the dynamic `network_policies` section. `openshell policy update` is useful when you want to: * add a new endpoint for an existing binary without touching other policy sections. * add a few REST or WebSocket allow/deny rules after you see a blocked request in the logs. * remove one endpoint or one named rule without rewriting the rest of the file. * preview a merged result locally with `--dry-run` before you send it to the gateway. Use `openshell policy set` instead when you want to replace the full policy, update static sections, or make broader edits that are easier to express in YAML. ### Update Commands The incremental update surface is split into endpoint-level operations and method/path rule-level operations for REST and WebSocket endpoints. | Flag | What it changes | Typical use | | -------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `--add-endpoint ` | Creates or merges a network rule and endpoint. | Allow a new host and port, optionally with `access`, `protocol`, `enforcement`, endpoint options, and binaries. | | `--remove-endpoint ` | Removes one host and port match from the current policy. | Drop a stale endpoint or remove one port from a multi-port endpoint. | | `--remove-rule ` | Deletes a named `network_policies` entry. | Remove a whole rule by name when you no longer need it. | | `--add-allow ` | Appends method/path allow rules to an existing REST or WebSocket endpoint. | Permit one additional REST method/path or WebSocket `WEBSOCKET_TEXT` path on an API that is already configured. | | `--add-deny ` | Appends method/path deny rules to an existing REST or WebSocket endpoint. | Block a sensitive REST path or WebSocket text-message path under an endpoint that is otherwise allowed. | | `--binary ` | Adds binaries to every `--add-endpoint` rule in the same command. | Bind a new endpoint to one or more executables. | | `--rule-name ` | Overrides the generated rule name. | Keep a stable human-chosen rule name when adding exactly one endpoint. | | `--dry-run` | Shows the merged policy locally and does not call the gateway. | Review the result before persisting it. | | `--wait` | Polls until the sandbox reports that the new revision loaded. | Confirm the change took effect before continuing. | | `--timeout ` | Sets the timeout for `--wait`. | Extend the wait window for slower sandboxes. | `--wait` and `--dry-run` cannot be used together. ### Add Endpoint Compared to Allow and Deny `--add-endpoint` works at the endpoint and rule level. It creates a new `network_policies` entry when needed, or merges into an existing rule that already covers the same host and port. Use it when you define where traffic can go and which binaries can send it. `--add-allow` and `--add-deny` work at the method/path rule level. They do not create binaries, and they do not create a new endpoint. They modify an existing endpoint that already has `protocol: rest` or `protocol: websocket`. This is the practical difference: * Use `--add-endpoint` to say "allow this binary to reach `api.github.com:443`." * Use `--add-allow` to say "for that existing REST endpoint, also allow `POST /repos/*/issues`." * Use `--add-deny` to say "for that existing REST endpoint, explicitly deny `POST /admin/**`." * Use `--add-allow` to say "for that existing WebSocket endpoint, also allow client text messages on `/v1/realtime/**`." Current constraints: * `--add-allow` and `--add-deny` work on `protocol: rest` and `protocol: websocket` endpoints. * `--add-deny` requires the endpoint to already have an allow base, either an `access` preset or explicit allow `rules`. * `protocol: sql` is not a practical incremental workflow today. OpenShell does not do full SQL parsing, and SQL enforcement is not meaningfully supported yet. ### Endpoint Specs `--add-endpoint` uses this format: ```text host:port[:access[:protocol[:enforcement[:options]]]] ``` Each segment has a fixed meaning: | Segment | Required | Meaning | | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | Yes | Destination hostname. | | `port` | Yes | Destination port, `1` through `65535`. | | `access` | No | Access preset for L7 endpoints: `read-only`, `read-write`, or `full`. Incremental updates expand presets into protocol-specific method/path rules for REST and WebSocket endpoints. | | `protocol` | No | L7 inspection mode: `rest`, `websocket`, or `sql`. `sql` is audit-only and not a recommended workflow today. | | `enforcement` | No | Enforcement mode for inspected traffic: `enforce` or `audit`. | | `options` | No | Comma-separated endpoint options. Use `websocket-credential-rewrite` with `protocol: websocket` or REST compatibility endpoints that perform a WebSocket upgrade. Use `request-body-credential-rewrite` only with `protocol: rest`. | Examples: | Example | Meaning | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `pypi.org:443` | Add a plain L4 endpoint. The proxy allows the TCP stream and does not inspect HTTP requests. | | `api.github.com:443:read-only:rest:enforce` | Add a REST endpoint with the `read-only` preset expanded by the policy engine into GET, HEAD, and OPTIONS access. | | `api.example.com:443:read-write:rest:enforce:request-body-credential-rewrite` | Add a REST endpoint that rewrites credential placeholders in supported text request bodies. | | `realtime.example.com:443:read-write:websocket:enforce` | Add a WebSocket endpoint with the `read-write` preset expanded by the policy engine into the upgrade `GET` and client `WEBSOCKET_TEXT` access. | | `realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite` | Add a WebSocket endpoint that rewrites `openshell:resolve:env:*` placeholders in client text frames after an allowed upgrade. | If you set `protocol: rest` or `protocol: websocket`, you also need an allow shape. With incremental updates, that means you should provide an `access` preset on `--add-endpoint`, then use `--add-allow` or `--add-deny` to refine method/path rules later. Use the `websocket-credential-rewrite` endpoint option with `protocol: websocket` when the sandbox should send credential placeholders in client text frames and have OpenShell resolve them after the allowed upgrade. The option can also be used with `protocol: rest` compatibility endpoints that perform a WebSocket upgrade. It is rejected for plain L4 or `protocol: sql` endpoints. Use the `request-body-credential-rewrite` endpoint option with `protocol: rest` when an API expects OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. OpenShell buffers up to 256 KiB, rewrites recognized credential placeholders, updates `Content-Length`, and rejects unresolved placeholders instead of forwarding them. The option is rejected for WebSocket, GraphQL, SQL, and plain L4 endpoints. Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials. For example: * `api.github.com:443:read-only:rest` is valid. * `realtime.example.com:443:read-write:websocket` is valid. * `api.github.com:443::rest` is invalid. It does not mean "allow all traffic." An L7 endpoint with `protocol` but no `access` or `rules` is rejected when the policy loads. Endpoint options belong to the individual `--add-endpoint` spec. When you pass multiple `--add-endpoint` flags in one command, every `--binary` value applies to every added endpoint in that command. If different endpoints need different binaries, use separate `policy update` commands. If you do not pass `--rule-name`, OpenShell generates one from the host and port, such as `allow_api_github_com_443`. ### Method/Path Rule Specs `--add-allow` and `--add-deny` use this format: ```text host:port:METHOD:path_glob ``` This string identifies an existing REST or WebSocket endpoint and the request pattern you want to add. In shell commands, quote the full `SPEC` when it contains `*` or `**` so your shell passes it literally instead of expanding it as a local file glob. | Segment | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | Existing endpoint host. | | `port` | Existing endpoint port. | | `METHOD` | HTTP method for REST endpoints, or `GET` / `WEBSOCKET_TEXT` for WebSocket endpoints. The CLI normalizes it to uppercase. | | `path_glob` | URL path glob. For WebSocket text messages, this still matches the upgraded request path, not message payload content. It must start with `/`, or be `**`, or start with `**/`. | This example: ```text api.github.com:443:POST:/repos/*/issues ``` means: * match the endpoint `api.github.com:443`. * match HTTP method `POST`. * match paths like `/repos/acme/issues`. * do not match deeper paths like `/repos/acme/project/issues/123` because `*` matches one path segment. Path globs follow the same semantics as YAML allow and deny rules: * `*` matches one path segment. * `**` matches any number of segments. * `/repos/*/issues` matches one repository owner or name segment in the middle. * `/repos/**` matches everything under `/repos/`. The rule-level commands only modify method and path constraints. They do not change binaries, hostnames, ports, protocol settings, or WebSocket message payload matching. ### Common Workflows Use these patterns as starting points when you decide whether to update an endpoint or append REST/WebSocket rules. #### Add a new L4 endpoint Use `--add-endpoint` when you need a new host and port and do not need REST inspection. ```shell openshell policy update demo \ --add-endpoint pypi.org:443 \ --add-endpoint files.pythonhosted.org:443 \ --binary /usr/bin/pip \ --binary /usr/local/bin/uv \ --wait ``` This creates or merges endpoint entries and binds them to the listed binaries. It does not create inspected method/path rules. #### Create a REST endpoint with a base allow set Use `--add-endpoint` first when the endpoint does not exist yet. ```shell openshell policy update demo \ --add-endpoint api.github.com:443:read-only:rest:enforce \ --binary /usr/bin/gh \ --wait ``` This creates a REST endpoint and sets its base allow behavior through the `read-only` access preset. #### Add one more REST allow rule Use `--add-allow` after the REST endpoint already exists. ```shell openshell policy update demo \ --add-allow 'api.github.com:443:POST:/repos/*/issues' \ --wait ``` This keeps the existing endpoint definition and appends one new allow rule. It does not add binaries or change the endpoint host and port. #### Add a REST deny rule under an allowed endpoint Use `--add-deny` when you want to carve out a blocked subtree under an existing REST endpoint. ```shell openshell policy update demo \ --add-deny 'api.github.com:443:POST:/admin/**' \ --wait ``` This adds a deny rule to the existing REST endpoint. The endpoint must already have an allow base. #### Create a WebSocket endpoint with a base allow set Use `--add-endpoint` with `protocol: websocket` when the destination is an RFC 6455 WebSocket API. ```shell openshell policy update demo \ --add-endpoint realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite \ --binary /usr/bin/node \ --wait ``` This creates a WebSocket endpoint and sets its base allow behavior through the `read-write` access preset. For WebSocket endpoints, `read-write` expands to the upgrade `GET` and client `WEBSOCKET_TEXT` messages on the upgraded request path. The rewrite option lets the sandbox send `openshell:resolve:env:*` placeholders in client text frames; OpenShell resolves them before forwarding to the upstream service. #### Add a WebSocket text-message deny rule Use `WEBSOCKET_TEXT` when you want to refine client-to-server text-frame policy without matching message payload content. ```shell openshell policy update demo \ --add-deny 'realtime.example.com:443:WEBSOCKET_TEXT:/v1/admin/**' \ --wait ``` This adds a deny rule to the existing WebSocket endpoint. The path glob matches the WebSocket upgrade path. #### Remove one endpoint or rule Use `--remove-endpoint` to remove one host and port pair, or `--remove-rule` to delete the whole named rule. ```shell openshell policy update demo --remove-endpoint pypi.org:443 --wait openshell policy update demo --remove-rule github_repos --wait ``` If the target endpoint is part of a multi-port endpoint, `--remove-endpoint` removes only the specified port and keeps the rest. ### Merge Semantics OpenShell applies all update flags from one `openshell policy update` command as one merge batch. The gateway validates the full merged result and persists at most one new policy revision. This means: * one command is atomic at the revision level. * multiple flags in one command succeed or fail together. * concurrent writers do not partially interleave one batch with another. When two updates race, the gateway uses optimistic retry. It fetches the latest revision, reapplies the full batch, validates the result again, and retries the write. This preserves the intent of each individual command while still allowing concurrent sandbox policy updates. ### Preview and Validation Use `--dry-run` when you want to inspect the merged YAML before you send it to the gateway. ```shell openshell policy update demo \ --add-allow 'api.github.com:443:GET:/repos/**' \ --dry-run ``` The CLI validates the argument shapes before it sends the request. The gateway then validates the merged policy against the current live policy and returns clear errors when: * a required segment is missing. * a port is outside `1` through `65535`. * `--add-allow` or `--add-deny` points at an endpoint that does not exist. * `--add-allow` or `--add-deny` targets an endpoint that is neither REST nor WebSocket. * `--add-deny` targets an endpoint that has no base allow set. ## Global Policy Override Use a global policy when you want one policy payload to apply to every sandbox. ```shell openshell policy set --global --policy ./global-policy.yaml ``` When a global policy is configured: * The global payload is applied in full for all sandboxes. * Sandbox-level policy updates are rejected until the global policy is removed. To restore sandbox-level policy control, delete the global policy setting: ```shell openshell policy delete --global ``` You can inspect a sandbox's effective settings and policy source with: ```shell openshell settings get ``` ## Debug Denied Requests Check `openshell logs --tail --source sandbox` for the denied host, path, and binary. For agent-authored draft updates on running sandboxes, enable [Policy Advisor](/sandboxes/policy-advisor). Policy advisor lets the sandboxed agent submit a narrow proposal through `policy.local` while a developer still approves or rejects the structured rule from outside the sandbox. When triaging denied requests, check: * Destination host and port to confirm which endpoint is missing. * Calling binary path to confirm which `binaries` entry needs to be added or adjusted. * HTTP method and path for REST endpoints, or `GET` / `WEBSOCKET_TEXT` and the upgraded request path for WebSocket endpoints, to confirm which `rules` entry needs to be added or adjusted. Then push the updated policy as described above. For small changes, prefer `openshell policy update` over rewriting the full YAML: ```shell openshell policy update --add-allow 'api.github.com:443:GET:/repos/**' --wait ``` ## Examples Add these blocks to the `network_policies` section of your sandbox policy. Apply simple endpoints and REST/WebSocket rule additions with `openshell policy update`, or apply any complete YAML block with `openshell policy set --policy --wait`. Use **Simple endpoint** for host-level allowlists and **Granular rules** for method/path control. Allow `pip install` and `uv pip install` to reach PyPI: ```yaml showLineNumbers={false} pypi: name: pypi endpoints: - host: pypi.org port: 443 - host: files.pythonhosted.org port: 443 binaries: - { path: /usr/bin/pip } - { path: /usr/local/bin/uv } ``` Endpoints without `protocol` use TCP passthrough, where the proxy allows the stream without inspecting payloads. If the stream is HTTP and TLS is auto-terminated, the proxy can still rewrite configured credential placeholders and closes keep-alive passthrough tunnels on policy reload before forwarding another request. WebSocket text-frame policy requires an explicit `protocol: websocket` endpoint. WebSocket payload credential rewrite can also be enabled on a `protocol: rest` compatibility endpoint with `websocket_credential_rewrite: true`. REST request body credential rewrite requires an inspected `protocol: rest` endpoint with `request_body_credential_rewrite: true`. Allow Claude and the GitHub CLI to reach `api.github.com` with separate REST and GraphQL endpoint scopes: read-only REST for general API paths, GraphQL operation inspection on `/graphql`, full REST write access for `alpha-repo`, and create/edit issues only for `bravo-repo`. Replace `` with your GitHub org or username. For an end-to-end walkthrough that combines this policy with a GitHub credential provider and sandbox creation, refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox). ```yaml showLineNumbers={false} github_repos: name: github_repos endpoints: - host: api.github.com port: 443 path: "/**" protocol: rest enforcement: enforce rules: - allow: method: GET path: "/**" - allow: method: HEAD path: "/**" - allow: method: OPTIONS path: "/**" - allow: method: "*" path: "/repos//alpha-repo/**" - allow: method: POST path: "/repos//bravo-repo/issues" - allow: method: PATCH path: "/repos//bravo-repo/issues/*" - host: api.github.com port: 443 path: "/graphql" protocol: graphql enforcement: enforce rules: - allow: operation_type: query - allow: operation_type: mutation fields: [createIssue, updateIssue, addComment] deny_rules: - operation_type: mutation fields: [deleteRepository, deleteRef, updateBranchProtectionRule] binaries: - { path: /usr/local/bin/claude } - { path: /usr/bin/gh } ``` Endpoints with `protocol: rest` enable HTTP request inspection and can opt in to supported text request body credential rewrite. Endpoints with `protocol: websocket` validate WebSocket upgrades and inspect client text messages on the upgraded request path. WebSocket endpoints can also classify GraphQL-over-WebSocket operation messages with the same operation rules used by GraphQL-over-HTTP. Endpoints with `protocol: graphql` parse GraphQL-over-HTTP payloads before evaluating rules. The endpoint-level `path` field lets these protocols share `api.github.com:443` without treating GraphQL payloads as plain REST `POST /graphql` requests. ### Query parameter matching REST rules can also constrain query parameter values: ```yaml showLineNumbers={false} download_api: name: download_api endpoints: - host: api.example.com port: 443 protocol: rest enforcement: enforce rules: - allow: method: GET path: "/api/v1/download" query: slug: "skill-*" version: any: ["1.*", "2.*"] binaries: - { path: /usr/bin/curl } ``` `query` matchers are case-sensitive and run on decoded values. If a request has duplicate keys (for example, `tag=a&tag=b`), every value for that key must match the configured glob(s). ### GraphQL matching GraphQL endpoints use `protocol: graphql`. The proxy parses GraphQL-over-HTTP `GET` and `POST` requests, classifies each operation, and evaluates rules against the operation type, optional operation name, and selected root fields. GraphQL endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `graphql` as a protocol. ```yaml showLineNumbers={false} github_graphql: name: github_graphql endpoints: - host: api.github.com port: 443 path: "/graphql" protocol: graphql enforcement: enforce rules: - allow: operation_type: query fields: [viewer, repository] - allow: operation_type: mutation operation_name: Issue* fields: [createIssue] deny_rules: - operation_type: mutation fields: [deleteRepository] binaries: - { path: /usr/bin/gh } ``` For allow rules, every selected root field in an operation must match one of the configured `fields` globs. For deny rules, one matching root field blocks the request. Batched GraphQL requests are fail-closed: if any operation is malformed, denied, or unregistered, the whole HTTP request is denied. Hash-only persisted queries cannot be classified from the request alone. OpenShell denies them unless the endpoint uses `persisted_queries: allow_registered` and provides a trusted `graphql_persisted_queries` entry keyed by hash or saved-query ID. ### GraphQL-over-WebSocket matching Some APIs carry GraphQL operations over RFC 6455 WebSockets, commonly for subscriptions and realtime updates. Configure these as `protocol: websocket`, allow the upgrade with a normal `GET` rule, then add GraphQL operation rules for client operation messages. OpenShell recognizes modern `graphql-transport-ws` `subscribe` messages and legacy `graphql-ws` `start` messages. ```yaml showLineNumbers={false} realtime_graphql: name: realtime_graphql endpoints: - host: realtime.example.com port: 443 path: "/graphql" protocol: websocket enforcement: enforce rules: - allow: method: GET path: "/graphql" - allow: operation_type: subscription fields: [messageAdded] - allow: operation_type: query fields: [viewer] websocket_credential_rewrite: true binaries: - { path: /usr/bin/node } ``` When a WebSocket endpoint has GraphQL operation policy, client operation messages are fail-closed on malformed JSON, unsupported message types, parse errors, unregistered hash-only persisted queries, or unallowed operations. Use GraphQL operation rules for client messages rather than a raw `WEBSOCKET_TEXT` allow rule. Protocol lifecycle messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed without payload logging; if `websocket_credential_rewrite: true` is set, placeholders inside those text messages are resolved before forwarding. ### GraphQL service policy shapes GraphQL field names are application-specific, so treat these as starting shapes to review against the actual app schema: | Service | Endpoint shape | Starting policy | | ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Railway | `backboard.railway.app/graphql/v2` | Allow `query`; allow only reviewed deployment mutations; deny `volumeDelete`, `projectDelete`, `*Delete`, `*Destroy`. | | GitHub | `api.github.com/graphql` | Allow `query`; optionally allow low-risk mutations such as reactions; deny broad destructive/admin roots like `deleteRef`, `deleteRepository`, `updateBranchProtectionRule`, and `delete*`. | | GitLab | `/api/graphql` | Prefer read-only token scopes where possible; allow `query`; deny mutations by default or allow only reviewed workflow roots. | | Shopify Admin | `*.myshopify.com/admin/api/**/graphql.json` | Allow `query`; allow app-specific mutations only; deny `*Delete`, `bulkOperationRunMutation`, and high-impact inventory/order/customer roots unless approved. | | monday.com | `api.monday.com/v2` | Allow board/item reads; allow tightly scoped create/update mutations only where needed; deny delete/archive roots. | | Salesforce GraphQL | Salesforce GraphQL endpoint | Allow `query`; deny record create/update/delete mutations unless the sandbox is intended to modify CRM data. | | Hygraph | Project content API endpoint | Allow content reads; deny generated destructive content roots such as `delete*`, `deleteMany*`, `unpublish*`, and batch mutations unless a publishing workflow requires them. | | Atlassian GraphQL Gateway | `api.atlassian.com/graphql` | Allow reads by default; require explicit mutation allowlists because the gateway spans Jira, Confluence, Bitbucket, and admin surfaces. | ## Next Steps Explore related topics: * To learn about the built-in sandbox policy, refer to [Default Policy](/reference/default-policy). * To view the full field-by-field YAML definition, refer to the [Policy Schema Reference](/reference/policy-schema). * To review the default policy breakdown, refer to [Default Policy](/reference/default-policy). # Use Policy Advisor > Let sandboxed agents propose narrow policy changes through policy.local while keeping developer approval in the loop. Policy advisor lets a running sandboxed agent ask for a narrow network policy change after OpenShell denies a request. The agent submits a draft through `policy.local`, a developer approves or rejects it from outside the sandbox, and approved network policy hot-reloads into the same sandbox. Policy advisor preserves OpenShell's default-deny posture. The structured rule is the approval contract, and the agent's rationale is supporting context. By default every proposal lands in the draft inbox for human review. Opt-in [auto mode](#approval-modes) lets the gateway approve provably safe proposals — those whose [prover delta](#what-auto-approval-checks) is empty — without a reviewer in the loop; proposals with any prover finding still require human approval. ## Enable Policy Advisor Policy advisor is disabled by default. Enable it globally when you want every sandbox on the selected gateway to expose the agent proposal surface: ```shell openshell settings set --global \ --key agent_policy_proposals_enabled \ --value true \ --yes ``` You can also enable it for one sandbox, unless the key is managed globally: ```shell openshell settings set \ --key agent_policy_proposals_enabled \ --value true ``` Check the effective setting for a sandbox: ```shell openshell settings get ``` The output shows whether `agent_policy_proposals_enabled` is `global`, `sandbox`, or `unset`. A global value overrides sandbox-scoped values. To return control to sandbox-scoped settings, delete the global key: ```shell openshell settings delete --global \ --key agent_policy_proposals_enabled \ --yes ``` Set the value before creating a sandbox when you want the first denied request to include policy advisor guidance. Running sandboxes poll settings and can enable the surface after startup, but startup enablement gives the agent the clearest first-denial path. ## Approval Modes Every proposal — mechanistic or agent-authored — is routed through the [policy prover](#what-auto-approval-checks). The `proposal_approval_mode` setting decides what happens when the prover finds nothing to flag. | Mode | When unset / `manual` | `auto` | | ------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------- | | Empty prover delta | Lands in the draft inbox for human review. | Approved automatically; the sandbox hot-reloads the new rule and the agent retries. | | Any prover finding | Lands in the draft inbox. | Lands in the draft inbox — auto-approval is gated on an empty delta. | `manual` is the default. Auto mode is an explicit opt-in; OpenShell's default-deny posture is preserved unless you choose otherwise. Enable auto mode at gateway scope when you want every sandbox on this gateway to auto-approve safe proposals: ```shell openshell settings set --global \ --key proposal_approval_mode \ --value auto \ --yes ``` Enable it for one sandbox when no global value is set: ```shell openshell settings set \ --key proposal_approval_mode \ --value auto ``` The shorthand at create time writes the sandbox-scoped setting for you: ```shell openshell sandbox create --approval-mode auto ``` Only `manual` and `auto` are accepted; typos like `autom` are rejected at configure time. Stale or unknown values found in storage are still treated as `manual` at runtime as a defense-in-depth measure. **Precedence.** Gateway scope wins over sandbox scope. A reviewer can pin `manual` for a fleet by setting it globally; per-sandbox overrides only apply when no global value is set. **Audit trail.** Every auto-approval emits a `CONFIG:APPROVED` event with `auto=true`, `source=`, `prover_delta=empty`, and `resolved_from=` so operators can reconstruct why a given approval ran without human review. ## How It Works When policy advisor is enabled, the sandbox supervisor turns on three agent-facing surfaces: * It installs `/etc/openshell/skills/policy_advisor.md` inside the sandbox. * It also installs `/etc/openshell/skills/policy-advisor/SKILL.md` as a short Codex/generic-agent pointer, and writes a root `/AGENTS.md` pointer only when the image does not already provide one. * It serves `http://policy.local` from inside the sandbox. * It adds `agent_guidance` and `next_steps` to L7 `policy_denied` response bodies so the agent can find the skill and local API. The loop has seven steps: 1. A sandboxed process attempts a network request that policy denies. 2. For inspected REST traffic, OpenShell returns a structured `403` body with fields such as `layer`, `host`, `port`, `binary`, `method`, `path`, `rule_missing`, `agent_guidance`, and `next_steps`. 3. The agent reads the policy advisor skill, inspects the current policy, and optionally reads recent denial log lines. 4. The agent submits one or more `addRule` proposals to `http://policy.local/v1/proposals`. 5. The gateway stores accepted proposals as pending draft chunks for the sandbox and runs the [policy prover](#what-auto-approval-checks) against the proposed delta. 6. Under `auto` mode, proposals with an empty prover delta are approved immediately and skipped past human review. Under `manual` mode (the default), every proposal — and under `auto` mode, every proposal with a prover finding — lands in the draft inbox for a developer to approve or reject. 7. The agent waits on `/v1/proposals/{chunk_id}/wait` until a decision is available. Approved proposals hot-reload into the sandbox; rejected proposals return `rejection_reason` and `validation_result` so the agent can revise. When a proposal is approved, `/wait` reports `policy_reloaded: true` only after the local sandbox policy covers the approved rule. At that point the agent can retry the original denied action once. If a proposal is rejected, `/wait` returns `rejection_reason` and `validation_result` so the agent can revise or stop. `validation_result` carries the categorical prover findings — `link_local_reach`, `l7_bypass_credentialed`, `credential_reach_expansion`, `capability_expansion` — so the agent can narrow the next attempt to the specific concern the prover flagged. ## What Gets Proposed OpenShell has two proposal paths: | Path | Source | Typical rule shape | | ----------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Mechanistic mapper | Aggregated denial summaries from the sandbox. | Groups by host, port, and binary. If L7 request samples are available, it can draft REST method and path rules. Otherwise it drafts an L4 endpoint. | | Agent-authored proposal | The in-sandbox agent, using `policy.local`. | Usually a REST `addRule` with exact host, port, binary, method, and path from the structured denial. It can also propose L4 rules for opaque protocols. | For REST APIs, prefer L7 rules over broad L4 access. A good proposal allows one method and the smallest safe path: ```json { "intent_summary": "Allow gh to update documentation in NVIDIA/OpenShell.", "operations": [ { "addRule": { "ruleName": "github_contents_docs_write", "rule": { "name": "github_contents_docs_write", "endpoints": [ { "host": "api.github.com", "port": 443, "protocol": "rest", "enforcement": "enforce", "rules": [ { "allow": { "method": "PUT", "path": "/repos/NVIDIA/OpenShell/contents/docs/**" } } ] } ], "binaries": [ { "path": "/usr/bin/gh" } ] } } } ] } ``` The current `policy.local` JSON shape covers L4 endpoints and REST method or path rules. Use [Customize Sandbox Policies](/sandboxes/policies) or [Policy Schema Reference](/reference/policy-schema) for policy fields that are not part of the agent-authored proposal surface, such as WebSocket credential rewrite, GraphQL operation matching, endpoint path scoping, and provider-owned policy bundles. Policy advisor proposals do not add `allowed_ips` automatically. If an advisor-proposed hostname resolves to an internal or private address, OpenShell's SSRF protections still block the connection until a developer explicitly adds the required `allowed_ips` entry. Exact hostname trust for user-declared policy endpoints does not apply to advisor-generated proposal binaries. ## What Auto-Approval Checks The policy prover runs against every proposal — mechanistic and agent-authored alike — and asks four formal questions about the proposed change. Each "yes" is one categorical finding. Any finding blocks auto-approval; only an empty delta is eligible. | Category | Triggered when | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `link_local_reach` | The proposal reaches a host in `169.254.0.0/16`, `fe80::/10`, or a known metadata hostname such as `metadata.google.internal` (cloud-metadata territory, which serves credentials regardless of sandbox state). Unconditional. | | `l7_bypass_credentialed` | A binary using a wire protocol the L7 proxy cannot inspect (`git-remote-https`, `ssh`, `nc`) gains reach to a host where a credential is in scope. | | `credential_reach_expansion` | A binary gains credentialed reach to a `(host, port)` it could not reach before. | | `capability_expansion` | On a `(binary, host, port)` that already had credentialed reach, the proposal adds a new HTTP method. The finding cites the specific method. | Findings are categorical — there is no severity tier. The reviewer reads the category and the structured evidence to decide. When the prover delta is empty, the proposal is provably safe under the model and auto-approval (if enabled) can fire. The full reasoning model lives in [`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). Provider profiles composed in via [Providers v2](/sandboxes/providers-v2) are part of the effective policy the prover reasons over. ## Review Proposals Review pending chunks from the host: ```shell openshell rule get --status pending ``` Under `auto` mode, only proposals the prover flagged appear here; empty-delta proposals are already approved and visible under `--status approved` with the auto-approval audit fields described in [Approval Modes](#approval-modes). Under `manual` mode, every proposal — regardless of prover verdict — shows up as pending. The output shows the chunk ID, status, rationale, binary, and endpoint summary. For L7 proposals, the endpoint summary includes the protocol, method, and path: ```text Endpoints: api.github.com:443 [L7 rest, allow PUT /repos/NVIDIA/OpenShell/contents/docs/**] ``` Approve only when the structured rule matches the access you intend to grant: ```shell openshell rule approve --chunk-id ``` Reject with guidance when the rule is too broad or points at the wrong target: ```shell openshell rule reject \ --chunk-id \ --reason "Scope this to docs/ paths only." ``` The rejection reason is returned to the agent through `policy.local`. The agent can use it to draft a narrower proposal. ## Agent API `policy.local` is available only inside the sandbox and uses plain HTTP: | Endpoint | Purpose | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `GET /v1/policy/current` | Returns the current effective sandbox policy as YAML. | | `GET /v1/denials?last=10` | Returns recent denied OCSF shorthand log lines, newest first. Query strings are redacted before lines are returned to the agent. | | `POST /v1/proposals` | Submits `addRule` operations. The response includes `accepted_chunk_ids` and `rejection_reasons`. | | `GET /v1/proposals/{chunk_id}` | Returns one proposal's current `pending`, `approved`, or `rejected` status. | | `GET /v1/proposals/{chunk_id}/wait?timeout=300` | Holds one HTTP request open until the proposal is approved, rejected, or the timeout expires. | If policy advisor is disabled, every route returns `404 feature_disabled`, the skill is not installed for new sandboxes, and L7 deny bodies do not advertise `policy.local` routes or include `agent_guidance`. ## What to Expect Approved network rules hot-reload without restarting the sandbox. HTTP L7 keep-alive connections are closed at the reload boundary so the next parsed request uses the new policy. Raw streams remain connection-scoped, as described in [Customize Sandbox Policies](/sandboxes/policies#policy-structure). Policy advisor emits audit events into the sandbox log. Use these lines to trace the full loop: ```shell openshell logs --since 10m ``` Look for `HTTP:* DENIED`, `CONFIG:PROPOSED`, `CONFIG:APPROVED` or `CONFIG:REJECTED`, `CONFIG:LOADED`, and the final allowed request if the agent retries successfully. Auto-approved chunks emit `CONFIG:APPROVED` with `auto=true`, `source=`, `prover_delta=empty`, and `resolved_from=`. ## Next Steps * Use [Customize Sandbox Policies](/sandboxes/policies) for manual policy updates and L7 rule syntax. * Use [Policy Schema Reference](/reference/policy-schema) for full YAML field details. * Use [Logging](/observability/logging) to interpret OCSF shorthand log entries. # Inference Routing > Understand and configure OpenShell inference routing through inference.local and external endpoints. OpenShell handles inference traffic through two paths: external endpoints and `inference.local`. | Path | How it works | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | External endpoints | Traffic to hosts like `api.openai.com` or `api.anthropic.com` is treated like any other outbound request, allowed or denied by `network_policies`. Refer to [Policies](/sandboxes/policies). | | `inference.local` | A sandbox-local HTTPS endpoint that routes model requests through the gateway. The privacy router strips sandbox-supplied credentials, forwards only approved inference headers, injects the configured backend credentials, and forwards to the managed model endpoint. | ## How `inference.local` Works When code inside a sandbox calls `https://inference.local`, the privacy router routes the request to the configured backend for that gateway. The configured model is applied to generation requests, provider credentials come from OpenShell rather than from code inside the sandbox, and only approved inference headers are forwarded upstream. If code calls an external inference host directly, OpenShell evaluates that traffic only through `network_policies`. | Property | Detail | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Credentials | No sandbox API keys needed. Credentials come from the configured provider record. The router strips caller-supplied `Authorization` before forwarding the request. | | Header forwarding | `inference.local` forwards only a per-provider header allowlist. OpenAI routes allow `openai-organization` and `x-model-id`. Anthropic routes allow `anthropic-version` and `anthropic-beta`. Vertex Claude rawPredict routes strip `anthropic-beta` and do not forward `anthropic-version` as a header because the router injects `anthropic_version` into the Vertex request body. NVIDIA routes allow `x-model-id`. All other caller headers are stripped. | | Configuration | One provider and one model define sandbox inference for the active gateway. Every sandbox on that gateway sees the same `inference.local` backend. | | Provider support | NVIDIA, Anthropic, Google Vertex AI, and any OpenAI-compatible provider all work through the same endpoint. Vertex routes Claude models through `/v1/messages` and non-Anthropic models through `/v1/chat/completions`. The gateway resolves the upstream Vertex host from the provider config, including regional, global, and supported multi-region endpoints. | | Streaming reliability | The router tolerates idle gaps of up to 120 seconds between streamed chunks so long reasoning responses are not cut off mid-stream. | | Hot refresh | OpenShell picks up provider credential changes and inference updates without recreating sandboxes. Changes propagate within about 5 seconds by default. | ## Supported API Patterns Supported request patterns depend on the provider configured for `inference.local`. | Pattern | Method | Path | | ---------------- | ------ | ---------------------- | | Chat Completions | `POST` | `/v1/chat/completions` | | Completions | `POST` | `/v1/completions` | | Responses | `POST` | `/v1/responses` | | Model Discovery | `GET` | `/v1/models` | | Model Discovery | `GET` | `/v1/models/*` | | Pattern | Method | Path | | -------- | ------ | -------------- | | Messages | `POST` | `/v1/messages` | Requests to `inference.local` that do not match the configured provider's supported patterns are denied. Google Vertex AI does not expose every OpenAI-compatible path through `inference.local`. Vertex routes for Gemini and other non-Anthropic models currently support Chat Completions. Vertex routes for Claude models use the Anthropic Messages pattern. Base URL overrides are only supported for non-Anthropic Vertex routes. ## Configure Inference Routing The managed local inference endpoint uses three values: | Value | Description | | --------------- | ------------------------------------------------------------------------------------ | | Provider record | The credential backend OpenShell uses to authenticate with the upstream model host. | | Model ID | The model to use for generation requests. | | Timeout | Per-request timeout in seconds for upstream inference calls. Defaults to 60 seconds. | For tested providers and base URLs, refer to [Supported Inference Providers](/sandboxes/manage-providers#supported-inference-providers). ## Create a Provider Create a provider that holds the backend credentials you want OpenShell to use. ```shell openshell provider create --name nvidia-prod --type nvidia --from-existing ``` This reads `NVIDIA_API_KEY` from your environment. Any cloud provider that exposes an OpenAI-compatible API works with the `openai` provider type. You need three values from the provider: the base URL, an API key, and a model name. ```shell openshell provider create \ --name my-cloud-provider \ --type openai \ --credential OPENAI_API_KEY= \ --config OPENAI_BASE_URL=https://api.example.com/v1 ``` Replace the base URL and API key with the values from your provider. For supported providers out of the box, refer to [Supported Inference Providers](/sandboxes/manage-providers#supported-inference-providers). For other providers, refer to your provider's documentation for the correct base URL, available models, and API key setup. ```shell openshell provider create \ --name vertex-local \ --type google-vertex-ai \ --from-gcloud-adc \ --config VERTEX_AI_PROJECT_ID=my-gcp-project \ --config VERTEX_AI_REGION=us-central1 ``` Use [Google Vertex AI](/providers/google-vertex-ai) for the full auth flows, including the production service-account refresh path, ADC-backed providers that mint `GOOGLE_VERTEX_AI_TOKEN`, and `--from-existing` support. ```shell openshell provider create \ --name my-local-model \ --type openai \ --credential OPENAI_API_KEY=empty-if-not-required \ --config OPENAI_BASE_URL=http://host.openshell.internal:11434/v1 ``` Use `--config OPENAI_BASE_URL` to point to any OpenAI-compatible server running where the gateway runs. For host-backed local inference, use `host.openshell.internal` or the host's LAN IP. Avoid `127.0.0.1` and `localhost`. Set `OPENAI_API_KEY` to a dummy value if the server does not require authentication. For a self-contained setup, the Ollama sandbox bundles Ollama inside the sandbox itself, so no host-level provider is needed. Refer to [Inference Ollama](/get-started/tutorials/inference-ollama) for details. Ollama also supports cloud-hosted models using the `:cloud` tag suffix, for example `qwen3.5:cloud`. ```shell openshell provider create --name anthropic-prod --type anthropic --from-existing ``` This reads `ANTHROPIC_API_KEY` from your environment. ## Set Inference Routing Point `inference.local` at that provider and choose the model to use: ```shell openshell inference set \ --provider nvidia-prod \ --model nvidia/nemotron-3-nano-30b-a3b ``` To override the default 60-second per-request timeout, add `--timeout`: ```shell openshell inference set \ --provider nvidia-prod \ --model nvidia/nemotron-3-nano-30b-a3b \ --timeout 300 ``` The value is in seconds. When `--timeout` is omitted or set to `0`, the default of 60 seconds applies. Increase `--timeout` when you expect extended thinking phases so the full response completes before the request deadline. ## Inspect and Update the Config Confirm that the provider and model are set correctly: ```shell openshell inference get Gateway inference: Provider: nvidia-prod Model: nvidia/nemotron-3-nano-30b-a3b Timeout: 300s Version: 1 ``` Use `update` when you want to change only one field: ```shell openshell inference update --model nvidia/nemotron-3-nano-30b-a3b openshell inference update --provider openai-prod openshell inference update --timeout 120 ``` ## Use the Local Endpoint from a Sandbox After inference is configured, code inside any sandbox can call `https://inference.local` directly. The client-supplied `model` and `api_key` values are not sent upstream — the privacy router injects the real credentials from the configured provider and rewrites the model before forwarding. Some SDKs require a non-empty API key even though `inference.local` does not use the sandbox-provided value; pass any placeholder such as `unused`. ```shell ANTHROPIC_BASE_URL="https://inference.local" ANTHROPIC_API_KEY=unused claude --bare ``` `--bare` skips the OAuth login flow and uses `ANTHROPIC_API_KEY` directly. The key is stripped by the proxy and never reaches the upstream provider. Claude Code appends `/v1/messages` to `ANTHROPIC_BASE_URL`, so omit the `/v1` suffix from the base URL. ```shell ANTHROPIC_BASE_URL="https://inference.local/v1" ANTHROPIC_API_KEY=unused opencode ``` OpenCode appends `/messages` directly to `ANTHROPIC_BASE_URL`. Include the `/v1` suffix so the full path becomes `/v1/messages`, which matches the inference pattern. ```python from openai import OpenAI client = OpenAI(base_url="https://inference.local/v1", api_key="unused") response = client.chat.completions.create( model="anything", messages=[{"role": "user", "content": "Hello"}], ) ``` ```python import anthropic client = anthropic.Anthropic( base_url="https://inference.local", api_key="unused", ) message = client.messages.create( model="anything", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], ) ``` Use `inference.local` when inference should stay private and credentials should not be exposed inside the sandbox. External providers reached directly belong in `network_policies` instead. When the upstream runs on the same machine as the gateway, bind it to `0.0.0.0` and point the provider at `host.openshell.internal` or the host's LAN IP. `127.0.0.1` and `localhost` usually fail because the request originates from the gateway or sandbox runtime, not from your shell. If the gateway runs on a remote host or behind a cloud deployment, `host.openshell.internal` points to that remote machine, not to your laptop. A locally running Ollama or vLLM process is not reachable from a remote gateway unless you add your own tunnel or shared network path. ## Verify from a Sandbox `openshell inference set` and `openshell inference update` verify the resolved upstream endpoint by default before saving the configuration. If the endpoint is not live yet, retry with `--no-verify` to persist the route without the probe. To confirm end-to-end connectivity from a sandbox, run: ```shell curl https://inference.local/v1/responses \ -H "Content-Type: application/json" \ -d '{ "instructions": "You are a helpful assistant.", "input": "Hello!" }' ``` A successful response confirms the privacy router can reach the configured backend and the model is serving requests. * Gateway-scoped: Every sandbox using the active gateway sees the same `inference.local` backend. * HTTPS only: `inference.local` is intercepted only for HTTPS traffic. * Hot reload: Provider, model, and timeout changes are picked up by running sandboxes within about 5 seconds by default. No sandbox recreation is required. ## Next Steps Explore related topics: * To follow a complete Ollama-based local setup, refer to [Inference Ollama](/get-started/tutorials/inference-ollama). * To follow a complete LM Studio-based local setup, refer to [Local Inference LM Studio](/get-started/tutorials/local-inference-lmstudio). * To control external endpoints, refer to [Policies](/sandboxes/policies). * To manage provider records, refer to [Providers](/sandboxes/manage-providers). # Google Vertex AI > Configure OpenShell to route inference traffic through Google Vertex AI, including Anthropic Claude and Gemini models. Google Vertex AI is a managed machine learning platform that hosts Anthropic Claude, Gemini, and third-party models through Google Cloud. OpenShell can route `inference.local` traffic to Vertex AI using gateway-managed credential refresh, so sandbox agents do not handle GCP credentials directly. ## Prerequisites Before creating a Vertex AI provider, ensure you have: * A GCP project with the [Vertex AI API](https://console.cloud.google.com/apis/library/aiplatform.googleapis.com) enabled. * One of the following: * A GCP service account with the **Vertex AI User** role and a downloaded JSON key file, for production use. * The `gcloud` CLI with Application Default Credentials configured, for local development. ## Authentication The `google-vertex-ai` provider supports two credential sources. ### Service Account Key Supply the JSON key file content as the `GOOGLE_SERVICE_ACCOUNT_KEY` credential. OpenShell persists that value only as gateway-side refresh bootstrap material until you update or delete it. The raw service-account JSON and private key are not sandbox runtime credentials and are not exposed to sandboxes. Runtime inference requests use short-lived access tokens minted by the gateway and stored under a separate credential key. ```shell openshell provider create \ --name vertex-prod \ --type google-vertex-ai \ --credential GOOGLE_SERVICE_ACCOUNT_KEY="$(cat /path/to/key.json)" \ --config VERTEX_AI_PROJECT_ID=my-gcp-project \ --config VERTEX_AI_REGION=us-central1 ``` Then configure gateway-managed refresh so the gateway uses the private key as refresh bootstrap material and rotates access tokens: ```shell openshell provider refresh configure vertex-prod \ --credential-key GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN \ --strategy google-service-account-jwt \ --material client_email="sa@my-gcp-project.iam.gserviceaccount.com" \ --material private_key="$(jq -r .private_key /path/to/key.json)" \ --secret-material-key private_key ``` ### gcloud Application Default Credentials For local development, configure ADC first, then pass `--from-gcloud-adc`: ```shell gcloud auth application-default login ``` ```shell openshell provider create \ --name vertex-local \ --type google-vertex-ai \ --from-gcloud-adc \ --config VERTEX_AI_PROJECT_ID=my-gcp-project \ --config VERTEX_AI_REGION=us-central1 ``` `--from-gcloud-adc` reads `GOOGLE_APPLICATION_CREDENTIALS` first, then falls back to `$CLOUDSDK_CONFIG/application_default_credentials.json` when that environment variable is set, then to `~/.config/gcloud/application_default_credentials.json`. It configures an OAuth2 refresh token flow on the gateway and immediately mints the first access token before the command returns. If the command succeeds, the provider is ready for inference right away. It only works with user credentials generated by `gcloud auth application-default login`. If your ADC file is a service account key, the CLI returns an error and directs you to use the service account key method above. ADC-backed providers mint and rotate access tokens into `GOOGLE_VERTEX_AI_TOKEN`. `--from-gcloud-adc` is only valid for `google-vertex-ai` providers. ## Configuration Keys Pass these as `--config KEY=VALUE` when creating the provider, or set them as environment variables and use `--from-existing`. | Key | Required | Default | Description | | --------------------------- | ----------------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `VERTEX_AI_PROJECT_ID` | Yes (unless `GOOGLE_VERTEX_AI_BASE_URL` or `VERTEX_AI_BASE_URL` is set) | — | GCP project ID. | | `VERTEX_AI_REGION` | No | `us-central1` | Vertex location selector. Use a regional location such as `us-central1`, or `global`, `us`, or `eu` for the supported global and multi-region endpoints. | | `GOOGLE_VERTEX_AI_BASE_URL` | No | — | Full base URL override for non-Anthropic routes. Must be an official Vertex AI HTTPS endpoint root. | | `VERTEX_AI_BASE_URL` | No | — | Backward-compatible alias for `GOOGLE_VERTEX_AI_BASE_URL`. | | `VERTEX_AI_PUBLISHER` | No | Inferred from model name | Set to `anthropic` to force Anthropic Messages API routing, or any other value for OpenAI-compatible routing. | When `VERTEX_AI_PROJECT_ID` is set and no base URL override is present, the gateway maps `VERTEX_AI_REGION` to the Vertex host automatically: * Regional locations such as `us-central1` use `https://-aiplatform.googleapis.com`. * `global` uses `https://aiplatform.googleapis.com`. * `us` and `eu` use `https://aiplatform..rep.googleapis.com`. For Anthropic models, OpenShell builds the publisher-model Vertex path automatically and injects `anthropic_version` into the request body. Vertex rawPredict does not receive `anthropic-version` as a header, and OpenShell strips `anthropic-beta` for Vertex Claude routes. For non-Anthropic models, OpenShell uses Vertex's OpenAI-compatible Chat Completions route under `.../endpoints/openapi/chat/completions`. Use `GOOGLE_VERTEX_AI_BASE_URL` or `VERTEX_AI_BASE_URL` only for non-Anthropic Vertex routes. OpenShell rejects Anthropic models when a base URL override is set because Anthropic routes require model-path shaping and `anthropic_version` body injection. Overrides must use `https://` and an official Vertex AI hostname such as `aiplatform.googleapis.com`, `aiplatform.us.rep.googleapis.com`, `aiplatform.eu.rep.googleapis.com`, or `-aiplatform.googleapis.com`. ## Supported Models Vertex AI hosts Anthropic Claude models (claude-3-5-sonnet, claude-3-opus, and others) through a native Messages API integration, and Gemini and other third-party models through Vertex's OpenAI-compatible Chat Completions endpoint. OpenShell infers the routing path from the model name. For the full list of available models and regions, refer to the [Google Cloud model garden documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/overview). Model names that match the `claude-*` pattern route through the Anthropic Messages API on Vertex. All other model names route through Vertex Chat Completions. Set `VERTEX_AI_PUBLISHER=anthropic` to force Anthropic routing when the model name does not follow the standard pattern. OpenShell exposes Anthropic Vertex routes for inference only. It does not advertise OpenAI-style model discovery for those routes, so use the Google Cloud docs or Model Garden to discover supported Anthropic model IDs. ## Configure Inference Routing Before configuring inference routing, enable provider endpoint injection so the Vertex AI network endpoints are automatically included in sandbox policies: ```shell openshell settings set --global --key providers_v2_enabled --value true --yes ``` Then point `inference.local` at the provider: ```shell openshell inference set \ --provider vertex-prod \ --model claude-sonnet-4-6 ``` Use `--no-verify` if the endpoint verification fails. This is common with the `global` region, where the validation probe may not match the actual rawPredict path: ```shell openshell inference set \ --provider vertex-prod \ --model claude-sonnet-4-6 \ --no-verify ``` Sandboxes on that gateway reach the model at `https://inference.local`. For full details on inference routing, refer to [Inference Routing](/sandboxes/inference-routing). ## Use from a Sandbox Agents inside sandboxes should reach Vertex AI through `inference.local`, not by connecting to Vertex AI directly. The gateway manages GCP credential refresh and request translation; the agent only needs to point its SDK at the local endpoint. The complete setup from scratch: ```shell # 1. Enable provider endpoint injection openshell settings set --global --key providers_v2_enabled --value true --yes # 2. Create the provider openshell provider create \ --name vertex-local \ --type google-vertex-ai \ --from-gcloud-adc \ --config VERTEX_AI_PROJECT_ID=my-gcp-project \ --config VERTEX_AI_REGION=us-central1 # 3. Configure inference routing openshell inference set --provider vertex-local --model claude-sonnet-4-6 --no-verify # 4. Create a sandbox with the provider attached openshell sandbox create --name my-sandbox --provider vertex-local ``` Then inside the sandbox, launch the agent as shown below. ```shell ANTHROPIC_BASE_URL="https://inference.local" ANTHROPIC_API_KEY=unused claude --bare ``` `--bare` skips the OAuth login flow and uses `ANTHROPIC_API_KEY` directly for authentication. The key value does not reach Vertex AI — `inference.local` strips it and injects the real GCP access token before forwarding. Do not set `CLAUDE_CODE_USE_VERTEX=1` inside the sandbox. That flag makes Claude Code connect directly to Vertex AI and attempt GCP credential discovery (ADC file, metadata service), which fails because the sandbox does not expose GCP credentials. Use `inference.local` instead. ```shell ANTHROPIC_BASE_URL="https://inference.local/v1" ANTHROPIC_API_KEY=unused opencode ``` OpenCode requires `/v1` in the base URL. Without it, OpenCode sends `POST /messages` instead of `POST /v1/messages`, which does not match the inference pattern and is denied. ### Policy Proposals After running an agent, the TUI (`openshell term`) may show policy proposals for denied endpoints. Common ones for Vertex AI sandboxes: | Endpoint | Action | Reason | | ----------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `metadata.google.internal:80` | **Reject** | Resolves to `169.254.169.254` (GCE metadata service). Always blocked regardless of policy — the proxy blocks the resolved IP unconditionally to prevent credential exfiltration. | | `downloads.claude.ai:443` | Approve if desired | Claude Code update checking and asset loading. Not required for inference. | | `storage.googleapis.com:443` | Approve if desired | Google Cloud Storage. Used by some Claude Code features. Not required for inference. | ## From Existing Environment If one of these token env vars is already set in your shell, create the provider with `--from-existing`: * `GOOGLE_VERTEX_AI_TOKEN` or `VERTEX_AI_TOKEN` * `GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN` or `VERTEX_AI_SERVICE_ACCOUNT_TOKEN` OpenShell also reads these config env vars during `--from-existing`: * `VERTEX_AI_PROJECT_ID` * `VERTEX_AI_REGION` * `GOOGLE_VERTEX_AI_BASE_URL` or `VERTEX_AI_BASE_URL` * `VERTEX_AI_PUBLISHER` Then create the provider: ```shell openshell provider create \ --name vertex-env \ --type google-vertex-ai \ --from-existing ``` This reads credentials and config from the environment variables listed in the configuration keys table above. ## Next Steps * To configure `inference.local` routing, refer to [Inference Routing](/sandboxes/inference-routing). * To manage provider credentials and refresh, refer to [Providers](/sandboxes/manage-providers). * To apply network policies to sandboxes using this provider, refer to [Policies](/sandboxes/policies). # Accessing Logs > How to view sandbox logs through the CLI, TUI, and directly on the sandbox filesystem. OpenShell provides three ways to access sandbox logs: the CLI, the TUI, and direct filesystem access inside the sandbox. ## CLI Use `openshell logs` to stream logs from a running sandbox: ```shell openshell logs smoke-l4 --source sandbox ``` The CLI receives logs from the gateway over gRPC. Each line includes a timestamp, source, level, and message: ```text [1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] [1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api] [1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox ``` OCSF structured events show `OCSF` as the level. Standard tracing events show `INFO`, `WARN`, or `ERROR`. Gateway-originated policy mutations also appear in this stream. When the gateway merges `openshell policy update` operations or approves or removes draft policy chunks, it emits `gateway` `OCSF` `CONFIG:*` lines for the affected sandbox so you can see the exact logical change that produced a new policy revision. ## TUI The TUI dashboard displays sandbox logs in real time. Logs appear in the log panel with the same format as the CLI. ## Gateway Log Storage The sandbox pushes logs to the gateway over gRPC in real time. The gateway stores a bounded buffer of recent log lines per sandbox. This buffer is not persisted to disk and is lost when the gateway restarts. For durable log storage, use the log files inside the sandbox or enable [OCSF JSON export](/observability/ocsf-json-export) and ship the JSONL files to an external log aggregator. ## Direct Filesystem Access Use `openshell sandbox connect` to open a shell inside the sandbox and read the log files directly: ```text openshell sandbox connect my-sandbox sandbox@my-sandbox:~$ cat /var/log/openshell.2026-04-01.log ``` You can also run a one-off command without an interactive shell: ```shell openshell sandbox connect my-sandbox -- cat /var/log/openshell.2026-04-01.log ``` The log files inside the sandbox contain the complete record, including events that the gRPC push channel can drop under load. The push channel is bounded and drops events rather than blocking. ## Filtering by Event Type The shorthand format is designed for `grep`. Some useful patterns: ```shell # All denied connections grep "DENIED\|BLOCKED" /var/log/openshell.*.log # All network events grep "OCSF NET:" /var/log/openshell.*.log # All L7 enforcement decisions grep "OCSF HTTP:" /var/log/openshell.*.log # Security findings only grep "OCSF FINDING:" /var/log/openshell.*.log # Policy changes grep "OCSF CONFIG:" /var/log/openshell.*.log # All OCSF events, excluding standard tracing grep "^.* OCSF " /var/log/openshell.*.log # Events at medium severity or above grep "\[MED\]\|\[HIGH\]\|\[CRIT\]\|\[FATAL\]" /var/log/openshell.*.log ``` ## Next Steps * Learn how the [log formats](/observability/logging) work and how to read the shorthand. * [Enable OCSF JSON export](/observability/ocsf-json-export) for machine-readable structured output. # Sandbox Logging > How OpenShell logs sandbox activity using standard tracing and OCSF structured events. Every OpenShell sandbox produces a log that records network connections, process lifecycle events, filesystem policy decisions, and configuration changes. The log uses two formats depending on the type of event. ## Log Formats ### Standard tracing Internal operational events use Rust's `tracing` framework with a conventional format: ```text 2026-04-01T03:28:39.160Z INFO openshell_sandbox: Fetching sandbox policy via gRPC 2026-04-01T03:28:39.175Z INFO openshell_sandbox: Creating OPA engine from proto policy data ``` These events cover startup plumbing, gRPC communication, and internal state transitions that are useful for debugging but do not represent security-relevant decisions. ### OCSF structured events Network, process, filesystem, and configuration events use the [Open Cybersecurity Schema Framework (OCSF)](https://ocsf.io) format. OCSF is an open standard for normalizing security telemetry across tools and platforms. OpenShell maps sandbox events to OCSF v1.7.0 event classes. In the log file, OCSF events appear in a shorthand format with an `OCSF` level label, designed for quick human and agent scanning: ```text 2026-04-01T04:04:13.058Z INFO openshell_sandbox: Starting sandbox 2026-04-01T04:04:13.065Z OCSF CONFIG:DISCOVERY [INFO] Server returned no policy; attempting local discovery 2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data 2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image 2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] 2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api engine:opa] 2026-04-01T04:04:32.690Z OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] ``` The `OCSF` label at column 25 distinguishes structured events from standard `INFO` tracing at the same position. Both formats appear in the same file. When viewed through the CLI or TUI, which receive logs through gRPC, the same distinction applies: ```text [1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] [1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] [1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox ``` ## OCSF Event Classes OpenShell maps sandbox events to these OCSF classes: | Shorthand prefix | OCSF class | Class UID | What it covers | | ---------------- | -------------------------- | --------- | ------------------------------------------------------------- | | `NET:` | Network Activity | 4001 | TCP proxy CONNECT tunnels, bypass detection, DNS failures | | `HTTP:` | HTTP Activity | 4002 | HTTP FORWARD requests, L7 enforcement decisions | | `SSH:` | SSH Activity | 4007 | SSH handshakes, authentication, channel operations | | `PROC:` | Process Activity | 1007 | Process start, exit, timeout, signal failures | | `FINDING:` | Detection Finding | 2004 | Security findings (nonce replay, proxy bypass, unsafe policy) | | `CONFIG:` | Device Config State Change | 5019 | Policy load/reload, Landlock, TLS setup, inference routes | | `LIFECYCLE:` | Application Lifecycle | 6002 | Sandbox supervisor start, SSH server ready | ## Reading the Shorthand Format The shorthand format follows this pattern: ```text CLASS:ACTIVITY [SEVERITY] ACTION DETAILS [CONTEXT] ``` ### Components **Class and activity** (`NET:OPEN`, `HTTP:GET`, `PROC:LAUNCH`) identify the OCSF event class and what happened. The class name always starts at the same column position for vertical scanning. **Severity** indicates the OCSF severity of the event: | Tag | Meaning | When used | | --------- | ------------- | -------------------------------------------------- | | `[INFO]` | Informational | Allowed connections, successful operations | | `[LOW]` | Low | DNS failures, operational warnings | | `[MED]` | Medium | Denied connections, policy violations | | `[HIGH]` | High | Security findings (nonce replay, bypass detection) | | `[CRIT]` | Critical | Process timeout kills | | `[FATAL]` | Fatal | Unrecoverable failures | **Action** (`ALLOWED`, `DENIED`, `BLOCKED`) is the security control disposition. Not all events have an action; informational config events, for example, do not. **Details** vary by event class: * Network: `process(pid) -> host:port` with the process identity and destination * HTTP: `METHOD url` with the HTTP method and target * SSH: peer address and authentication type * Process: `name(pid)` with exit code or command line * Config: description of what changed * Finding: quoted title with confidence level **Context** in brackets at the end provides the policy rule and enforcement engine that produced the decision. ### Examples An allowed HTTPS connection: ```text OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] ``` An L7 read-only policy denying a POST: ```text OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api engine:opa] ``` A connection denied because no policy matched: ```text OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] ``` A connection denied because the destination resolves to an always-blocked address: ```text OCSF NET:OPEN [MED] DENIED /usr/bin/curl(1618) -> 169.254.169.254:80 [policy:- engine:ssrf] [reason:resolves to always-blocked address] ``` An HTTP request to a non-default port. HTTP log URLs include the port whenever it differs from the scheme default (80 for `http`, 443 for `https`): ```text OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy:internal_api engine:opa] ``` Proxy and SSH servers ready: ```text OCSF NET:LISTEN [INFO] 10.200.0.1:3128 OCSF SSH:LISTEN [INFO] ``` An SSH connection accepted (one event per invocation, arriving over the supervisor's Unix socket, so there is no network peer address to log): ```text OCSF SSH:OPEN [INFO] ALLOWED ``` A process launched inside the sandbox: ```text OCSF PROC:LAUNCH [INFO] sleep(49) ``` A policy reload after a settings change: ```text OCSF CONFIG:DETECTED [INFO] Settings poll: config change detected [old_revision:2915564174587774909 new_revision:11008534403127604466 policy_changed:true] OCSF CONFIG:LOADED [INFO] Policy reloaded successfully [policy_hash:0cc0c2b525573c07] ``` ## Denial Reasons Denied `NET:` and `HTTP:` events carry a `[reason:...]` suffix that surfaces the decision detail from the event's `status_detail` field. The reason helps distinguish between policy misses, SSRF hardening, and L7 enforcement without inspecting the full OCSF JSONL record. Common reason phrases emitted by the sandbox include: | Reason | Meaning | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `no matching policy` | OPA evaluated the request and no allow rule matched. | | `resolves to always-blocked address` | The destination resolved to loopback, link-local, or unspecified. These ranges are always blocked, even when listed in `allowed_ips`. | | `resolves to which is not in allowed_ips, connection rejected` | The destination resolved to an IP outside the policy's `allowed_ips` allowlist. | | `DNS resolution failed for :` | The proxy could not resolve the destination. | | `port is a blocked control-plane port, connection rejected` | The destination port matches a control-plane port (etcd, Kubernetes API, kubelet) and is always blocked. | | `request-target contains an encoded '/' (%2F)` | The L7 HTTP parser rejected an encoded slash. Configure `allow_encoded_slash: true` on a REST endpoint when the upstream requires encoded slashes. | | `l7 deny` | An L7 policy rule denied the request. | Invalid `allowed_ips` entries and entries that overlap always-blocked ranges are rejected at policy-load time, so they never reach the runtime denial path. The phrases above come from the proxy's per-CONNECT `allowed_ips` and SSRF checks, not from policy validation. ## Proxy Error Responses When the HTTP CONNECT proxy denies a request or cannot reach the upstream, it returns an HTTP error response with a JSON body. Clients can parse the body to surface actionable failure details instead of treating the status code alone. A denied CONNECT returns `403 Forbidden`: ```json { "error": "policy_denied", "detail": "CONNECT api.example.com:443 not permitted by policy" } ``` An upstream that the proxy cannot reach returns `502 Bad Gateway`: ```json { "error": "upstream_unreachable", "detail": "connection to api.example.com:443 failed" } ``` The `error` field is a short machine-readable code (`policy_denied`, `ssrf_denied`, `upstream_unreachable`). The `detail` field is a human-readable explanation suitable for display in an agent transcript. For L7 REST denials, the body also includes structured policy fields such as `method`, `path`, `rule_missing`, and `next_steps`. When policy advisor is enabled, it also includes `agent_guidance`, a short plain-language instruction telling the agent to read `/etc/openshell/skills/policy_advisor.md`, propose the narrowest rule through `http://policy.local/v1/proposals`, wait for `policy_reloaded: true`, and retry. ## Filesystem Sandbox Logs Landlock filesystem restrictions emit `CONFIG:` events at startup and whenever the sandbox has to skip a requested path. On startup, the probe reports the kernel's supported Landlock ABI version alongside the requested path counts: ```text OCSF CONFIG:ENABLED [INFO] Landlock filesystem sandbox available [abi:v2 compat:BestEffort ro:4 rw:2] OCSF CONFIG:ENABLED [INFO] Applying Landlock filesystem sandbox [abi:V2 compat:BestEffort ro:4 rw:2] OCSF CONFIG:ENABLED [INFO] Landlock ruleset built [rules_applied:5 skipped:1] ``` When `landlock.compatibility` is `best_effort` and a requested path fails to open for reasons other than `NotFound` (for example, permission denied or a symlink loop), the sandbox continues without that path and emits a `[MED]` event so the degradation is not silent: ```text OCSF CONFIG:OTHER [MED] Skipping inaccessible Landlock path (best-effort) [path:/opt/data error:Permission denied (os error 13)] ``` Set `landlock.compatibility` to `hard_requirement` in the policy to make these failures fatal instead of degraded. ## Log File Location Inside the sandbox, logs are written to `/var/log/`: | File | Format | Rotation | | ------------------------------- | ---------------------------- | ------------------ | | `openshell.YYYY-MM-DD.log` | Shorthand + standard tracing | Daily, 3 files max | | `openshell-ocsf.YYYY-MM-DD.log` | OCSF JSONL when enabled | Daily, 3 files max | Both files rotate daily and retain the 3 most recent files to bound disk usage. ## Next Steps * [Access logs](/observability/accessing-logs) through the CLI, TUI, or sandbox filesystem. * [Enable OCSF JSON export](/observability/ocsf-json-export) for SIEM integration and compliance. * Learn about [network policies](/sandboxes/policies) that generate these events. # OCSF JSON Export > How to enable full OCSF JSON logging for SIEM integration, compliance, and structured analysis. The [shorthand log format](/observability/logging) is optimized for humans and agents reading logs in real time. For machine consumption, compliance archival, or SIEM integration, you can enable full OCSF JSON export. This writes every OCSF event as a complete JSON record in JSONL format, one JSON object per line. ## Enable JSON Export Use the `ocsf_json_enabled` setting to toggle JSON export. The setting can be applied globally, for all sandboxes, or per-sandbox. Global: ```shell openshell settings set --global --key ocsf_json_enabled --value true ``` Per-sandbox: ```shell openshell settings set my-sandbox --key ocsf_json_enabled --value true ``` The setting takes effect on the next poll cycle, by default every 10 seconds. No sandbox restart is required. To disable: ```shell openshell settings set --global --key ocsf_json_enabled --value false ``` ## Output Location When enabled, OCSF JSON records are written to `/var/log/openshell-ocsf.YYYY-MM-DD.log` inside the sandbox. The file rotates daily and retains the 3 most recent files, matching the main log file rotation. ## JSON Record Structure Each line is a complete OCSF v1.7.0 JSON object. Here is an example of a network connection event: ```json { "class_uid": 4001, "class_name": "Network Activity", "category_uid": 4, "category_name": "Network Activity", "activity_id": 1, "activity_name": "Open", "severity_id": 1, "severity": "Informational", "status_id": 1, "status": "Success", "time": 1775014138811, "message": "CONNECT allowed api.github.com:443", "metadata": { "product": { "name": "OpenShell Sandbox Supervisor", "vendor_name": "NVIDIA", "version": "0.3.0" }, "version": "1.7.0" }, "action_id": 1, "action": "Allowed", "disposition_id": 1, "disposition": "Allowed", "dst_endpoint": { "domain": "api.github.com", "port": 443 }, "src_endpoint": { "ip": "10.42.0.31", "port": 37494 }, "actor": { "process": { "name": "/usr/bin/curl", "pid": 57 } }, "firewall_rule": { "name": "github_api", "type": "opa" } } ``` And a denied connection: ```json { "class_uid": 4001, "class_name": "Network Activity", "activity_id": 1, "activity_name": "Open", "severity_id": 3, "severity": "Medium", "status_id": 2, "status": "Failure", "action_id": 2, "action": "Denied", "disposition_id": 2, "disposition": "Blocked", "status_detail": "no matching policy", "message": "CONNECT denied httpbin.org:443", "dst_endpoint": { "domain": "httpbin.org", "port": 443 }, "actor": { "process": { "name": "/usr/bin/curl", "pid": 63 } }, "firewall_rule": { "name": "-", "type": "opa" } } ``` The JSON examples above are formatted for readability. The actual JSONL file contains one JSON object per line with no whitespace formatting. ## OCSF Event Classes in JSON The `class_uid` field identifies the event type: | `class_uid` | Class | Shorthand prefix | | ----------- | -------------------------- | ---------------- | | 4001 | Network Activity | `NET:` | | 4002 | HTTP Activity | `HTTP:` | | 4007 | SSH Activity | `SSH:` | | 1007 | Process Activity | `PROC:` | | 2004 | Detection Finding | `FINDING:` | | 5019 | Device Config State Change | `CONFIG:` | | 6002 | Application Lifecycle | `LIFECYCLE:` | ## Integration with External Tools The JSONL file can be shipped to any tool that accepts OCSF-formatted data: | Tool | Integration Path | | -------------------- | ------------------------------------------------------------------------------------------------ | | Splunk | Use the [Splunk OCSF Add-on](https://splunkbase.splunk.com/app/6943) to ingest OCSF JSONL files. | | Amazon Security Lake | OCSF is the native schema for Security Lake. | | Elastic | Use Filebeat to ship JSONL files with the OCSF field mappings. | | Custom pipelines | Parse the JSONL file with `jq`, Python, or any JSON-capable tool. | Example with `jq` to extract all denied connections: ```shell cat /var/log/openshell-ocsf.2026-04-01.log | \ jq -c 'select(.action == "Denied")' ``` ## Relationship to Shorthand Logs The shorthand format in `openshell.YYYY-MM-DD.log` and the JSON format in `openshell-ocsf.YYYY-MM-DD.log` are derived from the same OCSF events. The shorthand is a human-readable projection; the JSON is the complete record. Both are generated at the same time from the same event data. The shorthand log is always active. The JSON export is opt-in through `ocsf_json_enabled`. ## Next Steps * Learn how to [read the shorthand format](/observability/logging) for real-time monitoring. * Refer to the [OCSF specification](https://schema.ocsf.io/) for the full schema reference. # Set Up OpenShell on Kubernetes > Deploy the OpenShell gateway to a Kubernetes cluster using the official Helm chart from GHCR. The OpenShell Helm chart is experimental and under active development. Templates, values, and defaults can change between releases. Do not use it in production. Use the Kubernetes deployment when the gateway should run on a shared cluster, in a cloud environment, or as part of team infrastructure. The Helm chart deploys the gateway as a StatefulSet and handles PKI bootstrap, RBAC, and sandbox namespace setup automatically. ## Prerequisites Make sure the following are in place before you install. | Prerequisite | Required | Notes | | ---------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Kubernetes 1.29+ with RBAC enabled | Yes | No additional notes. | | Helm 3.x | Yes | No additional notes. | | Agent Sandbox controller and CRDs | Yes | Install before the OpenShell chart. Refer to [Install Agent Sandbox](#install-agent-sandbox). | | cert-manager | No | Refer to [Managing Certificates](/kubernetes/managing-certificates). Use cert-manager only if you prefer it over the built-in PKI job. | | Kubernetes Gateway API | No | Refer to [Ingress](/kubernetes/ingress). Use it only for external access without port-forwarding. | ## Install Agent Sandbox OpenShell uses the [Agent Sandbox](https://agent-sandbox.sigs.k8s.io) Kubernetes SIG project to provision sandbox pods. Install the Agent Sandbox controller and its CRDs on your cluster before installing the OpenShell Helm chart. Apply the latest release manifest: ```shell kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml ``` This creates the `agent-sandbox-system` namespace, installs the `sandboxes.agents.x-k8s.io` CRD, and starts the controller. **Air-gapped clusters:** mirror the manifest above and the `registry.k8s.io/agent-sandbox/agent-sandbox-controller` image referenced inside it to your internal registry, then point the manifest's image reference at your mirror before applying. You will also need to mirror the OpenShell gateway and sandbox images — see the chart's `image.repository` value for the gateway and `server.sandboxImage` / `server.supervisorImage` for the sandbox runtime. Confirm the controller pod is running before proceeding: ```shell kubectl -n agent-sandbox-system get pods ``` The controller pod should reach `Running` status within a few seconds. For cluster-specific setup instructions, including KinD and GKE walkthroughs, refer to the [Agent Sandbox getting started guide](https://agent-sandbox.sigs.k8s.io/docs/getting_started/). ## Install OpenShell ## Create the namespace ```shell kubectl create namespace openshell ``` ## Install the chart Install from the OCI registry on GHCR. Replace `` with the chart version you want to install. ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell ``` To use the latest development build instead of a stable release: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version 0.0.0-dev \ --namespace openshell ``` The chart automatically generates PKI secrets on first install using pre-install Helm hooks. No manual secret creation is required. ## Wait for the gateway to be ready ```shell kubectl -n openshell rollout status statefulset/openshell ``` ## Connect to the gateway For local evaluation, use a port-forward: ```shell kubectl -n openshell port-forward svc/openshell 8080:8080 ``` The port-forward is for local evaluation only. For shared environments, expose the gateway through your ingress controller or access proxy. Refer to [Ingress](/kubernetes/ingress) for an external access option. ## Install the TLS client bundle The chart generates an mTLS bundle for transport security. Kubernetes deployments do not use that bundle as user authentication; configure OIDC or a trusted access proxy as described in [Access Control](/kubernetes/access-control). For local port-forwarded access, copy the generated bundle so the CLI can verify the gateway certificate: ```shell mkdir -p ~/.config/openshell/gateways/k8s/mtls kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.ca\.crt}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/ca.crt kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.tls\.crt}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/tls.crt kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.tls\.key}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/tls.key ``` The server certificate SANs include `localhost` and `127.0.0.1`, so hostname verification passes over the port-forward without extra flags. ## Register with the CLI In another terminal, register the gateway with the user authentication mode you configured and verify it is reachable. For example, with OIDC: ```shell openshell gateway add https://127.0.0.1:8080 --local --name k8s \ --oidc-issuer https://your-idp.example.com/realms/openshell \ --oidc-client-id openshell-cli openshell status ``` ## Configure Chart Values The most commonly changed values are: | Value | Purpose | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image.repository` / `image.tag` | Gateway container image. Defaults to `ghcr.io/nvidia/openshell/gateway:latest`. | | `server.sandboxNamespace` | Namespace where sandbox pods are created. Defaults to the Helm release namespace when left empty. | | `server.sandboxImage` | Default sandbox image used when a sandbox does not specify one. | | `server.sandboxImagePullSecrets` | Image pull secrets attached to sandbox pods. Referenced Secrets must exist in the sandbox namespace. | | `server.grpcEndpoint` | Endpoint that sandbox supervisors use to call back to the gateway. Must be reachable from inside the cluster. | | `server.appArmorProfile` | AppArmor profile requested for sandbox agent containers. Defaults to `Unconfined`. | | `server.disableTls` | Run the gateway over plaintext HTTP. Use only behind a trusted transport. | | `server.auth.allowUnauthenticatedUsers` | Accept user-facing calls without OIDC or mTLS credentials. Use only for trusted local development or a fully trusted access proxy. | | `server.enableLoopbackServiceHttp` | Enable local plaintext HTTP for loopback sandbox service URLs. Defaults to `true`. | | `pkiInitJob.serverDnsNames` / `certManager.serverDnsNames` | Additional gateway server DNS SANs. Wildcard SANs also enable sandbox service URLs under that domain. | | `supervisor.sideloadMethod` | How the supervisor binary is delivered into sandbox pods. Leave empty to auto-detect based on cluster version: clusters running Kubernetes 1.35 or later use `image-volume` (ImageVolume GA in 1.36); older clusters use `init-container`. Set explicitly to `image-volume` on Kubernetes 1.33 or 1.34 with the ImageVolume feature gate enabled, or to `init-container` to force the legacy path on any version. | Use a values file for repeatable deployments: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --values my-values.yaml ``` The chart defaults `server.appArmorProfile` to `Unconfined` because runtime/default AppArmor profiles can block the supervisor's network namespace mount setup on AppArmor-enabled nodes. Set `server.appArmorProfile` to an empty string to omit the field, `RuntimeDefault` to force the runtime default, or `Localhost/` when you load and manage a localhost profile on each node. To use private sandbox images, create a `kubernetes.io/dockerconfigjson` Secret in the sandbox namespace and reference its name: ```shell kubectl -n openshell create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username="$REGISTRY_USER" \ --docker-password="$REGISTRY_TOKEN" ``` ```yaml server: sandboxImage: registry.example.com/team/openshell-sandbox:latest sandboxImagePullSecrets: - name: regcred ``` ## RBAC The chart creates the following RBAC resources in the release namespace: | Resource | Scope | Name | | -------------------------------- | --------- | -------------------------------------- | | ServiceAccount | Namespace | `openshell` | | ServiceAccount | Namespace | `openshell-sandbox` (for sandbox pods) | | Role + RoleBinding | Namespace | `openshell-sandbox` | | ClusterRole + ClusterRoleBinding | Cluster | `openshell-node-reader` | The namespaced Role covers sandbox lifecycle and identity: | API Group | Resource | Verbs | | ----------------- | ------------------------------- | ----------------------------------------------- | | `agents.x-k8s.io` | `sandboxes`, `sandboxes/status` | create, delete, get, list, patch, update, watch | | `""` | `events` | get, list, watch | | `""` | `pods` | get | The ClusterRole grants node inspection and token validation: | API Group | Resource | Verbs | | ----------------------- | -------------- | ---------------- | | `authentication.k8s.io` | `tokenreviews` | create | | `""` | `nodes` | get, list, watch | To use an existing ServiceAccount instead of creating one, set `serviceAccount.create=false` and supply its name: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set serviceAccount.create=false \ --set serviceAccount.name=my-existing-sa ``` The ServiceAccount must already have the Role and ClusterRole bindings described above. ## Probes The gateway exposes `/healthz` for process liveness and `/readyz` for dependency-aware readiness on the health port. The Helm chart wires both into Kubernetes probes: * `startupProbe` and `livenessProbe` use `/healthz`. * `readinessProbe` uses `/readyz`, which reflects the latest result of an in-process background database check. ## Next Steps * To enable automatic certificate rotation with cert-manager, refer to [Managing Certificates](/kubernetes/managing-certificates). * To expose the gateway externally without port-forwarding, refer to [Ingress](/kubernetes/ingress). * To configure OIDC or reverse-proxy authentication, refer to [Access Control](/kubernetes/access-control). * To create your first sandbox, refer to [Manage Sandboxes](/sandboxes/manage-sandboxes). # Managing Certificates > Configure the OpenShell Helm chart to use cert-manager for mTLS certificate issuance and automatic renewal. The OpenShell gateway uses mTLS certificates for transport between the gateway and sandbox supervisors. These certificates are not Kubernetes user authentication; configure OIDC or a trusted access proxy for user access. The Helm chart supports two ways to provision and manage the certificate bundle: | Mode | When to use | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Built-in `pkiInitJob` (default) | The default path. A pre-install Kubernetes Job generates a self-signed CA and certificates during installation. No additional dependencies. | | cert-manager | Production deployments that need automatic certificate rotation managed by a running controller. | The rest of this page covers switching to cert-manager. The built-in mode requires no configuration. When `certManager.enabled=true`, cert-manager owns TLS certificate generation. The chart still runs a JWT-only initialization hook because cert-manager does not create the sandbox JWT signing Secret required by the gateway. This cert-manager precedence applies even if `pkiInitJob.enabled` remains true. ## Install cert-manager Add the Jetstack Helm repository and install cert-manager with CRD support enabled: ```shell helm repo add jetstack https://charts.jetstack.io helm repo update helm upgrade --install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait ``` Verify the cert-manager pods are running: ```shell kubectl -n cert-manager get pods ``` ## Install OpenShell with cert-manager PKI Pass the cert-manager values override when installing or upgrading the chart: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set certManager.enabled=true ``` The chart creates a self-signed CA, issues server and client certificates from it, and cert-manager handles renewal before expiry. The chart also runs a pre-install hook in JWT-only mode to create the gateway's sandbox JWT signing Secret. That Secret is separate from the cert-manager TLS certificate Secrets and is mounted at `/etc/openshell-jwt`. ## Next Steps Return to [Setup](/kubernetes/setup) to complete the installation. # Ingress > Expose the OpenShell gateway externally using the Kubernetes Gateway API and a GRPCRoute. By default, the OpenShell gateway is only reachable inside the cluster. To let CLI clients connect without a `kubectl port-forward`, expose the gateway through an ingress. OpenShell uses the [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io) for ingress. The chart creates a `GRPCRoute` that routes inbound gRPC traffic to the gateway pod. You need a Gateway API implementation installed on your cluster to fulfill the `GRPCRoute`. This page uses [Envoy Gateway](https://gateway.envoyproxy.io), which the chart is tested with. ## Install Envoy Gateway Envoy Gateway installs the Gateway API CRDs and registers the `eg` GatewayClass: ```shell helm install eg \ oci://docker.io/envoyproxy/gateway-helm \ --version v1.7.2 \ --namespace envoy-gateway-system \ --create-namespace \ --wait ``` Verify the GatewayClass is accepted: ```shell kubectl get gatewayclass eg ``` The `ACCEPTED` column should show `True`. ## Install OpenShell with Gateway API enabled Enable the GRPCRoute and let the chart create a Gateway resource in the `openshell` namespace: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set grpcRoute.enabled=true \ --set grpcRoute.gateway.create=true \ --set grpcRoute.gateway.className=eg ``` ## Get the external address After the Gateway is provisioned, Envoy Gateway creates a LoadBalancer service in the `openshell` namespace. Wait for it to get an external address: ```shell kubectl -n openshell get svc -l gateway.envoyproxy.io/owning-gateway-name=openshell ``` After the `EXTERNAL-IP` is assigned, register the gateway with the CLI: ```shell openshell gateway add http:// --name production openshell status ``` ## SSH Relay Sandbox SSH uses the gateway endpoint registered with the CLI. No separate Helm SSH host or port values are required. ## Next Steps Return to [Setup](/kubernetes/setup) to complete the installation. # Access Control > Configure OIDC user authentication or reverse-proxy auth termination for a Kubernetes-deployed OpenShell gateway. The OpenShell gateway supports two access-control models for human callers on Kubernetes: | Model | When to use | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | OIDC (recommended) | Production deployments. Integrates with an existing identity provider, supports role-based access control, and gives each user their own identity without distributing certificates. | | Reverse-proxy auth termination | An access proxy (Cloudflare Access, ngrok, corporate SSO) authenticates callers in front of the gateway. The gateway trusts the proxy and skips its own client-cert check. | The Helm chart always generates mTLS certificates at install time. The gateway uses them for transport-layer security regardless of which access-control model you choose. The client bundle in the `openshell-client-tls` secret is used internally by sandbox supervisors, not for granting access to individual users. For how the CLI resolves gateways and stores credentials, refer to [Gateway Authentication](/reference/gateway-auth). ## OIDC User Authentication Set `server.oidc.issuer` to enable OIDC. The gateway validates the `Authorization: Bearer ` header on every request against the issuer's JWKS endpoint. ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.oidc.issuer=https://your-idp.example.com/realms/openshell \ --set server.oidc.audience=openshell-cli ``` The `audience` value must match the client ID configured in your identity provider for the OpenShell resource server. ### OIDC values reference | Value | Default | Purpose | | ------------------------- | --------------- | ----------------------------------------------------- | | `server.oidc.issuer` | `""` | OIDC issuer URL. Empty disables OIDC. | | `server.oidc.audience` | `openshell-cli` | Expected `aud` claim in the JWT. | | `server.oidc.jwksTtl` | `3600` | JWKS key cache TTL in seconds. | | `server.oidc.rolesClaim` | `""` | Dot-separated path to the roles array in JWT claims. | | `server.oidc.adminRole` | `""` | Role name that grants admin access. | | `server.oidc.userRole` | `""` | Role name that grants standard user access. | | `server.oidc.scopesClaim` | `""` | Dot-separated path to the scopes array in JWT claims. | ### Auth-only mode vs. RBAC mode Leave both `adminRole` and `userRole` empty to use auth-only mode: any request with a valid JWT from the configured issuer is accepted, but no role distinction is enforced. Set both values to enable RBAC mode, where the gateway checks the role claim and enforces access based on the assigned role: ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.oidc.issuer=https://your-idp.example.com/realms/openshell \ --set server.oidc.audience=openshell-cli \ --set server.oidc.rolesClaim=realm_access.roles \ --set server.oidc.adminRole=openshell-admin \ --set server.oidc.userRole=openshell-user ``` Both `adminRole` and `userRole` must be set, or both must be empty. Setting only one is not supported. ### Provider-specific rolesClaim paths | Provider | rolesClaim value | | ------------------ | -------------------- | | Keycloak | `realm_access.roles` | | Microsoft Entra ID | `roles` | | Okta | `groups` | ## Reverse-Proxy Auth Termination When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, handles authentication in front of the OpenShell gateway, you can explicitly allow unauthenticated user calls at the gateway: ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.auth.allowUnauthenticatedUsers=true ``` The gateway still serves TLS and sandbox supervisors still authenticate with gateway-minted sandbox JWTs. User-facing CLI/API calls without OIDC or mTLS credentials are accepted as an unauthenticated local developer principal. The proxy is responsible for authenticating callers and forwarding only authorized traffic. To also disable TLS entirely (when the proxy terminates TLS before the request reaches the gateway): ```shell --set server.disableTls=true \ --set server.auth.allowUnauthenticatedUsers=true ``` Only enable unauthenticated users when the gateway is not reachable from outside a trusted local development environment or the proxy path is fully trusted. Never expose a plaintext, auth-disabled gateway to a public network. Register the gateway with the CLI using the proxy's public URL. The browser-based login flow runs automatically on first use: ```shell openshell gateway add https://gateway.example.com --name production ``` # OpenShift > Install the OpenShell Helm chart on OpenShift, including the SCC binding and chart overrides required by OpenShift's Security Context Constraints. The OpenShift install path is experimental. It currently requires running sandbox pods under the `privileged` SCC and installing the gateway with TLS and the PKI init job disabled. Use only for evaluation on a private network. OpenShift's [Security Context Constraints](https://docs.openshift.com/container-platform/latest/authentication/managing-security-context-constraints.html) reject the chart's default pod security settings. Installing on OpenShift requires precreating the namespace, granting the `privileged` SCC to the sandbox service account, and overriding a few chart values so the cluster admission controller can assign UIDs and FS groups itself. ## Prerequisites * OpenShift 4.x cluster with `oc` configured * Helm 3.x * [Agent Sandbox](/kubernetes/setup#install-agent-sandbox) controller and CRDs installed ## Install ## Create the namespace Pre-create the namespace so the SCC binding can be applied before the chart installs: ```shell oc create ns openshell ``` ## Grant the privileged SCC to sandbox pods Sandbox pods run under the `openshell-sandbox` service account in the `openshell` namespace and require the `privileged` SCC: ```shell oc adm policy add-scc-to-user privileged -z openshell-sandbox -n openshell ``` ## Install the chart with OpenShift overrides ```shell helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set pkiInitJob.enabled=false \ --set server.disableTls=true \ --set podSecurityContext.fsGroup=null \ --set securityContext.runAsUser=null ``` | Override | Reason | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `pkiInitJob.enabled=false` | Skips the built-in TLS PKI Job. TLS must also be disabled unless you provide TLS Secrets another way. | | `server.disableTls=true` | The gateway has no certificates without `pkiInitJob`, so it must run plaintext. | | `podSecurityContext.fsGroup=null` / `securityContext.runAsUser=null` | Clear the chart's hardcoded UID and fsGroup so OpenShift's SCC admission can assign them. | The gateway still needs the sandbox JWT signing Secret. When disabling `pkiInitJob` without enabling cert-manager, pre-create that Secret before installing the chart. ## Wait for the gateway to be ready ```shell oc -n openshell rollout status statefulset/openshell ``` ## Connect to the gateway The gateway is now running over plaintext HTTP. Connect with `oc port-forward`: ```shell oc -n openshell port-forward svc/openshell 8080:8080 ``` Register the gateway with the CLI: ```shell openshell gateway add http://127.0.0.1:8080 --local --name openshift openshell status ``` ## Next Steps * For TLS-enabled deployments, refer to [Managing Certificates](/kubernetes/managing-certificates) after SCC-compatible PKI is supported. * To expose the gateway externally, refer to [Ingress](/kubernetes/ingress). * To configure OIDC authentication, refer to [Access Control](/kubernetes/access-control). # Gateway Authentication > Gateway resolution, authentication modes, connection flow, OIDC support, and credential file layout. This page describes how the CLI resolves a gateway, authenticates with it, and where credentials are stored. For how to deploy or register gateways, refer to [Gateways](/sandboxes/manage-gateways). ## Gateway Resolution When any CLI command needs to talk to the gateway, it resolves the target through a priority chain: 1. `--gateway-endpoint ` flag (direct URL). 2. `-g ` flag. 3. `OPENSHELL_GATEWAY` environment variable. 4. Active gateway from `~/.config/openshell/active_gateway`. The CLI loads gateway metadata from disk to determine the endpoint URL and authentication mode. ## Authentication Modes The CLI uses one of these authentication modes depending on the gateway's configuration. ### mTLS The default mode for local Docker, Podman, and VM gateways without OIDC. The CLI presents a client certificate during the TLS handshake, and the gateway can map the verified certificate subject to a local user principal when mTLS user authentication is enabled. mTLS user authentication is for local single-user gateways. Kubernetes deployments must use OIDC or a trusted access proxy for user authentication; the Helm chart does not render `mtls_auth`. Set these environment variables before starting the gateway: | Environment variable | Purpose | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OPENSHELL_TLS_CERT` | Path to the gateway server certificate. | | `OPENSHELL_TLS_KEY` | Path to the gateway server private key. | | `OPENSHELL_TLS_CLIENT_CA` | Path to the CA certificate that verifies CLI client certificates. | | `OPENSHELL_ENABLE_MTLS_AUTH` | Set to `true` to authenticate CLI callers from verified client certificates. Defaults on for local Docker, Podman, and VM gateways with no OIDC issuer. | For local access, the server certificate must be valid for the endpoint the CLI uses. Include `localhost` and `127.0.0.1` in the certificate SANs when users connect to a local gateway through loopback. Package-managed local gateways on Homebrew, Debian, and RPM generate this bundle automatically for the `openshell` gateway name and use `https://127.0.0.1:17670` by default. When you register a package-managed local gateway with `openshell gateway add https://127.0.0.1:17670 --local --name openshell`, the CLI refreshes its mTLS bundle from the package-managed TLS directory. On Homebrew, the gateway service also mirrors the Docker sandbox client bundle into `$HOME/.local/state/openshell/homebrew/tls` before startup so Docker Desktop can bind-mount the files into sandbox containers. The CLI loads its mTLS bundle from `~/.config/openshell/gateways//mtls/`: | File | Purpose | | --------- | --------------------------------------------------------------- | | `ca.crt` | CA certificate that verifies the gateway server certificate. | | `tls.crt` | Client certificate. It must chain to `OPENSHELL_TLS_CLIENT_CA`. | | `tls.key` | Client private key for `tls.crt`. | The connection flow: 1. The CLI loads the certificate files. 2. Opens a TCP connection to the gateway endpoint. 3. Performs a TLS handshake, presenting the client certificate. 4. The gateway verifies the client certificate against its CA. 5. When mTLS user authentication is enabled, the gateway maps the verified certificate subject to a user principal. 6. The gateway authorizes the gRPC method. ### OIDC Gateways can validate OpenID Connect access tokens on gRPC requests. Configure OIDC when you want users, operators, or automation to authenticate with an identity provider such as Keycloak, Entra ID, or Okta. OIDC is application-layer authentication. TLS still controls the transport. If TLS client certificates remain required, the CLI must also have an mTLS bundle for the gateway. Configure the gateway with an issuer and audience: ```shell openshell-gateway \ --oidc-issuer https://idp.example.com/realms/openshell \ --oidc-audience openshell-cli \ --oidc-roles-claim realm_access.roles \ --oidc-admin-role openshell-admin \ --oidc-user-role openshell-user ``` The same settings are available through environment variables: | Environment variable | Purpose | Default | | ----------------------------- | ----------------------------------------------------------------------------------------- | -------------------- | | `OPENSHELL_OIDC_ISSUER` | OIDC issuer URL. The gateway discovers `/.well-known/openid-configuration` from this URL. | None | | `OPENSHELL_OIDC_AUDIENCE` | Expected JWT `aud` claim. | `openshell-cli` | | `OPENSHELL_OIDC_JWKS_TTL` | JWKS cache TTL in seconds. The gateway also refreshes on an unknown key ID. | `3600` | | `OPENSHELL_OIDC_ROLES_CLAIM` | Dot-separated claim path containing roles. | `realm_access.roles` | | `OPENSHELL_OIDC_ADMIN_ROLE` | Role required for admin operations. | `openshell-admin` | | `OPENSHELL_OIDC_USER_ROLE` | Role required for standard user operations. | `openshell-user` | | `OPENSHELL_OIDC_SCOPES_CLAIM` | Dot-separated claim path containing scopes. Empty disables scope enforcement. | Empty | For Helm deployments, set the same values under `server.oidc`: ```yaml server: oidc: issuer: https://idp.example.com/realms/openshell audience: openshell-cli rolesClaim: realm_access.roles adminRole: openshell-admin userRole: openshell-user scopesClaim: "" ``` Register an OIDC gateway with the CLI: ```shell openshell gateway add https://gateway.example.com \ --name production \ --oidc-issuer https://idp.example.com/realms/openshell \ --oidc-client-id openshell-cli \ --oidc-audience openshell-cli ``` When you register or log in to an OIDC gateway, the CLI uses the Authorization Code flow with PKCE. It opens a browser, receives the authorization code on a localhost callback, exchanges the code for tokens, and stores the token bundle under the gateway credential directory. If `OPENSHELL_OIDC_CLIENT_SECRET` is set, the CLI uses the client credentials flow instead. Use that mode for CI and other non-interactive automation. The connection flow: 1. The CLI loads the stored OIDC token bundle. 2. If the access token is expired and a refresh token is available, the CLI refreshes it. 3. The CLI connects to the gateway and attaches `authorization: Bearer ` metadata to each gRPC request. 4. The gateway validates the JWT signature, issuer, audience, expiration, and key ID against the issuer's JWKS. 5. The gateway extracts roles and optional scopes from the configured claim paths. 6. The gateway authorizes the gRPC method. Admin methods require the admin role, other authenticated methods require the user role. Admin role holders also satisfy user-role checks. If `OPENSHELL_OIDC_SCOPES_CLAIM` is set, the gateway also enforces scopes. It accepts space-delimited scope strings such as `scope: "openid sandbox:read"` and JSON arrays such as `scp: ["sandbox:read"]`. Standard OIDC scopes such as `openid`, `profile`, `email`, and `offline_access` are ignored for authorization. `openshell:all` grants access to all scoped methods. Supervisor-to-gateway RPCs do not use user OIDC tokens or mTLS user identity. Each sandbox supervisor presents a gateway-minted `Authorization: Bearer` token scoped to its sandbox ID. On Kubernetes, the gateway mints that token only after TokenReview validates the projected ServiceAccount token, the pod UID matches the live pod, and the pod's controlling `Sandbox` ownerReference matches the live Sandbox CR. Log upload, policy status, credential environment lookup, inference bundle lookup, and sandbox config sync run with sandbox-restricted scope, while CLI users authenticate with OIDC, edge auth, local mTLS user authentication, or an explicitly enabled unauthenticated local developer mode. `GetInferenceBundle` returns route material that includes provider credentials, so it requires a sandbox principal; user callers manage inference configuration through the user-facing inference APIs instead. Re-authenticate an OIDC gateway with: ```shell openshell gateway login production ``` ### Edge JWT (cloud gateways) For gateways behind a reverse proxy that handles authentication (e.g. Cloudflare Access), the CLI uses a browser-based login flow and routes traffic through a WebSocket tunnel. **Registration flow** (`openshell gateway add https://gateway.example.com`): 1. The CLI stores gateway metadata with the edge authentication mode. 2. Opens your browser to the gateway's authentication endpoint. 3. The reverse proxy handles login (SSO, identity provider, etc.). 4. After authentication, the browser relays the authorization token back to the CLI through a localhost callback. 5. The CLI stores the token and sets the gateway as active. **Connection flow** (subsequent commands): 1. The CLI starts a local proxy that listens on an ephemeral port. 2. The proxy opens a WebSocket connection (`wss://`) to the gateway, attaching the stored bearer token in the upgrade headers. 3. The reverse proxy authenticates the WebSocket upgrade request. 4. The gateway bridges the WebSocket into the same service that handles direct mTLS connections. 5. CLI commands send requests through the local proxy as plaintext HTTP/2 over the tunnel. This is transparent to the user. All CLI commands work the same regardless of whether the gateway uses mTLS, OIDC, or edge authentication. **Re-authentication**: If the token expires, run `openshell gateway login` to open the browser flow again and update the stored token. ### Plaintext When a gateway is deployed with `server.disableTls=true`, TLS is disabled entirely. The CLI connects over plain HTTP/2. This mode is intended for local port-forwarding or gateways behind a trusted reverse proxy or tunnel that handles TLS termination externally. Register a plaintext gateway with an explicit `http://` endpoint: ```shell openshell gateway add http://127.0.0.1:17670 --local ``` For Kubernetes local development, the Helm Skaffold overlay enables `[openshell.gateway.auth] allow_unauthenticated_users = true` so a port-forwarded plaintext gateway works without OIDC or mTLS user credentials. Leave this disabled for shared and production clusters. This stores the gateway with `auth_mode = plaintext`, skips mTLS client certificate lookup, and does not open the browser login flow. ## File Layout All gateway credentials and metadata are stored under `~/.config/openshell/`: ```text openshell/ active_gateway # Plain text: active gateway name gateways/ / metadata.json # Gateway metadata (endpoint, auth mode, type) mtls/ # mTLS bundle (local and remote gateways) ca.crt # CA certificate tls.crt # Client certificate tls.key # Client private key edge_token # Edge auth JWT (cloud gateways) oidc_token.json # OIDC access token, refresh token, and expiry metadata last_sandbox # Last-used sandbox for this gateway ``` For OIDC gateways, `metadata.json` also stores the issuer, CLI client ID, optional audience, and requested scopes. Treat `oidc_token.json` as a credential. OpenShell writes it with owner-only file permissions. # Default Policy Reference > Breakdown of the built-in default policy applied when you create an OpenShell sandbox without a custom policy. The default policy is the policy applied when you create an OpenShell sandbox without `--policy`. It is baked into the community base image ([`ghcr.io/nvidia/openshell-community/sandboxes/base`](https://github.com/nvidia/openshell-community)) and defined in the community repo's `dev-sandbox-policy.yaml`. ## Agent Compatibility The following table shows the coverage of the default policy for common agents. | Agent | Coverage | Action Required | | ----------- | -------- | ------------------------------------------------------------------------------ | | Claude Code | Full | None. Works out of the box. | | OpenCode | Partial | Add `opencode.ai` endpoint and OpenCode binary paths. | | Codex | None | Provide a complete custom policy with OpenAI endpoints and Codex binary paths. | If you run a non-Claude agent without a custom policy, the agent's API calls are denied by the proxy. You must provide a policy that declares the agent's endpoints and binaries. ## Default Policy Blocks The default policy blocks are defined in the community base image. Refer to the [openshell-community repository](https://github.com/nvidia/openshell-community) for the full `dev-sandbox-policy.yaml` source. # Policy Schema Reference > Complete field reference for the sandbox policy YAML including static and dynamic sections. Complete field reference for the sandbox policy YAML. Each field is documented with its type, whether it is required, and whether it is static (locked at sandbox creation) or dynamic (hot-reloadable on a running sandbox). ## Top-Level Structure A policy YAML file contains the following top-level fields: ```yaml showLineNumbers={false} version: 1 filesystem_policy: { ... } landlock: { ... } process: { ... } network_policies: { ... } ``` | Field | Type | Required | Category | Description | | ------------------- | ------- | -------- | -------- | ---------------------------------------------------------- | | `version` | integer | Yes | -- | Policy schema version. Must be `1`. | | `filesystem_policy` | object | No | Static | Controls which directories the agent can read and write. | | `landlock` | object | No | Static | Configures Landlock LSM enforcement behavior. | | `process` | object | No | Static | Sets the user and group the agent process runs as. | | `network_policies` | map | No | Dynamic | Declares which binaries can reach which network endpoints. | Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting. ## Version The version field identifies which schema the policy uses: | Field | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------------- | | `version` | integer | Yes | Schema version number. Currently must be `1`. | ## Filesystem Policy **Category:** Static Controls filesystem access inside the sandbox. Paths not listed in either `read_only` or `read_write` are inaccessible. | Field | Type | Required | Description | | ----------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------- | | `include_workdir` | bool | No | When `true`, automatically adds the agent's working directory to `read_write`. | | `read_only` | list of strings | No | Paths the agent can read but not modify. Typically system directories like `/usr`, `/lib`, `/etc`. | | `read_write` | list of strings | No | Paths the agent can read and write. Typically `/sandbox` (working directory) and `/tmp`. | **Validation constraints:** * Every path must be absolute (start with `/`). * Paths must not contain `..` traversal components. The server normalizes paths before storage, but rejects policies where traversal would escape the intended scope. * Read-write paths must not be overly broad (for example, `/` alone is rejected). * Each individual path must not exceed 4096 characters. * The combined total of `read_only` and `read_write` paths must not exceed 256. Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at creation or update time. Disk-loaded YAML policies that fail validation fall back to a restrictive default. Example: ```yaml showLineNumbers={false} filesystem_policy: include_workdir: true read_only: - /usr - /lib - /proc - /dev/urandom - /etc read_write: - /sandbox - /tmp - /dev/null ``` ## Landlock **Category:** Static Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforcement at the kernel level. Landlock provides mandatory filesystem access control below what UNIX permissions allow. | Field | Type | Required | Values | Description | | --------------- | ------ | -------- | --------------------------------- | --------------------------------------------------------------------------- | | `compatibility` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. Refer to the behavior table below. | **Compatibility modes:** | Value | Kernel ABI unavailable | Individual path inaccessible | All paths inaccessible | | ------------------ | ------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- | | `best_effort` | Warns and continues without Landlock. | Skips the path, applies remaining rules. | Warns and continues without Landlock (refuses to apply an empty ruleset). | | `hard_requirement` | Aborts sandbox startup. | Aborts sandbox startup. | Aborts sandbox startup. | `best_effort` (the default) is appropriate for most deployments. It handles missing paths gracefully. For example, `/app` might not exist in every container image but is included in the baseline path set for containers that do have it. Individual missing paths are skipped while the remaining filesystem rules are still enforced. `hard_requirement` is for environments where any gap in filesystem isolation is unacceptable. If a listed path cannot be opened for any reason (missing, permission denied, symlink loop), sandbox startup fails immediately rather than running with reduced protection. When a path is skipped under `best_effort`, the sandbox logs a warning that includes the path, the specific error, and a human-readable reason (for example, "path does not exist" or "permission denied"). Example: ```yaml showLineNumbers={false} landlock: compatibility: best_effort ``` ## Process **Category:** Static Sets the OS-level identity for the agent process inside the sandbox. | Field | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------------------------------- | | `run_as_user` | string | No | The user name or UID the agent process runs as. Default: `sandbox`. | | `run_as_group` | string | No | The group name or GID the agent process runs as. Default: `sandbox`. | **Validation constraint:** Neither `run_as_user` nor `run_as_group` can be set to `root` or `0`. Policies that request root process identity are rejected at creation or update time. Example: ```yaml showLineNumbers={false} process: run_as_user: sandbox run_as_group: sandbox ``` ## Network Policies **Category:** Dynamic A map of named network policy entries. Each entry declares a set of endpoints and a set of binaries. Only the listed binaries are permitted to connect to the listed endpoints. The map key is a logical identifier. The `name` field inside the entry is the display name used in logs. ### Network Policy Entry Each entry in the `network_policies` map has the following fields: | Field | Type | Required | Description | | ----------- | ------------------------ | -------- | ------------------------------------------------------------------------------- | | `name` | string | No | Display name for the policy entry. Used in log output. Defaults to the map key. | | `endpoints` | list of endpoint objects | Yes | Hosts and ports this entry permits. | | `binaries` | list of binary objects | Yes | Executables allowed to connect to these endpoints. | ### Endpoint Object Each endpoint defines a reachable destination and optional inspection rules. | Field | Type | Required | Description | | --------------------------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | string | Yes | Hostname or IP address. Supports a `*` wildcard inside the first DNS label only: `*.example.com`, `**.example.com`, and intra-label patterns like `*-aiplatform.googleapis.com` are accepted; bare `*`/`**`, TLD wildcards (`*.com`), and wildcards outside the first label are rejected at load time. | | `port` | integer | Yes | TCP port number. | | `path` | string | No | Optional HTTP path glob used to select between L7 endpoints that share the same host and port. Empty means all paths. Use this when REST and GraphQL live under the same host, such as `/repos/**` and `/graphql`. | | `protocol` | string | No | Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, or `graphql` for GraphQL-over-HTTP operation inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Omit for TCP passthrough. | | `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. | | `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. | | `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. | | `rules` | list of rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. | | `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. | | `allowed_ips` | list of string | No | CIDR or IP allowlist for SSRF override. Exact user-declared hostname endpoints may resolve to RFC 1918 private addresses without this field, but wildcard, hostless, and policy-advisor-proposed endpoints still require `allowed_ips` for private resolved IPs. Entries overlapping loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), or unspecified (`0.0.0.0`) are rejected at load time. | | `allow_encoded_slash` | bool | No | When `true`, L7 request parsing preserves `%2F` inside path segments instead of rejecting it. Use this for registries and APIs such as npm scoped packages (`/@scope%2Fname`). Defaults to `false`. | | `websocket_credential_rewrite` | bool | No | When `true` on a `protocol: rest` or `protocol: websocket` endpoint, OpenShell rewrites credential placeholders in client-to-server WebSocket text messages after an allowed HTTP `101` upgrade. Binary frames are relayed but not rewritten. Defaults to `false`. | | `request_body_credential_rewrite` | bool | No | When `true` on a `protocol: rest` endpoint, OpenShell rewrites credential placeholders in UTF-8 `application/json`, `application/x-www-form-urlencoded`, and `text/*` request bodies before forwarding upstream. The proxy buffers at most 256 KiB and updates `Content-Length` after rewriting. Defaults to `false`. | | `persisted_queries` | string | No | GraphQL hash-only behavior for `protocol: graphql` and GraphQL-over-WebSocket operation policy. Default is `deny`; use `allow_registered` only with `graphql_persisted_queries`. | | `graphql_persisted_queries` | map | No | Trusted GraphQL persisted-query registry keyed by hash or saved-query ID. Values contain `operation_type`, optional `operation_name`, and optional root `fields`. | | `graphql_max_body_bytes` | integer | No | Maximum GraphQL-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials. #### Access Levels The `access` field accepts one of the following values: | Value | REST expansion | WebSocket expansion | GraphQL expansion | | ------------ | ------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------- | | `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. | | `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. | | `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. | #### Allow Rule Objects Used when `access` is not set. Each entry in `rules` contains an `allow` object. The tables below list the fields inside that `allow` object. ##### REST Allow Rule (`protocol: rest`) REST allow rules match HTTP requests by method, path, and optional query parameters. | Field | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `method` | string | Yes | HTTP method to allow (for example, `GET`, `POST`). `*` matches any method. | | `path` | string | Yes | URL path pattern. Supports `*` and `**` glob syntax. | | `query` | map | No | Query parameter matchers keyed by decoded param name. Matcher value can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). | Example REST allow rules: ```yaml showLineNumbers={false} rules: - allow: method: GET path: /**/info/refs* query: service: "git-*" - allow: method: POST path: /**/git-upload-pack query: tag: any: ["v1.*", "v2.*"] ``` ##### WebSocket Allow Rule (`protocol: websocket`) WebSocket allow rules match the RFC 6455 HTTP upgrade by path and match client-to-server text messages on the same upgraded connection with the synthetic `WEBSOCKET_TEXT` method. Binary frames are relayed but are not rewritten. | Field | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `method` | string | Yes | `GET` allows the upgrade handshake, `WEBSOCKET_TEXT` allows client text messages after upgrade, and `*` matches both inspected actions. | | `path` | string | Yes | URL path pattern from the original upgrade request. Supports `*` and `**` glob syntax. | | `query` | map | No | Query parameter matchers from the original upgrade request. Matcher syntax is the same as REST allow rules. | Example WebSocket allow rules: ```yaml showLineNumbers={false} rules: - allow: method: GET path: /v1/realtime/** - allow: method: WEBSOCKET_TEXT path: /v1/realtime/** ``` ##### GraphQL Allow Rule (`protocol: graphql` or GraphQL-over-WebSocket) GraphQL allow rules match parsed GraphQL operations by operation type, optional operation name, and optional root fields. On `protocol: graphql`, they apply to GraphQL-over-HTTP `GET` and `POST` requests. On `protocol: websocket`, include a separate `GET` allow rule for the RFC 6455 upgrade, then use GraphQL allow rules for client operation messages using the `graphql-transport-ws` `subscribe` message type or the legacy `graphql-ws` `start` message type. | Field | Type | Required | Description | | ---------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | `operation_type` | string | Yes | GraphQL operation type: `query`, `mutation`, `subscription`, or `*`. | | `operation_name` | string | No | GraphQL operation-name glob. Omit to match any operation name. | | `fields` | list of string | No | GraphQL root-field globs. Every selected root field must match one configured glob. Omit to match all root fields. | Example GraphQL allow rules: ```yaml showLineNumbers={false} rules: - allow: operation_type: query fields: [viewer, repository] - allow: operation_type: mutation operation_name: Issue* fields: [createIssue] ``` Example GraphQL-over-WebSocket allow rules: ```yaml showLineNumbers={false} rules: - allow: method: GET path: /graphql - allow: operation_type: subscription fields: [messageAdded] - allow: operation_type: query fields: [viewer] ``` Do not combine `method`, `path`, or `query` with `operation_type`, `operation_name`, or `fields` inside the same WebSocket rule. When a WebSocket endpoint has GraphQL operation policy, use GraphQL rules for client messages instead of a raw `WEBSOCKET_TEXT` allow rule. #### Deny Rule Objects Blocks specific operations on endpoints that otherwise have broad access. Deny rules are evaluated after allow rules and take precedence: if a request matches any deny rule, it is blocked regardless of what the allow rules or access preset permit. ##### REST Deny Rule (`protocol: rest`) REST deny rules use the same field names as REST allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. | Field | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------------ | | `method` | string | Yes | HTTP method to deny (for example, `POST`, `DELETE`). `*` matches any method. | | `path` | string | Yes | URL path pattern. Same glob syntax as allow rules. Use `**` to match any path. | | `query` | map | No | Query parameter matchers. Same syntax as allow rule `query`. | Example REST deny rules: ```yaml showLineNumbers={false} endpoints: - host: api.github.com port: 443 protocol: rest enforcement: enforce access: read-write deny_rules: - method: POST path: "/repos/*/pulls/*/reviews" - method: PUT path: "/repos/*/branches/*/protection" - method: "*" path: "/repos/*/rulesets" ``` ##### WebSocket Deny Rule (`protocol: websocket`) WebSocket deny rules use the same field names as WebSocket allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. | Field | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `method` | string | Yes | `GET` denies matching upgrade handshakes, `WEBSOCKET_TEXT` denies matching client text messages after upgrade, and `*` matches both inspected actions. | | `path` | string | Yes | URL path pattern from the original upgrade request. Same glob syntax as allow rules. | | `query` | map | No | Query parameter matchers from the original upgrade request. Same syntax as allow rule `query`. | Example WebSocket deny rules: ```yaml showLineNumbers={false} endpoints: - host: realtime.example.com port: 443 protocol: websocket enforcement: enforce access: read-write deny_rules: - method: WEBSOCKET_TEXT path: "/v1/admin/**" ``` ##### GraphQL Deny Rule (`protocol: graphql` or GraphQL-over-WebSocket) GraphQL deny rules use the same field names as GraphQL allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. On WebSocket GraphQL endpoints, they apply only to classified GraphQL operation messages; protocol lifecycle messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed as WebSocket control-plane messages and are not payload-logged. | Field | Type | Required | Description | | ---------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `operation_type` | string | Yes | GraphQL operation type to deny: `query`, `mutation`, `subscription`, or `*`. | | `operation_name` | string | No | GraphQL operation-name glob. | | `fields` | list of string | No | GraphQL root-field globs. Any matching root field blocks the request. Omit to deny every operation that matches `operation_type` and `operation_name`. | Example GraphQL deny rules: ```yaml showLineNumbers={false} endpoints: - host: api.github.com port: 443 protocol: graphql enforcement: enforce access: read-write deny_rules: - operation_type: mutation fields: [deleteRepository] - operation_type: mutation operation_name: Admin* ``` ### Binary Object Identifies an executable that is permitted to use the associated endpoints. | Field | Type | Required | Description | | ------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `path` | string | Yes | Filesystem path to the executable. Supports glob patterns with `*` and `**`. For example, `/sandbox/.vscode-server/**` matches any executable under that directory tree. | ### Full Example The following policy grants read-only GitHub API access and npm registry access: ```yaml showLineNumbers={false} network_policies: github_rest_api: name: github-rest-api endpoints: - host: api.github.com port: 443 protocol: rest enforcement: enforce access: read-only binaries: - path: /usr/local/bin/claude - path: /usr/bin/node - path: /usr/bin/gh npm_registry: name: npm-registry endpoints: - host: registry.npmjs.org port: 443 protocol: rest access: read-only allow_encoded_slash: true binaries: - path: /usr/bin/npm - path: /usr/bin/node ``` # Sandbox Compute Drivers > Reference for Docker, Podman, MicroVM, and Kubernetes sandbox compute drivers. The gateway's configured compute driver determines how OpenShell creates each sandbox. The CLI workflow stays the same across drivers: you create, connect to, inspect, and delete sandboxes through the gateway API. Every compute driver runs the OpenShell supervisor inside the sandbox workload. The supervisor launches the agent process, applies policy, routes egress through the proxy, injects configured credentials, and maintains the gateway session. ## Configure a Compute Driver Configure the compute driver on the gateway. Current releases accept one driver per gateway. Set `compute_drivers` in the gateway TOML file: ```toml [openshell.gateway] compute_drivers = ["docker"] ``` Supported values are `docker`, `podman`, `kubernetes`, and `vm`. When `compute_drivers` is unset, the gateway auto-detects Kubernetes, then Podman, then Docker by CLI availability or a local Unix socket. The VM driver is never auto-detected; configure it explicitly with `compute_drivers = ["vm"]` or set `OPENSHELL_DRIVERS=vm` in the launch environment. Common gateway options: | Gateway TOML option | Description | | -------------------------------- | ------------------------------------------------------------------------------------------- | | `compute_drivers = [""]` | Select the compute driver. Supported values are `docker`, `podman`, `kubernetes`, and `vm`. | Set driver-specific values such as sandbox images, callback endpoints, network names, TLS material, and VM sizing in the gateway TOML file. See the [Gateway Configuration File](./gateway-config) reference for the full `[openshell.drivers.]` schema. Sandbox create supports `--cpu` and `--memory` for per-sandbox compute sizing. Docker and Podman apply them as runtime limits. Kubernetes applies them as both container requests and limits. The VM driver accepts the fields but currently ignores them. ## Docker Driver [Docker](https://www.docker.com/get-started/)-backed sandboxes run as containers on the gateway host. Use Docker for local development, single-machine gateways, and hosts that already use Docker Desktop or Docker Engine. The gateway talks to the Docker daemon to create sandbox containers. Docker is also required for local image builds from directories or Dockerfiles. For maintainer-level implementation details, refer to the [Docker driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-docker/README.md). Select Docker with `compute_drivers = ["docker"]` in `[openshell.gateway]`. Configure Docker driver values such as `grpc_endpoint`, `network_name`, `supervisor_bin`, `supervisor_image`, `image_pull_policy`, `ssh_socket_path`, `sandbox_pids_limit`, and `guest_tls_*` in `[openshell.drivers.docker]`. For GPU-backed Docker sandboxes, configure Docker CDI before starting the gateway so OpenShell can detect the daemon capability. ## Podman Driver [Podman](https://podman.io/)-backed sandboxes run as rootless containers on the gateway host. Use Podman for Linux workstation workflows that avoid a rootful Docker daemon. The gateway talks to the Podman API socket. The Podman driver requires Podman 5.x, cgroups v2, rootless networking, and an active Podman user socket. For maintainer-level implementation details, refer to the [Podman driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-podman/README.md) and [Podman networking notes](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-podman/NETWORKING.md). Select Podman with `compute_drivers = ["podman"]` in `[openshell.gateway]`. Configure Podman driver values such as `socket_path`, `network_name`, `supervisor_image`, `stop_timeout_secs`, `image_pull_policy`, `grpc_endpoint`, `host_gateway_ip`, `sandbox_ssh_socket_path`, `sandbox_pids_limit`, and `guest_tls_*` in `[openshell.drivers.podman]`. On macOS with `podman machine`, the driver uses gvproxy's host-loopback IP, `192.168.127.254`, for sandbox host aliases by default. Set `host_gateway_ip` only when your Podman machine uses a non-standard host-loopback address. On Linux, an empty `host_gateway_ip` keeps Podman's `host-gateway` resolver behavior. ## MicroVM Driver MicroVM-backed sandboxes run inside VM-backed isolation instead of a container boundary. Use MicroVM when workloads need a VM boundary instead of a local container boundary. The gateway uses the VM compute driver to create VM-backed sandboxes. MicroVM requires host virtualization support. It uses [libkrun](https://github.com/containers/libkrun) with Apple's [Hypervisor framework](https://developer.apple.com/documentation/hypervisor) on macOS, KVM on Linux, and [QEMU](https://www.qemu.org/) for GPU-backed sandboxes on Linux. The VM driver boots a cached immutable bootstrap ext4 root disk. When the requested sandbox image differs from the bootstrap image, the driver stages the registry image as an OCI layout, unpacks it inside a bootstrap VM with `umoci`, and caches the prepared image disk by image identity. Each sandbox receives that prepared disk read-only plus its own writable `overlay.ext4` disk for `/`, including `/sandbox` writes and runtime TLS material. The overlay persists for the sandbox lifetime and is deleted with the sandbox state directory. VM sandbox creation follows the same progress model as Kubernetes-backed sandboxes. The gateway accepts the sandbox, then the VM driver publishes watch events while it resolves the image, prepares or reuses the bootstrap and prepared image caches, creates the writable overlay, and starts the VM launcher. On gateway restart, the gateway starts a fresh VM driver process. The driver scans its state directory for accepted sandbox launch records, restarts those VMs, and reuses each sandbox's existing `overlay.ext4` so files written inside the sandbox remain available after the supervisor reconnects. For maintainer-level implementation details, refer to the [VM driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-vm/README.md). ### Enable the VM Driver The VM driver is opt-in. Release packages can install `openshell-driver-vm`, but the gateway does not select it unless you configure the driver explicitly. Enable VM by setting `compute_drivers = ["vm"]` in the gateway TOML file: ```toml [openshell.gateway] compute_drivers = ["vm"] ``` For a launch-time override, set `OPENSHELL_DRIVERS=vm` in the gateway environment and restart the service. Configure VM driver values such as `grpc_endpoint`, `driver_dir`, `state_dir`, `default_image`, `bootstrap_image`, `vcpus`, `mem_mib`, `overlay_disk_mib`, `krun_log_level`, and `guest_tls_*` in `[openshell.drivers.vm]`. The VM `state_dir` stores overlay disks, console logs, runtime state, image-rootfs cache, and the private `run/compute-driver.sock` socket. The gateway starts `openshell-driver-vm` over a private Unix socket and passes its process ID so the driver can reject unexpected local clients. The driver's standalone TCP listener is disabled unless `--allow-unauthenticated-tcp` is set for local development. ### Local image resolution The VM driver resolves sandbox images from a local container engine before falling back to registry pulls. It tries Docker first, then falls back to the Podman socket (Docker-compatible API). On Linux with Podman, enable the API socket so the driver can find local images: ```shell systemctl --user start podman.socket ``` ### Host Firewall The VM driver creates nftables rules on the host for each sandbox VM's TAP network interface. These rules provide NAT for VM connectivity and defense-in-depth isolation: unsolicited inbound connections to the VM are dropped, and the VM can only reach the gateway port on the host. Primary security enforcement (proxy-only egress and bypass detection) is handled by the sandbox supervisor inside the VM guest. On hosts with restrictive firewalls (e.g. firewalld), the host firewall may additionally block VM traffic that the driver's rules accept. If VM sandboxes cannot reach the network, verify that the host firewall allows forwarding and input for `vmtap-*` interfaces. See the [VM driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-vm/README.md#host-side-nftables-rules) for details. ## Kubernetes Driver Kubernetes-backed sandboxes run as pods in the configured sandbox namespace. Use Kubernetes for shared clusters, remote compute, GPU scheduling, and operator-managed environments. Helm deployments set Kubernetes driver values through the chart. For maintainer-level implementation details, refer to the [Kubernetes driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-kubernetes/README.md). | Gateway configuration | Helm value | Description | | ------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `compute_drivers = ["kubernetes"]` | Not applicable | Select the Kubernetes compute driver. | | `[openshell.drivers.kubernetes].namespace` | `server.sandboxNamespace` | Set the namespace for sandbox resources. The Helm chart defaults to the release namespace when left empty. | | `service_account_name` | `sandboxServiceAccount.name` | Set the Kubernetes service account assigned to sandbox pods and accepted by the gateway TokenReview bootstrap path. The Helm chart creates a dedicated sandbox service account by default. | | `default_image` | `server.sandboxImage` | Set the default sandbox image. | | `image_pull_policy` | `server.sandboxImagePullPolicy` | Set the Kubernetes image pull policy for sandbox pods. | | `image_pull_secrets` | `server.sandboxImagePullSecrets` | Attach Kubernetes image pull secrets to sandbox pods. Referenced Secrets must exist in the sandbox namespace. | | `grpc_endpoint` | `server.grpcEndpoint` | Set the gateway callback endpoint reachable from sandbox pods. | | `client_tls_secret_name` | `server.tls.clientTlsSecretName` | Mount sandbox client TLS materials from a Kubernetes secret. | | `supervisor_image` | `supervisor.image.repository` / `supervisor.image.tag` | Set the supervisor image that provides the `openshell-sandbox` binary. | | `supervisor_image_pull_policy` | `supervisor.image.pullPolicy` | Set the Kubernetes image pull policy for the supervisor image. | | `supervisor_sideload_method` | `supervisor.sideloadMethod` | How the supervisor binary is delivered into sandbox pods. Leave empty to auto-detect from cluster version. Set to `image-volume` to mount the supervisor OCI image directly as a volume (requires Kubernetes 1.33+ with the ImageVolume feature gate; GA in 1.36), or `init-container` to copy it through an init container on older clusters. | | `app_armor_profile` | `server.appArmorProfile` | Set the sandbox agent container's AppArmor profile. Helm defaults this to `Unconfined` so AppArmor-enabled nodes do not block supervisor network namespace setup. Set the Helm value to an empty string to omit the field, or use `RuntimeDefault` or `Localhost/` for operator-managed profiles. | | `workspace_default_storage_size` | `server.workspaceDefaultStorageSize` | Set the default workspace PVC size for new sandboxes. | | `sa_token_ttl_secs` | `server.sandboxJwt.k8sSaTokenTtlSecs` | Set the projected ServiceAccount token TTL used for the bootstrap token exchange. | The Kubernetes driver creates namespaced `agents.x-k8s.io/v1alpha1` `Sandbox` resources from the Kubernetes SIG Apps [agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) project. The Agent Sandbox controller turns those resources into sandbox pods and related storage. `Sandbox.spec.volumeClaimTemplates` is immutable after creation. To change storage configuration, delete the sandbox and create a new one with the updated spec. # Gateway Configuration File > Reference for the OpenShell gateway TOML configuration file (RFC 0003). The OpenShell gateway reads its configuration from a TOML file when `--config` or `OPENSHELL_GATEWAY_CONFIG` is set. When neither is set, the gateway reads `$XDG_CONFIG_HOME/openshell/gateway.toml` if that file exists. If no config file exists, the gateway starts from built-in defaults. Gateway process flags and gateway `OPENSHELL_*` environment variables override the file. Compute driver settings live in the driver TOML tables. See [RFC 0003](https://github.com/NVIDIA/OpenShell/blob/main/rfc/0003-gateway-configuration/README.md) for the full schema. ## Source Precedence ```text Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in default ``` `database_url` is env-only. The loader rejects it when it appears in the file. When `OPENSHELL_DB_URL` is unset, the gateway stores its SQLite database under `$XDG_STATE_HOME/openshell/gateway/openshell.db`. ## Package-Managed Locations Package-managed gateways do not require a TOML file. Create one at the package's optional config location when you need to override built-in defaults. Set `OPENSHELL_GATEWAY_CONFIG` in the launch environment to use a different file. | Package | Optional Gateway TOML location | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Homebrew | `$XDG_CONFIG_HOME/openshell/gateway.toml` when it exists, otherwise an existing Homebrew prefix config such as `/opt/homebrew/var/openshell/gateway.toml`. | | Debian/Ubuntu | `$XDG_CONFIG_HOME/openshell/gateway.toml`, usually `~/.config/openshell/gateway.toml` for the systemd user service. | | Fedora/RHEL RPM | `$XDG_CONFIG_HOME/openshell/gateway.toml`, usually `~/.config/openshell/gateway.toml` for the systemd user service. | | Snap | `$SNAP_COMMON/gateway.toml`, usually `/var/snap/openshell/common/gateway.toml`. | ## Layout The file is rooted at `[openshell]`. Gateway-wide settings live under `[openshell.gateway]`. Each compute driver owns its own `[openshell.drivers.]` table. Shared keys set at gateway scope are inherited into driver tables when not overridden. ```toml [openshell] version = 1 [openshell.gateway] # ... gateway-wide settings ... [openshell.gateway.tls] # ... gateway listener TLS ... [openshell.gateway.oidc] # ... JWT bearer auth ... [openshell.drivers.kubernetes] # ... driver-specific settings ... ``` ## Full Example A complete gateway configuration covering every section. Trim to the fields you need. ```toml # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 [openshell] version = 1 [openshell.gateway] bind_address = "0.0.0.0:8080" health_bind_address = "0.0.0.0:8081" metrics_bind_address = "0.0.0.0:9090" log_level = "info" # When empty, the gateway auto-detects Kubernetes, then Podman, then Docker. # VM is never auto-detected and requires an explicit entry here. compute_drivers = ["kubernetes"] sandbox_namespace = "openshell" ssh_session_ttl_secs = 3600 # Subject Alternative Names baked into the gateway server certificate. # Wildcard DNS SANs (e.g. "*.dev.openshell.localhost") also enable sandbox # service URLs under that domain. server_sans = ["openshell", "*.dev.openshell.localhost"] # Allow plaintext HTTP routing for loopback sandbox service URLs. enable_loopback_service_http = true # Set true only for local plaintext gateways or trusted TLS termination. disable_tls = false # Shared driver defaults. These inherit into [openshell.drivers.] tables # when the driver-specific table does not override them. default_image = "ghcr.io/nvidia/openshell/sandbox:latest" supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" client_tls_secret_name = "openshell-client-tls" service_account_name = "openshell-sandbox" host_gateway_ip = "10.0.0.1" enable_user_namespaces = false sa_token_ttl_secs = 3600 guest_tls_ca = "/etc/openshell/certs/ca.pem" guest_tls_cert = "/etc/openshell/certs/client.pem" guest_tls_key = "/etc/openshell/certs/client-key.pem" # Gateway listener TLS (distinct from the per-driver guest_tls_*). [openshell.gateway.tls] cert_path = "/etc/openshell/certs/gateway.pem" key_path = "/etc/openshell/certs/gateway-key.pem" client_ca_path = "/etc/openshell/certs/client-ca.pem" require_client_auth = false [openshell.gateway.gateway_jwt] signing_key_path = "/etc/openshell/jwt/signing.pem" public_key_path = "/etc/openshell/jwt/public.pem" kid_path = "/etc/openshell/jwt/kid" gateway_id = "openshell" # Omit or set to 0 only for local single-player Docker, Podman, or VM gateways. ttl_secs = 3600 [openshell.gateway.auth] allow_unauthenticated_users = false [openshell.gateway.mtls_auth] enabled = false [openshell.gateway.oidc] issuer = "https://idp.example.com/realms/openshell" audience = "openshell-cli" jwks_ttl_secs = 3600 roles_claim = "realm_access.roles" admin_role = "openshell-admin" user_role = "openshell-user" scopes_claim = "" ``` Local Docker, Podman, and VM gateways can also set `[openshell.gateway.mtls_auth] enabled = true` to authenticate CLI callers from verified client certificates. Kubernetes deployments must leave this unset and use OIDC or a trusted access proxy; the Helm chart does not render this table. `[openshell.gateway.gateway_jwt] ttl_secs` controls gateway-minted sandbox JWT lifetime. When omitted, it defaults to `0`: the token `exp` claim and `expires_at_ms` response field become `0`, and the sandbox JWT does not expire. Use that default only for local single-player Docker, Podman, or VM gateways. Kubernetes and other shared deployments should set a positive TTL; Helm renders `3600` seconds by default, and the gateway logs a warning when a Kubernetes gateway uses `0`. `[openshell.gateway.auth] allow_unauthenticated_users = true` is an unsafe local-development and trusted-proxy escape hatch. It accepts user-facing CLI/API calls without OIDC or mTLS credentials while sandbox supervisors still authenticate with gateway-minted sandbox JWTs. Leave it false for shared and production gateways. `image_pull_policy` is intentionally not a shared gateway key. Kubernetes and Docker use `Always`, `IfNotPresent`, or `Never`. Podman uses `always`, `missing`, `never`, or `newer`. Set it inside the relevant driver table. ## Driver References Each example is a complete TOML file for one compute driver. The examples repeat `[openshell]` and `[openshell.gateway]` so they stay copyable, and the driver tables list the accepted driver-specific keys. Driver-specific values override inherited gateway defaults. The gateway rejects unknown driver fields after inheritance is merged. ### Kubernetes The gateway runs as a Pod and creates sandbox Pods in another namespace. mTLS material for sandboxes is delivered through a Kubernetes Secret rather than host-side file paths. ```toml [openshell] version = 1 [openshell.gateway] bind_address = "0.0.0.0:8080" health_bind_address = "0.0.0.0:8081" metrics_bind_address = "0.0.0.0:9090" log_level = "info" compute_drivers = ["kubernetes"] [openshell.gateway.tls] cert_path = "/etc/openshell-tls/server/tls.crt" key_path = "/etc/openshell-tls/server/tls.key" client_ca_path = "/etc/openshell-tls/client-ca/ca.crt" [openshell.drivers.kubernetes] namespace = "agents" service_account_name = "openshell-sandbox" default_image = "ghcr.io/nvidia/openshell/sandbox:latest" image_pull_policy = "IfNotPresent" image_pull_secrets = ["regcred"] supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" supervisor_image_pull_policy = "IfNotPresent" # Use the image volume on Kubernetes >= 1.35 (GA in 1.36); switch to "init-container" # on older clusters or where the ImageVolume feature gate is off. supervisor_sideload_method = "image-volume" grpc_endpoint = "https://openshell-gateway.agents.svc:8080" ssh_socket_path = "/run/openshell/ssh.sock" client_tls_secret_name = "openshell-client-tls" host_gateway_ip = "10.0.0.1" enable_user_namespaces = false app_armor_profile = "Unconfined" workspace_default_storage_size = "10Gi" # Kubernetes RuntimeClass applied to sandbox pods when the API request does # not specify one. Empty (default) = omit the field, using the cluster default. # default_runtime_class_name = "kata-containers" # Kubelet clamps projected tokens below 600 seconds. The driver caps values at 86400. sa_token_ttl_secs = 3600 ``` ### Docker Sandboxes run as containers on a local bridge network. The supervisor binary is bind-mounted from the host (no in-cluster image pull required); guest mTLS material is supplied as host paths. ```toml [openshell] version = 1 [openshell.gateway] bind_address = "127.0.0.1:17670" log_level = "info" compute_drivers = ["docker"] [openshell.drivers.docker] default_image = "ghcr.io/nvidia/openshell/sandbox:latest" # Docker vocabulary: Always | IfNotPresent | Never. Empty behaves like IfNotPresent. image_pull_policy = "IfNotPresent" sandbox_namespace = "docker-dev" # Empty auto-detects https://host.openshell.internal: when guest TLS is set. grpc_endpoint = "https://host.openshell.internal:17670" # Skip the image-pull-and-extract step by pointing at a locally built binary. supervisor_bin = "/usr/local/libexec/openshell/openshell-sandbox" supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" guest_tls_ca = "/etc/openshell/certs/ca.pem" guest_tls_cert = "/etc/openshell/certs/client.pem" guest_tls_key = "/etc/openshell/certs/client-key.pem" network_name = "openshell-docker" host_gateway_ip = "172.17.0.1" ssh_socket_path = "/run/openshell/ssh.sock" # Set to 0 to leave Docker's runtime default unchanged. sandbox_pids_limit = 2048 ``` ### Podman Sandboxes run as Podman containers on a user-mode bridge network. The supervisor image is mounted read-only via Podman's `type=image` mount; guest mTLS material is supplied as host paths. ```toml [openshell] version = 1 [openshell.gateway] bind_address = "127.0.0.1:17670" log_level = "info" compute_drivers = ["podman"] [openshell.drivers.podman] # Rootless socket path. For root Podman use /run/podman/podman.sock. socket_path = "/run/user/1000/podman/podman.sock" default_image = "ghcr.io/nvidia/openshell/sandbox:latest" image_pull_policy = "missing" # always | missing | never | newer grpc_endpoint = "https://host.containers.internal:17670" # The gateway overwrites gateway_port from bind_address at runtime. gateway_port = 17670 network_name = "openshell" # Omit for the platform default: empty on Linux, 192.168.127.254 on macOS Podman machine. # Set "" to force Podman's host-gateway resolver. # host_gateway_ip = "192.168.127.254" sandbox_ssh_socket_path = "/run/openshell/ssh.sock" stop_timeout_secs = 10 supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" guest_tls_ca = "/etc/openshell/certs/ca.pem" guest_tls_cert = "/etc/openshell/certs/client.pem" guest_tls_key = "/etc/openshell/certs/client-key.pem" # Set to 0 to leave Podman's runtime default unchanged. sandbox_pids_limit = 2048 ``` ### MicroVM Each sandbox runs inside its own libkrun microVM managed by the standalone `openshell-driver-vm` subprocess. Use this driver when you want stronger isolation than container namespaces alone. ```toml [openshell] version = 1 [openshell.gateway] bind_address = "127.0.0.1:17670" log_level = "info" # VM is never auto-detected; an explicit entry here is required. compute_drivers = ["vm"] [openshell.drivers.vm] state_dir = "/var/lib/openshell/vm" # Where the gateway looks for the openshell-driver-vm subprocess binary. driver_dir = "/usr/local/libexec/openshell" default_image = "ghcr.io/nvidia/openshell/sandbox:latest" grpc_endpoint = "https://host.containers.internal:17670" # Empty falls back to default_image. bootstrap_image = "ghcr.io/nvidia/openshell/sandbox:latest" krun_log_level = 1 vcpus = 2 mem_mib = 2048 overlay_disk_mib = 4096 guest_tls_ca = "/var/lib/openshell/guest-tls/ca.pem" guest_tls_cert = "/var/lib/openshell/guest-tls/client.pem" guest_tls_key = "/var/lib/openshell/guest-tls/client-key.pem" ``` # Support Matrix This page lists the host platform, compute driver, software, runtime, and kernel requirements for running OpenShell. ## Supported Platforms OpenShell publishes multi-architecture gateway images for `linux/amd64` and `linux/arm64`. The CLI, package-managed gateway, and standalone gateway binary are supported on the following host platforms: | Platform | Architecture | Status | | -------------------------------- | --------------------- | ------------ | | Linux (Debian/Ubuntu) | x86\_64 (amd64) | Supported | | Linux (Debian/Ubuntu) | aarch64 (arm64) | Supported | | macOS (Docker Desktop) | Apple Silicon (arm64) | Supported | | Windows (WSL 2 + Docker Desktop) | x86\_64 | Experimental | On Linux, the `openshell` CLI is a static musl binary and does not require glibc at runtime. ## Standalone Gateway Binary OpenShell publishes standalone `openshell-gateway` release assets for manual download on these platforms: | Platform | Artifact pattern | | --------------------- | --------------------------------------------- | | Linux x86\_64 (amd64) | `openshell-gateway-x86_64-unknown-linux-gnu` | | Linux aarch64 (arm64) | `openshell-gateway-aarch64-unknown-linux-gnu` | | macOS Apple Silicon | `openshell-gateway-aarch64-apple-darwin` | These artifacts are attached to GitHub releases. Kubernetes deployments should use the Helm chart and the published gateway image. On Linux, `openshell-gateway` requires glibc 2.31 or newer. Compatible systems include, for example, Ubuntu 20.04+, RHEL 9+, Amazon Linux 2023+, and Fedora 32+. ## Compute Drivers The gateway can manage sandboxes through several compute drivers. | Compute Driver | Status | Notes | | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Docker | Supported for local development and single-machine gateways. | Requires Docker Desktop or Docker Engine on the gateway host. | | Podman | Supported for rootless local and workstation workflows. | Requires a Podman-compatible socket and rootless networking setup. | | Kubernetes | Supported through the [OpenShell Helm chart](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). | Requires a Kubernetes cluster supplied by the operator. | | MicroVM | Supported for VM-backed sandboxes. | Uses the VM compute driver and libkrun-based runtime. | ## Software Prerequisites Install the software for the compute driver you use: | Component | Minimum Version | Notes | | ------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------- | | Docker Desktop or Docker Engine | 28.04 | Required for Docker-backed gateways, local image builds, and Docker development workflows. | | Podman | 5.x | Required for Podman-backed gateways. | | Kubernetes | 1.29 | Required for Helm deployments and Kubernetes sandbox scheduling. | | Helm | 3.x | Required to install `deploy/helm/openshell`. | | kubectl | Compatible with your cluster | Required for Kubernetes operational inspection and secret creation. | | Host virtualization | Host dependent | Required for MicroVM-backed gateways. MicroVM uses Hypervisor.framework on macOS and KVM on Linux. | ## Sandbox Runtime Versions Sandbox container images are maintained in the [openshell-community](https://github.com/nvidia/openshell-community) repository. Refer to that repository for the current list of installed components and their versions. ## Container Images OpenShell publishes the gateway image for `linux/amd64` and `linux/arm64`. | Image | Reference | Pulled When | | ------- | ----------------------------------------- | ----------------------------------------------------------------- | | Gateway | `ghcr.io/nvidia/openshell/gateway:latest` | Helm chart install or upgrade, or standalone container deployment | The Helm chart in `deploy/helm/openshell` deploys the gateway StatefulSet, service account, service, persistent storage, and network policy for Kubernetes. Sandbox images are maintained separately in the [openshell-community](https://github.com/nvidia/openshell-community) repository. To override the default image references, use Helm values: | Helm value | Purpose | | -------------------------------- | ------------------------------------- | | `image.repository` / `image.tag` | Override the gateway image reference. | | `server.sandboxImage` | Override the default sandbox image. | ## Kernel Requirements OpenShell enforces sandbox isolation through two Linux kernel security modules: | Module | Requirement | Details | | -------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Landlock LSM](https://docs.kernel.org/security/landlock.html) | Recommended | Enforces filesystem access restrictions at the kernel level. The `best_effort` compatibility mode uses the highest Landlock ABI the host kernel supports. The `hard_requirement` mode fails sandbox creation if the required ABI is unavailable. | | seccomp | Required | Filters dangerous system calls. Available on all modern Linux kernels (3.17+). | On macOS, these kernel modules run inside the Docker Desktop Linux VM, not on the host kernel. ## Agent Compatibility For the full list of supported agents and their default policy coverage, refer to the [Supported Agents](/about/supported-agents) page. # OpenShell Security Best Practices for Controls, Risks, and Configuration Guidance > A guide to every configurable security control in OpenShell: defaults, what you can change, and the risks of each choice. OpenShell enforces sandbox security across four layers: network, filesystem, process, and inference. This page documents every configurable control, its default, what it protects, and the risk of relaxing it. For the full policy YAML schema, refer to the [Policy Schema](/reference/policy-schema). For the architecture of each enforcement layer, refer to [How OpenShell Works](/about/how-it-works). If you use [NemoClaw](https://github.com/NVIDIA/NemoClaw), its [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) guide covers additional entrypoint-level controls, policy presets, provider trust tiers, and posture profiles specific to the NemoClaw blueprint. ## Enforcement Layers OpenShell applies security controls at two enforcement points. OpenShell locks static controls at sandbox creation and requires destroying and recreating the sandbox to change them. You can update dynamic controls on a running sandbox with `openshell policy update` or `openshell policy set`. | Layer | What it protects | Enforcement point | Changeable at runtime | | ---------- | --------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------- | | Network | Unauthorized outbound connections and data exfiltration. | CONNECT proxy + OPA policy engine | Yes. Use `openshell policy update`, `openshell policy set`, or operator approval in the TUI. | | Filesystem | System binary tampering, credential theft, config manipulation. | Landlock LSM (kernel level) | No. Requires sandbox re-creation. | | Process | Privilege escalation, fork bombs, dangerous syscalls. | Seccomp BPF + privilege drop (`setuid`/`setgid`) | No. Requires sandbox re-creation. | | Inference | Credential exposure, unauthorized model access. | Proxy intercept of `inference.local` | Yes. Use `openshell inference set`. | ## Network Controls The CONNECT proxy and OPA policy engine enforce all network controls at the gateway level. ### Deny-by-Default Egress Every outbound connection from the sandbox goes through the CONNECT proxy. The proxy evaluates each connection against the OPA policy engine. If no `network_policies` entry matches the destination host, port, and calling binary, the proxy denies the connection. | Aspect | Detail | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | All egress denied. Only endpoints listed in `network_policies` can receive traffic. | | What you can change | Add entries to `network_policies` in the policy YAML. Apply statically at creation (`--policy`) or dynamically (`openshell policy update` for incremental changes, `openshell policy set` for full replacement). | | Risk if relaxed | Each allowed endpoint is a potential data exfiltration path. The agent can send workspace content, credentials, or conversation history to any reachable host. | | Recommendation | Add only endpoints the agent needs for its task. Start with a minimal policy and use denied-request logs (`openshell logs --source sandbox`) to identify missing endpoints. | ### Network Namespace Isolation The sandbox runs in a dedicated Linux network namespace with a veth pair. All traffic routes through the host-side veth IP (`10.200.0.1`) where the proxy listens. Even if a process ignores proxy environment variables, it can only reach the proxy. | Aspect | Detail | | ------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Default | Always active. The sandbox cannot bypass the proxy at the network level. | | What you can change | This is not a user-facing knob. OpenShell always enforces it in proxy mode. | | Risk if bypassed | Without network namespace isolation, a process could connect directly to the internet, bypassing all policy enforcement. | | Recommendation | No action needed. OpenShell enforces this automatically. | ### User Namespace Isolation Kubernetes user namespaces (`hostUsers: false`) map container UID 0 to an unprivileged host UID. Capabilities like `CAP_SYS_ADMIN` become namespaced. They grant power over container-local resources only, not the host. This provides defense-in-depth: even if a container escape vulnerability exists, the attacker lands as an unprivileged host user. | Aspect | Detail | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Disabled. Set `server.enableUserNamespaces: true` in Helm values or `enable_user_namespaces = true` in the gateway config to enable cluster-wide. | | What you can change | Enable cluster-wide through Helm or gateway config. Override per-sandbox through the `user_namespaces` field on `SandboxTemplate` in the API. | | Prerequisites | Kubernetes 1.33+ with user namespace support available (beta through 1.35, GA in 1.36+), a container runtime that supports user namespaces (containerd 2.0+, CRI-O 1.25+), and Linux 5.12+ for ID-mapped mounts. | | Risk if enabled with GPU | NVIDIA device plugin compatibility with user namespaces is unverified. OpenShell logs a warning when both GPU and user namespaces are active on the same sandbox. | | Recommendation | Enable on non-GPU clusters running Kubernetes with user namespace support available (1.33+ beta, 1.36+ GA) for stronger host isolation. Test GPU workloads separately before enabling on GPU clusters. | ### Binary Identity Binding The proxy identifies which binary initiated each connection by reading `/proc//exe` (the kernel-trusted executable path). It walks the process tree for ancestor binaries and parses `/proc//cmdline` for script interpreters. The proxy SHA256-hashes each binary on first use (trust-on-first-use). If someone replaces a binary mid-session, the hash mismatch triggers an immediate deny. | Aspect | Detail | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Every `network_policies` entry requires a `binaries` list. Only listed binaries can reach the associated endpoints. Binary paths support glob patterns (`*` for one path component, `**` for recursive). | | What you can change | Add binaries to an endpoint entry. Use glob patterns for directory-scoped access (for example, `/sandbox/.vscode-server/**`). | | Risk if relaxed | Broad glob patterns (like `/**`) allow any binary to reach the endpoint, defeating the purpose of binary-scoped enforcement. | | Recommendation | Scope binaries to the specific executables that need each endpoint. Use narrow globs when the exact path varies (for example, across Python virtual environments). | ### L4-Only vs L7 Inspection The `protocol` field on an endpoint controls whether the proxy inspects individual HTTP requests inside the tunnel. | Aspect | Detail | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Endpoints without a `protocol` field use L4-only enforcement: the proxy checks host, port, and binary, then relays the TCP stream without inspecting payloads. | | What you can change | Add `protocol: rest` to enable per-request HTTP method/path inspection, `protocol: websocket` to inspect RFC 6455 upgrade handshakes and client text messages, or `protocol: graphql` to inspect GraphQL-over-HTTP operation type, operation name, and root fields. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket messages. Pair inspected protocols with `rules` or access presets (`full`, `read-only`, `read-write`). REST endpoints that need credential placeholders in supported text request bodies can set `request_body_credential_rewrite: true`. | | Risk if relaxed | L4-only endpoints allow the agent to send any data through the tunnel after the initial connection is permitted. The proxy cannot see HTTP methods, paths, or GraphQL operations. Adding `access: full` with L7 inspection enables observability but permits all inspected actions. | | Recommendation | Use `protocol: rest` with specific `rules` for APIs where intent is encoded in method and path. Add `request_body_credential_rewrite: true` only for REST APIs that require OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. Use `protocol: graphql` for GraphQL-over-HTTP APIs where destructive operations are body-encoded. Use `protocol: websocket` for RFC 6455 endpoints, with explicit `GET` and `WEBSOCKET_TEXT` rules for raw text protocols or explicit GraphQL operation rules for GraphQL-over-WebSocket. Prefer `access: read-only` or explicit allowlists, and deny hash-only persisted queries unless you maintain a trusted registry. Omit `protocol` for non-HTTP protocols. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. | ### Enforcement Mode (`audit` vs `enforce`) When L7 inspection is active, the `enforcement` field controls whether the proxy blocks or logs rule violations. | Aspect | Detail | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | `audit`. The proxy logs violations but forwards traffic. | | What you can change | Set `enforcement: enforce` to block requests that do not match any `rules` entry. Denied requests receive a `403 Forbidden` response with a JSON body describing the violation. | | Risk if relaxed | `audit` mode provides visibility but does not prevent unauthorized actions. An agent can still perform write or delete operations on an API even if the rules would deny them. | | Recommendation | Start with `audit` to understand traffic patterns and verify that rules are correct. Switch to `enforce` after you validate that the rules match the intended access pattern. | ### TLS Handling The proxy auto-detects TLS on every tunnel by peeking the first bytes. When a TLS ClientHello is detected, the proxy terminates TLS transparently using a per-sandbox ephemeral CA. This enables credential injection and L7 inspection without explicit configuration. | Aspect | Detail | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Auto-detect and terminate. OpenShell generates the sandbox CA at startup and injects it into the process trust stores (`NODE_EXTRA_CA_CERTS`, `DENO_CERT`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `GIT_SSL_CAINFO`). | | What you can change | Set `tls: skip` on an endpoint to disable TLS detection and termination for that endpoint. Use this for client-certificate mTLS to upstream or non-standard binary protocols. | | Risk if relaxed | `tls: skip` disables credential injection and L7 inspection for that endpoint. The proxy relays encrypted traffic without seeing the contents. | | Recommendation | Use auto-detect (the default) for most endpoints. Use `tls: skip` only when the upstream requires the client's own TLS certificate (mTLS) or uses a non-HTTP protocol. | ### SSRF Protection After OPA policy allows a connection, the proxy resolves DNS and rejects undeclared internal destinations. | Aspect | Detail | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Default | The proxy blocks private IPs for undeclared, wildcard, hostless, and policy-advisor-proposed endpoints. Exact hostnames declared in user policy may resolve to private RFC 1918 addresses. Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), and unspecified (`0.0.0.0`) addresses are always blocked and cannot be overridden with `allowed_ips`. | | What you can change | Declare a known internal service as an exact `host` endpoint, or add `allowed_ips` (CIDR notation) to an endpoint to permit specific private IP ranges for wildcard or hostless policies. On Linux, the proxy consults the sandbox's `/etc/hosts` before DNS, so Kubernetes `hostAliases` can make LAN-only hostnames resolvable. Policies with `allowed_ips` entries that overlap loopback, link-local, or unspecified addresses fail to load with a clear validation error. | | Risk if relaxed | Without SSRF protection, a misconfigured policy could allow the agent to reach cloud metadata services (`169.254.169.254`), internal databases, or other infrastructure endpoints through DNS rebinding. | | Recommendation | Prefer exact hostname endpoints for stable internal services. Use `allowed_ips` when you need hostless or wildcard authorization, and scope the CIDR as narrowly as possible (for example, `10.0.5.20/32` for a single host). Loopback, link-local, and unspecified addresses are always blocked regardless of `allowed_ips`. `hostAliases` change resolution, not authorization: `host: searxng.local` with `/etc/hosts` mapping to `192.168.1.105` is trusted only when that exact hostname is declared by user policy. The policy advisor does not propose rules for always-blocked destinations and still requires a separate `allowed_ips` edit for private-IP endpoints. | ### Operator Approval When the agent requests an endpoint not in the policy, OpenShell blocks it and surfaces the request in the TUI for operator review. The system merges approved endpoints into the sandbox's policy as a new durable revision. | Aspect | Detail | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Default | Enabled. The proxy blocks unlisted endpoints and requires approval. | | What you can change | Approved endpoints persist across sandbox restarts within the same sandbox instance. They reset when the sandbox is destroyed and recreated. | | Risk if relaxed | Approving an endpoint permanently widens the running sandbox's policy. Review each request before approving. | | Recommendation | Use operator approval for exploratory work. For recurring endpoints, add them to the policy YAML with appropriate binary and path restrictions. To reset all approved endpoints, destroy and recreate the sandbox. | ## Filesystem Controls Landlock LSM restricts which paths the sandbox process can read or write at the kernel level. ### Landlock LSM Landlock enforces filesystem access at the kernel level. Paths listed in `read_only` receive read-only access. Paths listed in `read_write` receive full access. All other paths are inaccessible. Landlock setup runs in two phases. The parent supervisor probes the kernel ABI and opens the configured path file descriptors before forking. The child then applies the ruleset with `restrict_self()` after privilege drop. At startup, OpenShell emits the selected ABI version and the applied read-only and read-write rule counts so you can confirm what the kernel accepted. | Aspect | Detail | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | `compatibility: best_effort`. Uses the highest kernel ABI available. Missing paths are skipped. If the kernel does not support Landlock or any configured path cannot be opened, the sandbox continues without those restrictions and emits a High-severity OCSF `DetectionFinding`. | | What you can change | Set `compatibility: hard_requirement` to abort sandbox startup if Landlock is unavailable or any configured path cannot be opened. | | Risk if relaxed | On kernels without Landlock (pre-5.13), or when all paths fail to open, the sandbox runs without kernel-level filesystem restrictions. The agent can access any file the process user can access. | | Recommendation | Use `best_effort` for development. Use `hard_requirement` in environments where any gap in filesystem isolation is unacceptable. Treat High-severity Landlock findings as a signal to investigate the host kernel or the image. Run on Ubuntu 22.04+ or any kernel 5.13+ for Landlock support. | ### Read-Only vs Read-Write Paths The policy separates filesystem paths into read-only and read-write groups. | Aspect | Detail | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | System paths (`/usr`, `/lib`, `/etc`, `/var/log`) are read-only. Working paths (`/sandbox`, `/tmp`) are read-write. `/app` is conditionally included if it exists. | | What you can change | Add or remove paths in `filesystem_policy.read_only` and `filesystem_policy.read_write`. | | Risk if relaxed | Making system paths writable lets the agent replace binaries, modify TLS trust stores, or change DNS resolution. Validation rejects broad read-write paths (like `/`). | | Recommendation | Keep system paths read-only. If the agent needs additional writable space, add a specific subdirectory. | ### Path Validation OpenShell validates policies before they take effect. | Constraint | Behavior | | ------------------------------------------------------------------- | --------------------------------- | | Paths must be absolute (start with `/`). | Rejected with `INVALID_ARGUMENT`. | | Paths must not contain `..` traversal. | Rejected with `INVALID_ARGUMENT`. | | Read-write paths must not be overly broad (for example, `/` alone). | Rejected with `INVALID_ARGUMENT`. | | Each path must not exceed 4096 characters. | Rejected with `INVALID_ARGUMENT`. | | Combined `read_only` + `read_write` paths must not exceed 256. | Rejected with `INVALID_ARGUMENT`. | ## Process Controls The sandbox supervisor drops privileges, applies seccomp filters, and enforces process-level restrictions during startup. ### Privilege Drop The sandbox process runs as a non-root user after explicit privilege dropping. | Aspect | Detail | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | `run_as_user: sandbox`, `run_as_group: sandbox`. The supervisor calls `setuid()`/`setgid()` with post-condition verification, disables core dumps with `RLIMIT_CORE=0`, and on Linux sets `PR_SET_DUMPABLE=0`. | | What you can change | Set `run_as_user` and `run_as_group` in the `process` section. Validation rejects root (`root` or `0`). | | Risk if relaxed | Running as a higher-privilege user increases the impact of container escape vulnerabilities. | | Recommendation | Keep the `sandbox` user. Do not attempt to set root. | ### Seccomp Filters OpenShell applies seccomp in two phases. A narrow supervisor-startup prelude runs before CLI parsing and async runtime initialization, then the child process receives the broader runtime seccomp filter after privilege drop. | Aspect | Detail | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Startup prelude | After privileged bootstrap helpers complete, the supervisor sets `PR_SET_NO_NEW_PRIVS` and synchronizes a seccomp filter across all runtime threads that blocks `mount`, the new mount API syscalls, `pivot_root`, `umount2`, `bpf`, `perf_event_open`, `userfaultfd`, module-loading syscalls, and kexec. This closes the long-lived privileged remount and kernel-surface window while leaving required setup syscalls such as `setns` available. | | Socket domains | The filter allows `AF_INET` and `AF_INET6` (for proxy communication) and blocks `AF_PACKET`, `AF_BLUETOOTH`, and `AF_VSOCK` with `EPERM`. `AF_NETLINK` is partially allowed: only `NETLINK_ROUTE` (protocol 0) is permitted so that `getifaddrs(3)` works; all other netlink protocols are blocked. Write operations via `NETLINK_ROUTE` still require `CAP_NET_ADMIN`, which the sandbox does not grant. | | Runtime unconditional syscall blocks | `memfd_create`, `ptrace`, `bpf`, `process_vm_readv`, `process_vm_writev`, `pidfd_open`, `pidfd_getfd`, `pidfd_send_signal`, `io_uring_setup`, `mount`, `fsopen`, `fsconfig`, `fsmount`, `fspick`, `move_mount`, `open_tree`, `setns`, `umount2`, `pivot_root`, `userfaultfd`, `perf_event_open`. | | Conditional syscall blocks | `execveat` with `AT_EMPTY_PATH`, `unshare` and `clone` with `CLONE_NEWUSER`, and `seccomp(SECCOMP_SET_MODE_FILTER)` are denied with `EPERM`. | | What you can change | This is not a user-facing knob. OpenShell enforces it automatically. | | Risk if relaxed | The blocked syscalls support container escape (`mount`, `pivot_root`, `move_mount`, namespace creation), cross-process observation (`ptrace`, `process_vm_readv`, `pidfd_*`), raw kernel bypass (`bpf`, `io_uring_setup`, `perf_event_open`), and filter evasion (`seccomp`, `userfaultfd`). | | Recommendation | No action needed. OpenShell enforces this automatically. | ### Enforcement Application Order The sandbox supervisor applies enforcement in a specific order during process startup. This ordering is intentional: named network-namespace setup still relies on privileged helpers, and privilege dropping still needs `/etc/group` and `/etc/passwd`, which Landlock subsequently restricts. 1. Privileged supervisor bootstrap helpers, including network-namespace setup and optional `nft` probes. 2. Supervisor startup prelude seccomp (`PR_SET_NO_NEW_PRIVS` plus the early syscall denylist) synchronized across runtime threads. 3. Network namespace entry (`setns`) in child `pre_exec`. 4. Privilege drop (`initgroups` + `setgid` + `setuid`). 5. Core-dump hardening (`RLIMIT_CORE=0`, plus `PR_SET_DUMPABLE=0` on Linux). 6. Landlock filesystem restrictions. 7. Runtime seccomp socket domain and syscall filters. ## Inference Controls OpenShell routes all inference traffic through the gateway to isolate provider credentials from the sandbox. ### Routed Inference through `inference.local` The proxy intercepts HTTPS CONNECT requests to `inference.local` and routes matching inference API requests through the sandbox-local router. The agent never receives the provider API key. | Aspect | Detail | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Always active. The proxy handles `inference.local` before OPA policy evaluation. The gateway injects credentials on the host side. | | Keep-alive isolation | If a sandbox reuses a keep-alive connection that previously carried a routed inference request for a subsequent non-inference request, the proxy denies the non-inference request with `connection not allowed by policy` and closes the connection. This prevents agents from reusing an inference-authorized connection for other destinations. | | What you can change | Configure inference routes with `openshell inference set`. | | Risk if bypassed | If an inference provider's host is added directly to `network_policies`, the agent could reach it with a stolen or hardcoded key, bypassing credential isolation. | | Recommendation | Do not add inference provider hosts to `network_policies`. Use OpenShell inference routing instead. | ## Gateway Security The gateway secures communication between the CLI, sandbox workloads, and external clients with mutual TLS and token-based authentication. ### mTLS Gateway transport uses TLS, with client certificate checks available where the deployment provides a client CA. Local single-user Docker, Podman, and VM gateways can use the verified client certificate as user authentication. Kubernetes deployments use the certificate bundle for transport and sandbox supervisor connectivity only; configure OIDC or a trusted access proxy for user authentication. | Aspect | Detail | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Default | Local TLS bundles enable mTLS user authentication for single-user local gateways. Helm deployments generate mTLS certificates for transport, while sandbox supervisors authenticate API calls with gateway-minted sandbox JWTs. TLS-enabled loopback gateways also accept plaintext HTTP for sandbox service hostnames by default. | | What you can change | Configure OIDC or a trusted access proxy for multi-user gateways, set `OPENSHELL_ENABLE_MTLS_AUTH=true` for local single-user gateways, enable `server.auth.allowUnauthenticatedUsers=true` only for trusted local Kubernetes development or a fully trusted proxy, disable TLS only for trusted reverse-proxy setups, or disable loopback service HTTP with `--enable-loopback-service-http=false`. | | Risk if relaxed | Disabling TLS removes transport-level protection entirely. Allowing unauthenticated users removes the gateway user-auth boundary and must not be exposed to shared or public networks. Treating transport certificates as shared user identity in Kubernetes would collapse user and sandbox trust boundaries. Loopback service HTTP is local-only and rejects cross-origin browser requests, but any local process can still reach exposed service URLs directly. | | Recommendation | Use local mTLS user authentication only for single-user Docker, Podman, and VM gateways. Use OIDC or a trusted access proxy for Kubernetes and shared deployments. | ### SSH Tunnel Authentication SSH connections to sandboxes travel through the gateway over the sandbox supervisor's authenticated control path. Each SSH connect call also carries a short-lived session token scoped to a specific sandbox. The sandbox never exposes an SSH port on the network. Its SSH daemon listens on a local Unix socket that only the sandbox's own supervisor process can reach. | Aspect | Detail | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Default | Session tokens expire after 24 hours. Concurrent connections are limited to 10 per token and 20 per sandbox. | | What you can change | Configure `ssh_session_ttl_secs`. Set to 0 for no expiry. | | Risk if relaxed | Longer TTLs or no expiry increase the window for stolen token reuse. Higher connection limits increase the blast radius of a compromised token. | | Recommendation | Keep the 24-hour default. Monitor connection counts through the TUI. | ## Common Mistakes The following patterns weaken security without providing meaningful benefit. | Mistake | Why it matters | What to do instead | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Omitting an inspected protocol on REST or WebSocket API endpoints | Without `protocol: rest` or `protocol: websocket`, the proxy uses L4-only enforcement. It allows the TCP stream through after checking host, port, and binary, but cannot inspect individual HTTP requests or WebSocket text messages. | Add `protocol: rest` or `protocol: websocket` with specific `rules` to enable method and path control. | | Using `access: full` when finer rules would suffice | `access: full` with `protocol: rest` or `protocol: websocket` enables inspection but allows all methods and paths for that protocol. | Use `access: read-only` or explicit `rules` to restrict what the agent can do at the L7 level. | | Adding endpoints permanently when operator approval would suffice | Adding endpoints to the policy YAML makes them permanently reachable across all instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset on re-creation. | | Using broad binary globs | A glob like `/**` allows any binary to reach the endpoint, defeating binary-scoped enforcement. | Scope globs to specific directories (for example, `/sandbox/.vscode-server/**`). | | Skipping TLS termination on HTTPS APIs | Setting `tls: skip` disables credential injection and L7 inspection. | Use the default auto-detect behavior unless the upstream requires client-certificate mTLS. | | Setting `enforcement: enforce` before auditing | Jumping to `enforce` without first running in `audit` mode risks breaking the agent's workflow. | Start with `audit`, review the logs, and switch to `enforce` after you validate the rules. | ## Related Topics * [Policies](/sandboxes/policies) for applying and iterating on sandbox policies. * [Policy Schema](/reference/policy-schema) for the full field-by-field YAML reference. * [Default Policy](/reference/default-policy) for the built-in default policy breakdown. * [Gateway Auth](/reference/gateway-auth) for gateway authentication details. * [How OpenShell Works](/about/how-it-works) for the system architecture. * NemoClaw [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) for entrypoint-level controls (capability drops, PATH hardening, build toolchain removal), policy presets, provider trust tiers, and posture profiles. # License > NVIDIA OpenShell is licensed under the Apache License, Version 2.0. NVIDIA OpenShell is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). Apache License Version 2.0, January 2004 [http://www.apache.org/licenses/](http://www.apache.org/licenses/) TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to the Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by the Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding any notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2025-2026 NVIDIA CORPORATION & AFFILIATES Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.