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
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,7 @@ jobs:
# OpenClaw balanced/open and Hermes open common-egress agent-turn
# coverage against real sandbox policy and live hosted inference.
env:
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
run: |
set -euo pipefail
Expand Down
5 changes: 3 additions & 2 deletions docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ On DGX Spark, DGX Station, and Windows WSL, an interactive installer offers expr
Express install switches onboarding to non-interactive mode, allows `sudo` password prompts for required host changes, and selects the managed local inference path for that platform.
On DGX Spark and DGX Station, express install selects managed vLLM; DGX Spark uses `qwen3.6-35b-a3b-nvfp4` by default, and DGX Station uses `deepseek-v4-flash` by default.
Unless `NEMOCLAW_POLICY_TIER` is set, it applies sandbox policy in `suggested` mode with the `balanced` tier by default.
That tier uses the base sandbox policy plus supported package, model, web-search, local-inference, and read-only weather presets.
That tier uses the base sandbox policy plus supported package, model, web-search, and local-inference presets.
On DGX Spark, express install uses `my-spark-assistant` as the sandbox name unless `NEMOCLAW_SANDBOX_NAME` is already set.
On WSL, express install selects the Windows-host Ollama setup path.
Set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` before launching the installer when you want to choose a provider yourself.
Expand Down Expand Up @@ -195,7 +195,8 @@ Review [Messaging Channels](../manage-sandboxes/messaging-channels) before enabl

After the sandbox image builds and OpenClaw starts inside the sandbox, NemoClaw asks which network policy tier to apply.
Web search and messaging selections happen before this point so the sandbox image and the policy suggestions stay aligned.
The default **Balanced** tier includes common development presets such as npm, PyPI, Hugging Face, Homebrew, read-only weather lookups, and Brave Search when the selected agent supports web search.
The default **Balanced** tier includes common development presets such as npm, PyPI, Hugging Face, Homebrew, and Brave Search when the selected agent supports web search. Apply the `weather` preset explicitly if your agent needs read-only weather lookups.
OpenClaw sandboxes also receive the `openclaw-pricing` preset automatically so session-cost records can populate without manual configuration.
Use the arrow keys or `j` and `k` to move, Space to select, and Enter to confirm.

The preset selector lets you include more destinations, such as GitHub, Jira, Slack, Telegram, or local inference.
Expand Down
3 changes: 2 additions & 1 deletion docs/network-policy/integration-policy-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ The Brave Search API key is still configured separately during onboarding or thr
## Weather and Public Reference Lookups

Use the `weather` preset when the agent needs read-only weather or geocoding lookups.
The Balanced and Open tiers include it by default.
The Open tier includes it by default.
Balanced and Restricted sandboxes require the explicit `policy-add weather` step below.
The preset covers read-only public weather and geocoding lookups, including current conditions, forecasts, geocoding, and weather alerts, without enabling messaging or productivity APIs.

```bash
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,15 @@ This catches desync caused by external edits to the gateway policy or stale regi
Preset summaries come only from the YAML `preset.description` field.
NemoClaw does not render network-policy rule bodies as prose in `policy-list` output.

Each active preset is annotated with its provenance so you can tell why it is applied:

- `[from <tier> tier]` — the preset name matches an entry in the sandbox's current tier definition (see [Policy Tiers](../reference/network-policies#policy-tiers)).
- `[from openclaw agent]` or `[from hermes agent]` — the preset name matches a NemoClaw-managed agent preset and the active agent matches that label (for example `openclaw-pricing` on an OpenClaw sandbox, or Nous-hosted gateway presets on a Hermes sandbox).
- `[user-added]` — anything else: presets applied later through `policy-add`, presets that match no tier or agent default, or presets that match the opposite agent's reserved names on a sandbox running the other agent.
- `[source unverified]` — the row is active but the local registry and live gateway state disagree. When the gateway cannot be queried, this renders as `[source unverified (gateway unreachable)]`. The provenance check is suppressed in these trust-degraded states because the source cannot be confirmed against both halves of the sandbox policy view.

Provenance tags are inferred from the sandbox's current tier and agent metadata at display time and are not persisted per preset. A preset whose name appears in the sandbox's current tier YAML is labelled `[from <tier> tier]` even when an operator added it manually with `policy-add` after onboarding, and OpenClaw-only or Hermes-only preset names are only labelled `[from <agent> agent]` when the active agent matches that label.

```bash
nemohermes my-assistant policy-list
```
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,15 @@ This catches desync caused by external edits to the gateway policy or stale regi
Preset summaries come only from the YAML `preset.description` field.
NemoClaw does not render network-policy rule bodies as prose in `policy-list` output.

Each active preset is annotated with its provenance so you can tell why it is applied:

- `[from <tier> tier]` — the preset name matches an entry in the sandbox's current tier definition (see [Policy Tiers](../reference/network-policies#policy-tiers)).
- `[from openclaw agent]` or `[from hermes agent]` — the preset name matches a NemoClaw-managed agent preset and the active agent matches that label (for example `openclaw-pricing` on an OpenClaw sandbox, or Nous-hosted gateway presets on a Hermes sandbox).
- `[user-added]` — anything else: presets applied later through `policy-add`, presets that match no tier or agent default, or presets that match the opposite agent's reserved names on a sandbox running the other agent.
- `[source unverified]` — the row is active but the local registry and live gateway state disagree. When the gateway cannot be queried, this renders as `[source unverified (gateway unreachable)]`. The provenance check is suppressed in these trust-degraded states because the source cannot be confirmed against both halves of the sandbox policy view.

Provenance tags are inferred from the sandbox's current tier and agent metadata at display time and are not persisted per preset. A preset whose name appears in the sandbox's current tier YAML is labelled `[from <tier> tier]` even when an operator added it manually with `policy-add` after onboarding, and OpenClaw-only or Hermes-only preset names are only labelled `[from <agent> agent]` when the active agent matches that label.

```bash
$$nemoclaw my-assistant policy-list
```
Expand Down
7 changes: 6 additions & 1 deletion docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ The baseline policy is always applied regardless of the selected tier.
| Tier | Presets included | Description |
|------|------------------|-------------|
| Restricted | None | Base sandbox only. No third-party network access beyond inference and core agent tooling. |
| Balanced (default) | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported`, `weather` | Full dev tooling, read-only weather lookups, and web search for agents that support web search. No messaging platform access. |
| Balanced (default) | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported` | Full dev tooling and web search for agents that support web search. No messaging platform access. Apply the `weather` preset explicitly if your agent needs read-only weather lookups. |
| Open | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported`, `weather`, `public-reference`, `slack`, `discord`, `telegram`, `wechat` (experimental), `whatsapp` (experimental), `jira`, `outlook` | Broad access across third-party services including messaging, productivity, weather, and public-reference APIs. |

After selecting a tier, a combined preset and access-mode screen lets you include or exclude individual presets and toggle each between read (GET only) and read-write (GET + POST/PUT/PATCH) access.
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.
Hermes managed-tool gateway selections can add Hermes-specific presets, such as Nous-hosted web, image, audio, browser, or code tools, without applying unsupported OpenClaw-only presets.
OpenClaw onboarding also adds the `openclaw-pricing` preset on top of tier defaults so session-cost records can populate from LiteLLM and OpenRouter without manual configuration.
When the OpenClaw OTEL diagnostics feature is enabled with a local endpoint, NemoClaw adds the `openclaw-diagnostics-otel-local` preset on the same basis.
The applied set therefore reflects the chosen tier *plus* any agent-required presets, so `policy-list` may show one or more presets that do not appear in the tier table above.
The `policy-list` provenance tags are inferred from the current tier YAML and the active agent at display time and are not persisted per preset.
A preset whose name matches an entry in the sandbox's current tier definition is labelled `[from <tier> tier]` even when an operator added it manually with `policy-add` after onboarding; OpenClaw-only and Hermes-only preset names are only labelled `[from <agent> agent]` when the active agent matches.
Claude Code direct egress is not included in any policy tier.
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.
Expand Down
5 changes: 2 additions & 3 deletions nemoclaw-blueprint/policies/tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,17 @@ tiers:

- name: balanced
label: Balanced
description: Full dev tooling, web search, and core weather lookups. Package installs, model downloads, and inference. No messaging platform access.
description: Full dev tooling and web search. Package installs, model downloads, and inference. No messaging platform access.
presets:
- { name: npm, access: read-write }
- { name: pypi, access: read-write }
- { name: huggingface, access: read-write }
- { name: brew, access: read-write }
- { name: brave, access: read-write }
- { name: weather, access: read }

- name: open
label: Open
description: Broad access across third-party services including messaging and productivity. User accepts responsibility for the expanded scope.
description: Broad access across third-party services including messaging, productivity, and core weather lookups. User accepts responsibility for the expanded scope.
presets:
- { name: npm, access: read-write }
- { name: pypi, access: read-write }
Expand Down
247 changes: 247 additions & 0 deletions src/lib/actions/sandbox/policy-channel-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";

type PresetInfo = {
name: string;
description?: string;
};

const moduleMocks = vi.hoisted(() => ({
getSandbox: vi.fn<(sandboxName: string) => Record<string, unknown> | null>(),
getCustomPolicies: vi.fn<(sandboxName: string) => PresetInfo[]>(),
listPresets: vi.fn<() => PresetInfo[]>(),
listCustomPresets: vi.fn<(sandboxName: string) => PresetInfo[]>(),
getAppliedPresets: vi.fn<(sandboxName: string) => string[]>(),
getGatewayPresets: vi.fn<(sandboxName: string) => string[] | null>(),
}));

vi.mock("../../state/registry", async (importOriginal) => ({
...(await importOriginal<typeof import("../../state/registry")>()),
getSandbox: moduleMocks.getSandbox,
getCustomPolicies: moduleMocks.getCustomPolicies,
}));

vi.mock("../../policy", async (importOriginal) => ({
...(await importOriginal<typeof import("../../policy")>()),
listPresets: moduleMocks.listPresets,
listCustomPresets: moduleMocks.listCustomPresets,
getAppliedPresets: moduleMocks.getAppliedPresets,
getGatewayPresets: moduleMocks.getGatewayPresets,
}));

import { listSandboxPolicies } from "./policy-channel";

const POLICY_PRESETS: PresetInfo[] = [
{ name: "npm", description: "npm and Yarn registry access" },
{ name: "pypi", description: "Python Package Index access" },
{ name: "discord", description: "Discord API access" },
{ name: "openclaw-pricing", description: "OpenClaw pricing lookup" },
{ name: "nous-web", description: "Nous Portal managed web search gateway" },
];

let logSpy: MockInstance;
let errSpy: MockInstance;

function printedText(): string {
return [...logSpy.mock.calls, ...errSpy.mock.calls]
.map((call) => call.map(String).join(" "))
.join("\n");
}

function arrangeListing({
appliedNames,
gatewayNames,
tier,
agent,
}: {
appliedNames: string[];
gatewayNames: string[] | null;
tier: string | null;
agent: string | null;
}): void {
moduleMocks.getSandbox.mockReturnValue({
name: "test-sandbox",
agent,
policyTier: tier,
policies: appliedNames,
});
moduleMocks.getAppliedPresets.mockReturnValue(appliedNames);
moduleMocks.getGatewayPresets.mockReturnValue(gatewayNames);
}

beforeEach(() => {
vi.clearAllMocks();
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
moduleMocks.getCustomPolicies.mockReturnValue([]);
moduleMocks.listPresets.mockReturnValue(POLICY_PRESETS);
moduleMocks.listCustomPresets.mockReturnValue([]);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe("listSandboxPolicies provenance", () => {
it("tags active tier-default presets with their tier provenance (#5774)", () => {
arrangeListing({
appliedNames: ["npm", "pypi"],
gatewayNames: ["npm", "pypi"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toContain("● npm [from balanced tier]");
expect(output).toContain("● pypi [from balanced tier]");
});

it("keeps tier attribution when a custom registry entry shadows a tier preset (#5774)", () => {
moduleMocks.getCustomPolicies.mockReturnValue([
{ name: "npm", description: "sandbox-scoped custom npm policy" },
]);
arrangeListing({
appliedNames: ["npm"],
gatewayNames: ["npm"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toContain("● npm [from balanced tier]");
expect(output).not.toContain("● npm [user-added]");
});

it("tags openclaw-pricing as an OpenClaw agent preset (#5774)", () => {
arrangeListing({
appliedNames: ["openclaw-pricing"],
gatewayNames: ["openclaw-pricing"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

expect(printedText()).toContain("● openclaw-pricing [from openclaw agent]");
});

it("tags nous-* presets as Hermes agent presets on Hermes (#5774)", () => {
arrangeListing({
appliedNames: ["nous-web"],
gatewayNames: ["nous-web"],
tier: "open",
agent: "hermes",
});

listSandboxPolicies("test-sandbox");

expect(printedText()).toContain("● nous-web [from hermes agent]");
});

it("tags presets outside the tier and agent defaults as user-added (#5774)", () => {
arrangeListing({
appliedNames: ["discord"],
gatewayNames: ["discord"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

expect(printedText()).toContain("● discord [user-added]");
});

it("omits the provenance tag for inactive presets (#5774)", () => {
arrangeListing({
appliedNames: ["npm"],
gatewayNames: ["npm"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toMatch(/○ pypi —/);
expect(output).not.toMatch(/○ pypi \[/);
});

it.each([
{
agent: "hermes",
preset: "openclaw-pricing",
forbidden: "[from openclaw agent]",
},
{ agent: "openclaw", preset: "nous-web", forbidden: "[from hermes agent]" },
])("does not use another agent's provenance for $preset (#5774)", ({
agent,
preset,
forbidden,
}) => {
arrangeListing({
appliedNames: [preset],
gatewayNames: [preset],
tier: "balanced",
agent,
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toContain(`● ${preset} [user-added]`);
expect(output).not.toContain(forbidden);
});

it("falls back to user-added when policyTier is missing (#5774)", () => {
arrangeListing({
appliedNames: ["npm"],
gatewayNames: ["npm"],
tier: null,
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toContain("● npm [user-added]");
expect(output).not.toContain("[from balanced tier]");
});

it("does not trust tier provenance for gateway-only desync (#5774)", () => {
arrangeListing({
appliedNames: [],
gatewayNames: ["npm"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).not.toContain("● npm [from balanced tier]");
expect(output).toContain(
"● npm [source unverified] — npm and Yarn registry access (active on gateway, missing from local state)",
);
});

it("marks registry-only provenance as gateway-unreachable (#5774)", () => {
arrangeListing({
appliedNames: ["npm"],
gatewayNames: null,
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).not.toContain("● npm [from balanced tier]");
expect(output).toContain("● npm [source unverified (gateway unreachable)]");
});
});
Loading
Loading