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
235 changes: 235 additions & 0 deletions docs/_ext/policy_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Sphinx extension that generates tables from a sandbox policy YAML file.

Usage in MyST markdown::

```{policy-table} deploy/docker/sandbox/dev-sandbox-policy.yaml
```

The directive reads the YAML relative to the repo root and emits:
1. A "Filesystem, Landlock, and Process" table.
2. One subsection per ``network_policies`` block with endpoint and binary tables.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml
from docutils import nodes
from docutils.statemachine import StringList
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective


def _tls_display(ep: dict[str, Any]) -> str:
tls = ep.get("tls")
return tls if tls else "\u2014"


def _access_display(ep: dict[str, Any]) -> str:
if "rules" in ep:
rules = ep["rules"]
parts = []
for r in rules:
allow = r.get("allow", {})
parts.append(f"``{allow.get('method', '*')} {allow.get('path', '/**')}``")
return ", ".join(parts)
access = ep.get("access")
if access:
return access
return "L4 passthrough"


def _binaries_line(binaries: list[dict[str, str]]) -> str:
paths = [f"``{b['path']}``" for b in binaries]
return ", ".join(paths)


BLOCK_INFO: dict[str, dict[str, str]] = {
"claude_code": {
"title": "Anthropic API and Telemetry",
"description": (
"Allows Claude Code to reach its API, feature-flagging "
"(Statsig), error reporting (Sentry), release notes, and "
"the Claude platform dashboard."
),
},
"github_ssh_over_https": {
"title": "Git Clone and Fetch",
"description": (
"Allows ``git clone``, ``git fetch``, and ``git pull`` over "
"HTTPS via Git Smart HTTP. Push (``git-receive-pack``) is "
"disabled by default."
),
},
"nvidia_inference": {
"title": "NVIDIA API Catalog",
"description": (
"Allows outbound calls to the NVIDIA hosted inference API. "
"Used by agents that route LLM requests through "
"``integrate.api.nvidia.com``."
),
},
"github_rest_api": {
"title": "GitHub API (Read-Only)",
"description": (
"Grants read-only access to the GitHub REST API. Enables "
"issue reads, PR listing, and repository metadata lookups "
"without allowing mutations."
),
},
"pypi": {
"title": "Python Package Installation",
"description": (
"Allows ``pip install`` and ``uv pip install`` to reach PyPI, "
"python-build-standalone releases on GitHub, and "
"``downloads.python.org``."
),
},
"vscode": {
"title": "VS Code Remote and Marketplace",
"description": (
"Allows VS Code Server, Remote Containers, and extension "
"marketplace traffic so remote development sessions can "
"download updates and extensions."
),
},
"gitlab": {
"title": "GitLab",
"description": (
"Allows the ``glab`` CLI to reach ``gitlab.com`` for "
"repository and merge-request operations."
),
},
}


def _block_title(key: str, name: str) -> str:
info = BLOCK_INFO.get(key)
return info["title"] if info else name


def _block_description(key: str) -> str | None:
info = BLOCK_INFO.get(key)
return info["description"] if info else None


class PolicyTableDirective(SphinxDirective):
"""Render sandbox policy YAML as tables."""

required_arguments = 1
has_content = False

def run(self) -> list[nodes.Node]:
repo_root = Path(self.env.srcdir).parent
yaml_path = repo_root / self.arguments[0]

self.env.note_dependency(str(yaml_path))

if not yaml_path.exists():
msg = self.state_machine.reporter.warning(
f"Policy YAML not found: {yaml_path}",
line=self.lineno,
)
return [msg]

policy = yaml.safe_load(yaml_path.read_text())

lines: list[str] = []

fs = policy.get("filesystem_policy", {})
landlock = policy.get("landlock", {})
proc = policy.get("process", {})

lines.append("(default-policy-fs-landlock-process)=")
lines.append("<h2>Filesystem, Landlock, and Process</h2>")
lines.append("")
lines.append("| Section | Setting | Value |")
lines.append("|---|---|---|")

ro = fs.get("read_only", [])
rw = fs.get("read_write", [])
workdir = fs.get("include_workdir", False)
lines.append(
f"| **Filesystem** | Read-only | {', '.join(f'``{p}``' for p in ro)} |"
)
lines.append(f"| | Read-write | {', '.join(f'``{p}``' for p in rw)} |")
lines.append(f"| | Workdir included | {'Yes' if workdir else 'No'} |")

compat = landlock.get("compatibility", "best_effort")
lines.append(
f"| **Landlock** | Compatibility | ``{compat}`` "
f"(uses the highest ABI the host kernel supports) |"
)

user = proc.get("run_as_user", "")
group = proc.get("run_as_group", "")
lines.append(f"| **Process** | User / Group | ``{user}`` / ``{group}`` |")
lines.append("")

net = policy.get("network_policies", {})
if net:
lines.append("(default-policy-network-policies)=")
lines.append("<h2>Network Policy Blocks</h2>")
lines.append("")
lines.append(
"Each block pairs a set of endpoints (host and port) with "
"a set of binaries (executable paths inside the sandbox). "
"The proxy identifies the calling binary by resolving the "
"socket to a PID through ``/proc/net/tcp`` and reading "
"``/proc/{pid}/exe``. A connection is allowed only when both "
"the destination and the calling binary match an entry in the "
"same block. All other outbound traffic is denied."
)
lines.append("")

for key, block in net.items():
name = block.get("name", key)
endpoints = block.get("endpoints", [])
binaries = block.get("binaries", [])

lines.append(f"<h3>{_block_title(key, name)}</h3>")
lines.append("")
desc = _block_description(key)
if desc:
lines.append(desc)
lines.append("")

has_rules = any("rules" in ep for ep in endpoints)
if has_rules:
lines.append("| Endpoint | Port | TLS | Rules |")
else:
lines.append("| Endpoint | Port | TLS | Access |")
lines.append("|---|---|---|---|")

for ep in endpoints:
host = ep.get("host", "")
port = ep.get("port", "")
tls = _tls_display(ep)
access = _access_display(ep)
lines.append(f"| ``{host}`` | {port} | {tls} | {access} |")

lines.append("")
lines.append(
f"Only the following binaries can use these endpoints: "
f"{_binaries_line(binaries)}."
)
lines.append("")

rst = StringList(lines, source=str(yaml_path))
container = nodes.container()
self.state.nested_parse(rst, self.content_offset, container)
return container.children


def setup(app: Sphinx) -> dict[str, Any]:
app.add_directive("policy-table", PolicyTableDirective)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
2 changes: 1 addition & 1 deletion docs/about/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ Use pre-built sandbox images from the [NemoClaw Community](https://github.com/NV

- [Architecture Overview](architecture.md): Understand the components that make up the OpenShell runtime.
- [Get Started](../index.md): Install the CLI and create your first sandbox.
- [Security Model](../safety-and-privacy/security-model.md): Learn how OpenShell enforces isolation across all protection layers.
- [Safety and Privacy](../safety-and-privacy/index.md): Learn how OpenShell enforces isolation across all protection layers.
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent / "_ext"))

project = "NVIDIA OpenShell Developer Guide"
this_year = date.today().year
Expand All @@ -23,6 +24,7 @@
"sphinx_copybutton",
"sphinx_design",
"sphinxcontrib.mermaid",
"policy_table",
]

autodoc_default_options = {
Expand Down
4 changes: 2 additions & 2 deletions docs/get-started/run-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,5 @@ This is useful when you plan to reconnect later or iterate on the policy while t

- {doc}`../sandboxes/create-and-manage`: Learn the isolation model and sandbox lifecycle.
- {doc}`../sandboxes/providers`: Understand how credentials are injected without exposing them to agent code.
- {doc}`../safety-and-privacy/policies`: Customize the default policy or write your own.
- {doc}`../safety-and-privacy/policies`: Explore network policies and per-endpoint rules.
- {doc}`../safety-and-privacy/default-policies`: What the built-in default policy allows and denies.
- {doc}`../safety-and-privacy/policies`: Write custom policies and configure network rules.
2 changes: 1 addition & 1 deletion docs/get-started/run-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,4 @@ $ nemoclaw sandbox delete opencode-sandbox
- {doc}`../safety-and-privacy/policies`: How the proxy evaluates network rules and policy enforcement.
- {doc}`../inference/index`: Inference route configuration, protocol detection, and transparent rerouting.
- {doc}`../sandboxes/providers`: Provider types, credential discovery, and manual and automatic creation.
- {doc}`../safety-and-privacy/security-model`: The four protection layers and how they interact.
- {doc}`../safety-and-privacy/index`: The four protection layers and how they interact.
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ sandboxes/community-sandboxes
:hidden:

safety-and-privacy/index
safety-and-privacy/security-model
safety-and-privacy/default-policies
safety-and-privacy/policies
```

Expand All @@ -229,6 +229,7 @@ inference/configure-routes
:hidden:

reference/cli
reference/default-policy
reference/policy-schema
reference/architecture
```
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/default-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Default Policy Reference

The default policy is the policy applied when you create an OpenShell sandbox without `--policy`. It is defined in the [`deploy/docker/sandbox/dev-sandbox-policy.yaml`](https://github.com/NVIDIA/NemoClaw/blob/main/deploy/docker/sandbox/dev-sandbox-policy.yaml) file.

The following tables show the default policy blocks pre-configured in the file.

```{policy-table} deploy/docker/sandbox/dev-sandbox-policy.yaml
```
36 changes: 36 additions & 0 deletions docs/safety-and-privacy/default-policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Built-in Default Policy

NVIDIA OpenShell ships a built-in policy that covers common agent workflows out of the box.
When you create a sandbox without `--policy`, OpenShell applies the default policy. This policy controls three things:

- What the agent can access on disk. Filesystem paths are split into read-only and read-write sets. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level.
- What the agent can reach on the network. Each network policy block pairs a set of allowed destinations (host and port) with a set of allowed binaries (executable paths inside the sandbox). The proxy resolves every outbound connection to the binary that opened it. A connection is allowed only when both the destination and the calling binary match an entry in the same block. Everything else is denied.
- What privileges the agent has. The agent runs as an unprivileged user with seccomp filters that block dangerous system calls. There is no `sudo`, no `setuid`, and no path to elevated privileges.

## 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. See [Run OpenCode with NVIDIA Inference](../get-started/run-opencode.md). |
| Codex | None | Provide a complete custom policy with OpenAI endpoints and Codex binary paths. |

:::{important}
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.
:::

## What the Default Policy Allows

The default policy defines six network policy blocks, plus filesystem isolation, Landlock enforcement, and process identity. For the full breakdown of each block, see {doc}`../reference/default-policy`.

## Next Steps

- {doc}`policies`: Write custom policies, configure network rules, and iterate on a running sandbox.
- [Policy Schema Reference](../reference/policy-schema.md): Complete field reference for the policy YAML.
27 changes: 23 additions & 4 deletions docs/safety-and-privacy/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,28 @@ Filesystem and process restrictions are locked at creation time. Network and
inference rules are hot-reloadable on a running sandbox, so you can iterate on
access rules without recreating the sandbox.

## Threat Scenarios

When an AI agent runs with unrestricted access, it can read any file, reach any
network host, call any API with your credentials, and install arbitrary software.
The table below shows how the four layers address concrete threats.

| Threat | Without Protection | With OpenShell |
|---|---|---|
| **Data exfiltration** | Agent uploads source code to an external server via `curl`. | Network policy blocks all outbound connections except explicitly approved hosts. |
| **Credential theft** | Agent reads `~/.ssh/id_rsa` or `~/.aws/credentials` and exfiltrates them. | [Landlock](https://docs.kernel.org/security/landlock.html) limits the agent to declared paths (`/sandbox`, `/tmp`, read-only system dirs). |
| **Unauthorized API calls** | Agent calls `api.openai.com` with your key, sending data to a third-party provider. | Privacy router intercepts and reroutes calls to a backend you control. |
| **Privilege escalation** | Agent runs `sudo apt install`, modifies `/etc/passwd`, or scans the internal network. | Agent runs as an unprivileged user with seccomp filters; no `sudo`, no `setuid`. |

:::{important}
All four layers work together. No single layer is sufficient on its own.
Filesystem restrictions do not prevent network exfiltration. Network policies do
not prevent local privilege escalation. Process restrictions do not control
where inference traffic goes. Defense in depth means every layer covers gaps
that the others cannot.
:::

## Next Steps

- {doc}`security-model`: Threat scenarios and how each protection layer
addresses them.
- {doc}`policies`: Policy structure, evaluation order, and how to iterate on
rules.
- {doc}`default-policies`: The built-in policy that ships with OpenShell and what each block allows.
- {doc}`policies`: Write custom policies, configure network rules, and iterate on a running sandbox.
Loading
Loading