Learning NemoClaw

11 · Adding Tools and Policies to a NemoClaw Deployment

This document covers how to extend a NemoClaw-managed sandbox with additional tools (binaries, packages, scripts) and network policies — both for fresh deployments and for existing sandboxes where agents and workspace state already exist.

The core constraint to understand first

NemoClaw's sandbox has two categories of configuration, and they have different lifecycles:

Category Examples Can change on a running sandbox?
Static (locked at creation) Installed binaries, filesystem layout, process user, seccomp No — requires sandbox recreation
Dynamic (hot-reloadable) Network policies, inference routing Yes — apply while the sandbox is running

This means:

Understanding this split saves you from accidentally destroying agent state when you only needed a policy change.


Part 1: Adding network policies to an existing sandbox (no recreation needed)

This is the simplest case. Your sandbox is running, your agents have state, and you just need the agent to reach a new service.

Option A: Approve a request live (session-scoped, quick experimentation)

When the agent tries to reach a blocked host, it shows up in the OpenShell TUI:

openshell term

You see the destination host, port, binary, and HTTP method/path. Approve it interactively. This approval persists across sandbox restarts but resets when the sandbox is destroyed and recreated.

Best for: quick experimentation, one-off testing, figuring out which hosts a new tool actually needs.

Option B: Apply a preset (persistent, built-in integrations)

NemoClaw ships presets for common services. Check what's available:

nemoclaw my-assistant policy-list          # see what's already applied

Available presets in nemoclaw-blueprint/policies/presets/:

Preset What it opens
telegram api.telegram.org (GET/POST /bot*/**)
discord discord.com, gateway.discord.gg, cdn.discordapp.com
slack slack.com, api.slack.com, hooks.slack.com, WebSocket gateways
github github.com, api.github.com (for git and gh binaries)
jira *.atlassian.net, auth.atlassian.com, api.atlassian.com
brave Brave Search API
huggingface HuggingFace Hub
npm registry.npmjs.org
pypi PyPI package index
brew Homebrew
outlook Microsoft Outlook/Graph API

Apply one:

nemoclaw my-assistant policy-add                    # interactive — shows available presets
nemoclaw my-assistant policy-add --dry-run           # preview what would change

Or apply directly via OpenShell:

openshell policy set my-assistant --policy nemoclaw-blueprint/policies/presets/github.yaml --wait

The --wait flag ensures the policy is active before the command returns. This is a hot-reload — no sandbox restart, no agent state lost.

Option C: Write a custom policy preset (persistent, org-specific)

For internal services that don't have a built-in preset. Create a new YAML file:

# nemoclaw-blueprint/policies/presets/acme-internal.yaml

preset:
  name: acme_internal
  description: "Acme Corp internal services"

network_policies:
  acme_api:
    name: acme_api
    endpoints:
      - host: api.internal.acme.com
        port: 443
        protocol: rest
        enforcement: enforce
        tls: terminate
        rules:
          - allow: { method: GET, path: "/v2/**" }
          - allow: { method: POST, path: "/v2/**" }
          - allow: { method: PUT, path: "/v2/resources/**" }
    binaries:
      - { path: /usr/local/bin/openclaw }
      - { path: /usr/local/bin/node }

  acme_artifacts:
    name: acme_artifacts
    endpoints:
      - host: artifacts.acme.com
        port: 443
        protocol: rest
        enforcement: enforce
        tls: terminate
        rules:
          - allow: { method: GET, path: "/**" }
    binaries:
      - { path: /usr/local/bin/node }
      - { path: /usr/local/bin/npm }

Apply it:

openshell policy set my-assistant --policy nemoclaw-blueprint/policies/presets/acme-internal.yaml --wait

No restart. No agent state lost.

Policy YAML reference — the fields you'll use

network_policies:
  policy_name:                          # unique identifier
    name: policy_name                   # must match the key
    endpoints:
      - host: api.example.com          # exact host or wildcard (*.example.com)
        port: 443                       # target port
        protocol: rest                  # "rest" for HTTP inspection
        enforcement: enforce            # "enforce" = block violations
        tls: terminate                  # OpenShell terminates TLS to inspect methods/paths
        rules:
          - allow: { method: GET, path: "/v1/**" }
          - allow: { method: POST, path: "/v1/submit" }

      # For WebSockets or non-HTTP protocols, use access: full instead of rules
      - host: ws.example.com
        port: 443
        access: full                    # CONNECT tunnel, no HTTP inspection

    binaries:                           # ONLY these binaries may use this policy
      - { path: /usr/local/bin/node }
      - { path: /usr/bin/ssh }

Key choices:

Field When to use
tls: terminate When you want to inspect HTTP methods and paths (most REST APIs)
access: full When the protocol isn't HTTP (SSH, WebSocket, gRPC streaming) or when method/path inspection isn't needed
rules with specific paths When you want fine-grained control (e.g., allow GET but not DELETE)
rules with /** When you trust the service and just need host-level gating
Wildcard hosts (*.acme.com) When you need to cover multiple subdomains

Modifying the baseline policy

If you want a policy to apply to every sandbox your org creates (not just an opt-in preset), edit the baseline directly:

# Edit the baseline policy
vim nemoclaw-blueprint/policies/openclaw-sandbox.yaml

Add your policy block under network_policies: alongside the existing claude_code, nvidia, clawhub, etc. blocks.

Apply the updated baseline:

openshell policy set my-assistant --policy nemoclaw-blueprint/policies/openclaw-sandbox.yaml --wait

Or, if you want it to take effect on the next nemoclaw onboard, just save the file — NemoClaw reads the baseline from disk during onboarding.


Part 2: Adding tools to an existing sandbox (requires recreation)

This is where it gets more involved. Installing new binaries means changing the container image, which means recreating the sandbox, which means backing up your agent's workspace state first.

Step 1: Back up workspace state

This is mandatory. Sandbox recreation destroys the PVC (persistent volume) that holds your agent's workspace files (SOUL.md, USER.md, IDENTITY.md, AGENTS.md, MEMORY.md, memory/).

# Using the convenience script (recommended)
./scripts/backup-workspace.sh backup my-assistant

# Or manually
SANDBOX=my-assistant
BACKUP_DIR=~/.nemoclaw/backups/$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"

openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/SOUL.md     "$BACKUP_DIR/"
openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/USER.md     "$BACKUP_DIR/"
openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/IDENTITY.md "$BACKUP_DIR/"
openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/AGENTS.md   "$BACKUP_DIR/"
openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/MEMORY.md   "$BACKUP_DIR/"
openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/memory/     "$BACKUP_DIR/memory/"

Verify:

ls ~/.nemoclaw/backups/$(ls -t ~/.nemoclaw/backups/ | head -1)/
# Expected: AGENTS.md  IDENTITY.md  MEMORY.md  SOUL.md  USER.md  memory/

Six entries. If any are missing, the restore won't fully rehydrate the agent.

Step 2: Create a custom Dockerfile

Write a Dockerfile that extends the NemoClaw sandbox image:

# Dockerfile.custom-sandbox
FROM ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest

USER root

# ── System tools ──────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
    openssh-client \
    ansible \
    kubectl \
    curl \
    jq \
    && rm -rf /var/lib/apt/lists/*

# ── Python-based tools ────────────────────────────────────
RUN pip3 install --no-cache-dir \
    boto3 \
    paramiko \
    requests

# ── Custom scripts from your org ──────────────────────────
COPY scripts/acme-deploy.sh /usr/local/bin/acme-deploy
RUN chmod +x /usr/local/bin/acme-deploy

# ── SSH config directory setup ────────────────────────────
# Create the directory structure; actual keys/config will be
# injected at runtime via OpenShell providers or mounted.
RUN mkdir -p /sandbox/.ssh && \
    chown sandbox:sandbox /sandbox/.ssh && \
    chmod 700 /sandbox/.ssh

# ── Ansible config directory ─────────────────────────────
RUN mkdir -p /sandbox/.ansible && \
    chown sandbox:sandbox /sandbox/.ansible

# Drop back to sandbox user
USER sandbox

Step 3: Update filesystem policy (if your tools need writable paths)

If the tools you're adding need to write state to disk, you must add those paths to the filesystem policy before recreation. The filesystem policy is Landlock-based and locked at creation — you cannot add writable paths after the fact.

Edit nemoclaw-blueprint/policies/openclaw-sandbox.yaml:

filesystem_policy:
  include_workdir: false
  read_only:
    - /usr
    - /lib
    - /proc
    - /dev/urandom
    - /app
    - /etc
    - /var/log
    - /sandbox
    - /sandbox/.openclaw
  read_write:
    - /tmp
    - /dev/null
    - /sandbox/.openclaw-data
    - /sandbox/.nemoclaw
    # ── Added for custom tools ──
    - /sandbox/.ssh                    # SSH known_hosts, config
    - /sandbox/.ansible                # Ansible state, tmp files, galaxy cache
    - /sandbox/.kube                   # kubectl config and cache

Step 4: Add network policies for your new tools

Your tools need egress. Create presets or add to the baseline. Here are examples for common tools:

SSH access to internal infrastructure:

# nemoclaw-blueprint/policies/presets/ssh-internal.yaml
preset:
  name: ssh_internal
  description: "SSH access to internal infrastructure"

network_policies:
  ssh_infra:
    name: ssh_infra
    endpoints:
      # Specific hosts (most secure)
      - host: bastion.acme.com
        port: 22
        access: full
      - host: deploy-01.acme.com
        port: 22
        access: full

      # Or wildcard (more convenient, less restrictive)
      # - host: "*.internal.acme.com"
      #   port: 22
      #   access: full
    binaries:
      - { path: /usr/bin/ssh }
      - { path: /usr/bin/scp }
      - { path: /usr/bin/ansible }
      - { path: /usr/bin/ansible-playbook }

Kubernetes API access:

# nemoclaw-blueprint/policies/presets/kubernetes.yaml
preset:
  name: kubernetes
  description: "Kubernetes API server access"

network_policies:
  k8s_api:
    name: k8s_api
    endpoints:
      - host: k8s-api.acme.com
        port: 6443
        access: full
    binaries:
      - { path: /usr/local/bin/kubectl }

Package registries (for pip/npm installs at runtime):

# nemoclaw-blueprint/policies/presets/package-registries.yaml
preset:
  name: package_registries
  description: "Python and Node package registries"

network_policies:
  pypi:
    name: pypi
    endpoints:
      - host: pypi.org
        port: 443
        protocol: rest
        enforcement: enforce
        tls: terminate
        rules:
          - allow: { method: GET, path: "/**" }
      - host: files.pythonhosted.org
        port: 443
        protocol: rest
        enforcement: enforce
        tls: terminate
        rules:
          - allow: { method: GET, path: "/**" }
    binaries:
      - { path: /usr/bin/pip3 }
      - { path: /usr/bin/python3 }
      - { path: /usr/local/bin/node }

Step 5: Recreate the sandbox with the custom image

nemoclaw onboard --from ./Dockerfile.custom-sandbox

This will:

  1. Build the custom image
  2. Destroy the existing sandbox
  3. Create a new sandbox from the custom image
  4. Apply the baseline policy (which now includes your filesystem changes)
  5. Set up providers and inference routing

Step 6: Apply your custom policy presets

# Apply each preset
openshell policy set my-assistant --policy nemoclaw-blueprint/policies/presets/ssh-internal.yaml --wait
openshell policy set my-assistant --policy nemoclaw-blueprint/policies/presets/kubernetes.yaml --wait

Step 7: Restore workspace state

# Using the convenience script
./scripts/backup-workspace.sh restore my-assistant

# Or manually (use the timestamp of your backup)
SANDBOX=my-assistant
BACKUP_DIR=~/.nemoclaw/backups/20260412-143000

openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/SOUL.md"     /sandbox/.openclaw/workspace/
openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/USER.md"     /sandbox/.openclaw/workspace/
openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/IDENTITY.md" /sandbox/.openclaw/workspace/
openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/AGENTS.md"   /sandbox/.openclaw/workspace/
openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/MEMORY.md"   /sandbox/.openclaw/workspace/
openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/memory/"     /sandbox/.openclaw/workspace/memory/

Step 8: Verify

# Connect and check tools are available
nemoclaw my-assistant connect

sandbox$ which ssh ansible kubectl
sandbox$ ssh -V
sandbox$ ansible --version

# Verify agent state was restored
sandbox$ ls /sandbox/.openclaw/workspace/
# Should show: AGENTS.md  IDENTITY.md  MEMORY.md  SOUL.md  USER.md  memory/

# Verify network policies
sandbox$ exit
openshell policy get my-assistant

Part 3: How custom images affect the NemoClaw blueprint

When you use nemoclaw onboard --from ./Dockerfile, you're overriding the image that the blueprint pins. This has important implications for reproducibility and verification that you need to understand before going to production.

What the blueprint pins

The blueprint (nemoclaw-blueprint/blueprint.yaml) has two digest fields that must match:

# Top-level field — quick verification without parsing the component tree
digest: "sha256:b3d832b596ab6b7184a9dcb4ae93337ca32851a4f93b00765cc12de26baa3a9a"

components:
  sandbox:
    # Same digest, pinned on the image reference
    image: "ghcr.io/nvidia/openshell-community/sandboxes/openclaw@sha256:b3d832b..."

These are the same hash. NVIDIA's release tooling and CI tests enforce they stay in sync (issue #1438). This is what makes NemoClaw deployments reproducible — every machine running nemoclaw onboard with this blueprint gets the exact same container image.

What --from does to the blueprint

When you run nemoclaw onboard --from ./Dockerfile.custom, the onboard code uses your Dockerfile to build the sandbox image instead of pulling the blueprint's pinned image. The rest of the blueprint — policies, inference profiles, credential filtering, provider setup — continues to apply normally.

Default nemoclaw onboard nemoclaw onboard --from ./Dockerfile
Image source Blueprint-pinned @sha256:... Your custom Dockerfile
Digest verification Verified against blueprint Skipped — your image has a different digest
Reproducibility Guaranteed (same image everywhere) Your responsibility
Network policies Applied from blueprint Applied from blueprint (unchanged)
Filesystem policies Applied from blueprint Applied from blueprint (unchanged)
Inference routing From blueprint profiles From blueprint profiles (unchanged)
Credential handling Filtered from build args Filtered from build args (unchanged)
Process isolation seccomp + run_as_user: sandbox seccomp + run_as_user: sandbox (unchanged)

In short: --from bypasses image pinning but keeps everything else from the blueprint. The blueprint is not "broken" by a custom image; the image portion is simply overridden.

The Dockerfile base image matters

In Part 2 above, the example Dockerfile uses :latest:

FROM ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest

This is fine for experimentation but not for production. The :latest tag is mutable — NVIDIA can push a new image at any time, and your next build will silently pick it up. For production, pin your base to the same digest the blueprint uses:

FROM ghcr.io/nvidia/openshell-community/sandboxes/openclaw@sha256:b3d832b596ab6b7184a9dcb4ae93337ca32851a4f93b00765cc12de26baa3a9a

This ensures your custom image is built on the exact base that NVIDIA tested and verified.

What to watch out for

Version drift. The blueprint declares min_openclaw_version: "2026.3.0". If your custom image somehow installs a different OpenClaw version (e.g., by running npm update -g openclaw in the Dockerfile), it may not match what the blueprint expects. Stick to extending the base image, not modifying the software it ships.

Upgrade path. When NVIDIA ships a new blueprint with a bumped image digest, you need to update your Dockerfile's FROM line to the new base image and rebuild. If you forget, your custom image stays on the old base while the blueprint moves forward.

Blueprint tests will fail. If you run the NemoClaw test suite (npm test), the validate-blueprint.test.ts tests verify that the top-level digest: matches components.sandbox.image. These tests validate the blueprint file on disk, not your runtime image, so they'll still pass. But they won't catch drift between your custom image and the blueprint.

The enterprise approach: fork the blueprint

For production fleet deployment, don't rely on --from. Instead, maintain your own blueprint:

Step 1: Build and push your custom image to your own registry:

docker build -t your-registry.acme.com/sandboxes/openclaw-custom:v1 \
  -f Dockerfile.custom-sandbox .
docker push your-registry.acme.com/sandboxes/openclaw-custom:v1

# Get the digest
docker inspect --format='{{index .RepoDigests 0}}' \
  your-registry.acme.com/sandboxes/openclaw-custom:v1
# → your-registry.acme.com/sandboxes/openclaw-custom@sha256:abc123...

Step 2: Fork nemoclaw-blueprint/ and update the image reference:

# Your forked blueprint.yaml
digest: "sha256:abc123..."    # YOUR image's digest

components:
  sandbox:
    image: "your-registry.acme.com/sandboxes/openclaw-custom@sha256:abc123..."
    name: "openclaw"
    forward_ports:
      - 18789

Step 3: Run nemoclaw onboard without --from:

Now the blueprint itself points to your custom image with full digest verification. Every machine in your fleet gets the exact same custom image, and you maintain the reproducibility guarantee that NemoClaw was designed for.

Step 4: When NVIDIA releases a new blueprint:

  1. Update your Dockerfile's FROM to the new base image digest
  2. Rebuild and push your custom image
  3. Update your forked blueprint.yaml with the new digest
  4. Roll out nemoclaw onboard across your fleet

Quick comparison: --from vs forked blueprint

--from ./Dockerfile Forked blueprint
Best for Dev, experimentation, quick iteration Production fleet deployment
Digest verification Skipped Full (against your image)
Reproducible across machines Only if same Dockerfile + same base tag Yes (digest-pinned)
Upgrade path Update Dockerfile, rebuild locally Update Dockerfile, push to registry, update blueprint
Auditability Low — what's running depends on when it was built High — security team verifies one blueprint

Part 4: The decision flowchart (updated)

What do you need to add?
│
├── A new network destination (host/port/path)?
│   ├── One-off test? ──────────────► Approve in `openshell term`
│   ├── Built-in preset exists? ────► `nemoclaw <name> policy-add`
│   └── Custom service? ───────────► Write a preset YAML, apply with
│                                     `openshell policy set` (hot-reload)
│
│   ✅ No sandbox recreation. No backup needed. No agent state lost.
│
├── A new binary/tool (ssh, ansible, kubectl, etc.)?
│   │
│   │  ⚠ REQUIRES SANDBOX RECREATION
│   │
│   ├── 1. Back up workspace state
│   ├── 2. Write a Dockerfile extending the NemoClaw image
│   ├── 3. Update filesystem_policy if tool needs writable paths
│   ├── 4. Write network policy presets for the tool's egress
│   ├── 5. Dev/experiment: `nemoclaw onboard --from ./Dockerfile`
│   │   Production: fork blueprint, push image to registry (see Part 3)
│   ├── 6. Apply policy presets
│   └── 7. Restore workspace state
│
├── A new inference provider/model?
│   ├── Same provider family? ──────► `openshell inference set` (hot)
│   └── Different family? ─────────► Add profile to blueprint.yaml,
│                                     `nemoclaw onboard --resume
│                                      --recreate-sandbox` (recreation)
│
└── A change to process isolation (user, ulimit, seccomp)?
    │
    │  ⚠ REQUIRES SANDBOX RECREATION
    │
    ├── 1. Back up workspace state
    ├── 2. Edit openclaw-sandbox.yaml `process:` section
    ├── 3. `nemoclaw onboard`
    └── 4. Restore workspace state

Part 5: Binary pinning — a security decision you must make

When you add a tool and a network policy, you must decide which binaries are allowed to use that policy. This is the binaries: field in the policy YAML, and it's the most important security decision in this entire process.

The question

Should the AI agent (OpenClaw) be able to use your new tool's network access?

Scenario A: Only humans use the tool (agent cannot)

The agent cannot SSH into your servers. Only you, connected to the sandbox via nemoclaw my-assistant connect, can use ssh.

binaries:
  - { path: /usr/bin/ssh }
  # OpenClaw and Node are NOT listed — they cannot use this policy

Scenario B: The agent can invoke the tool

The agent can use SSH as a tool — e.g., it could run ssh bastion.acme.com deploy-app if /elevated on is set for the session.

binaries:
  - { path: /usr/bin/ssh }
  - { path: /usr/local/bin/openclaw }
  - { path: /usr/local/bin/node }

The risk tradeoff

Approach Convenience Risk
Human-only (ssh binary only) Low — you must SSH manually Low — agent cannot reach infrastructure
Agent-enabled (add openclaw/node) High — agent can automate deployments High — a prompt injection could trigger SSH commands to your infrastructure

For most enterprises, start with human-only and only add agent access after you've validated the agent's behavior in your environment. You can always widen binary pinning later with a hot policy reload — no recreation needed.


Part 6: Complete worked example — adding SSH and Ansible

Here's the full sequence, end to end, for an existing sandbox named my-assistant:

# ── 1. Back up ──────────────────────────────────────────
./scripts/backup-workspace.sh backup my-assistant

# ── 2. Create Dockerfile ────────────────────────────────
cat > Dockerfile.with-ssh-ansible << 'EOF'
FROM ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
    openssh-client ansible && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /sandbox/.ssh /sandbox/.ansible && \
    chown sandbox:sandbox /sandbox/.ssh /sandbox/.ansible && \
    chmod 700 /sandbox/.ssh
USER sandbox
EOF

# ── 3. Update filesystem policy ─────────────────────────
# Add to read_write in openclaw-sandbox.yaml:
#   - /sandbox/.ssh
#   - /sandbox/.ansible

# ── 4. Create network policy preset ─────────────────────
cat > nemoclaw-blueprint/policies/presets/ssh-infra.yaml << 'EOF'
preset:
  name: ssh_infra
  description: "SSH access to internal infrastructure"
network_policies:
  ssh_infra:
    name: ssh_infra
    endpoints:
      - host: bastion.acme.com
        port: 22
        access: full
      - host: "*.servers.acme.com"
        port: 22
        access: full
    binaries:
      - { path: /usr/bin/ssh }
      - { path: /usr/bin/scp }
      - { path: /usr/bin/ansible }
      - { path: /usr/bin/ansible-playbook }
EOF

# ── 5. Recreate sandbox with custom image ───────────────
nemoclaw onboard --from ./Dockerfile.with-ssh-ansible

# ── 6. Apply network policies ──────────────────────────
openshell policy set my-assistant \
  --policy nemoclaw-blueprint/policies/presets/ssh-infra.yaml --wait

# ── 7. Restore workspace ───────────────────────────────
./scripts/backup-workspace.sh restore my-assistant

# ── 8. Verify ───────────────────────────────────────────
nemoclaw my-assistant connect
sandbox$ which ssh ansible
sandbox$ cat /sandbox/.openclaw/workspace/SOUL.md | head -5
sandbox$ ssh bastion.acme.com    # should connect (if your key is available)

Quick reference: what needs recreation vs. what doesn't

Change Recreation needed? Backup needed?
Allow a new host in network policy No (hot-reload) No
Add a new policy preset No (hot-reload) No
Remove a network policy block No (hot-reload) No
Switch model (same provider family) No (hot swap) No
Switch model (different provider family) Yes Yes
Install a new binary/package Yes Yes
Add a writable filesystem path Yes Yes
Change process user or limits Yes Yes
Update the container image Yes Yes
Use --from with custom Dockerfile Yes Yes
Fork blueprint with custom image Yes (once, then reproducible) Yes

← Back to 00-INDEX.md