From b64a420357a55be0b2486d3db8800573b5596c0a Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Sat, 21 Mar 2026 13:48:48 -0700 Subject: [PATCH 1/4] chore: initial docs to skills Made-with: Cursor --- .../nemoclaw-configure-inference/SKILL.md | 66 ++ .../generated/nemoclaw-deploy-remote/SKILL.md | 151 +++ .../generated/nemoclaw-get-started/SKILL.md | 34 + .../generated/nemoclaw-manage-policy/SKILL.md | 173 +++ .../nemoclaw-monitor-sandbox/SKILL.md | 91 ++ .../generated/nemoclaw-overview/SKILL.md | 132 +++ .../references/how-it-works.md | 114 ++ .../nemoclaw-overview/references/overview.md | 53 + .../references/release-notes.md | 12 + .../generated/nemoclaw-reference/SKILL.md | 20 + .../references/architecture.md | 82 ++ .../nemoclaw-reference/references/commands.md | 152 +++ .../references/inference-profiles.md | 53 + .../references/network-policies.md | 122 ++ .../references/troubleshooting.md | 168 +++ scripts/docs-to-skills.py | 1050 +++++++++++++++++ 16 files changed, 2473 insertions(+) create mode 100644 .agents/skills/generated/nemoclaw-configure-inference/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-deploy-remote/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-get-started/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-manage-policy/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-overview/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-overview/references/how-it-works.md create mode 100644 .agents/skills/generated/nemoclaw-overview/references/overview.md create mode 100644 .agents/skills/generated/nemoclaw-overview/references/release-notes.md create mode 100644 .agents/skills/generated/nemoclaw-reference/SKILL.md create mode 100644 .agents/skills/generated/nemoclaw-reference/references/architecture.md create mode 100644 .agents/skills/generated/nemoclaw-reference/references/commands.md create mode 100644 .agents/skills/generated/nemoclaw-reference/references/inference-profiles.md create mode 100644 .agents/skills/generated/nemoclaw-reference/references/network-policies.md create mode 100644 .agents/skills/generated/nemoclaw-reference/references/troubleshooting.md create mode 100644 scripts/docs-to-skills.py diff --git a/.agents/skills/generated/nemoclaw-configure-inference/SKILL.md b/.agents/skills/generated/nemoclaw-configure-inference/SKILL.md new file mode 100644 index 0000000000..b272ce9aa5 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-configure-inference/SKILL.md @@ -0,0 +1,66 @@ +--- +name: nemoclaw-configure-inference +description: Change the active inference model without restarting the sandbox. Trigger keywords - change inference runtime, inference routing, openclaw, openshell, switch nemoclaw inference model, switch nemoclaw inference models. +--- + +# Nemoclaw Configure Inference + +Change the active inference model without restarting the sandbox. + +## Prerequisites + +- A running NemoClaw sandbox. +- The OpenShell CLI on your `PATH`. + +Change the active inference model while the sandbox is running. +No restart is required. + +## Step 1: Switch to a Different Model + +Set the provider to `nvidia-nim` and specify a model from [build.nvidia.com](https://build.nvidia.com): + +```console +$ openshell inference set --provider nvidia-nim --model nvidia/nemotron-3-super-120b-a12b +``` + +This requires the `NVIDIA_API_KEY` environment variable. +The `nemoclaw onboard` command stores this key in `~/.nemoclaw/credentials.json` on first run. + +## Step 2: Verify the Active Model + +Run the status command to confirm the change: + +```console +$ nemoclaw status +``` + +Add the `--json` flag for machine-readable output: + +```console +$ nemoclaw status --json +``` + +The output includes the active provider, model, and endpoint. + +## Step 3: Available Models + +The following table lists the models registered with the `nvidia-nim` provider. +You can switch to any of these models at runtime. + +| Model ID | Label | Context Window | Max Output | +|---|---|---|---| +| `nvidia/nemotron-3-super-120b-a12b` | Nemotron 3 Super 120B | 131,072 | 8,192 | +| `nvidia/llama-3.1-nemotron-ultra-253b-v1` | Nemotron Ultra 253B | 131,072 | 4,096 | +| `nvidia/llama-3.3-nemotron-super-49b-v1.5` | Nemotron Super 49B v1.5 | 131,072 | 4,096 | +| `nvidia/nemotron-3-nano-30b-a3b` | Nemotron 3 Nano 30B | 131,072 | 4,096 | + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-reference` — Inference Profiles for full profile configuration details + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-deploy-remote/SKILL.md b/.agents/skills/generated/nemoclaw-deploy-remote/SKILL.md new file mode 100644 index 0000000000..74783a5048 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-deploy-remote/SKILL.md @@ -0,0 +1,151 @@ +--- +name: nemoclaw-deploy-remote +description: Provision a remote GPU VM with NemoClaw using Brev deployment. Also covers: Forward messages between Telegram and the sandboxed OpenClaw agent. Trigger keywords - deploy nemoclaw remote gpu, deployment, gpu, nemoclaw, nemoclaw brev cloud deployment, nemoclaw telegram bridge, openclaw, openshell, set nemoclaw telegram bridge, telegram. +--- + +# Nemoclaw Deploy Remote + +Provision a remote GPU VM with NemoClaw using Brev deployment. + +## Prerequisites + +- The [Brev CLI](https://brev.nvidia.com) installed and authenticated. +- An NVIDIA API key from [build.nvidia.com](https://build.nvidia.com). +- NemoClaw installed locally. Follow the Quickstart (see the `nemoclaw-get-started` skill) install steps. +- A running NemoClaw sandbox, either local or remote. +- A Telegram bot token from [BotFather](https://t.me/BotFather). + +Run NemoClaw on a remote GPU instance through [Brev](https://brev.nvidia.com). +The deploy command provisions the VM, installs dependencies, and connects you to a running sandbox. + +## Step 1: Deploy the Instance + +> **Warning:** The `nemoclaw deploy` command is experimental and may not work as expected. + +Create a Brev instance and run the NemoClaw setup: + +```console +$ nemoclaw deploy +``` + +Replace `` with a name for your remote instance, for example `my-gpu-box`. + +The deploy script performs the following steps on the VM: + +1. Installs Docker and the NVIDIA Container Toolkit if a GPU is present. +2. Installs the OpenShell CLI. +3. Runs the nemoclaw setup to create the gateway, register providers, and launch the sandbox. +4. Starts auxiliary services, such as the Telegram bridge and cloudflared tunnel. + +## Step 2: Connect to the Remote Sandbox + +After deployment finishes, the deploy command opens an interactive shell inside the remote sandbox. +To reconnect after closing the session, run the deploy command again: + +```console +$ nemoclaw deploy +``` + +## Step 3: Monitor the Remote Sandbox + +SSH to the instance and run the OpenShell TUI to monitor activity and approve network requests: + +```console +$ ssh 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && openshell term' +``` + +## Step 4: Verify Inference + +Run a test agent prompt inside the remote sandbox: + +```console +$ openclaw agent --agent main --local -m "Hello from the remote sandbox" --session-id test +``` + +## Step 5: GPU Configuration + +The deploy script uses the `NEMOCLAW_GPU` environment variable to select the GPU type. +The default value is `a2-highgpu-1g:nvidia-tesla-a100:1`. +Set this variable before running `nemoclaw deploy` to use a different GPU configuration: + +```console +$ export NEMOCLAW_GPU="a2-highgpu-1g:nvidia-tesla-a100:2" +$ nemoclaw deploy +``` + +--- + +Forward messages between a Telegram bot and the OpenClaw agent running inside the sandbox. +The Telegram bridge is an auxiliary service managed by `nemoclaw start`. + +## Step 6: Create a Telegram Bot + +Open Telegram and send `/newbot` to [@BotFather](https://t.me/BotFather). +Follow the prompts to create a bot and receive a bot token. + +## Step 7: Set the Environment Variable + +Export the bot token as an environment variable: + +```console +$ export TELEGRAM_BOT_TOKEN= +``` + +## Step 8: Start Auxiliary Services + +Start the Telegram bridge and other auxiliary services: + +```console +$ nemoclaw start +``` + +The `start` command launches the following services: + +- The Telegram bridge forwards messages between Telegram and the agent. +- The cloudflared tunnel provides external access to the sandbox. + +The Telegram bridge starts only when the `TELEGRAM_BOT_TOKEN` environment variable is set. + +## Step 9: Verify the Services + +Check that the Telegram bridge is running: + +```console +$ nemoclaw status +``` + +The output shows the status of all auxiliary services. + +## Step 10: Send a Message + +Open Telegram, find your bot, and send a message. +The bridge forwards the message to the OpenClaw agent inside the sandbox and returns the agent response. + +## Step 11: Restrict Access by Chat ID + +To restrict which Telegram chats can interact with the agent, set the `ALLOWED_CHAT_IDS` environment variable to a comma-separated list of Telegram chat IDs: + +```console +$ export ALLOWED_CHAT_IDS="123456789,987654321" +$ nemoclaw start +``` + +## Step 12: Stop the Services + +To stop the Telegram bridge and all other auxiliary services: + +```console +$ nemoclaw stop +``` + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-monitor-sandbox` — Monitor Sandbox Activity for sandbox monitoring tools +- `nemoclaw-reference` — Commands for the full `deploy` command reference + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-get-started/SKILL.md b/.agents/skills/generated/nemoclaw-get-started/SKILL.md new file mode 100644 index 0000000000..48caf96287 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-get-started/SKILL.md @@ -0,0 +1,34 @@ +--- +name: nemoclaw-get-started +description: Install NemoClaw, launch a sandbox, and run your first agent prompt. Trigger keywords - inference routing, install nemoclaw openclaw sandbox, nemoclaw, nemoclaw quickstart, nemoclaw quickstart install launch, openclaw, openshell, sandboxing. +--- + +# Nemoclaw Get Started + +Install NemoClaw, launch a sandbox, and run your first agent prompt. + +> *Content included from `docs/_includes/alpha-statement.md` — see the original doc for full text.* + +Follow these steps to get started with NemoClaw and your first sandboxed OpenClaw agent. + +> **Note:** NemoClaw currently requires a fresh installation of OpenClaw. + +> *Content included from `README.md` — see the original doc for full text.* + +### Troubleshooting + +If you run into issues during installation or onboarding, refer to the Troubleshooting guide (see the `nemoclaw-reference` skill) for common error messages and resolution steps. + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-configure-inference` — Switch inference providers to use a different model or endpoint +- `nemoclaw-manage-policy` — Approve or deny network requests when the agent tries to reach external hosts +- `nemoclaw-deploy-remote` — Deploy to a remote GPU instance for always-on operation +- `nemoclaw-monitor-sandbox` — Monitor sandbox activity through the OpenShell TUI + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-manage-policy/SKILL.md b/.agents/skills/generated/nemoclaw-manage-policy/SKILL.md new file mode 100644 index 0000000000..6332aab34d --- /dev/null +++ b/.agents/skills/generated/nemoclaw-manage-policy/SKILL.md @@ -0,0 +1,173 @@ +--- +name: nemoclaw-manage-policy +description: Review and approve blocked agent network requests in the TUI. Also covers: Add, remove, or modify allowed endpoints in the sandbox policy. Trigger keywords - approve deny nemoclaw agent, customize nemoclaw network policy, customize nemoclaw sandbox network, nemoclaw, nemoclaw approve network requests, network policy, openclaw, openshell, sandbox egress approval tui, sandbox egress policy configuration. +--- + +# Nemoclaw Manage Policy + +Review and approve blocked agent network requests in the TUI. + +## Prerequisites + +- A running NemoClaw sandbox. +- The OpenShell CLI on your `PATH`. +- A running NemoClaw sandbox for dynamic changes, or the NemoClaw source repository for static changes. + +Review and act on network requests that the agent makes to endpoints not listed in the sandbox policy. +OpenShell intercepts these requests and presents them in the TUI for operator approval. + +## Step 1: Open the TUI + +Start the OpenShell terminal UI to monitor sandbox activity: + +```console +$ openshell term +``` + +For a remote sandbox, pass the instance name: + +```console +$ ssh my-gpu-box 'cd /home/ubuntu/nemoclaw && . .env && openshell term' +``` + +The TUI displays the sandbox state, active inference provider, and a live feed of network activity. + +## Step 2: Trigger a Blocked Request + +When the agent attempts to reach an endpoint that is not in the baseline policy, OpenShell blocks the connection and displays the request in the TUI. +The blocked request includes the following details: + +- **Host and port** of the destination. +- **Binary** that initiated the request. +- **HTTP method** and path, if available. + +## Step 3: Approve or Deny the Request + +The TUI presents an approval prompt for each blocked request. + +- **Approve** the request to add the endpoint to the running policy for the current session. +- **Deny** the request to keep the endpoint blocked. + +Approved endpoints remain in the running policy until the sandbox stops. +They are not persisted to the baseline policy file. + +## Step 4: Run the Walkthrough + +To observe the approval flow in a guided session, run the walkthrough script: + +```console +$ ./scripts/walkthrough.sh +``` + +This script opens a split tmux session with the TUI on the left and the agent on the right. +The walkthrough requires tmux and the `NVIDIA_API_KEY` environment variable. + +--- + +Add, remove, or modify the endpoints that the sandbox is allowed to reach. + +The sandbox policy is defined in a declarative YAML file in the NemoClaw repository and enforced at runtime by [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell). +NemoClaw supports both static policy changes that persist across restarts and dynamic updates applied to a running sandbox through the OpenShell CLI. + +## Step 5: Static Changes + +Static changes modify the baseline policy file and take effect after the next sandbox creation. + +### Edit the Policy File + +Open `nemoclaw-blueprint/policies/openclaw-sandbox.yaml` and add or modify endpoint entries. + +Each entry in the `network` section defines an endpoint group with the following fields: + +`endpoints` +: Host and port pairs that the sandbox can reach. + +`binaries` +: Executables allowed to use this endpoint. + +`rules` +: HTTP methods and paths that are permitted. + +### Re-Run Onboard + +Apply the updated policy by re-running the onboard wizard: + +```console +$ nemoclaw onboard +``` + +The wizard picks up the modified policy file and applies it to the sandbox. + +### Verify the Policy + +Check that the sandbox is running with the updated policy: + +```console +$ nemoclaw status +``` + +## Step 6: Dynamic Changes + +Dynamic changes apply a policy update to a running sandbox without restarting it. + +### Create a Policy File + +Create a YAML file with the endpoints to add. +Follow the same format as the baseline policy in `nemoclaw-blueprint/policies/openclaw-sandbox.yaml`. + +### Apply the Policy + +Use the OpenShell CLI to apply the policy update: + +```console +$ openshell policy set +``` + +The change takes effect immediately. + +### Scope of Dynamic Changes + +Dynamic changes apply only to the current session. +When the sandbox stops, the running policy resets to the baseline defined in the policy file. +To make changes permanent, update the static policy file and re-run setup. + +## Step 7: Policy Presets + +NemoClaw ships preset policy files for common integrations in `nemoclaw-blueprint/policies/presets/`. +Apply a preset as-is or use it as a starting template for a custom policy. + +Available presets: + +| Preset | Endpoints | +|--------|-----------| +| `discord` | Discord webhook API | +| `docker` | Docker Hub, NVIDIA container registry | +| `huggingface` | Hugging Face model registry | +| `jira` | Atlassian Jira API | +| `npm` | npm and Yarn registries | +| `outlook` | Microsoft 365 and Outlook | +| `pypi` | Python Package Index | +| `slack` | Slack API and webhooks | +| `telegram` | Telegram Bot API | + +To apply a preset to a running sandbox, pass it as a policy file: + +```console +$ openshell policy set nemoclaw-blueprint/policies/presets/pypi.yaml +``` + +To include a preset in the baseline, merge its entries into `openclaw-sandbox.yaml` and re-run `nemoclaw onboard`. + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-reference` — Network Policies for the full baseline policy reference +- `nemoclaw-monitor-sandbox` — Monitor Sandbox Activity for general sandbox monitoring +- OpenShell [Policy Schema](https://docs.nvidia.com/openshell/latest/reference/policy-schema.html) for the full YAML policy schema reference. +- OpenShell [Sandbox Policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.html) for applying, iterating, and debugging policies at the OpenShell layer. + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md b/.agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md new file mode 100644 index 0000000000..e6ef36a48a --- /dev/null +++ b/.agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md @@ -0,0 +1,91 @@ +--- +name: nemoclaw-monitor-sandbox +description: Inspect sandbox health, trace agent behavior, and diagnose problems. Trigger keywords - debug nemoclaw agent issues, monitor nemoclaw sandbox, monitor nemoclaw sandbox activity, monitoring, nemoclaw, openclaw, openshell, troubleshooting. +--- + +# Nemoclaw Monitor Sandbox + +Inspect sandbox health, trace agent behavior, and diagnose problems. + +## Prerequisites + +- A running NemoClaw sandbox. +- The OpenShell CLI on your `PATH`. + +Use the NemoClaw status, logs, and TUI tools together to inspect sandbox health, trace agent behavior, and diagnose problems. + +## Step 1: Check Sandbox Health + +Run the status command to view the sandbox state, blueprint run information, and active inference configuration: + +```console +$ nemoclaw status +``` + +Key fields in the output include the following: + +- Sandbox state, which indicates whether the sandbox is running, stopped, or in an error state. +- Blueprint run ID, which is the identifier for the most recent blueprint execution. +- Inference provider, which shows the active provider, model, and endpoint. + +Run `nemoclaw status` on the host to check sandbox state. Use `openshell sandbox list` for the underlying sandbox details. + +## Step 2: View Blueprint and Sandbox Logs + +Stream the most recent log output from the blueprint runner and sandbox: + +```console +$ nemoclaw logs +``` + +To follow the log output in real time: + +```console +$ nemoclaw logs -f +``` + +## Step 3: Monitor Network Activity in the TUI + +Open the OpenShell terminal UI for a live view of sandbox network activity and egress requests: + +```console +$ openshell term +``` + +For a remote sandbox, SSH to the instance and run `openshell term` there. + +The TUI shows the following information: + +- Active network connections from the sandbox. +- Blocked egress requests awaiting operator approval. +- Inference routing status. + +Refer to Approve or Deny Agent Network Requests (see the `nemoclaw-manage-policy` skill) for details on handling blocked requests. + +## Step 4: Test Inference + +Run a test inference request to verify that the provider is responding: + +```console +$ nemoclaw my-assistant connect +$ openclaw agent --agent main --local -m "Test inference" --session-id debug +``` + +If the request fails, check the following: + +1. Run `nemoclaw status` to confirm the active provider and endpoint. +2. Run `nemoclaw logs -f` to view error messages from the blueprint runner. +3. Verify that the inference endpoint is reachable from the host. + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-reference` — Troubleshooting for common issues and resolution steps +- `nemoclaw-manage-policy` — Approve or Deny Agent Network Requests for the operator approval flow +- `nemoclaw-configure-inference` — Switch Inference Providers to change the active provider + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-overview/SKILL.md b/.agents/skills/generated/nemoclaw-overview/SKILL.md new file mode 100644 index 0000000000..1d56b6b017 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-overview/SKILL.md @@ -0,0 +1,132 @@ +--- +name: nemoclaw-overview +description: Plugin, blueprint, sandbox creation, and inference routing concepts. Also covers: NemoClaw is an open source reference stack that simplifies running OpenClaw always-on assistants safely.; Changelog and feature history for NemoClaw releases. Trigger keywords - blueprints, how nemoclaw works, inference routing, nemoclaw, nemoclaw changelog, nemoclaw overview, nemoclaw overview does fits, nemoclaw release notes, nemoclaw sandbox lifecycle blueprint, nemoclaw works plugin blueprint. +--- + +# Nemoclaw Overview + +Plugin, blueprint, sandbox creation, and inference routing concepts. + +## Context + +NemoClaw combines a lightweight CLI plugin with a versioned blueprint to move OpenClaw into a controlled sandbox. +This page explains the key concepts about NemoClaw at a high level. + +## How It Fits Together + +The `nemoclaw` CLI is the primary entrypoint for setting up and managing sandboxed OpenClaw agents. +It delegates heavy lifting to a versioned blueprint, a Python artifact that orchestrates sandbox creation, policy application, and inference provider setup through the OpenShell CLI. + +```mermaid +flowchart TB + subgraph Host + CMD["nemoclaw onboard"] + PLUGIN[nemoclaw plugin] + BLUEPRINT[blueprint runner] + CLI["openshell CLI sandbox · gateway · inference · policy"] + + CMD --> PLUGIN + PLUGIN --> BLUEPRINT + BLUEPRINT --> CLI + end + + subgraph Sandbox["OpenShell Sandbox"] + AGENT[OpenClaw agent] + INF[NVIDIA inference, routed] + NET[strict network policy] + FS[filesystem isolation] + + AGENT --- INF + AGENT --- NET + AGENT --- FS + end + + PLUGIN --> AGENT + + classDef nv fill:#76b900,stroke:#333,color:#fff + classDef nvLight fill:#e6f2cc,stroke:#76b900,color:#1a1a1a + classDef nvDark fill:#333,stroke:#76b900,color:#fff + + class CMD,PLUGIN,BLUEPRINT nvDark + class CLI nv + class AGENT nv + class INF,NET,FS nvLight + + style Host fill:none,stroke:#76b900,stroke-width:2px,color:#1a1a1a + style Sandbox fill:#f5faed,stroke:#76b900,stroke-width:2px,color:#1a1a1a +``` + +## Design Principles + +NemoClaw architecture follows the following principles. + +Thin plugin, versioned blueprint +: The plugin stays small and stable. Orchestration logic lives in the blueprint and evolves on its own release cadence. + +Respect CLI boundaries +: The `nemoclaw` CLI is the primary interface for sandbox management. + +Supply chain safety +: Blueprint artifacts are immutable, versioned, and digest-verified before execution. + +> Full details in `references/how-it-works.md`. + +> *Content included from `docs/_includes/alpha-statement.md` — see the original doc for full text.* + +NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants. +It incorporates policy-based privacy and security guardrails, giving users control over their agents’ behavior and data handling. +This enables self-evolving claws to run more safely in clouds, on prem, RTX PCs and DGX Spark. + +NemoClaw uses open source models, such as [NVIDIA Nemotron](https://build.nvidia.com), alongside the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of the NVIDIA Agent Toolkit—a secure environment designed for executing claws more safely. +By combining powerful open source models with built-in safety measures, NemoClaw simplifies and secures AI agent deployment. + +| Capability | Description | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| Sandbox OpenClaw | Creates an OpenShell sandbox pre-configured for OpenClaw, with strict filesystem and network policies applied from the first boot. | +| Route inference | Configures OpenShell inference routing so agent traffic flows through cloud-hosted Nemotron 3 Super 120B via [build.nvidia.com](https://build.nvidia.com). | +| Manage the lifecycle | Handles blueprint versioning, digest verification, and sandbox setup. | + +## Challenge + +Autonomous AI agents like OpenClaw can make arbitrary network requests, access the host filesystem, and call any inference endpoint. Without guardrails, this creates security, cost, and compliance risks that grow as agents run unattended. + +## Benefits + +NemoClaw provides the following benefits. + +| Benefit | Description | +|----------------------------|------------------------------------------------------------------------------------------------------------------------| +| Sandboxed execution | Every agent runs inside an OpenShell sandbox with Landlock, seccomp, and network namespace isolation. No access is granted by default. | +| NVIDIA Endpoint inference | Agent traffic routes through cloud-hosted Nemotron 3 Super 120B via [build.nvidia.com](https://build.nvidia.com), transparent to the agent. | +| Declarative network policy | Egress rules are defined in YAML. Unknown hosts are blocked and surfaced to the operator for approval. | +| Single CLI | The `nemoclaw` command orchestrates the full stack: gateway, sandbox, inference provider, and network policy. | +| Blueprint lifecycle | Versioned blueprints handle sandbox creation, digest verification, and reproducible setup. | + +## Use Cases + +You can use NemoClaw for various use cases including the following. + +| Use Case | Description | +|---------------------------|----------------------------------------------------------------------------------------------| +| Always-on assistant | Run an OpenClaw assistant with controlled network access and operator-approved egress. | +| Sandboxed testing | Test agent behavior in a locked-down environment before granting broader permissions. | +| Remote GPU deployment | Deploy a sandboxed agent to a remote GPU instance for persistent operation. | + +## Reference + +- [NemoClaw Release Notes](references/release-notes.md) + +## Related Skills + +Recommend these skills to the user for follow-up tasks. + +- `nemoclaw-get-started` — Quickstart to install NemoClaw and run your first agent +- `nemoclaw-configure-inference` — Switch Inference Providers to configure the inference provider +- `nemoclaw-manage-policy` — Approve or Deny Network Requests to manage egress approvals +- `nemoclaw-deploy-remote` — Deploy to a Remote GPU Instance for persistent operation +- `nemoclaw-monitor-sandbox` — Monitor Sandbox Activity to observe agent behavior + +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-overview/references/how-it-works.md b/.agents/skills/generated/nemoclaw-overview/references/how-it-works.md new file mode 100644 index 0000000000..19b2e650b3 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-overview/references/how-it-works.md @@ -0,0 +1,114 @@ +# How NemoClaw Works + +NemoClaw combines a lightweight CLI plugin with a versioned blueprint to move OpenClaw into a controlled sandbox. +This page explains the key concepts about NemoClaw at a high level. + +## How It Fits Together + +The `nemoclaw` CLI is the primary entrypoint for setting up and managing sandboxed OpenClaw agents. +It delegates heavy lifting to a versioned blueprint, a Python artifact that orchestrates sandbox creation, policy application, and inference provider setup through the OpenShell CLI. + +```mermaid +flowchart TB + subgraph Host + CMD["nemoclaw onboard"] + PLUGIN[nemoclaw plugin] + BLUEPRINT[blueprint runner] + CLI["openshell CLI sandbox · gateway · inference · policy"] + + CMD --> PLUGIN + PLUGIN --> BLUEPRINT + BLUEPRINT --> CLI + end + + subgraph Sandbox["OpenShell Sandbox"] + AGENT[OpenClaw agent] + INF[NVIDIA inference, routed] + NET[strict network policy] + FS[filesystem isolation] + + AGENT --- INF + AGENT --- NET + AGENT --- FS + end + + PLUGIN --> AGENT + + classDef nv fill:#76b900,stroke:#333,color:#fff + classDef nvLight fill:#e6f2cc,stroke:#76b900,color:#1a1a1a + classDef nvDark fill:#333,stroke:#76b900,color:#fff + + class CMD,PLUGIN,BLUEPRINT nvDark + class CLI nv + class AGENT nv + class INF,NET,FS nvLight + + style Host fill:none,stroke:#76b900,stroke-width:2px,color:#1a1a1a + style Sandbox fill:#f5faed,stroke:#76b900,stroke-width:2px,color:#1a1a1a +``` + +## Design Principles + +NemoClaw architecture follows the following principles. + +Thin plugin, versioned blueprint +: The plugin stays small and stable. Orchestration logic lives in the blueprint and evolves on its own release cadence. + +Respect CLI boundaries +: The `nemoclaw` CLI is the primary interface for sandbox management. + +Supply chain safety +: Blueprint artifacts are immutable, versioned, and digest-verified before execution. + +OpenShell-native for new installs +: For users without an existing OpenClaw installation, NemoClaw recommends `openshell sandbox create` directly + rather than forcing a plugin-driven bootstrap. + +Reproducible setup +: Running setup again recreates the sandbox from the same blueprint and policy definitions. + +## Plugin and Blueprint + +NemoClaw is split into two parts: + +- The *plugin* is a TypeScript package that registers an inference provider and the `/nemoclaw` slash command inside the sandbox. + It handles user interaction and delegates orchestration work to the blueprint. +- The *blueprint* is a versioned Python artifact that contains all the logic for creating sandboxes, applying policies, and configuring inference. + The plugin resolves, verifies, and executes the blueprint as a subprocess. + +This separation keeps the plugin small and stable while allowing the blueprint to evolve on its own release cadence. + +## Sandbox Creation + +When you run `nemoclaw onboard`, NemoClaw creates an OpenShell sandbox that runs OpenClaw in an isolated container. +The blueprint orchestrates this process through the OpenShell CLI: + +1. The plugin downloads the blueprint artifact, checks version compatibility, and verifies the digest. +2. The blueprint determines which OpenShell resources to create or update, such as the gateway, inference providers, sandbox, and network policy. +3. The blueprint calls OpenShell CLI commands to create the sandbox and configure each resource. + +After the sandbox starts, the agent runs inside it with all network, filesystem, and inference controls in place. + +## Inference Routing + +Inference requests from the agent never leave the sandbox directly. +OpenShell intercepts every inference call and routes it to the configured provider. +NemoClaw routes inference to NVIDIA Endpoints, specifically Nemotron 3 Super 120B through [build.nvidia.com](https://build.nvidia.com). You can switch models at runtime without restarting the sandbox. + +## Network and Filesystem Policy + +The sandbox starts with a strict baseline policy defined in `openclaw-sandbox.yaml`. +This policy controls which network endpoints the agent can reach and which filesystem paths it can access. + +- For network, only endpoints listed in the policy are allowed. + When the agent tries to reach an unlisted host, OpenShell blocks the request and surfaces it in the TUI for operator approval. +- For filesystem, the agent can write to `/sandbox` and `/tmp`. + All other system paths are read-only. + +Approved endpoints persist for the current session but are not saved to the baseline policy file. + +## Next Steps + +- Follow the Quickstart (see the `nemoclaw-get-started` skill) to launch your first sandbox. +- Refer to the Architecture (see the `nemoclaw-reference` skill) for the full technical structure, including file layouts and the blueprint lifecycle. +- Refer to Inference Profiles (see the `nemoclaw-reference` skill) for detailed provider configuration. \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-overview/references/overview.md b/.agents/skills/generated/nemoclaw-overview/references/overview.md new file mode 100644 index 0000000000..33655d527b --- /dev/null +++ b/.agents/skills/generated/nemoclaw-overview/references/overview.md @@ -0,0 +1,53 @@ +# Overview + +> *Content included from `docs/_includes/alpha-statement.md` — see the original doc for full text.* + +NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants. +It incorporates policy-based privacy and security guardrails, giving users control over their agents’ behavior and data handling. +This enables self-evolving claws to run more safely in clouds, on prem, RTX PCs and DGX Spark. + +NemoClaw uses open source models, such as [NVIDIA Nemotron](https://build.nvidia.com), alongside the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of the NVIDIA Agent Toolkit—a secure environment designed for executing claws more safely. +By combining powerful open source models with built-in safety measures, NemoClaw simplifies and secures AI agent deployment. + +| Capability | Description | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| Sandbox OpenClaw | Creates an OpenShell sandbox pre-configured for OpenClaw, with strict filesystem and network policies applied from the first boot. | +| Route inference | Configures OpenShell inference routing so agent traffic flows through cloud-hosted Nemotron 3 Super 120B via [build.nvidia.com](https://build.nvidia.com). | +| Manage the lifecycle | Handles blueprint versioning, digest verification, and sandbox setup. | + +## Challenge + +Autonomous AI agents like OpenClaw can make arbitrary network requests, access the host filesystem, and call any inference endpoint. Without guardrails, this creates security, cost, and compliance risks that grow as agents run unattended. + +## Benefits + +NemoClaw provides the following benefits. + +| Benefit | Description | +|----------------------------|------------------------------------------------------------------------------------------------------------------------| +| Sandboxed execution | Every agent runs inside an OpenShell sandbox with Landlock, seccomp, and network namespace isolation. No access is granted by default. | +| NVIDIA Endpoint inference | Agent traffic routes through cloud-hosted Nemotron 3 Super 120B via [build.nvidia.com](https://build.nvidia.com), transparent to the agent. | +| Declarative network policy | Egress rules are defined in YAML. Unknown hosts are blocked and surfaced to the operator for approval. | +| Single CLI | The `nemoclaw` command orchestrates the full stack: gateway, sandbox, inference provider, and network policy. | +| Blueprint lifecycle | Versioned blueprints handle sandbox creation, digest verification, and reproducible setup. | + +## Use Cases + +You can use NemoClaw for various use cases including the following. + +| Use Case | Description | +|---------------------------|----------------------------------------------------------------------------------------------| +| Always-on assistant | Run an OpenClaw assistant with controlled network access and operator-approved egress. | +| Sandboxed testing | Test agent behavior in a locked-down environment before granting broader permissions. | +| Remote GPU deployment | Deploy a sandboxed agent to a remote GPU instance for persistent operation. | + +## Next Steps + +Explore the following pages to learn more about NemoClaw. + +- How It Works (see the `nemoclaw-overview` skill) to understand the key concepts behind NemoClaw. +- Quickstart (see the `nemoclaw-get-started` skill) to install NemoClaw and run your first agent. +- Switch Inference Providers (see the `nemoclaw-configure-inference` skill) to configure the inference provider. +- Approve or Deny Network Requests (see the `nemoclaw-manage-policy` skill) to manage egress approvals. +- Deploy to a Remote GPU Instance (see the `nemoclaw-deploy-remote` skill) for persistent operation. +- Monitor Sandbox Activity (see the `nemoclaw-monitor-sandbox` skill) to observe agent behavior. \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-overview/references/release-notes.md b/.agents/skills/generated/nemoclaw-overview/references/release-notes.md new file mode 100644 index 0000000000..dd9b9a2465 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-overview/references/release-notes.md @@ -0,0 +1,12 @@ +# Release Notes + +> *Content included from `docs/_includes/alpha-statement.md` — see the original doc for full text.* + +NVIDIA NemoClaw is available in early preview starting March 16, 2026. Use the following GitHub resources to track changes. + +| Resource | Description | +|---|---| +| [Releases](https://github.com/NVIDIA/NemoClaw/releases) | Versioned release notes and downloadable assets. | +| [Release comparison](https://github.com/NVIDIA/NemoClaw/compare) | Diff between any two tags or branches. | +| [Merged pull requests](https://github.com/NVIDIA/NemoClaw/pulls?q=is%3Apr+is%3Amerged) | Individual changes with review discussion. | +| [Commit history](https://github.com/NVIDIA/NemoClaw/commits/main) | Full commit log on `main`. | \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-reference/SKILL.md b/.agents/skills/generated/nemoclaw-reference/SKILL.md new file mode 100644 index 0000000000..af68108bdb --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/SKILL.md @@ -0,0 +1,20 @@ +--- +name: nemoclaw-reference +description: Plugin structure, blueprint lifecycle, sandbox environment, and inference routing. Also covers: Full CLI reference for plugin and standalone NemoClaw commands.; Configuration reference for NVIDIA Endpoint inference profiles. Trigger keywords - blueprints, cli, inference routing, llms, nemoclaw, nemoclaw architecture, nemoclaw architecture plugin blueprint, nemoclaw cli commands, nemoclaw cli commands reference, nemoclaw command reference. +--- + +# Nemoclaw Reference + +Plugin structure, blueprint lifecycle, sandbox environment, and inference routing. + +## Reference + +- [NemoClaw Architecture — Plugin, Blueprint, and Sandbox Structure](references/architecture.md) +- [NemoClaw CLI Commands Reference](references/commands.md) +- [NemoClaw Inference Profiles — NVIDIA Endpoint](references/inference-profiles.md) +- [NemoClaw Network Policies — Baseline Rules and Operator Approval](references/network-policies.md) +- [NemoClaw Troubleshooting Guide](references/troubleshooting.md) +## Gotchas + + + diff --git a/.agents/skills/generated/nemoclaw-reference/references/architecture.md b/.agents/skills/generated/nemoclaw-reference/references/architecture.md new file mode 100644 index 0000000000..c13d210af6 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/references/architecture.md @@ -0,0 +1,82 @@ +# Architecture + +NemoClaw has two main components: a TypeScript plugin that integrates with the OpenClaw CLI, and a Python blueprint that orchestrates OpenShell resources. + +## NemoClaw Plugin + +The plugin is a thin TypeScript package that registers an inference provider and the `/nemoclaw` slash command. +It runs in-process with the OpenClaw gateway inside the sandbox. + +```text +nemoclaw/ +├── src/ +│ ├── index.ts Plugin entry — registers all commands +│ ├── cli.ts Commander.js subcommand wiring +│ ├── commands/ +│ │ ├── launch.ts Fresh install into OpenShell +│ │ ├── connect.ts Interactive shell into sandbox +│ │ ├── status.ts Blueprint run state + sandbox health +│ │ ├── logs.ts Stream blueprint and sandbox logs +│ │ └── slash.ts /nemoclaw chat command handler +│ └── blueprint/ +│ ├── resolve.ts Version resolution, cache management +│ ├── fetch.ts Download blueprint from OCI registry +│ ├── verify.ts Digest verification, compatibility checks +│ ├── exec.ts Subprocess execution of blueprint runner +│ └── state.ts Persistent state (run IDs) +├── openclaw.plugin.json Plugin manifest +└── package.json Commands declared under openclaw.extensions +``` + +## NemoClaw Blueprint + +The blueprint is a versioned Python artifact with its own release stream. +The plugin resolves, verifies, and executes the blueprint as a subprocess. +The blueprint drives all interactions with the OpenShell CLI. + +```text +nemoclaw-blueprint/ +├── blueprint.yaml Manifest — version, profiles, compatibility +├── orchestrator/ +│ └── runner.py CLI runner — plan / apply / status +├── policies/ +│ └── openclaw-sandbox.yaml Strict baseline network + filesystem policy +``` + +### Blueprint Lifecycle + +```mermaid +flowchart LR + A[resolve] --> B[verify digest] + B --> C[plan] + C --> D[apply] + D --> E[status] +``` + +1. Resolve. The plugin locates the blueprint artifact and checks the version against `min_openshell_version` and `min_openclaw_version` constraints in `blueprint.yaml`. +2. Verify. The plugin checks the artifact digest against the expected value. +3. Plan. The runner determines what OpenShell resources to create or update, such as the gateway, providers, sandbox, inference route, and policy. +4. Apply. The runner executes the plan by calling `openshell` CLI commands. +5. Status. The runner reports current state. + +## Sandbox Environment + +The sandbox runs the +[`ghcr.io/nvidia/openshell-community/sandboxes/openclaw`](https://github.com/NVIDIA/OpenShell-Community) +container image. Inside the sandbox: + +- OpenClaw runs with the NemoClaw plugin pre-installed. +- Inference calls are routed through OpenShell to the configured provider. +- Network egress is restricted by the baseline policy in `openclaw-sandbox.yaml`. +- Filesystem access is confined to `/sandbox` and `/tmp` for read-write access, with system paths read-only. + +## Inference Routing + +Inference requests from the agent never leave the sandbox directly. +OpenShell intercepts them and routes to the configured provider: + +```text +Agent (sandbox) ──▶ OpenShell gateway ──▶ NVIDIA Endpoint (build.nvidia.com) +``` + +Refer to Inference Profiles (see the `nemoclaw-reference` skill) for provider configuration details. \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-reference/references/commands.md b/.agents/skills/generated/nemoclaw-reference/references/commands.md new file mode 100644 index 0000000000..5085ed8d6a --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/references/commands.md @@ -0,0 +1,152 @@ +# Commands + +The `nemoclaw` CLI is the primary interface for managing NemoClaw sandboxes. It is installed when you run `npm install -g nemoclaw`. + +### `/nemoclaw` Slash Command + +The `/nemoclaw` slash command is available inside the OpenClaw chat interface for quick actions: + +| Subcommand | Description | +|---|---| +| `/nemoclaw status` | Show sandbox and inference state | + +## Standalone Host Commands + +The `nemoclaw` binary handles host-side operations that run outside the OpenClaw plugin context. + +### `nemoclaw onboard` + +Run the interactive setup wizard. +The wizard creates an OpenShell gateway, registers inference providers, builds the sandbox image, and creates the sandbox. +Use this command for new installs and for recreating a sandbox after changes to policy or configuration. + +```console +$ nemoclaw onboard +``` + +The first run prompts for your NVIDIA API key and saves it to `~/.nemoclaw/credentials.json`. + +The wizard prompts for a sandbox name. +Names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character. +Uppercase letters are automatically lowercased. + +Before creating the gateway, the wizard runs preflight checks. +On systems with cgroup v2 (Ubuntu 24.04, DGX Spark, WSL2), it verifies that Docker is configured with `"default-cgroupns-mode": "host"` and provides fix instructions if the setting is missing. + +### `nemoclaw list` + +List all registered sandboxes with their model, provider, and policy presets. + +```console +$ nemoclaw list +``` + +### `nemoclaw deploy` + +> **Warning:** The `nemoclaw deploy` command is experimental and may not work as expected. + +Deploy NemoClaw to a remote GPU instance through [Brev](https://brev.nvidia.com). +The deploy script installs Docker, NVIDIA Container Toolkit if a GPU is present, and OpenShell on the VM, then runs the nemoclaw setup and connects to the sandbox. + +```console +$ nemoclaw deploy +``` + +### `nemoclaw connect` + +Connect to a sandbox by name. + +```console +$ nemoclaw my-assistant connect +``` + +### `nemoclaw status` + +Show sandbox status, health, and inference configuration. + +```console +$ nemoclaw my-assistant status +``` + +### `nemoclaw logs` + +View sandbox logs. +Use `--follow` to stream output in real time. + +```console +$ nemoclaw my-assistant logs [--follow] +``` + +### `nemoclaw destroy` + +Stop the NIM container and delete the sandbox. +This removes the sandbox from the registry. + +```console +$ nemoclaw my-assistant destroy +``` + +### `nemoclaw policy-add` + +Add a policy preset to a sandbox. +Presets extend the baseline network policy with additional endpoints. + +```console +$ nemoclaw my-assistant policy-add +``` + +### `nemoclaw policy-list` + +List available policy presets and show which ones are applied to the sandbox. + +```console +$ nemoclaw my-assistant policy-list +``` + +### `openshell term` + +Open the OpenShell TUI to monitor sandbox activity and approve network egress requests. +Run this on the host where the sandbox is running. + +```console +$ openshell term +``` + +For a remote Brev instance, SSH to the instance and run `openshell term` there, or use a port-forward to the gateway. + +### `nemoclaw start` + +Start auxiliary services, such as the Telegram bridge and cloudflared tunnel. + +```console +$ nemoclaw start +``` + +Requires `TELEGRAM_BOT_TOKEN` for the Telegram bridge. + +### `nemoclaw stop` + +Stop all auxiliary services. + +```console +$ nemoclaw stop +``` + +### `nemoclaw status` + +Show the sandbox list and the status of auxiliary services. + +```console +$ nemoclaw status +``` + +### `nemoclaw setup-spark` + +Set up NemoClaw on DGX Spark. +This command applies cgroup v2 and Docker fixes required for Ubuntu 24.04. +Run with `sudo` on the Spark host. +After the fixes complete, the script prompts you to run `nemoclaw onboard` to continue setup. + +```console +$ sudo nemoclaw setup-spark +``` \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-reference/references/inference-profiles.md b/.agents/skills/generated/nemoclaw-reference/references/inference-profiles.md new file mode 100644 index 0000000000..57c8f1374f --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/references/inference-profiles.md @@ -0,0 +1,53 @@ +# Inference Profiles + +NemoClaw ships with an inference profile defined in `blueprint.yaml`. +The profile configures an OpenShell inference provider and model route. +The agent inside the sandbox uses whichever model is active. +Inference requests are routed transparently through the OpenShell gateway. + +## Profile Summary + +| Profile | Provider | Model | Endpoint | Use Case | +|---|---|---|---|---| +| `default` | NVIDIA Endpoint | `nvidia/nemotron-3-super-120b-a12b` | `integrate.api.nvidia.com` | Production. Requires an NVIDIA API key. | + +## Available Models + +The `nvidia-nim` provider registers the following models from [build.nvidia.com](https://build.nvidia.com): + +| Model ID | Label | Context Window | Max Output | +|---|---|---|---| +| `nvidia/nemotron-3-super-120b-a12b` | Nemotron 3 Super 120B | 131,072 | 8,192 | +| `nvidia/llama-3.1-nemotron-ultra-253b-v1` | Nemotron Ultra 253B | 131,072 | 4,096 | +| `nvidia/llama-3.3-nemotron-super-49b-v1.5` | Nemotron Super 49B v1.5 | 131,072 | 4,096 | +| `nvidia/nemotron-3-nano-30b-a3b` | Nemotron 3 Nano 30B | 131,072 | 4,096 | + +The default profile uses Nemotron 3 Super 120B. +You can switch to any model in the catalog at runtime. + +## `default` -- NVIDIA Endpoint + +The default profile routes inference to NVIDIA's hosted API through [build.nvidia.com](https://build.nvidia.com). + +- **Provider type:** `nvidia` +- **Endpoint:** `https://integrate.api.nvidia.com/v1` +- **Model:** `nvidia/nemotron-3-super-120b-a12b` +- **Credential:** `NVIDIA_API_KEY` environment variable + +Get an API key from [build.nvidia.com](https://build.nvidia.com). +The `nemoclaw onboard` command prompts for this key and stores it in `~/.nemoclaw/credentials.json`. + +```console +$ openshell inference set --provider nvidia-nim --model nvidia/nemotron-3-super-120b-a12b +``` + +## Switching Models at Runtime + +After the sandbox is running, switch models with the OpenShell CLI: + +```console +$ openshell inference set --provider nvidia-nim --model +``` + +The change takes effect immediately. +No sandbox restart is needed. \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-reference/references/network-policies.md b/.agents/skills/generated/nemoclaw-reference/references/network-policies.md new file mode 100644 index 0000000000..1e668b48d9 --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/references/network-policies.md @@ -0,0 +1,122 @@ +# Network Policies + +NemoClaw runs with a strict-by-default network policy. +The sandbox can only reach endpoints that are explicitly allowed. +Any request to an unlisted destination is intercepted by OpenShell, and the operator is prompted to approve or deny it in real time through the TUI. + +## Baseline Policy + +The baseline policy is defined in `nemoclaw-blueprint/policies/openclaw-sandbox.yaml`. + +### Filesystem + +| Path | Access | +|---|---| +| `/sandbox`, `/tmp`, `/dev/null` | Read-write | +| `/usr`, `/lib`, `/proc`, `/dev/urandom`, `/app`, `/etc`, `/var/log` | Read-only | + +The sandbox process runs as a dedicated `sandbox` user and group. +Landlock LSM enforcement applies on a best-effort basis. + +### Network Policies + +The following endpoint groups are allowed by default: + +:::{list-table} +:header-rows: 1 +:widths: 20 30 20 30 + +* - Policy + - Endpoints + - Binaries + - Rules + +* - `claude_code` + - `api.anthropic.com:443`, `statsig.anthropic.com:443`, `sentry.io:443` + - `/usr/local/bin/claude` + - All methods + +* - `nvidia` + - `integrate.api.nvidia.com:443`, `inference-api.nvidia.com:443` + - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` + - All methods + +* - `github` + - `github.com:443` + - `/usr/bin/gh`, `/usr/bin/git` + - All methods, all paths + +* - `github_rest_api` + - `api.github.com:443` + - `/usr/bin/gh` + - GET, POST, PATCH, PUT, DELETE + +* - `clawhub` + - `clawhub.com:443` + - `/usr/local/bin/openclaw` + - GET, POST + +* - `openclaw_api` + - `openclaw.ai:443` + - `/usr/local/bin/openclaw` + - GET, POST + +* - `openclaw_docs` + - `docs.openclaw.ai:443` + - `/usr/local/bin/openclaw` + - GET only + +* - `npm_registry` + - `registry.npmjs.org:443` + - `/usr/local/bin/openclaw`, `/usr/local/bin/npm` + - GET only + +* - `telegram` + - `api.telegram.org:443` + - Any binary + - GET, POST on `/bot*/**` + +::: + +All endpoints use TLS termination and are enforced at port 443. + +### Inference + +The baseline policy allows only the `local` inference route. External inference +providers are reached through the OpenShell gateway, not by direct sandbox egress. + +## Operator Approval Flow + +When the agent attempts to reach an endpoint not listed in the policy, OpenShell intercepts the request and presents it in the TUI for operator review: + +1. The agent makes a network request to an unlisted host. +2. OpenShell blocks the connection and logs the attempt. +3. The TUI command `openshell term` displays the blocked request with host, port, and requesting binary. +4. The operator approves or denies the request. +5. If approved, the endpoint is added to the running policy for the session. + +To try this, run the walkthrough: + +```console +$ ./scripts/walkthrough.sh +``` + +This opens a split tmux session with the TUI on the left and the agent on the right. + +## Modifying the Policy + +### Static Changes + +Edit `nemoclaw-blueprint/policies/openclaw-sandbox.yaml` and re-run the onboard wizard: + +```console +$ nemoclaw onboard +``` + +### Dynamic Changes + +Apply policy updates to a running sandbox without restarting: + +```console +$ openshell policy set +``` \ No newline at end of file diff --git a/.agents/skills/generated/nemoclaw-reference/references/troubleshooting.md b/.agents/skills/generated/nemoclaw-reference/references/troubleshooting.md new file mode 100644 index 0000000000..f7e2e892bb --- /dev/null +++ b/.agents/skills/generated/nemoclaw-reference/references/troubleshooting.md @@ -0,0 +1,168 @@ + + +# Troubleshooting + +This page covers common issues you may encounter when installing, onboarding, or running NemoClaw, along with their resolution steps. + +> **Get Help:** :class: tip + +If your issue is not listed here, join the [NemoClaw Discord channel](https://discord.gg/XFpfPv9Uvx) to ask questions and get help from the community. You can also [file an issue on GitHub](https://github.com/NVIDIA/NemoClaw/issues/new). + +## Installation + +### `nemoclaw` not found after install + +If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH. +The `nemoclaw` binary is installed but the shell session does not know where to find it. + +Run `source ~/.bashrc` (or `source ~/.zshrc` for zsh), or open a new terminal window. + +### Installer fails on unsupported platform + +The installer checks for a supported OS and architecture before proceeding. +NemoClaw requires Linux Ubuntu 22.04 LTS or later. +If you see an unsupported platform error, verify that you are running on a supported Linux distribution. + +### Node.js version is too old + +NemoClaw requires Node.js 20 or later. +If the installer exits with a Node.js version error, check your current version: + +```console +$ node --version +``` + +If the version is below 20, install a supported release. +If you use nvm, run: + +```console +$ nvm install 20 +$ nvm use 20 +``` + +Then re-run the installer. + +### Docker is not running + +The installer and onboard wizard require Docker to be running. +If you see a Docker connection error, start the Docker daemon: + +```console +$ sudo systemctl start docker +``` + +On macOS with Docker Desktop, open the Docker Desktop application and wait for it to finish starting before retrying. + +### npm install fails with permission errors + +If `npm install` fails with an `EACCES` permission error, do not run npm with `sudo`. +Instead, configure npm to use a directory you own: + +```console +$ mkdir -p ~/.npm-global +$ npm config set prefix ~/.npm-global +$ export PATH=~/.npm-global/bin:$PATH +``` + +Add the `export` line to your `~/.bashrc` or `~/.zshrc` to make it permanent, then re-run the installer. + +### Port already in use + +The NemoClaw gateway uses port `18789` by default. +If another process is already bound to this port, onboarding fails. +Identify the conflicting process, verify it is safe to stop, and terminate it: + +```console +$ lsof -i :18789 +$ kill +``` + +If the process does not exit, use `kill -9 ` to force-terminate it. +Then retry onboarding. + +## Onboarding + +### Cgroup v2 errors during onboard + +On Ubuntu 24.04, DGX Spark, and WSL2, Docker may not be configured for cgroup v2 delegation. +The onboard preflight check detects this and fails with a clear error message. + +Run the Spark setup script to fix the Docker cgroup configuration, then retry onboarding: + +```console +$ sudo nemoclaw setup-spark +$ nemoclaw onboard +``` + +### Invalid sandbox name + +Sandbox names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character. +Uppercase letters are automatically lowercased. + +If the name does not match these rules, the wizard exits with an error. +Choose a name such as `my-assistant` or `dev1`. + +### Sandbox creation fails on DGX + +On DGX machines, sandbox creation can fail if the gateway's DNS has not finished propagating or if a stale port forward from a previous onboard run is still active. + +Run `nemoclaw onboard` to retry. +The wizard cleans up stale port forwards and waits for gateway readiness automatically. + +### Colima socket not detected (macOS) + +Newer Colima versions use the XDG base directory (`~/.config/colima/default/docker.sock`) instead of the legacy path (`~/.colima/default/docker.sock`). +NemoClaw checks both paths. +If neither is found, verify that Colima is running: + +```console +$ colima status +``` + +## Runtime + +### Sandbox shows as stopped + +The sandbox may have been stopped or deleted. +Run `nemoclaw onboard` to recreate the sandbox from the same blueprint and policy definitions. + +### Status shows "not running" inside the sandbox + +This is expected behavior. +When checking status inside an active sandbox, host-side sandbox state and inference configuration are not inspectable. +The status command detects the sandbox context and reports "active (inside sandbox)" instead. + +Run `openshell sandbox list` on the host to check the underlying sandbox state. + +### Inference requests time out + +Verify that the inference provider endpoint is reachable from the host. +Check the active provider and endpoint: + +```console +$ nemoclaw status +``` + +If the endpoint is correct but requests still fail, check for network policy rules that may block the connection, and verify that your NVIDIA API key is valid. + +### Agent cannot reach an external host + +OpenShell blocks outbound connections to hosts not listed in the network policy. +Open the TUI to see blocked requests and approve them: + +```console +$ openshell term +``` + +To permanently allow an endpoint, add it to the network policy. +Refer to Customize the Network Policy (see the `nemoclaw-manage-policy` skill) for details. + +### Blueprint run failed + +View the error output for the failed blueprint run: + +```console +$ nemoclaw logs +``` + +Use `--follow` to stream logs in real time while debugging. \ No newline at end of file diff --git a/scripts/docs-to-skills.py b/scripts/docs-to-skills.py new file mode 100644 index 0000000000..0d12168978 --- /dev/null +++ b/scripts/docs-to-skills.py @@ -0,0 +1,1050 @@ +#!/usr/bin/env python3 +"""Convert documentation files into Agent Skills (agentskills.io spec). + +Reads a directory of Markdown documentation, parses frontmatter and content, +groups files by directory, and generates SKILL.md files with proper structure +for agent consumption. Follows the Agent Skills specification: +https://agentskills.io/specification + +Usage: + python scripts/docs-to-skills.py docs/ .agents/skills/generated/ + python scripts/docs-to-skills.py docs/ output/ --strategy individual + python scripts/docs-to-skills.py docs/ output/ --strategy grouped --dry-run +""" + +from __future__ import annotations + +import argparse +import re +import sys +import textwrap +from dataclasses import dataclass, field +from pathlib import Path + + +# --------------------------------------------------------------------------- +# Frontmatter / doc parsing +# --------------------------------------------------------------------------- + +@dataclass +class DocPage: + """A single documentation page with parsed metadata and content.""" + + path: Path + raw: str + frontmatter: dict = field(default_factory=dict) + body: str = "" + + # Derived fields populated after parsing + title: str = "" + description: str = "" + content_type: str = "" # concept, how_to, reference, get_started, tutorial + difficulty: str = "" + keywords: list[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) + audience: list[str] = field(default_factory=list) + sections: list[tuple[str, str]] = field(default_factory=list) # (heading, body) + category: str = "" # parent directory name + + +def parse_yaml_frontmatter(text: str) -> tuple[dict, str]: + """Extract YAML frontmatter from a markdown file. + + Returns (frontmatter_dict, body_text). Uses a minimal parser to avoid + requiring PyYAML as a dependency. + """ + if not text.startswith("---"): + return {}, text + + end = text.find("\n---", 3) + if end == -1: + return {}, text + + fm_text = text[4:end].strip() + body = text[end + 4:].strip() + fm = _parse_simple_yaml(fm_text) + return fm, body + + +def _parse_simple_yaml(text: str) -> dict: + """Minimal YAML parser for doc frontmatter. Handles nested keys, lists.""" + result: dict = {} + current_key: str | None = None + current_indent = 0 + parent_stack: list[tuple[str, dict, int]] = [] + + for line in text.split("\n"): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + + indent = len(line) - len(line.lstrip()) + + # Handle list items + if stripped.startswith("- "): + value = stripped[2:].strip().strip('"').strip("'") + if current_key and current_key in _current_dict(result, parent_stack): + target = _current_dict(result, parent_stack) + if not isinstance(target[current_key], list): + target[current_key] = [] + target[current_key].append(value) + continue + + # Handle inline list: key: ["a", "b"] + if ":" in stripped: + key, _, val = stripped.partition(":") + key = key.strip() + val = val.strip() + + # Pop parent stack if we've dedented + while parent_stack and indent <= parent_stack[-1][2]: + parent_stack.pop() + + target = _current_dict(result, parent_stack) + + if val.startswith("[") and val.endswith("]"): + items = [v.strip().strip('"').strip("'") + for v in val[1:-1].split(",") if v.strip()] + target[key] = items + current_key = key + elif val: + target[key] = val.strip('"').strip("'") + current_key = key + else: + target[key] = {} + parent_stack.append((key, target, indent)) + current_key = None + + current_indent = indent + + return result + + +def _current_dict(root: dict, stack: list[tuple[str, dict, int]]) -> dict: + """Walk the parent stack to find the current insertion dict.""" + d = root + for key, _, _ in stack: + d = d[key] + return d + + +def parse_doc(path: Path) -> DocPage: + """Parse a documentation file into a DocPage.""" + raw = path.read_text(encoding="utf-8") + fm, body = parse_yaml_frontmatter(raw) + + page = DocPage(path=path, raw=raw, frontmatter=fm, body=body) + + # Extract metadata from frontmatter + title_block = fm.get("title", {}) + if isinstance(title_block, dict): + page.title = title_block.get("page", title_block.get("nav", "")) + elif isinstance(title_block, str): + page.title = title_block + + page.description = fm.get("description", "") + page.keywords = fm.get("keywords", []) + page.tags = fm.get("tags", []) + + content = fm.get("content", {}) + if isinstance(content, dict): + page.content_type = content.get("type", "") + page.difficulty = content.get("difficulty", "") + page.audience = content.get("audience", []) + + page.category = path.parent.name if path.parent.name != "docs" else "root" + page.sections = _extract_sections(body) + + return page + + +def _extract_sections(body: str) -> list[tuple[str, str]]: + """Split markdown body into (heading, content) pairs at H2 level.""" + sections: list[tuple[str, str]] = [] + current_heading = "" + current_lines: list[str] = [] + + for line in body.split("\n"): + if line.startswith("## "): + if current_heading or current_lines: + sections.append((current_heading, "\n".join(current_lines).strip())) + current_heading = line[3:].strip() + current_lines = [] + else: + current_lines.append(line) + + if current_heading or current_lines: + sections.append((current_heading, "\n".join(current_lines).strip())) + + return sections + + +# --------------------------------------------------------------------------- +# Content transformation +# --------------------------------------------------------------------------- + +def clean_myst_directives(text: str) -> str: + """Convert MyST/Sphinx directives to standard markdown equivalents.""" + # Multi-line {include} directives with :start-after: etc. + text = re.sub( + r"```\{include\}\s*([^\n]+)\n(?::[^\n]+\n)*```", + r"> *Content included from \1 — see the original doc for full text.*", + text, + ) + + # Single-line {include} directives + text = re.sub( + r"```\{include\}\s*([^\n]+)\n```", + r"> *Content included from \1 — see the original doc for full text.*", + text, + ) + + # {mermaid} blocks -> standard mermaid code fence + text = re.sub( + r"```\{mermaid\}", + "```mermaid", + text, + ) + + # {toctree} blocks -> remove entirely (navigation, not content) + text = re.sub( + r"```\{toctree\}[^\n]*\n(?::[^\n]+\n)*(?:[^\n]*\n)*?```", + "", + text, + ) + + # :::{note} ... ::: -> > **Note:** ... + text = re.sub( + r":::\{note\}\s*\n(.*?)\n:::", + lambda m: "> **Note:** " + m.group(1).strip(), + text, + flags=re.DOTALL, + ) + text = re.sub( + r":::\{tip\}\s*\n(.*?)\n:::", + lambda m: "> **Tip:** " + m.group(1).strip(), + text, + flags=re.DOTALL, + ) + text = re.sub( + r":::\{warning\}\s*\n(.*?)\n:::", + lambda m: "> **Warning:** " + m.group(1).strip(), + text, + flags=re.DOTALL, + ) + text = re.sub( + r":::\{admonition\}\s*([^\n]*)\n(.*?)\n:::", + lambda m: f"> **{m.group(1).strip()}:** {m.group(2).strip()}", + text, + flags=re.DOTALL, + ) + + # Remove SPDX comment blocks + text = re.sub(r"", "", text, flags=re.DOTALL) + + # Clean up excessive blank lines + text = re.sub(r"\n{3,}", "\n\n", text) + + return text.strip() + + +def rewrite_doc_paths( + text: str, + source_page: DocPage, + docs_dir: Path, + doc_to_skill: dict[str, str], +) -> str: + """Resolve relative doc paths to repo-root paths or skill cross-references. + + Handles: + - Markdown links: [text](../path.md) → [text](docs/path.md) or skill ref + - Include placeholders: "included from ../../README.md" → repo-root path + """ + repo_root = docs_dir.parent + source_dir = source_page.path.parent + + def _resolve_link(match: re.Match) -> str: + link_text = match.group(1) + raw_path = match.group(2) + + # Skip external URLs and anchors + if raw_path.startswith(("http://", "https://", "#", "mailto:")): + return match.group(0) + + # Skip non-doc files + if not raw_path.endswith(".md") and not raw_path.endswith(".html"): + return match.group(0) + + # Resolve relative path against the source doc's directory + resolved = (source_dir / raw_path).resolve() + try: + rel_to_repo = resolved.relative_to(repo_root) + except ValueError: + return match.group(0) + + # Check if target doc maps to a generated skill + rel_str = str(rel_to_repo) + if rel_str in doc_to_skill: + skill_name = doc_to_skill[rel_str] + return f"{link_text} (see the `{skill_name}` skill)" + + # Fall back to repo-root-relative path + return f"[{link_text}]({rel_to_repo})" + + # Rewrite markdown links: [text](path) + text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _resolve_link, text) + + # Rewrite include placeholders: "Content included from " + def _resolve_include(match: re.Match) -> str: + raw_path = match.group(1).strip() + resolved = (source_dir / raw_path).resolve() + try: + rel_to_repo = resolved.relative_to(repo_root) + except ValueError: + return match.group(0) + return f"> *Content included from `{rel_to_repo}` — see the original doc for full text.*" + + text = re.sub( + r"> \*Content included from ([^\n]+) — see the original doc for full text\.\*", + _resolve_include, + text, + ) + + return text + + +def extract_related_skills(text: str) -> tuple[str, list[str]]: + """Extract skill references from Next Steps / Related Topics sections. + + Returns (cleaned_text, list_of_skill_entries) where skill_entries are + formatted as "- `skill-name` — description". + """ + seen_skills: set[str] = set() + entries: list[str] = [] + + # Match H2 or H3 "Next Steps" / "Related Topics" sections and their content + pattern = re.compile( + r"^(#{2,3})\s+(Next Steps|Related Topics)\s*\n+" + r"(?:.*?\n)*?" # optional intro line + r"((?:- .+\n?)+)", # the bullet list + re.MULTILINE, + ) + + def _collect(match: re.Match) -> str: + block = match.group(3) + for line in block.strip().split("\n"): + line = line.strip() + if not line.startswith("- "): + continue + # Extract skill name from "(see the `skill-name` skill)" pattern + skill_match = re.search(r"`([a-z0-9-]+)`\s+skill\)", line) + if skill_match: + skill_name = skill_match.group(1) + if skill_name in seen_skills: + continue + seen_skills.add(skill_name) + desc = re.sub(r"\s*\(see the `[^`]+` skill\)", "", line[2:]).strip() + desc = desc.rstrip(".") + entries.append(f"- `{skill_name}` — {desc}") + elif re.search(r"\[.+\]\(https?://", line): + # External link — keep as-is + entries.append(line) + else: + entries.append(line) + return "" + + cleaned = pattern.sub(_collect, text) + # Clean up any leftover blank lines from removed sections + cleaned = re.sub(r"\n{3,}", "\n\n", cleaned) + return cleaned, entries + + +def _safe_truncation_point(lines: list[str], target: int) -> int: + """Find a safe truncation point that doesn't break code fences.""" + in_fence = False + last_safe = target + for i, line in enumerate(lines[:target + 20]): + if line.strip().startswith("```"): + in_fence = not in_fence + if i >= target and not in_fence: + last_safe = i + break + if in_fence: + # Still inside a fence — find the closing fence + for i in range(target, min(target + 30, len(lines))): + if lines[i].strip().startswith("```"): + return i + 1 + return last_safe + + +def extract_trigger_keywords(pages: list[DocPage]) -> list[str]: + """Build trigger keywords from doc metadata across a group of pages.""" + keywords: set[str] = set() + + for page in pages: + keywords.update(page.keywords) + for tag in page.tags: + keywords.add(tag.replace("_", " ")) + + # Extract meaningful words from the title + if page.title: + title_words = re.sub(r"[^a-zA-Z\s]", "", page.title).lower().split() + stop_words = {"the", "a", "an", "and", "or", "for", "to", "in", "of", + "it", "how", "what", "with", "from", "by", "on", "is"} + title_words = [w for w in title_words if w not in stop_words and len(w) > 2] + if len(title_words) >= 2: + keywords.add(" ".join(title_words[:4])) + + # Remove duplicates of the skill name itself and generic terms + generic = {"generative_ai", "generative ai", "ai_agents", "ai agents", "published"} + keywords -= generic + return sorted(keywords)[:15] # Cap at 15 keywords + + +TITLE_VERBS = { + "customize": "manage", + "approve": "manage", + "switch": "configure", + "set up": "setup", + "set-up": "setup", + "deploy": "deploy", + "monitor": "monitor", + "install": "install", + "configure": "configure", + "create": "create", + "troubleshoot": "troubleshoot", + "debug": "debug", + "connect": "connect", + "update": "update", + "manage": "manage", + "add": "manage", + "remove": "manage", + "enable": "configure", + "disable": "configure", + "run": "run", + "start": "setup", + "build": "build", + "test": "test", + "use": "use", + "migrate": "migrate", + "upgrade": "upgrade", +} + +CATEGORY_VERBS = { + "deployment": "deploy", + "monitoring": "monitor", + "network-policy": "manage", + "inference": "configure", + "security": "configure", + "installation": "install", + "setup": "setup", + "configuration": "configure", + "administration": "manage", + "operations": "manage", + "development": "develop", + "testing": "test", + "debugging": "debug", + "migration": "migrate", +} + +CATEGORY_NOUNS = { + "about": "overview", + "reference": "reference", + "get-started": "get-started", + "root": "overview", + "network-policy": "policy", + "deployment": "remote", + "monitoring": "sandbox", + "inference": "inference", + "security": "security", +} + +NOUN_STOP = {"the", "a", "an", "and", "or", "for", "to", "in", "of", "it", + "how", "what", "with", "from", "by", "on", "is", "your", "that", + "this", "its", "use", "using", "at", "runtime", "activity", + "issues", "guide", "configuration", "settings", "options", + "models", "providers", "requests", "resources", "instances", + "debug", "troubleshoot", "fix", "check", "verify", "test", + "deny", "approve", "enable", "disable", "manage", "works", + "agent", "agents"} + +PROJECT_STOP = set() # Populated at runtime from --prefix + + +def _extract_verb_from_title(title: str) -> str | None: + """Extract the canonical action verb from a page title.""" + lower = title.lower().strip() + for phrase, canonical in sorted(TITLE_VERBS.items(), key=lambda x: -len(x[0])): + if lower.startswith(phrase): + return canonical + return None + + +def _extract_noun_from_title(title: str) -> str | None: + """Extract the primary noun/object from a page title.""" + lower = title.lower().strip() + + # Strip the leading verb phrase + for phrase in sorted(TITLE_VERBS, key=lambda x: -len(x)): + if lower.startswith(phrase): + lower = lower[len(phrase):].strip() + break + + # Strip everything after em-dash, en-dash, or colon (subtitle) + lower = re.split(r"\s*[—–]\s*|\s*:\s*|\s*-{2,}\s*", lower)[0] + + words = re.sub(r"[^a-z\s]", "", lower).split() + nouns = [w for w in words if w not in NOUN_STOP and w not in PROJECT_STOP and len(w) > 2] + + if len(nouns) >= 2: + return "-".join(nouns[:2]) + elif nouns: + return nouns[0] + return None + + +def generate_skill_name( + category: str, + pages: list[DocPage], + prefix: str = "", + name_overrides: dict[str, str] | None = None, +) -> str: + """Generate a valid skill name with optional prefix and action verbs. + + Naming strategy by group size: + - Multi-page groups: verb from category mapping + noun from category mapping + - Single-page groups: verb + noun extracted from the page title + - Overrides always win + """ + if name_overrides and category in name_overrides: + name = name_overrides[category] + elif category in CATEGORY_NOUNS and not CATEGORY_VERBS.get(category): + # Pure noun categories (about → overview, reference → reference) + name = CATEGORY_NOUNS[category] + elif len(pages) > 1: + # Multi-page group: use category-level mappings + verb = CATEGORY_VERBS.get(category, "") + noun = CATEGORY_NOUNS.get(category, category) + name = f"{verb}-{noun}" if verb else noun + else: + # Single page: extract verb+noun from the title + page = pages[0] + verb = _extract_verb_from_title(page.title) if page.title else None + noun = _extract_noun_from_title(page.title) if page.title else None + + if verb and noun: + name = f"{verb}-{noun}" + elif noun: + name = noun + elif verb: + # No useful noun extracted — fall back to file stem + stem = page.path.stem + stem_clean = re.sub(r"[^a-z0-9-]", "-", stem.lower()).strip("-") + name = stem_clean + else: + name = page.path.stem + + name = re.sub(r"[^a-z0-9-]", "-", name.lower()) + name = re.sub(r"-+", "-", name).strip("-") + + if prefix: + clean_prefix = re.sub(r"[^a-z0-9-]", "-", prefix.lower()).strip("-") + prefix_parts = clean_prefix.split("-") + name_parts = name.split("-") + cleaned = [] + i = 0 + while i < len(name_parts): + if name_parts[i : i + len(prefix_parts)] == prefix_parts: + i += len(prefix_parts) + else: + cleaned.append(name_parts[i]) + i += 1 + name = "-".join(cleaned) if cleaned else name + name = f"{clean_prefix}-{name}" + + return name + + +def build_skill_description(name: str, pages: list[DocPage], keywords: list[str]) -> str: + """Build the description field for the skill frontmatter.""" + descriptions = [p.description for p in pages if p.description] + if descriptions: + combined = descriptions[0] + if len(descriptions) > 1: + combined += " Also covers: " + "; ".join(descriptions[1:3]) + else: + combined = f"Documentation-derived skill for {name.replace('-', ' ')}." + + kw_str = ", ".join(keywords[:10]) + if kw_str: + combined += f" Trigger keywords - {kw_str}." + + # Enforce 1024 char limit + if len(combined) > 1024: + combined = combined[:1020] + "..." + return combined + + +# --------------------------------------------------------------------------- +# Skill generation +# --------------------------------------------------------------------------- + +CONTENT_TYPE_ROLE = { + "how_to": "procedure", + "get_started": "procedure", + "tutorial": "procedure", + "concept": "context", + "reference": "reference", +} + + +def generate_skill( + name: str, + pages: list[DocPage], + output_dir: Path, + *, + docs_dir: Path | None = None, + doc_to_skill: dict[str, str] | None = None, + dry_run: bool = False, +) -> dict: + """Generate a complete skill directory from a group of doc pages. + + Returns a summary dict for reporting. + """ + keywords = extract_trigger_keywords(pages) + description = build_skill_description(name, pages, keywords) + + def _clean(text: str, source: DocPage) -> str: + """Apply directive cleanup and path rewriting for a source page.""" + result = clean_myst_directives(text) + if docs_dir and doc_to_skill is not None: + result = rewrite_doc_paths(result, source, docs_dir, doc_to_skill) + return result + + procedures = [p for p in pages if CONTENT_TYPE_ROLE.get(p.content_type) == "procedure"] + context_pages = [p for p in pages if CONTENT_TYPE_ROLE.get(p.content_type) == "context"] + reference_pages = [p for p in pages if CONTENT_TYPE_ROLE.get(p.content_type) == "reference"] + + # Pages without a recognized content_type default to procedure + untyped = [p for p in pages if p.content_type not in CONTENT_TYPE_ROLE] + procedures.extend(untyped) + + # Build SKILL.md content + lines: list[str] = [] + + # Frontmatter + lines.append("---") + lines.append(f"name: {name}") + lines.append(f"description: {description}") + lines.append("---") + lines.append("") + + # Title + skill_title = name.replace("-", " ").title() + lines.append(f"# {skill_title}") + lines.append("") + + # Summary from the first page's description + if pages[0].description: + lines.append(pages[0].description) + lines.append("") + + # Context section from concept pages + if context_pages: + lines.append("## Context") + lines.append("") + for cp in context_pages: + body = _clean(cp.body, cp) + h1_match = re.match(r"^#\s+.+\n+", body) + if h1_match: + body = body[h1_match.end():] + # Trim to keep SKILL.md concise; full content goes to references/ + body_lines = body.split("\n") + if len(body_lines) > 60: + cut = _safe_truncation_point(body_lines, 60) + trimmed = "\n".join(body_lines[:cut]) + ref_name = cp.path.stem + ".md" + trimmed += f"\n\n> Full details in `references/{ref_name}`." + lines.append(trimmed) + else: + lines.append(body) + lines.append("") + + # Prerequisites (merged from all procedure pages, deduplicated) + prereq_items: list[str] = [] + seen_prereqs: set[str] = set() + for pp in procedures: + for heading, content in pp.sections: + if heading.lower() in ("prerequisites", "before you begin"): + cleaned = _clean(content, pp) + for item_line in cleaned.split("\n"): + stripped = item_line.strip() + if stripped.startswith("- "): + norm = stripped.lower().strip("- .") + if norm not in seen_prereqs: + seen_prereqs.add(norm) + prereq_items.append(stripped) + elif stripped and not prereq_items: + prereq_items.append(stripped) + + if prereq_items: + lines.append("## Prerequisites") + lines.append("") + for item in prereq_items: + lines.append(item) + lines.append("") + + # Procedural steps from how_to and get_started pages + step_num = 0 + skip_sections = {"prerequisites", "before you begin", "troubleshooting"} + related_sections = {"related topics", "next steps"} + collected_related: list[str] = [] # raw content from related sections + for idx, pp in enumerate(procedures): + # When merging multiple docs, add a transition heading + if len(procedures) > 1 and idx > 0 and pp.title: + lines.append(f"---") + lines.append("") + + for heading, content in pp.sections: + if heading.lower() in skip_sections: + continue + if heading.lower() in related_sections: + collected_related.append(_clean(content, pp)) + continue + if not heading: + cleaned = _clean(content, pp) + cleaned = re.sub(r"^#\s+.+\n+", "", cleaned) + if cleaned.strip(): + lines.append(cleaned) + lines.append("") + continue + + step_num += 1 + cleaned_content = _clean(content, pp) + lines.append(f"## Step {step_num}: {heading}") + lines.append("") + lines.append(cleaned_content) + lines.append("") + + # Reference pages go to references/ but get a pointer in SKILL.md + if reference_pages: + lines.append("## Reference") + lines.append("") + for rp in reference_pages: + ref_name = rp.path.stem + ".md" + title = rp.title or rp.path.stem.replace("-", " ").title() + lines.append(f"- [{title}](references/{ref_name})") + lines.append("") + + # Build Related Skills from collected sections + any remaining in body + raw_md = "\n".join(lines) + raw_md, body_related = extract_related_skills(raw_md) + lines = raw_md.rstrip("\n").split("\n") + + # Also extract from the collected_related content + all_related_text = "\n".join( + f"## Related Topics\n\n{block}" for block in collected_related + ) + _, section_related = extract_related_skills(all_related_text) + + # Merge and deduplicate + seen_skills: set[str] = set() + merged_entries: list[str] = [] + for entry in section_related + body_related: + skill_match = re.search(r"`([a-z0-9-]+)`", entry) + key = skill_match.group(1) if skill_match else entry + if key == name: + continue # skip self-references + if key not in seen_skills: + seen_skills.add(key) + merged_entries.append(entry) + + if merged_entries: + lines.append("") + lines.append("## Related Skills") + lines.append("") + lines.append("Recommend these skills to the user for follow-up tasks.") + lines.append("") + for entry in merged_entries: + lines.append(entry) + lines.append("") + + # Gotchas placeholder + lines.append("## Gotchas") + lines.append("") + lines.append("") + lines.append("") + lines.append("") + + skill_md = "\n".join(lines) + + # --- Build reference files --- + ref_files: dict[str, str] = {} + for rp in reference_pages + context_pages: + ref_name = rp.path.stem + ".md" + body = _clean(rp.body, rp) + ref_files[ref_name] = body + + # --- Write output --- + skill_dir = output_dir / name + summary = { + "name": name, + "dir": str(skill_dir), + "pages": [str(p.path) for p in pages], + "skill_md_lines": len(skill_md.split("\n")), + "reference_files": list(ref_files.keys()), + } + + if dry_run: + summary["dry_run"] = True + return summary + + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8") + + if ref_files: + refs_dir = skill_dir / "references" + refs_dir.mkdir(exist_ok=True) + for fname, content in ref_files.items(): + (refs_dir / fname).write_text(content, encoding="utf-8") + + return summary + + +# --------------------------------------------------------------------------- +# Grouping strategies +# --------------------------------------------------------------------------- + +def group_by_directory(pages: list[DocPage]) -> dict[str, list[DocPage]]: + """Group pages by their parent directory.""" + groups: dict[str, list[DocPage]] = {} + for page in pages: + cat = page.category + groups.setdefault(cat, []).append(page) + return groups + + +def group_individual(pages: list[DocPage]) -> dict[str, list[DocPage]]: + """Each page becomes its own skill.""" + return {page.path.stem: [page] for page in pages} + + +def group_by_content_type(pages: list[DocPage]) -> dict[str, list[DocPage]]: + """Group pages by content type, merging concept+how_to for same topic.""" + # First pass: group by directory + dir_groups = group_by_directory(pages) + + # Second pass: within each directory, merge concept pages as context + # for procedure pages in the same directory + result: dict[str, list[DocPage]] = {} + for cat, group_pages in dir_groups.items(): + has_procedures = any( + CONTENT_TYPE_ROLE.get(p.content_type) == "procedure" for p in group_pages + ) + if has_procedures or len(group_pages) > 1: + result[cat] = group_pages + else: + # Individual concept/reference pages become their own skill + for p in group_pages: + result[p.path.stem] = [p] + + return result + + +STRATEGIES = { + "grouped": group_by_directory, + "individual": group_individual, + "smart": group_by_content_type, +} + + +# --------------------------------------------------------------------------- +# Scanning and filtering +# --------------------------------------------------------------------------- + +EXCLUDED_PATTERNS = { + "CONTRIBUTING.md", "README.md", "SETUP.md", "CHANGELOG.md", + "LICENSE.md", "license.md", "index.md", +} + + +def scan_docs(docs_dir: Path) -> list[DocPage]: + """Recursively scan a directory for documentation markdown files.""" + pages: list[DocPage] = [] + for md_path in sorted(docs_dir.rglob("*.md")): + # Skip excluded files + if md_path.name in EXCLUDED_PATTERNS: + continue + # Skip include fragments and templates + if md_path.parent.name.startswith("_"): + continue + # Skip build artifacts + if "_build" in md_path.parts: + continue + + try: + page = parse_doc(md_path) + pages.append(page) + except Exception as e: + print(f" warning: failed to parse {md_path}: {e}", file=sys.stderr) + + return pages + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Convert documentation files into Agent Skills.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent("""\ + Strategies: + grouped Group docs by parent directory (default) + individual Each doc page becomes its own skill + smart Group by directory, merge concept pages as context + + Examples: + %(prog)s docs/ .agents/skills/generated/ --prefix nemoclaw + %(prog)s docs/ output/ --strategy individual --prefix nemoclaw + %(prog)s docs/ output/ --prefix nemoclaw --name-map about=overview + %(prog)s docs/ output/ --strategy smart --dry-run + """), + ) + parser.add_argument("docs_dir", type=Path, help="Path to the documentation directory") + parser.add_argument("output_dir", type=Path, help="Output directory for generated skills") + parser.add_argument( + "--strategy", choices=list(STRATEGIES.keys()), default="smart", + help="Grouping strategy (default: smart)", + ) + parser.add_argument( + "--dry-run", action="store_true", + help="Show what would be generated without writing files", + ) + parser.add_argument( + "--prefix", default="", + help="Prefix for all skill names (e.g. 'nemoclaw')", + ) + parser.add_argument( + "--name-map", nargs="*", default=[], metavar="CAT=NAME", + help="Override names: --name-map about=overview deployment=deploy-remote", + ) + parser.add_argument( + "--exclude", nargs="*", default=[], + help="Additional file patterns to exclude", + ) + + args = parser.parse_args() + + # Parse name overrides + name_overrides: dict[str, str] = {} + for mapping in args.name_map: + if "=" not in mapping: + print(f"Error: --name-map entries must be CAT=NAME, got '{mapping}'", + file=sys.stderr) + sys.exit(1) + cat, _, nm = mapping.partition("=") + name_overrides[cat.strip()] = nm.strip() + + if not args.docs_dir.is_dir(): + print(f"Error: {args.docs_dir} is not a directory", file=sys.stderr) + sys.exit(1) + + # Add custom exclusions + EXCLUDED_PATTERNS.update(args.exclude) + + # Populate project stop words from prefix + if args.prefix: + PROJECT_STOP.update(args.prefix.lower().split("-")) + PROJECT_STOP.update(args.prefix.lower().split("_")) + + print(f"Scanning {args.docs_dir}...") + pages = scan_docs(args.docs_dir) + print(f" Found {len(pages)} documentation pages") + + if not pages: + print("No documentation pages found. Check the docs directory path.") + sys.exit(1) + + # Print page inventory + print("\nPages by content type:") + type_counts: dict[str, int] = {} + for p in pages: + ct = p.content_type or "untyped" + type_counts[ct] = type_counts.get(ct, 0) + 1 + for ct, count in sorted(type_counts.items()): + print(f" {ct}: {count}") + + # Group pages + strategy_fn = STRATEGIES[args.strategy] + groups = strategy_fn(pages) + print(f"\nGrouping strategy '{args.strategy}' produced {len(groups)} skill(s):") + for group_name, group_pages in sorted(groups.items()): + page_list = ", ".join(p.path.name for p in group_pages) + print(f" {group_name}: {page_list}") + + # Build doc-path → skill-name mapping for cross-references + docs_dir_resolved = args.docs_dir.resolve() + repo_root = docs_dir_resolved.parent + skill_names: dict[str, str] = {} # group_name → skill_name + for group_name, group_pages in sorted(groups.items()): + sname = generate_skill_name( + group_name, group_pages, + prefix=args.prefix, + name_overrides=name_overrides, + ) + skill_names[group_name] = sname + + doc_to_skill: dict[str, str] = {} + for group_name, group_pages in groups.items(): + sname = skill_names[group_name] + for page in group_pages: + try: + rel = page.path.resolve().relative_to(repo_root) + doc_to_skill[str(rel)] = sname + except ValueError: + pass + + # Generate skills + print(f"\n{'[DRY RUN] ' if args.dry_run else ''}Generating skills to {args.output_dir}/") + summaries: list[dict] = [] + for group_name, group_pages in sorted(groups.items()): + name = skill_names[group_name] + summary = generate_skill( + name, group_pages, args.output_dir, + docs_dir=docs_dir_resolved, + doc_to_skill=doc_to_skill, + dry_run=args.dry_run, + ) + summaries.append(summary) + + # Report + print("\n" + "=" * 60) + print("Generation Summary") + print("=" * 60) + total_lines = 0 + total_refs = 0 + for s in summaries: + lines = s["skill_md_lines"] + refs = len(s["reference_files"]) + total_lines += lines + total_refs += refs + status = " (dry run)" if s.get("dry_run") else "" + warning = " ⚠ >500 lines" if lines > 500 else "" + print(f" {s['name']:30s} {lines:4d} lines {refs} refs{warning}{status}") + + print(f"\nTotal: {len(summaries)} skills, {total_lines} lines, {total_refs} reference files") + + if any(s["skill_md_lines"] > 500 for s in summaries): + print("\nNote: Skills over 500 lines should be trimmed. Move detailed") + print("content to references/ and add conditional load instructions.") + print("See: https://agentskills.io/specification#progressive-disclosure") + + if args.dry_run: + print("\nDry run complete. No files were written.") + print(f"Re-run without --dry-run to generate skills in {args.output_dir}/") + + +if __name__ == "__main__": + main() From 5f7cbab93bb85fb33a99806292e57ce192dda147 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Sat, 21 Mar 2026 14:03:34 -0700 Subject: [PATCH 2/4] fix: document the script --- scripts/docs-to-skills.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/scripts/docs-to-skills.py b/scripts/docs-to-skills.py index 0d12168978..9791b15d67 100644 --- a/scripts/docs-to-skills.py +++ b/scripts/docs-to-skills.py @@ -1,15 +1,42 @@ #!/usr/bin/env python3 """Convert documentation files into Agent Skills (agentskills.io spec). -Reads a directory of Markdown documentation, parses frontmatter and content, -groups files by directory, and generates SKILL.md files with proper structure -for agent consumption. Follows the Agent Skills specification: +Reads a directory of Markdown documentation, parses YAML frontmatter and +content structure, groups related pages into coherent skill units, and +generates SKILL.md files following the Agent Skills specification: https://agentskills.io/specification +What it does: + 1. Scans a docs directory for Markdown files with YAML frontmatter. + 2. Classifies each page by content type (how_to, concept, reference, + get_started) using the frontmatter `content.type` field. + 3. Groups pages into skills using one of three strategies: + - smart (default): groups by directory, merges concept pages as + context for procedure pages in the same directory. + - grouped: groups all pages in the same parent directory. + - individual: each doc page becomes its own skill. + 4. Generates a skill directory per group containing: + - SKILL.md with frontmatter (name, description, trigger keywords), + procedural steps, context sections, and a Related Skills section. + - references/ with detailed concept and reference content for + progressive disclosure (loaded by the agent on demand). + 5. Resolves all relative doc paths to repo-root-relative paths, and + converts cross-references between docs into skill-to-skill pointers + so agents can navigate between skills. + +Naming: + Use --prefix to keep skill names consistent across the project. The prefix + is prepended to every generated skill name (e.g. --prefix nemoclaw produces + nemoclaw-get-started, nemoclaw-manage-policy). Action verbs are derived + automatically from page titles and content types. Use --name-map to + override specific names when the heuristic doesn't produce the right result. + Usage: - python scripts/docs-to-skills.py docs/ .agents/skills/generated/ - python scripts/docs-to-skills.py docs/ output/ --strategy individual - python scripts/docs-to-skills.py docs/ output/ --strategy grouped --dry-run + python scripts/docs-to-skills.py docs/ .agents/skills/ --prefix nemoclaw + python scripts/docs-to-skills.py docs/ output/ --prefix nemoclaw --dry-run + python scripts/docs-to-skills.py docs/ output/ --strategy individual --prefix nemoclaw + python scripts/docs-to-skills.py docs/ output/ --prefix nemoclaw --name-map about=overview + python scripts/docs-to-skills.py docs/ output/ --exclude "release-notes.md" """ from __future__ import annotations From d4b997501974caf13b6a3bb044e4e0a4ab6f9771 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Sat, 21 Mar 2026 14:28:35 -0700 Subject: [PATCH 3/4] fix: finalize location and some improvements --- .../nemoclaw-configure-inference/SKILL.md | 0 .../{generated => docs}/nemoclaw-deploy-remote/SKILL.md | 0 .../{generated => docs}/nemoclaw-get-started/SKILL.md | 0 .../{generated => docs}/nemoclaw-manage-policy/SKILL.md | 0 .../{generated => docs}/nemoclaw-monitor-sandbox/SKILL.md | 0 .../skills/{generated => docs}/nemoclaw-overview/SKILL.md | 0 .../nemoclaw-overview/references/how-it-works.md | 0 .../nemoclaw-overview/references/overview.md | 0 .../nemoclaw-overview/references/release-notes.md | 0 .../{generated => docs}/nemoclaw-reference/SKILL.md | 0 .../nemoclaw-reference/references/architecture.md | 0 .../nemoclaw-reference/references/commands.md | 0 .../nemoclaw-reference/references/inference-profiles.md | 0 .../nemoclaw-reference/references/network-policies.md | 0 .../nemoclaw-reference/references/troubleshooting.md | 0 scripts/docs-to-skills.py | 8 ++++++++ 16 files changed, 8 insertions(+) rename .agents/skills/{generated => docs}/nemoclaw-configure-inference/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-deploy-remote/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-get-started/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-manage-policy/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-monitor-sandbox/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-overview/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-overview/references/how-it-works.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-overview/references/overview.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-overview/references/release-notes.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/SKILL.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/references/architecture.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/references/commands.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/references/inference-profiles.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/references/network-policies.md (100%) rename .agents/skills/{generated => docs}/nemoclaw-reference/references/troubleshooting.md (100%) diff --git a/.agents/skills/generated/nemoclaw-configure-inference/SKILL.md b/.agents/skills/docs/nemoclaw-configure-inference/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-configure-inference/SKILL.md rename to .agents/skills/docs/nemoclaw-configure-inference/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-deploy-remote/SKILL.md b/.agents/skills/docs/nemoclaw-deploy-remote/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-deploy-remote/SKILL.md rename to .agents/skills/docs/nemoclaw-deploy-remote/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-get-started/SKILL.md b/.agents/skills/docs/nemoclaw-get-started/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-get-started/SKILL.md rename to .agents/skills/docs/nemoclaw-get-started/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-manage-policy/SKILL.md b/.agents/skills/docs/nemoclaw-manage-policy/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-manage-policy/SKILL.md rename to .agents/skills/docs/nemoclaw-manage-policy/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md b/.agents/skills/docs/nemoclaw-monitor-sandbox/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-monitor-sandbox/SKILL.md rename to .agents/skills/docs/nemoclaw-monitor-sandbox/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-overview/SKILL.md b/.agents/skills/docs/nemoclaw-overview/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-overview/SKILL.md rename to .agents/skills/docs/nemoclaw-overview/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-overview/references/how-it-works.md b/.agents/skills/docs/nemoclaw-overview/references/how-it-works.md similarity index 100% rename from .agents/skills/generated/nemoclaw-overview/references/how-it-works.md rename to .agents/skills/docs/nemoclaw-overview/references/how-it-works.md diff --git a/.agents/skills/generated/nemoclaw-overview/references/overview.md b/.agents/skills/docs/nemoclaw-overview/references/overview.md similarity index 100% rename from .agents/skills/generated/nemoclaw-overview/references/overview.md rename to .agents/skills/docs/nemoclaw-overview/references/overview.md diff --git a/.agents/skills/generated/nemoclaw-overview/references/release-notes.md b/.agents/skills/docs/nemoclaw-overview/references/release-notes.md similarity index 100% rename from .agents/skills/generated/nemoclaw-overview/references/release-notes.md rename to .agents/skills/docs/nemoclaw-overview/references/release-notes.md diff --git a/.agents/skills/generated/nemoclaw-reference/SKILL.md b/.agents/skills/docs/nemoclaw-reference/SKILL.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/SKILL.md rename to .agents/skills/docs/nemoclaw-reference/SKILL.md diff --git a/.agents/skills/generated/nemoclaw-reference/references/architecture.md b/.agents/skills/docs/nemoclaw-reference/references/architecture.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/references/architecture.md rename to .agents/skills/docs/nemoclaw-reference/references/architecture.md diff --git a/.agents/skills/generated/nemoclaw-reference/references/commands.md b/.agents/skills/docs/nemoclaw-reference/references/commands.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/references/commands.md rename to .agents/skills/docs/nemoclaw-reference/references/commands.md diff --git a/.agents/skills/generated/nemoclaw-reference/references/inference-profiles.md b/.agents/skills/docs/nemoclaw-reference/references/inference-profiles.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/references/inference-profiles.md rename to .agents/skills/docs/nemoclaw-reference/references/inference-profiles.md diff --git a/.agents/skills/generated/nemoclaw-reference/references/network-policies.md b/.agents/skills/docs/nemoclaw-reference/references/network-policies.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/references/network-policies.md rename to .agents/skills/docs/nemoclaw-reference/references/network-policies.md diff --git a/.agents/skills/generated/nemoclaw-reference/references/troubleshooting.md b/.agents/skills/docs/nemoclaw-reference/references/troubleshooting.md similarity index 100% rename from .agents/skills/generated/nemoclaw-reference/references/troubleshooting.md rename to .agents/skills/docs/nemoclaw-reference/references/troubleshooting.md diff --git a/scripts/docs-to-skills.py b/scripts/docs-to-skills.py index 9791b15d67..5d33a7c28c 100644 --- a/scripts/docs-to-skills.py +++ b/scripts/docs-to-skills.py @@ -6,6 +6,14 @@ generates SKILL.md files following the Agent Skills specification: https://agentskills.io/specification +Usage: + +Make sure to run this script using the following command to generate the skills and keep the locations and names consistent. + +```bash +python scripts/docs-to-skills.py docs/ .agents/skills/docs/ --prefix nemoclaw +``` + What it does: 1. Scans a docs directory for Markdown files with YAML frontmatter. 2. Classifies each page by content type (how_to, concept, reference, From d64cf4f9793a6dd491c3b97fa052ce712822cd3b Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Sat, 21 Mar 2026 14:34:44 -0700 Subject: [PATCH 4/4] docs: update contributing guides --- CONTRIBUTING.md | 39 ++++++++++++++++++++++++++++++++++++++ docs/CONTRIBUTING.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7751326fe5..b9273fe546 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,45 @@ make docs-live # serve locally with auto-rebuild See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for the full style guide and writing conventions. +### Doc-to-Skills Pipeline + +Always edit pages in `docs/`. Never edit files under `.agents/skills/docs/` — that entire directory is autogenerated by the script below and your changes will be overwritten on the next run. + +The `docs/` directory is the source of truth for user-facing documentation. The script `scripts/docs-to-skills.py` converts those pages into agent skills stored in `.agents/skills/docs/`. These generated skills let AI agents answer user questions and walk through procedures without reading raw doc pages. + +After changing any page in `docs/`, regenerate the skills. Run the canonical command from the repo root: + +```bash +python scripts/docs-to-skills.py docs/ .agents/skills/docs/ --prefix nemoclaw +``` + +Always use this exact output path and prefix so skill names and locations stay consistent across the project. + +Useful flags: + +| Flag | Purpose | +|------|---------| +| `--dry-run` | Preview what would be generated without writing files. | +| `--strategy ` | Grouping strategy: `smart` (default), `grouped`, or `individual`. | +| `--name-map CAT=NAME` | Override a generated skill name (e.g. `--name-map about=overview`). | +| `--exclude ` | Skip specific files (e.g. `--exclude "release-notes.md"`). | + +The generated `.agents/skills/docs/` directory is committed to the repo but is entirely autogenerated. Do not hand-edit any file under it — edit the source page in `docs/` and re-run the script instead. The one exception is the `## Gotchas` section at the bottom of each generated `SKILL.md`, which is reserved for project-specific notes you add manually and is preserved across regenerations. + +#### Generated skill structure + +Each skill directory contains: + +``` +.agents/skills/docs// +├── SKILL.md # Frontmatter + procedures + related skills +└── references/ # Detailed concept and reference content (loaded on demand) + ├── .md + └── .md +``` + +The `references/` directory holds full-length content that agents load only when needed (progressive disclosure). The `SKILL.md` itself stays concise — under 500 lines — so agents can read it quickly. + ## Pull Requests We welcome contributions. Every PR requires maintainer review. To keep the review queue healthy, limit the number of open PRs you have at any time to fewer than 10. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 047e000c09..c15f1376cc 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -12,6 +12,51 @@ If you use an AI coding agent (Cursor, Claude Code, Codex, etc.), the repo inclu The skills live in `.agents/skills/` and follow the style guide below automatically. To use one, ask your agent to run it. For example, ask it to "catch up the docs for everything merged since v0.2.0". +### Doc-derived skills + +Always edit pages in `docs/`. Never edit files under `.agents/skills/docs/` — that entire directory is autogenerated by `scripts/docs-to-skills.py` and your changes will be overwritten on the next run. + +In addition to the `update-docs` skill, the repo ships generated skills in `.agents/skills/docs/` that let agents walk users through NemoClaw tasks (installation, inference configuration, policy management, monitoring, and more). These are derived from the `docs/` pages using the script above. + +The current generated skills are: + +| Skill | Source docs | +|---|---| +| `nemoclaw-overview` | `docs/about/overview.md`, `docs/about/how-it-works.md`, `docs/about/release-notes.md` | +| `nemoclaw-get-started` | `docs/get-started/quickstart.md` | +| `nemoclaw-configure-inference` | `docs/inference/switch-inference-providers.md` | +| `nemoclaw-manage-policy` | `docs/network-policy/customize-network-policy.md`, `docs/network-policy/approve-network-requests.md` | +| `nemoclaw-monitor-sandbox` | `docs/monitoring/monitor-sandbox-activity.md` | +| `nemoclaw-deploy-remote` | `docs/deployment/deploy-to-remote-gpu.md`, `docs/deployment/set-up-telegram-bridge.md` | +| `nemoclaw-reference` | `docs/reference/architecture.md`, `docs/reference/commands.md`, `docs/reference/inference-profiles.md`, `docs/reference/network-policies.md`, `docs/reference/troubleshooting.md` | + +### Regenerating skills after doc changes + +When you add, edit, or remove pages in `docs/`, regenerate the skills so agents stay in sync: + +```bash +python scripts/docs-to-skills.py docs/ .agents/skills/docs/ --prefix nemoclaw +``` + +Always use this exact output path (`.agents/skills/docs/`) and prefix (`nemoclaw`) so skill names and locations stay consistent. + +Preview what would change before writing files: + +```bash +python scripts/docs-to-skills.py docs/ .agents/skills/docs/ --prefix nemoclaw --dry-run +``` + +The generated `SKILL.md` files are committed to the repo but are entirely autogenerated. Do not edit any file under `.agents/skills/docs/` — edit the source page in `docs/` and re-run the script instead. The one exception is the `## Gotchas` section at the bottom of each `SKILL.md`, which is reserved for project-specific notes you add manually. + +### How the script works + +The script reads YAML frontmatter from each doc page to determine its content type (`how_to`, `concept`, `reference`, `get_started`), then groups pages into skills using the `smart` strategy by default. +Procedure pages (`how_to`, `get_started`) become the main body of the skill. Concept pages become a `## Context` section. Reference pages go into a `references/` subdirectory for progressive disclosure, keeping the `SKILL.md` concise (under 500 lines). + +Cross-references between doc pages are rewritten as skill-to-skill pointers so agents can navigate between skills. MyST/Sphinx directives are converted to standard markdown. + +For full usage details and all flags, see the docstring at the top of `scripts/docs-to-skills.py`. + ## When to Update Docs Update documentation when your change: