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
21 changes: 0 additions & 21 deletions agents/hermes/policy-permissive.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,6 @@ process:

network_policies:

claude_code:
name: claude_code
endpoints:
- host: api.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: statsig.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: sentry.io
port: 443
protocol: rest
enforcement: enforce
access: full
binaries:
- { path: "/**" }

nvidia:
name: nvidia
endpoints:
Expand Down
21 changes: 0 additions & 21 deletions agents/openclaw/policy-permissive.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,6 @@ process:

network_policies:

claude_code:
name: claude_code
endpoints:
- host: api.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: statsig.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: sentry.io
port: 443
protocol: rest
enforcement: enforce
access: full
binaries:
- { path: "/**" }

nvidia:
name: nvidia
endpoints:
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ After selecting a tier, a combined preset and access-mode screen lets you includ
Tier-default presets are pre-selected; additional presets can be added from the full list.
NemoClaw filters tier defaults by the active agent's supported integrations.
For example, Hermes onboarding omits the Brave Search preset because Hermes does not use NemoClaw's OpenClaw web-search configuration.
Claude Code direct egress is not included in any tier or `shields down` permissive policy.
If you install and run the Claude Code CLI inside the sandbox with its own credentials, apply the `claude-code` preset explicitly.
Normal NemoClaw Anthropic inference still routes through the OpenShell gateway.

Tier definitions are stored in `nemoclaw-blueprint/policies/tiers.yaml`.

Expand Down
5 changes: 3 additions & 2 deletions docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ NemoClaw ships preset policy files in `nemoclaw-blueprint/policies/presets/` for
|---|---|---|
| `brave` | Brave Search API. | Agent can issue search queries. |
| `brew` | Homebrew (Linuxbrew) package manager. The sandbox base image includes the `brew` binary; this preset opens network egress to GitHub and the Homebrew formulae index so `brew install` can fetch bottles. | Allows installing arbitrary Homebrew packages, which may contain malicious code. |
| `claude-code` | Claude Code CLI API, telemetry, and crash-report endpoints. | Allows a separately installed Claude Code CLI to reach Anthropic and telemetry hosts with its own credentials. Do not use this preset for NemoClaw inference routing. |
| `discord` | Discord REST API, WebSocket gateway, CDN. | CDN endpoint (`cdn.discordapp.com`) allows GET to any path. WebSocket uses `access: full` (no inspection). |
| `github` | GitHub and GitHub REST API. | Gives agent read/write access to repositories and issues via `git`. |
| `huggingface` | Hugging Face Hub (download-only) and inference router. | Allows downloading arbitrary models and datasets. POST is restricted to the inference router only. |
Expand Down Expand Up @@ -458,7 +459,7 @@ The agent never receives the provider API key.
| Default | The agent talks to `inference.local`. The host owns the credential and upstream endpoint. |
| What you can change | You cannot configure this architecture. The system always enforces it. |
| Risk if bypassed | If the agent could reach an inference endpoint directly (by adding it to the network policy), it would need an API key. Since the sandbox does not contain credentials, this acts as defense-in-depth. However, adding an inference provider's host to the network policy without going through OpenShell routing could let the agent use a stolen or hardcoded key. |
| Recommendation | Do not add inference provider hosts (such as `api.openai.com` or `api.anthropic.com`) to the network policy. Use OpenShell inference routing instead. |
| Recommendation | Do not add inference provider hosts (such as `api.openai.com` or `api.anthropic.com`) to the network policy for NemoClaw model traffic. Use OpenShell inference routing instead. The `claude-code` preset is a separate opt-in exception for running the Claude Code CLI with its own credentials, not a way to configure NemoClaw inference. |

### Provider Trust Tiers

Expand Down Expand Up @@ -529,7 +530,7 @@ The following patterns weaken security without providing meaningful benefit.
| Adding endpoints to the baseline policy for one-off requests | Adding an endpoint to the baseline policy makes it permanently reachable across all sandbox instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset when you destroy and recreate the sandbox. |
| Relying solely on the entrypoint for capability drops | The entrypoint drops dangerous capabilities using `capsh`, but this is best-effort. If `capsh` is unavailable or `CAP_SETPCAP` is not in the bounding set, the container runs with the default capability set. | Pass `--cap-drop=ALL` at the container runtime level as defense-in-depth. |
| Leaving `/sandbox/.openclaw` writable on sensitive workloads | This directory contains the OpenClaw gateway configuration. A writable `.openclaw` lets the agent disable CORS, redirect inference routing, or weaken gateway protections. | Lock config for always-on assistants handling sensitive data. |
| Adding inference provider hosts to the network policy | Direct network access to an inference host bypasses credential isolation and usage tracking. | Use OpenShell inference routing instead of adding hosts like `api.openai.com` or `api.anthropic.com` to the network policy. |
| Adding inference provider hosts to the network policy for NemoClaw inference | Direct network access to an inference host bypasses credential isolation and usage tracking. | Use OpenShell inference routing instead of adding hosts like `api.openai.com` or `api.anthropic.com` to the network policy. Apply `claude-code` only when intentionally running the separate Claude Code CLI inside the sandbox. |
| Disabling device auth for remote deployments | Without device auth, any device on the network can connect to the gateway without pairing. Combined with a cloudflared tunnel, this makes the dashboard publicly accessible and unauthenticated. | Keep `NEMOCLAW_DISABLE_DEVICE_AUTH` at its default (`0`). Only set it to `1` for local headless or development environments. |

## Known Limitations
Expand Down
21 changes: 0 additions & 21 deletions nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,6 @@ process:
network_policies:
# ── Base policy endpoints (all methods, all paths) ──

claude_code:
name: claude_code
endpoints:
- host: api.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: statsig.anthropic.com
port: 443
protocol: rest
enforcement: enforce
access: full
- host: sentry.io
port: 443
protocol: rest
enforcement: enforce
access: full
binaries:
- { path: "/**" }

nvidia:
name: nvidia
endpoints:
Expand Down
41 changes: 41 additions & 0 deletions nemoclaw-blueprint/policies/presets/claude-code.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

preset:
name: claude-code
description: "Claude Code API, telemetry, and crash-report access"

network_policies:
claude_code:
name: claude_code
endpoints:
- host: api.anthropic.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: statsig.anthropic.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: sentry.io
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
binaries:
- { path: /usr/local/bin/claude }
- { path: /usr/bin/claude }
- { path: /tmp/npm-global/bin/claude }
- { path: /home/linuxbrew/.linuxbrew/bin/claude }
# Claude Code is distributed as an npm CLI, so OpenShell may observe
# the Node runtime instead of the shim that launched it.
- { path: /usr/local/bin/node }
- { path: /usr/bin/node }
63 changes: 61 additions & 2 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ selectFromList(items, options)

describe("policies", () => {
describe("listPresets", () => {
it("returns all 20 presets", () => {
it("returns all 21 presets", () => {
const presets = policies.listPresets();
expect(presets.length).toBe(20);
expect(presets.length).toBe(21);
});

it("each preset has name and description", () => {
Expand All @@ -165,6 +165,7 @@ describe("policies", () => {
const expected = [
"brave",
"brew",
"claude-code",
"discord",
"github",
"huggingface",
Expand Down Expand Up @@ -1641,6 +1642,64 @@ exit 1
}
});

it("Claude Code hosts require the explicit claude-code preset", () => {
const claudeHosts = new Set(["api.anthropic.com", "statsig.anthropic.com", "sentry.io"]);
const permissivePolicyPaths = [
"nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml",
"agents/openclaw/policy-permissive.yaml",
"agents/hermes/policy-permissive.yaml",
];

for (const relativePath of permissivePolicyPaths) {
const parsed = parseRepoYaml(relativePath) as {
network_policies?: Record<string, { endpoints?: Array<{ host?: string }> }>;
};
expect(parsed.network_policies, relativePath).not.toHaveProperty("claude_code");
const hosts = Object.values(parsed.network_policies ?? {})
.flatMap((policy) => policy.endpoints ?? [])
.map((endpoint) => endpoint.host)
.filter((host): host is string => typeof host === "string");
expect(hosts.filter((host) => claudeHosts.has(host)), relativePath).toEqual([]);
}

const preset = parseRepoYaml("nemoclaw-blueprint/policies/presets/claude-code.yaml") as {
preset?: { name?: string };
network_policies?: Record<
string,
{
endpoints?: Array<{
host?: string;
port?: number;
protocol?: string;
enforcement?: string;
access?: string;
rules?: unknown[];
}>;
binaries?: Array<{ path?: string }>;
}
>;
};
const claudePolicy = preset.network_policies?.claude_code;
expect(preset.preset?.name).toBe("claude-code");
expect(claudePolicy).toBeDefined();
expect((claudePolicy?.endpoints ?? []).map((endpoint) => endpoint.host).sort()).toEqual(
[...claudeHosts].sort(),
);
for (const endpoint of claudePolicy?.endpoints ?? []) {
expect(endpoint.port).toBe(443);
expect(endpoint.protocol).toBe("rest");
expect(endpoint.enforcement).toBe("enforce");
expect(endpoint).not.toHaveProperty("access");
expect(endpoint.rules).toEqual(
expect.arrayContaining([
{ allow: { method: "GET", path: "/**" } },
{ allow: { method: "POST", path: "/**" } },
]),
);
}
expect((claudePolicy?.binaries ?? []).map((binary) => binary.path)).not.toContain("/**");
});

it("brew preset whitelists the PATH wrapper and Homebrew-managed entrypoints (#3913)", () => {
const content = requirePresetContent(policies.loadPreset("brew"));
const parsed = YAML.parse(content);
Expand Down
Loading