Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
[![PyPI](https://img.shields.io/badge/PyPI-openshell-orange?logo=pypi)](https://pypi.org/project/openshell/)
[![Security Policy](https://img.shields.io/badge/Security-Report%20a%20Vulnerability-red)](SECURITY.md)
[![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://docs.nvidia.com/openshell/latest/index.html)
[![Project Status](https://img.shields.io/badge/status-alpha-orange)](https://docs.nvidia.com/openshell/latest/about/release-notes.html)
[![Project Status](https://img.shields.io/badge/status-alpha-orange)](https://github.com/NVIDIA/OpenShell/releases)

> [!IMPORTANT]
> **OpenShell 0.1.0 is coming soon.** [Track progress in the 0.1.0 milestone](https://github.com/NVIDIA/OpenShell/milestone/10), [read the prerelease documentation](https://docs.nvidia.com/openshell/dev/index.html), or [install a prerelease](#prerelease-and-development-builds).
> **New in OpenShell 0.1.0:** a stable release cadence, an improved security model, an expanded extension surface, and new APIs. [Read the 0.1.0 upgrade guide](https://docs.nvidia.com/openshell/latest/upgrade/0-1-0).

OpenShell is the safe, private runtime for autonomous AI agents. It provides sandboxed execution environments that protect your data, credentials, and infrastructure — governed by declarative YAML policies that prevent unauthorized file access, data exfiltration, and uncontrolled network activity.

OpenShell is built agent-first. It ships public agent skills for using and operating OpenShell, plus separate repository-aware workflows for contributors and maintainers.

## Quickstart
## Install OpenShell

### Prerequisites

Expand Down Expand Up @@ -190,20 +190,13 @@ Docker-backed GPU sandboxes auto-select CDI when available and otherwise fall ba

**Requirements:** NVIDIA drivers and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) must be installed on the host. The sandbox image itself must include the appropriate GPU drivers and libraries for your workload — the default Ubuntu image does not. See the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for building a custom sandbox image with GPU support.

## Supported Agents
## Running Agents

OpenShell can run Linux agents packaged in OCI images. The default Ubuntu
workload does not bundle agent CLIs. Build or select an image containing your
agent, then authorize its binary paths, service endpoints, and credentials.

| Agent | Integration |
| ----- | ----------- |
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | Package Claude Code in a workload image and attach a `claude-code` provider or another endpoint-bearing model profile. |
| [OpenCode](https://opencode.ai/) | Package OpenCode in a workload image and attach its model provider and policy. |
| [Codex](https://developers.openai.com/codex) | Package Codex in a workload image and attach an OpenAI provider and policy. |
| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | Package the CLI in a workload image and attach GitHub credentials and policy. |
| [OpenClaw](https://openclaw.ai/) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. |
| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. |
agent, then authorize its binary paths, service endpoints, and credentials. See
[Run Your First Agent](https://docs.nvidia.com/openshell/latest/about/run-an-agent)
for the image, provider, and policy workflow.

## Key Commands

Expand Down Expand Up @@ -284,7 +277,7 @@ Agent implementation is human-directed: a user may request a phase directly, or
## Learn More

- [Full Documentation](https://docs.nvidia.com/openshell/latest/index.html) — overview, architecture, tutorials, and reference
- [Quickstart](https://docs.nvidia.com/openshell/latest/get-started/quickstart) — detailed install and first sandbox walkthrough
- [Run Your First Agent](https://docs.nvidia.com/openshell/latest/about/run-an-agent) — prepare an image, attach providers, and launch an agent
- [GitHub Sandbox Tutorial](https://docs.nvidia.com/openshell/latest/get-started/tutorials/github-sandbox) — end-to-end scoped GitHub repo access
- [Architecture](https://github.com/NVIDIA/OpenShell/tree/main/architecture) — detailed architecture docs and design decisions
- [Roadmap](https://github.com/orgs/NVIDIA/projects/233) — planned work and project priorities
Expand Down
40 changes: 37 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,40 @@ TAG=0.0.115
skopeo inspect "docker://ghcr.io/nvidia/openshell/gateway:${TAG}"
```

`IMAGE_TAG` sets only the gateway/supervisor image; the CLI under test is always
built from your branch. To validate against images from your exact commit
instead, build and push them and point `OPENSHELL_REGISTRY`/`IMAGE_TAG` at them.
`IMAGE_TAG` sets the default tag for the gateway/supervisor image pair; the CLI
under test is always built from your branch. To validate against images from
your exact commit instead, build and push them and point
`OPENSHELL_REGISTRY`/`IMAGE_TAG` at them.

Test wrappers accept independent image overrides:

```shell
GATEWAY_IMAGE=registry.example.com/custom/gateway:test \
SUPERVISOR_IMAGE=registry.example.com/custom/supervisor:test \
SANDBOX_IMAGE=registry.example.com/custom/sandbox:test \
mise run e2e:kubernetes
```

`GATEWAY_IMAGE` applies to the Kubernetes gateway container. `SUPERVISOR_IMAGE`
applies to the trusted supervisor image selected by the Kubernetes, Docker, and
Podman wrappers. `SANDBOX_IMAGE` applies to the trusted workload-side runtime
image that stages the `openshell-sandbox` binary. A repository-only value
inherits `IMAGE_TAG`; a value with an explicit tag or `@sha256:` digest is used
as-is. When these variables are unset, the existing `OPENSHELL_REGISTRY` plus
`IMAGE_TAG` behavior is retained.
The Docker and Podman wrappers continue to give
`OPENSHELL_DOCKER_SUPERVISOR_IMAGE` and `OPENSHELL_SUPERVISOR_IMAGE` precedence
over `SUPERVISOR_IMAGE`.

Digest-pinned Kubernetes overrides require disabling local image builds, because
Docker cannot tag a locally built image with a digest reference:

```shell
OPENSHELL_E2E_KUBE_BUILD_IMAGES=0 \
GATEWAY_IMAGE=registry.example.com/custom/gateway@sha256:<digest> \
SUPERVISOR_IMAGE=registry.example.com/custom/supervisor@sha256:<digest> \
mise run e2e:kubernetes
```

Available task variants:

Expand All @@ -386,6 +417,9 @@ Kubernetes e2e environment variables:
| `OPENSHELL_E2E_KUBERNETES_FEATURES` | Cargo feature flags (default: `e2e,e2e-host-gateway,e2e-kubernetes`) |
| `IMAGE_TAG` | Gateway/supervisor image tag (default: `latest` for existing clusters) |
| `OPENSHELL_REGISTRY` | Image registry prefix (default: `ghcr.io/nvidia/openshell`) |
| `GATEWAY_IMAGE` | Kubernetes gateway image repository or complete tagged/digest-pinned image reference; digests require `OPENSHELL_E2E_KUBE_BUILD_IMAGES=0` |
| `SUPERVISOR_IMAGE` | Gateway/supervisor image repository or complete tagged/digest-pinned image reference; Kubernetes digests require `OPENSHELL_E2E_KUBE_BUILD_IMAGES=0` |
| `SANDBOX_IMAGE` | Trusted sandbox runtime image repository or complete tagged/digest-pinned image reference |

Run a single test directly with cargo:

Expand Down
62 changes: 46 additions & 16 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,30 @@ and explicit `/sandbox` values select `/sandbox`; other paths must already
exist without symlink or reserved-mount collisions and must be usable by the
resolved identity. Kubernetes and VM use `/sandbox`.

### Executable Identity Binding

Every mediated network open carries a connection-bound `BinaryIdentity` from
the isolation backend. The identity contains the socket-owning executable and
each executable ancestor, nearest first, as an absolute workload path plus a
SHA-256 digest. The backend resolves these values for the accepted connection
and hashes already-open live executable objects rather than reopening their
paths. Command-line paths remain diagnostic context and cannot authorize a
request.

Before policy evaluation, the supervisor validates and pins the complete leaf
and ancestor chain in one runtime-scoped trust-on-first-use cache. It rejects
missing digests, invalid paths, conflicting evidence within a chain, or a
digest that differs from an existing path pin. Validation and insertion are
atomic, so a rejected chain cannot leave partial pins.

The cache is shared across authorization paths for the lifetime of the
supervisor network runtime. Policy reloads replace policy state without
clearing executable pins; restarting the runtime creates a new cache. OPA
receives executable and ancestor paths plus endpoint policy context. Digests
remain supervisor-side integrity evidence and are not policy inputs. Any
unavailable, incomplete, or conflicting executable evidence fails closed
before OPA can authorize the connection.

The Kubernetes driver creates the namespace-wide empty-egress workload fence
before a suspended Sandbox CR, then provisions split immutable bootstrap
Secrets, the private runtime Service, and a gated supervisor Pod. A
Expand Down Expand Up @@ -465,11 +489,12 @@ management. RBAC uses a namespace-scoped Role.

**Managed** auto-creates a K8s namespace per workspace on first sandbox create.
Each new namespace receives a ServiceAccount and the configured gateway-only
SSH ingress NetworkPolicy. Configured image-pull Secrets are copied from the
driver's source namespace on every sandbox create so registry credential
rotations propagate. Their names are operator-selected gateway configuration,
not caller attachments. Copies carry gateway and workspace ownership labels; an
unrelated existing target is never adopted. The namespace also copies
SSH ingress NetworkPolicy. Each sandbox runtime generation gets immutable copies
of the configured image-pull Secrets, read from the driver's source namespace and
named after the generation, so a sandbox picks up rotated registry credentials
on its next start. Their sources are operator-selected gateway configuration,
not caller attachments. An existing Secret with a generation name fails the
create and is never adopted. The namespace also copies
OpenShift SCC UID-range and supplemental-group annotations from the gateway
namespace when present. The driver deletes the namespace during workspace
deletion. The workspace remains durably `Terminating` until the Kubernetes API
Expand All @@ -481,21 +506,25 @@ DNS-1123 label at startup) so the namespace prefix fits within the K8s 63-charac
limit. RBAC promotes sandbox CRD permissions to a ClusterRole and adds namespace
`create`/`delete` and ServiceAccount `create`/`get` permissions.

Secret copies use server-side apply. Kubernetes authorizes an apply to an
existing Secret as `patch`, but also requires `create` authorization when the
target does not exist. RBAC cannot constrain `create` by `resourceNames`, so
managed mode grants cluster-wide Secret `create`, `list`, and `delete` for
generation-scoped bootstrap Secret creation and rollback recovery while keeping
source reads and subsequent patches restricted to the explicitly configured TLS
and image-pull Secret names. Recovery lists bootstrap Secrets by sandbox and
component labels, then deletes stale generations with UID preconditions. The
Outside shared mode, the gateway client TLS material is staged into each
generation's supervisor bootstrap Secret rather than mounted from a Secret in
the workspace namespace. Every Secret the driver writes into a workspace
namespace is therefore generation-scoped, immutable, and created with `create`
only. RBAC cannot constrain `create` by `resourceNames`, so managed mode grants
cluster-wide Secret `create` and `delete`; source reads use a Role in the
driver's source namespace. Recovery deletes the Secrets of the recorded and
target runtime generations by exact name; Pod owner references let garbage
collection remove any other generation. The
driver exercises these broad permissions only in gateway-owned managed
namespaces. This depends on the managed-mode ownership invariant described below;
the gateway ServiceAccount must not be shared with unrelated workloads.

Operator mode does not create NetworkPolicies or copy image-pull Secrets.
Platform teams must apply the gateway ingress boundary and provision configured
image-pull Secrets in every operator-managed namespace.
The gateway ClusterRole grants no Secret permissions in operator mode. The
`openshell-workspace` chart Role installed in each operator-managed namespace
grants bootstrap Secret `create` and `delete`.

**Operator** uses pre-provisioned namespaces discovered through two optional
sources: a K8s label selector (`operator_namespace_label`) and a drop-in
Expand Down Expand Up @@ -555,9 +584,10 @@ isolation boundary.
### Credential Driver Integration

The Kubernetes Secrets credential driver (`openshell-driver-kubernetes-secrets`)
stores secrets in workspace-specific namespaces when `workspace_mode` is managed
or operator. In shared mode, all secrets render into the single configured
namespace.
stores every provider credential in its single configured namespace, in every
workspace mode, and rejects handles that reference another namespace. The
gateway reaches those Secrets through a namespaced Role; the gateway
ClusterRole grants no credential Secret permissions.

When runtime infrastructure changes, validate the relevant sandbox e2e path and
update the matching driver README if a maintainer-facing constraint changes.
9 changes: 4 additions & 5 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ driver/runtime identity recorded at provisioning before returning the current
generation-bound session JWT. Session authentication checks the durable runtime
generation and token lineage for every sandbox RPC, so a replaced runtime and
legacy unbound tokens cannot retain provider or control-plane access. The
gateway admits only explicitly typed, generation-bound session JWTs for sandbox
RPCs. It does not accept the pre-session untyped JWT format. The
Kubernetes driver uses its own named configuration to run TokenReview and
verify the live pod and controlling Sandbox CR. Its runtime identity binds the
namespace, immutable Sandbox CR UID, and supervisor Pod UID. Restart preserves
Expand All @@ -356,11 +358,8 @@ can recover that same successor for 30 seconds when the request matches, but it
cannot authorize ordinary RPCs or choose another successor. Advancing the
successor removes that retry path across every gateway replica. Short
`gateway_jwt.ttl_secs` lifetimes still bound the exposure of a current bearer
that has not yet been refreshed.
Omitting `gateway_jwt.ttl_secs` selects non-expiring tokens for local
single-player Docker, Podman, and VM gateways; those tokens carry `exp = 0`.
Kubernetes and other shared deployments should set a positive TTL. Explicit
zero is rejected.
that has not yet been refreshed. Omitting `gateway_jwt.ttl_secs` uses a
900-second lifetime. Explicit zero is rejected.

Gateway JWT signing-key rotation is currently an offline operator action. The
runtime loads one active signing key and one matching public verification key
Expand Down
1 change: 1 addition & 0 deletions architecture/sandbox-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ can contain a valid sequence gap.

| Path | Current bound | Terminal behavior |
|---|---:|---|
| Executable identity pins | 4,096 unique paths per supervisor lifetime | Reject an entire identity chain before insertion when its new paths would exceed the bound. Existing pins remain usable and are never evicted. Staged TCP reports resource exhaustion. |
| Corporate proxy CONNECT response headers | 8 KiB | Fail the tunnel. |
| Corporate proxy CONNECT handshake | 30 s total | Fail the tunnel; validated-address attempts share the aggregate budget. |
| Token-grant HTTP request | 30 s request and connect | Fail credential resolution. |
Expand Down
12 changes: 12 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ TCP Service, or VM vsock channel. Independent bidirectional `Exchange` RPCs
carry lifecycle, exec, TCP, and forwarding traffic, while one persistent
bidirectional `Mediate` RPC carries multiplexed DNS traffic. General application
UDP is unsupported; UDP DNS remains mediated by the supervisor.
Both ends size HTTP/2 flow control so the connection window exceeds the
per-stream window times the concurrent-stream limit plus a reserve. Relays whose
workload stops reading therefore stall only their own streams and cannot starve
DNS, exec, or control traffic of connection-level credit. The supervisor caps
concurrent TCP relay and pending-accept streams below the stream limit, so new
workload connections queue before control exchanges lose stream slots.
The sandbox probes HTTP/2 connection liveness every five seconds and closes
connections that miss a ten-second acknowledgement deadline. Closing a
connection freezes the owned workload process tree and cancels its stream
Expand All @@ -39,6 +45,12 @@ credentials to claim the existing runtime generation. Confirmation resumes the
workload; expiration terminates it. A credential replacement does not displace
the active connection until the new connection is confirmed. Idle healthy
connections remain usable.
A failed stream alone does not trigger reconnection. The supervisor first
reconfirms on the current connection and keeps it if the boundary answers.
Otherwise it closes that transport before replaying attach, and retries while
the sandbox still reports the equal-epoch connection as active, until the
boundary observes the disconnect. The sandbox reports attach and confirm
rejections as typed errors rather than closing the stream.
TCP mediation accepts use the same authenticated transport recovery as process waits. A healthy idle accept has no timeout. An interrupted pending open fails closed, while a replacement accept waits for new workload traffic; decisions and established byte streams are not replayed. Boundary rejections and failed recovery remain terminal to the proxy.

A renewed Sandbox Protocol bearer is authenticated even when its credential epoch is unchanged. The supervisor confirms that bearer on the active physical connection and records its fingerprint only after confirmation succeeds, preserving pending streams and the mediation session. Changing the credential epoch still requires an authenticated replacement connection.
Expand Down
13 changes: 8 additions & 5 deletions crates/openshell-binary-identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ Docker obtains an authoritative thread ID from seccomp notification, while the
co-located Linux path maps an accepted socket to its owning processes.

Given an authoritative Linux PID and an optional trusted process-tree root, the
crate reads the executable path from procfs, hashes the live `/proc/<pid>/exe`
object, and collects bounded executable ancestry and diagnostic command-line
paths. Resolution failures are returned as `ResolveError` so the caller can
deny the associated connection.
crate opens the leaf and bounded ancestor `/proc/<pid>/exe` objects, hashes
those already-open live objects, and returns path-and-digest evidence for the
whole executable chain. It also collects diagnostic command-line paths, which
never authorize network access. Resolution fails without returning a partial
identity if any executable cannot be opened, hashed, or validated after
hashing. The caller receives `ResolveError` and denies the associated
connection.

The crate does not intercept connections, authenticate remote observers, or
evaluate policy. The isolation backend remains responsible for binding the
resolved identity to the active boundary and exact accepted connection before
constructing `MediatedConnection`.
constructing `PendingTcpOpen`.
Loading
Loading