Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ 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.
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
6 changes: 6 additions & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,12 @@ 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]` — pre-selected by the chosen policy tier (see [Policy Tiers](../reference/network-policies#policy-tiers)).
- `[from hermes agent]` — added by NemoClaw to satisfy the active agent's required integrations (for example Nous-hosted managed-tool gateway presets).
- `[user-added]` — applied later through `policy-add` or by including the preset during onboarding outside the tier defaults.

```bash
nemohermes my-assistant policy-list
```
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,12 @@ 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]` — pre-selected by the chosen policy tier (see [Policy Tiers](../reference/network-policies#policy-tiers)).
- `[from openclaw agent]` or `[from hermes agent]` — added by NemoClaw to satisfy the active agent's required integrations (for example `openclaw-pricing` for OpenClaw, or Nous-hosted gateway presets for Hermes).
- `[user-added]` — applied later through `policy-add` or by including the preset during onboarding outside the tier defaults.

```bash
$$nemoclaw my-assistant policy-list
```
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Tier-default presets are pre-selected; additional presets can be added from the
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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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.
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
97 changes: 96 additions & 1 deletion src/lib/actions/sandbox/policy-channel-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ const store = D("credentials/store.js");
const registry = D("state/registry.js");
const onboardSession = D("state/onboard-session.js");
const policies = D("policy/index.js");
const { addSandboxPolicy, removeSandboxPolicy } = D("actions/sandbox/policy-channel.js") as {
const { addSandboxPolicy, listSandboxPolicies, removeSandboxPolicy } = D(
"actions/sandbox/policy-channel.js",
) as {
addSandboxPolicy: (sandboxName: string, options?: PolicyAddOptions) => Promise<void>;
listSandboxPolicies: (sandboxName: string) => void;
removeSandboxPolicy: (sandboxName: string, options?: PolicyRemoveOptions) => Promise<void>;
};

Expand Down Expand Up @@ -331,3 +334,95 @@ describe("removeSandboxPolicy", () => {
expect(removePresetMock).not.toHaveBeenCalled();
});
});

describe("listSandboxPolicies", () => {
function arrangeListing({
appliedNames,
gatewayNames,
tier,
agent,
}: {
appliedNames: string[];
gatewayNames: string[] | null;
tier: string | null;
agent: string | null;
}): void {
getSandboxMock.mockReturnValue({
name: "test-sandbox",
agent,
policyTier: tier,
policies: appliedNames,
});
getAppliedPresetsMock.mockReturnValue(appliedNames);
vi.spyOn(policies, "getGatewayPresets").mockReturnValue(gatewayNames);
}

it("tags active tier-default presets with their tier provenance", () => {
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("tags openclaw-pricing as an OpenClaw agent preset", () => {
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 when applied to a Hermes sandbox", () => {
arrangeListing({
appliedNames: ["nous-web"],
gatewayNames: ["nous-web"],
tier: "open",
agent: "hermes",
});

listSandboxPolicies("test-sandbox");

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

it("tags user-added presets that are neither tier nor agent defaults", () => {
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", () => {
arrangeListing({
appliedNames: ["npm"],
gatewayNames: ["npm"],
tier: "balanced",
agent: "openclaw",
});

listSandboxPolicies("test-sandbox");

const output = printedText();
expect(output).toMatch(/○ pypi —/);
expect(output).not.toMatch(/○ pypi \[/);
});
});
19 changes: 18 additions & 1 deletion src/lib/actions/sandbox/policy-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const onboardProviders = require("../../onboard/providers");
import { filterSetupPolicyPresetsForAgent } from "../../onboard/agent-policy-presets";
import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config";
import * as policies from "../../policy";
import {
classifyPresetProvenance,
formatPresetProvenanceTag,
} from "../../policy/preset-provenance";

const onboardSession =
require("../../state/onboard-session") as typeof import("../../state/onboard-session");
Expand Down Expand Up @@ -268,6 +272,12 @@ export function listSandboxPolicies(sandboxName: string) {
// array of matched preset names when reachable (possibly empty).
const gatewayPresets = policies.getGatewayPresets(sandboxName);

const sandboxEntry = registry.getSandbox(sandboxName);
const provenanceContext = {
tierName: sandboxEntry?.policyTier ?? null,
agentName: sandboxEntry?.agent ?? null,
};

console.log("");
console.log(` Policy presets for sandbox '${sandboxName}':`);
allPresets.forEach((p: { name: string; description: string }) => {
Expand All @@ -291,7 +301,14 @@ export function listSandboxPolicies(sandboxName: string) {
marker = "○";
suffix = " (recorded locally, not active on gateway)";
}
console.log(` ${marker} ${p.name} — ${p.description}${suffix}`);

let provenanceTag = "";
if (marker === "●") {
provenanceTag = ` [${formatPresetProvenanceTag(
classifyPresetProvenance(p.name, provenanceContext),
)}]`;
}
console.log(` ${marker} ${p.name}${provenanceTag} — ${p.description}${suffix}`);
});

if (gatewayPresets === null) {
Expand Down
7 changes: 2 additions & 5 deletions src/lib/onboard/agent-policy-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { HERMES_TOOL_GATEWAY_PRESET_NAMES } from "./hermes-managed-tools";
import { isOpenclawAgent, OPENCLAW_OTEL_LOCAL_POLICY_PRESET } from "./openclaw-otel-policy-presets";
import { isOpenclawAgent, OPENCLAW_ONLY_POLICY_PRESETS } from "./openclaw-otel-policy-presets";

const OPENCLAW_ONLY_POLICY_PRESETS = new Set<string>([
"openclaw-pricing",
OPENCLAW_OTEL_LOCAL_POLICY_PRESET,
]);
export { OPENCLAW_ONLY_POLICY_PRESETS };

function isHermesAgent(agent: string | null | undefined): boolean {
return typeof agent === "string" && agent.trim().toLowerCase() === "hermes";
Expand Down
13 changes: 10 additions & 3 deletions src/lib/onboard/hermes-managed-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import type { HermesAuthMethod } from "../hermes-provider-auth";
import * as hermesProviderAuth from "../hermes-provider-auth";
import { HERMES_TOOL_GATEWAY_PRESET_NAMES } from "./hermes-tool-gateway-preset-names";

export { HERMES_TOOL_GATEWAY_PRESET_NAMES };

type PromptFn = (message: string) => Promise<string>;
type RawInput = NodeJS.ReadStream & {
Expand Down Expand Up @@ -52,9 +55,13 @@ export const HERMES_TOOL_GATEWAY_PRESETS = [
},
] as const;

export const HERMES_TOOL_GATEWAY_PRESET_NAMES = new Set<string>(
HERMES_TOOL_GATEWAY_PRESETS.map((preset) => preset.name),
);
for (const preset of HERMES_TOOL_GATEWAY_PRESETS) {
if (!HERMES_TOOL_GATEWAY_PRESET_NAMES.has(preset.name)) {
throw new Error(
`HERMES_TOOL_GATEWAY_PRESETS contains '${preset.name}' which is missing from the canonical name list in hermes-tool-gateway-preset-names.ts. Update the leaf list.`,
);
}
}

export function parseHermesToolGatewayPresetEnv(raw: string | null | undefined): string[] {
const values = String(raw || "")
Expand Down
14 changes: 14 additions & 0 deletions src/lib/onboard/hermes-tool-gateway-preset-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export const HERMES_TOOL_GATEWAY_PRESET_NAME_LIST = [
"nous-web",
"nous-image",
"nous-audio",
"nous-browser",
"nous-code",
] as const;

export const HERMES_TOOL_GATEWAY_PRESET_NAMES = new Set<string>(
HERMES_TOOL_GATEWAY_PRESET_NAME_LIST,
);
5 changes: 5 additions & 0 deletions src/lib/onboard/openclaw-otel-policy-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const LOCAL_OPENCLAW_OTEL_PORT = "4318";

export const OPENCLAW_OTEL_LOCAL_POLICY_PRESET = "openclaw-diagnostics-otel-local";

export const OPENCLAW_ONLY_POLICY_PRESETS = new Set<string>([
"openclaw-pricing",
OPENCLAW_OTEL_LOCAL_POLICY_PRESET,
]);

export function isOpenclawOtelEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const raw = env.NEMOCLAW_OPENCLAW_OTEL;
return (
Expand Down
136 changes: 136 additions & 0 deletions src/lib/policy/preset-provenance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

interface FakeTierPreset {
name: string;
access: string;
}
interface FakeTier {
name: string;
label: string;
description: string;
presets: FakeTierPreset[];
}

const TIER_FIXTURES: Record<string, FakeTier> = {
balanced: {
name: "balanced",
label: "Balanced",
description: "balanced fixture",
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" },
],
},
open: {
name: "open",
label: "Open",
description: "open fixture",
presets: [
{ name: "npm", access: "read-write" },
{ name: "slack", access: "read-write" },
{ name: "weather", access: "read" },
],
},
};

vi.mock("./tiers", () => ({
getTier: (name: string): FakeTier | undefined => TIER_FIXTURES[name],
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import { classifyPresetProvenance, formatPresetProvenanceTag } from "./preset-provenance";

describe("classifyPresetProvenance", () => {
it("classifies tier-default presets as tier-sourced for that tier", () => {
expect(classifyPresetProvenance("npm", { tierName: "balanced" })).toEqual({
source: "tier",
tier: "balanced",
});
expect(classifyPresetProvenance("weather", { tierName: "balanced" })).toEqual({
source: "tier",
tier: "balanced",
});
});

it("classifies tier-default presets under the Open tier too", () => {
expect(classifyPresetProvenance("slack", { tierName: "open" })).toEqual({
source: "tier",
tier: "open",
});
});

it("classifies openclaw-pricing as agent-sourced for openclaw", () => {
expect(
classifyPresetProvenance("openclaw-pricing", {
tierName: "balanced",
agentName: "openclaw",
}),
).toEqual({ source: "agent", agent: "openclaw" });
});

it("classifies openclaw-diagnostics-otel-local as openclaw-agent-sourced", () => {
expect(
classifyPresetProvenance("openclaw-diagnostics-otel-local", {
tierName: "balanced",
}),
).toEqual({ source: "agent", agent: "openclaw" });
});

it("classifies nous-* gateway presets as hermes-agent-sourced", () => {
expect(classifyPresetProvenance("nous-web", { tierName: "open" })).toEqual({
source: "agent",
agent: "hermes",
});
expect(classifyPresetProvenance("nous-code", {})).toEqual({
source: "agent",
agent: "hermes",
});
});

it("falls back to user-source for non-tier, non-agent presets", () => {
expect(classifyPresetProvenance("custom-private", { tierName: "balanced" })).toEqual({
source: "user",
});
});

it("treats missing tier context as no tier match", () => {
expect(classifyPresetProvenance("npm", {})).toEqual({ source: "user" });
expect(classifyPresetProvenance("npm", { tierName: null })).toEqual({
source: "user",
});
});

it("normalises preset name casing", () => {
expect(classifyPresetProvenance("OPENCLAW-PRICING", { tierName: "balanced" })).toEqual({
source: "agent",
agent: "openclaw",
});
});
});

describe("formatPresetProvenanceTag", () => {
it("renders the tier source as 'from <tier> tier'", () => {
expect(formatPresetProvenanceTag({ source: "tier", tier: "balanced" })).toBe(
"from balanced tier",
);
});

it("renders the agent source as 'from <agent> agent'", () => {
expect(formatPresetProvenanceTag({ source: "agent", agent: "openclaw" })).toBe(
"from openclaw agent",
);
expect(formatPresetProvenanceTag({ source: "agent", agent: "hermes" })).toBe(
"from hermes agent",
);
});

it("renders the user source as 'user-added'", () => {
expect(formatPresetProvenanceTag({ source: "user" })).toBe("user-added");
});
});
Loading
Loading