Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ Four tiers are available:
| Restricted | No tier defaults. Web search or other integrations selected earlier can still add their required presets; deselect them during policy review for baseline-only access. |
| Balanced (default) | Full dev tooling and a selected, supported web search provider. Package installs, model downloads, and inference. No messaging platform access by default. |
| Open | Broad access across third-party services including supported messaging and productivity presets. Agent-specific unsupported presets are filtered out. |
| Personal | Lets every sandbox binary open TCP connections to public and private address ranges on destination ports 80 and 443. Unspecified, loopback, and link-local ranges remain blocked. Also selects every maintained preset supported by the active agent. Intended only for trusted personal-use workloads. |
| Personal | Selects one broad web authority that lets every sandbox binary open TCP connections to public and private address ranges on destination ports 80 and 443. It replaces overlapping exact web endpoints while preserving non-web policy. Unspecified, loopback, and link-local ranges remain blocked. Intended only for trusted personal-use workloads. |

After selecting a tier, the wizard shows a combined preset and access-mode screen where you can include or exclude individual presets and toggle each between read and read-write access.
For details on tiers and the presets each includes, refer to [Network Policies](network-policies#policy-tiers).
Expand All @@ -665,7 +665,8 @@ Onboarding removes any preset that is not in the list.
`skip` leaves the applied set untouched and does not apply tier defaults.
NemoClaw filters tier suggestions and resume selections by active agent support and the selected web search provider.
During automatic suggestion and resume reconciliation, it removes stale web-search selections when they conflict with the active agent or selected provider.
The Personal tier is the exception: it preserves every applicable maintained web-search preset even when onboarding did not configure that provider.
The Personal tier instead uses `personal-open-internet` for web transport and does not select Brave Search or Tavily Search merely to enable ordinary web fetches.
This makes keyless fetches available to any sandbox binary, but it does not add a provider-free `web_search` implementation.
<AgentOnly variant="hermes">
For Hermes, this includes replacing stale `nous-web` when Tavily is selected.
</AgentOnly>
Expand Down
19 changes: 10 additions & 9 deletions docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ This is the supported registry-backed way to replay a live exact-key removal dur
| Restricted | No tier defaults | Starts from the baseline policy. Web search or messaging integrations selected earlier can still suggest their required presets; deselect them during policy review for baseline-only access. Restricted suppresses other agent-required additions; reapply them later with `policy add` only after reviewing the additional egress. |
| Balanced (default) | `npm`, `pypi`, `huggingface`, `brew`, selected `brave` or `tavily` web search preset | Full dev tooling and web search when you select a provider the active agent supports. No messaging platform access. Apply the `weather` preset explicitly if your agent needs read-only weather lookups. |
| Open | `npm`, `pypi`, `huggingface`, `brew`, selected `brave` or `tavily` web search preset, `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. |
| Personal | `personal-open-internet` and every maintained preset supported by the active agent | Lets every sandbox binary open TCP connections to public and private address ranges on destination ports `80` and `443`. Unspecified, loopback, and link-local ranges remain blocked. Also selects every maintained preset applicable to the active agent. |
| Personal | `personal-open-internet` | Lets every sandbox binary open TCP connections to public and private address ranges on destination ports `80` and `443`. The broad route replaces overlapping web endpoints while preserving non-web policy. Unspecified, loopback, and link-local ranges remain blocked. |

<Warning title="Personal Tier Network Access">
The Personal tier applies the `personal-open-internet` policy preset with a hostless L4 endpoint on destination ports `80` and `443`.
Expand All @@ -155,10 +155,10 @@ The sandbox's filesystem, process, gateway authentication, and managed credentia
Use this tier only for trusted personal workloads with trusted prompts and data.
</Warning>

For a fresh onboarding run, the experimental portable profile selects `weather`, `public-reference`, and `github` when `NEMOCLAW_POLICY_PRESETS` is unset, blank, or contains only whitespace.
This default does not select `personal-open-internet`.
For a fresh onboarding run, the experimental portable profile selects the Personal tier in `suggested` mode when `NEMOCLAW_POLICY_PRESETS` is unset, blank, or contains only whitespace.
The resulting tier selection applies `personal-open-internet`.
If `NEMOCLAW_POLICY_PRESETS` contains a non-blank list, portable onboarding treats that explicit list as authoritative instead.
`$$nemoclaw onboard --resume` does not inject the fresh-onboarding default.
`$$nemoclaw onboard --resume` does not inject the fresh-onboarding Personal selection.

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 built-in preset list available to the sandbox's active agent.
Expand All @@ -182,16 +182,17 @@ These modes skip policy preset additions, but they do not preserve a disabled ch
`$$nemoclaw onboard --resume` reconciles the policy selection instead of skipping it when the effective messaging selection omits a channel whose preset remains applied.
NemoClaw keeps the preset for an in-sandbox QR-paired channel such as WhatsApp because you pair that channel inside the sandbox rather than through host environment values.
</AgentOnly>
The Personal tier instead keeps every applicable maintained web-search preset selected by default.
The Personal tier does not select a Brave Search or Tavily Search preset by default.
Its broad route supports ordinary keyless web fetches, while `web_search` still requires a separately configured provider.
Explicit custom preset lists and manual interactive selections remain operator-controlled.
<AgentOnly variant="hermes">
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.
When Hermes uses Tavily, NemoClaw removes `nous-web` from the effective managed-tool selection while preserving other selected Nous tool presets.
</AgentOnly>
<AgentOnly variant="openclaw">
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.
On the Balanced and Open tiers, enabling OpenClaw OTEL diagnostics with a local endpoint adds the `openclaw-diagnostics-otel-local` preset.
The Personal tier selects that preset by default, while Restricted suppresses it during reconciliation.
OpenClaw onboarding also adds the `openclaw-pricing` preset on top of Balanced and Open tier defaults so session-cost records can populate from LiteLLM and OpenRouter without manual configuration.
On the Balanced, Open, and Personal tiers, enabling OpenClaw OTEL diagnostics with a local endpoint adds the `openclaw-diagnostics-otel-local` preset.
Personal suppresses the overlapping pricing route because `personal-open-internet` already owns ports `80` and `443`, while Restricted suppresses it to retain the restricted posture.
</AgentOnly>
<AgentOnly variant="deepagents">
When LangChain Deep Agents Code is onboarded with `--observability`, NemoClaw adds the `observability-otlp-local` preset on Balanced, Open, and Personal tiers.
Expand All @@ -202,7 +203,7 @@ The applied set therefore reflects the chosen tier *plus* any agent-required pre
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; agent-specific preset names are only labelled `[from <agent> agent]` when the active agent matches.
Claude Code direct egress is not included in the Restricted, Balanced, or Open tiers.
The Personal tier selects the `claude-code` preset by default; on other tiers, apply it explicitly if you install and run the Claude Code CLI inside the sandbox with its own credentials.
Personal's broad route permits its port `443` transport but does not install or configure Claude Code; on other tiers, apply the `claude-code` preset explicitly if you install and run the CLI inside the sandbox with its own credentials.
Normal NemoClaw Anthropic inference still routes through the OpenShell gateway.

Tier definitions are stored in `nemoclaw-blueprint/policies/tiers.yaml`.
Expand Down
4 changes: 2 additions & 2 deletions docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ The preset separately grants that registry transport to its listed npm, Yarn, an
</AgentOnly>

<Warning title="Personal Tier">
The Personal tier selects `personal-open-internet` and every maintained preset supported by the active agent.
The Personal tier selects `personal-open-internet` as its web authority and preserves non-web policy.
The open-internet preset allows every sandbox binary to reach public and private address ranges on destination ports `80` and `443` through L4 passthrough.
Traffic on those ports is not limited to HTTP or HTTPS.
OpenShell does not inspect the hostname, application protocol, HTTP method, path, or body for those connections.
Expand Down Expand Up @@ -372,7 +372,7 @@ Use only for a trusted single-user sandbox that needs arbitrary TCP egress on de
- Select the Personal tier during onboarding.
- Treat every prompt, downloaded package, webpage, and workspace file as able to trigger external TCP traffic on destination ports `80` and `443`.
- Do not place raw credentials or sensitive data in the sandbox unless the agent must use them.
- Return to Balanced or Restricted and recreate the sandbox when this broad egress is no longer required.
- Create a new Balanced or Restricted sandbox when this broad egress is no longer required. NemoClaw refuses to remove Personal in place because its normalized policy has already replaced overlapping web entries.

### Integration Testing

Expand Down
38 changes: 4 additions & 34 deletions nemoclaw-blueprint/policies/tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#
# Tier definitions for the NemoClaw policy selector.
#
# Each tier maps to a named posture. The base sandbox policy
# (openclaw-sandbox.yaml) is always applied regardless of tier.
# Tiers add presets on top of that baseline.
# Each tier maps to a named posture. The base sandbox policy is always the
# starting point. Personal then replaces overlapping port 80/443 endpoints
# with its single broad L4 authority while preserving non-web policy.
#
# access values:
# read — GET only (or equivalent read-only rules for that service)
Expand Down Expand Up @@ -52,36 +52,6 @@ tiers:

- name: personal
label: Personal
description: Trusted personal-use posture. Lets every sandbox binary open TCP connections to public and private address ranges on destination ports 80 and 443. Unspecified, loopback, and link-local ranges remain blocked. Also enables every maintained preset that applies to the selected agent. Do not use with untrusted prompts or data.
description: Trusted personal-use posture. Lets every sandbox binary open TCP connections to public and private address ranges on destination ports 80 and 443. Unspecified, loopback, and link-local ranges remain blocked. This broad route replaces overlapping web endpoints while active. Do not use with untrusted prompts or data.
presets:
- { name: personal-open-internet, access: read-write }
- { 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: tavily, access: read-write }
- { name: weather, access: read-write }
- { name: public-reference, access: read-write }
- { name: github, access: read-write }
- { name: gmail, access: read-write }
- { name: jira, access: read-write }
- { name: outlook, access: read-write }
- { name: claude-code, access: read-write }
- { name: local-inference, access: read-write }
- { name: local-memory, access: read-write }
- { name: openclaw-pricing, access: read-write }
- { name: openclaw-diagnostics-otel-local, access: read-write }
- { name: observability-otlp-local, access: read-write }
- { name: nous-web, access: read-write }
- { name: nous-image, access: read-write }
- { name: nous-audio, access: read-write }
- { name: nous-browser, access: read-write }
- { name: nous-code, access: read-write }
- { name: slack, access: read-write }
- { name: discord, access: read-write }
- { name: telegram, access: read-write }
- { name: googlechat, access: read-write }
- { name: wechat, access: read-write }
- { name: whatsapp, access: read-write }
- { name: teams, access: read-write }
31 changes: 28 additions & 3 deletions src/lib/onboard/policy-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { type WebSearchConfig, webSearchProviderForConfig } from "../inference/web-search";
import * as policies from "../policy";
import * as tiers from "../policy/tiers";
import { PERSONAL_POLICY_TIER_NAME } from "../policy/tiers";
import { PERSONAL_OPEN_INTERNET_PRESET_NAME, PERSONAL_POLICY_TIER_NAME } from "../policy/tiers";
import {
filterSetupPolicyPresetNamesForAgent,
filterSetupPolicyPresetsForAgent,
Expand Down Expand Up @@ -43,6 +43,7 @@ import * as policyTierEnv from "./policy-tier-env";
import {
agentRequiredPresetAdditions,
emitSuppressedAgentRequiredPresetsNote,
filterSuppressedAgentRequiredPresets,
RESTRICTED_TIER_NAME,
} from "./policy-tier-suppression";
import { withPolicyApplicationTrace } from "./tracing";
Expand Down Expand Up @@ -323,7 +324,7 @@ export function computeSetupPresetSuggestions(
if (HERMES_TOOL_GATEWAY_PRESET_NAMES.has(preset)) add(preset);
}
}
return suggestions;
return filterSuppressedAgentRequiredPresets(suggestions, tierName, agent);
}

export { type PreparedPolicyResumeSelection, preparePolicyPresetResumeSelection };
Expand Down Expand Up @@ -357,6 +358,18 @@ function requireSandboxReady(
}
}

function refuseInPlacePersonalRemoval(
personalAlreadyActive: boolean,
target: readonly string[],
): void {
if (personalAlreadyActive && !target.includes(PERSONAL_OPEN_INTERNET_PRESET_NAME)) {
console.error(
" Personal open internet cannot be removed in place because it replaces overlapping web routes. Create a new sandbox with another policy tier instead.",
);
process.exit(1);
}
}

async function setupPoliciesWithSelectionInner(
deps: SetupPolicySelectionDeps,
sandboxName: string,
Expand Down Expand Up @@ -453,7 +466,12 @@ async function setupPoliciesWithSelectionInner(
// still get filtered. An interrupted create can reach this fresh-selection
// branch before presets are recorded, so its persisted tier must also win
// over a new prompt or non-interactive default.
const recordedTierName = options.tierName ?? deps.getRecordedPolicyTier?.(sandboxName) ?? null;
const persistedTierName = deps.getRecordedPolicyTier?.(sandboxName) ?? null;
const recordedTierName = options.tierName ?? persistedTierName;
const personalAlreadyActive =
currentAppliedPresets.includes(PERSONAL_OPEN_INTERNET_PRESET_NAME) ||
persistedTierName === PERSONAL_POLICY_TIER_NAME ||
(selectedPresets !== null && options.tierName === PERSONAL_POLICY_TIER_NAME);
if (chosen !== null) {
const knownSelectablePresets = new Set(selectablePresets.map((preset) => preset.name));
chosen = mergeRequiredSetupPolicyPresets(chosen, {
Expand All @@ -476,6 +494,7 @@ async function setupPoliciesWithSelectionInner(

if (selectedPresets !== null) {
const resumeSelection = chosen || [];
refuseInPlacePersonalRemoval(personalAlreadyActive, resumeSelection);
if (onSelection) onSelection(resumeSelection);
requireSandboxReady(deps, sandboxName, "before");
deps.note(` [resume] Reapplying policy presets: ${resumeSelection.join(", ")}`);
Expand All @@ -485,6 +504,9 @@ async function setupPoliciesWithSelectionInner(
}

const tierName = recordedTierName ?? (await deps.selectPolicyTier());
if (personalAlreadyActive && tierName !== PERSONAL_POLICY_TIER_NAME) {
refuseInPlacePersonalRemoval(personalAlreadyActive, []);
}
deps.setPolicyTier?.(sandboxName, tierName);
const personalTier = tierName === PERSONAL_POLICY_TIER_NAME;
const suggestions = excludePresets(
Expand Down Expand Up @@ -516,6 +538,7 @@ async function setupPoliciesWithSelectionInner(
deps.note(" [non-interactive] Skipping policy presets.");
const retainedPresets = excludePresets(pruneUnavailablePresets(currentAppliedPresets));
if (retainedPresets.length < currentAppliedPresets.length) {
refuseInPlacePersonalRemoval(personalAlreadyActive, retainedPresets);
if (onSelection) onSelection(retainedPresets);
requireSandboxReady(deps, sandboxName, "before");
deps.note(" [non-interactive] Removing excluded or unavailable policy presets.");
Expand Down Expand Up @@ -596,6 +619,7 @@ async function setupPoliciesWithSelectionInner(
}
}

refuseInPlacePersonalRemoval(personalAlreadyActive, chosen);
if (onSelection) onSelection(chosen);
requireSandboxReady(deps, sandboxName, "before");
deps.note(` [non-interactive] Applying policy presets: ${chosen.join(", ")}`);
Expand Down Expand Up @@ -635,6 +659,7 @@ async function setupPoliciesWithSelectionInner(
),
);

refuseInPlacePersonalRemoval(personalAlreadyActive, interactiveChoice);
if (onSelection) onSelection(interactiveChoice);
requireSandboxReady(deps, sandboxName, "before");

Expand Down
Loading
Loading