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:
- Adding a tool (installing
ssh,ansible,kubectl, etc.) = static change = sandbox must be recreated. - Adding a network policy (allowing a new host/port) = dynamic change = can be applied live.
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:
- Build the custom image
- Destroy the existing sandbox
- Create a new sandbox from the custom image
- Apply the baseline policy (which now includes your filesystem changes)
- 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:
- Update your Dockerfile's
FROMto the new base image digest - Rebuild and push your custom image
- Update your forked
blueprint.yamlwith the new digest - Roll out
nemoclaw onboardacross 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