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
27 changes: 8 additions & 19 deletions docs/network-policy/customize-network-policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Dynamic changes apply a policy update to a running sandbox without restarting it
> A running sandbox's live policy is the baseline policy plus every preset that was layered on during onboarding.
> Applying a file that contains only the baseline (or only a single preset) silently drops every other preset that was in effect.

### Option 1: Add a Preset File and Use `policy-add` (Recommended)
### Add a Preset File with `policy-add` (Recommended)

This path preserves existing policy entries and is the only NemoClaw-supported flow for merging new entries into a running policy.

Expand Down Expand Up @@ -147,31 +147,20 @@ Provider-composed `_provider_*` entries are excluded because OpenShell reserves
Existing presets and the baseline remain in place.
The preset file under `presets/` also persists across sandbox recreations.

### Option 2: Snapshot, Edit, and Set with OpenShell
### Export, Edit, and Set the Base Policy

Use this path only when you cannot add a file under the NemoClaw source tree.
Start from the current live policy so the presets layered on at onboarding stay in the file you apply.
Start from the current base policy so the presets layered on at onboarding stay in the file you apply.
Requires OpenShell 0.0.72+ for the round-trippable `policy get --base` and `policy set --wait` syntax.
Strip the OpenShell metadata header before editing the file, then validate the raw policy shape before replacing your editable copy.
The command order below matches the commands NemoClaw emits internally.
Use NemoClaw to validate the base policy and strip the OpenShell metadata header before writing your editable copy.

```bash
# shellcheck shell=bash
# Source-of-truth review:
# invalidState: OpenShell 0.0.72 policy get --base emits metadata before the --- YAML header.
# sourceBoundary: OpenShell CLI output is owned by the separate OpenShell project.
# whyNotSourceFix: NemoClaw pins OpenShell but cannot change that upstream formatter here.
# regressionTest: test/policy-roundtrip-docs.test.ts validates this shared docs pattern.
# removalCondition: remove this pipeline after pinned OpenShell emits clean raw YAML.
tmp_policy=$(mktemp)
openshell policy get --base my-assistant \
| awk 'found { print } /^---$/ { found = 1 } END { if (!found) exit 1 }' \
> "$tmp_policy" \
&& grep -q '^version:' "$tmp_policy" \
&& grep -q '^network_policies:' "$tmp_policy" \
&& mv "$tmp_policy" current-policy.yaml
$$nemoclaw my-assistant policy-get > current-policy.yaml
```

The command exits non-zero instead of emitting a partial policy when retrieval or validation fails.
Do not use `--raw` for this workflow because raw output retains the metadata header.

Edit `current-policy.yaml` to add your entries under `network_policies:`, keeping the existing `version` field intact, then apply:

```bash
Expand Down
22 changes: 6 additions & 16 deletions docs/network-policy/integration-policy-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -405,26 +405,16 @@ Use `policy-list` for normal preset state:
$$nemoclaw my-assistant policy-list
```

Use OpenShell when you need an editable copy of the live policy.
Use the NemoClaw policy export when you need an editable copy of the round-trippable base policy.
Requires OpenShell 0.0.72+ for the round-trippable `policy get --base` and `policy set --wait` syntax.

```bash
# shellcheck shell=bash
# Source-of-truth review:
# invalidState: OpenShell 0.0.72 policy get --base emits metadata before the --- YAML header.
# sourceBoundary: OpenShell CLI output is owned by the separate OpenShell project.
# whyNotSourceFix: NemoClaw pins OpenShell but cannot change that upstream formatter here.
# regressionTest: test/policy-roundtrip-docs.test.ts validates this shared docs pattern.
# removalCondition: remove this pipeline after pinned OpenShell emits clean raw YAML.
tmp_policy=$(mktemp)
openshell policy get --base my-assistant \
| awk 'found { print } /^---$/ { found = 1 } END { if (!found) exit 1 }' \
> "$tmp_policy" \
&& grep -q '^version:' "$tmp_policy" \
&& grep -q '^network_policies:' "$tmp_policy" \
&& mv "$tmp_policy" current-policy.yaml
$$nemoclaw my-assistant policy-get > current-policy.yaml
```

The export strips OpenShell metadata and exits non-zero if the base policy cannot be retrieved or validated.
Do not add `--raw` when you plan to edit and reapply the file.

If you must replace the live policy, edit the policy file and apply it back to the sandbox:

```bash
Expand All @@ -440,4 +430,4 @@ Use `$$nemoclaw my-assistant policy-add` for maintained NemoClaw presets.
- [Approve or Deny Agent Network Requests](approve-network-requests) for the interactive OpenShell TUI flow.
- [Customize the Sandbox Network Policy](customize-network-policy) for static policy edits and raw OpenShell policy files.
- [Messaging Channels](../manage-sandboxes/messaging-channels) for Telegram, Discord, Slack, WeChat, and WhatsApp channel configuration.
- [Commands](../reference/commands) for the full `policy-add`, `policy-list`, `policy-remove`, and `channels` command reference.
- [Commands](../reference/commands) for the full `policy-get`, `policy-add`, `policy-list`, `policy-remove`, and `channels` command reference.
24 changes: 7 additions & 17 deletions docs/reference/cli-selection-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,27 +139,17 @@ Use `openshell` when the docs explicitly call for a live OpenShell gateway opera
openshell policy update <sandbox-name> --add-endpoint api.example.com:443:read-only:rest:enforce
```

- Inspect or replace raw OpenShell policy:
- Export the round-trippable OpenShell base policy through NemoClaw, then replace it through OpenShell:

Requires OpenShell 0.0.72+ for the round-trippable `policy get --base` and `policy set --wait` syntax.

```bash
# shellcheck shell=bash
# Source-of-truth review:
# invalidState: OpenShell 0.0.72 policy get --base emits metadata before the --- YAML header.
# sourceBoundary: OpenShell CLI output is owned by the separate OpenShell project.
# whyNotSourceFix: NemoClaw pins OpenShell but cannot change that upstream formatter here.
# regressionTest: test/policy-roundtrip-docs.test.ts validates this shared docs pattern.
# removalCondition: remove this pipeline after pinned OpenShell emits clean raw YAML.
tmp_policy=$(mktemp)
openshell policy get --base <sandbox-name> \
| awk 'found { print } /^---$/ { found = 1 } END { if (!found) exit 1 }' \
> "$tmp_policy" \
&& grep -q '^version:' "$tmp_policy" \
&& grep -q '^network_policies:' "$tmp_policy" \
&& mv "$tmp_policy" current-policy.yaml
$$nemoclaw <sandbox-name> policy-get > current-policy.yaml
```

NemoClaw strips the OpenShell metadata header and exits non-zero if it cannot validate the base policy.
Do not use `--raw` for a file that you plan to reapply.

Edit or review `current-policy.yaml`, then apply it:

```bash
Expand Down Expand Up @@ -275,8 +265,8 @@ Use `$$nemoclaw <name> policy-add` or `policy-remove` for NemoClaw presets and c
NemoClaw merges the new policy with the live policy and reapplies presets during rebuilds.

Use `openshell policy update` for precise live endpoint or REST rule changes.
Use `openshell policy get --base <name>` and `openshell policy set --policy <file> --wait <name>` only when you need to edit and replace the round-trippable base policy.
Use `--full` only to inspect the effective policy, including provider-composed rules.
Use `$$nemoclaw <name> policy-get` and `openshell policy set --policy <file> --wait <name>` only when you need to edit and replace the round-trippable base policy.
Use `openshell policy get --full <name>` only to inspect the effective policy, including provider-composed rules.

### Move Workspace Files

Expand Down
27 changes: 25 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,29 @@ Managed MCP ownership disables the local-only fallback because exact provider cl
$$nemoclaw my-assistant destroy [--yes|-y|--force] [--cleanup-gateway|--no-cleanup-gateway]
```

### `$$nemoclaw <name> policy-get`

Export the sandbox's round-trippable OpenShell base policy as YAML.
The command runs `openshell policy get --base`, validates the returned policy, and strips the OpenShell metadata header.
The default output is suitable for review, editing, and later use with `openshell policy set`.
The command exits non-zero when OpenShell fails, returns an empty response, or returns content that is not valid policy YAML.

```bash
$$nemoclaw my-assistant policy-get > current-policy.yaml
```

Use `--raw` only to inspect the unparsed OpenShell response, including its metadata header:

```bash
$$nemoclaw my-assistant policy-get --raw
```

Do not pass `--raw` output to `openshell policy set` because the metadata header is not part of the policy document.

| Flag | Description |
|------|-------------|
| `--raw` | Print the unparsed `openshell policy get --base` response, including its metadata header. |

### `$$nemoclaw <name> policy-add`

Add a policy preset to a sandbox.
Expand All @@ -1472,8 +1495,8 @@ If the preset name is unknown or already applied, the command exits non-zero wit
Built-in preset choices are scoped to the sandbox's active agent. Messaging channel presets appear only when NemoClaw has a matching channel policy for that agent; unavailable channel presets use the standard unknown-preset error before endpoint preview or confirmation.
Custom preset files are tracked with the sandbox that applied them.
`policy-list`, `policy-add`, and `policy-remove` compare the local registry and live gateway state using that sandbox-scoped preset metadata, so custom presets do not appear missing just because they are not part of the built-in preset catalog.
Before `policy-add` writes a merged policy, it reads and parses the current live policy from OpenShell.
If the live policy read returns non-empty output that NemoClaw cannot parse, the command exits non-zero instead of overwriting the live policy with only the new preset.
Before `policy-add` writes a merged policy, it reads and parses the round-trippable base policy from OpenShell.
If the base policy read returns non-empty output that NemoClaw cannot parse, the command exits non-zero instead of overwriting the live policy with only the new preset.
Fix the gateway or policy read problem, then rerun the command.
For custom presets, the command also reports when the preset reached the gateway but NemoClaw could not record it in the local sandbox registry, because unrecorded custom presets will not appear in `policy-list` or `status`.
Recover or re-onboard the sandbox, then re-apply the custom preset.
Expand Down
20 changes: 5 additions & 15 deletions docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -270,26 +270,16 @@ Apply policy updates to a running sandbox without restarting:
openshell policy update <sandbox-name> --add-endpoint api.example.com:443:read-only:rest:enforce
```

To replace the live policy with a complete raw policy file, start from the live policy and use `openshell policy set`.
To replace the live policy with a complete base policy file, export the current base policy and use `openshell policy set`.
Requires OpenShell 0.0.72+ for the round-trippable `policy get --base` and `policy set --wait` syntax.

```bash
# shellcheck shell=bash
# Source-of-truth review:
# invalidState: OpenShell 0.0.72 policy get --base emits metadata before the --- YAML header.
# sourceBoundary: OpenShell CLI output is owned by the separate OpenShell project.
# whyNotSourceFix: NemoClaw pins OpenShell but cannot change that upstream formatter here.
# regressionTest: test/policy-roundtrip-docs.test.ts validates this shared docs pattern.
# removalCondition: remove this pipeline after pinned OpenShell emits clean raw YAML.
tmp_policy=$(mktemp)
openshell policy get --base <sandbox-name> \
| awk 'found { print } /^---$/ { found = 1 } END { if (!found) exit 1 }' \
> "$tmp_policy" \
&& grep -q '^version:' "$tmp_policy" \
&& grep -q '^network_policies:' "$tmp_policy" \
&& mv "$tmp_policy" current-policy.yaml
$$nemoclaw <sandbox-name> policy-get > current-policy.yaml
```

NemoClaw strips the OpenShell metadata header and exits non-zero if it cannot validate the base policy.
Do not add `--raw` when you plan to edit and reapply the file.

Edit or review `current-policy.yaml`, then apply it:

```bash
Expand Down
6 changes: 6 additions & 0 deletions scripts/checks/openshell-policy-mutation-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ interface AuditedMutationRead {
}

export const MUTATION_READS: readonly AuditedMutationRead[] = [
{
relativePath: "src/lib/actions/sandbox/policy-get.ts",
expectedReadCalls: 1,
baseCommand: "runCapture(buildPolicyGetCommand(sandboxName))",
fullCommand: "runCapture(buildPolicyGetFullCommand(sandboxName))",
},
{
relativePath: "src/lib/policy/index.ts",
expectedReadCalls: 6,
Expand Down
75 changes: 75 additions & 0 deletions src/commands/sandbox/policy/get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

const mocks = vi.hoisted(() => ({
getSandboxPolicy: vi.fn(() => ({ raw: "", yaml: "" })),
}));

vi.mock("../../../lib/actions/sandbox/policy-get", () => ({
getSandboxPolicy: mocks.getSandboxPolicy,
}));

import SandboxPolicyGetCommand from "./get";

const rootDir = process.cwd();

describe("sandbox:policy:get command", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("outputs parsed base-policy YAML by default", async () => {
mocks.getSandboxPolicy.mockReturnValue({
raw: "Version: 1\nHash: abc\nStatus: active\n---\nversion: 1\nnetwork_policies: []",
yaml: "version: 1\nnetwork_policies: []",
});

const logSpy = vi.spyOn(SandboxPolicyGetCommand.prototype, "log");
await SandboxPolicyGetCommand.run(["alpha"], rootDir);

expect(mocks.getSandboxPolicy).toHaveBeenCalledWith("alpha");
expect(logSpy).toHaveBeenCalledWith("version: 1\nnetwork_policies: []");
});

it("outputs the unparsed base-policy response with --raw", async () => {
const rawOutput =
"Version: 1\nHash: abc\nStatus: active\n---\nversion: 1\nnetwork_policies: []";
mocks.getSandboxPolicy.mockReturnValue({
raw: rawOutput,
yaml: "version: 1\nnetwork_policies: []",
});

const logSpy = vi.spyOn(SandboxPolicyGetCommand.prototype, "log");
await SandboxPolicyGetCommand.run(["alpha", "--raw"], rootDir);

expect(logSpy).toHaveBeenCalledWith(rawOutput);
});

it("exits with error when the base policy is empty", async () => {
mocks.getSandboxPolicy.mockReturnValue({ raw: "", yaml: "" });

await expect(SandboxPolicyGetCommand.run(["alpha"], rootDir)).rejects.toThrow(
/Failed to retrieve base policy/,
);
});

it("exits with error when base-policy YAML cannot be parsed", async () => {
mocks.getSandboxPolicy.mockReturnValue({ raw: "some output", yaml: "" });

await expect(SandboxPolicyGetCommand.run(["alpha"], rootDir)).rejects.toThrow(
/Failed to parse base policy YAML/,
);
});

it("propagates OpenShell retrieval failures", async () => {
mocks.getSandboxPolicy.mockImplementationOnce(() => {
throw new Error("Failed to retrieve base policy for sandbox 'alpha'.");
});

await expect(SandboxPolicyGetCommand.run(["alpha"], rootDir)).rejects.toThrow(
/Failed to retrieve base policy for sandbox 'alpha'/,
);
});
});
51 changes: 51 additions & 0 deletions src/commands/sandbox/policy/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Flags } from "@oclif/core";

import { getSandboxPolicy } from "../../../lib/actions/sandbox/policy-get";
import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
import { sandboxNameArg } from "../../../lib/sandbox/command-support";

export default class SandboxPolicyGetCommand extends NemoClawCommand {
static id = "sandbox:policy:get";
static strict = true;
static summary = "Export the round-trippable sandbox base policy";
static description =
"Retrieve the OpenShell base policy for a sandbox. By default, strips the OpenShell metadata header and outputs YAML suitable for review, editing, and policy set. Use --raw to emit the unparsed --base response.";
static usage = ["<name> [--raw]"];
static examples = [
"<%= config.bin %> sandbox policy get alpha",
"<%= config.bin %> sandbox policy get alpha --raw",
];
static args = {
sandboxName: sandboxNameArg,
};
static flags = {
raw: Flags.boolean({
description: "Output the unparsed OpenShell --base response, including its metadata header",
default: false,
}),
};

public async run(): Promise<void> {
const { args, flags } = await this.parse(SandboxPolicyGetCommand);

const { raw, yaml } = getSandboxPolicy(args.sandboxName);

if (!raw) {
this.error("Failed to retrieve base policy from sandbox.");
}

if (flags.raw) {
this.log(raw);
return;
}

if (!yaml) {
this.error("Failed to parse base policy YAML from sandbox output.");
}

this.log(yaml);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading
Loading