diff --git a/docs/reference/network-policies.mdx b/docs/reference/network-policies.mdx
index 34eb9882a45..45cd90b6ab6 100644
--- a/docs/reference/network-policies.mdx
+++ b/docs/reference/network-policies.mdx
@@ -67,6 +67,13 @@ The baseline policy is always applied regardless of the selected tier.
| 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` | Full dev tooling and web search for agents that support web search. No messaging platform access. |
| Open | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported`, `slack`, `discord`, `telegram`, `wechat` (experimental), `whatsapp` (experimental), `jira`, `outlook` | Broad access across third-party services including messaging and productivity. |
+| Allow All | None — swaps the base policy | Catch-all egress to any public host (no network filtering). Opt-in for trusted dev/testing only. |
+
+
+The **Allow All** tier disables egress filtering. Instead of layering presets onto the deny-by-default baseline, it swaps the base policy for `nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml`, a single catch-all (`host: "*"`) that lets the agent reach **any** public host on ports 80/443. Every reachable host is a potential data-exfiltration path for workspace files, credentials, or conversation history. SSRF and private-network blocking still apply — `localhost`, link-local, and RFC1918 ranges remain blocked — but no HTTP-level filtering is performed. Use it only for trusted development or testing, and prefer a specific tier plus presets, or [operator approval](#operator-approval-flow), for everything else.
+
+
+Allow All has no presets, so the combined preset screen is skipped. Enable it non-interactively with `NEMOCLAW_POLICY_TIER=allow-all`, or apply it to an already-running sandbox with `nemoclaw sandbox:shields:down --policy allow-all`.
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.
diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx
index 11539658778..196ec5890e7 100644
--- a/docs/security/best-practices.mdx
+++ b/docs/security/best-practices.mdx
@@ -104,6 +104,10 @@ The sandbox blocks all outbound connections unless you explicitly list the endpo
| Risk if relaxed | Each allowed endpoint is a potential data exfiltration path. The agent can send workspace content, credentials, or conversation history to any reachable host. |
| Recommendation | Add only endpoints the agent needs for its task. Prefer operator approval for one-off requests over permanently widening the baseline. |
+
+The **Allow All** policy tier (`NEMOCLAW_POLICY_TIER=allow-all`) and `nemoclaw sandbox:shields:down --policy allow-all` disable egress filtering entirely with a catch-all `host: "*"` rule. This removes the deny-by-default protection above for the selected sandbox: the agent can reach **any** public host. SSRF and private-network blocking still apply (`localhost`, link-local, and RFC1918 ranges stay blocked), but no host, method, or path filtering is performed. Treat allow-all as trusted-development-only. It is never the shipped default — deny-by-default remains in effect unless you opt in.
+
+
### Binary-Scoped Endpoint Rules
Each network policy entry restricts which executables can reach the endpoint using the `binaries` field.
diff --git a/nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml b/nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml
new file mode 100644
index 00000000000..c0c62232ab2
--- /dev/null
+++ b/nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml
@@ -0,0 +1,67 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Allow-all network posture — opt-in via the "allow-all" policy tier
+# (NEMOCLAW_POLICY_TIER=allow-all) or `shields down --policy allow-all`.
+#
+# A single catch-all network policy (host: "*") permits egress to ANY public
+# host on ports 80/443 with full L7 access. This is the only NemoClaw policy
+# that uses a bare "*" host; the dangerous-host guardrail in
+# scripts/validate-configs.ts exempts files named *-allow-all.yaml for exactly
+# this reason (subdomain wildcards remain the norm everywhere else).
+#
+# WARNING: This disables egress filtering. Every reachable host becomes a
+# potential data-exfiltration path (workspace files, credentials, conversation
+# history). SSRF / private-network blocking (ssrf.ts, private-networks.ts) still
+# applies — localhost, link-local, and RFC1918 ranges remain blocked. Do not use
+# in production. Prefer a specific tier + presets, or operator approval, instead.
+#
+# NOTE: bare host "*" support in the OpenShell L7 proxy must be verified on a
+# live sandbox before relying on this file. If the proxy rejects "*", switch the
+# endpoints below to `enforcement: audit` (log-only, non-blocking). See the
+# allow-all rollout task / docs/reference/network-policies.mdx.
+
+version: 1
+
+filesystem_policy:
+ # Mirrors openclaw-sandbox-permissive.yaml. OpenShell rejects include_workdir
+ # changes and filesystem path removals on live sandboxes.
+ include_workdir: true
+ read_only:
+ - /usr
+ - /lib
+ - /proc
+ - /dev/urandom
+ - /app
+ - /etc
+ - /var/log
+ read_write:
+ - /tmp
+ - /dev/null
+ - /sandbox/.openclaw
+ - /sandbox/.nemoclaw
+ - /home/linuxbrew
+
+landlock:
+ compatibility: best_effort
+
+process:
+ run_as_user: sandbox
+ run_as_group: sandbox
+
+network_policies:
+ allow_all:
+ name: allow_all
+ endpoints:
+ - host: "*"
+ port: 443
+ protocol: rest
+ enforcement: enforce
+ access: full
+ - host: "*"
+ port: 80
+ protocol: rest
+ enforcement: enforce
+ access: full
+ binaries:
+ - { path: "/**" }
diff --git a/nemoclaw-blueprint/policies/tiers.yaml b/nemoclaw-blueprint/policies/tiers.yaml
index 8a090549712..5073c3458f7 100644
--- a/nemoclaw-blueprint/policies/tiers.yaml
+++ b/nemoclaw-blueprint/policies/tiers.yaml
@@ -43,3 +43,14 @@ tiers:
- { name: whatsapp, access: read-write }
- { name: jira, access: read-write }
- { name: outlook, access: read-write }
+
+ # DANGER: disables egress filtering. Unlike the other tiers, allow-all does
+ # NOT layer presets onto the deny-by-default base — it swaps the base policy
+ # for openclaw-sandbox-allow-all.yaml (a catch-all host: "*"), so the agent can
+ # reach ANY public host. SSRF / private-network blocking still applies. Has no
+ # presets by design; the catch-all makes per-service presets moot. Opt in only
+ # for trusted dev/testing. See docs/reference/network-policies.mdx.
+ - name: allow-all
+ label: Allow All (no egress filtering — dev/testing only)
+ description: Catch-all egress to any public host. Swaps the base policy for the allow-all catch-all; presets do not apply. Maximum scope — you accept full responsibility.
+ presets: []
diff --git a/scripts/validate-configs.ts b/scripts/validate-configs.ts
index cd24310fd43..3da26662ea4 100755
--- a/scripts/validate-configs.ts
+++ b/scripts/validate-configs.ts
@@ -9,7 +9,7 @@
// npx tsx scripts/validate-configs.ts # validate all known config files
// npx tsx scripts/validate-configs.ts --file --schema # validate one file
-import { existsSync, readFileSync, readdirSync } from "node:fs";
+import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
@@ -46,6 +46,7 @@ function discoverTargets(): ConfigTarget[] {
files: [
"nemoclaw-blueprint/policies/openclaw-sandbox.yaml",
"nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml",
+ "nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml",
],
},
{
@@ -209,6 +210,24 @@ interface DangerousHostFinding {
host: string;
}
+/**
+ * Designated allow-all policy files (basename `*-allow-all.yaml`) are the one
+ * place a bare catch-all `host: "*"` is intentional — they implement the opt-in
+ * allow-all posture. Such files are exempted ONLY for the bare wildcard family
+ * (`*` / `*:port`); IP catch-alls (`0.0.0.0/0`, `::/0`, …) stay rejected
+ * everywhere so a typo can never silently widen egress to raw address ranges.
+ */
+function isAllowAllPolicyFile(file: string): boolean {
+ const base = file.replaceAll("\\", "/").split("/").pop() ?? "";
+ return /-allow-all\.ya?ml$/.test(base);
+}
+
+/** True for the bare wildcard catch-all family ("*" / "*:443"), excluding IP catch-alls. */
+function isBareWildcardHost(host: string): boolean {
+ const trimmed = host.trim();
+ return trimmed === "*" || trimmed.startsWith("*:");
+}
+
const ROUTER_API_BASE_HOST_ALLOWLIST: ReadonlySet = new Set(["integrate.api.nvidia.com"]);
/**
@@ -339,7 +358,15 @@ function main(): void {
const schemaErrors = !valid && validate.errors ? validate.errors.length : 0;
// Semantic check: walk the parsed doc and reject catch-all hosts.
// Runs regardless of schema outcome so operators see all issues at once.
- const dangerous = [...findDangerousHosts(data), ...findDangerousRouterApiBases(data)];
+ // Designated allow-all files may use the bare wildcard ("*"/"*:port") on
+ // purpose; everything else (and all IP catch-alls) stays rejected.
+ const allowBareWildcard = isAllowAllPolicyFile(file);
+ const dangerous = [
+ ...findDangerousHosts(data).filter(
+ (finding) => !(allowBareWildcard && isBareWildcardHost(finding.host)),
+ ),
+ ...findDangerousRouterApiBases(data),
+ ];
if (schemaErrors > 0 || dangerous.length > 0) {
console.error(`FAIL: ${file}`);
@@ -373,11 +400,13 @@ function main(): void {
// Export for unit tests without re-running main().
export {
DANGEROUS_HOSTS,
- ROUTER_API_BASE_HOST_ALLOWLIST,
- isDangerousHost,
+ discoverTargets,
findDangerousHosts,
findDangerousRouterApiBases,
- discoverTargets,
+ isAllowAllPolicyFile,
+ isBareWildcardHost,
+ isDangerousHost,
+ ROUTER_API_BASE_HOST_ALLOWLIST,
};
// Only run main() when invoked directly (skip on test `import`).
diff --git a/src/commands/sandbox/shields/down.ts b/src/commands/sandbox/shields/down.ts
index 97ed6ecfc2b..a8eee5a752c 100644
--- a/src/commands/sandbox/shields/down.ts
+++ b/src/commands/sandbox/shields/down.ts
@@ -2,11 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
import { Flags } from "@oclif/core";
-import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
-
import { shieldsTimeoutDurationFlag } from "../../../lib/cli/duration-flags";
-import * as shields from "../../../lib/shields/index";
+import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
import { sandboxNameArg } from "../../../lib/sandbox/command-support";
+import * as shields from "../../../lib/shields/index";
export default class ShieldsDownCommand extends NemoClawCommand {
static id = "sandbox:shields:down";
@@ -14,12 +13,16 @@ export default class ShieldsDownCommand extends NemoClawCommand {
static strict = true;
static summary = "Lower sandbox security shields";
static description = "Temporarily lower sandbox shields.";
- static usage = [" [--timeout 5m] [--reason ] [--policy permissive]"];
+ static usage = [" [--timeout 5m] [--reason ] [--policy permissive|allow-all]"];
static args = { sandboxName: sandboxNameArg };
static flags = {
timeout: shieldsTimeoutDurationFlag({ description: "Duration before shields are restored" }),
reason: Flags.string({ description: "Reason for lowering shields" }),
- policy: Flags.string({ description: "Policy to apply while shields are down" }),
+ policy: Flags.string({
+ description:
+ 'Policy while shields are down: "permissive" (full access on known hosts), ' +
+ '"allow-all" (catch-all egress to any host — dev/testing only), or a path to a YAML file',
+ }),
};
public async run(): Promise {
diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts
index b0016494038..0a0816a9025 100644
--- a/src/lib/onboard.ts
+++ b/src/lib/onboard.ts
@@ -3364,7 +3364,23 @@ async function createSandbox(
"policies",
"openclaw-sandbox.yaml",
);
- const basePolicyPath = (agent && agentOnboard.getAgentPolicyPath(agent)) || defaultPolicyPath;
+ let basePolicyPath = (agent && agentOnboard.getAgentPolicyPath(agent)) || defaultPolicyPath;
+ // Allow-all is not a preset bundle — it swaps the base policy for the
+ // catch-all (host: "*"). At create time the tier is known only when it comes
+ // from the environment (non-interactive) or a recorded recreate; interactive
+ // fresh onboards choose the tier post-create and apply it via
+ // setupPoliciesWithSelection. Swapping the base here means an env/recreate
+ // allow-all sandbox boots permissive from the very first request.
+ const recordedTier = registry.getSandbox(sandboxName)?.policyTier ?? null;
+ const envTier = (process.env.NEMOCLAW_POLICY_TIER || "").trim().toLowerCase();
+ const allowAllAtCreate =
+ envTier === "allow-all" || (recordedTier === "allow-all" && !envTier);
+ if (allowAllAtCreate) {
+ basePolicyPath = policies.ALLOW_ALL_POLICY_PATH;
+ console.log(
+ " Policy tier 'allow-all': booting with catch-all egress (no network filtering).",
+ );
+ }
const tokensByEnvKey = Object.fromEntries(
messagingTokenDefs.map(({ envKey, token }) => [envKey, token]),
);
@@ -6335,6 +6351,7 @@ async function setupPoliciesWithSelection(
syncPresetSelection,
selectPolicyTier,
setPolicyTier: (sandbox, tierName) => registry.updateSandbox(sandbox, { policyTier: tierName }),
+ applyAllowAllPolicy: (sandbox) => policies.applyAllowAllPolicy(sandbox),
selectTierPresetsAndAccess,
parsePolicyPresetEnv,
env: process.env,
diff --git a/src/lib/onboard/policy-selection.ts b/src/lib/onboard/policy-selection.ts
index 25b83ca3a73..a70d12c9dc1 100644
--- a/src/lib/onboard/policy-selection.ts
+++ b/src/lib/onboard/policy-selection.ts
@@ -73,6 +73,13 @@ export type SetupPolicySelectionDeps = {
) => void;
selectPolicyTier: () => Promise;
setPolicyTier?: (sandboxName: string, tierName: string) => void;
+ /**
+ * Apply the allow-all catch-all policy to a running sandbox. Used when the
+ * "allow-all" tier is chosen interactively (a fresh onboard creates with the
+ * default base, so the catch-all must be applied post-create). Env/recreate
+ * allow-all sandboxes already boot with the catch-all base.
+ */
+ applyAllowAllPolicy?: (sandboxName: string) => void;
selectTierPresetsAndAccess: (
tierName: string,
presets: Preset[],
@@ -330,6 +337,22 @@ async function setupPoliciesWithSelectionInner(
const tierName = await deps.selectPolicyTier();
deps.setPolicyTier?.(sandboxName, tierName);
+
+ // Allow-all is a catch-all posture, not a preset bundle. Apply the catch-all
+ // policy directly and skip preset selection entirely. (Env/recreate
+ // allow-all already booted with the catch-all base; this covers the
+ // interactive fresh-onboard case, and is idempotent for the others.)
+ if (tierName === "allow-all") {
+ if (onSelection) onSelection([]);
+ if (!deps.waitForSandboxReady(sandboxName)) {
+ console.error(` Sandbox '${sandboxName}' was not ready for policy application.`);
+ process.exit(1);
+ }
+ deps.note(" Applying allow-all policy: catch-all egress to any host (no network filtering).");
+ deps.applyAllowAllPolicy?.(sandboxName);
+ return [];
+ }
+
const suggestions = pruneDisabledPresets(
computeSetupPresetSuggestions(deps, tierName, {
enabledChannels,
diff --git a/src/lib/policy/index.ts b/src/lib/policy/index.ts
index e7218398dee..9b9ab9402c2 100644
--- a/src/lib/policy/index.ts
+++ b/src/lib/policy/index.ts
@@ -3,7 +3,7 @@
//
// Policy preset management — list, load, merge, and apply presets.
-import type { JsonValue, JsonObject } from "../core/json-types";
+import type { JsonObject, JsonValue } from "../core/json-types";
const fs = require("fs");
const path = require("path");
@@ -13,6 +13,8 @@ const YAML = require("yaml");
const { ROOT, run, runCapture } = require("../runner");
const registry = require("../state/registry");
const { loadAgent } = require("../agent/defs");
+const { cleanupTempDir } = require("../onboard/temp-files");
+const { buildRuntimePermissivePolicy } = require("../shields/permissive-runtime");
// Late-binding access via the module exports so tests can spy on
// resolveOpenshell without rewiring requires.
const openshellResolveModule = require("../adapters/openshell/resolve");
@@ -1228,6 +1230,23 @@ const PERMISSIVE_POLICY_PATH = path.join(
"openclaw-sandbox-permissive.yaml",
);
+const ALLOW_ALL_POLICY_PATH = path.join(
+ ROOT,
+ "nemoclaw-blueprint",
+ "policies",
+ "openclaw-sandbox-allow-all.yaml",
+);
+
+/**
+ * Resolve the on-disk path to the allow-all catch-all policy. Unlike the
+ * permissive policy there is no agent-specific variant: the catch-all
+ * (`host: "*"`) is agent-independent, and live filesystem paths are unioned in
+ * at apply time via {@link buildRuntimePermissivePolicy}.
+ */
+function resolveAllowAllPolicyPath(_sandboxName?: string): string {
+ return ALLOW_ALL_POLICY_PATH;
+}
+
/**
* Resolve the on-disk path to the permissive policy YAML for the given
* sandbox, honoring the agent-specific override registered in
@@ -1271,35 +1290,75 @@ function applyPermissivePolicy(sandboxName: string): void {
console.log(" Applied permissive policy.");
}
+/**
+ * Apply the allow-all catch-all policy to a running sandbox. Unions the live
+ * sandbox's filesystem paths into the static allow-all baseline (OpenShell
+ * rejects path removals on a live sandbox; runtime-injected paths such as /proc
+ * on GPU or /opt/hermes are absent from the static YAML). Mirrors the runtime
+ * handling used by `shields down`.
+ */
+function applyAllowAllPolicy(sandboxName: string): void {
+ const isRfc1123Label = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName);
+ if (!sandboxName || sandboxName.length > 63 || !isRfc1123Label) {
+ throw new Error(
+ `Invalid or truncated sandbox name: '${sandboxName}'. ` +
+ `Names must be 1-63 chars, lowercase alphanumeric, with optional internal hyphens.`,
+ );
+ }
+
+ const basePath = resolveAllowAllPolicyPath(sandboxName);
+ if (!fs.existsSync(basePath)) {
+ throw new Error(`Allow-all policy not found: ${basePath}`);
+ }
+
+ assertOpenshellResolvable();
+ const livePolicyYaml = runCapture(buildPolicyGetCommand(sandboxName), { ignoreError: true });
+ const policyFile = buildRuntimePermissivePolicy(basePath, {
+ livePolicyYaml: livePolicyYaml || null,
+ readBasePolicy: () => fs.readFileSync(basePath, "utf-8"),
+ });
+ const isTemp = policyFile !== basePath;
+ console.log(" Applying allow-all policy...");
+ try {
+ run(buildPolicySetCommand(policyFile, sandboxName));
+ } finally {
+ if (isTemp) cleanupTempDir(policyFile, "nemoclaw-permissive-runtime");
+ }
+ console.log(" Applied allow-all policy.");
+}
+
export {
- PRESETS_DIR,
- PERMISSIVE_POLICY_PATH,
- listPresets,
- loadPreset,
+ ALLOW_ALL_POLICY_PATH,
+ applyAllowAllPolicy,
+ applyPermissivePolicy,
+ applyPreset,
+ applyPresetContent,
+ applyPresets,
+ assertOpenshellResolvable,
+ buildPolicyGetCommand,
+ buildPolicySetCommand,
+ clampSetupPolicyPresetNames,
+ extractPresetEntries,
+ filterSetupPolicyPresets,
+ getAppliedPresets,
+ getGatewayPresets,
getPresetEndpoints,
getPresetValidationWarning,
- setupPolicyPresetSupported,
- filterSetupPolicyPresets,
+ listCustomPresets,
+ listPresets,
listSetupPolicyPresets,
- clampSetupPolicyPresetNames,
- extractPresetEntries,
- parseCurrentPolicy,
- buildPolicySetCommand,
- buildPolicyGetCommand,
- assertOpenshellResolvable,
+ loadPreset,
+ loadPresetFromFile,
mergePresetIntoPolicy,
mergePresetNamesIntoPolicy,
- removePresetFromPolicy,
- applyPreset,
- applyPresets,
- applyPresetContent,
- loadPresetFromFile,
+ PERMISSIVE_POLICY_PATH,
+ PRESETS_DIR,
+ parseCurrentPolicy,
removePreset,
- applyPermissivePolicy,
+ removePresetFromPolicy,
+ resolveAllowAllPolicyPath,
resolvePermissivePolicyPath,
- getAppliedPresets,
- getGatewayPresets,
- listCustomPresets,
- selectFromList,
selectForRemoval,
+ selectFromList,
+ setupPolicyPresetSupported,
};
diff --git a/src/lib/shields/index.ts b/src/lib/shields/index.ts
index 7c27a465061..4c8e6fe451a 100644
--- a/src/lib/shields/index.ts
+++ b/src/lib/shields/index.ts
@@ -24,6 +24,7 @@ const {
buildPolicySetCommand,
parseCurrentPolicy,
resolvePermissivePolicyPath,
+ resolveAllowAllPolicyPath,
} = require("../policy");
const {
parseDuration,
@@ -906,15 +907,19 @@ function shieldsDown(sandboxName: string, opts: ShieldsDownOpts = {}): void {
// 2. Determine and apply relaxed policy
let policyFile: string;
let policyFileIsTemp = false;
- if (policyName === "permissive") {
- const basePath = resolvePermissivePolicyPath(sandboxName);
+ if (policyName === "permissive" || policyName === "allow-all") {
+ const basePath =
+ policyName === "allow-all"
+ ? resolveAllowAllPolicyPath(sandboxName)
+ : resolvePermissivePolicyPath(sandboxName);
// Union the live sandbox's filesystem_policy.read_only/read_write into
- // the static permissive baseline. OpenShell rejects removal of those
- // paths on a live sandbox, and runtime-injected entries (/proc on
- // GPU, /opt/hermes on Hermes, /home/linuxbrew on post-#3913 OpenClaw,
- // etc.) are not present in the static YAML. See #3942, #3957, #3168.
- // policyYaml is the pre-parsed body we already captured for the
- // snapshot above — reuse it instead of re-fetching.
+ // the static baseline. OpenShell rejects removal of those paths on a live
+ // sandbox, and runtime-injected entries (/proc on GPU, /opt/hermes on
+ // Hermes, /home/linuxbrew on post-#3913 OpenClaw, etc.) are not present in
+ // the static YAML. See #3942, #3957, #3168. policyYaml is the pre-parsed
+ // body we already captured for the snapshot above — reuse it instead of
+ // re-fetching. (allow-all reuses the same FS-union helper; only its
+ // network_policies differ — a single catch-all host: "*".)
policyFile = buildRuntimePermissivePolicy(basePath, {
livePolicyYaml: policyYaml,
readBasePolicy: () => fs.readFileSync(basePath, "utf-8"),
@@ -924,7 +929,7 @@ function shieldsDown(sandboxName: string, opts: ShieldsDownOpts = {}): void {
policyFile = path.resolve(policyName);
} else {
console.error(
- ` Unknown policy "${policyName}". Use "permissive" or a path to a YAML file.`,
+ ` Unknown policy "${policyName}". Use "permissive", "allow-all", or a path to a YAML file.`,
);
return failShieldsCommand(`Unknown policy "${policyName}"`, opts.throwOnError);
}
@@ -1341,16 +1346,16 @@ function isShieldsDown(sandboxName: string, allowInlineRecovery = false): boolea
// ---------------------------------------------------------------------------
export {
- shieldsDown,
- shieldsUp,
- shieldsStatus,
- isShieldsDown,
+ DEFAULT_TIMEOUT_SECONDS,
+ deriveShieldsMode,
getShieldsPosture,
+ isShieldsDown,
killTimer,
- deriveShieldsMode,
- parseDuration,
lockAgentConfig,
- unlockAgentConfig,
MAX_TIMEOUT_SECONDS,
- DEFAULT_TIMEOUT_SECONDS,
+ parseDuration,
+ shieldsDown,
+ shieldsStatus,
+ shieldsUp,
+ unlockAgentConfig,
};
diff --git a/test/policy-selection-allow-all.test.ts b/test/policy-selection-allow-all.test.ts
new file mode 100644
index 00000000000..8a1b8f20c94
--- /dev/null
+++ b/test/policy-selection-allow-all.test.ts
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Covers the allow-all branch in setupPoliciesWithSelection: when the
+// "allow-all" tier is chosen, the catch-all policy is applied directly to the
+// running sandbox and preset selection/sync is skipped entirely.
+
+import { describe, expect, it, vi } from "vitest";
+
+// hermes-managed-tools pulls in hermes-provider-auth, which does a runtime
+// require("./onboard/providers") that only resolves against the compiled dist
+// tree. We don't exercise Hermes tool gateways here, so stub it to keep the
+// source-level import graph self-contained.
+vi.mock("../src/lib/onboard/hermes-managed-tools", () => ({
+ HERMES_TOOL_GATEWAY_PRESET_NAMES: new Set(),
+ mergeRequiredHermesToolGatewayPolicyPresets: (presets: string[]) => presets,
+}));
+
+import { setupPoliciesWithSelection } from "../src/lib/onboard/policy-selection";
+
+function baseDeps(tierName: string, overrides: Record = {}) {
+ return {
+ policies: {
+ setupPolicyPresetSupported: () => true,
+ listSetupPolicyPresets: () => [{ name: "npm" }, { name: "pypi" }],
+ listCustomPresets: () => [],
+ getAppliedPresets: () => [],
+ clampSetupPolicyPresetNames: (names: string[]) => names,
+ },
+ tiers: {
+ resolveTierPresets: () => [],
+ getTier: (name: string) => (name === tierName ? { name } : null),
+ },
+ localInferenceProviders: [],
+ step: () => {},
+ note: () => {},
+ isNonInteractive: () => false,
+ waitForSandboxReady: () => true,
+ syncPresetSelection: vi.fn(),
+ selectPolicyTier: async () => tierName,
+ setPolicyTier: vi.fn(),
+ applyAllowAllPolicy: vi.fn(),
+ selectTierPresetsAndAccess: async () => [],
+ parsePolicyPresetEnv: () => [],
+ env: {},
+ ...overrides,
+ };
+}
+
+describe("setupPoliciesWithSelection — allow-all tier", () => {
+ it("applies the allow-all policy and skips preset sync", async () => {
+ const deps = baseDeps("allow-all");
+ const result = await setupPoliciesWithSelection(deps as never, "sbx");
+
+ expect(result).toEqual([]);
+ expect(deps.applyAllowAllPolicy).toHaveBeenCalledWith("sbx");
+ expect(deps.setPolicyTier).toHaveBeenCalledWith("sbx", "allow-all");
+ expect(deps.syncPresetSelection).not.toHaveBeenCalled();
+ });
+
+ it("records the chosen tier via setPolicyTier", async () => {
+ const deps = baseDeps("allow-all");
+ await setupPoliciesWithSelection(deps as never, "sbx");
+ expect(deps.setPolicyTier).toHaveBeenCalledWith("sbx", "allow-all");
+ });
+
+ it("does NOT take the allow-all branch for a normal tier", async () => {
+ const deps = baseDeps("balanced", {
+ selectTierPresetsAndAccess: async () => [{ name: "npm", access: "read-write" }],
+ });
+ await setupPoliciesWithSelection(deps as never, "sbx");
+ expect(deps.applyAllowAllPolicy).not.toHaveBeenCalled();
+ expect(deps.syncPresetSelection).toHaveBeenCalled();
+ });
+});
diff --git a/test/policy-tiers.test.ts b/test/policy-tiers.test.ts
index 6ba72cbea31..4eff5653378 100644
--- a/test/policy-tiers.test.ts
+++ b/test/policy-tiers.test.ts
@@ -67,13 +67,13 @@ function mustGetTier(name: string): Tier {
describe("tiers", () => {
describe("listTiers", () => {
- it("returns exactly 3 tiers", () => {
- expect(tiers.listTiers()).toHaveLength(3);
+ it("returns exactly 4 tiers", () => {
+ expect(tiers.listTiers()).toHaveLength(4);
});
- it("tiers are ordered restricted → balanced → open", () => {
+ it("tiers are ordered restricted → balanced → open → allow-all", () => {
const names = tiers.listTiers().map((tier: Tier) => tier.name);
- expect(names).toEqual(["restricted", "balanced", "open"]);
+ expect(names).toEqual(["restricted", "balanced", "open", "allow-all"]);
});
it("each tier has name, label, description, and presets array", () => {
@@ -85,9 +85,9 @@ describe("tiers", () => {
}
});
- it("labels are human-readable capitalised strings", () => {
+ it("first three labels are human-readable capitalised strings", () => {
const labels = tiers.listTiers().map((tier: Tier) => tier.label);
- expect(labels).toEqual(["Restricted", "Balanced", "Open"]);
+ expect(labels.slice(0, 3)).toEqual(["Restricted", "Balanced", "Open"]);
});
});
@@ -107,11 +107,30 @@ describe("tiers", () => {
expect(tier.name).toBe("open");
});
+ it("returns the allow-all tier", () => {
+ const tier = mustGetTier("allow-all");
+ expect(tier.name).toBe("allow-all");
+ });
+
it("returns null for an unknown tier", () => {
expect(tiers.getTier("nonexistent")).toBeNull();
});
});
+ describe("tier: allow-all", () => {
+ it("has no presets — it swaps the base policy instead of layering presets", () => {
+ expect(mustGetTier("allow-all").presets).toHaveLength(0);
+ });
+
+ it("label and description warn it is dev/testing only", () => {
+ const tier = mustGetTier("allow-all");
+ expect(tier.label.toLowerCase()).toContain("allow all");
+ expect(`${tier.label} ${tier.description}`.toLowerCase()).toMatch(
+ /dev|testing|no egress filtering|catch-all/,
+ );
+ });
+ });
+
describe("tier: restricted", () => {
it("has no presets — base sandbox policy only", () => {
expect(mustGetTier("restricted").presets).toHaveLength(0);
diff --git a/test/validate-configs-dangerous-hosts.test.ts b/test/validate-configs-dangerous-hosts.test.ts
index 75f54fc03c2..330b84b986d 100644
--- a/test/validate-configs-dangerous-hosts.test.ts
+++ b/test/validate-configs-dangerous-hosts.test.ts
@@ -16,6 +16,8 @@ import {
ROUTER_API_BASE_HOST_ALLOWLIST,
findDangerousHosts,
findDangerousRouterApiBases,
+ isAllowAllPolicyFile,
+ isBareWildcardHost,
isDangerousHost,
} from "../scripts/validate-configs";
@@ -61,6 +63,49 @@ describe("isDangerousHost", () => {
});
});
+describe("isAllowAllPolicyFile (allow-all exemption scope)", () => {
+ it.each([
+ "nemoclaw-blueprint/policies/openclaw-sandbox-allow-all.yaml",
+ "openclaw-sandbox-allow-all.yaml",
+ "agents/hermes/policy-allow-all.yml",
+ "C:\\repo\\policies\\some-allow-all.yaml",
+ ])("treats %s as an allow-all file", (file) => {
+ expect(isAllowAllPolicyFile(file)).toBe(true);
+ });
+
+ it.each([
+ "nemoclaw-blueprint/policies/openclaw-sandbox.yaml",
+ "nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml",
+ "nemoclaw-blueprint/policies/presets/slack.yaml",
+ "allow-all.json",
+ "allow-all-extra.yaml",
+ ])("does not treat %s as an allow-all file", (file) => {
+ expect(isAllowAllPolicyFile(file)).toBe(false);
+ });
+});
+
+describe("isBareWildcardHost (exempted family — bare wildcard only)", () => {
+ it.each(["*", "*:443", " * ", "*:80"])("flags %s as a bare wildcard", (host) => {
+ expect(isBareWildcardHost(host)).toBe(true);
+ });
+
+ it.each(["0.0.0.0", "0.0.0.0/0", "::", "::/0", "*.example.com", "example.com"])(
+ "does not treat %s as a bare wildcard",
+ (host) => {
+ expect(isBareWildcardHost(host)).toBe(false);
+ },
+ );
+
+ it("never exempts IP catch-alls even though they are dangerous", () => {
+ // These remain blocked everywhere — including in allow-all files — so a typo
+ // can never silently widen egress to a raw address range.
+ for (const host of ["0.0.0.0/0", "::/0"]) {
+ expect(isDangerousHost(host)).toBe(true);
+ expect(isBareWildcardHost(host)).toBe(false);
+ }
+ });
+});
+
describe("findDangerousRouterApiBases", () => {
it("allows the public NVIDIA Build endpoint", () => {
expect(